2020-04-19 23:35:37 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"io"
|
|
|
|
"mime/multipart"
|
2020-07-29 20:50:55 +00:00
|
|
|
"strconv"
|
2020-04-19 23:35:37 +00:00
|
|
|
|
2020-07-29 19:29:30 +00:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
|
2020-10-28 22:39:59 +00:00
|
|
|
"github.com/diamondburned/arikawa/v2/discord"
|
|
|
|
"github.com/diamondburned/arikawa/v2/utils/httputil"
|
|
|
|
"github.com/diamondburned/arikawa/v2/utils/json"
|
2020-11-03 17:14:25 +00:00
|
|
|
"github.com/diamondburned/arikawa/v2/utils/json/option"
|
2020-04-19 23:35:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const AttachmentSpoilerPrefix = "SPOILER_"
|
|
|
|
|
2020-11-03 17:13:56 +00:00
|
|
|
// AllowedMentions is a allowlist of mentions for a message.
|
2020-04-19 23:35:37 +00:00
|
|
|
//
|
2020-11-03 17:13:56 +00:00
|
|
|
// Allowlists
|
2020-04-19 23:35:37 +00:00
|
|
|
//
|
2020-11-03 17:13:56 +00:00
|
|
|
// Roles and Users are slices that act as allowlists for IDs that are allowed
|
|
|
|
// to be mentioned. For example, if only 1 ID is provided in Users, then only
|
|
|
|
// that ID will be parsed in the message. No other IDs will be. The same
|
|
|
|
// example also applies for roles.
|
2020-04-19 23:35:37 +00:00
|
|
|
//
|
|
|
|
// If Parse is an empty slice and both Users and Roles are empty slices, then no
|
|
|
|
// mentions will be parsed.
|
|
|
|
//
|
|
|
|
// Constraints
|
|
|
|
//
|
|
|
|
// If the Users slice is not empty, then Parse must not have AllowUserMention.
|
|
|
|
// Likewise, if the Roles slice is not empty, then Parse must not have
|
|
|
|
// AllowRoleMention. This is because everything provided in Parse will make
|
|
|
|
// Discord parse it completely, meaning they would be mutually exclusive with
|
2020-11-03 17:13:56 +00:00
|
|
|
// Roles and Users.
|
|
|
|
//
|
2020-11-03 17:16:42 +00:00
|
|
|
// https://discord.com/developers/docs/resources/channel#allowed-mentions-object
|
2020-04-19 23:35:37 +00:00
|
|
|
type AllowedMentions struct {
|
2020-05-11 22:06:19 +00:00
|
|
|
// Parse is an array of allowed mention types to parse from the content.
|
2020-04-19 23:35:37 +00:00
|
|
|
Parse []AllowedMentionType `json:"parse"`
|
2020-05-11 22:06:19 +00:00
|
|
|
// Roles is an array of role_ids to mention (Max size of 100).
|
2020-07-21 20:27:59 +00:00
|
|
|
Roles []discord.RoleID `json:"roles,omitempty"`
|
2020-05-11 22:06:19 +00:00
|
|
|
// Users is an array of user_ids to mention (Max size of 100).
|
2020-07-21 20:27:59 +00:00
|
|
|
Users []discord.UserID `json:"users,omitempty"`
|
2020-11-03 17:14:25 +00:00
|
|
|
// RepliedUser is used specifically for inline replies to specify, whether
|
|
|
|
// to mention the author of the message you are replying to or not.
|
|
|
|
RepliedUser option.Bool `json:"replied_user,omitempty"`
|
2020-04-19 23:35:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// AllowedMentionType is a constant that tells Discord what is allowed to parse
|
2020-11-03 17:14:25 +00:00
|
|
|
// from a message content. This can help prevent things such as an
|
|
|
|
// unintentional @everyone mention.
|
2020-04-19 23:35:37 +00:00
|
|
|
type AllowedMentionType string
|
|
|
|
|
2020-11-03 17:14:25 +00:00
|
|
|
// https://discord.com/developers/docs/resources/channel#allowed-mentions-object-allowed-mention-types
|
2020-04-19 23:35:37 +00:00
|
|
|
const (
|
|
|
|
// AllowRoleMention makes Discord parse roles in the content.
|
|
|
|
AllowRoleMention AllowedMentionType = "roles"
|
|
|
|
// AllowUserMention makes Discord parse user mentions in the content.
|
|
|
|
AllowUserMention AllowedMentionType = "users"
|
|
|
|
// AllowEveryoneMention makes Discord parse @everyone mentions.
|
|
|
|
AllowEveryoneMention AllowedMentionType = "everyone"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Verify checks the AllowedMentions against constraints mentioned in
|
|
|
|
// AllowedMentions' documentation. This will be called on SendMessageComplex.
|
|
|
|
func (am AllowedMentions) Verify() error {
|
|
|
|
if len(am.Roles) > 100 {
|
2020-05-16 21:14:49 +00:00
|
|
|
return errors.Errorf("roles slice length %d is over 100", len(am.Roles))
|
2020-04-19 23:35:37 +00:00
|
|
|
}
|
|
|
|
if len(am.Users) > 100 {
|
2020-05-16 21:14:49 +00:00
|
|
|
return errors.Errorf("users slice length %d is over 100", len(am.Users))
|
2020-04-19 23:35:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, allowed := range am.Parse {
|
|
|
|
switch allowed {
|
|
|
|
case AllowRoleMention:
|
|
|
|
if len(am.Roles) > 0 {
|
2020-05-16 21:14:49 +00:00
|
|
|
return errors.New(`parse has AllowRoleMention and Roles slice is not empty`)
|
2020-04-19 23:35:37 +00:00
|
|
|
}
|
|
|
|
case AllowUserMention:
|
|
|
|
if len(am.Users) > 0 {
|
2020-05-16 21:14:49 +00:00
|
|
|
return errors.New(`parse has AllowUserMention and Users slice is not empty`)
|
2020-04-19 23:35:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ErrEmptyMessage is returned if either a SendMessageData or an
|
|
|
|
// ExecuteWebhookData has both an empty Content and no Embed(s).
|
2020-05-16 21:14:49 +00:00
|
|
|
var ErrEmptyMessage = errors.New("message is empty")
|
2020-04-19 23:35:37 +00:00
|
|
|
|
|
|
|
// SendMessageFile represents a file to be uploaded to Discord.
|
|
|
|
type SendMessageFile struct {
|
|
|
|
Name string
|
|
|
|
Reader io.Reader
|
|
|
|
}
|
|
|
|
|
|
|
|
// SendMessageData is the full structure to send a new message to Discord with.
|
|
|
|
type SendMessageData struct {
|
2020-05-11 22:06:19 +00:00
|
|
|
// Content are the message contents (up to 2000 characters).
|
2020-04-19 23:35:37 +00:00
|
|
|
Content string `json:"content,omitempty"`
|
2020-05-11 22:06:19 +00:00
|
|
|
// Nonce is a nonce that can be used for optimistic message sending.
|
|
|
|
Nonce string `json:"nonce,omitempty"`
|
2020-04-19 23:35:37 +00:00
|
|
|
|
2020-05-11 22:06:19 +00:00
|
|
|
// TTS is true if this is a TTS message.
|
|
|
|
TTS bool `json:"tts,omitempty"`
|
|
|
|
// Embed is embedded rich content.
|
2020-04-19 23:35:37 +00:00
|
|
|
Embed *discord.Embed `json:"embed,omitempty"`
|
|
|
|
|
|
|
|
Files []SendMessageFile `json:"-"`
|
|
|
|
|
2020-05-11 22:06:19 +00:00
|
|
|
// AllowedMentions are the allowed mentions for a message.
|
2020-04-19 23:35:37 +00:00
|
|
|
AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
|
2020-11-03 18:32:19 +00:00
|
|
|
// Reference allows you to reference another message to create a reply. The
|
|
|
|
// referenced message must be from the same channel.
|
2020-11-03 17:36:07 +00:00
|
|
|
//
|
|
|
|
// Only MessageID is necessary. You may also include a channel_id and
|
|
|
|
// guild_id in the reference. However, they are not necessary, but will be
|
|
|
|
// validated if sent.
|
2020-11-03 18:32:19 +00:00
|
|
|
Reference *discord.MessageReference `json:"message_reference,omitempty"`
|
2020-04-19 23:35:37 +00:00
|
|
|
}
|
|
|
|
|
2020-05-08 03:43:46 +00:00
|
|
|
func (data *SendMessageData) WriteMultipart(body *multipart.Writer) error {
|
2020-07-29 20:50:55 +00:00
|
|
|
return writeMultipart(body, data, data.Files)
|
2020-04-19 23:35:37 +00:00
|
|
|
}
|
|
|
|
|
2020-05-11 22:06:19 +00:00
|
|
|
// SendMessageComplex posts a message to a guild text or DM channel. If
|
|
|
|
// operating on a guild channel, this endpoint requires the SEND_MESSAGES
|
|
|
|
// permission to be present on the current user. If the tts field is set to
|
|
|
|
// true, the SEND_TTS_MESSAGES permission is required for the message to be
|
|
|
|
// spoken. Returns a message object. Fires a Message Create Gateway event.
|
|
|
|
//
|
|
|
|
// The maximum request size when sending a message is 8MB.
|
|
|
|
//
|
|
|
|
// This endpoint supports requests with Content-Types of both application/json
|
|
|
|
// and multipart/form-data. You must however use multipart/form-data when
|
|
|
|
// uploading files. Note that when sending multipart/form-data requests the
|
|
|
|
// embed field cannot be used, however you can pass a JSON encoded body as form
|
|
|
|
// value for payload_json, where additional request parameters such as embed
|
|
|
|
// can be set.
|
|
|
|
//
|
|
|
|
// Note that when sending application/json you must send at least one of
|
|
|
|
// content or embed, and when sending multipart/form-data, you must send at
|
|
|
|
// least one of content, embed or file. For a file attachment, the
|
|
|
|
// Content-Disposition subpart header MUST contain a filename parameter.
|
2020-04-19 23:35:37 +00:00
|
|
|
func (c *Client) SendMessageComplex(
|
2020-07-21 20:27:59 +00:00
|
|
|
channelID discord.ChannelID, data SendMessageData) (*discord.Message, error) {
|
2020-04-19 23:35:37 +00:00
|
|
|
|
2020-04-20 23:30:12 +00:00
|
|
|
if data.Content == "" && data.Embed == nil && len(data.Files) == 0 {
|
2020-04-19 23:35:37 +00:00
|
|
|
return nil, ErrEmptyMessage
|
|
|
|
}
|
|
|
|
|
|
|
|
if data.AllowedMentions != nil {
|
|
|
|
if err := data.AllowedMentions.Verify(); err != nil {
|
2020-05-16 21:14:49 +00:00
|
|
|
return nil, errors.Wrap(err, "allowedMentions error")
|
2020-04-19 23:35:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if data.Embed != nil {
|
|
|
|
if err := data.Embed.Validate(); err != nil {
|
2020-05-16 21:14:49 +00:00
|
|
|
return nil, errors.Wrap(err, "embed error")
|
2020-04-19 23:35:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var URL = EndpointChannels + channelID.String() + "/messages"
|
|
|
|
var msg *discord.Message
|
|
|
|
|
|
|
|
if len(data.Files) == 0 {
|
|
|
|
// No files, so no need for streaming.
|
2020-05-08 03:43:46 +00:00
|
|
|
return msg, c.RequestJSON(&msg, "POST", URL, httputil.WithJSONBody(data))
|
2020-04-19 23:35:37 +00:00
|
|
|
}
|
|
|
|
|
2020-11-14 23:30:18 +00:00
|
|
|
resp, err := c.MeanwhileMultipart(data.WriteMultipart, "POST", URL)
|
2020-04-19 23:35:37 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var body = resp.GetBody()
|
|
|
|
defer body.Close()
|
|
|
|
|
2020-05-08 03:43:46 +00:00
|
|
|
return msg, json.DecodeStream(body, &msg)
|
2020-04-19 23:35:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type ExecuteWebhookData struct {
|
2020-05-11 22:06:19 +00:00
|
|
|
// Content are the message contents (up to 2000 characters).
|
|
|
|
//
|
|
|
|
// Required: one of content, file, embeds
|
2020-04-19 23:35:37 +00:00
|
|
|
Content string `json:"content,omitempty"`
|
|
|
|
|
2020-05-11 22:06:19 +00:00
|
|
|
// Username overrides the default username of the webhook
|
|
|
|
Username string `json:"username,omitempty"`
|
|
|
|
// AvatarURL overrides the default avatar of the webhook.
|
|
|
|
AvatarURL discord.URL `json:"avatar_url,omitempty"`
|
|
|
|
|
|
|
|
// TTS is true if this is a TTS message.
|
|
|
|
TTS bool `json:"tts,omitempty"`
|
|
|
|
// Embeds contains embedded rich content.
|
|
|
|
//
|
|
|
|
// Required: one of content, file, embeds
|
2020-04-19 23:35:37 +00:00
|
|
|
Embeds []discord.Embed `json:"embeds,omitempty"`
|
|
|
|
|
|
|
|
Files []SendMessageFile `json:"-"`
|
|
|
|
|
2020-05-11 22:06:19 +00:00
|
|
|
// AllowedMentions are the allowed mentions for the message.
|
2020-04-19 23:35:37 +00:00
|
|
|
AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
|
|
|
|
}
|
|
|
|
|
2020-05-08 03:43:46 +00:00
|
|
|
func (data *ExecuteWebhookData) WriteMultipart(body *multipart.Writer) error {
|
2020-07-29 20:50:55 +00:00
|
|
|
return writeMultipart(body, data, data.Files)
|
|
|
|
}
|
|
|
|
|
|
|
|
func writeMultipart(body *multipart.Writer, item interface{}, files []SendMessageFile) error {
|
|
|
|
defer body.Close()
|
|
|
|
|
|
|
|
// Encode the JSON body first
|
|
|
|
w, err := body.CreateFormField("payload_json")
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to create bodypart for JSON")
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.EncodeStream(w, item); err != nil {
|
|
|
|
return errors.Wrap(err, "failed to encode JSON")
|
|
|
|
}
|
|
|
|
|
|
|
|
for i, file := range files {
|
|
|
|
num := strconv.Itoa(i)
|
|
|
|
|
|
|
|
w, err := body.CreateFormFile("file"+num, file.Name)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to create bodypart for "+num)
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := io.Copy(w, file.Reader); err != nil {
|
|
|
|
return errors.Wrap(err, "failed to write for file "+num)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2020-04-19 23:35:37 +00:00
|
|
|
}
|