discordmuffin/interactions.go
2021-03-12 09:45:35 -05:00

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