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 }