302 lines
9.3 KiB
Go
302 lines
9.3 KiB
Go
package discordgo
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ed25519"
|
|
"encoding/hex"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// InteractionDeadline is the time allowed to respond to an interaction.
|
|
const InteractionDeadline = time.Second * 3
|
|
|
|
// ApplicationCommand represents an application's slash command.
|
|
type ApplicationCommand struct {
|
|
ID string `json:"id"`
|
|
ApplicationID string `json:"application_id,omitempty"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
Version string `json:"version,omitempty"`
|
|
Options []*ApplicationCommandOption `json:"options"`
|
|
}
|
|
|
|
// ApplicationCommandOptionType indicates the type of a slash command's option.
|
|
type ApplicationCommandOptionType uint8
|
|
|
|
// Application command option types.
|
|
const (
|
|
ApplicationCommandOptionSubCommand = ApplicationCommandOptionType(iota + 1)
|
|
ApplicationCommandOptionSubCommandGroup
|
|
ApplicationCommandOptionString
|
|
ApplicationCommandOptionInteger
|
|
ApplicationCommandOptionBoolean
|
|
ApplicationCommandOptionUser
|
|
ApplicationCommandOptionChannel
|
|
ApplicationCommandOptionRole
|
|
)
|
|
|
|
// ApplicationCommandOption represents an option/subcommand/subcommands group.
|
|
type ApplicationCommandOption struct {
|
|
Type ApplicationCommandOptionType `json:"type"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
// NOTE: This feature was on the API, but at some point developers decided to remove it.
|
|
// So I commented it, until it will be officially on the docs.
|
|
// Default bool `json:"default"`
|
|
Required bool `json:"required"`
|
|
Choices []*ApplicationCommandOptionChoice `json:"choices"`
|
|
Options []*ApplicationCommandOption `json:"options"`
|
|
}
|
|
|
|
// ApplicationCommandOptionChoice represents a slash command option choice.
|
|
type ApplicationCommandOptionChoice struct {
|
|
Name string `json:"name"`
|
|
Value interface{} `json:"value"`
|
|
}
|
|
|
|
// InteractionType indicates the type of an interaction event.
|
|
type InteractionType uint8
|
|
|
|
// Interaction types
|
|
const (
|
|
InteractionPing = InteractionType(iota + 1)
|
|
InteractionApplicationCommand
|
|
)
|
|
|
|
// Interaction represents an interaction event created via a slash command.
|
|
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"`
|
|
|
|
// The member who invoked this interaction.
|
|
// NOTE: this field is only filled when the slash command was invoked in a guild;
|
|
// if it was invoked in a DM, the `User` field will be filled instead.
|
|
// Make sure to check for `nil` before using this field.
|
|
Member *Member `json:"member"`
|
|
// The user who invoked this interaction.
|
|
// NOTE: this field is only filled when the slash command was invoked in a DM;
|
|
// if it was invoked in a guild, the `Member` field will be filled instead.
|
|
// Make sure to check for `nil` before using this field.
|
|
User *User `json:"user"`
|
|
|
|
Token string `json:"token"`
|
|
Version int `json:"version"`
|
|
}
|
|
|
|
// ApplicationCommandInteractionData contains data received in an interaction event.
|
|
type ApplicationCommandInteractionData struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Options []*ApplicationCommandInteractionDataOption `json:"options"`
|
|
}
|
|
|
|
// ApplicationCommandInteractionDataOption represents an option of a slash command.
|
|
type ApplicationCommandInteractionDataOption struct {
|
|
Name string `json:"name"`
|
|
// NOTE: Contains the value specified by InteractionType.
|
|
Value interface{} `json:"value,omitempty"`
|
|
Options []*ApplicationCommandInteractionDataOption `json:"options,omitempty"`
|
|
}
|
|
|
|
// IntValue is a utility function for casting option value to integer
|
|
func (o ApplicationCommandInteractionDataOption) IntValue() int64 {
|
|
if v, ok := o.Value.(float64); ok {
|
|
return int64(v)
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// UintValue is a utility function for casting option value to unsigned integer
|
|
func (o ApplicationCommandInteractionDataOption) UintValue() uint64 {
|
|
if v, ok := o.Value.(float64); ok {
|
|
return uint64(v)
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// FloatValue is a utility function for casting option value to float
|
|
func (o ApplicationCommandInteractionDataOption) FloatValue() float64 {
|
|
if v, ok := o.Value.(float64); ok {
|
|
return v
|
|
}
|
|
|
|
return 0.0
|
|
}
|
|
|
|
// StringValue is a utility function for casting option value to string
|
|
func (o ApplicationCommandInteractionDataOption) StringValue() string {
|
|
if v, ok := o.Value.(string); ok {
|
|
return v
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// BoolValue is a utility function for casting option value to bool
|
|
func (o ApplicationCommandInteractionDataOption) BoolValue() bool {
|
|
if v, ok := o.Value.(bool); ok {
|
|
return v
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// ChannelValue is a utility function for casting option value to channel object.
|
|
// s : Session object, if not nil, function additionaly fetches all channel's data
|
|
func (o ApplicationCommandInteractionDataOption) ChannelValue(s *Session) *Channel {
|
|
chanID := o.StringValue()
|
|
if chanID == "" {
|
|
return nil
|
|
}
|
|
|
|
if s == nil {
|
|
return &Channel{ID: chanID}
|
|
}
|
|
|
|
ch, err := s.State.Channel(chanID)
|
|
if err != nil {
|
|
ch, err = s.Channel(chanID)
|
|
if err != nil {
|
|
return &Channel{ID: chanID}
|
|
}
|
|
}
|
|
|
|
return ch
|
|
}
|
|
|
|
// RoleValue is a utility function for casting option value to role object.
|
|
// s : Session object, if not nil, function additionaly fetches all role's data
|
|
func (o ApplicationCommandInteractionDataOption) RoleValue(s *Session, gID string) *Role {
|
|
roleID := o.StringValue()
|
|
if roleID == "" {
|
|
return nil
|
|
}
|
|
|
|
if s == nil || gID == "" {
|
|
return &Role{ID: roleID}
|
|
}
|
|
|
|
r, err := s.State.Role(roleID, gID)
|
|
if err != nil {
|
|
roles, err := s.GuildRoles(gID)
|
|
if err == nil {
|
|
for _, r = range roles {
|
|
if r.ID == roleID {
|
|
return r
|
|
}
|
|
}
|
|
}
|
|
return &Role{ID: roleID}
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// UserValue is a utility function for casting option value to user object.
|
|
// s : Session object, if not nil, function additionaly fetches all user's data
|
|
func (o ApplicationCommandInteractionDataOption) UserValue(s *Session) *User {
|
|
userID := o.StringValue()
|
|
if userID == "" {
|
|
return nil
|
|
}
|
|
|
|
if s == nil {
|
|
return &User{ID: userID}
|
|
}
|
|
|
|
u, err := s.User(userID)
|
|
if err != nil {
|
|
return &User{ID: userID}
|
|
}
|
|
|
|
return u
|
|
}
|
|
|
|
// InteractionResponseType is type of interaction response.
|
|
type InteractionResponseType uint8
|
|
|
|
// Interaction response types.
|
|
const (
|
|
// InteractionResponsePong is for ACK ping event.
|
|
InteractionResponsePong = InteractionResponseType(iota + 1)
|
|
// InteractionResponseAcknowledge is for ACK a command without sending a message, eating the user's input.
|
|
// NOTE: this type is being imminently deprecated, and **will be removed when this occurs.**
|
|
InteractionResponseAcknowledge
|
|
// InteractionResponseChannelMessage is for responding with a message, eating the user's input.
|
|
// NOTE: this type is being imminently deprecated, and **will be removed when this occurs.**
|
|
InteractionResponseChannelMessage
|
|
// InteractionResponseChannelMessageWithSource is for responding with a message, showing the user's input.
|
|
InteractionResponseChannelMessageWithSource
|
|
// InteractionResponseDeferredChannelMessageWithSource acknowledges that the event was received, and that a follow-up will come later.
|
|
// It was previously named InteractionResponseACKWithSource.
|
|
InteractionResponseDeferredChannelMessageWithSource
|
|
)
|
|
|
|
// InteractionResponse represents a response for an interaction event.
|
|
type InteractionResponse struct {
|
|
Type InteractionResponseType `json:"type,omitempty"`
|
|
Data *InteractionApplicationCommandResponseData `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"`
|
|
Embeds []*MessageEmbed `json:"embeds,omitempty"`
|
|
AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
|
|
|
|
// NOTE: Undocumented feature, be careful with it.
|
|
Flags uint64 `json:"flags,omitempty"`
|
|
}
|
|
|
|
// VerifyInteraction implements message verification of the discord interactions api
|
|
// signing algorithm, as documented here:
|
|
// https://discord.com/developers/docs/interactions/slash-commands#security-and-authorization
|
|
func VerifyInteraction(r *http.Request, key ed25519.PublicKey) bool {
|
|
var msg bytes.Buffer
|
|
|
|
signature := r.Header.Get("X-Signature-Ed25519")
|
|
if signature == "" {
|
|
return false
|
|
}
|
|
|
|
sig, err := hex.DecodeString(signature)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
if len(sig) != ed25519.SignatureSize {
|
|
return false
|
|
}
|
|
|
|
timestamp := r.Header.Get("X-Signature-Timestamp")
|
|
if timestamp == "" {
|
|
return false
|
|
}
|
|
|
|
msg.WriteString(timestamp)
|
|
|
|
defer r.Body.Close()
|
|
var body bytes.Buffer
|
|
|
|
// at the end of the function, copy the original body back into the request
|
|
defer func() {
|
|
r.Body = ioutil.NopCloser(&body)
|
|
}()
|
|
|
|
// copy body into buffers
|
|
_, err = io.Copy(&msg, io.TeeReader(r.Body, &body))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return ed25519.Verify(key, msg.Bytes(), sig)
|
|
}
|