1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-11-01 04:24:19 +00:00
arikawa/discord/command.go
diamondburned d8438f3b51
discord: Refactor interactions and components
This commit gets rid of contain-it-all structs and instead opt for
interface union types containing underlying concrete types with no
overloading.

The code is much more verbose by doing this, but the API is much nicer
to use. The only disadvantage in that regard is the interface assertion
being too verbose and risky for users at times.
2021-11-09 18:31:06 -08:00

581 lines
17 KiB
Go

package discord
import (
"fmt"
"time"
"github.com/diamondburned/arikawa/v3/utils/json"
"github.com/pkg/errors"
)
// CommandType is the type of the command, which describes the intended
// invokation source of the command.
type CommandType uint
const (
ChatInputCommand CommandType = iota + 1
UserCommand
MessageCommand
)
// Command is the base "command" model that belongs to an application. This is
// what you are creating when you POST a new command.
//
// https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure
type Command struct {
// ID is the unique id of the command.
ID CommandID `json:"id"`
// Type is the intended source of the command.
Type CommandType `json:"type,omitempty"`
// AppID is the unique id of the parent application.
AppID AppID `json:"application_id"`
// GuildID is the guild id of the command, if not global.
GuildID GuildID `json:"guild_id,omitempty"`
// Name is the 1-32 lowercase character name matching ^[\w-]{1,32}$.
Name string `json:"name"`
// Description is the 1-100 character description.
Description string `json:"description"`
// Options are the parameters for the command. Its types are value types,
// which can either be a SubcommandOption or a SubcommandGroupOption.
//
// Note that required options must be listed before optional options, and
// a command, or each individual subcommand, can have a maximum of 25
// options.
//
// It is only present on ChatInputCommands.
Options CommandOptions `json:"options,omitempty"`
// NoDefaultPermissions defines whether the command is NOT enabled by
// default when the app is added to a guild.
NoDefaultPermission bool `json:"-"`
// Version is an autoincrementing version identifier updated during
// substantial record changes
Version Snowflake `json:"version,omitempty"`
}
// CreatedAt returns a time object representing when the command was created.
func (c *Command) CreatedAt() time.Time {
return c.ID.Time()
}
func (c *Command) MarshalJSON() ([]byte, error) {
type RawCommand Command
cmd := struct {
*RawCommand
DefaultPermission bool `json:"default_permission"`
}{RawCommand: (*RawCommand)(c)}
// Discord defaults default_permission to true, so we need to invert the
// meaning of the field (>No<DefaultPermission) to match Go's default
// value, false.
cmd.DefaultPermission = !c.NoDefaultPermission
return json.Marshal(cmd)
}
func (c *Command) UnmarshalJSON(data []byte) error {
type rawCommand Command
cmd := struct {
*rawCommand
DefaultPermission bool `json:"default_permission"`
}{
rawCommand: (*rawCommand)(c),
}
if err := json.Unmarshal(data, &cmd); err != nil {
return err
}
// Discord defaults default_permission to true, so we need to invert the
// meaning of the field (>No<DefaultPermission) to match Go's default
// value, false.
c.NoDefaultPermission = !cmd.DefaultPermission
// Discord defaults type to 1 if omitted.
if c.Type == 0 {
c.Type = ChatInputCommand
}
return nil
}
// commandTypeCheckError is returned if a one of Command's Options fails the
// type check.
type commandTypeCheckError struct {
name string
got interface{}
expect string
}
// Name returns the name of the erroneous command.
func (err commandTypeCheckError) Name() string {
return err.name
}
// Data returns the erroneous data that belongs to this error. It is usually
// either a CommandOption or a CommandOptionValue.
func (err commandTypeCheckError) Data() interface{} {
return err.got
}
// Error implements error.
func (err commandTypeCheckError) Error() string {
return fmt.Sprintf(
"error at option name %q: expected %s, got %T",
err.name, err.expect, err.got,
)
}
// CommandOptions is used primarily for unmarshaling.
type CommandOptions []CommandOption
// UnmarshalJSON unmarshals b into these CommandOptions.
func (c *CommandOptions) UnmarshalJSON(b []byte) error {
var unknowns []UnknownCommandOption
if err := json.Unmarshal(b, &unknowns); err != nil {
return err
}
if len(unknowns) == 0 {
*c = nil
return nil
}
*c = make([]CommandOption, len(unknowns))
for i, v := range unknowns {
(*c)[i] = v.data
}
return nil
}
// UnknownCommandOption is used for unknown or unmarshaled CommandOption values.
// It is used in the unmarshaling stage for all CommandOption types.
//
// An UnknownCommandOption will satisfy both CommandOption and
// CommandOptionValue. Code that type-switches on either of them should not
// assume that only the expected types are used.
type UnknownCommandOption struct {
OptionName string `json:"name"`
OptionType CommandOptionType `json:"type"`
raw json.Raw
data CommandOption
}
// Name returns the supposeed name for this UnknownCommandOption.
func (u *UnknownCommandOption) Name() string {
return u.OptionName
}
// Type returns the supposed type for this UnknownCommandOption.
func (u *UnknownCommandOption) Type() CommandOptionType {
return u.OptionType
}
// Raw returns the raw JSON of this UnknownCommandOption. It will only return a
// non-nil blob of JSON if the command option's type cannot be found. If this
// method doesn't return nil, then Data's type will be UnknownCommandOption.
func (u *UnknownCommandOption) Raw() json.Raw {
return u.raw
}
// Data returns the underlying data type, which is a type that satisfies either
// CommandOption or CommandOptionValue.
func (u *UnknownCommandOption) Data() CommandOption {
return u.data
}
// Implement both CommandOption and CommandOptionValue.
func (u *UnknownCommandOption) _val() {}
// UnmarshalJSON parses the JSON into the struct as-is then reads all its
// children Options/Choices (if subcommand(group)). Typed command options are
// created into u.Data, or u.Raw if the type is unknown. This is done from the
// bottom up.
func (u *UnknownCommandOption) UnmarshalJSON(b []byte) error {
type unknown UnknownCommandOption
if err := json.Unmarshal(b, (*unknown)(u)); err != nil {
return errors.Wrap(err, "failed to unmarshal unknown")
}
switch u.Type() {
case SubcommandOptionType:
u.data = &SubcommandOption{}
case SubcommandGroupOptionType:
u.data = &SubcommandGroupOption{}
case StringOptionType:
u.data = &StringOption{}
case IntegerOptionType:
u.data = &IntegerOption{}
case BooleanOptionType:
u.data = &BooleanOption{}
case UserOptionType:
u.data = &UserOption{}
case ChannelOptionType:
u.data = &ChannelOption{}
case RoleOptionType:
u.data = &RoleOption{}
case MentionableOptionType:
u.data = &MentionableOption{}
case NumberOptionType:
u.data = &NumberOption{}
default:
// Copy the blob of bytes into a new slice.
u.raw = append(json.Raw(nil), b...)
u.data = u
return nil
}
if err := json.Unmarshal(b, u.data); err != nil {
return errors.Wrapf(err, "failed to unmarshal type %d", u.Type())
}
return nil
}
// CommandOptionType is the enumerated integer type for command options. The
// user usually won't have to touch any of these enum constants.
type CommandOptionType uint
const (
SubcommandOptionType CommandOptionType = iota + 1
SubcommandGroupOptionType
StringOptionType
IntegerOptionType
BooleanOptionType
UserOptionType
ChannelOptionType
RoleOptionType
MentionableOptionType
NumberOptionType
maxOptionType // for bound checking
)
// CommandOption is a union of command option types. The constructors for
// CommandOption will hint the types that can be a CommandOption.
type CommandOption interface {
Name() string
Type() CommandOptionType
}
// Maintaining these structs is quite an effort. If a new field is added into
// the generic CommandOption type, you MUST update ALL CommandOption structs.
// This means copy-pasting, yes.
// SubcommandGroupOption is a subcommand group that fits into a CommandOption.
type SubcommandGroupOption struct {
OptionName string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Subcommands []*SubcommandOption `json:"options"`
}
// Name implements CommandOption.
func (s *SubcommandGroupOption) Name() string { return s.OptionName }
// Type implements CommandOption.
func (s *SubcommandGroupOption) Type() CommandOptionType { return SubcommandGroupOptionType }
// SubcommandOption is a subcommand option that fits into a CommandOption.
type SubcommandOption struct {
OptionName string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
// Options contains command option values. All CommandOption types except
// for SubcommandOption and SubcommandGroupOption will implement this
// interface.
Options []CommandOptionValue `json:"options"`
}
// Name implements CommandOption.
func (s *SubcommandOption) Name() string { return s.OptionName }
// Type implements CommandOption.
func (s *SubcommandOption) Type() CommandOptionType { return SubcommandOptionType }
// UnmarshalJSON unmarshals the given JSON bytes. It actually does
// type-checking.
func (s *SubcommandOption) UnmarshalJSON(b []byte) error {
type raw SubcommandOption
var opt struct {
*raw
Type CommandOptionType `json:"type"`
Options []UnknownCommandOption `json:"options"`
}
opt.raw = (*raw)(s)
if err := json.Unmarshal(b, &opt); err != nil {
return err
}
if opt.Type != SubcommandOptionType {
return fmt.Errorf("unexpected (not SubcommandOption) type %d", s.Type())
}
s.Options = make([]CommandOptionValue, len(opt.Options))
for i, opt := range opt.Options {
ov, ok := opt.data.(CommandOptionValue)
if !ok {
return commandTypeCheckError{opt.OptionName, opt.data, "CommandOptionValue"}
}
s.Options[i] = ov
}
return nil
}
// CommandOptionValue is a subcommand option that fits into a subcommand.
type CommandOptionValue interface {
CommandOption
_val()
}
// StringOption is a subcommand option that fits into a CommandOptionValue.
type StringOption struct {
OptionName string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Choices []StringChoice `json:"choices,omitempty"`
Autocomplete bool `json:"autocomplete"`
}
// Name implements CommandOption.
func (s *StringOption) Name() string { return s.OptionName }
// Type implements CommandOptionValue.
func (s *StringOption) Type() CommandOptionType { return StringOptionType }
func (s *StringOption) _val() {}
// StringChoice is a pair of string key to a string.
type StringChoice struct {
Name string `json:"name"`
Value string `json:"value"`
}
// IntegerOption is a subcommand option that fits into a CommandOptionValue.
type IntegerOption struct {
OptionName string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Choices []IntegerChoice `json:"choices,omitempty"`
}
// Name implements CommandOption.
func (i *IntegerOption) Name() string { return i.OptionName }
// Type implements CommandOptionValue.
func (i *IntegerOption) Type() CommandOptionType { return IntegerOptionType }
func (i *IntegerOption) _val() {}
// IntegerChoice is a pair of string key to an integer.
type IntegerChoice struct {
Name string `json:"name"`
Value int `json:"value"`
}
// BooleanOption is a subcommand option that fits into a CommandOptionValue.
type BooleanOption struct {
OptionName string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Choices []BooleanChoice `json:"choices,omitempty"`
}
// Name implements CommandOption.
func (b *BooleanOption) Name() string { return b.OptionName }
// Type implements CommandOptionValue.
func (b *BooleanOption) Type() CommandOptionType { return BooleanOptionType }
func (b *BooleanOption) _val() {}
// BooleanChoice is a pair of string key to a boolean.
type BooleanChoice struct {
Name string `json:"name"`
Value bool `json:"value"`
}
// UserOption is a subcommand option that fits into a CommandOptionValue.
type UserOption struct {
OptionName string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Choices []UserChoice `json:"choices,omitempty"`
}
// Name implements CommandOption.
func (u *UserOption) Name() string { return u.OptionName }
// Type implements CommandOptionValue.
func (u *UserOption) Type() CommandOptionType { return UserOptionType }
func (u *UserOption) _val() {}
// UserChoice is a pair of string key to a user ID.
type UserChoice struct {
Name string `json:"name"`
Value UserID `json:"value,string"`
}
// ChannelOption is a subcommand option that fits into a CommandOptionValue.
type ChannelOption struct {
OptionName string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Choices []ChannelChoice `json:"choices,omitempty"`
ChannelTypes []ChannelType `json:"channel_types,omitempty"`
}
// Name implements CommandOption.
func (c *ChannelOption) Name() string { return c.OptionName }
// Type implements CommandOptionValue.
func (c *ChannelOption) Type() CommandOptionType { return ChannelOptionType }
func (c *ChannelOption) _val() {}
// ChannelChoice is a pair of string key to a channel ID.
type ChannelChoice struct {
Name string `json:"name"`
Value ChannelID `json:"value,string"`
}
// RoleOption is a subcommand option that fits into a CommandOptionValue.
type RoleOption struct {
OptionName string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Choices []RoleChoice `json:"choices,omitempty"`
}
// Name implements CommandOption.
func (r *RoleOption) Name() string { return r.OptionName }
// Type implements CommandOptionValue.
func (r *RoleOption) Type() CommandOptionType { return RoleOptionType }
func (r *RoleOption) _val() {}
// RoleChoice is a pair of string key to a role ID.
type RoleChoice struct {
Name string `json:"name"`
Value RoleID `json:"value,string"`
}
// MentionableOption is a subcommand option that fits into a CommandOptionValue.
type MentionableOption struct {
OptionName string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Choices []MentionableChoice `json:"choices,omitempty"`
}
// Name implements CommandOption.
func (m *MentionableOption) Name() string { return m.OptionName }
// Type implements CommandOptionValue.
func (m *MentionableOption) Type() CommandOptionType { return MentionableOptionType }
func (m *MentionableOption) _val() {}
// MentionableChoice is a pair of string key to a mentionable snowflake IDs. To
// use this correctly, use the Resolved field.
type MentionableChoice struct {
Name string `json:"name"`
Value Snowflake `json:"value,string"`
}
// NumberOption is a subcommand option that fits into a CommandOptionValue.
type NumberOption struct {
OptionName string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
Choices []NumberChoice `json:"choices,omitempty"`
}
// Name implements CommandOption.
func (n *NumberOption) Name() string { return n.OptionName }
// Type implements CommandOptionValue.
func (n *NumberOption) Type() CommandOptionType { return NumberOptionType }
func (n *NumberOption) _val() {}
// NumberChoice is a pair of string key to a float64 values.
type NumberChoice struct {
Name string `json:"name"`
Value float64 `json:"value"`
}
// Generated with utils/generate-option-marshalers.sh
// MarshalJSON marshals SubcommandOption to JSON with the "type" field.
func (s *SubcommandOption) MarshalJSON() ([]byte, error) {
type raw SubcommandOption
return json.Marshal(struct {
Type CommandOptionType `json:"type"`
*raw
}{
Type: s.Type(),
raw: (*raw)(s),
})
}
// MarshalJSON marshals SubcommandGroupOption to JSON with the "type" field.
func (s *SubcommandGroupOption) MarshalJSON() ([]byte, error) {
type raw SubcommandGroupOption
return json.Marshal(struct {
Type CommandOptionType `json:"type"`
*raw
}{
Type: s.Type(),
raw: (*raw)(s),
})
}
// MarshalJSON marshals StringOption to JSON with the "type" field.
func (s *StringOption) MarshalJSON() ([]byte, error) {
type raw StringOption
return json.Marshal(struct {
Type CommandOptionType `json:"type"`
*raw
}{
Type: s.Type(),
raw: (*raw)(s),
})
}
// MarshalJSON marshals IntegerOption to JSON with the "type" field.
func (i *IntegerOption) MarshalJSON() ([]byte, error) {
type raw IntegerOption
return json.Marshal(struct {
Type CommandOptionType `json:"type"`
*raw
}{
Type: i.Type(),
raw: (*raw)(i),
})
}
// MarshalJSON marshals BooleanOption to JSON with the "type" field.
func (b *BooleanOption) MarshalJSON() ([]byte, error) {
type raw BooleanOption
return json.Marshal(struct {
Type CommandOptionType `json:"type"`
*raw
}{
Type: b.Type(),
raw: (*raw)(b),
})
}
// MarshalJSON marshals UserOption to JSON with the "type" field.
func (u *UserOption) MarshalJSON() ([]byte, error) {
type raw UserOption
return json.Marshal(struct {
Type CommandOptionType `json:"type"`
*raw
}{
Type: u.Type(),
raw: (*raw)(u),
})
}