Browse Source

Selects component (#954)

* 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>

* 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

* Godoc fix

* Gofmt fix

* Corrected naming and docs

* Rolled back API version

* Requested fixes

* Added support of components to webhook and regular messages

* Interactions: select menus

* Example fix

* Merge fix

* Some fixes

* Added missing documentation

* Fix components unmarshaling

* Godoc fix

* Requested fixes

* Fixed unmarshaling issues

* Components example: cleanup

* Gofmt fix

* Godoc fix

* URL field renaming fix

* Added flags to followups

* Updated components example

* Fixed typo in components example

* Merge fix

* Improve handling of invalid interaction situations

* 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

* Merge fixes

* Fixed rebase consequences

Co-authored-by: Carson Hoffman <c@rsonhoffman.com>
Co-authored-by: plally <pierce@vulpes.dev>
Fedor Lapshin 2 years ago
parent
commit
4ebe5a08ee
6 changed files with 453 additions and 96 deletions
  1. 50 7
      components.go
  2. 378 79
      examples/components/main.go
  3. 1 1
      examples/slash_commands/main.go
  4. 3 1
      interactions.go
  5. 19 8
      restapi.go
  6. 2 0
      webhook.go

+ 50 - 7
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.
@@ -82,6 +83,7 @@ func (r *ActionsRow) UnmarshalJSON(data []byte) error {
 	for i, v := range v.RawComponents {
 		r.Components[i] = v.MessageComponent
 	}
+
 	return err
 }
 
@@ -107,8 +109,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"`
@@ -116,10 +118,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, URL is mutually exclusive with CustomID.
 	URL      string `json:"url,omitempty"`
@@ -144,6 +146,47 @@ 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"`
+	// 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"`
+	// 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"`
+}
+
+// Type is a method to get the type of a component.
+func (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(),
+	})
+}

+ 378 - 79
examples/components/main.go

@@ -2,9 +2,12 @@ package main
 
 import (
 	"flag"
+	"fmt"
 	"log"
 	"os"
 	"os/signal"
+	"strings"
+	"time"
 
 	"github.com/bwmarrin/discordgo"
 )
@@ -28,110 +31,406 @@ func init() {
 	}
 }
 
-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",
+// Important note: call every command in order it's placed in the example.
+
+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: "📜",
 									},
-									discordgo.Button{
-										Label:    "No",
-										Style:    discordgo.DangerButton,
-										Disabled: false,
-										CustomID: "no_btn",
+									Label: "Documentation",
+									Style: discordgo.LinkButton,
+									URL:   "https://discord.com/developers/docs/interactions/message-components#buttons",
+								},
+								discordgo.Button{
+									Emoji: discordgo.ComponentEmoji{
+										Name: "🔧",
 									},
-									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: "🤷",
-										},
+									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",
 								},
 							},
-							// 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)
+			}
+		},
+		"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)
-				}
+				},
+			})
+			if err != nil {
+				panic(err)
 			}
-			return
-		}
-		// Type for button press will be always InteractionButton (3)
-		if i.Type != discordgo.InteractionMessageComponent {
-			return
-		}
+		},
+		"select": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
+			var response *discordgo.InteractionResponse
 
-		content := "Thanks for your feedback "
+			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()
 
-		// 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)"
-		}
+			const stackoverflowFormat = `https://stackoverflow.com/questions/tagged/%s`
 
-		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,
+			err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+				Type: discordgo.InteractionResponseChannelMessageWithSource,
+				Data: &discordgo.InteractionResponseData{
+					Content: "Here is your stackoverflow URL: " + 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{
-								Label:    "Our sponsor",
-								Style:    discordgo.LinkButton,
-								Disabled: false,
-								URL:      "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
-								Emoji: discordgo.ButtonEmoji{
-									Name: "💠",
+								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)
+			}
+		},
+	}
+	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 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 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: "fd_no",
+								},
+								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.ComponentEmoji{
+										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)
+			}
+		},
+		"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: "Now let's take a look on selects. This is single item select menu.",
+						Flags:   1 << 6,
+						Components: []discordgo.MessageComponent{
+							discordgo.ActionsRow{
+								Components: []discordgo.MessageComponent{
+									discordgo.SelectMenu{
+										// 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",
+												// 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: "js",
+												Emoji: discordgo.ComponentEmoji{
+													Name: "🟨",
+												},
+												Description: "JavaScript programming language",
+											},
+											{
+												Label: "Python",
+												Value: "py",
+												Emoji: discordgo.ComponentEmoji{
+													Name: "🐍",
+												},
+												Description: "Python programming language",
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+			case "multi":
+				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: " +
+							"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: "💻",
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+
+			}
+			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{
-		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: "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",
 	})
 
 	if err != nil {

+ 1 - 1
examples/slash_commands/main.go

@@ -290,7 +290,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.",

+ 3 - 1
interactions.go

@@ -219,6 +219,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.
@@ -379,7 +382,6 @@ type InteractionResponseData struct {
 	Embeds          []*MessageEmbed         `json:"embeds,omitempty"`
 	AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
 
-	// NOTE: Undocumented feature, be careful with it.
 	Flags uint64 `json:"flags,omitempty"`
 
 	Files []*File `json:"-"`

+ 19 - 8
restapi.go

@@ -2165,29 +2165,40 @@ func (s *Session) WebhookMessage(webhookID, token, messageID string) (message *M
 	return
 }
 
-// WebhookMessageEdit edits 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 edit
-func (s *Session) WebhookMessageEdit(webhookID, token, messageID string, data *WebhookEdit) (err error) {
+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 {
 		contentType, body, err := MultipartBodyWithJSON(data, data.Files)
 		if err != nil {
-			return err
+			return nil, err
 		}
 
-		_, err = s.request("PATCH", uri, contentType, body, uri, 0)
+		response, err = s.request("PATCH", uri, contentType, body, uri, 0)
+		if err != nil {
+			return nil, err
+		}
 	} else {
-		_, err = s.RequestWithBucketID("PATCH", uri, data, EndpointWebhookToken("", ""))
+		response, err = s.RequestWithBucketID("PATCH", uri, data, EndpointWebhookToken("", ""))
+
+		if err != nil {
+			return nil, err
+		}
 	}
+
+	err = unmarshal(response, &st)
 	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
+// messageID : The ID of a message to edit
 func (s *Session) WebhookMessageDelete(webhookID, token, messageID string) (err error) {
 	uri := EndpointWebhookMessage(webhookID, token, messageID)
 
@@ -2512,7 +2523,7 @@ 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)
 }
 
@@ -2541,7 +2552,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)
 }
 

+ 2 - 0
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.