From e6cc7448b76cca7c0ca61b0d5af211a5583221cd Mon Sep 17 00:00:00 2001 From: nitroflap Date: Mon, 17 May 2021 00:55:16 +0300 Subject: [PATCH] Interactions: buttons --- components.go | 97 ++++++++++++++++++++ endpoints.go | 2 +- examples/components/main.go | 151 ++++++++++++++++++++++++++++++++ examples/slash_commands/main.go | 10 +-- interactions.go | 46 ++++++---- message.go | 1 + 6 files changed, 286 insertions(+), 21 deletions(-) create mode 100644 components.go create mode 100644 examples/components/main.go diff --git a/components.go b/components.go new file mode 100644 index 000000000..1f6e6575b --- /dev/null +++ b/components.go @@ -0,0 +1,97 @@ +package discordgo + +import ( + "encoding/json" +) + +// ComponentType is type of component. +type ComponentType uint + +// Component types. +const ( + ActionsRowComponent ComponentType = iota + 1 + ButtonComponent +) + +// Component is a base interface for all components +type Component interface { + json.Marshaler + Type() ComponentType +} + +// ActionsRow is a container for components within one row. +type ActionsRow struct { + Components []Component `json:"components"` +} + +func (r ActionsRow) MarshalJSON() ([]byte, error) { + type actionRow ActionsRow + + return json.Marshal(struct { + actionRow + Type ComponentType `json:"type"` + }{ + actionRow: actionRow(r), + Type: r.Type(), + }) +} + +func (r ActionsRow) Type() ComponentType { + return ActionsRowComponent +} + +// ButtonStyle is style of button. +type ButtonStyle uint + +// Button styles. +const ( + // PrimaryButton is a button with blurple color. + PrimaryButton ButtonStyle = iota + 1 + // SecondaryButton is a button with grey color. + SecondaryButton + // SuccessButton is a button with green color. + SuccessButton + // DangerButton is a button with red color. + DangerButton + // LinkButton is a special type of button which navigates to a URL. Has grey color. + LinkButton +) + +// ButtonEmoji represents button emoji, if it does have one. +type ButtonEmoji struct { + Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` + Animated bool `json:"animated,omitempty"` +} + +// Button represents button component. +type Button struct { + Label string `json:"label"` + Style ButtonStyle `json:"style"` + Disabled bool `json:"disabled"` + Emoji ButtonEmoji `json:"emoji"` + + // NOTE: Only button with LinkButton style can have link. Also, Link is mutually exclusive with CustomID. + Link string `json:"url,omitempty"` + CustomID string `json:"custom_id,omitempty"` +} + +func (b Button) MarshalJSON() ([]byte, error) { + type button Button + + if b.Style == 0 { + b.Style = PrimaryButton + } + + return json.Marshal(struct { + button + Type ComponentType `json:"type"` + }{ + button: button(b), + Type: b.Type(), + }) +} + +func (b Button) Type() ComponentType { + return ButtonComponent +} diff --git a/endpoints.go b/endpoints.go index e5d7d9de5..d3b897d24 100644 --- a/endpoints.go +++ b/endpoints.go @@ -14,7 +14,7 @@ package discordgo import "strconv" // APIVersion is the Discord API version used for the REST and Websocket API. -var APIVersion = "8" +var APIVersion = "9" // Known Discord API Endpoints. var ( diff --git a/examples/components/main.go b/examples/components/main.go new file mode 100644 index 000000000..ae43e5762 --- /dev/null +++ b/examples/components/main.go @@ -0,0 +1,151 @@ +package main + +import ( + "flag" + "github.com/bwmarrin/discordgo" + "log" + "os" + "os/signal" +) + +// Bot parameters +var ( + GuildID = flag.String("guild", "", "Test guild ID") + ChannelID = flag.String("channel", "", "Test channel ID") + BotToken = flag.String("token", "", "Bot access token") + AppID = flag.String("app", "", "Application ID") +) + +var s *discordgo.Session + +func init() { flag.Parse() } + +func init() { + var err error + s, err = discordgo.New("Bot " + *BotToken) + if err != nil { + log.Fatalf("Invalid bot parameters: %v", err) + } +} + +func main() { + s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { + log.Println("Bot is up!") + }) + // Buttons are part of interactions, so we register InteractionCreate handler + s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { + if i.Type == discordgo.InteractionApplicationCommand { + if i.Data.Name == "feedback" { + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Are you satisfied with Buttons?", + // Buttons and other components are specified in Components field. + Components: []discordgo.Component{ + // ActionRow is a container of all buttons in the same raw. + discordgo.ActionsRow{ + Components: []discordgo.Component{ + discordgo.Button{ + Label: "Yes", + Style: discordgo.SuccessButton, + Disabled: false, + CustomID: "yes_btn", + }, + discordgo.Button{ + Label: "No", + Style: discordgo.DangerButton, + Disabled: false, + CustomID: "no_btn", + }, + discordgo.Button{ + Label: "I don't know", + Style: discordgo.LinkButton, + Disabled: false, + // Link buttons doesn't require CustomID and does not trigger the gateway/HTTP event + Link: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + Emoji: discordgo.ButtonEmoji{ + Name: "🤷‍♂️", + }, + }, + }, + }, + // The message may have multiple actions rows. + discordgo.ActionsRow{ + Components: []discordgo.Component{ + discordgo.Button{ + Label: "Discord Developers server", + Style: discordgo.LinkButton, + Disabled: false, + Link: "https://discord.gg/discord-developers", + }, + }, + }, + }, + }, + }) + if err != nil { + panic(err) + } + } + return + } + // Type for button press will be always InteractionButton (3) + if i.Type != discordgo.InteractionButton { + return + } + + content := "Thanks for your feedback " + + // CustomID field contains the same id as when was sent. It's used to identify the which button was clicked. + switch i.Data.CustomID { + case "yes_btn": + content += "(yes)" + case "no_btn": + content += "(no)" + } + + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + // Buttons also may update the message which they was attached to. + // Or may just acknowledge (InteractionResponseDeferredMessageUpdate) that the event was received and not update the message. + // To update it later you need to use interaction response edit endpoint. + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Content: content, + Components: []discordgo.Component{ + discordgo.ActionsRow{ + Components: []discordgo.Component{ + discordgo.Button{ + Label: "Our sponsor", + Style: discordgo.LinkButton, + Disabled: false, + Link: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + Emoji: discordgo.ButtonEmoji{ + Name: "💠", + }, + }, + }, + }, + }, + }, + }) + }) + _, err := s.ApplicationCommandCreate(*AppID, *GuildID, &discordgo.ApplicationCommand{ + Name: "feedback", + Description: "Give your feedback", + }) + + if err != nil { + log.Fatalf("Cannot create slash command: %v", err) + } + + err = s.Open() + if err != nil { + log.Fatalf("Cannot open the session: %v", err) + } + defer s.Close() + + stop := make(chan os.Signal) + signal.Notify(stop, os.Interrupt) + <-stop + log.Println("Graceful shutdown") +} diff --git a/examples/slash_commands/main.go b/examples/slash_commands/main.go index 00255afb2..33d110a54 100644 --- a/examples/slash_commands/main.go +++ b/examples/slash_commands/main.go @@ -163,7 +163,7 @@ var ( "basic-command": func(s *discordgo.Session, i *discordgo.InteractionCreate) { s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionApplicationCommandResponseData{ + Data: &discordgo.InteractionResponseData{ Content: "Hey there! Congratulations, you just executed your first slash command", }, }) @@ -199,7 +199,7 @@ var ( s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ // Ignore type for now, we'll discuss them in "responses" part Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionApplicationCommandResponseData{ + Data: &discordgo.InteractionResponseData{ Content: fmt.Sprintf( msgformat, margs..., @@ -231,7 +231,7 @@ var ( } s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionApplicationCommandResponseData{ + Data: &discordgo.InteractionResponseData{ Content: content, }, }) @@ -273,7 +273,7 @@ var ( err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseType(i.Data.Options[0].IntValue()), - Data: &discordgo.InteractionApplicationCommandResponseData{ + Data: &discordgo.InteractionResponseData{ Content: content, }, }) @@ -306,7 +306,7 @@ var ( s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionApplicationCommandResponseData{ + Data: &discordgo.InteractionResponseData{ // Note: this isn't documented, but you can use that if you want to. // This flag just allows you to create messages visible only for the caller of the command // (user who triggered the command) diff --git a/interactions.go b/interactions.go index 7f631dab0..60a6a1198 100644 --- a/interactions.go +++ b/interactions.go @@ -62,17 +62,22 @@ type InteractionType uint8 // Interaction types const ( - InteractionPing = InteractionType(iota + 1) + InteractionPing InteractionType = iota + 1 InteractionApplicationCommand + InteractionButton ) -// Interaction represents an interaction event created via a slash command. +// Interaction represents data of an interaction. type Interaction struct { - ID string `json:"id"` - Type InteractionType `json:"type"` - Data ApplicationCommandInteractionData `json:"data"` - GuildID string `json:"guild_id"` - ChannelID string `json:"channel_id"` + ID string `json:"id"` + Type InteractionType `json:"type"` + Data InteractionData `json:"data"` + GuildID string `json:"guild_id"` + ChannelID string `json:"channel_id"` + + // The message on which interaction was used. + // NOTE: this field is only filled when the button click interaction triggered. Otherwise it will be nil. + Message *Message `json:"message"` // The member who invoked this interaction. // NOTE: this field is only filled when the slash command was invoked in a guild; @@ -89,11 +94,16 @@ type Interaction struct { Version int `json:"version"` } -// ApplicationCommandInteractionData contains data received in an interaction event. -type ApplicationCommandInteractionData struct { +// Interaction contains data received from InteractionCreate event. +type InteractionData struct { + // Application command ID string `json:"id"` Name string `json:"name"` Options []*ApplicationCommandInteractionDataOption `json:"options"` + + // Components + CustomID string `json:"custom_id"` + ComponentType ComponentType `json:"component_type"` } // ApplicationCommandInteractionDataOption represents an option of a slash command. @@ -238,18 +248,24 @@ const ( // InteractionResponseDeferredChannelMessageWithSource acknowledges that the event was received, and that a follow-up will come later. // It was previously named InteractionResponseACKWithSource. InteractionResponseDeferredChannelMessageWithSource + + // InteractionResponseDeferredMessageUpdate acknowledges that the button click event was received, and message update will come later. + InteractionResponseDeferredMessageUpdate + // InteractionResponseUpdateMessage is for updating the message to which button was attached to. + InteractionResponseUpdateMessage ) // InteractionResponse represents a response for an interaction event. type InteractionResponse struct { - Type InteractionResponseType `json:"type,omitempty"` - Data *InteractionApplicationCommandResponseData `json:"data,omitempty"` + Type InteractionResponseType `json:"type,omitempty"` + Data *InteractionResponseData `json:"data,omitempty"` } -// InteractionApplicationCommandResponseData is response data for a slash command interaction. -type InteractionApplicationCommandResponseData struct { - TTS bool `json:"tts,omitempty"` - Content string `json:"content,omitempty"` +// InteractionResponseData is response data for an interaction. +type InteractionResponseData struct { + TTS bool `json:"tts"` + Content string `json:"content"` + Components []Component `json:"components,omitempty"` Embeds []*MessageEmbed `json:"embeds,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` diff --git a/message.go b/message.go index f1a49c8b5..126b0dfa6 100644 --- a/message.go +++ b/message.go @@ -168,6 +168,7 @@ type MessageSend struct { Content string `json:"content,omitempty"` Embed *MessageEmbed `json:"embed,omitempty"` TTS bool `json:"tts"` + Components []Component `json:"components"` Files []*File `json:"-"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` Reference *MessageReference `json:"message_reference,omitempty"`