From 10c8837000b2bf6df54e9ee008d9bf6d8605cfaf Mon Sep 17 00:00:00 2001 From: Scott Date: Sun, 30 May 2021 05:28:37 +0100 Subject: [PATCH] api: Finalized buttons implementation (#200) * all: Added Components fields to message-related types * discord: Documented Reactions field * discord: Implement fix for Component * gateway: Added User and Message fields to InteractionCreateEvent * api: Made InteractionResponseData fields optional for UpdateMessage responses * api: Deprecated and updated interaction response types * gateway: Update optional interaction event fields * discord: Added ComponentWrap for json unmarshalling * state: Update components on MessageUpdate * Updated buttons example --- _example/buttons/main.go | 15 +-- api/interaction.go | 18 ++-- api/message.go | 2 + api/send.go | 3 + discord/component.go | 145 ++++++++++++++++++++++++---- discord/message.go | 4 +- gateway/events.go | 12 ++- state/store/defaultstore/message.go | 3 + 8 files changed, 162 insertions(+), 40 deletions(-) diff --git a/_example/buttons/main.go b/_example/buttons/main.go index 57c5692..7e6dd87 100644 --- a/_example/buttons/main.go +++ b/_example/buttons/main.go @@ -8,6 +8,7 @@ import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/session" + "github.com/diamondburned/arikawa/v2/utils/json/option" ) // To run, do `APP_ID="APP ID" GUILD_ID="GUILD ID" BOT_TOKEN="TOKEN HERE" go run .` @@ -35,9 +36,9 @@ func main() { Data: &api.InteractionResponseData{ Content: "This is a message with a button!", Components: []discord.Component{ - discord.ActionRow{ + discord.ActionRowComponent{ Components: []discord.Component{ - discord.Button{ + discord.ButtonComponent{ Label: "Hello World!", CustomID: "first_button", Emoji: &discord.ButtonEmoji{ @@ -45,22 +46,22 @@ func main() { }, Style: discord.PrimaryButton, }, - discord.Button{ + discord.ButtonComponent{ Label: "Secondary", CustomID: "second_button", Style: discord.SecondaryButton, }, - discord.Button{ + discord.ButtonComponent{ Label: "Success", CustomID: "success_button", Style: discord.SuccessButton, }, - discord.Button{ + discord.ButtonComponent{ Label: "Danger", CustomID: "danger_button", Style: discord.DangerButton, }, - discord.Button{ + discord.ButtonComponent{ Label: "Link", URL: "https://google.com", Style: discord.LinkButton, @@ -83,7 +84,7 @@ func main() { data := api.InteractionResponse{ Type: api.UpdateMessage, Data: &api.InteractionResponseData{ - Content: "Custom ID: " + e.Data.CustomID, + Content: option.NewNullableString("Custom ID: " + e.Data.CustomID), }, } diff --git a/api/interaction.go b/api/interaction.go index 21b3d00..0432759 100644 --- a/api/interaction.go +++ b/api/interaction.go @@ -3,18 +3,20 @@ package api import ( "github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/utils/httputil" + "github.com/diamondburned/arikawa/v2/utils/json/option" ) var EndpointInteractions = Endpoint + "interactions/" type InteractionResponseType uint +// https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactioncallbacktype const ( PongInteraction InteractionResponseType = iota + 1 - AcknowledgeInteraction - MessageInteraction + _ + _ MessageInteractionWithSource - AcknowledgeInteractionWithSource + DeferredMessageInteractionWithSource DeferredMessageUpdate UpdateMessage ) @@ -27,11 +29,11 @@ type InteractionResponse struct { // InteractionResponseData is InteractionApplicationCommandCallbackData in the // official documentation. type InteractionResponseData struct { - TTS bool `json:"tts"` - Content string `json:"content"` - Components []discord.Component `json:"components,omitempty"` - Embeds []discord.Embed `json:"embeds,omitempty"` - AllowedMentions AllowedMentions `json:"allowed_mentions,omitempty"` + TTS option.NullableBool `json:"tts,omitempty"` + Content option.NullableString `json:"content,omitempty"` + Components *[]discord.Component `json:"components,omitempty"` + Embeds *[]discord.Embed `json:"embeds,omitempty"` + AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` } // RespondInteraction responds to an incoming interaction. It is also known as diff --git a/api/message.go b/api/message.go index ee0363e..0c1d351 100644 --- a/api/message.go +++ b/api/message.go @@ -291,6 +291,8 @@ type EditMessageData struct { Content option.NullableString `json:"content,omitempty"` // Embed contains embedded rich content. Embed *discord.Embed `json:"embed,omitempty"` + // Components contains the new components to attach. + Components *[]discord.Component `json:"components,omitempty"` // AllowedMentions are the allowed mentions for a message. AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` // Flags edits the flags of a message (only SUPPRESS_EMBEDS can currently diff --git a/api/send.go b/api/send.go index 772935a..f2bd489 100644 --- a/api/send.go +++ b/api/send.go @@ -105,6 +105,9 @@ type SendMessageData struct { // Files is the list of file attachments to be uploaded. To reference a file // in an embed, use (sendpart.File).AttachmentURI(). Files []sendpart.File `json:"-"` + // Components is the list of components (such as buttons) to be attached to + // the message. + Components []discord.Component `json:"components,omitempty"` // AllowedMentions are the allowed mentions for a message. AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` diff --git a/discord/component.go b/discord/component.go index 946394f..5f25acb 100644 --- a/discord/component.go +++ b/discord/component.go @@ -1,15 +1,70 @@ package discord -import "encoding/json" +import ( + "errors" + + "github.com/diamondburned/arikawa/v2/utils/json" +) + +var ErrNestedActionRow = errors.New("action row cannot have action row as a child") // ComponentType is the type of a component. type ComponentType uint const ( - ActionRowComponent ComponentType = iota + 1 - ButtonComponent + ActionRowComponentType ComponentType = iota + 1 + ButtonComponentType ) +// ComponentWrap wraps component for the purpose of JSON unmarshalling. +// Type assetions should be made on Component to access the underlying data. +type ComponentWrap struct { + Component Component +} + +// UnwrapComponents returns a slice of the underlying component interfaces. +func UnwrapComponents(wraps []ComponentWrap) []Component { + components := make([]Component, len(wraps)) + for i, w := range wraps { + components[i] = w.Component + } + + return components +} + +// Type returns the underlying component's type. +func (c ComponentWrap) Type() ComponentType { + return c.Component.Type() +} + +// MarshalJSON marshals the component in the format Discord expects. +func (c *ComponentWrap) MarshalJSON() ([]byte, error) { + return c.Component.MarshalJSON() +} + +// UnmarshalJSON unmarshals json into the component. +func (c *ComponentWrap) UnmarshalJSON(b []byte) error { + var t struct { + Type ComponentType `json:"type"` + } + + err := json.Unmarshal(b, &t) + if err != nil { + return err + } + + switch t.Type { + case ActionRowComponentType: + c.Component = &ActionRowComponent{} + case ButtonComponentType: + c.Component = &ButtonComponent{} + default: + c.Component = &UnknownComponent{typ: t.Type} + } + + return json.Unmarshal(b, c.Component) +} + // Component is a component that can be attached to an interaction response. type Component interface { json.Marshaler @@ -17,30 +72,70 @@ type Component interface { } // ActionRow is a row of components at the bottom of a message. -type ActionRow struct { +type ActionRowComponent struct { Components []Component `json:"components"` } -// Type implements the InteractionComponent interface. -func (ActionRow) Type() ComponentType { - return ActionRowComponent +// Type implements the Component Data interface. +func (ActionRowComponent) Type() ComponentType { + return ActionRowComponentType } // MarshalJSON marshals the action row in the format Discord expects. -func (a ActionRow) MarshalJSON() ([]byte, error) { - type actionRow ActionRow +func (a ActionRowComponent) MarshalJSON() ([]byte, error) { + type actionRow ActionRowComponent return json.Marshal(struct { actionRow Type ComponentType `json:"type"` }{ actionRow: actionRow(a), - Type: ActionRowComponent, + Type: ActionRowComponentType, }) } +// UnmarshalJSON unmarshals json into the components. +func (a *ActionRowComponent) UnmarshalJSON(b []byte) error { + type actionRow ActionRowComponent + + type rowTypes struct { + Components []struct { + Type ComponentType `json:"type"` + } `json:"components"` + } + + var r rowTypes + err := json.Unmarshal(b, &r) + if err != nil { + return err + } + + a.Components = make([]Component, len(r.Components)) + for i, t := range r.Components { + switch t.Type { + case ActionRowComponentType: + // ActionRow cannot have child components of type Actionrow + return ErrNestedActionRow + case ButtonComponentType: + a.Components[i] = &ButtonComponent{} + default: + a.Components[i] = &UnknownComponent{typ: t.Type} + } + } + + alias := actionRow(*a) + err = json.Unmarshal(b, &alias) + if err != nil { + return err + } + + *a = ActionRowComponent(alias) + + return nil +} + // Button is a clickable button that may be added to an interaction response. -type Button struct { +type ButtonComponent struct { Label string `json:"label"` // CustomID attached to InteractionCreate event when clicked. CustomID string `json:"custom_id"` @@ -51,6 +146,11 @@ type Button struct { Disabled bool `json:"disabled,omitempty"` } +// Type implements the Component Data interface. +func (ButtonComponent) Type() ComponentType { + return ButtonComponentType +} + // ButtonStyle is the style to display a button in. type ButtonStyle uint @@ -70,14 +170,9 @@ type ButtonEmoji struct { Animated bool `json:"animated,omitempty"` } -// Type implements the InteractionComponent interface. -func (Button) Type() ComponentType { - return ButtonComponent -} - // MarshalJSON marshals the button in the format Discord expects. -func (b Button) MarshalJSON() ([]byte, error) { - type button Button +func (b ButtonComponent) MarshalJSON() ([]byte, error) { + type button ButtonComponent if b.Style == 0 { b.Style = PrimaryButton // Sane default for button. @@ -88,6 +183,18 @@ func (b Button) MarshalJSON() ([]byte, error) { Type ComponentType `json:"type"` }{ button: button(b), - Type: ButtonComponent, + Type: ButtonComponentType, }) } + +// UnknownComponent is reserved for components with unknown or not yet +// implemented components types. +type UnknownComponent struct { + json.Raw + typ ComponentType +} + +// Type implements the Component Data interface. +func (u UnknownComponent) Type() ComponentType { + return u.typ +} diff --git a/discord/message.go b/discord/message.go index 258833c..f27935e 100644 --- a/discord/message.go +++ b/discord/message.go @@ -72,8 +72,10 @@ type Message struct { Attachments []Attachment `json:"attachments"` // Embeds contains any embedded content. Embeds []Embed `json:"embeds"` - + // Reactions contains any reactions to the message. Reactions []Reaction `json:"reactions,omitempty"` + // Components contains any attached components. + Components []ComponentWrap `json:"components,omitempty"` // Used for validating a message was sent Nonce string `json:"nonce,omitempty"` diff --git a/gateway/events.go b/gateway/events.go index 91f087e..4690cd8 100644 --- a/gateway/events.go +++ b/gateway/events.go @@ -372,14 +372,16 @@ type ( type ( InteractionCreateEvent struct { ID discord.InteractionID `json:"id"` - Type InteractionType `json:"type"` - Data InteractionData `json:"data"` - GuildID discord.GuildID `json:"guild_id"` - ChannelID discord.ChannelID `json:"channel_id"` AppID discord.AppID `json:"application_id"` - Member discord.Member `json:"member"` + Type InteractionType `json:"type"` + Data *InteractionData `json:"data,omitempty"` + GuildID discord.GuildID `json:"guild_id,omitempty"` + ChannelID discord.ChannelID `json:"channel_id,omitempty"` + Member *discord.Member `json:"member,omitempty"` + User *discord.User `json:"user,omitempty"` Token string `json:"token"` Version int `json:"version"` + Message *discord.Message `json:"message"` } ) diff --git a/state/store/defaultstore/message.go b/state/store/defaultstore/message.go index 5fa7b0e..ece745d 100644 --- a/state/store/defaultstore/message.go +++ b/state/store/defaultstore/message.go @@ -164,6 +164,9 @@ func DiffMessage(src discord.Message, dst *discord.Message) { if src.Reactions != nil { dst.Reactions = src.Reactions } + if src.Components != nil { + dst.Components = src.Components + } } func (s *Message) MessageRemove(channelID discord.ChannelID, messageID discord.MessageID) error {