1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-11-01 04:24:19 +00:00
arikawa/api/interaction.go
diamondburned d8438f3b51
discord: Refactor interactions and components
This commit gets rid of contain-it-all structs and instead opt for
interface union types containing underlying concrete types with no
overloading.

The code is much more verbose by doing this, but the API is much nicer
to use. The only disadvantage in that regard is the interface assertion
being too verbose and risky for users at times.
2021-11-09 18:31:06 -08:00

294 lines
9.5 KiB
Go

package api
import (
"mime/multipart"
"strconv"
"github.com/pkg/errors"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/utils/json/option"
"github.com/diamondburned/arikawa/v3/utils/sendpart"
)
var EndpointInteractions = Endpoint + "interactions/"
type InteractionResponseType uint
// https://discord.com/developers/docs/interactions/slash-commands#interaction-response-object-interaction-callback-type
const (
PongInteraction InteractionResponseType = iota + 1
_
_
MessageInteractionWithSource
DeferredMessageInteractionWithSource
DeferredMessageUpdate
UpdateMessage
AutocompleteResult
)
// InteractionResponseFlags implements flags for an
// InteractionApplicationCommandCallbackData.
//
// https://discord.com/developers/docs/interactions/slash-commands#interaction-response-object-interaction-application-command-callback-data-flags
type InteractionResponseFlags uint
const EphemeralResponse InteractionResponseFlags = 64
type InteractionResponse struct {
Type InteractionResponseType `json:"type"`
Data *InteractionResponseData `json:"data,omitempty"`
}
// NeedsMultipart returns true if the InteractionResponse has files.
func (resp InteractionResponse) NeedsMultipart() bool {
return resp.Data != nil && resp.Data.NeedsMultipart()
}
func (resp InteractionResponse) WriteMultipart(body *multipart.Writer) error {
return sendpart.Write(body, resp, resp.Data.Files)
}
// InteractionResponseData is InteractionApplicationCommandCallbackData in the
// official documentation.
type InteractionResponseData struct {
// Content are the message contents (up to 2000 characters).
//
// Required: one of content, file, embeds
Content option.NullableString `json:"content,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
Embeds *[]discord.Embed `json:"embeds,omitempty"`
// Components is the list of components (such as buttons) to be attached to
// the message.
Components *discord.ContainerComponents `json:"components,omitempty"`
// AllowedMentions are the allowed mentions for the message.
AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
// Flags are the interaction application command callback data flags.
Flags InteractionResponseFlags `json:"flags,omitempty"`
// Files represents a list of files to upload. This will not be
// JSON-encoded and will only be available through WriteMultipart.
Files []sendpart.File `json:"-"`
// Choices are the results to display on autocomplete interaction events.
//
// During all other events, this should not be provided.
Choices *[]AutocompleteChoice `json:"choices"`
}
// NeedsMultipart returns true if the InteractionResponseData has files.
func (d InteractionResponseData) NeedsMultipart() bool {
return len(d.Files) > 0
}
func (d InteractionResponseData) WriteMultipart(body *multipart.Writer) error {
return sendpart.Write(body, d, d.Files)
}
// AutocompleteChoice is the choice in ApplicationCommandAutocompleteResult in
// the official documentation.
type AutocompleteChoice struct {
Name string `json:"name"`
Value string `json:"value"`
}
// RespondInteraction responds to an incoming interaction. It is also known as
// an "interaction callback".
func (c *Client) RespondInteraction(
id discord.InteractionID, token string, resp InteractionResponse) error {
if resp.Data != nil {
if resp.Type == MessageInteractionWithSource {
// A new message is being created, make sure none of the fields
// are null or empty.
if (resp.Data.Content == nil || resp.Data.Content.Val == "") &&
(resp.Data.Embeds == nil || len(*resp.Data.Embeds) == 0) &&
len(resp.Data.Files) == 0 {
return ErrEmptyMessage
}
} else if resp.Type == UpdateMessage {
// A component is being updated. We therefore don't know what
// fields are filled. The only thing we can check is if content,
// embeds and files are null.
if (resp.Data.Content != nil && !resp.Data.Content.Init) &&
(resp.Data.Embeds != nil && *resp.Data.Embeds == nil) && len(resp.Data.Files) == 0 {
return ErrEmptyMessage
}
}
if resp.Data.AllowedMentions != nil {
if err := resp.Data.AllowedMentions.Verify(); err != nil {
return errors.Wrap(err, "allowedMentions error")
}
}
if resp.Data.Embeds != nil {
sum := 0
for i, embed := range *resp.Data.Embeds {
if err := embed.Validate(); err != nil {
return errors.Wrap(err, "embed error at "+strconv.Itoa(i))
}
sum += embed.Length()
if sum > 6000 {
return &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of all text in embeds"}
}
(*resp.Data.Embeds)[i] = embed // embed.Validate changes fields
}
}
}
URL := EndpointInteractions + id.String() + "/" + token + "/callback"
return sendpart.POST(c.Client, resp, nil, URL)
}
// InteractionResponse returns the initial interaction response.
func (c *Client) InteractionResponse(
appID discord.AppID, token string) (*discord.Message, error) {
var m *discord.Message
return m, c.RequestJSON(
&m, "GET",
EndpointWebhooks+appID.String()+"/"+token+"/messages/@original")
}
type EditInteractionResponseData struct {
// Content are the new message contents (up to 2000 characters).
Content option.NullableString `json:"content,omitempty"`
// Embeds contains embedded rich content.
Embeds *[]discord.Embed `json:"embeds,omitempty"`
// Components contains the new components to attach.
Components *discord.ContainerComponents `json:"components,omitempty"`
// AllowedMentions are the allowed mentions for the message.
AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
// Attachments are the attached files to keep.
Attachments *[]discord.Attachment `json:"attachments,omitempty"`
// Files represents a list of files to upload. This will not be
// JSON-encoded and will only be available through WriteMultipart.
Files []sendpart.File `json:"-"`
}
// NeedsMultipart returns true if the SendMessageData has files.
func (data EditInteractionResponseData) NeedsMultipart() bool {
return len(data.Files) > 0
}
func (data EditInteractionResponseData) WriteMultipart(body *multipart.Writer) error {
return sendpart.Write(body, data, data.Files)
}
// EditInteractionResponse edits the initial Interaction response.
func (c *Client) EditInteractionResponse(
appID discord.AppID,
token string, data EditInteractionResponseData) (*discord.Message, error) {
if data.AllowedMentions != nil {
if err := data.AllowedMentions.Verify(); err != nil {
return nil, errors.Wrap(err, "allowedMentions error")
}
}
if data.Embeds != nil {
sum := 0
for i, e := range *data.Embeds {
if err := e.Validate(); err != nil {
return nil, errors.Wrap(err, "embed error")
}
sum += e.Length()
if sum > 6000 {
return nil, &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of text in embeds"}
}
(*data.Embeds)[i] = e // e.Validate changes fields
}
}
var msg *discord.Message
return msg, sendpart.PATCH(c.Client, data, &msg,
EndpointWebhooks+appID.String()+"/"+token+"/messages/@original")
}
// DeleteInteractionResponse deletes the initial interaction response.
func (c *Client) DeleteInteractionResponse(appID discord.AppID, token string) error {
return c.FastRequest("DELETE",
EndpointWebhooks+appID.String()+"/"+token+"/messages/@original")
}
// CreateInteractionFollowup creates a followup message for an interaction.
func (c *Client) CreateInteractionFollowup(
appID discord.AppID, token string, data InteractionResponseData) (*discord.Message, error) {
if (data.Content == nil || data.Content.Val == "") &&
(data.Embeds == nil || len(*data.Embeds) == 0) && len(data.Files) == 0 {
return nil, ErrEmptyMessage
}
if data.AllowedMentions != nil {
if err := data.AllowedMentions.Verify(); err != nil {
return nil, errors.Wrap(err, "allowedMentions error")
}
}
if data.Embeds != nil {
sum := 0
for i, embed := range *data.Embeds {
if err := embed.Validate(); err != nil {
return nil, errors.Wrap(err, "embed error at "+strconv.Itoa(i))
}
sum += embed.Length()
if sum > 6000 {
return nil, &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of all text in embeds"}
}
(*data.Embeds)[i] = embed // embed.Validate changes fields
}
}
var msg *discord.Message
return msg, sendpart.POST(
c.Client, data, msg, EndpointWebhooks+appID.String()+"/"+token+"?")
}
func (c *Client) EditInteractionFollowup(
appID discord.AppID, messageID discord.MessageID,
token string, data EditInteractionResponseData) (*discord.Message, error) {
if data.AllowedMentions != nil {
if err := data.AllowedMentions.Verify(); err != nil {
return nil, errors.Wrap(err, "allowedMentions error")
}
}
if data.Embeds != nil {
sum := 0
for i, e := range *data.Embeds {
if err := e.Validate(); err != nil {
return nil, errors.Wrap(err, "embed error")
}
sum += e.Length()
if sum > 6000 {
return nil, &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of text in embeds"}
}
(*data.Embeds)[i] = e // e.Validate changes fields
}
}
var msg *discord.Message
return msg, sendpart.PATCH(c.Client, data, &msg,
EndpointWebhooks+appID.String()+"/"+token+"/messages/"+messageID.String())
}
// DeleteInteractionFollowup deletes a followup message for an interaction.
func (c *Client) DeleteInteractionFollowup(
appID discord.AppID, messageID discord.MessageID, token string) error {
return c.FastRequest("DELETE",
EndpointWebhooks+appID.String()+"/"+token+"/messages/"+messageID.String())
}