1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-11-19 21:32:49 +00:00

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
This commit is contained in:
Scott 2021-05-30 05:28:37 +01:00 committed by diamondburned
parent e04b19eb0f
commit 10c8837000
8 changed files with 162 additions and 40 deletions

View file

@ -8,6 +8,7 @@ import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/arikawa/v2/gateway" "github.com/diamondburned/arikawa/v2/gateway"
"github.com/diamondburned/arikawa/v2/session" "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 .` // 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{ Data: &api.InteractionResponseData{
Content: "This is a message with a button!", Content: "This is a message with a button!",
Components: []discord.Component{ Components: []discord.Component{
discord.ActionRow{ discord.ActionRowComponent{
Components: []discord.Component{ Components: []discord.Component{
discord.Button{ discord.ButtonComponent{
Label: "Hello World!", Label: "Hello World!",
CustomID: "first_button", CustomID: "first_button",
Emoji: &discord.ButtonEmoji{ Emoji: &discord.ButtonEmoji{
@ -45,22 +46,22 @@ func main() {
}, },
Style: discord.PrimaryButton, Style: discord.PrimaryButton,
}, },
discord.Button{ discord.ButtonComponent{
Label: "Secondary", Label: "Secondary",
CustomID: "second_button", CustomID: "second_button",
Style: discord.SecondaryButton, Style: discord.SecondaryButton,
}, },
discord.Button{ discord.ButtonComponent{
Label: "Success", Label: "Success",
CustomID: "success_button", CustomID: "success_button",
Style: discord.SuccessButton, Style: discord.SuccessButton,
}, },
discord.Button{ discord.ButtonComponent{
Label: "Danger", Label: "Danger",
CustomID: "danger_button", CustomID: "danger_button",
Style: discord.DangerButton, Style: discord.DangerButton,
}, },
discord.Button{ discord.ButtonComponent{
Label: "Link", Label: "Link",
URL: "https://google.com", URL: "https://google.com",
Style: discord.LinkButton, Style: discord.LinkButton,
@ -83,7 +84,7 @@ func main() {
data := api.InteractionResponse{ data := api.InteractionResponse{
Type: api.UpdateMessage, Type: api.UpdateMessage,
Data: &api.InteractionResponseData{ Data: &api.InteractionResponseData{
Content: "Custom ID: " + e.Data.CustomID, Content: option.NewNullableString("Custom ID: " + e.Data.CustomID),
}, },
} }

View file

@ -3,18 +3,20 @@ package api
import ( import (
"github.com/diamondburned/arikawa/v2/discord" "github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/arikawa/v2/utils/httputil" "github.com/diamondburned/arikawa/v2/utils/httputil"
"github.com/diamondburned/arikawa/v2/utils/json/option"
) )
var EndpointInteractions = Endpoint + "interactions/" var EndpointInteractions = Endpoint + "interactions/"
type InteractionResponseType uint type InteractionResponseType uint
// https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactioncallbacktype
const ( const (
PongInteraction InteractionResponseType = iota + 1 PongInteraction InteractionResponseType = iota + 1
AcknowledgeInteraction _
MessageInteraction _
MessageInteractionWithSource MessageInteractionWithSource
AcknowledgeInteractionWithSource DeferredMessageInteractionWithSource
DeferredMessageUpdate DeferredMessageUpdate
UpdateMessage UpdateMessage
) )
@ -27,11 +29,11 @@ type InteractionResponse struct {
// InteractionResponseData is InteractionApplicationCommandCallbackData in the // InteractionResponseData is InteractionApplicationCommandCallbackData in the
// official documentation. // official documentation.
type InteractionResponseData struct { type InteractionResponseData struct {
TTS bool `json:"tts"` TTS option.NullableBool `json:"tts,omitempty"`
Content string `json:"content"` Content option.NullableString `json:"content,omitempty"`
Components []discord.Component `json:"components,omitempty"` Components *[]discord.Component `json:"components,omitempty"`
Embeds []discord.Embed `json:"embeds,omitempty"` Embeds *[]discord.Embed `json:"embeds,omitempty"`
AllowedMentions AllowedMentions `json:"allowed_mentions,omitempty"` AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
} }
// RespondInteraction responds to an incoming interaction. It is also known as // RespondInteraction responds to an incoming interaction. It is also known as

View file

@ -291,6 +291,8 @@ type EditMessageData struct {
Content option.NullableString `json:"content,omitempty"` Content option.NullableString `json:"content,omitempty"`
// Embed contains embedded rich content. // Embed contains embedded rich content.
Embed *discord.Embed `json:"embed,omitempty"` 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 are the allowed mentions for a message.
AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
// Flags edits the flags of a message (only SUPPRESS_EMBEDS can currently // Flags edits the flags of a message (only SUPPRESS_EMBEDS can currently

View file

@ -105,6 +105,9 @@ type SendMessageData struct {
// Files is the list of file attachments to be uploaded. To reference a file // Files is the list of file attachments to be uploaded. To reference a file
// in an embed, use (sendpart.File).AttachmentURI(). // in an embed, use (sendpart.File).AttachmentURI().
Files []sendpart.File `json:"-"` 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 are the allowed mentions for a message.
AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`

View file

@ -1,15 +1,70 @@
package discord 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. // ComponentType is the type of a component.
type ComponentType uint type ComponentType uint
const ( const (
ActionRowComponent ComponentType = iota + 1 ActionRowComponentType ComponentType = iota + 1
ButtonComponent 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. // Component is a component that can be attached to an interaction response.
type Component interface { type Component interface {
json.Marshaler json.Marshaler
@ -17,30 +72,70 @@ type Component interface {
} }
// ActionRow is a row of components at the bottom of a message. // ActionRow is a row of components at the bottom of a message.
type ActionRow struct { type ActionRowComponent struct {
Components []Component `json:"components"` Components []Component `json:"components"`
} }
// Type implements the InteractionComponent interface. // Type implements the Component Data interface.
func (ActionRow) Type() ComponentType { func (ActionRowComponent) Type() ComponentType {
return ActionRowComponent return ActionRowComponentType
} }
// MarshalJSON marshals the action row in the format Discord expects. // MarshalJSON marshals the action row in the format Discord expects.
func (a ActionRow) MarshalJSON() ([]byte, error) { func (a ActionRowComponent) MarshalJSON() ([]byte, error) {
type actionRow ActionRow type actionRow ActionRowComponent
return json.Marshal(struct { return json.Marshal(struct {
actionRow actionRow
Type ComponentType `json:"type"` Type ComponentType `json:"type"`
}{ }{
actionRow: actionRow(a), 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. // Button is a clickable button that may be added to an interaction response.
type Button struct { type ButtonComponent struct {
Label string `json:"label"` Label string `json:"label"`
// CustomID attached to InteractionCreate event when clicked. // CustomID attached to InteractionCreate event when clicked.
CustomID string `json:"custom_id"` CustomID string `json:"custom_id"`
@ -51,6 +146,11 @@ type Button struct {
Disabled bool `json:"disabled,omitempty"` 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. // ButtonStyle is the style to display a button in.
type ButtonStyle uint type ButtonStyle uint
@ -70,14 +170,9 @@ type ButtonEmoji struct {
Animated bool `json:"animated,omitempty"` 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. // MarshalJSON marshals the button in the format Discord expects.
func (b Button) MarshalJSON() ([]byte, error) { func (b ButtonComponent) MarshalJSON() ([]byte, error) {
type button Button type button ButtonComponent
if b.Style == 0 { if b.Style == 0 {
b.Style = PrimaryButton // Sane default for button. b.Style = PrimaryButton // Sane default for button.
@ -88,6 +183,18 @@ func (b Button) MarshalJSON() ([]byte, error) {
Type ComponentType `json:"type"` Type ComponentType `json:"type"`
}{ }{
button: button(b), 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
}

View file

@ -72,8 +72,10 @@ type Message struct {
Attachments []Attachment `json:"attachments"` Attachments []Attachment `json:"attachments"`
// Embeds contains any embedded content. // Embeds contains any embedded content.
Embeds []Embed `json:"embeds"` Embeds []Embed `json:"embeds"`
// Reactions contains any reactions to the message.
Reactions []Reaction `json:"reactions,omitempty"` Reactions []Reaction `json:"reactions,omitempty"`
// Components contains any attached components.
Components []ComponentWrap `json:"components,omitempty"`
// Used for validating a message was sent // Used for validating a message was sent
Nonce string `json:"nonce,omitempty"` Nonce string `json:"nonce,omitempty"`

View file

@ -372,14 +372,16 @@ type (
type ( type (
InteractionCreateEvent struct { InteractionCreateEvent struct {
ID discord.InteractionID `json:"id"` 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"` 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"` Token string `json:"token"`
Version int `json:"version"` Version int `json:"version"`
Message *discord.Message `json:"message"`
} }
) )

View file

@ -164,6 +164,9 @@ func DiffMessage(src discord.Message, dst *discord.Message) {
if src.Reactions != nil { if src.Reactions != nil {
dst.Reactions = src.Reactions dst.Reactions = src.Reactions
} }
if src.Components != nil {
dst.Components = src.Components
}
} }
func (s *Message) MessageRemove(channelID discord.ChannelID, messageID discord.MessageID) error { func (s *Message) MessageRemove(channelID discord.ChannelID, messageID discord.MessageID) error {