Skip to content

Commit

Permalink
Interactions: the Buttons (#933)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
FedorLap2006 and CarsonHoffman committed Jun 27, 2021
1 parent e72c457 commit 421e149
Show file tree
Hide file tree
Showing 8 changed files with 464 additions and 34 deletions.
149 changes: 149 additions & 0 deletions 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 changes: 20 additions & 0 deletions events.go
Expand Up @@ -162,19 +162,34 @@ 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
// BeforeUpdate will be nil if the Message was not previously cached in the state cache.
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
Expand Down Expand Up @@ -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 changes: 151 additions & 0 deletions 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")
}

0 comments on commit 421e149

Please sign in to comment.