1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-11-16 03:44:26 +00:00
arikawa/discord/interaction.go
diamondburned 47c06557c2
discord: Add CommandInteractionOptions.Unmarshal
This commit adds an Unmarshal method into CommandInteractionOptions. It
is probably the first commit to break the rule of keeping package
discord simple.

The goal of this rationale is that package discord should continue
prioritizing convenience and ease of use by providing small, helpful
additions without being oversized.

This method, while it uses reflect, is actually fairly small in
implementation. Its functionality should be kept to a bare minimum and
as such will not cover every single use case (but should cover most).
2022-08-14 23:40:06 -07:00

612 lines
18 KiB
Go

package discord
import (
"fmt"
"reflect"
"strings"
"github.com/diamondburned/arikawa/v3/utils/json"
"github.com/pkg/errors"
)
// InteractionEvent describes the full incoming interaction event. It may be a
// gateway event or a webhook event.
//
// https://discord.com/developers/docs/topics/gateway#interactions
type InteractionEvent struct {
ID InteractionID `json:"id"`
Data InteractionData `json:"data"`
AppID AppID `json:"application_id"`
ChannelID ChannelID `json:"channel_id,omitempty"`
Token string `json:"token"`
Version int `json:"version"`
// Message is the message the component was attached to.
// Only present for component interactions, not command interactions.
Message *Message `json:"message,omitempty"`
// Member is only present if this came from a guild. To get a user, use the
// Sender method.
Member *Member `json:"member,omitempty"`
GuildID GuildID `json:"guild_id,omitempty"`
// User is only present if this didn't come from a guild. To get a user, use
// the Sender method.
User *User `json:"user,omitempty"`
// Locale is the selected language of the invoking user. It is returned in
// all interactions except ping interactions. Use this Locale field to
// obtain the language of the user who used the interaction.
Locale Language `json:"locale,omitempty"`
// GuildLocale is the guild's preferred locale, if invoked in a guild.
GuildLocale string `json:"guild_locale,omitempty"`
}
// Sender returns the sender of this event from either the Member field or the
// User field. If neither of those fields are available, then nil is returned.
func (e *InteractionEvent) Sender() *User {
if e.User != nil {
return e.User
}
if e.Member != nil {
return &e.Member.User
}
return nil
}
// SenderID returns the sender's ID. See Sender for more information. If Sender
// returns nil, then 0 is returned.
func (e *InteractionEvent) SenderID() UserID {
if sender := e.Sender(); sender != nil {
return sender.ID
}
return 0
}
func (e *InteractionEvent) UnmarshalJSON(b []byte) error {
type event InteractionEvent
target := struct {
Type InteractionDataType `json:"type"`
Data json.Raw `json:"data"`
*event
}{
event: (*event)(e),
}
if err := json.Unmarshal(b, &target); err != nil {
return err
}
var err error
switch target.Type {
case PingInteractionType:
e.Data = &PingInteraction{}
return nil // Ping isn't actually an object.
case CommandInteractionType:
e.Data = &CommandInteraction{}
case ComponentInteractionType:
d, err := ParseComponentInteraction(target.Data)
if err != nil {
return errors.Wrap(err, "failed to unmarshal component interaction event data")
}
e.Data = d
return nil
case AutocompleteInteractionType:
e.Data = &AutocompleteInteraction{}
case ModalInteractionType:
e.Data = &ModalInteraction{}
default:
e.Data = &UnknownInteractionData{
Raw: target.Data,
typ: target.Type,
}
return nil
}
if err := json.Unmarshal(target.Data, e.Data); err != nil {
return errors.Wrap(err, "failed to unmarshal interaction event data")
}
return err
}
func (e *InteractionEvent) MarshalJSON() ([]byte, error) {
type event InteractionEvent
if e.Data == nil {
return nil, errors.New("missing InteractionEvent.Data")
}
if e.Data.InteractionType() == 0 {
return nil, errors.New("unexpected 0 InteractionEvent.Data.Type")
}
v := struct {
Type InteractionDataType `json:"type"`
*event
}{
Type: e.Data.InteractionType(),
event: (*event)(e),
}
return json.Marshal(v)
}
// InteractionDataType is the type of each Interaction, enumerated in
// integers.
type InteractionDataType uint
const (
PingInteractionType InteractionDataType = iota + 1
CommandInteractionType
ComponentInteractionType
AutocompleteInteractionType
ModalInteractionType
)
// InteractionData holds the respose data of an interaction, or more
// specifically, the data that Discord sends to us. Type assertions should be
// made on it to access the underlying data.
//
// The following types implement this interface:
//
// - *PingInteraction
// - *AutocompleteInteraction
// - *CommandInteraction
// - *ModalInteraction
// - *SelectInteraction (also ComponentInteraction)
// - *ButtonInteraction (also ComponentInteraction)
//
type InteractionData interface {
InteractionType() InteractionDataType
data()
}
// PingInteraction is a ping Interaction response.
type PingInteraction struct{}
// InteractionType implements InteractionData.
func (*PingInteraction) InteractionType() InteractionDataType { return PingInteractionType }
func (*PingInteraction) data() {}
// AutocompleteInteraction is an autocompletion Interaction response.
type AutocompleteInteraction struct {
CommandID CommandID `json:"id"`
// Name of command autocomplete is triggered for.
Name string `json:"name"`
CommandType CommandType `json:"type"`
Version string `json:"version"`
Options AutocompleteOptions `json:"options"`
}
// Type implements ComponentInteraction.
func (*AutocompleteInteraction) InteractionType() InteractionDataType {
return AutocompleteInteractionType
}
func (*AutocompleteInteraction) data() {}
// AutocompleteOptions is a list of autocompletion options.
// Use `Find` to get your named autocompletion option.
type AutocompleteOptions []AutocompleteOption
// Find returns the named autocomplete option.
func (o AutocompleteOptions) Find(name string) AutocompleteOption {
for _, opt := range o {
if strings.EqualFold(opt.Name, name) {
return opt
}
}
return AutocompleteOption{}
}
// AutocompleteOption is an autocompletion option in an AutocompleteInteraction.
type AutocompleteOption struct {
Type CommandOptionType `json:"type"`
Name string `json:"name"`
Value json.Raw `json:"value"`
Focused bool `json:"focused"`
Options []AutocompleteOption `json:"options"`
}
// String will return the value if the option's value is a valid string.
// Otherwise, it will return the raw JSON value of the other type.
func (o AutocompleteOption) String() string {
var value string
if err := json.Unmarshal(o.Value, &value); err != nil {
return string(o.Value)
}
return value
}
// IntValue reads the option's value as an int.
func (o AutocompleteOption) IntValue() (int64, error) {
var i int64
err := o.Value.UnmarshalTo(&i)
return i, err
}
// BoolValue reads the option's value as a bool.
func (o AutocompleteOption) BoolValue() (bool, error) {
var b bool
err := o.Value.UnmarshalTo(&b)
return b, err
}
// SnowflakeValue reads the option's value as a snowflake.
func (o AutocompleteOption) SnowflakeValue() (Snowflake, error) {
var id Snowflake
err := o.Value.UnmarshalTo(&id)
return id, err
}
// FloatValue reads the option's value as a float64.
func (o AutocompleteOption) FloatValue() (float64, error) {
var f float64
err := o.Value.UnmarshalTo(&f)
return f, err
}
// ComponentInteraction is a union component interaction response types. The
// types can be whatever the constructors for this type will return.
//
// The following types implement this interface:
//
// - *SelectInteraction
// - *ButtonInteraction
//
type ComponentInteraction interface {
InteractionData
// ID returns the ID of the component in response. Not all component
// interactions will have a component ID.
ID() ComponentID
// Type returns the type of the component in response.
Type() ComponentType
resp()
}
// SelectInteraction is a select component's response.
type SelectInteraction struct {
CustomID ComponentID `json:"custom_id"`
Values []string `json:"values"`
}
// ID implements ComponentInteraction.
func (s *SelectInteraction) ID() ComponentID { return s.CustomID }
// Type implements ComponentInteraction.
func (s *SelectInteraction) Type() ComponentType { return SelectComponentType }
// InteractionType implements InteractionData.
func (s *SelectInteraction) InteractionType() InteractionDataType {
return ComponentInteractionType
}
func (s *SelectInteraction) resp() {}
func (s *SelectInteraction) data() {}
// ButtonInteraction is a button component's response. It is the custom ID of
// the button within the component tree.
type ButtonInteraction struct {
CustomID ComponentID `json:"custom_id"`
}
// ID implements ComponentInteraction.
func (b *ButtonInteraction) ID() ComponentID { return b.CustomID }
// Type implements ComponentInteraction.
func (b *ButtonInteraction) Type() ComponentType { return ButtonComponentType }
// InteractionType implements InteractionData.
func (b *ButtonInteraction) InteractionType() InteractionDataType {
return ComponentInteractionType
}
func (b *ButtonInteraction) data() {}
func (b *ButtonInteraction) resp() {}
// ParseComponentInteraction parses the given bytes as a component response.
func ParseComponentInteraction(b []byte) (ComponentInteraction, error) {
var t struct {
Type ComponentType `json:"component_type"`
CustomID ComponentID `json:"custom_id"`
}
if err := json.Unmarshal(b, &t); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal component interaction header")
}
var d ComponentInteraction
switch t.Type {
case ButtonComponentType:
d = &ButtonInteraction{CustomID: t.CustomID}
case SelectComponentType:
d = &SelectInteraction{CustomID: t.CustomID}
default:
d = &UnknownComponent{
Raw: append(json.Raw(nil), b...),
id: t.CustomID,
typ: t.Type,
}
return d, nil
}
if err := json.Unmarshal(b, d); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal component interaction data")
}
return d, nil
}
// CommandInteractionOptions is a list of interaction options.
// Use `Find` to get your named interaction option
type CommandInteractionOptions []CommandInteractionOption
// CommandInteraction is an application command interaction that Discord sends
// to us.
type CommandInteraction struct {
ID CommandID `json:"id"`
Name string `json:"name"`
Options CommandInteractionOptions `json:"options"`
// GuildID is the id of the guild the command is registered to
GuildID GuildID `json:"guild_id,omitempty"`
// TargetID is the id of the user or message targeted by a user or message command.
//
// See TargetUserID and TargetMessageID
TargetID Snowflake `json:"target_id,omitempty"`
Resolved struct {
// User contains user objects.
Users map[UserID]User `json:"users,omitempty"`
// Members contains partial member objects (missing User, Deaf and
// Mute).
Members map[UserID]Member `json:"members,omitempty"`
// Role contains role objects.
Roles map[RoleID]Role `json:"roles,omitempty"`
// Channels contains partial channel objects that only have ID, Name,
// Type and Permissions. Threads will also have ThreadMetadata and
// ParentID.
Channels map[ChannelID]Channel `json:"channels,omitempty"`
// Messages contains partial message objects. All fields without
// omitempty are presumably present.
Messages map[MessageID]Message `json:"messages,omitempty"`
// Attachments contains attachments objects.
Attachments map[AttachmentID]Attachment `json:"attachments,omitempty"`
}
}
// InteractionType implements InteractionData.
func (*CommandInteraction) InteractionType() InteractionDataType {
return CommandInteractionType
}
// TargetUserID is the id of the user targeted by a user command
func (c *CommandInteraction) TargetUserID() UserID {
return UserID(c.TargetID)
}
// TargetMessageID is the id of the message targeted by a message command
func (c *CommandInteraction) TargetMessageID() MessageID {
return MessageID(c.TargetID)
}
func (*CommandInteraction) data() {}
// CommandInteractionOption is an option for a Command interaction response.
type CommandInteractionOption struct {
Type CommandOptionType `json:"type"`
Name string `json:"name"`
Value json.Raw `json:"value"`
Options CommandInteractionOptions `json:"options"`
}
var optionSupportedSnowflakeTypes = map[reflect.Type]struct{}{
reflect.TypeOf(ChannelID(0)): {},
reflect.TypeOf(UserID(0)): {},
reflect.TypeOf(RoleID(0)): {},
reflect.TypeOf(MessageID(0)): {},
reflect.TypeOf(Snowflake(0)): {},
}
// Unmarshal unmarshals the options into the struct pointer v. Each struct field
// must be exported and is of a supported type.
//
// Fields that don't satisfy any of the above are ignored. The "discord" struct
// tag with a value "-" is ignored. Fields that aren't found in the list of
// options and have a "?" at the end of the "discord" struct tag are ignored.
//
// Supported Types
//
// The following types are supported:
//
// - ChannelID
// - UserID
// - RoleID
// - MessageID
// - Snowflake
// - string
// - bool
// - int* (int, int8, int16, int32, int64)
// - float* (float32, float64)
// - (any struct and struct pointer)
//
// Any types that are derived from any of the above built-in types are also
// supported.
func (o CommandInteractionOptions) Unmarshal(v interface{}) error {
return o.unmarshal(reflect.ValueOf(v))
}
func (o CommandInteractionOptions) unmarshal(rv reflect.Value) error {
rt := rv.Type()
if rt.Kind() != reflect.Ptr {
return errors.New("v is not a pointer")
}
rv = rv.Elem()
rt = rt.Elem()
if rt.Kind() != reflect.Struct {
return errors.New("v is not a pointer to a struct")
}
numField := rt.NumField()
for i := 0; i < numField; i++ {
fieldStruct := rt.Field(i)
if !fieldStruct.IsExported() {
continue
}
name := fieldStruct.Tag.Get("discord")
switch name {
case "-":
continue
case "?":
name = fieldStruct.Name + "?"
case "":
name = fieldStruct.Name
}
option := o.Find(strings.TrimSuffix(name, "?"))
fieldv := rv.Field(i)
fieldt := fieldStruct.Type
if strings.HasSuffix(name, "?") {
name = strings.TrimSuffix(name, "?")
if option.Type == 0 {
// not found
continue
}
} else if fieldStruct.Type.Kind() == reflect.Ptr {
fieldt = fieldt.Elem()
if option.Type == 0 {
// not found
fieldv.Set(reflect.NewAt(fieldt, nil))
continue
}
// found, so allocate new value and use that to set
newv := reflect.New(fieldt)
fieldv.Set(newv)
fieldv = newv.Elem()
} else if option.Type == 0 {
// not found AND the field is not a pointer, so error out
return fmt.Errorf("option %q is required but not found", name)
}
if _, ok := optionSupportedSnowflakeTypes[fieldt]; ok {
snowflake, err := option.SnowflakeValue()
if err != nil {
return errors.Wrapf(err, "option %q is not a valid snowflake", name)
}
fieldv.Set(reflect.ValueOf(snowflake).Convert(fieldt))
continue
}
switch fieldt.Kind() {
case reflect.Struct:
if err := option.Options.unmarshal(fieldv.Addr()); err != nil {
return errors.Wrapf(err, "option %q has invalid suboptions", name)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
i64, err := option.IntValue()
if err != nil {
return errors.Wrapf(err, "option %q is not a valid int64", name)
}
fieldv.Set(reflect.ValueOf(i64).Convert(fieldt))
case reflect.Float32, reflect.Float64:
i64, err := option.IntValue()
if err != nil {
return errors.Wrapf(err, "option %q is not a valid int", name)
}
fieldv.Set(reflect.ValueOf(i64).Convert(fieldt))
case reflect.Bool:
b, err := option.BoolValue()
if err != nil {
return errors.Wrapf(err, "option %q is not a valid bool", name)
}
fieldv.Set(reflect.ValueOf(b).Convert(fieldt))
case reflect.String:
fieldv.Set(reflect.ValueOf(option.String()).Convert(fieldt))
default:
return fmt.Errorf("field %s (%q) has unknown type %s", fieldStruct.Name, name, fieldt)
}
}
return nil
}
// Find returns the named command option
func (o CommandInteractionOptions) Find(name string) CommandInteractionOption {
for _, opt := range o {
if strings.EqualFold(opt.Name, name) {
return opt
}
}
return CommandInteractionOption{}
}
// String will return the value if the option's value is a valid string.
// Otherwise, it will return the raw JSON value of the other type.
func (o CommandInteractionOption) String() string {
var value string
if err := json.Unmarshal(o.Value, &value); err != nil {
return string(o.Value)
}
return value
}
// IntValue reads the option's value as an int.
func (o CommandInteractionOption) IntValue() (int64, error) {
var i int64
err := o.Value.UnmarshalTo(&i)
return i, err
}
// BoolValue reads the option's value as a bool.
func (o CommandInteractionOption) BoolValue() (bool, error) {
var b bool
err := o.Value.UnmarshalTo(&b)
return b, err
}
// SnowflakeValue reads the option's value as a snowflake.
func (o CommandInteractionOption) SnowflakeValue() (Snowflake, error) {
var id Snowflake
err := o.Value.UnmarshalTo(&id)
return id, err
}
// FloatValue reads the option's value as a float64.
func (o CommandInteractionOption) FloatValue() (float64, error) {
var f float64
err := o.Value.UnmarshalTo(&f)
return f, err
}
// ModalInteraction is the submitted modal form
type ModalInteraction struct {
CustomID ComponentID `json:"custom_id"`
Components ContainerComponents `json:"components"`
}
// InteractionType implements InteractionData.
func (m *ModalInteraction) InteractionType() InteractionDataType {
return ModalInteractionType
}
func (m *ModalInteraction) data() {}
// UnknownInteractionData describes an Interaction response with an unknown
// type.
type UnknownInteractionData struct {
json.Raw
typ InteractionDataType
}
// InteractionType implements InteractionData.
func (u *UnknownInteractionData) InteractionType() InteractionDataType {
return u.typ
}
func (u *UnknownInteractionData) data() {}