Skip to content

Commit

Permalink
Interactions: buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
FedorLap2006 committed May 16, 2021
1 parent 577e7dd commit e6cc744
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 21 deletions.
97 changes: 97 additions & 0 deletions components.go
@@ -0,0 +1,97 @@
package discordgo

import (
"encoding/json"
)

// ComponentType is type of component.
type ComponentType uint

// Component types.
const (
ActionsRowComponent ComponentType = iota + 1
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"`
}

func (r ActionsRow) MarshalJSON() ([]byte, error) {
type actionRow ActionsRow

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

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
// 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"`
CustomID string `json:"custom_id,omitempty"`
}

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

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.
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 @@ -163,7 +163,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 @@ -199,7 +199,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 @@ -231,7 +231,7 @@ var (
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionApplicationCommandResponseData{
Data: &discordgo.InteractionResponseData{
Content: content,
},
})
Expand Down Expand Up @@ -273,7 +273,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 @@ -306,7 +306,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
46 changes: 31 additions & 15 deletions interactions.go
Expand Up @@ -62,17 +62,22 @@ type InteractionType uint8

// Interaction types
const (
InteractionPing = InteractionType(iota + 1)
InteractionPing InteractionType = iota + 1
InteractionApplicationCommand
InteractionButton
)

// 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 @@ -89,11 +94,16 @@ type Interaction struct {
Version int `json:"version"`
}

// ApplicationCommandInteractionData contains data received in an interaction event.
type ApplicationCommandInteractionData struct {
// Interaction contains data received from InteractionCreate event.
type InteractionData struct {
// 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 @@ -238,18 +248,24 @@ const (
// InteractionResponseDeferredChannelMessageWithSource acknowledges that the event was received, and that a follow-up will come later.
// It was previously named InteractionResponseACKWithSource.
InteractionResponseDeferredChannelMessageWithSource

// InteractionResponseDeferredMessageUpdate acknowledges that the button click event was received, and message update will come later.
InteractionResponseDeferredMessageUpdate
// InteractionResponseUpdateMessage is for updating the message to which button was attached to.
InteractionResponseUpdateMessage
)

// 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 {

This comment has been minimized.

Copy link
@zacharyburkett

zacharyburkett May 28, 2021

This is a breaking change, no?

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

0 comments on commit e6cc744

Please sign in to comment.