Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactions: the Buttons #933

Merged
merged 33 commits into from Jun 27, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e6cc744
Interactions: buttons
FedorLap2006 May 16, 2021
4a8d880
Fix merge
FedorLap2006 May 16, 2021
8930b92
Doc fix
FedorLap2006 May 16, 2021
23fb77c
Gofmt fix
FedorLap2006 May 16, 2021
93b0bcc
Fix typo
FedorLap2006 May 17, 2021
26e0fe2
Remaking interaction data into interface
FedorLap2006 May 18, 2021
5656869
Godoc fix
FedorLap2006 May 18, 2021
4ed73c2
Gofmt fix
FedorLap2006 May 18, 2021
e10d270
Godoc fix
FedorLap2006 May 18, 2021
9646e09
InteractionData helper functions and some fixes in slash commands exa…
FedorLap2006 May 18, 2021
e946bc8
Fix components example
FedorLap2006 May 18, 2021
5dfd40e
Yet another fix of components example
FedorLap2006 May 18, 2021
a081bca
Merge branch 'bwmarrin:master' into master
FedorLap2006 May 20, 2021
3c6bda6
Fix interaction unmarshaling
FedorLap2006 May 21, 2021
72aac18
Gofmt fix
FedorLap2006 May 21, 2021
8c792ad
Godoc fix
FedorLap2006 May 21, 2021
f5c78d0
Gofmt fix
FedorLap2006 May 23, 2021
e7fb87f
Corrected naming and docs
FedorLap2006 May 26, 2021
495990c
Rolled back API version
FedorLap2006 May 30, 2021
2f86d0c
Requested fixes
FedorLap2006 May 30, 2021
931b4e6
Merge and enum fix
FedorLap2006 May 30, 2021
8670b0e
Added support of components to webhook and regular messages
FedorLap2006 Jun 15, 2021
adc5b70
Fix components unmarshaling
FedorLap2006 Jun 18, 2021
afb1057
Godoc fix
FedorLap2006 Jun 18, 2021
23a197a
Requested fixes
FedorLap2006 Jun 19, 2021
7b457eb
Fixed unmarshaling issues
FedorLap2006 Jun 22, 2021
9dbe959
Components example: cleanup
FedorLap2006 Jun 22, 2021
6f3aa6b
Added components tracking to state
FedorLap2006 Jun 22, 2021
881a24b
Requested fixes
FedorLap2006 Jun 23, 2021
bcc0a2f
Renaming fix
FedorLap2006 Jun 23, 2021
5149e24
Remove more named returns
CarsonHoffman Jun 23, 2021
10b295f
Merge branch 'bwmarrin:master' into master
FedorLap2006 Jun 26, 2021
3bc7e7b
Minor English fixes
CarsonHoffman Jun 27, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
101 changes: 101 additions & 0 deletions components.go
@@ -0,0 +1,101 @@
package discordgo

import (
"encoding/json"
)

// ComponentType is type of component.
type ComponentType uint

// Component types.
const (
ActionsRowComponent ComponentType = iota + 1
FedorLap2006 marked this conversation as resolved.
Show resolved Hide resolved
ButtonComponent
)

// Component is a base interface for all components.
type Component interface {
json.Marshaler
Type() ComponentType
}

// ActionsRow is a container for components within one row.
type ActionsRow struct {
Components []Component `json:"components"`
}
FedorLap2006 marked this conversation as resolved.
Show resolved Hide resolved

// MarshalJSON is a method for marshaling ActionsRow to a JSON object.
func (r ActionsRow) MarshalJSON() ([]byte, error) {
type actionRow ActionsRow

return json.Marshal(struct {
actionRow
Type ComponentType `json:"type"`
}{
actionRow: actionRow(r),
Type: r.Type(),
})
}

// 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 = iota + 1
FedorLap2006 marked this conversation as resolved.
Show resolved Hide resolved
// SecondaryButton is a button with grey color.
SecondaryButton
// SuccessButton is a button with green color.
SuccessButton
// DangerButton is a button with red color.
DangerButton
// LinkButton is a special type of button which navigates to a URL. Has grey color.
LinkButton
)

// 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, Link is mutually exclusive with CustomID.
Link string `json:"url,omitempty"`
CarsonHoffman marked this conversation as resolved.
Show resolved Hide resolved
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
}
2 changes: 1 addition & 1 deletion endpoints.go
Expand Up @@ -14,7 +14,7 @@ package discordgo
import "strconv"

// APIVersion is the Discord API version used for the REST and Websocket API.
var APIVersion = "8"
var APIVersion = "9"

// Known Discord API Endpoints.
var (
Expand Down
151 changes: 151 additions & 0 deletions examples/components/main.go
@@ -0,0 +1,151 @@
package main

import (
"flag"
"github.com/bwmarrin/discordgo"
"log"
"os"
"os/signal"
)

// Bot parameters
var (
GuildID = flag.String("guild", "", "Test guild ID")
ChannelID = flag.String("channel", "", "Test channel 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.Data.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.Component{
// ActionRow is a container of all buttons in the same raw.
FedorLap2006 marked this conversation as resolved.
Show resolved Hide resolved
discordgo.ActionsRow{
Components: []discordgo.Component{
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 doesn't require CustomID and does not trigger the gateway/HTTP event
Link: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
Emoji: discordgo.ButtonEmoji{
Name: "🤷‍♂️",
},
},
},
},
// The message may have multiple actions rows.
discordgo.ActionsRow{
Components: []discordgo.Component{
discordgo.Button{
Label: "Discord Developers server",
Style: discordgo.LinkButton,
Disabled: false,
Link: "https://discord.gg/discord-developers",
},
},
},
},
},
})
if err != nil {
panic(err)
}
}
return
}
// Type for button press will be always InteractionButton (3)
if i.Type != discordgo.InteractionButton {
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.Data.CustomID {
case "yes_btn":
content += "(yes)"
case "no_btn":
content += "(no)"
}

s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
// Buttons also may update the message which they was attached to.
// 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.Component{
discordgo.ActionsRow{
Components: []discordgo.Component{
discordgo.Button{
Label: "Our sponsor",
Style: discordgo.LinkButton,
Disabled: false,
Link: "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)
signal.Notify(stop, os.Interrupt)
<-stop
log.Println("Graceful shutdown")
}
10 changes: 5 additions & 5 deletions examples/slash_commands/main.go
Expand Up @@ -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",
},
})
Expand Down Expand Up @@ -191,7 +191,7 @@ var (
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...,
Expand Down Expand Up @@ -223,7 +223,7 @@ var (
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionApplicationCommandResponseData{
Data: &discordgo.InteractionResponseData{
Content: content,
},
})
Expand Down Expand Up @@ -259,7 +259,7 @@ var (

err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseType(i.Data.Options[0].IntValue()),
Data: &discordgo.InteractionApplicationCommandResponseData{
Data: &discordgo.InteractionResponseData{
Content: content,
},
})
Expand Down Expand Up @@ -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)
Expand Down
45 changes: 30 additions & 15 deletions interactions.go
Expand Up @@ -63,17 +63,22 @@ type InteractionType uint8

// Interaction types
const (
InteractionPing = InteractionType(iota + 1)
InteractionPing InteractionType = iota + 1
InteractionApplicationCommand
CarsonHoffman marked this conversation as resolved.
Show resolved Hide resolved
InteractionButton
FedorLap2006 marked this conversation as resolved.
Show resolved Hide resolved
)

// 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:"data"`
GuildID string `json:"guild_id"`
ChannelID string `json:"channel_id"`

// The message on which interaction was used.
// NOTE: this field is only filled when the button click interaction triggered. 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;
Expand All @@ -90,11 +95,16 @@ type Interaction struct {
Version int `json:"version"`
}

// ApplicationCommandInteractionData contains data received in an interaction event.
type ApplicationCommandInteractionData struct {
// InteractionData contains data received from InteractionCreate event.
type InteractionData struct {
FedorLap2006 marked this conversation as resolved.
Show resolved Hide resolved
// Application command
ID string `json:"id"`
Name string `json:"name"`
Options []*ApplicationCommandInteractionDataOption `json:"options"`

// Components
CustomID string `json:"custom_id"`
ComponentType ComponentType `json:"component_type"`
}

// ApplicationCommandInteractionDataOption represents an option of a slash command.
Expand Down Expand Up @@ -232,18 +242,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 button click event was received, and message will be updated later.
InteractionResponseDeferredMessageUpdate InteractionResponseType = 6
// InteractionResponseUpdateMessage is for updating the message to which button was attached to.
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 []Component `json:"components,omitempty"`
Embeds []*MessageEmbed `json:"embeds,omitempty"`
AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`

Expand Down
1 change: 1 addition & 0 deletions message.go
Expand Up @@ -168,6 +168,7 @@ type MessageSend struct {
Content string `json:"content,omitempty"`
Embed *MessageEmbed `json:"embed,omitempty"`
TTS bool `json:"tts"`
Components []Component `json:"components"`
Files []*File `json:"-"`
AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
Reference *MessageReference `json:"message_reference,omitempty"`
Expand Down