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 <c@rsonhoffman.com>
This commit is contained in:
Fedor Lapshin 2021-06-27 19:16:52 +03:00 committed by GitHub
parent e72c457cb4
commit 421e149650
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 464 additions and 34 deletions

149
components.go Normal file
View file

@ -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
}

View file

@ -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)
}

151
examples/components/main.go Normal file
View file

@ -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")
}

View file

@ -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)
}
})

View file

@ -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"`

View file

@ -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"`

View file

@ -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
}

View file

@ -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"`
}