From 421e14965030a2bf9d787bf9de8592c7944fae11 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] 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 | 149 +++++++++++++++++++++++++++++++ events.go | 20 +++++ examples/components/main.go | 151 ++++++++++++++++++++++++++++++++ examples/slash_commands/main.go | 42 ++++----- interactions.go | 106 +++++++++++++++++++--- message.go | 25 ++++++ state.go | 3 + webhook.go | 2 + 8 files changed, 464 insertions(+), 34 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 0000000..c786bca --- /dev/null +++ b/components.go @@ -0,0 +1,149 @@ +package discordgo + +import ( + "encoding/json" +) + +// ComponentType is type of component. +type ComponentType uint + +// MessageComponent types. +const ( + ActionsRowComponent ComponentType = 1 + ButtonComponent ComponentType = 2 +) + +// MessageComponent is a base interface for all message components. +type MessageComponent interface { + json.Marshaler + Type() ComponentType +} + +type unmarshalableMessageComponent struct { + MessageComponent +} + +// UnmarshalJSON is a helper function to unmarshal MessageComponent object. +func (umc *unmarshalableMessageComponent) UnmarshalJSON(src []byte) error { + var v struct { + Type ComponentType `json:"type"` + } + err := json.Unmarshal(src, &v) + if err != nil { + return err + } + + 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 err + } + umc.MessageComponent = data + return err +} + +// ActionsRow is a container for components within one row. +type ActionsRow struct { + Components []MessageComponent `json:"components"` +} + +// MarshalJSON is a method for marshaling ActionsRow to a JSON object. +func (r ActionsRow) MarshalJSON() ([]byte, error) { + type actionsRow ActionsRow + + return json.Marshal(struct { + actionsRow + Type ComponentType `json:"type"` + }{ + actionsRow: actionsRow(r), + Type: r.Type(), + }) +} + +// UnmarshalJSON is a helper function to unmarshal Actions Row. +func (r *ActionsRow) UnmarshalJSON(data []byte) error { + var v struct { + RawComponents []unmarshalableMessageComponent `json:"components"` + } + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + r.Components = make([]MessageComponent, len(v.RawComponents)) + for i, v := range v.RawComponents { + r.Components[i] = v.MessageComponent + } + return err +} + +// Type is a method to get the type of a component. +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 = 1 + // SecondaryButton is a button with grey color. + SecondaryButton ButtonStyle = 2 + // SuccessButton is a button with green color. + SuccessButton ButtonStyle = 3 + // DangerButton is a button with red color. + DangerButton ButtonStyle = 4 + // LinkButton is a special type of button which navigates to a URL. Has grey color. + LinkButton ButtonStyle = 5 +) + +// 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, URL is mutually exclusive with CustomID. + URL string `json:"url,omitempty"` + 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 + + if b.Style == 0 { + b.Style = PrimaryButton + } + + return json.Marshal(struct { + button + Type ComponentType `json:"type"` + }{ + button: button(b), + Type: b.Type(), + }) +} + +// Type is a method to get the type of a component. +func (b Button) Type() ComponentType { + return ButtonComponent +} diff --git a/events.go b/events.go index 87ad7d9..a1647fa 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 0000000..fee7480 --- /dev/null +++ b/examples/components/main.go @@ -0,0 +1,151 @@ +package main + +import ( + "flag" + "log" + "os" + "os/signal" + + "github.com/bwmarrin/discordgo" +) + +// Bot parameters +var ( + GuildID = flag.String("guild", "", "Test guild 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.ApplicationCommandData().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.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 don't require CustomID and do not trigger the gateway/HTTP event + URL: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + Emoji: discordgo.ButtonEmoji{ + Name: "🤷", + }, + }, + }, + }, + // The message may have multiple actions rows. + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "Discord Developers server", + Style: discordgo.LinkButton, + Disabled: false, + URL: "https://discord.gg/discord-developers", + }, + }, + }, + }, + }, + }) + if err != nil { + panic(err) + } + } + 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 to which they are attached. + // 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, + URL: "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, 1) + 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 e615fec..57d27ca 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{ @@ -258,8 +258,8 @@ var ( } err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseType(i.Data.Options[0].IntValue()), - Data: &discordgo.InteractionApplicationCommandResponseData{ + Type: discordgo.InteractionResponseType(i.ApplicationCommandData().Options[0].IntValue()), + 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 3c27760..4cb115b 100644 --- a/interactions.go +++ b/interactions.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/ed25519" "encoding/hex" + "encoding/json" "io" "io/ioutil" "net/http" @@ -65,15 +66,20 @@ type InteractionType uint8 const ( InteractionPing InteractionType = 1 InteractionApplicationCommand InteractionType = 2 + InteractionMessageComponent InteractionType = 3 ) -// 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:"-"` + GuildID string `json:"guild_id"` + ChannelID string `json:"channel_id"` + + // The message on which interaction was used. + // NOTE: this field is only filled when a button click triggered the interaction. 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,7 +96,60 @@ type Interaction struct { Version int `json:"version"` } -// ApplicationCommandInteractionData contains data received in an interaction event. +type interaction Interaction + +type rawInteraction struct { + interaction + Data json.RawMessage `json:"data"` +} + +// UnmarshalJSON is a method for unmarshalling JSON object to Interaction. +func (i *Interaction) UnmarshalJSON(raw []byte) error { + var tmp rawInteraction + err := json.Unmarshal(raw, &tmp) + if err != nil { + return err + } + + *i = Interaction(tmp.interaction) + + switch tmp.Type { + case InteractionApplicationCommand: + v := ApplicationCommandInteractionData{} + err = json.Unmarshal(tmp.Data, &v) + if err != nil { + return err + } + i.Data = v + case InteractionMessageComponent: + v := MessageComponentInteractionData{} + err = json.Unmarshal(tmp.Data, &v) + if err != nil { + return err + } + i.Data = v + } + return nil +} + +// 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) +} + +// ApplicationCommandData is helper function to assert the inner InteractionData to ApplicationCommandInteractionData. +// Make sure to check that the Type of the interaction is InteractionApplicationCommand before calling. +func (i Interaction) ApplicationCommandData() (data ApplicationCommandInteractionData) { + return i.Data.(ApplicationCommandInteractionData) +} + +// InteractionData is a common interface for all types of interaction data. +type InteractionData interface { + Type() InteractionType +} + +// ApplicationCommandInteractionData contains the data of application command interaction. type ApplicationCommandInteractionData struct { ID string `json:"id"` Name string `json:"name"` @@ -108,6 +167,22 @@ type ApplicationCommandInteractionDataResolved struct { Channels map[string]*Channel `json:"channels"` } +// 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"` @@ -243,18 +318,23 @@ const ( InteractionResponseChannelMessageWithSource InteractionResponseType = 4 // InteractionResponseDeferredChannelMessageWithSource acknowledges that the event was received, and that a follow-up will come later. 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. + InteractionResponseUpdateMessage InteractionResponseType = 7 ) // 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 []MessageComponent `json:"components"` Embeds []*MessageEmbed `json:"embeds,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` diff --git a/message.go b/message.go index c4445e6..b8cd8e4 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 []MessageComponent `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 2eeabd8..698612e 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 6c3d2df..d61b281 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"` }