From 8c70cbfbaff7dde2eea6fde87f1a14f8126355bc Mon Sep 17 00:00:00 2001 From: Fedor Lapshin <42876964+FedorLap2006@users.noreply.github.com> Date: Sun, 27 Jun 2021 19:16:52 +0300 Subject: [PATCH 01/39] Interactions: the Buttons (#933) * Interactions: buttons * Doc fix * Gofmt fix * Fix typo * Remaking interaction data into interface * Godoc fix * Gofmt fix * Godoc fix * InteractionData helper functions and some fixes in slash commands example * Fix components example * Yet another fix of components example * Fix interaction unmarshaling * Gofmt fix * Godoc fix * Gofmt fix * Corrected naming and docs * Rolled back API version * Requested fixes * Added support of components to webhook and regular messages * Fix components unmarshaling * Godoc fix * Requested fixes * Fixed unmarshaling issues * Components example: cleanup * Added components tracking to state * Requested fixes * Renaming fix * Remove more named returns * Minor English fixes Co-authored-by: Carson Hoffman --- components.go | 97 ++++++++++++++++++++ endpoints.go | 2 +- events.go | 20 +++++ examples/components/main.go | 152 ++++++++++++++++++++++++++++++++ examples/slash_commands/main.go | 40 ++++----- interactions.go | 66 +++++++------- message.go | 25 ++++++ state.go | 3 + webhook.go | 2 + 9 files changed, 356 insertions(+), 51 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/events.go b/events.go index 87ad7d9aa..a1647fa20 100644 --- a/events.go +++ b/events.go @@ -162,6 +162,11 @@ type MessageCreate struct { *Message } +// UnmarshalJSON is a helper function to unmarshal MessageCreate object. +func (m *MessageCreate) UnmarshalJSON(b []byte) error { + return json.Unmarshal(b, &m.Message) +} + // MessageUpdate is the data for a MessageUpdate event. type MessageUpdate struct { *Message @@ -169,12 +174,22 @@ type MessageUpdate struct { BeforeUpdate *Message `json:"-"` } +// UnmarshalJSON is a helper function to unmarshal MessageUpdate object. +func (m *MessageUpdate) UnmarshalJSON(b []byte) error { + return json.Unmarshal(b, &m.Message) +} + // MessageDelete is the data for a MessageDelete event. type MessageDelete struct { *Message BeforeDelete *Message `json:"-"` } +// UnmarshalJSON is a helper function to unmarshal MessageDelete object. +func (m *MessageDelete) UnmarshalJSON(b []byte) error { + return json.Unmarshal(b, &m.Message) +} + // MessageReactionAdd is the data for a MessageReactionAdd event. type MessageReactionAdd struct { *MessageReaction @@ -272,3 +287,8 @@ type WebhooksUpdate struct { type InteractionCreate struct { *Interaction } + +// UnmarshalJSON is a helper function to unmarshal Interaction object. +func (i *InteractionCreate) UnmarshalJSON(b []byte) error { + return json.Unmarshal(b, &i.Interaction) +} diff --git a/examples/components/main.go b/examples/components/main.go new file mode 100644 index 000000000..37b06fe0a --- /dev/null +++ b/examples/components/main.go @@ -0,0 +1,152 @@ +package main + +import ( + "flag" + "log" + "os" + "os/signal" + + "github.com/bwmarrin/discordgo" +) + +// 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 e615fec62..f01df153c 100644 --- a/examples/slash_commands/main.go +++ b/examples/slash_commands/main.go @@ -155,7 +155,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", }, }) @@ -166,9 +166,9 @@ var ( // Also, as you can see, here is used utility functions to convert the value // to particular type. Yeah, you can use just switch type, // but this is much simpler - i.Data.Options[0].StringValue(), - i.Data.Options[1].IntValue(), - i.Data.Options[2].BoolValue(), + i.ApplicationCommandData().Options[0].StringValue(), + i.ApplicationCommandData().Options[1].IntValue(), + i.ApplicationCommandData().Options[2].BoolValue(), } msgformat := ` Now you just learned how to use command options. Take a look to the value of which you've just entered: @@ -176,22 +176,22 @@ var ( > integer_option: %d > bool_option: %v ` - if len(i.Data.Options) >= 4 { - margs = append(margs, i.Data.Options[3].ChannelValue(nil).ID) + if len(i.ApplicationCommandData().Options) >= 4 { + margs = append(margs, i.ApplicationCommandData().Options[3].ChannelValue(nil).ID) msgformat += "> channel-option: <#%s>\n" } - if len(i.Data.Options) >= 5 { - margs = append(margs, i.Data.Options[4].UserValue(nil).ID) + if len(i.ApplicationCommandData().Options) >= 5 { + margs = append(margs, i.ApplicationCommandData().Options[4].UserValue(nil).ID) msgformat += "> user-option: <@%s>\n" } - if len(i.Data.Options) >= 6 { - margs = append(margs, i.Data.Options[5].RoleValue(nil, "").ID) + if len(i.ApplicationCommandData().Options) >= 6 { + margs = append(margs, i.ApplicationCommandData().Options[5].RoleValue(nil, "").ID) msgformat += "> role-option: <@&%s>\n" } 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..., @@ -204,15 +204,15 @@ var ( // As you can see, the name of subcommand (nested, top-level) or subcommand group // is provided through arguments. - switch i.Data.Options[0].Name { + switch i.ApplicationCommandData().Options[0].Name { case "subcmd": content = "The top-level subcommand is executed. Now try to execute the nested one." default: - if i.Data.Options[0].Name != "scmd-grp" { + if i.ApplicationCommandData().Options[0].Name != "scmd-grp" { return } - switch i.Data.Options[0].Options[0].Name { + switch i.ApplicationCommandData().Options[0].Options[0].Name { case "nst-subcmd": content = "Nice, now you know how to execute nested commands too" default: @@ -223,7 +223,7 @@ var ( } s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionApplicationCommandResponseData{ + Data: &discordgo.InteractionResponseData{ Content: content, }, }) @@ -238,7 +238,7 @@ var ( content := "" // As you can see, the response type names used here are pretty self-explanatory, // but for those who want more information see the official documentation - switch i.Data.Options[0].IntValue() { + switch i.ApplicationCommandData().Options[0].IntValue() { case int64(discordgo.InteractionResponseChannelMessageWithSource): content = "You just responded to an interaction, sent a message and showed the original one. " + @@ -247,7 +247,7 @@ var ( "\nAlso... you can edit your response, wait 5 seconds and this message will be changed" default: err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseType(i.Data.Options[0].IntValue()), + Type: discordgo.InteractionResponseType(i.ApplicationCommandData().Options[0].IntValue()), }) if err != nil { s.FollowupMessageCreate(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{ @@ -259,7 +259,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, }, }) @@ -292,7 +292,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) @@ -330,7 +330,7 @@ var ( func init() { s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { - if h, ok := commandHandlers[i.Data.Name]; ok { + if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { h(s, i) } }) diff --git a/interactions.go b/interactions.go index 3c277609a..fe2c5c1c7 100644 --- a/interactions.go +++ b/interactions.go @@ -63,17 +63,22 @@ type InteractionType uint8 // Interaction types const ( - InteractionPing InteractionType = 1 - InteractionApplicationCommand InteractionType = 2 + 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; @@ -90,22 +95,16 @@ type Interaction struct { Version int `json:"version"` } -// ApplicationCommandInteractionData contains data received in an interaction event. -type ApplicationCommandInteractionData struct { - ID string `json:"id"` - Name string `json:"name"` - Resolved *ApplicationCommandInteractionDataResolved `json:"resolved"` - Options []*ApplicationCommandInteractionDataOption `json:"options"` -} +// Interaction contains data received from InteractionCreate event. +type InteractionData struct { + // Application command + ID string `json:"id"` + Name string `json:"name"` + Options []*ApplicationCommandInteractionDataOption `json:"options"` -// ApplicationCommandInteractionDataResolved contains resolved data for command arguments. -// Partial Member objects are missing user, deaf and mute fields. -// Partial Channel objects only have id, name, type and permissions fields. -type ApplicationCommandInteractionDataResolved struct { - Users map[string]*User `json:"users"` - Members map[string]*Member `json:"members"` - Roles map[string]*Role `json:"roles"` - Channels map[string]*Channel `json:"channels"` + // Components + CustomID string `json:"custom_id"` + ComponentType ComponentType `json:"component_type"` } // ApplicationCommandInteractionDataOption represents an option of a slash command. @@ -242,19 +241,26 @@ const ( // InteractionResponseChannelMessageWithSource is for responding with a message, showing the user's input. InteractionResponseChannelMessageWithSource InteractionResponseType = 4 // InteractionResponseDeferredChannelMessageWithSource acknowledges that the event was received, and that a follow-up will come later. - InteractionResponseDeferredChannelMessageWithSource InteractionResponseType = 5 + // 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 c4445e6fc..8349e655f 100644 --- a/message.go +++ b/message.go @@ -10,6 +10,7 @@ package discordgo import ( + "encoding/json" "io" "regexp" "strings" @@ -80,6 +81,9 @@ type Message struct { // A list of attachments present in the message. Attachments []*MessageAttachment `json:"attachments"` + // A list of components attached to the message. + Components []MessageComponent `json:"-"` + // A list of embeds present in the message. Multiple // embeds can currently only be sent by webhooks. Embeds []*MessageEmbed `json:"embeds"` @@ -125,6 +129,25 @@ type Message struct { Flags MessageFlags `json:"flags"` } +// UnmarshalJSON is a helper function to unmarshal the Message. +func (m *Message) UnmarshalJSON(data []byte) error { + type message Message + var v struct { + message + RawComponents []unmarshalableMessageComponent `json:"components"` + } + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + *m = Message(v.message) + m.Components = make([]MessageComponent, len(v.RawComponents)) + for i, v := range v.RawComponents { + m.Components[i] = v.MessageComponent + } + return err +} + // GetCustomEmojis pulls out all the custom (Non-unicode) emojis from a message and returns a Slice of the Emoji struct. func (m *Message) GetCustomEmojis() []*Emoji { var toReturn []*Emoji @@ -168,6 +191,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"` @@ -180,6 +204,7 @@ type MessageSend struct { // is also where you should get the instance from. type MessageEdit struct { Content *string `json:"content,omitempty"` + Components []MessageComponent `json:"components"` Embed *MessageEmbed `json:"embed,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` diff --git a/state.go b/state.go index 2eeabd802..698612e79 100644 --- a/state.go +++ b/state.go @@ -655,6 +655,9 @@ func (s *State) MessageAdd(message *Message) error { if message.Author != nil { m.Author = message.Author } + if message.Components != nil { + m.Components = message.Components + } return nil } diff --git a/webhook.go b/webhook.go index 6c3d2df80..d61b2817a 100644 --- a/webhook.go +++ b/webhook.go @@ -32,6 +32,7 @@ type WebhookParams struct { AvatarURL string `json:"avatar_url,omitempty"` TTS bool `json:"tts,omitempty"` Files []*File `json:"-"` + Components []MessageComponent `json:"components"` Embeds []*MessageEmbed `json:"embeds,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` } @@ -39,6 +40,7 @@ type WebhookParams struct { // WebhookEdit stores data for editing of a webhook message. type WebhookEdit struct { Content string `json:"content,omitempty"` + Components []MessageComponent `json:"components"` Embeds []*MessageEmbed `json:"embeds,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` } From 6e7c007e66f3e4071210f1941221127f99dda5af Mon Sep 17 00:00:00 2001 From: nitroflap Date: Mon, 17 May 2021 01:18:31 +0300 Subject: [PATCH 02/39] Doc fix --- components.go | 7 +++++-- interactions.go | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/components.go b/components.go index 1f6e6575b..e727b11b2 100644 --- a/components.go +++ b/components.go @@ -13,7 +13,7 @@ const ( ButtonComponent ) -// Component is a base interface for all components +// Component is a base interface for all components. type Component interface { json.Marshaler Type() ComponentType @@ -23,7 +23,7 @@ type Component interface { type ActionsRow struct { Components []Component `json:"components"` } - +// MarshalJSON is a method for marshaling ActionsRow to a JSON object. func (r ActionsRow) MarshalJSON() ([]byte, error) { type actionRow ActionsRow @@ -36,6 +36,7 @@ func (r ActionsRow) MarshalJSON() ([]byte, error) { }) } +// Type is a method to get the type of a component. func (r ActionsRow) Type() ComponentType { return ActionsRowComponent } @@ -76,6 +77,7 @@ type Button struct { CustomID string `json:"custom_id,omitempty"` } +// MarshalJSON is a method for marshaling Button to a JSON object. func (b Button) MarshalJSON() ([]byte, error) { type button Button @@ -92,6 +94,7 @@ func (b Button) MarshalJSON() ([]byte, error) { }) } +// Type is a method to get the type of a component. func (b Button) Type() ComponentType { return ButtonComponent } diff --git a/interactions.go b/interactions.go index fe2c5c1c7..6c0f10be5 100644 --- a/interactions.go +++ b/interactions.go @@ -95,7 +95,7 @@ type Interaction struct { Version int `json:"version"` } -// Interaction contains data received from InteractionCreate event. +// InteractionData contains data received from InteractionCreate event. type InteractionData struct { // Application command ID string `json:"id"` From 4979e3c7b1b1fd600f0275d3886ab50f667c9975 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Mon, 17 May 2021 01:29:46 +0300 Subject: [PATCH 03/39] Gofmt fix --- components.go | 1 + 1 file changed, 1 insertion(+) diff --git a/components.go b/components.go index e727b11b2..e87f86eef 100644 --- a/components.go +++ b/components.go @@ -23,6 +23,7 @@ type Component interface { type ActionsRow struct { Components []Component `json:"components"` } + // MarshalJSON is a method for marshaling ActionsRow to a JSON object. func (r ActionsRow) MarshalJSON() ([]byte, error) { type actionRow ActionsRow From 05b6b55e9cff99dc362c89f65b946cd8cbd87099 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Mon, 17 May 2021 10:37:33 +0300 Subject: [PATCH 04/39] Fix typo --- examples/components/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/components/main.go b/examples/components/main.go index 37b06fe0a..4b1b390f1 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -43,7 +43,7 @@ func main() { 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. + // ActionRow is a container of all buttons within the same row. discordgo.ActionsRow{ Components: []discordgo.Component{ discordgo.Button{ From 873831328e0b7ede1a8fcf077de1f1c33c452b70 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Tue, 18 May 2021 18:21:07 +0300 Subject: [PATCH 05/39] Remaking interaction data into interface --- interactions.go | 53 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/interactions.go b/interactions.go index 6c0f10be5..5d6b6ec1a 100644 --- a/interactions.go +++ b/interactions.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/ed25519" "encoding/hex" + "encoding/json" "io" "io/ioutil" "net/http" @@ -95,18 +96,62 @@ type Interaction struct { Version int `json:"version"` } -// InteractionData contains data received from InteractionCreate event. -type InteractionData struct { - // Application command +type rawInteraction struct { + Type InteractionType `json:"type"` + Data json.RawMessage `json:"data"` +} + +// UnmarshalJSON is a method for unmarshalling JSON object to Interaction. +func (i *Interaction) UnmarshalJSON(raw []byte) (err error) { + var tmp rawInteraction + err = json.Unmarshal(raw, &tmp) + if err != nil { + return + } + + switch i.Type { + case InteractionApplicationCommand: + v := ApplicationCommandInteractionData{} + err = json.Unmarshal(tmp.Data, &v) + if err != nil { + return + } + i.Data = v + case InteractionButton: + v := ButtonInteractionData{} + err = json.Unmarshal(tmp.Data, &v) + if err != nil { + return + } + i.Data = v + } + return nil +} + +type InteractionData interface { + Type() InteractionType +} + +// ApplicationCommandInteractionData contains the data of application command interaction. +type ApplicationCommandInteractionData struct { ID string `json:"id"` Name string `json:"name"` Options []*ApplicationCommandInteractionDataOption `json:"options"` +} - // Components +func (ApplicationCommandInteractionData) Type() InteractionType { + return InteractionApplicationCommand +} + +type ButtonInteractionData struct { CustomID string `json:"custom_id"` ComponentType ComponentType `json:"component_type"` } +func (ButtonInteractionData) Type() InteractionType { + return InteractionButton +} + // ApplicationCommandInteractionDataOption represents an option of a slash command. type ApplicationCommandInteractionDataOption struct { Name string `json:"name"` From 59bc781f50f6414773e1a627962d5f7e16462e40 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Tue, 18 May 2021 18:30:21 +0300 Subject: [PATCH 06/39] Godoc fix --- interactions.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interactions.go b/interactions.go index 5d6b6ec1a..a9a080a47 100644 --- a/interactions.go +++ b/interactions.go @@ -128,6 +128,7 @@ func (i *Interaction) UnmarshalJSON(raw []byte) (err error) { return nil } +// Interaction is a common interface for all types of interaction data. type InteractionData interface { Type() InteractionType } @@ -139,6 +140,8 @@ type ApplicationCommandInteractionData struct { Options []*ApplicationCommandInteractionDataOption `json:"options"` } + +// Type returns the type of interaction data. func (ApplicationCommandInteractionData) Type() InteractionType { return InteractionApplicationCommand } @@ -148,6 +151,7 @@ type ButtonInteractionData struct { ComponentType ComponentType `json:"component_type"` } +// Type returns the type of interaction data. func (ButtonInteractionData) Type() InteractionType { return InteractionButton } From 8299946567d9c73affa93634efefd8e239d88618 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Tue, 18 May 2021 18:31:27 +0300 Subject: [PATCH 07/39] Gofmt fix --- interactions.go | 1 - 1 file changed, 1 deletion(-) diff --git a/interactions.go b/interactions.go index a9a080a47..d7ed8decb 100644 --- a/interactions.go +++ b/interactions.go @@ -140,7 +140,6 @@ type ApplicationCommandInteractionData struct { Options []*ApplicationCommandInteractionDataOption `json:"options"` } - // Type returns the type of interaction data. func (ApplicationCommandInteractionData) Type() InteractionType { return InteractionApplicationCommand From 64e23e0a88656b0c392874f042fb627d56783df5 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Tue, 18 May 2021 19:00:53 +0300 Subject: [PATCH 08/39] Godoc fix --- interactions.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interactions.go b/interactions.go index d7ed8decb..ddba8e5b1 100644 --- a/interactions.go +++ b/interactions.go @@ -128,7 +128,7 @@ func (i *Interaction) UnmarshalJSON(raw []byte) (err error) { return nil } -// Interaction is a common interface for all types of interaction data. +// InteractionData is a common interface for all types of interaction data. type InteractionData interface { Type() InteractionType } @@ -145,6 +145,7 @@ func (ApplicationCommandInteractionData) Type() InteractionType { return InteractionApplicationCommand } +// ButtonInteractionData contains the data of button interaction. type ButtonInteractionData struct { CustomID string `json:"custom_id"` ComponentType ComponentType `json:"component_type"` From b51ca56f7fdfb5b8511cdfde902ed8e636d08b52 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Tue, 18 May 2021 20:59:06 +0300 Subject: [PATCH 09/39] InteractionData helper functions and some fixes in slash commands example --- examples/slash_commands/main.go | 2 +- interactions.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/examples/slash_commands/main.go b/examples/slash_commands/main.go index f01df153c..57d27ca5e 100644 --- a/examples/slash_commands/main.go +++ b/examples/slash_commands/main.go @@ -258,7 +258,7 @@ var ( } err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseType(i.Data.Options[0].IntValue()), + Type: discordgo.InteractionResponseType(i.ApplicationCommandData().Options[0].IntValue()), Data: &discordgo.InteractionResponseData{ Content: content, }, diff --git a/interactions.go b/interactions.go index ddba8e5b1..95305a26b 100644 --- a/interactions.go +++ b/interactions.go @@ -128,6 +128,22 @@ func (i *Interaction) UnmarshalJSON(raw []byte) (err error) { return nil } +// ButtonData is helper function to convert InteractionData to ButtonInteractionData. +func (i Interaction) ButtonData() (data ButtonInteractionData) { + if i.Type != InteractionButton { + return + } + return i.Data.(ButtonInteractionData) +} + +// ApplicationCommandData is helper function to convert InteractionData to ApplicationCommandInteractionData. +func (i Interaction) ApplicationCommandData() (data ApplicationCommandInteractionData) { + if i.Type != InteractionApplicationCommand { + return + } + return i.Data.(ApplicationCommandInteractionData) +} + // InteractionData is a common interface for all types of interaction data. type InteractionData interface { Type() InteractionType From 215311922ffabb8358a3e13dddb6d85c597791cb Mon Sep 17 00:00:00 2001 From: nitroflap Date: Tue, 18 May 2021 21:22:02 +0300 Subject: [PATCH 10/39] Fix components example --- examples/components/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/components/main.go b/examples/components/main.go index 4b1b390f1..15a71117f 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -36,7 +36,7 @@ func main() { // 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" { + if i.ButtonData().Name == "feedback" { err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ @@ -98,7 +98,7 @@ func main() { 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 { + switch i.ButtonData().CustomID { case "yes_btn": content += "(yes)" case "no_btn": From 274b9ecbf6c63689574d6286824d5a6f2a3ca8dd Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 19 May 2021 01:47:50 +0300 Subject: [PATCH 11/39] Yet another fix of components example --- examples/components/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/components/main.go b/examples/components/main.go index 15a71117f..67de0b57a 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -36,7 +36,7 @@ func main() { // 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.ButtonData().Name == "feedback" { + if i.ApplicationCommandData().Name == "feedback" { err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ From c2751aab48f6c0d6a2cb58b6957f834aa8cf21bf Mon Sep 17 00:00:00 2001 From: nitroflap Date: Fri, 21 May 2021 17:40:59 +0300 Subject: [PATCH 12/39] Fix interaction unmarshaling --- events.go | 2 +- interactions.go | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/events.go b/events.go index a1647fa20..0544468a7 100644 --- a/events.go +++ b/events.go @@ -288,7 +288,7 @@ type InteractionCreate struct { *Interaction } -// UnmarshalJSON is a helper function to unmarshal Interaction object. func (i *InteractionCreate) UnmarshalJSON(b []byte) error { + i.Interaction = new(Interaction) return json.Unmarshal(b, &i.Interaction) } diff --git a/interactions.go b/interactions.go index 95305a26b..3cc09d404 100644 --- a/interactions.go +++ b/interactions.go @@ -73,7 +73,7 @@ const ( type Interaction struct { ID string `json:"id"` Type InteractionType `json:"type"` - Data InteractionData `json:"data"` + Data InteractionData `json:"-"` GuildID string `json:"guild_id"` ChannelID string `json:"channel_id"` @@ -96,8 +96,10 @@ type Interaction struct { Version int `json:"version"` } +type interaction Interaction + type rawInteraction struct { - Type InteractionType `json:"type"` + interaction Data json.RawMessage `json:"data"` } @@ -109,7 +111,9 @@ func (i *Interaction) UnmarshalJSON(raw []byte) (err error) { return } - switch i.Type { + *i = Interaction(tmp.interaction) + + switch tmp.Type { case InteractionApplicationCommand: v := ApplicationCommandInteractionData{} err = json.Unmarshal(tmp.Data, &v) From 641b43dea9c098ab941345dcc0d4ed0bd03d7677 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Fri, 21 May 2021 17:59:21 +0300 Subject: [PATCH 13/39] Godoc fix --- events.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/events.go b/events.go index 0544468a7..a849ddabe 100644 --- a/events.go +++ b/events.go @@ -287,7 +287,9 @@ type WebhooksUpdate struct { type InteractionCreate struct { *Interaction } - +// UnmarshalJSON is a helper function to unmarshal Interaction object. +// Since it's a pointer json.Unmarshal does not unmarshals it correctly (Interaction field is nil). +// And so we need to unmarshal it manually. func (i *InteractionCreate) UnmarshalJSON(b []byte) error { i.Interaction = new(Interaction) return json.Unmarshal(b, &i.Interaction) From b54bdb4bcde4c92bf6c8aa29ee69d154c21074cc Mon Sep 17 00:00:00 2001 From: nitroflap Date: Mon, 24 May 2021 02:45:03 +0300 Subject: [PATCH 14/39] Gofmt fix --- events.go | 1 + 1 file changed, 1 insertion(+) diff --git a/events.go b/events.go index a849ddabe..34d6a29f1 100644 --- a/events.go +++ b/events.go @@ -287,6 +287,7 @@ type WebhooksUpdate struct { type InteractionCreate struct { *Interaction } + // UnmarshalJSON is a helper function to unmarshal Interaction object. // Since it's a pointer json.Unmarshal does not unmarshals it correctly (Interaction field is nil). // And so we need to unmarshal it manually. From 81384d555e4e174ac293330272782e4c1dfd0164 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Thu, 27 May 2021 01:13:16 +0300 Subject: [PATCH 15/39] Corrected naming and docs --- components.go | 8 ++++---- examples/components/main.go | 16 ++++++++-------- interactions.go | 36 +++++++++++++++++------------------- message.go | 2 +- 4 files changed, 30 insertions(+), 32 deletions(-) diff --git a/components.go b/components.go index e87f86eef..527cc2b7b 100644 --- a/components.go +++ b/components.go @@ -7,21 +7,21 @@ import ( // ComponentType is type of component. type ComponentType uint -// Component types. +// MessageComponent types. const ( ActionsRowComponent ComponentType = iota + 1 ButtonComponent ) -// Component is a base interface for all components. -type Component interface { +// MessageComponent is a base interface for all message components. +type MessageComponent interface { json.Marshaler Type() ComponentType } // ActionsRow is a container for components within one row. type ActionsRow struct { - Components []Component `json:"components"` + Components []MessageComponent `json:"components"` } // MarshalJSON is a method for marshaling ActionsRow to a JSON object. diff --git a/examples/components/main.go b/examples/components/main.go index 67de0b57a..bc9201213 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -42,10 +42,10 @@ func main() { Data: &discordgo.InteractionResponseData{ Content: "Are you satisfied with Buttons?", // Buttons and other components are specified in Components field. - Components: []discordgo.Component{ + Components: []discordgo.MessageComponent{ // ActionRow is a container of all buttons within the same row. discordgo.ActionsRow{ - Components: []discordgo.Component{ + Components: []discordgo.MessageComponent{ discordgo.Button{ Label: "Yes", Style: discordgo.SuccessButton, @@ -65,14 +65,14 @@ func main() { // 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: "🤷‍♂ī¸", + Name: "🤷", }, }, }, }, // The message may have multiple actions rows. discordgo.ActionsRow{ - Components: []discordgo.Component{ + Components: []discordgo.MessageComponent{ discordgo.Button{ Label: "Discord Developers server", Style: discordgo.LinkButton, @@ -91,14 +91,14 @@ func main() { return } // Type for button press will be always InteractionButton (3) - if i.Type != discordgo.InteractionButton { + if i.Type != discordgo.InteractionMessageComponent { 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.ButtonData().CustomID { + switch i.MessageComponentData().CustomID { case "yes_btn": content += "(yes)" case "no_btn": @@ -112,9 +112,9 @@ func main() { Type: discordgo.InteractionResponseUpdateMessage, Data: &discordgo.InteractionResponseData{ Content: content, - Components: []discordgo.Component{ + Components: []discordgo.MessageComponent{ discordgo.ActionsRow{ - Components: []discordgo.Component{ + Components: []discordgo.MessageComponent{ discordgo.Button{ Label: "Our sponsor", Style: discordgo.LinkButton, diff --git a/interactions.go b/interactions.go index 3cc09d404..1f44801b8 100644 --- a/interactions.go +++ b/interactions.go @@ -66,7 +66,7 @@ type InteractionType uint8 const ( InteractionPing InteractionType = iota + 1 InteractionApplicationCommand - InteractionButton + InteractionMessageComponent ) // Interaction represents data of an interaction. @@ -121,8 +121,8 @@ func (i *Interaction) UnmarshalJSON(raw []byte) (err error) { return } i.Data = v - case InteractionButton: - v := ButtonInteractionData{} + case InteractionMessageComponent: + v := MessageComponentInteractionData{} err = json.Unmarshal(tmp.Data, &v) if err != nil { return @@ -132,12 +132,12 @@ func (i *Interaction) UnmarshalJSON(raw []byte) (err error) { return nil } -// ButtonData is helper function to convert InteractionData to ButtonInteractionData. -func (i Interaction) ButtonData() (data ButtonInteractionData) { - if i.Type != InteractionButton { +// MessageComponentData is helper function to convert InteractionData to MessageComponentInteractionData. +func (i Interaction) MessageComponentData() (data MessageComponentInteractionData) { + if i.Type != InteractionMessageComponent { return } - return i.Data.(ButtonInteractionData) + return i.Data.(MessageComponentInteractionData) } // ApplicationCommandData is helper function to convert InteractionData to ApplicationCommandInteractionData. @@ -165,15 +165,15 @@ func (ApplicationCommandInteractionData) Type() InteractionType { return InteractionApplicationCommand } -// ButtonInteractionData contains the data of button interaction. -type ButtonInteractionData struct { +// MessageComponentInteractionData contains the data of message component interaction. +type MessageComponentInteractionData struct { CustomID string `json:"custom_id"` ComponentType ComponentType `json:"component_type"` } // Type returns the type of interaction data. -func (ButtonInteractionData) Type() InteractionType { - return InteractionButton +func (MessageComponentInteractionData) Type() InteractionType { + return InteractionMessageComponent } // ApplicationCommandInteractionDataOption represents an option of a slash command. @@ -310,13 +310,11 @@ const ( // InteractionResponseChannelMessageWithSource is for responding with a message, showing the user's input. InteractionResponseChannelMessageWithSource InteractionResponseType = 4 // 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 + InteractionResponseDeferredChannelMessageWithSource InteractionResponseType = 5 + // InteractionResponseDeferredMessageUpdate acknowledges that the message component interaction event was received, and message will be updated later. + InteractionResponseDeferredMessageUpdate InteractionResponseType = 6 + // InteractionResponseUpdateMessage is for updating the message to which message component was attached to. + InteractionResponseUpdateMessage InteractionResponseType = 7 ) // InteractionResponse represents a response for an interaction event. @@ -329,7 +327,7 @@ type InteractionResponse struct { type InteractionResponseData struct { TTS bool `json:"tts"` Content string `json:"content"` - Components []Component `json:"components,omitempty"` + Components []MessageComponent `json:"components,omitempty"` Embeds []*MessageEmbed `json:"embeds,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` diff --git a/message.go b/message.go index 8349e655f..b8cd8e4fe 100644 --- a/message.go +++ b/message.go @@ -191,7 +191,7 @@ type MessageSend struct { Content string `json:"content,omitempty"` Embed *MessageEmbed `json:"embed,omitempty"` TTS bool `json:"tts"` - Components []Component `json:"components"` + Components []MessageComponent `json:"components"` Files []*File `json:"-"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` Reference *MessageReference `json:"message_reference,omitempty"` From 5cb816c1d21100a7e59cdbbf5f5dfa41e4fe8e29 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Sun, 30 May 2021 12:49:03 +0300 Subject: [PATCH 16/39] Rolled back API version --- endpoints.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoints.go b/endpoints.go index d3b897d24..e5d7d9de5 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 = "9" +var APIVersion = "8" // Known Discord API Endpoints. var ( From 5b150bd6ef62b4dec86c25068cd0e672b39d3eb0 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Sun, 30 May 2021 12:49:42 +0300 Subject: [PATCH 17/39] Requested fixes --- components.go | 14 +++++++------- interactions.go | 6 ------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/components.go b/components.go index 527cc2b7b..9c4ca99bd 100644 --- a/components.go +++ b/components.go @@ -9,8 +9,8 @@ type ComponentType uint // MessageComponent types. const ( - ActionsRowComponent ComponentType = iota + 1 - ButtonComponent + ActionsRowComponent ComponentType = 1 + ButtonComponent ComponentType = 2 ) // MessageComponent is a base interface for all message components. @@ -48,15 +48,15 @@ type ButtonStyle uint // Button styles. const ( // PrimaryButton is a button with blurple color. - PrimaryButton ButtonStyle = iota + 1 + PrimaryButton ButtonStyle = 1 // SecondaryButton is a button with grey color. - SecondaryButton + SecondaryButton ButtonStyle = 2 // SuccessButton is a button with green color. - SuccessButton + SuccessButton ButtonStyle = 3 // DangerButton is a button with red color. - DangerButton + DangerButton ButtonStyle = 4 // LinkButton is a special type of button which navigates to a URL. Has grey color. - LinkButton + LinkButton ButtonStyle = 5 ) // ButtonEmoji represents button emoji, if it does have one. diff --git a/interactions.go b/interactions.go index 1f44801b8..13786940c 100644 --- a/interactions.go +++ b/interactions.go @@ -134,17 +134,11 @@ func (i *Interaction) UnmarshalJSON(raw []byte) (err error) { // MessageComponentData is helper function to convert InteractionData to MessageComponentInteractionData. func (i Interaction) MessageComponentData() (data MessageComponentInteractionData) { - if i.Type != InteractionMessageComponent { - return - } return i.Data.(MessageComponentInteractionData) } // ApplicationCommandData is helper function to convert InteractionData to ApplicationCommandInteractionData. func (i Interaction) ApplicationCommandData() (data ApplicationCommandInteractionData) { - if i.Type != InteractionApplicationCommand { - return - } return i.Data.(ApplicationCommandInteractionData) } From c0d6117524caf027e738696c4baa9947930fd370 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 16 Jun 2021 01:04:32 +0300 Subject: [PATCH 18/39] Added support of components to webhook and regular messages --- interactions.go | 2 +- message.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interactions.go b/interactions.go index 13786940c..a8798ec49 100644 --- a/interactions.go +++ b/interactions.go @@ -321,7 +321,7 @@ type InteractionResponse struct { type InteractionResponseData struct { TTS bool `json:"tts"` Content string `json:"content"` - Components []MessageComponent `json:"components,omitempty"` + Components []MessageComponent `json:"components"` Embeds []*MessageEmbed `json:"embeds,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` diff --git a/message.go b/message.go index b8cd8e4fe..ec37419d8 100644 --- a/message.go +++ b/message.go @@ -82,7 +82,7 @@ type Message struct { Attachments []*MessageAttachment `json:"attachments"` // A list of components attached to the message. - Components []MessageComponent `json:"-"` + Components []MessageComponent `json:"components"` // A list of embeds present in the message. Multiple // embeds can currently only be sent by webhooks. From 9bd0fe8b4de8a3242185d1c98db8c18dbe4a9541 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Thu, 17 Jun 2021 20:45:47 +0300 Subject: [PATCH 19/39] Interactions: select menus --- components.go | 50 +++- examples/components/main.go | 396 ++++++++++++++++++++++++++------ examples/slash_commands/main.go | 18 ++ interactions.go | 3 + restapi.go | 92 ++++++-- webhook.go | 1 + 6 files changed, 468 insertions(+), 92 deletions(-) diff --git a/components.go b/components.go index 9c4ca99bd..e637cf86a 100644 --- a/components.go +++ b/components.go @@ -11,6 +11,7 @@ type ComponentType uint const ( ActionsRowComponent ComponentType = 1 ButtonComponent ComponentType = 2 + SelectMenuComponent ComponentType = 3 ) // MessageComponent is a base interface for all message components. @@ -59,8 +60,8 @@ const ( LinkButton ButtonStyle = 5 ) -// ButtonEmoji represents button emoji, if it does have one. -type ButtonEmoji struct { +// ComponentEmoji represents button emoji, if it does have one. +type ComponentEmoji struct { Name string `json:"name,omitempty"` ID string `json:"id,omitempty"` Animated bool `json:"animated,omitempty"` @@ -68,10 +69,10 @@ type ButtonEmoji struct { // Button represents button component. type Button struct { - Label string `json:"label"` - Style ButtonStyle `json:"style"` - Disabled bool `json:"disabled"` - Emoji ButtonEmoji `json:"emoji"` + Label string `json:"label"` + Style ButtonStyle `json:"style"` + Disabled bool `json:"disabled"` + Emoji ComponentEmoji `json:"emoji"` // NOTE: Only button with LinkButton style can have link. Also, Link is mutually exclusive with CustomID. Link string `json:"url,omitempty"` @@ -99,3 +100,40 @@ func (b Button) MarshalJSON() ([]byte, error) { func (b Button) Type() ComponentType { return ButtonComponent } + + +type SelectMenuOption struct { + Label string `json:"label,omitempty"` + Value string `json:"value"` + Description string `json:"description"` + Emoji ComponentEmoji `json:"emoji"` + Default bool `json:"default"` +} + + +type SelectMenu struct { + CustomID string `json:"custom_id,omitempty"` + Placeholder string `json:"placeholder"` + MinValues int `json:"min_values,omitempty"` + MaxValues int `json:"max_values,omitempty"` + Options []SelectMenuOption `json:"options"` +} + +func (m SelectMenu) Type() ComponentType { + return SelectMenuComponent +} + +// MarshalJSON is a method for marshaling SelectMenu to a JSON object. +func (m SelectMenu) MarshalJSON() ([]byte, error) { + type selectMenu SelectMenu + + return json.Marshal(struct { + selectMenu + Type ComponentType `json:"type"` + }{ + selectMenu: selectMenu(m), + Type: m.Type(), + }) +} + + diff --git a/examples/components/main.go b/examples/components/main.go index bc9201213..7d8e852d0 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -2,11 +2,13 @@ package main import ( "flag" + "fmt" + "github.com/bwmarrin/discordgo" "log" "os" "os/signal" - - "github.com/bwmarrin/discordgo" + "sort" + "sync" ) // Bot parameters @@ -29,55 +31,273 @@ func init() { } } +type MessageVoteStats struct { + PeopleVoted map[string]string + Votes map[string]int + Author string +} + +var pollVotes map[string]MessageVoteStats +var pollVotesMtx sync.RWMutex + +func constructVotesEmbed(stats MessageVoteStats) *discordgo.MessageEmbed { + var votes []struct { + label string + count int + percent float32 + } + if len(stats.PeopleVoted) == 0 { + return &discordgo.MessageEmbed{ + Fields: []*discordgo.MessageEmbedField{ + {Name: "Go", Value: "Percentage: 0.0%\nPeople voted: 0"}, + {Name: "JS", Value: "Percentage: 0.0%\nPeople voted: 0"}, + {Name: "Python", Value: "Percentage: 0.0%\nPeople voted: 0"}, + }, + Color: 0xFFA1F0, + Footer: &discordgo.MessageEmbedFooter{Text: fmt.Sprintf("%d people voted", len(stats.PeopleVoted))}, + } + } + for k, v := range stats.Votes { + votes = append(votes, struct { + label string + count int + percent float32 + }{label: k, count: v, percent: (100.0 / float32(len(stats.PeopleVoted))) * float32(v)}) + } + sort.Slice(votes, func(i, j int) bool { return votes[i].count > votes[j].count }) + + var fields []*discordgo.MessageEmbedField + + for _, v := range votes { + fields = append(fields, &discordgo.MessageEmbedField{ + Name: v.label, + Value: fmt.Sprintf("Percentage: %.1f%%\nPeople voted: %d", v.percent, v.count), + }) + } + + return &discordgo.MessageEmbed{ + Fields: fields, + Color: 0xFFA1F0, + Footer: &discordgo.MessageEmbedFooter{Text: fmt.Sprintf("%d people voted", len(stats.PeopleVoted))}, + } +} + func main() { s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { log.Println("Bot is up!") + pollVotesMtx.Lock() + defer pollVotesMtx.Unlock() + pollVotes = make(map[string]MessageVoteStats) }) // 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.ApplicationCommandData().Name == "feedback" { - err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + var data *discordgo.InteractionResponseData + switch i.ApplicationCommandData().Name { + case "buttons": + data = &discordgo.InteractionResponseData{ + Content: "Are you satisfied with Buttons?", + // Buttons and other components are specified in Components field. + Components: []discordgo.MessageComponent{ + // ActionRow is a container of all buttons within the same row. + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "Yes", + Style: discordgo.SuccessButton, + Disabled: false, + CustomID: "yes", + }, + discordgo.Button{ + Label: "No", + Style: discordgo.DangerButton, + Disabled: false, + CustomID: "no", + }, + 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://discord.dev/interactions/message-components", + Emoji: discordgo.ComponentEmoji{ + Name: "🤷", + }, + }, + }, + }, + // The message may have multiple actions rows. + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "Ask the question in #buttons on Discord Developers server", + Style: discordgo.LinkButton, + Disabled: false, + Link: "https://discord.gg/discord-developers", + }, + }, + }, + }, + } + case "poll": + data = &discordgo.InteractionResponseData{ + TTS: false, + Content: "What's your favorite most beloved programming languages?", + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.SelectMenu{ + CustomID: "choice", + Placeholder: "Select your favorite language", + Options: []discordgo.SelectMenuOption{ + { + Label: "Go", + Value: "Go", + Description: "Go programming language.", + Emoji: discordgo.ComponentEmoji{}, + Default: false, + }, + { + Label: "JS", + Value: "JS", + Description: "JavaScript programming language.", + Emoji: discordgo.ComponentEmoji{}, + Default: false, + }, + { + Label: "Python", + Value: "Python", + Description: "Python programming language.", + Emoji: discordgo.ComponentEmoji{Name: ""}, + Default: false, + }, + }, + }, + }, + }, + }, + Flags: 0, + } + } + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: data, + }) + if err != nil { + panic(err) + } + if i.ApplicationCommandData().Name == "poll" { + msg, err := s.InteractionResponse(*AppID, i.Interaction) + if err != nil { + panic(err) + } + pollVotesMtx.Lock() + defer pollVotesMtx.Unlock() + pollVotes[msg.ID] = MessageVoteStats{ + Votes: map[string]int{"Go": 0, "JS": 0, "Python": 0}, + Author: i.Member.User.ID, + PeopleVoted: make(map[string]string), + } + fmt.Println(pollVotes[msg.ID]) + } + return + } + // Type for button press will be always InteractionButton (3) + if i.Type != discordgo.InteractionMessageComponent { + return + } + + // CustomID field contains the same id as when was sent. It's used to identify the which button was clicked. + switch i.MessageComponentData().CustomID { + case "yes", "no": + content := "Thanks for your feedback " + i.MessageComponentData().CustomID + 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{ + TTS: false, + Content: content, + Flags: 1 << 6, // Ephemeral message + }, + }) + case "end_poll": + if i.Member.User.ID != pollVotes[i.Message.ID].Author { + 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. + Content: "You didn't start this poll, so you can't end it", + Flags: 1 << 6, + }, + }) + return + } + pollVotesMtx.Lock() + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{constructVotesEmbed(pollVotes[i.Message.ID])}, + Components: []discordgo.MessageComponent{}, + }, + }) + delete(pollVotes, i.Message.ID) + pollVotesMtx.Unlock() + + case "choice": + pollVotesMtx.Lock() + defer pollVotesMtx.Unlock() + stats := pollVotes[i.Message.ID] + fmt.Println(stats) + if len(i.MessageComponentData().Values) == 0 { + stats.Votes[stats.PeopleVoted[i.Member.User.ID]]-- + delete(stats.PeopleVoted, i.Member.User.ID) + pollVotes[i.Message.ID] = stats + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + constructVotesEmbed(pollVotes[i.Message.ID]), + }, Components: []discordgo.MessageComponent{ - // ActionRow is a container of all buttons within the same row. discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ - 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: "🤷", + discordgo.SelectMenu{ + CustomID: "choice", + Placeholder: "Select your favorite language", + Options: []discordgo.SelectMenuOption{ + { + Label: "Go", + Value: "Go", + Description: "Go programming language.", + Emoji: discordgo.ComponentEmoji{}, + Default: false, + }, + { + Label: "JS", + Value: "javascript", + Description: "JavaScript programming language.", + Emoji: discordgo.ComponentEmoji{}, + Default: false, + }, + { + Label: "Python", + Value: "py", + Description: "Python programming language.", + Emoji: discordgo.ComponentEmoji{Name: ""}, + Default: false, + }, }, }, }, }, - // The message may have multiple actions rows. discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ discordgo.Button{ - Label: "Discord Developers server", - Style: discordgo.LinkButton, + Label: "End the poll", + CustomID: "end_poll", + Style: discordgo.DangerButton, Disabled: false, - Link: "https://discord.gg/discord-developers", }, }, }, @@ -87,52 +307,92 @@ func main() { if err != nil { panic(err) } + return + } + if _, ok := stats.PeopleVoted[i.Member.User.ID]; ok { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "You already voted", + Flags: 1 << 6, + }, + }) + return + } + stats.PeopleVoted[i.Member.User.ID] = i.MessageComponentData().Values[0] + if stats.Votes == nil { + stats.Votes = map[string]int{"Go": 0, "Python": 0, "JS": 0} } - return - } - // Type for button press will be always InteractionButton (3) - if i.Type != discordgo.InteractionMessageComponent { - 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.MessageComponentData().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.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.Button{ - Label: "Our sponsor", - Style: discordgo.LinkButton, - Disabled: false, - Link: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - Emoji: discordgo.ButtonEmoji{ - Name: "💠", + stats.Votes[i.MessageComponentData().Values[0]]++ + pollVotes[i.Message.ID] = stats + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + constructVotesEmbed(pollVotes[i.Message.ID]), + }, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.SelectMenu{ + CustomID: "choice", + Placeholder: "Select your favorite language", + Options: []discordgo.SelectMenuOption{ + { + Label: "Go", + Value: "Go", + Description: "Go programming language.", + Emoji: discordgo.ComponentEmoji{}, + Default: false, + }, + { + Label: "JS", + Value: "JS", + Description: "JavaScript programming language.", + Emoji: discordgo.ComponentEmoji{}, + Default: false, + }, + { + Label: "Python", + Value: "Python", + Description: "Python programming language.", + Emoji: discordgo.ComponentEmoji{Name: ""}, + Default: false, + }, + }, + }, + }, + }, + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "End the poll", + CustomID: "end_poll", + Style: discordgo.DangerButton, + Disabled: false, }, }, }, }, }, - }, - }) + }) + if err != nil { + panic(err) + } + } }) _, err := s.ApplicationCommandCreate(*AppID, *GuildID, &discordgo.ApplicationCommand{ - Name: "feedback", - Description: "Give your feedback", + Name: "buttons", + Description: "Test the buttons if you got courage", + }) + + if err != nil { + log.Fatalf("Cannot create slash command: %v", err) + } + _, err = s.ApplicationCommandCreate(*AppID, *GuildID, &discordgo.ApplicationCommand{ + Name: "poll", + Description: "Lo and behold: dropdowns are coming", }) if err != nil { diff --git a/examples/slash_commands/main.go b/examples/slash_commands/main.go index 57d27ca5e..a3cc6cf2f 100644 --- a/examples/slash_commands/main.go +++ b/examples/slash_commands/main.go @@ -87,6 +87,24 @@ var ( }, }, }, + { + Name: "tic", + Description: "something 2", + Options: []*discordgo.ApplicationCommandOption { + { + Name: "tac", + Description: "tac 2" + Type: discordgo.ApplicationCommandOptionSubCommandGroup, + Options: []*discordgo.ApplicationCommandOption { + { + Name: "toe", + Description: "toe 3", + Type: discordgo.ApplicationCommandOptionSubCommand, + }, + }, + }, + }, + }, { Name: "subcommands", Description: "Subcommands and command groups example", diff --git a/interactions.go b/interactions.go index a8798ec49..9d0e27acb 100644 --- a/interactions.go +++ b/interactions.go @@ -163,6 +163,9 @@ func (ApplicationCommandInteractionData) Type() InteractionType { type MessageComponentInteractionData struct { CustomID string `json:"custom_id"` ComponentType ComponentType `json:"component_type"` + + // NOTE: Only filled when ComponentType is SelectMenuComponent (3). Otherwise is nil. + Values []string `json:"values"` } // Type returns the type of interaction data. diff --git a/restapi.go b/restapi.go index cf2c3304e..52bba6b48 100644 --- a/restapi.go +++ b/restapi.go @@ -2236,43 +2236,95 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho return } -// WebhookMessage gets a webhook message. +// WebhookMessageEdit edits a webhook message and returns a new one. // webhookID : The ID of a webhook // token : The auth token for the webhook -// messageID : The ID of message to get -func (s *Session) WebhookMessage(webhookID, token, messageID string) (message *Message, err error) { +// messageID : The ID of message to edit +func (s *Session) WebhookMessageEdit(webhookID, token, messageID string, data *WebhookEdit) (st *Message, err error) { uri := EndpointWebhookMessage(webhookID, token, messageID) + var response []byte + if len(data.Files) > 0 { + body := &bytes.Buffer{} + bodywriter := multipart.NewWriter(body) + + var payload []byte + payload, err = json.Marshal(data) + if err != nil { + return + } + + var p io.Writer + + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="payload_json"`) + h.Set("Content-Type", "application/json") + + p, err = bodywriter.CreatePart(h) + if err != nil { + return + } + + if _, err = p.Write(payload); err != nil { + return + } + + for i, file := range data.Files { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name))) + contentType := file.ContentType + if contentType == "" { + contentType = "application/octet-stream" + } + h.Set("Content-Type", contentType) + + p, err = bodywriter.CreatePart(h) + if err != nil { + return + } + + if _, err = io.Copy(p, file.Reader); err != nil { + return + } + } - body, err := s.RequestWithBucketID("GET", uri, nil, EndpointWebhookToken("", "")) + err = bodywriter.Close() + if err != nil { + return + } + + response, err = s.request("PATCH", uri, bodywriter.FormDataContentType(), body.Bytes(), uri, 0) + } else { + response, err = s.RequestWithBucketID("PATCH", uri, data, uri) + } if err != nil { return } - err = json.Unmarshal(body, &message) - + err = unmarshal(response, &st) return } -// WebhookMessageEdit edits a webhook message. +// WebhookMessageDelete deletes a webhook message. // webhookID : The ID of a webhook // token : The auth token for the webhook // messageID : The ID of message to edit -func (s *Session) WebhookMessageEdit(webhookID, token, messageID string, data *WebhookEdit) (err error) { +func (s *Session) WebhookMessageDelete(webhookID, token, messageID string) (err error) { uri := EndpointWebhookMessage(webhookID, token, messageID) - _, err = s.RequestWithBucketID("PATCH", uri, data, EndpointWebhookToken("", "")) - + _, err = s.RequestWithBucketID("DELETE", uri, nil, EndpointWebhookToken("", "")) return } -// WebhookMessageDelete deletes a webhook message. -// webhookID : The ID of a webhook -// token : The auth token for the webhook -// messageID : The ID of message to edit -func (s *Session) WebhookMessageDelete(webhookID, token, messageID string) (err error) { +func (s *Session) WebhookMessage(webhookID, token, messageID string) (st *Message, err error) { uri := EndpointWebhookMessage(webhookID, token, messageID) - _, err = s.RequestWithBucketID("DELETE", uri, nil, EndpointWebhookToken("", "")) + response, err := s.RequestWithBucketID("GET", uri, nil, uri) + + if err != nil { + return + } + + err = unmarshal(response, &st) return } @@ -2585,10 +2637,14 @@ func (s *Session) InteractionResponse(appID string, interaction *Interaction) (* // appID : The application ID. // interaction : Interaction instance. // newresp : Updated response message data. -func (s *Session) InteractionResponseEdit(appID string, interaction *Interaction, newresp *WebhookEdit) error { +func (s *Session) InteractionResponseEdit(appID string, interaction *Interaction, newresp *WebhookEdit) (*Message, error) { return s.WebhookMessageEdit(appID, interaction.Token, "@original", newresp) } +func (s *Session) InteractionResponse(appID string, interaction *Interaction) (*Message, error) { + return s.WebhookMessage(appID, interaction.Token, "@original") +} + // InteractionResponseDelete deletes the response to an interaction. // appID : The application ID. // interaction : Interaction instance. @@ -2614,7 +2670,7 @@ func (s *Session) FollowupMessageCreate(appID string, interaction *Interaction, // interaction : Interaction instance. // messageID : The followup message ID. // data : Data to update the message -func (s *Session) FollowupMessageEdit(appID string, interaction *Interaction, messageID string, data *WebhookEdit) error { +func (s *Session) FollowupMessageEdit(appID string, interaction *Interaction, messageID string, data *WebhookEdit) (*Message, error) { return s.WebhookMessageEdit(appID, interaction.Token, messageID, data) } diff --git a/webhook.go b/webhook.go index d61b2817a..647bb8b7c 100644 --- a/webhook.go +++ b/webhook.go @@ -41,6 +41,7 @@ type WebhookParams struct { type WebhookEdit struct { Content string `json:"content,omitempty"` Components []MessageComponent `json:"components"` + Files []*File `json:"-"` Embeds []*MessageEmbed `json:"embeds,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` } From 789aa19cd1c4749252b24a9eae61f65e7c02c6f4 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Thu, 17 Jun 2021 20:48:08 +0300 Subject: [PATCH 20/39] Example fix --- examples/components/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/components/main.go b/examples/components/main.go index 7d8e852d0..bc052bab0 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -202,7 +202,7 @@ func main() { } return } - // Type for button press will be always InteractionButton (3) + // Type for all components will be always InteractionMessageComponent if i.Type != discordgo.InteractionMessageComponent { return } From 91f8d0a0feb28531ccefaf98d29f848aa36e59e7 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Thu, 17 Jun 2021 21:15:21 +0300 Subject: [PATCH 21/39] Merge fix --- examples/slash_commands/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/slash_commands/main.go b/examples/slash_commands/main.go index a3cc6cf2f..1772629ab 100644 --- a/examples/slash_commands/main.go +++ b/examples/slash_commands/main.go @@ -93,7 +93,7 @@ var ( Options: []*discordgo.ApplicationCommandOption { { Name: "tac", - Description: "tac 2" + Description: "tac 2", Type: discordgo.ApplicationCommandOptionSubCommandGroup, Options: []*discordgo.ApplicationCommandOption { { @@ -288,7 +288,7 @@ var ( return } time.AfterFunc(time.Second*5, func() { - err = s.InteractionResponseEdit(s.State.User.ID, i.Interaction, &discordgo.WebhookEdit{ + _, err = s.InteractionResponseEdit(s.State.User.ID, i.Interaction, &discordgo.WebhookEdit{ Content: content + "\n\nWell, now you know how to create and edit responses. " + "But you still don't know how to delete them... so... wait 10 seconds and this " + "message will be deleted.", From 462151e971659602e4549ae9f45a3593e95b554f Mon Sep 17 00:00:00 2001 From: nitroflap Date: Thu, 17 Jun 2021 21:50:15 +0300 Subject: [PATCH 22/39] Some fixes --- components.go | 16 ++++++---------- examples/slash_commands/main.go | 18 ------------------ 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/components.go b/components.go index e637cf86a..f0e908c19 100644 --- a/components.go +++ b/components.go @@ -101,7 +101,6 @@ func (b Button) Type() ComponentType { return ButtonComponent } - type SelectMenuOption struct { Label string `json:"label,omitempty"` Value string `json:"value"` @@ -110,13 +109,12 @@ type SelectMenuOption struct { Default bool `json:"default"` } - type SelectMenu struct { - CustomID string `json:"custom_id,omitempty"` - Placeholder string `json:"placeholder"` - MinValues int `json:"min_values,omitempty"` - MaxValues int `json:"max_values,omitempty"` - Options []SelectMenuOption `json:"options"` + CustomID string `json:"custom_id,omitempty"` + Placeholder string `json:"placeholder"` + MinValues int `json:"min_values,omitempty"` + MaxValues int `json:"max_values,omitempty"` + Options []SelectMenuOption `json:"options"` } func (m SelectMenu) Type() ComponentType { @@ -132,8 +130,6 @@ func (m SelectMenu) MarshalJSON() ([]byte, error) { Type ComponentType `json:"type"` }{ selectMenu: selectMenu(m), - Type: m.Type(), + Type: m.Type(), }) } - - diff --git a/examples/slash_commands/main.go b/examples/slash_commands/main.go index 1772629ab..14e4ea6a5 100644 --- a/examples/slash_commands/main.go +++ b/examples/slash_commands/main.go @@ -87,24 +87,6 @@ var ( }, }, }, - { - Name: "tic", - Description: "something 2", - Options: []*discordgo.ApplicationCommandOption { - { - Name: "tac", - Description: "tac 2", - Type: discordgo.ApplicationCommandOptionSubCommandGroup, - Options: []*discordgo.ApplicationCommandOption { - { - Name: "toe", - Description: "toe 3", - Type: discordgo.ApplicationCommandOptionSubCommand, - }, - }, - }, - }, - }, { Name: "subcommands", Description: "Subcommands and command groups example", From 2ed8862e71c005309dbf4e6aff3dcf59990db2da Mon Sep 17 00:00:00 2001 From: nitroflap Date: Fri, 18 Jun 2021 00:03:32 +0300 Subject: [PATCH 23/39] Added missing documentation --- components.go | 26 +++++++++++++++++--------- examples/components/main.go | 4 +++- restapi.go | 9 ++++++++- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/components.go b/components.go index f0e908c19..cca5e0613 100644 --- a/components.go +++ b/components.go @@ -39,7 +39,7 @@ func (r ActionsRow) MarshalJSON() ([]byte, error) { } // Type is a method to get the type of a component. -func (r ActionsRow) Type() ComponentType { +func (ActionsRow) Type() ComponentType { return ActionsRowComponent } @@ -97,27 +97,35 @@ func (b Button) MarshalJSON() ([]byte, error) { } // Type is a method to get the type of a component. -func (b Button) Type() ComponentType { +func (Button) Type() ComponentType { return ButtonComponent } +// SelectMenuOption represents an option for a select menu. type SelectMenuOption struct { Label string `json:"label,omitempty"` Value string `json:"value"` Description string `json:"description"` Emoji ComponentEmoji `json:"emoji"` - Default bool `json:"default"` + // Determines whenever option is selected by default or not. + Default bool `json:"default"` } +// SelectMenu represents select menu component. type SelectMenu struct { - CustomID string `json:"custom_id,omitempty"` - Placeholder string `json:"placeholder"` - MinValues int `json:"min_values,omitempty"` - MaxValues int `json:"max_values,omitempty"` - Options []SelectMenuOption `json:"options"` + CustomID string `json:"custom_id,omitempty"` + // The text which will be shown in the menu if there's no default options or all options was deselected and component was closed. + Placeholder string `json:"placeholder"` + // This value determines the minimal amount of selected items in the menu. + MinValues int `json:"min_values,omitempty"` + // This value determines the maximal amount of selected items in the menu. + // If MaxValues or MinValues are greater than one then the user can select multiple items in the component. + MaxValues int `json:"max_values,omitempty"` + Options []SelectMenuOption `json:"options"` } -func (m SelectMenu) Type() ComponentType { +// Type is a method to get the type of a component. +func (SelectMenu) Type() ComponentType { return SelectMenuComponent } diff --git a/examples/components/main.go b/examples/components/main.go index bc052bab0..db3c897c2 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -3,12 +3,13 @@ package main import ( "flag" "fmt" - "github.com/bwmarrin/discordgo" "log" "os" "os/signal" "sort" "sync" + + "github.com/bwmarrin/discordgo" ) // Bot parameters @@ -31,6 +32,7 @@ func init() { } } +// MessageVoteStats represents user votes for a particular message type MessageVoteStats struct { PeopleVoted map[string]string Votes map[string]int diff --git a/restapi.go b/restapi.go index 52bba6b48..1a184ebb0 100644 --- a/restapi.go +++ b/restapi.go @@ -2307,7 +2307,7 @@ func (s *Session) WebhookMessageEdit(webhookID, token, messageID string, data *W // WebhookMessageDelete deletes a webhook message. // webhookID : The ID of a webhook // token : The auth token for the webhook -// messageID : The ID of message to edit +// messageID : The ID of a message to edit func (s *Session) WebhookMessageDelete(webhookID, token, messageID string) (err error) { uri := EndpointWebhookMessage(webhookID, token, messageID) @@ -2315,6 +2315,10 @@ func (s *Session) WebhookMessageDelete(webhookID, token, messageID string) (err return } +// WebhookMessage retrieves and returnes the message sent by a webhook. +// webhookID : The webhook ID +// token : The auth token for the webhook +// messageID : The ID of a message to retrieve func (s *Session) WebhookMessage(webhookID, token, messageID string) (st *Message, err error) { uri := EndpointWebhookMessage(webhookID, token, messageID) @@ -2641,6 +2645,9 @@ func (s *Session) InteractionResponseEdit(appID string, interaction *Interaction return s.WebhookMessageEdit(appID, interaction.Token, "@original", newresp) } +// InteractionResponse retrieves and returns the interaction response message. +// appID : The application ID. +// interaction : Interaction instance. func (s *Session) InteractionResponse(appID string, interaction *Interaction) (*Message, error) { return s.WebhookMessage(appID, interaction.Token, "@original") } From 515f1c95dab2fd0c38f286b5b9956c75b90d97ca Mon Sep 17 00:00:00 2001 From: nitroflap Date: Fri, 18 Jun 2021 19:14:02 +0300 Subject: [PATCH 24/39] Fix components unmarshaling --- components.go | 39 +++++++++++++++++++++++++++++++++++---- message.go | 2 +- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/components.go b/components.go index cca5e0613..a998dd39b 100644 --- a/components.go +++ b/components.go @@ -20,6 +20,37 @@ type MessageComponent interface { Type() ComponentType } +type UnmarshableMessageComponent struct { + MessageComponent +} + +func (umc *UnmarshableMessageComponent) UnmarshalJSON(src []byte) (err error) { + var v struct { + Type ComponentType `json:"type"` + } + err = json.Unmarshal(src, &v) + if err != nil { + return + } + + var data MessageComponent + switch v.Type { + case ActionsRowComponent: + v := ActionsRow{} + err = json.Unmarshal(src, &v) + data = v + case ButtonComponent: + v := Button{} + err = json.Unmarshal(src, &v) + data = v + } + if err != nil { + return + } + umc.MessageComponent = data + return +} + // ActionsRow is a container for components within one row. type ActionsRow struct { Components []MessageComponent `json:"components"` @@ -27,14 +58,14 @@ type ActionsRow struct { // MarshalJSON is a method for marshaling ActionsRow to a JSON object. func (r ActionsRow) MarshalJSON() ([]byte, error) { - type actionRow ActionsRow + type actionsRow ActionsRow return json.Marshal(struct { - actionRow + actionsRow Type ComponentType `json:"type"` }{ - actionRow: actionRow(r), - Type: r.Type(), + actionsRow: actionsRow(r), + Type: r.Type(), }) } diff --git a/message.go b/message.go index ec37419d8..375932ec8 100644 --- a/message.go +++ b/message.go @@ -82,7 +82,7 @@ type Message struct { Attachments []*MessageAttachment `json:"attachments"` // A list of components attached to the message. - Components []MessageComponent `json:"components"` + Components []UnmarshableMessageComponent `json:"components"` // A list of embeds present in the message. Multiple // embeds can currently only be sent by webhooks. From c4079fed0e5739f441ef36f923bf0c1614d88378 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Fri, 18 Jun 2021 21:54:57 +0300 Subject: [PATCH 25/39] Godoc fix --- components.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components.go b/components.go index a998dd39b..0f2993e74 100644 --- a/components.go +++ b/components.go @@ -20,10 +20,13 @@ type MessageComponent interface { Type() ComponentType } +// UnmarshableMessageComponent a helper for components which need to be unmarshaled (like the ones in message object). +// Since interfaces can't be unmarshaled this exists. type UnmarshableMessageComponent struct { MessageComponent } +// UnmarshalJSON is a helper function to unmarshal MessageComponent object. func (umc *UnmarshableMessageComponent) UnmarshalJSON(src []byte) (err error) { var v struct { Type ComponentType `json:"type"` From fd4ad1525c3b5b2a6a8c8982eb3a38cff15f4582 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Sat, 19 Jun 2021 21:24:40 +0300 Subject: [PATCH 26/39] Requested fixes --- components.go | 6 ++---- message.go | 17 +++++++++-------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/components.go b/components.go index 0f2993e74..c7cd20b76 100644 --- a/components.go +++ b/components.go @@ -20,14 +20,12 @@ type MessageComponent interface { Type() ComponentType } -// UnmarshableMessageComponent a helper for components which need to be unmarshaled (like the ones in message object). -// Since interfaces can't be unmarshaled this exists. -type UnmarshableMessageComponent struct { +type unmarshalableMessageComponent struct { MessageComponent } // UnmarshalJSON is a helper function to unmarshal MessageComponent object. -func (umc *UnmarshableMessageComponent) UnmarshalJSON(src []byte) (err error) { +func (umc *unmarshalableMessageComponent) UnmarshalJSON(src []byte) (err error) { var v struct { Type ComponentType `json:"type"` } diff --git a/message.go b/message.go index 375932ec8..2cc922661 100644 --- a/message.go +++ b/message.go @@ -82,7 +82,7 @@ type Message struct { Attachments []*MessageAttachment `json:"attachments"` // A list of components attached to the message. - Components []UnmarshableMessageComponent `json:"components"` + Components []MessageComponent `json:"-"` // A list of embeds present in the message. Multiple // embeds can currently only be sent by webhooks. @@ -129,23 +129,24 @@ type Message struct { Flags MessageFlags `json:"flags"` } -// UnmarshalJSON is a helper function to unmarshal the Message. -func (m *Message) UnmarshalJSON(data []byte) error { +// UnmarshalJSON is a helper function to unmarshal the message. +// NOTE: It exists only because message components can't be unmarshaled by default, so we need to apply manual unmarshaling. +func (m *Message) UnmarshalJSON(data []byte) (err error) { type message Message var v struct { message RawComponents []unmarshalableMessageComponent `json:"components"` } - err := json.Unmarshal(data, &v) + err = json.Unmarshal(data, &v) if err != nil { - return err + return } *m = Message(v.message) - m.Components = make([]MessageComponent, len(v.RawComponents)) + m.Components = make([]MessageComponent, len(v.Components)) for i, v := range v.RawComponents { - m.Components[i] = v.MessageComponent + m.Components[i] = v } - return err + return } // GetCustomEmojis pulls out all the custom (Non-unicode) emojis from a message and returns a Slice of the Emoji struct. From fec9fdf45969d35c7a17a077091888c1f2e4525c Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 23 Jun 2021 00:24:32 +0300 Subject: [PATCH 27/39] Fixed unmarshaling issues --- components.go | 16 ++++++++++++++++ events.go | 5 +---- message.go | 7 +++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/components.go b/components.go index c7cd20b76..643e1dae1 100644 --- a/components.go +++ b/components.go @@ -70,6 +70,22 @@ func (r ActionsRow) MarshalJSON() ([]byte, error) { }) } +// UnmarshalJSON is a helper function to unmarshal Actions Row. +func (r *ActionsRow) UnmarshalJSON(data []byte) (err error) { + var v struct { + RawComponents []unmarshalableMessageComponent `json:"components"` + } + err = json.Unmarshal(data, &v) + if err != nil { + return + } + r.Components = make([]MessageComponent, len(v.RawComponents)) + for i, v := range v.RawComponents { + r.Components[i] = v.MessageComponent + } + return +} + // Type is a method to get the type of a component. func (ActionsRow) Type() ComponentType { return ActionsRowComponent diff --git a/events.go b/events.go index 34d6a29f1..f44eba4ac 100644 --- a/events.go +++ b/events.go @@ -186,7 +186,7 @@ type MessageDelete struct { } // UnmarshalJSON is a helper function to unmarshal MessageDelete object. -func (m *MessageDelete) UnmarshalJSON(b []byte) error { +func (m *MessageDelete) UnmarshalJSON(b []byte) (err error) { return json.Unmarshal(b, &m.Message) } @@ -289,9 +289,6 @@ type InteractionCreate struct { } // UnmarshalJSON is a helper function to unmarshal Interaction object. -// Since it's a pointer json.Unmarshal does not unmarshals it correctly (Interaction field is nil). -// And so we need to unmarshal it manually. func (i *InteractionCreate) UnmarshalJSON(b []byte) error { - i.Interaction = new(Interaction) return json.Unmarshal(b, &i.Interaction) } diff --git a/message.go b/message.go index 2cc922661..77b8dd90c 100644 --- a/message.go +++ b/message.go @@ -129,8 +129,7 @@ type Message struct { Flags MessageFlags `json:"flags"` } -// UnmarshalJSON is a helper function to unmarshal the message. -// NOTE: It exists only because message components can't be unmarshaled by default, so we need to apply manual unmarshaling. +// UnmarshalJSON is a helper function to unmarshal the Message. func (m *Message) UnmarshalJSON(data []byte) (err error) { type message Message var v struct { @@ -142,9 +141,9 @@ func (m *Message) UnmarshalJSON(data []byte) (err error) { return } *m = Message(v.message) - m.Components = make([]MessageComponent, len(v.Components)) + m.Components = make([]MessageComponent, len(v.RawComponents)) for i, v := range v.RawComponents { - m.Components[i] = v + m.Components[i] = v.MessageComponent } return } From 9ba0bf7a0b387c833c3b91abfd0bf59c56b4145c Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 23 Jun 2021 00:26:29 +0300 Subject: [PATCH 28/39] Components example: cleanup --- examples/components/main.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/components/main.go b/examples/components/main.go index db3c897c2..aaf6d96eb 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -14,10 +14,9 @@ import ( // 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") + GuildID = flag.String("guild", "", "Test guild ID") + BotToken = flag.String("token", "", "Bot access token") + AppID = flag.String("app", "", "Application ID") ) var s *discordgo.Session @@ -407,7 +406,7 @@ func main() { } defer s.Close() - stop := make(chan os.Signal) + stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt) <-stop log.Println("Graceful shutdown") From 8c0d7154aba8b03d075f0f22752fba36cc97dcb0 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 30 Jun 2021 19:37:05 +0300 Subject: [PATCH 29/39] Gofmt fix --- examples/components/main.go | 2 +- restapi.go | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/examples/components/main.go b/examples/components/main.go index aaf6d96eb..f7c094cd2 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -90,7 +90,7 @@ func main() { defer pollVotesMtx.Unlock() pollVotes = make(map[string]MessageVoteStats) }) - // Buttons are part of interactions, so we register InteractionCreate handler + // Components are part of interactions, so we register InteractionCreate handler s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { if i.Type == discordgo.InteractionApplicationCommand { var data *discordgo.InteractionResponseData diff --git a/restapi.go b/restapi.go index 1a184ebb0..d4f28f455 100644 --- a/restapi.go +++ b/restapi.go @@ -2315,23 +2315,6 @@ func (s *Session) WebhookMessageDelete(webhookID, token, messageID string) (err return } -// WebhookMessage retrieves and returnes the message sent by a webhook. -// webhookID : The webhook ID -// token : The auth token for the webhook -// messageID : The ID of a message to retrieve -func (s *Session) WebhookMessage(webhookID, token, messageID string) (st *Message, err error) { - uri := EndpointWebhookMessage(webhookID, token, messageID) - - response, err := s.RequestWithBucketID("GET", uri, nil, uri) - - if err != nil { - return - } - - err = unmarshal(response, &st) - return -} - // MessageReactionAdd creates an emoji reaction to a message. // channelID : The channel ID. // messageID : The message ID. From 0949c93c335667fb2d00f64e052a054ee451d5e2 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 30 Jun 2021 19:38:23 +0300 Subject: [PATCH 30/39] Godoc fix --- interactions.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interactions.go b/interactions.go index 9d0e27acb..6320cc580 100644 --- a/interactions.go +++ b/interactions.go @@ -132,7 +132,8 @@ func (i *Interaction) UnmarshalJSON(raw []byte) (err error) { return nil } -// MessageComponentData is helper function to convert InteractionData to MessageComponentInteractionData. +// MessageComponentData is helper function to assert the inner InteractionData to MessageComponentInteractionData. +// Make sure to check that the Type of the interaction is InteractionMessageComponent before calling. func (i Interaction) MessageComponentData() (data MessageComponentInteractionData) { return i.Data.(MessageComponentInteractionData) } From d0a2096c78ce8238f095cf6f0cee195e89715e8f Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 30 Jun 2021 23:40:54 +0300 Subject: [PATCH 31/39] URL field renaming fix --- examples/components/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/components/main.go b/examples/components/main.go index f7c094cd2..661adf98d 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -120,7 +120,7 @@ func main() { Style: discordgo.LinkButton, Disabled: false, // Link buttons doesn't require CustomID and does not trigger the gateway/HTTP event - Link: "https://discord.dev/interactions/message-components", + URL: "https://discord.dev/interactions/message-components", Emoji: discordgo.ComponentEmoji{ Name: "🤷", }, @@ -134,7 +134,7 @@ func main() { Label: "Ask the question in #buttons on Discord Developers server", Style: discordgo.LinkButton, Disabled: false, - Link: "https://discord.gg/discord-developers", + URL: "https://discord.gg/discord-developers", }, }, }, From 9b0225fe12fc7bec1340490fbf7a572e44fcce32 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Mon, 26 Jul 2021 22:15:41 +0300 Subject: [PATCH 32/39] Added flags to followups --- interactions.go | 4 +--- webhook.go | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/interactions.go b/interactions.go index 6320cc580..91b9fe525 100644 --- a/interactions.go +++ b/interactions.go @@ -328,9 +328,7 @@ type InteractionResponseData struct { Components []MessageComponent `json:"components"` Embeds []*MessageEmbed `json:"embeds,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` - - // NOTE: Undocumented feature, be careful with it. - Flags uint64 `json:"flags,omitempty"` + Flags uint64 `json:"flags,omitempty"` } // VerifyInteraction implements message verification of the discord interactions api diff --git a/webhook.go b/webhook.go index 647bb8b7c..f44fdbd49 100644 --- a/webhook.go +++ b/webhook.go @@ -35,6 +35,8 @@ type WebhookParams struct { Components []MessageComponent `json:"components"` Embeds []*MessageEmbed `json:"embeds,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` + // NOTE: Works only for followup messages. + Flags uint64 `json:"flags,omitempty"` } // WebhookEdit stores data for editing of a webhook message. From bdb76ad0dda2f3122c9864769f3d05f963fefcb6 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Mon, 26 Jul 2021 22:18:23 +0300 Subject: [PATCH 33/39] Updated components example --- examples/components/main.go | 591 +++++++++++++++++++----------------- 1 file changed, 314 insertions(+), 277 deletions(-) diff --git a/examples/components/main.go b/examples/components/main.go index 661adf98d..48d179ee3 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -6,8 +6,8 @@ import ( "log" "os" "os/signal" - "sort" - "sync" + "strings" + "time" "github.com/bwmarrin/discordgo" ) @@ -31,96 +31,212 @@ func init() { } } -// MessageVoteStats represents user votes for a particular message -type MessageVoteStats struct { - PeopleVoted map[string]string - Votes map[string]int - Author string -} - -var pollVotes map[string]MessageVoteStats -var pollVotesMtx sync.RWMutex +// Important note: call every command in order it's placed in the example. -func constructVotesEmbed(stats MessageVoteStats) *discordgo.MessageEmbed { - var votes []struct { - label string - count int - percent float32 - } - if len(stats.PeopleVoted) == 0 { - return &discordgo.MessageEmbed{ - Fields: []*discordgo.MessageEmbedField{ - {Name: "Go", Value: "Percentage: 0.0%\nPeople voted: 0"}, - {Name: "JS", Value: "Percentage: 0.0%\nPeople voted: 0"}, - {Name: "Python", Value: "Percentage: 0.0%\nPeople voted: 0"}, - }, - Color: 0xFFA1F0, - Footer: &discordgo.MessageEmbedFooter{Text: fmt.Sprintf("%d people voted", len(stats.PeopleVoted))}, - } - } - for k, v := range stats.Votes { - votes = append(votes, struct { - label string - count int - percent float32 - }{label: k, count: v, percent: (100.0 / float32(len(stats.PeopleVoted))) * float32(v)}) - } - sort.Slice(votes, func(i, j int) bool { return votes[i].count > votes[j].count }) +var ( + componentsHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ + "fd_no": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Huh. I see, maybe some of these resources might help you?", + Flags: 1 << 6, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Emoji: discordgo.ComponentEmoji{ + Name: "📜", + }, + Label: "Documentation", + Style: discordgo.LinkButton, + URL: "https://discord.com/developers/docs/interactions/message-components#buttons", + }, + discordgo.Button{ + Emoji: discordgo.ComponentEmoji{ + Name: "🔧", + }, + Label: "Discord developers", + Style: discordgo.LinkButton, + URL: "https://discord.gg/discord-developers", + }, + discordgo.Button{ + Emoji: discordgo.ComponentEmoji{ + Name: "đŸĻĢ", + }, + Label: "Discord Gophers", + Style: discordgo.LinkButton, + URL: "https://discord.gg/7RuRrVHyXF", + }, + }, + }, + }, + }, + }) + if err != nil { + panic(err) + } + }, + "fd_yes": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Great! If you wanna know more or just have questions, feel free to visit Discord Devs and Discord Gophers server. " + + "But now, when you know how buttons work, let's move onto select menus (execute `/selects single`)", + Flags: 1 << 6, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Emoji: discordgo.ComponentEmoji{ + Name: "🔧", + }, + Label: "Discord developers", + Style: discordgo.LinkButton, + URL: "https://discord.gg/discord-developers", + }, + discordgo.Button{ + Emoji: discordgo.ComponentEmoji{ + Name: "đŸĻĢ", + }, + Label: "Discord Gophers", + Style: discordgo.LinkButton, + URL: "https://discord.gg/7RuRrVHyXF", + }, + }, + }, + }, + }, + }) + if err != nil { + panic(err) + } + }, + "select": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + var response *discordgo.InteractionResponse - var fields []*discordgo.MessageEmbedField + data := i.MessageComponentData() + switch data.Values[0] { + case "go": + response = &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "This is the way.", + Flags: 1 << 6, + }, + } + default: + response = &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "It is not the way to go.", + Flags: 1 << 6, + }, + } + } + err := s.InteractionRespond(i.Interaction, response) + if err != nil { + panic(err) + } + time.Sleep(time.Second) // Doing that so user won't see instant response. + _, err = s.FollowupMessageCreate(*AppID, i.Interaction, true, &discordgo.WebhookParams{ + Content: "Anyways, now when you know how to use single select menus, let's see how multi select menus work. " + + "Try calling `/selects multi` command.", + Flags: 1 << 6, + }) + if err != nil { + panic(err) + } + }, + "stackoverflow_tags": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + data := i.MessageComponentData() - for _, v := range votes { - fields = append(fields, &discordgo.MessageEmbedField{ - Name: v.label, - Value: fmt.Sprintf("Percentage: %.1f%%\nPeople voted: %d", v.percent, v.count), - }) - } + const stackoverflowFormat = `https://stackoverflow.com/questions/tagged/%s` - return &discordgo.MessageEmbed{ - Fields: fields, - Color: 0xFFA1F0, - Footer: &discordgo.MessageEmbedFooter{Text: fmt.Sprintf("%d people voted", len(stats.PeopleVoted))}, + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Here is your stackoverflow link: " + fmt.Sprintf(stackoverflowFormat, strings.Join(data.Values, "+")), + Flags: 1 << 6, + }, + }) + if err != nil { + panic(err) + } + time.Sleep(time.Second) // Doing that so user won't see instant response. + _, err = s.FollowupMessageCreate(*AppID, i.Interaction, true, &discordgo.WebhookParams{ + Content: "Now you know everything about select component. If you want to know more or ask a question - feel free to.", + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Emoji: discordgo.ComponentEmoji{ + Name: "📜", + }, + Label: "Documentation", + Style: discordgo.LinkButton, + URL: "https://discord.com/developers/docs/interactions/message-components#select-menus", + }, + discordgo.Button{ + Emoji: discordgo.ComponentEmoji{ + Name: "🔧", + }, + Label: "Discord developers", + Style: discordgo.LinkButton, + URL: "https://discord.gg/discord-developers", + }, + discordgo.Button{ + Emoji: discordgo.ComponentEmoji{ + Name: "đŸĻĢ", + }, + Label: "Discord Gophers", + Style: discordgo.LinkButton, + URL: "https://discord.gg/7RuRrVHyXF", + }, + }, + }, + }, + Flags: 1 << 6, + }) + if err != nil { + panic(err) + } + }, } -} - -func main() { - s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { - log.Println("Bot is up!") - pollVotesMtx.Lock() - defer pollVotesMtx.Unlock() - pollVotes = make(map[string]MessageVoteStats) - }) - // Components are part of interactions, so we register InteractionCreate handler - s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { - if i.Type == discordgo.InteractionApplicationCommand { - var data *discordgo.InteractionResponseData - switch i.ApplicationCommandData().Name { - case "buttons": - data = &discordgo.InteractionResponseData{ - Content: "Are you satisfied with Buttons?", + commandsHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ + "buttons": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Are you comfortable with buttons and other message components?", + Flags: 1 << 6, // Buttons and other components are specified in Components field. Components: []discordgo.MessageComponent{ // ActionRow is a container of all buttons within the same row. discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ discordgo.Button{ - Label: "Yes", - Style: discordgo.SuccessButton, + // Label is what the user will see on the button. + Label: "Yes", + // Style provides coloring of the button. There are not so many styles tho. + Style: discordgo.SuccessButton, + // Disabled allows bot to disable some buttons for users. Disabled: false, - CustomID: "yes", + // CustomID is a thing telling Discord which data to send when this button will be pressed. + CustomID: "fd_yes", }, discordgo.Button{ Label: "No", Style: discordgo.DangerButton, Disabled: false, - CustomID: "no", + CustomID: "fd_no", }, 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 - URL: "https://discord.dev/interactions/message-components", + // Link buttons don't require CustomID and do not trigger the gateway/HTTP event + URL: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", Emoji: discordgo.ComponentEmoji{ Name: "🤷", }, @@ -131,7 +247,7 @@ func main() { discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ discordgo.Button{ - Label: "Ask the question in #buttons on Discord Developers server", + Label: "Discord Developers server", Style: discordgo.LinkButton, Disabled: false, URL: "https://discord.gg/discord-developers", @@ -139,248 +255,157 @@ func main() { }, }, }, - } - case "poll": - data = &discordgo.InteractionResponseData{ - TTS: false, - Content: "What's your favorite most beloved programming languages?", - Components: []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.SelectMenu{ - CustomID: "choice", - Placeholder: "Select your favorite language", - Options: []discordgo.SelectMenuOption{ - { - Label: "Go", - Value: "Go", - Description: "Go programming language.", - Emoji: discordgo.ComponentEmoji{}, - Default: false, - }, - { - Label: "JS", - Value: "JS", - Description: "JavaScript programming language.", - Emoji: discordgo.ComponentEmoji{}, - Default: false, - }, - { - Label: "Python", - Value: "Python", - Description: "Python programming language.", - Emoji: discordgo.ComponentEmoji{Name: ""}, - Default: false, - }, - }, - }, - }, - }, - }, - Flags: 0, - } - } - err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: data, + }, }) if err != nil { panic(err) } - if i.ApplicationCommandData().Name == "poll" { - msg, err := s.InteractionResponse(*AppID, i.Interaction) - if err != nil { - panic(err) - } - pollVotesMtx.Lock() - defer pollVotesMtx.Unlock() - pollVotes[msg.ID] = MessageVoteStats{ - Votes: map[string]int{"Go": 0, "JS": 0, "Python": 0}, - Author: i.Member.User.ID, - PeopleVoted: make(map[string]string), - } - fmt.Println(pollVotes[msg.ID]) - } - return - } - // Type for all components will be always InteractionMessageComponent - if i.Type != discordgo.InteractionMessageComponent { - return - } - - // CustomID field contains the same id as when was sent. It's used to identify the which button was clicked. - switch i.MessageComponentData().CustomID { - case "yes", "no": - content := "Thanks for your feedback " + i.MessageComponentData().CustomID - 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{ - TTS: false, - Content: content, - Flags: 1 << 6, // Ephemeral message - }, - }) - case "end_poll": - if i.Member.User.ID != pollVotes[i.Message.ID].Author { - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + }, + "selects": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + var response *discordgo.InteractionResponse + switch i.ApplicationCommandData().Options[0].Name { + case "single": + response = &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ - Content: "You didn't start this poll, so you can't end it", + Content: "Now let's take a look on selects. This is single item select menu.", Flags: 1 << 6, - }, - }) - return - } - pollVotesMtx.Lock() - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseUpdateMessage, - Data: &discordgo.InteractionResponseData{ - Embeds: []*discordgo.MessageEmbed{constructVotesEmbed(pollVotes[i.Message.ID])}, - Components: []discordgo.MessageComponent{}, - }, - }) - delete(pollVotes, i.Message.ID) - pollVotesMtx.Unlock() - - case "choice": - pollVotesMtx.Lock() - defer pollVotesMtx.Unlock() - stats := pollVotes[i.Message.ID] - fmt.Println(stats) - if len(i.MessageComponentData().Values) == 0 { - stats.Votes[stats.PeopleVoted[i.Member.User.ID]]-- - delete(stats.PeopleVoted, i.Member.User.ID) - pollVotes[i.Message.ID] = stats - err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseUpdateMessage, - Data: &discordgo.InteractionResponseData{ - Embeds: []*discordgo.MessageEmbed{ - constructVotesEmbed(pollVotes[i.Message.ID]), - }, Components: []discordgo.MessageComponent{ discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ discordgo.SelectMenu{ - CustomID: "choice", - Placeholder: "Select your favorite language", + // Select menu, as other components, must have a customID, so we set it to this value. + CustomID: "select", + Placeholder: "Choose your favorite programming language 👇", Options: []discordgo.SelectMenuOption{ { - Label: "Go", - Value: "Go", - Description: "Go programming language.", - Emoji: discordgo.ComponentEmoji{}, + Label: "Go", + // As with components, this things must have their own unique "id" to identify which is which. + // In this case such id is Value field. + Value: "go", + Emoji: discordgo.ComponentEmoji{ + Name: "đŸĻĻ", + }, + // You can also make it a default option, but in this case we won't. Default: false, + Description: "Go programming language", }, { - Label: "JS", - Value: "javascript", - Description: "JavaScript programming language.", - Emoji: discordgo.ComponentEmoji{}, - Default: false, + Label: "JS", + Value: "js", + Emoji: discordgo.ComponentEmoji{ + Name: "🟨", + }, + Description: "JavaScript programming language", }, { - Label: "Python", - Value: "py", - Description: "Python programming language.", - Emoji: discordgo.ComponentEmoji{Name: ""}, - Default: false, + Label: "Python", + Value: "py", + Emoji: discordgo.ComponentEmoji{ + Name: "🐍", + }, + Description: "Python programming language", }, }, }, }, }, - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.Button{ - Label: "End the poll", - CustomID: "end_poll", - Style: discordgo.DangerButton, - Disabled: false, - }, - }, - }, }, }, - }) - if err != nil { - panic(err) } - return - } - if _, ok := stats.PeopleVoted[i.Member.User.ID]; ok { - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + case "multi": + response = &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ - Content: "You already voted", - Flags: 1 << 6, - }, - }) - return - } - stats.PeopleVoted[i.Member.User.ID] = i.MessageComponentData().Values[0] - if stats.Votes == nil { - stats.Votes = map[string]int{"Go": 0, "Python": 0, "JS": 0} - } - - stats.Votes[i.MessageComponentData().Values[0]]++ - pollVotes[i.Message.ID] = stats - err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseUpdateMessage, - Data: &discordgo.InteractionResponseData{ - Embeds: []*discordgo.MessageEmbed{ - constructVotesEmbed(pollVotes[i.Message.ID]), - }, - Components: []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.SelectMenu{ - CustomID: "choice", - Placeholder: "Select your favorite language", - Options: []discordgo.SelectMenuOption{ - { - Label: "Go", - Value: "Go", - Description: "Go programming language.", - Emoji: discordgo.ComponentEmoji{}, - Default: false, - }, - { - Label: "JS", - Value: "JS", - Description: "JavaScript programming language.", - Emoji: discordgo.ComponentEmoji{}, - Default: false, - }, - { - Label: "Python", - Value: "Python", - Description: "Python programming language.", - Emoji: discordgo.ComponentEmoji{Name: ""}, - Default: false, + Content: "The tastiest things are left for the end. Let's see how the multi-item select menu works:" + + "try generating your own stackoverflow search link", + Flags: 1 << 6, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.SelectMenu{ + CustomID: "stackoverflow_tags", + Placeholder: "Select tags to search on StackOverflow", + // This is where confusion comes from. If you don't specify these things you will get single item select. + // These fields control the minimum and maximum amount of selected items. + MinValues: 1, + MaxValues: 3, + Options: []discordgo.SelectMenuOption{ + { + Label: "Go", + Description: "Simple yet powerful programming language", + Value: "go", + // Default works the same for multi-select menus. + Default: false, + Emoji: discordgo.ComponentEmoji{ + Name: "đŸĻĻ", + }, + }, + { + Label: "JS", + Description: "Multiparadigm OOP language", + Value: "javascript", + Emoji: discordgo.ComponentEmoji{ + Name: "🟨", + }, + }, + { + Label: "Python", + Description: "OOP prototyping programming language", + Value: "python", + Emoji: discordgo.ComponentEmoji{ + Name: "🐍", + }, + }, + { + Label: "Web", + Description: "Web related technologies", + Value: "web", + Emoji: discordgo.ComponentEmoji{ + Name: "🌐", + }, + }, + { + Label: "Desktop", + Description: "Desktop applications", + Value: "desktop", + Emoji: discordgo.ComponentEmoji{ + Name: "đŸ’ģ", + }, + }, }, }, }, }, }, - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.Button{ - Label: "End the poll", - CustomID: "end_poll", - Style: discordgo.DangerButton, - Disabled: false, - }, - }, - }, }, - }, - }) + } + + } + err := s.InteractionRespond(i.Interaction, response) if err != nil { panic(err) } + }, + } +) + +func main() { + s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { + log.Println("Bot is up!") + }) + // Components are part of interactions, so we register InteractionCreate handler + s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { + switch i.Type { + case discordgo.InteractionApplicationCommand: + if h, ok := commandsHandlers[i.ApplicationCommandData().Name]; ok { + h(s, i) + } + case discordgo.InteractionMessageComponent: + + if h, ok := componentsHandlers[i.MessageComponentData().CustomID]; ok { + h(s, i) + } } }) _, err := s.ApplicationCommandCreate(*AppID, *GuildID, &discordgo.ApplicationCommand{ @@ -392,7 +417,19 @@ func main() { log.Fatalf("Cannot create slash command: %v", err) } _, err = s.ApplicationCommandCreate(*AppID, *GuildID, &discordgo.ApplicationCommand{ - Name: "poll", + Name: "selects", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "multi", + Description: "Multi-item select menu", + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "single", + Description: "Single-item select menu", + }, + }, Description: "Lo and behold: dropdowns are coming", }) From 213ca6713aba3852e363a5b5d1663cc18a0db641 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Mon, 26 Jul 2021 22:41:55 +0300 Subject: [PATCH 34/39] Fixed typo in components example --- examples/components/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/components/main.go b/examples/components/main.go index 48d179ee3..1a323c203 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -317,7 +317,7 @@ var ( response = &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ - Content: "The tastiest things are left for the end. Let's see how the multi-item select menu works:" + + Content: "The tastiest things are left for the end. Let's see how the multi-item select menu works: " + "try generating your own stackoverflow search link", Flags: 1 << 6, Components: []discordgo.MessageComponent{ From 464cb59f32060d221a0e22e0ce2597a106a57a39 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Fri, 30 Jul 2021 19:32:46 +0300 Subject: [PATCH 35/39] Merge fix --- examples/components/main.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/components/main.go b/examples/components/main.go index 1a323c203..4a84df9d2 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -50,7 +50,7 @@ var ( }, Label: "Documentation", Style: discordgo.LinkButton, - URL: "https://discord.com/developers/docs/interactions/message-components#buttons", + Link: "https://discord.com/developers/docs/interactions/message-components#buttons", }, discordgo.Button{ Emoji: discordgo.ComponentEmoji{ @@ -58,7 +58,7 @@ var ( }, Label: "Discord developers", Style: discordgo.LinkButton, - URL: "https://discord.gg/discord-developers", + Link: "https://discord.gg/discord-developers", }, discordgo.Button{ Emoji: discordgo.ComponentEmoji{ @@ -66,7 +66,7 @@ var ( }, Label: "Discord Gophers", Style: discordgo.LinkButton, - URL: "https://discord.gg/7RuRrVHyXF", + Link: "https://discord.gg/7RuRrVHyXF", }, }, }, @@ -93,7 +93,7 @@ var ( }, Label: "Discord developers", Style: discordgo.LinkButton, - URL: "https://discord.gg/discord-developers", + Link: "https://discord.gg/discord-developers", }, discordgo.Button{ Emoji: discordgo.ComponentEmoji{ @@ -101,7 +101,7 @@ var ( }, Label: "Discord Gophers", Style: discordgo.LinkButton, - URL: "https://discord.gg/7RuRrVHyXF", + Link: "https://discord.gg/7RuRrVHyXF", }, }, }, @@ -175,7 +175,7 @@ var ( }, Label: "Documentation", Style: discordgo.LinkButton, - URL: "https://discord.com/developers/docs/interactions/message-components#select-menus", + Link: "https://discord.com/developers/docs/interactions/message-components#select-menus", }, discordgo.Button{ Emoji: discordgo.ComponentEmoji{ @@ -183,7 +183,7 @@ var ( }, Label: "Discord developers", Style: discordgo.LinkButton, - URL: "https://discord.gg/discord-developers", + Link: "https://discord.gg/discord-developers", }, discordgo.Button{ Emoji: discordgo.ComponentEmoji{ @@ -191,7 +191,7 @@ var ( }, Label: "Discord Gophers", Style: discordgo.LinkButton, - URL: "https://discord.gg/7RuRrVHyXF", + Link: "https://discord.gg/7RuRrVHyXF", }, }, }, @@ -236,7 +236,7 @@ var ( Style: discordgo.LinkButton, Disabled: false, // Link buttons don't require CustomID and do not trigger the gateway/HTTP event - URL: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + Link: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", Emoji: discordgo.ComponentEmoji{ Name: "🤷", }, @@ -250,7 +250,7 @@ var ( Label: "Discord Developers server", Style: discordgo.LinkButton, Disabled: false, - URL: "https://discord.gg/discord-developers", + Link: "https://discord.gg/discord-developers", }, }, }, From 6450f396fc59d24219dd2e246c578480815e4c0e Mon Sep 17 00:00:00 2001 From: Carson Hoffman Date: Sun, 25 Jul 2021 13:39:31 -0400 Subject: [PATCH 36/39] Improve handling of invalid interaction situations --- interactions.go | 95 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 27 deletions(-) diff --git a/interactions.go b/interactions.go index 91b9fe525..600154bce 100644 --- a/interactions.go +++ b/interactions.go @@ -5,6 +5,7 @@ import ( "crypto/ed25519" "encoding/hex" "encoding/json" + "fmt" "io" "io/ioutil" "net/http" @@ -40,6 +41,30 @@ const ( ApplicationCommandOptionMentionable ApplicationCommandOptionType = 9 ) +func (t ApplicationCommandOptionType) String() string { + switch t { + case ApplicationCommandOptionSubCommand: + return "SubCommand" + case ApplicationCommandOptionSubCommandGroup: + return "SubCommandGroup" + case ApplicationCommandOptionString: + return "String" + case ApplicationCommandOptionInteger: + return "Integer" + case ApplicationCommandOptionBoolean: + return "Boolean" + case ApplicationCommandOptionUser: + return "User" + case ApplicationCommandOptionChannel: + return "Channel" + case ApplicationCommandOptionRole: + return "Role" + case ApplicationCommandOptionMentionable: + return "Mentionable" + } + return fmt.Sprintf("ApplicationCommandOptionType(%d)", t) +} + // ApplicationCommandOption represents an option/subcommand/subcommands group. type ApplicationCommandOption struct { Type ApplicationCommandOptionType `json:"type"` @@ -69,6 +94,18 @@ const ( InteractionMessageComponent ) +func (t InteractionType) String() string { + switch t { + case InteractionPing: + return "Ping" + case InteractionApplicationCommand: + return "ApplicationCommand" + case InteractionMessageComponent: + return "MessageComponent" + } + return fmt.Sprintf("InteractionType(%d)", t) +} + // Interaction represents data of an interaction. type Interaction struct { ID string `json:"id"` @@ -135,11 +172,17 @@ func (i *Interaction) UnmarshalJSON(raw []byte) (err error) { // MessageComponentData is helper function to assert the inner InteractionData to MessageComponentInteractionData. // Make sure to check that the Type of the interaction is InteractionMessageComponent before calling. func (i Interaction) MessageComponentData() (data MessageComponentInteractionData) { + if i.Type != InteractionMessageComponent { + panic("MessageComponentData called on interaction of type " + i.Type.String()) + } return i.Data.(MessageComponentInteractionData) } // ApplicationCommandData is helper function to convert InteractionData to ApplicationCommandInteractionData. func (i Interaction) ApplicationCommandData() (data ApplicationCommandInteractionData) { + if i.Type != InteractionApplicationCommand { + panic("ApplicationCommandData called on interaction of type " + i.Type.String()) + } return i.Data.(ApplicationCommandInteractionData) } @@ -176,32 +219,32 @@ func (MessageComponentInteractionData) Type() InteractionType { // ApplicationCommandInteractionDataOption represents an option of a slash command. type ApplicationCommandInteractionDataOption struct { - Name string `json:"name"` - // NOTE: Contains the value specified by InteractionType. + Name string `json:"name"` + Type ApplicationCommandOptionType `json:"type"` + // NOTE: Contains the value specified by Type. Value interface{} `json:"value,omitempty"` Options []*ApplicationCommandInteractionDataOption `json:"options,omitempty"` } // IntValue is a utility function for casting option value to integer func (o ApplicationCommandInteractionDataOption) IntValue() int64 { - if v, ok := o.Value.(float64); ok { - return int64(v) + if o.Type != ApplicationCommandOptionInteger { + panic("IntValue called on data option of type " + o.Type.String()) } - - return 0 + return int64(o.Value.(float64)) } // UintValue is a utility function for casting option value to unsigned integer func (o ApplicationCommandInteractionDataOption) UintValue() uint64 { - if v, ok := o.Value.(float64); ok { - return uint64(v) + if o.Type != ApplicationCommandOptionInteger { + panic("UintValue called on data option of type " + o.Type.String()) } - - return 0 + return uint64(o.Value.(float64)) } // FloatValue is a utility function for casting option value to float func (o ApplicationCommandInteractionDataOption) FloatValue() float64 { + // TODO: limit calls to Number type once it is released if v, ok := o.Value.(float64); ok { return v } @@ -211,29 +254,27 @@ func (o ApplicationCommandInteractionDataOption) FloatValue() float64 { // StringValue is a utility function for casting option value to string func (o ApplicationCommandInteractionDataOption) StringValue() string { - if v, ok := o.Value.(string); ok { - return v + if o.Type != ApplicationCommandOptionString { + panic("StringValue called on data option of type " + o.Type.String()) } - - return "" + return o.Value.(string) } // BoolValue is a utility function for casting option value to bool func (o ApplicationCommandInteractionDataOption) BoolValue() bool { - if v, ok := o.Value.(bool); ok { - return v + if o.Type != ApplicationCommandOptionBoolean { + panic("BoolValue called on data option of type " + o.Type.String()) } - - return false + return o.Value.(bool) } // ChannelValue is a utility function for casting option value to channel object. // s : Session object, if not nil, function additionally fetches all channel's data func (o ApplicationCommandInteractionDataOption) ChannelValue(s *Session) *Channel { - chanID := o.StringValue() - if chanID == "" { - return nil + if o.Type != ApplicationCommandOptionChannel { + panic("ChannelValue called on data option of type " + o.Type.String()) } + chanID := o.Value.(string) if s == nil { return &Channel{ID: chanID} @@ -253,10 +294,10 @@ func (o ApplicationCommandInteractionDataOption) ChannelValue(s *Session) *Chann // RoleValue is a utility function for casting option value to role object. // s : Session object, if not nil, function additionally fetches all role's data func (o ApplicationCommandInteractionDataOption) RoleValue(s *Session, gID string) *Role { - roleID := o.StringValue() - if roleID == "" { - return nil + if o.Type != ApplicationCommandOptionRole && o.Type != ApplicationCommandOptionMentionable { + panic("RoleValue called on data option of type " + o.Type.String()) } + roleID := o.Value.(string) if s == nil || gID == "" { return &Role{ID: roleID} @@ -281,10 +322,10 @@ func (o ApplicationCommandInteractionDataOption) RoleValue(s *Session, gID strin // UserValue is a utility function for casting option value to user object. // s : Session object, if not nil, function additionally fetches all user's data func (o ApplicationCommandInteractionDataOption) UserValue(s *Session) *User { - userID := o.StringValue() - if userID == "" { - return nil + if o.Type != ApplicationCommandOptionUser && o.Type != ApplicationCommandOptionMentionable { + panic("UserValue called on data option of type " + o.Type.String()) } + userID := o.Value.(string) if s == nil { return &User{ID: userID} From c6f37ada5f461459c85803ad9b0090c573a99b63 Mon Sep 17 00:00:00 2001 From: plally Date: Fri, 30 Jul 2021 11:22:08 -0400 Subject: [PATCH 37/39] support allowing webhook edits with files, and responding to interactions with files (#931) * allow files in webhook message edits * add Files to WebhookEdit struct * move the construction of the multipart body for files into a shared function * allow interaction responses to have files * go fmt * fix err shadowing * document MakeFilesBody * rename MakeFilesBody -> EncodeWithFiles. fix InteractionRespond responding twice * use resp in InteractionRespond files, add basic-command-with-files example command * import strings and go fmt * EncodeWithFiles -> MultiPartBodyWithJSON * go fmt * fix example for slash_commands * move files to responsedata --- examples/slash_commands/main.go | 20 ++++ interactions.go | 5 +- restapi.go | 169 +++++--------------------------- util.go | 60 ++++++++++++ webhook.go | 1 + 5 files changed, 111 insertions(+), 144 deletions(-) diff --git a/examples/slash_commands/main.go b/examples/slash_commands/main.go index 14e4ea6a5..64e37bfc6 100644 --- a/examples/slash_commands/main.go +++ b/examples/slash_commands/main.go @@ -6,6 +6,7 @@ import ( "log" "os" "os/signal" + "strings" "time" "github.com/bwmarrin/discordgo" @@ -39,6 +40,10 @@ var ( // of the command. Description: "Basic command", }, + { + Name: "basic-command-with-files", + Description: "Basic command with files", + }, { Name: "options", Description: "Command for demonstrating options", @@ -160,6 +165,21 @@ var ( }, }) }, + "basic-command-with-files": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Hey there! Congratulations, you just executed your first slash command with a file in the response", + Files: []*discordgo.File{ + { + ContentType: "text/plain", + Name: "test.txt", + Reader: strings.NewReader("Hello Discord!!"), + }, + }, + }, + }) + }, "options": func(s *discordgo.Session, i *discordgo.InteractionCreate) { margs := []interface{}{ // Here we need to convert raw interface{} value to wanted type. diff --git a/interactions.go b/interactions.go index 600154bce..0c55ad708 100644 --- a/interactions.go +++ b/interactions.go @@ -369,7 +369,10 @@ type InteractionResponseData struct { Components []MessageComponent `json:"components"` Embeds []*MessageEmbed `json:"embeds,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` - Flags uint64 `json:"flags,omitempty"` + + Flags uint64 `json:"flags,omitempty"` + + Files []*File `json:"-"` } // VerifyInteraction implements message verification of the discord interactions api diff --git a/restapi.go b/restapi.go index d4f28f455..66e32aedc 100644 --- a/restapi.go +++ b/restapi.go @@ -21,9 +21,7 @@ import ( "io" "io/ioutil" "log" - "mime/multipart" "net/http" - "net/textproto" "net/url" "strconv" "strings" @@ -1573,55 +1571,12 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) var response []byte if len(files) > 0 { - body := &bytes.Buffer{} - bodywriter := multipart.NewWriter(body) - - var payload []byte - payload, err = json.Marshal(data) - if err != nil { - return - } - - var p io.Writer - - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", `form-data; name="payload_json"`) - h.Set("Content-Type", "application/json") - - p, err = bodywriter.CreatePart(h) - if err != nil { - return - } - - if _, err = p.Write(payload); err != nil { - return - } - - for i, file := range files { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name))) - contentType := file.ContentType - if contentType == "" { - contentType = "application/octet-stream" - } - h.Set("Content-Type", contentType) - - p, err = bodywriter.CreatePart(h) - if err != nil { - return - } - - if _, err = io.Copy(p, file.Reader); err != nil { - return - } - } - - err = bodywriter.Close() - if err != nil { - return + contentType, body, encodeErr := MultipartBodyWithJSON(data, files) + if encodeErr != nil { + return st, encodeErr } - response, err = s.request("POST", endpoint, bodywriter.FormDataContentType(), body.Bytes(), endpoint, 0) + response, err = s.request("POST", endpoint, contentType, body, endpoint, 0) } else { response, err = s.RequestWithBucketID("POST", endpoint, data, endpoint) } @@ -2176,55 +2131,12 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho var response []byte if len(data.Files) > 0 { - body := &bytes.Buffer{} - bodywriter := multipart.NewWriter(body) - - var payload []byte - payload, err = json.Marshal(data) - if err != nil { - return - } - - var p io.Writer - - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", `form-data; name="payload_json"`) - h.Set("Content-Type", "application/json") - - p, err = bodywriter.CreatePart(h) - if err != nil { - return - } - - if _, err = p.Write(payload); err != nil { - return - } - - for i, file := range data.Files { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name))) - contentType := file.ContentType - if contentType == "" { - contentType = "application/octet-stream" - } - h.Set("Content-Type", contentType) - - p, err = bodywriter.CreatePart(h) - if err != nil { - return - } - - if _, err = io.Copy(p, file.Reader); err != nil { - return - } - } - - err = bodywriter.Close() - if err != nil { - return + contentType, body, encodeErr := MultipartBodyWithJSON(data, data.Files) + if encodeErr != nil { + return st, encodeErr } - response, err = s.request("POST", uri, bodywriter.FormDataContentType(), body.Bytes(), uri, 0) + response, err = s.request("POST", uri, contentType, body, uri, 0) } else { response, err = s.RequestWithBucketID("POST", uri, data, uri) } @@ -2242,62 +2154,25 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho // messageID : The ID of message to edit func (s *Session) WebhookMessageEdit(webhookID, token, messageID string, data *WebhookEdit) (st *Message, err error) { uri := EndpointWebhookMessage(webhookID, token, messageID) + var response []byte if len(data.Files) > 0 { - body := &bytes.Buffer{} - bodywriter := multipart.NewWriter(body) - - var payload []byte - payload, err = json.Marshal(data) + contentType, body, err := MultipartBodyWithJSON(data, data.Files) if err != nil { - return + return nil, err } - var p io.Writer - - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", `form-data; name="payload_json"`) - h.Set("Content-Type", "application/json") - - p, err = bodywriter.CreatePart(h) + response, err = s.request("PATCH", uri, contentType, body, uri, 0) if err != nil { - return - } - - if _, err = p.Write(payload); err != nil { - return + return nil, err } - for i, file := range data.Files { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name))) - contentType := file.ContentType - if contentType == "" { - contentType = "application/octet-stream" - } - h.Set("Content-Type", contentType) - - p, err = bodywriter.CreatePart(h) - if err != nil { - return - } - - if _, err = io.Copy(p, file.Reader); err != nil { - return - } - } + } else { + response, err = s.RequestWithBucketID("PATCH", uri, data, EndpointWebhookToken("", "")) - err = bodywriter.Close() if err != nil { - return + return nil, err } - - response, err = s.request("PATCH", uri, bodywriter.FormDataContentType(), body.Bytes(), uri, 0) - } else { - response, err = s.RequestWithBucketID("PATCH", uri, data, uri) - } - if err != nil { - return } err = unmarshal(response, &st) @@ -2605,11 +2480,19 @@ func (s *Session) ApplicationCommands(appID, guildID string) (cmd []*Application // appID : The application ID. // interaction : Interaction instance. // resp : Response message data. -func (s *Session) InteractionRespond(interaction *Interaction, resp *InteractionResponse) error { +func (s *Session) InteractionRespond(interaction *Interaction, resp *InteractionResponse) (err error) { endpoint := EndpointInteractionResponse(interaction.ID, interaction.Token) - _, err := s.RequestWithBucketID("POST", endpoint, *resp, endpoint) + if resp.Data != nil && len(resp.Data.Files) > 0 { + contentType, body, err := MultipartBodyWithJSON(resp, resp.Data.Files) + if err != nil { + return err + } + _, err = s.request("POST", endpoint, contentType, body, endpoint, 0) + } else { + _, err = s.RequestWithBucketID("POST", endpoint, *resp, endpoint) + } return err } diff --git a/util.go b/util.go index 8a2b2e01d..fae34f716 100644 --- a/util.go +++ b/util.go @@ -1,6 +1,12 @@ package discordgo import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/textproto" "strconv" "time" ) @@ -15,3 +21,57 @@ func SnowflakeTimestamp(ID string) (t time.Time, err error) { t = time.Unix(0, timestamp*1000000) return } + +// MultipartBodyWithJSON returns the contentType and body for a discord request +// data : The object to encode for payload_json in the multipart request +// files : Files to include in the request +func MultipartBodyWithJSON(data interface{}, files []*File) (requestContentType string, requestBody []byte, err error) { + body := &bytes.Buffer{} + bodywriter := multipart.NewWriter(body) + + payload, err := json.Marshal(data) + if err != nil { + return + } + + var p io.Writer + + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="payload_json"`) + h.Set("Content-Type", "application/json") + + p, err = bodywriter.CreatePart(h) + if err != nil { + return + } + + if _, err = p.Write(payload); err != nil { + return + } + + for i, file := range files { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name))) + contentType := file.ContentType + if contentType == "" { + contentType = "application/octet-stream" + } + h.Set("Content-Type", contentType) + + p, err = bodywriter.CreatePart(h) + if err != nil { + return + } + + if _, err = io.Copy(p, file.Reader); err != nil { + return + } + } + + err = bodywriter.Close() + if err != nil { + return + } + + return bodywriter.FormDataContentType(), body.Bytes(), nil +} diff --git a/webhook.go b/webhook.go index f44fdbd49..8319a952f 100644 --- a/webhook.go +++ b/webhook.go @@ -45,5 +45,6 @@ type WebhookEdit struct { Components []MessageComponent `json:"components"` Files []*File `json:"-"` Embeds []*MessageEmbed `json:"embeds,omitempty"` + Files []*File `json:"-"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` } From d44abc7f915eb3a316ed6cfea00bcc68dee197e9 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Fri, 30 Jul 2021 20:22:15 +0300 Subject: [PATCH 38/39] Merge fixes --- components.go | 9 ++++----- examples/components/main.go | 22 +++++++++++----------- interactions.go | 16 ---------------- restapi.go | 24 +++++++++++++++++------- webhook.go | 1 - 5 files changed, 32 insertions(+), 40 deletions(-) diff --git a/components.go b/components.go index cfde3fa01..124e95e5c 100644 --- a/components.go +++ b/components.go @@ -118,10 +118,10 @@ type ComponentEmoji struct { // Button represents button component. type Button struct { - Label string `json:"label"` - Style ButtonStyle `json:"style"` - Disabled bool `json:"disabled"` - Emoji ButtonEmoji `json:"emoji"` + Label string `json:"label"` + Style ButtonStyle `json:"style"` + Disabled bool `json:"disabled"` + Emoji ComponentEmoji `json:"emoji"` // NOTE: Only button with LinkButton style can have link. Also, URL is mutually exclusive with CustomID. URL string `json:"url,omitempty"` @@ -190,4 +190,3 @@ func (m SelectMenu) MarshalJSON() ([]byte, error) { Type: m.Type(), }) } - diff --git a/examples/components/main.go b/examples/components/main.go index 4a84df9d2..2e80c6411 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -50,7 +50,7 @@ var ( }, Label: "Documentation", Style: discordgo.LinkButton, - Link: "https://discord.com/developers/docs/interactions/message-components#buttons", + URL: "https://discord.com/developers/docs/interactions/message-components#buttons", }, discordgo.Button{ Emoji: discordgo.ComponentEmoji{ @@ -58,7 +58,7 @@ var ( }, Label: "Discord developers", Style: discordgo.LinkButton, - Link: "https://discord.gg/discord-developers", + URL: "https://discord.gg/discord-developers", }, discordgo.Button{ Emoji: discordgo.ComponentEmoji{ @@ -66,7 +66,7 @@ var ( }, Label: "Discord Gophers", Style: discordgo.LinkButton, - Link: "https://discord.gg/7RuRrVHyXF", + URL: "https://discord.gg/7RuRrVHyXF", }, }, }, @@ -93,7 +93,7 @@ var ( }, Label: "Discord developers", Style: discordgo.LinkButton, - Link: "https://discord.gg/discord-developers", + URL: "https://discord.gg/discord-developers", }, discordgo.Button{ Emoji: discordgo.ComponentEmoji{ @@ -101,7 +101,7 @@ var ( }, Label: "Discord Gophers", Style: discordgo.LinkButton, - Link: "https://discord.gg/7RuRrVHyXF", + URL: "https://discord.gg/7RuRrVHyXF", }, }, }, @@ -156,7 +156,7 @@ var ( err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ - Content: "Here is your stackoverflow link: " + fmt.Sprintf(stackoverflowFormat, strings.Join(data.Values, "+")), + Content: "Here is your stackoverflow URL: " + fmt.Sprintf(stackoverflowFormat, strings.Join(data.Values, "+")), Flags: 1 << 6, }, }) @@ -175,7 +175,7 @@ var ( }, Label: "Documentation", Style: discordgo.LinkButton, - Link: "https://discord.com/developers/docs/interactions/message-components#select-menus", + URL: "https://discord.com/developers/docs/interactions/message-components#select-menus", }, discordgo.Button{ Emoji: discordgo.ComponentEmoji{ @@ -183,7 +183,7 @@ var ( }, Label: "Discord developers", Style: discordgo.LinkButton, - Link: "https://discord.gg/discord-developers", + URL: "https://discord.gg/discord-developers", }, discordgo.Button{ Emoji: discordgo.ComponentEmoji{ @@ -191,7 +191,7 @@ var ( }, Label: "Discord Gophers", Style: discordgo.LinkButton, - Link: "https://discord.gg/7RuRrVHyXF", + URL: "https://discord.gg/7RuRrVHyXF", }, }, }, @@ -236,7 +236,7 @@ var ( Style: discordgo.LinkButton, Disabled: false, // Link buttons don't require CustomID and do not trigger the gateway/HTTP event - Link: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + URL: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", Emoji: discordgo.ComponentEmoji{ Name: "🤷", }, @@ -250,7 +250,7 @@ var ( Label: "Discord Developers server", Style: discordgo.LinkButton, Disabled: false, - Link: "https://discord.gg/discord-developers", + URL: "https://discord.gg/discord-developers", }, }, }, diff --git a/interactions.go b/interactions.go index bbeea440a..0ba1dfcd2 100644 --- a/interactions.go +++ b/interactions.go @@ -218,22 +218,6 @@ func (MessageComponentInteractionData) Type() InteractionType { return InteractionMessageComponent } -// Type returns the type of interaction data. -func (ApplicationCommandInteractionData) Type() InteractionType { - return InteractionApplicationCommand -} - -// MessageComponentInteractionData contains the data of message component interaction. -type MessageComponentInteractionData struct { - CustomID string `json:"custom_id"` - ComponentType ComponentType `json:"component_type"` -} - -// Type returns the type of interaction data. -func (MessageComponentInteractionData) Type() InteractionType { - return InteractionMessageComponent -} - // ApplicationCommandInteractionDataOption represents an option of a slash command. type ApplicationCommandInteractionDataOption struct { Name string `json:"name"` diff --git a/restapi.go b/restapi.go index b5321fcb0..a4f1f6e98 100644 --- a/restapi.go +++ b/restapi.go @@ -2148,6 +2148,23 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho return } +// WebhookMessage gets a webhook message. +// webhookID : The ID of a webhook +// token : The auth token for the webhook +// messageID : The ID of message to get +func (s *Session) WebhookMessage(webhookID, token, messageID string) (message *Message, err error) { + uri := EndpointWebhookMessage(webhookID, token, messageID) + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointWebhookToken("", "")) + if err != nil { + return + } + + err = json.Unmarshal(body, &message) + + return +} + // WebhookMessageEdit edits a webhook message and returns a new one. // webhookID : The ID of a webhook // token : The auth token for the webhook @@ -2510,13 +2527,6 @@ func (s *Session) InteractionResponseEdit(appID string, interaction *Interaction return s.WebhookMessageEdit(appID, interaction.Token, "@original", newresp) } -// InteractionResponse retrieves and returns the interaction response message. -// appID : The application ID. -// interaction : Interaction instance. -func (s *Session) InteractionResponse(appID string, interaction *Interaction) (*Message, error) { - return s.WebhookMessage(appID, interaction.Token, "@original") -} - // InteractionResponseDelete deletes the response to an interaction. // appID : The application ID. // interaction : Interaction instance. diff --git a/webhook.go b/webhook.go index 8319a952f..f54a45ce1 100644 --- a/webhook.go +++ b/webhook.go @@ -43,7 +43,6 @@ type WebhookParams struct { type WebhookEdit struct { Content string `json:"content,omitempty"` Components []MessageComponent `json:"components"` - Files []*File `json:"-"` Embeds []*MessageEmbed `json:"embeds,omitempty"` Files []*File `json:"-"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` From 072a9ff725f947507ed6ad3ba248c2dcf51f51f2 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Sat, 31 Jul 2021 20:30:30 +0300 Subject: [PATCH 39/39] Fixed rebase consequences --- interactions.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/interactions.go b/interactions.go index 0ba1dfcd2..281dd7960 100644 --- a/interactions.go +++ b/interactions.go @@ -194,9 +194,20 @@ type InteractionData interface { // ApplicationCommandInteractionData contains the data of application command interaction. type ApplicationCommandInteractionData struct { - ID string `json:"id"` - Name string `json:"name"` - Options []*ApplicationCommandInteractionDataOption `json:"options"` + ID string `json:"id"` + Name string `json:"name"` + Resolved *ApplicationCommandInteractionDataResolved `json:"resolved"` + Options []*ApplicationCommandInteractionDataOption `json:"options"` +} + +// ApplicationCommandInteractionDataResolved contains resolved data for command arguments. +// Partial Member objects are missing user, deaf and mute fields. +// Partial Channel objects only have id, name, type and permissions fields. +type ApplicationCommandInteractionDataResolved struct { + Users map[string]*User `json:"users"` + Members map[string]*Member `json:"members"` + Roles map[string]*Role `json:"roles"` + Channels map[string]*Channel `json:"channels"` } // Type returns the type of interaction data.