github.com/diamondburned/arikawa@v1.3.14/api/send.go (about)

     1  package api
     2  
     3  import (
     4  	"io"
     5  	"mime/multipart"
     6  	"strconv"
     7  
     8  	"github.com/pkg/errors"
     9  
    10  	"github.com/diamondburned/arikawa/discord"
    11  	"github.com/diamondburned/arikawa/utils/httputil"
    12  	"github.com/diamondburned/arikawa/utils/json"
    13  )
    14  
    15  const AttachmentSpoilerPrefix = "SPOILER_"
    16  
    17  // AllowedMentions is a whitelist of mentions for a message.
    18  // https://discordapp.com/developers/docs/resources/channel#allowed-mentions-object
    19  //
    20  // Whitelists
    21  //
    22  // Roles and Users are slices that act as whitelists for IDs that are allowed to
    23  // be mentioned. For example, if only 1 ID is provided in Users, then only that
    24  // ID will be parsed in the message. No other IDs will be. The same example also
    25  // applies for roles.
    26  //
    27  // If Parse is an empty slice and both Users and Roles are empty slices, then no
    28  // mentions will be parsed.
    29  //
    30  // Constraints
    31  //
    32  // If the Users slice is not empty, then Parse must not have AllowUserMention.
    33  // Likewise, if the Roles slice is not empty, then Parse must not have
    34  // AllowRoleMention. This is because everything provided in Parse will make
    35  // Discord parse it completely, meaning they would be mutually exclusive with
    36  // whitelist slices, Roles and Users.
    37  type AllowedMentions struct {
    38  	// Parse is an array of allowed mention types to parse from the content.
    39  	Parse []AllowedMentionType `json:"parse"`
    40  	// Roles is an array of role_ids to mention (Max size of 100).
    41  	Roles []discord.RoleID `json:"roles,omitempty"`
    42  	// Users is an array of user_ids to mention (Max size of 100).
    43  	Users []discord.UserID `json:"users,omitempty"`
    44  }
    45  
    46  // AllowedMentionType is a constant that tells Discord what is allowed to parse
    47  // from a message content. This can help prevent things such as an unintentional
    48  // @everyone mention.
    49  type AllowedMentionType string
    50  
    51  const (
    52  	// AllowRoleMention makes Discord parse roles in the content.
    53  	AllowRoleMention AllowedMentionType = "roles"
    54  	// AllowUserMention makes Discord parse user mentions in the content.
    55  	AllowUserMention AllowedMentionType = "users"
    56  	// AllowEveryoneMention makes Discord parse @everyone mentions.
    57  	AllowEveryoneMention AllowedMentionType = "everyone"
    58  )
    59  
    60  // Verify checks the AllowedMentions against constraints mentioned in
    61  // AllowedMentions' documentation. This will be called on SendMessageComplex.
    62  func (am AllowedMentions) Verify() error {
    63  	if len(am.Roles) > 100 {
    64  		return errors.Errorf("roles slice length %d is over 100", len(am.Roles))
    65  	}
    66  	if len(am.Users) > 100 {
    67  		return errors.Errorf("users slice length %d is over 100", len(am.Users))
    68  	}
    69  
    70  	for _, allowed := range am.Parse {
    71  		switch allowed {
    72  		case AllowRoleMention:
    73  			if len(am.Roles) > 0 {
    74  				return errors.New(`parse has AllowRoleMention and Roles slice is not empty`)
    75  			}
    76  		case AllowUserMention:
    77  			if len(am.Users) > 0 {
    78  				return errors.New(`parse has AllowUserMention and Users slice is not empty`)
    79  			}
    80  		}
    81  	}
    82  
    83  	return nil
    84  }
    85  
    86  // ErrEmptyMessage is returned if either a SendMessageData or an
    87  // ExecuteWebhookData has both an empty Content and no Embed(s).
    88  var ErrEmptyMessage = errors.New("message is empty")
    89  
    90  // SendMessageFile represents a file to be uploaded to Discord.
    91  type SendMessageFile struct {
    92  	Name   string
    93  	Reader io.Reader
    94  }
    95  
    96  // SendMessageData is the full structure to send a new message to Discord with.
    97  type SendMessageData struct {
    98  	// Content are the message contents (up to 2000 characters).
    99  	Content string `json:"content,omitempty"`
   100  	// Nonce is a nonce that can be used for optimistic message sending.
   101  	Nonce string `json:"nonce,omitempty"`
   102  
   103  	// TTS is true if this is a TTS message.
   104  	TTS bool `json:"tts,omitempty"`
   105  	// Embed is embedded rich content.
   106  	Embed *discord.Embed `json:"embed,omitempty"`
   107  
   108  	Files []SendMessageFile `json:"-"`
   109  
   110  	// AllowedMentions are the allowed mentions for a message.
   111  	AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
   112  }
   113  
   114  func (data *SendMessageData) WriteMultipart(body *multipart.Writer) error {
   115  	return writeMultipart(body, data, data.Files)
   116  }
   117  
   118  // SendMessageComplex posts a message to a guild text or DM channel. If
   119  // operating on a guild channel, this endpoint requires the SEND_MESSAGES
   120  // permission to be present on the current user. If the tts field is set to
   121  // true, the SEND_TTS_MESSAGES permission is required for the message to be
   122  // spoken. Returns a message object. Fires a Message Create Gateway event.
   123  //
   124  // The maximum request size when sending a message is 8MB.
   125  //
   126  // This endpoint supports requests with Content-Types of both application/json
   127  // and multipart/form-data. You must however use multipart/form-data when
   128  // uploading files. Note that when sending multipart/form-data requests the
   129  // embed field cannot be used, however you can pass a JSON encoded body as form
   130  // value for payload_json, where additional request parameters such as embed
   131  // can be set.
   132  //
   133  // Note that when sending application/json you must send at least one of
   134  // content or embed, and when sending multipart/form-data, you must send at
   135  // least one of content, embed or file. For a file attachment, the
   136  // Content-Disposition subpart header MUST contain a filename parameter.
   137  func (c *Client) SendMessageComplex(
   138  	channelID discord.ChannelID, data SendMessageData) (*discord.Message, error) {
   139  
   140  	if data.Content == "" && data.Embed == nil && len(data.Files) == 0 {
   141  		return nil, ErrEmptyMessage
   142  	}
   143  
   144  	if data.AllowedMentions != nil {
   145  		if err := data.AllowedMentions.Verify(); err != nil {
   146  			return nil, errors.Wrap(err, "allowedMentions error")
   147  		}
   148  	}
   149  
   150  	if data.Embed != nil {
   151  		if err := data.Embed.Validate(); err != nil {
   152  			return nil, errors.Wrap(err, "embed error")
   153  		}
   154  	}
   155  
   156  	var URL = EndpointChannels + channelID.String() + "/messages"
   157  	var msg *discord.Message
   158  
   159  	if len(data.Files) == 0 {
   160  		// No files, so no need for streaming.
   161  		return msg, c.RequestJSON(&msg, "POST", URL, httputil.WithJSONBody(data))
   162  	}
   163  
   164  	writer := func(mw *multipart.Writer) error {
   165  		return data.WriteMultipart(mw)
   166  	}
   167  
   168  	resp, err := c.MeanwhileMultipart(writer, "POST", URL)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	var body = resp.GetBody()
   174  	defer body.Close()
   175  
   176  	return msg, json.DecodeStream(body, &msg)
   177  }
   178  
   179  type ExecuteWebhookData struct {
   180  	// Content are the message contents (up to 2000 characters).
   181  	//
   182  	// Required: one of content, file, embeds
   183  	Content string `json:"content,omitempty"`
   184  
   185  	// Username overrides the default username of the webhook
   186  	Username string `json:"username,omitempty"`
   187  	// AvatarURL overrides the default avatar of the webhook.
   188  	AvatarURL discord.URL `json:"avatar_url,omitempty"`
   189  
   190  	// TTS is true if this is a TTS message.
   191  	TTS bool `json:"tts,omitempty"`
   192  	// Embeds contains embedded rich content.
   193  	//
   194  	// Required: one of content, file, embeds
   195  	Embeds []discord.Embed `json:"embeds,omitempty"`
   196  
   197  	Files []SendMessageFile `json:"-"`
   198  
   199  	// AllowedMentions are the allowed mentions for the message.
   200  	AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
   201  }
   202  
   203  func (data *ExecuteWebhookData) WriteMultipart(body *multipart.Writer) error {
   204  	return writeMultipart(body, data, data.Files)
   205  }
   206  
   207  func writeMultipart(body *multipart.Writer, item interface{}, files []SendMessageFile) error {
   208  	defer body.Close()
   209  
   210  	// Encode the JSON body first
   211  	w, err := body.CreateFormField("payload_json")
   212  	if err != nil {
   213  		return errors.Wrap(err, "failed to create bodypart for JSON")
   214  	}
   215  
   216  	if err := json.EncodeStream(w, item); err != nil {
   217  		return errors.Wrap(err, "failed to encode JSON")
   218  	}
   219  
   220  	for i, file := range files {
   221  		num := strconv.Itoa(i)
   222  
   223  		w, err := body.CreateFormFile("file"+num, file.Name)
   224  		if err != nil {
   225  			return errors.Wrap(err, "failed to create bodypart for "+num)
   226  		}
   227  
   228  		if _, err := io.Copy(w, file.Reader); err != nil {
   229  			return errors.Wrap(err, "failed to write for file "+num)
   230  		}
   231  	}
   232  
   233  	return nil
   234  }