From d6bc738e505d92373affaade9128e03b3a5fe230 Mon Sep 17 00:00:00 2001 From: ItsLychee Date: Sun, 13 Feb 2022 21:15:28 -0600 Subject: [PATCH] discord: Modal interaction support (#310) * Support modal interactions along with the TextInput component * Replace ModalInteraction with Modal to prevent confusion * Fix the required field from not being used correctly * PR Fixes --- api/interaction.go | 6 ++++ discord/component.go | 69 ++++++++++++++++++++++++++++++++++++++++++ discord/interaction.go | 16 ++++++++++ 3 files changed, 91 insertions(+) diff --git a/api/interaction.go b/api/interaction.go index 03dc8ed..3d6c7a4 100644 --- a/api/interaction.go +++ b/api/interaction.go @@ -25,6 +25,7 @@ const ( DeferredMessageUpdate UpdateMessage AutocompleteResult + ModalResponse ) // InteractionResponseFlags implements flags for an @@ -78,6 +79,11 @@ type InteractionResponseData struct { // // During all other events, this should not be provided. Choices *[]AutocompleteChoice `json:"choices"` + + // CustomID used with the modal + CustomID option.NullableString `json:"custom_id,omitempty"` + // Title is the heading of the modal window + Title option.NullableString `json:"title,omitempty"` } // NeedsMultipart returns true if the InteractionResponseData has files. diff --git a/discord/component.go b/discord/component.go index 3a00c36..164ce8e 100644 --- a/discord/component.go +++ b/discord/component.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/diamondburned/arikawa/v3/utils/json" + "github.com/diamondburned/arikawa/v3/utils/json/option" "github.com/pkg/errors" ) @@ -15,6 +16,7 @@ const ( ActionRowComponentType ButtonComponentType SelectComponentType + TextInputComponentType ) // String formats Type's name as a string. @@ -26,6 +28,8 @@ func (t ComponentType) String() string { return "Button" case SelectComponentType: return "Select" + case TextInputComponentType: + return "TextInput" default: return fmt.Sprintf("ComponentType(%d)", int(t)) } @@ -126,6 +130,8 @@ func ParseComponent(b []byte) (Component, error) { c = &ButtonComponent{} case SelectComponentType: c = &SelectComponent{} + case TextInputComponentType: + c = &TextInputComponent{} default: c = &UnknownComponent{typ: t.Type} } @@ -448,6 +454,69 @@ func (s *SelectComponent) MarshalJSON() ([]byte, error) { return json.Marshal(msg) } +type TextInputStyle uint8 + +const ( + _ TextInputStyle = iota + TextInputShortStyle + TextInputParagraphStyle +) + +// TextInputComponents provide a user-facing text box to be filled out. They can only +// be used with modals. +type TextInputComponent struct { + // CustomID provides a developer-defined ID for the input (max 100 chars) + CustomID ComponentID `json:"custom_id"` + // Style determines if the component should use the short or paragraph style + Style TextInputStyle `json:"style"` + // Label is the title of this component, describing its use + Label string `json:"label"` + // ValueLimits is the minimum and maximum length for the input + ValueLimits [2]int `json:"-"` + // Required dictates whether or not the user must fill out the component + Required bool `json:"required"` + // Value is the pre-filled value of this component (max 4000 chars) + Value option.NullableString `json:"value,omitempty"` + // Placeholder is the text that appears when the input is empty (max 100 chars) + Placeholder option.NullableString `json:"placeholder,omitempty"` +} + +func (s *TextInputComponent) _cmp() {} +func (s *TextInputComponent) _icp() {} + +func (i *TextInputComponent) ID() ComponentID { + return i.CustomID +} + +func (i *TextInputComponent) Type() ComponentType { + return TextInputComponentType +} + +func (i *TextInputComponent) MarshalJSON() ([]byte, error) { + type text TextInputComponent + + type Msg struct { + Type ComponentType `json:"type"` + *text + MinValues *int `json:"max_values,omitempty"` + MaxValues *int `json:"min_values,omitempty"` + } + + m := Msg{ + Type: i.Type(), + text: (*text)(i), + } + + if i.ValueLimits != [2]int{0, 0} { + m.MinValues = new(int) + m.MaxValues = new(int) + + *m.MinValues = i.ValueLimits[0] + *m.MaxValues = i.ValueLimits[1] + } + return json.Marshal(m) +} + // Unknown is reserved for components with unknown or not yet implemented // components types. It can also be used in place of a ComponentInteraction. type UnknownComponent struct { diff --git a/discord/interaction.go b/discord/interaction.go index 679b38c..bd11ae8 100644 --- a/discord/interaction.go +++ b/discord/interaction.go @@ -86,6 +86,8 @@ func (e *InteractionEvent) UnmarshalJSON(b []byte) error { return nil case AutocompleteInteractionType: e.Data = &AutocompleteInteraction{} + case ModalInteractionType: + e.Data = &ModalInteraction{} default: e.Data = &UnknownInteractionData{ Raw: target.Data, @@ -131,6 +133,7 @@ const ( CommandInteractionType ComponentInteractionType AutocompleteInteractionType + ModalInteractionType ) // InteractionData holds the respose data of an interaction, or more @@ -365,6 +368,19 @@ func (o CommandInteractionOption) FloatValue() (float64, error) { 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 {