forked from NotUnlikeTheWaves/discordgo
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Interactions: the Buttons (bwmarrin#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>
- Loading branch information
Showing
8 changed files
with
464 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
} |
Oops, something went wrong.