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>
This commit is contained in:
Fedor Lapshin 2021-08-09 03:16:07 +03:00 committed by GitHub
parent ab47f123ba
commit 4ebe5a08ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 467 additions and 110 deletions

View file

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

View file

@ -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",
},
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: "🤷",
},
// 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: "📜",
},
Label: "Documentation",
Style: discordgo.LinkButton,
URL: "https://discord.com/developers/docs/interactions/message-components#buttons",
},
},
// 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",
discordgo.Button{
Emoji: discordgo.ComponentEmoji{
Name: "🔧",
},
Label: "Discord developers",
Style: discordgo.LinkButton,
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: "💠",
discordgo.Button{
Emoji: discordgo.ComponentEmoji{
Name: "🦫",
},
Label: "Discord Gophers",
Style: discordgo.LinkButton,
URL: "https://discord.gg/7RuRrVHyXF",
},
},
},
},
},
},
})
})
if err != nil {
panic(err)
}
},
"fd_yes": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Great! If you wanna know more or just have questions, feel free to visit Discord Devs and Discord Gophers server. " +
"But now, when you know how buttons work, let's move onto select menus (execute `/selects single`)",
Flags: 1 << 6,
Components: []discordgo.MessageComponent{
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
discordgo.Button{
Emoji: discordgo.ComponentEmoji{
Name: "🔧",
},
Label: "Discord developers",
Style: discordgo.LinkButton,
URL: "https://discord.gg/discord-developers",
},
discordgo.Button{
Emoji: discordgo.ComponentEmoji{
Name: "🦫",
},
Label: "Discord Gophers",
Style: discordgo.LinkButton,
URL: "https://discord.gg/7RuRrVHyXF",
},
},
},
},
},
})
if err != nil {
panic(err)
}
},
"select": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
var response *discordgo.InteractionResponse
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()
const stackoverflowFormat = `https://stackoverflow.com/questions/tagged/%s`
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{
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 {

View file

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

View file

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

View file

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

View file

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