Explorar o código

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>
Fedor Lapshin %!s(int64=3) %!d(string=hai) anos
pai
achega
421e149650
Modificáronse 8 ficheiros con 464 adicións e 34 borrados
  1. 149 0
      components.go
  2. 20 0
      events.go
  3. 151 0
      examples/components/main.go
  4. 21 21
      examples/slash_commands/main.go
  5. 93 13
      interactions.go
  6. 25 0
      message.go
  7. 3 0
      state.go
  8. 2 0
      webhook.go

+ 149 - 0
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
+}

+ 20 - 0
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)
+}

+ 151 - 0
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")
+}

+ 21 - 21
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)
 		}
 	})

+ 93 - 13
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"`
 

+ 25 - 0
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"`
 

+ 3 - 0
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
 		}

+ 2 - 0
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"`
 }