From 09e3d894b734d812cc53927b87bf7e503e61d9f7 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Thu, 9 Dec 2021 04:03:43 +0300 Subject: [PATCH 01/11] feat: modal interactions and text input component --- components.go | 42 +++++++++++ examples/modals/main.go | 159 ++++++++++++++++++++++++++++++++++++++++ interactions.go | 56 +++++++++++++- 3 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 examples/modals/main.go diff --git a/components.go b/components.go index 2ad19a07d..b7b4708e7 100644 --- a/components.go +++ b/components.go @@ -13,6 +13,7 @@ const ( ActionsRowComponent ComponentType = 1 ButtonComponent ComponentType = 2 SelectMenuComponent ComponentType = 3 + InputTextComponent ComponentType = 4 ) // MessageComponent is a base interface for all message components. @@ -42,6 +43,8 @@ func (umc *unmarshalableMessageComponent) UnmarshalJSON(src []byte) error { umc.MessageComponent = &Button{} case SelectMenuComponent: umc.MessageComponent = &SelectMenu{} + case InputTextComponent: + umc.MessageComponent = &InputText{} default: return fmt.Errorf("unknown component type: %d", v.Type) } @@ -195,3 +198,42 @@ func (m SelectMenu) MarshalJSON() ([]byte, error) { Type: m.Type(), }) } + +// InputText represents text input component. +type InputText struct { + CustomID string `json:"custom_id,omitempty"` + Label string `json:"label"` + Style TextStyleType `json:"style"` + Placeholder string `json:"placeholder,omitempty"` + Value string `json:"value,omitempty"` + Required bool `json:"required"` + MinLength int `json:"min_length"` + MaxLength int `json:"max_length,omitempty"` +} + +// Type is a method to get the type of a component. +func (InputText) Type() ComponentType { + return InputTextComponent +} + +// MarshalJSON is a method for marshaling InputText to a JSON object. +func (m InputText) MarshalJSON() ([]byte, error) { + type inputText InputText + + return json.Marshal(struct { + inputText + Type ComponentType `json:"type"` + }{ + inputText: inputText(m), + Type: m.Type(), + }) +} + +// TextStyleType is style of text in InputText component. +type TextStyleType uint + +// Text styles +const ( + TextStyleShort TextStyleType = 1 + TextStyleParagraph TextStyleType = 2 +) diff --git a/examples/modals/main.go b/examples/modals/main.go new file mode 100644 index 000000000..0a38c54de --- /dev/null +++ b/examples/modals/main.go @@ -0,0 +1,159 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "os/signal" + "strings" + + "github.com/bwmarrin/discordgo" +) + +// Bot parameters +var ( + GuildID = flag.String("guild", "", "Test guild ID") + BotToken = flag.String("token", "", "Bot access token") + AppID = flag.String("app", "", "Application ID") + Cleanup = flag.Bool("cleanup", true, "Cleanup of commands") + ResultsChannel = flag.String("results", "", "Channel where send survey results to") +) + +var s *discordgo.Session + +func init() { + flag.Parse() +} + +func init() { + var err error + s, err = discordgo.New("Bot " + *BotToken) + if err != nil { + log.Fatalf("Invalid bot parameters: %v", err) + } +} + +var ( + commands = []discordgo.ApplicationCommand{ + { + Name: "modals-survey", + Description: "Take a survey about modals", + }, + } + commandsHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ + "modals-survey": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseModal, + Data: &discordgo.InteractionResponseData{ + CustomID: "modals_survey_" + i.Interaction.Member.User.ID, + Title: "Modals survey", + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.InputText{ + CustomID: "opinion", + Label: "What is your opinion on them?", + Style: discordgo.TextStyleShort, + Placeholder: "Don't be shy, share your opinion with us", + Required: true, + MaxLength: 300, + }, + }, + }, + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.InputText{ + CustomID: "suggestions", + Label: "What would you suggest to improve them?", + Style: discordgo.TextStyleParagraph, + Required: false, + MaxLength: 2000, + }, + }, + }, + }, + }, + }) + if err != nil { + panic(err) + } + }, + } +) + +func main() { + s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { + log.Println("Bot is up!") + }) + + s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { + switch i.Type { + case discordgo.InteractionApplicationCommand: + if h, ok := commandsHandlers[i.ApplicationCommandData().Name]; ok { + h(s, i) + } + case discordgo.InteractionModalSubmit: + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Thank you for taking your time to fill this survey", + Flags: 1 << 6, + }, + }) + if err != nil { + panic(err) + } + data := i.ModalSubmitData() + + if !strings.HasPrefix(data.CustomID, "modals_survey") { + return + } + + userid := strings.Split(data.CustomID, "_")[2] + _, err = s.ChannelMessageSend(*ResultsChannel, fmt.Sprintf( + "Feedback received. From <@%s>\n\n**Opinion**:\n%s\n\n**Suggestions**:\n%s", + userid, + data.Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.InputText).Value, + data.Components[1].(*discordgo.ActionsRow).Components[0].(*discordgo.InputText).Value, + )) + if err != nil { + panic(err) + } + } + }) + + cmdIDs := make(map[string]string, len(commands)) + + for _, cmd := range commands { + rcmd, err := s.ApplicationCommandCreate(*AppID, *GuildID, &cmd) + if err != nil { + log.Fatalf("Cannot create slash command %q: %v", cmd.Name, err) + } + + cmdIDs[rcmd.ID] = rcmd.Name + } + + err := s.Open() + if err != nil { + log.Fatalf("Cannot open the session: %v", err) + } + defer s.Close() + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt) + <-stop + log.Println("Graceful shutdown") + + if !*Cleanup { + return + } + + for id, name := range cmdIDs { + err := s.ApplicationCommandDelete(*AppID, *GuildID, id) + if err != nil { + log.Fatalf("Cannot delete slash command %q: %v", name, err) + } + } + +} diff --git a/interactions.go b/interactions.go index 6f1891792..b56e46ada 100644 --- a/interactions.go +++ b/interactions.go @@ -113,6 +113,7 @@ const ( InteractionApplicationCommand InteractionType = 2 InteractionMessageComponent InteractionType = 3 InteractionApplicationCommandAutocomplete InteractionType = 4 + InteractionModalSubmit InteractionType = 5 ) func (t InteractionType) String() string { @@ -123,6 +124,8 @@ func (t InteractionType) String() string { return "ApplicationCommand" case InteractionMessageComponent: return "MessageComponent" + case InteractionModalSubmit: + return "ModalSubmit" } return fmt.Sprintf("InteractionType(%d)", t) } @@ -137,8 +140,8 @@ type Interaction struct { // The message on which interaction was used. // NOTE: this field is only filled when a button click triggered the interaction. Otherwise it will be nil. - Message *Message `json:"message"` + Message *Message `json:"message"` // 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. @@ -186,6 +189,13 @@ func (i *Interaction) UnmarshalJSON(raw []byte) error { return err } i.Data = v + case InteractionModalSubmit: + v := ModalSubmitInteractionData{} + err = json.Unmarshal(tmp.Data, &v) + if err != nil { + return err + } + i.Data = v } return nil } @@ -208,6 +218,15 @@ func (i Interaction) ApplicationCommandData() (data ApplicationCommandInteractio return i.Data.(ApplicationCommandInteractionData) } +// ModalSubmitData is helper function to assert the innter InteractionData to ModalSubmitInteractionData. +// Make sure to check that the Type of the interaction is InteractionModalSubmit before calling. +func (i Interaction) ModalSubmitData() (data ModalSubmitInteractionData) { + if i.Type != InteractionModalSubmit { + panic("ModalSubmitData called on interaction of type " + i.Type.String()) + } + return i.Data.(ModalSubmitInteractionData) +} + // InteractionData is a common interface for all types of interaction data. type InteractionData interface { Type() InteractionType @@ -256,6 +275,36 @@ func (MessageComponentInteractionData) Type() InteractionType { return InteractionMessageComponent } +// ModalSubmitInteractionData contains the data of modal submit interaction. +type ModalSubmitInteractionData struct { + CustomID string `json:"custom_id"` + Components []MessageComponent `json:"-"` +} + +// Type returns the type of interaction data. +func (ModalSubmitInteractionData) Type() InteractionType { + return InteractionModalSubmit +} + +// UnmarshalJSON is a helper function to correctly unmarshal Components. +func (d *ModalSubmitInteractionData) UnmarshalJSON(data []byte) error { + type modalSubmitInteractionData ModalSubmitInteractionData + var v struct { + modalSubmitInteractionData + RawComponents []unmarshalableMessageComponent `json:"components"` + } + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + *d = ModalSubmitInteractionData(v.modalSubmitInteractionData) + d.Components = make([]MessageComponent, len(v.RawComponents)) + for i, v := range v.RawComponents { + d.Components[i] = v.MessageComponent + } + return err +} + // ApplicationCommandInteractionDataOption represents an option of a slash command. type ApplicationCommandInteractionDataOption struct { Name string `json:"name"` @@ -398,6 +447,8 @@ const ( InteractionResponseUpdateMessage InteractionResponseType = 7 // InteractionApplicationCommandAutocompleteResult shows autocompletion results. Autocomplete interaction only. InteractionApplicationCommandAutocompleteResult InteractionResponseType = 8 + // InteractionResponseModal is for responding to an interaction with a modal window. + InteractionResponseModal InteractionResponseType = 9 ) // InteractionResponse represents a response for an interaction event. @@ -418,6 +469,9 @@ type InteractionResponseData struct { // NOTE: autocomplete interaction only. Choices []*ApplicationCommandOptionChoice `json:"choices,omitempty"` + + CustomID string `json:"custom_id,omitempty"` + Title string `json:"title,omitempty"` } // VerifyInteraction implements message verification of the discord interactions api From a0095c14ed964b9fe8de42b67c98b6167d6eb4f2 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 9 Feb 2022 03:39:28 +0300 Subject: [PATCH 02/11] chore(components): rename InputText to TextInput --- components.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/components.go b/components.go index b7b4708e7..6c68de845 100644 --- a/components.go +++ b/components.go @@ -13,7 +13,7 @@ const ( ActionsRowComponent ComponentType = 1 ButtonComponent ComponentType = 2 SelectMenuComponent ComponentType = 3 - InputTextComponent ComponentType = 4 + TextInputComponent ComponentType = 4 ) // MessageComponent is a base interface for all message components. @@ -43,8 +43,8 @@ func (umc *unmarshalableMessageComponent) UnmarshalJSON(src []byte) error { umc.MessageComponent = &Button{} case SelectMenuComponent: umc.MessageComponent = &SelectMenu{} - case InputTextComponent: - umc.MessageComponent = &InputText{} + case TextInputComponent: + umc.MessageComponent = &TextInput{} default: return fmt.Errorf("unknown component type: %d", v.Type) } @@ -199,8 +199,8 @@ func (m SelectMenu) MarshalJSON() ([]byte, error) { }) } -// InputText represents text input component. -type InputText struct { +// TextInput represents text input component. +type TextInput struct { CustomID string `json:"custom_id,omitempty"` Label string `json:"label"` Style TextStyleType `json:"style"` @@ -212,13 +212,13 @@ type InputText struct { } // Type is a method to get the type of a component. -func (InputText) Type() ComponentType { - return InputTextComponent +func (TextInput) Type() ComponentType { + return TextInputComponent } -// MarshalJSON is a method for marshaling InputText to a JSON object. -func (m InputText) MarshalJSON() ([]byte, error) { - type inputText InputText +// MarshalJSON is a method for marshaling TextInput to a JSON object. +func (m TextInput) MarshalJSON() ([]byte, error) { + type inputText TextInput return json.Marshal(struct { inputText @@ -229,7 +229,7 @@ func (m InputText) MarshalJSON() ([]byte, error) { }) } -// TextStyleType is style of text in InputText component. +// TextStyleType is style of text in TextInput component. type TextStyleType uint // Text styles From 1b48b9b22e0b58e173fce5d3abda3e4b9395c3c9 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 9 Feb 2022 03:45:50 +0300 Subject: [PATCH 03/11] fix(examples/modals): renamed InputText to TextInput --- examples/modals/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/modals/main.go b/examples/modals/main.go index 0a38c54de..284814e42 100644 --- a/examples/modals/main.go +++ b/examples/modals/main.go @@ -51,7 +51,7 @@ var ( Components: []discordgo.MessageComponent{ discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ - discordgo.InputText{ + discordgo.TextInput{ CustomID: "opinion", Label: "What is your opinion on them?", Style: discordgo.TextStyleShort, @@ -63,7 +63,7 @@ var ( }, discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ - discordgo.InputText{ + discordgo.TextInput{ CustomID: "suggestions", Label: "What would you suggest to improve them?", Style: discordgo.TextStyleParagraph, From 19d46d852a7a8b69b930290b2286c99ddc71b93d Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 9 Feb 2022 03:46:09 +0300 Subject: [PATCH 04/11] feat(examples/modals): added MinLength --- examples/modals/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/modals/main.go b/examples/modals/main.go index 284814e42..bdb3960aa 100644 --- a/examples/modals/main.go +++ b/examples/modals/main.go @@ -58,6 +58,7 @@ var ( Placeholder: "Don't be shy, share your opinion with us", Required: true, MaxLength: 300, + MinLength: 10, }, }, }, From dcf6fe0128ee0e6456357e758120e28ff0dbcbd6 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 9 Feb 2022 03:47:44 +0300 Subject: [PATCH 05/11] fix(examples/modals): more renaming of InputText to TextInput --- examples/modals/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/modals/main.go b/examples/modals/main.go index bdb3960aa..90e9887d6 100644 --- a/examples/modals/main.go +++ b/examples/modals/main.go @@ -115,8 +115,8 @@ func main() { _, err = s.ChannelMessageSend(*ResultsChannel, fmt.Sprintf( "Feedback received. From <@%s>\n\n**Opinion**:\n%s\n\n**Suggestions**:\n%s", userid, - data.Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.InputText).Value, - data.Components[1].(*discordgo.ActionsRow).Components[0].(*discordgo.InputText).Value, + data.Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value, + data.Components[1].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value, )) if err != nil { panic(err) From 147703db8e8d13f5099d56af059aa0c8267f28ae Mon Sep 17 00:00:00 2001 From: Fedor Lapshin Date: Wed, 9 Feb 2022 03:58:16 +0300 Subject: [PATCH 06/11] feat(components): renamed TextStyleType to TextInputStyleType --- components.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components.go b/components.go index 6c68de845..b2943c58a 100644 --- a/components.go +++ b/components.go @@ -229,11 +229,11 @@ func (m TextInput) MarshalJSON() ([]byte, error) { }) } -// TextStyleType is style of text in TextInput component. -type TextStyleType uint +// TextInputStyleType is style of text in TextInput component. +type TextInputStyleType uint // Text styles const ( - TextStyleShort TextStyleType = 1 - TextStyleParagraph TextStyleType = 2 + TextInputShort TextStyleType = 1 + TextInputParagraph TextStyleType = 2 ) From 21ba398898a50ec5c426225981c4ef9371c44293 Mon Sep 17 00:00:00 2001 From: Fedor Lapshin Date: Wed, 9 Feb 2022 14:07:36 +0300 Subject: [PATCH 07/11] feat(components): add omitempty and use TextInputStyle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ToπSenpai <15636011+TopiSenpai@users.noreply.github.com> --- components.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components.go b/components.go index b2943c58a..1f4ba58fb 100644 --- a/components.go +++ b/components.go @@ -201,13 +201,13 @@ func (m SelectMenu) MarshalJSON() ([]byte, error) { // TextInput represents text input component. type TextInput struct { - CustomID string `json:"custom_id,omitempty"` + CustomID string `json:"custom_id"` Label string `json:"label"` - Style TextStyleType `json:"style"` + Style TextInputStyle `json:"style"` Placeholder string `json:"placeholder,omitempty"` Value string `json:"value,omitempty"` - Required bool `json:"required"` - MinLength int `json:"min_length"` + Required bool `json:"required,omitempty"` + MinLength int `json:"min_length,omitempty"` MaxLength int `json:"max_length,omitempty"` } From e6b33f37b70abcafeee8c3422c0c09eee7d6c870 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 9 Feb 2022 14:12:31 +0300 Subject: [PATCH 08/11] fix(components): renamed TextInputStyleType to TextInputStyle and fixed example --- components.go | 22 +++++++++++----------- examples/modals/main.go | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/components.go b/components.go index 1f4ba58fb..4537bb26a 100644 --- a/components.go +++ b/components.go @@ -201,14 +201,14 @@ func (m SelectMenu) MarshalJSON() ([]byte, error) { // TextInput represents text input component. type TextInput struct { - CustomID string `json:"custom_id"` - Label string `json:"label"` + CustomID string `json:"custom_id"` + Label string `json:"label"` Style TextInputStyle `json:"style"` - Placeholder string `json:"placeholder,omitempty"` - Value string `json:"value,omitempty"` - Required bool `json:"required,omitempty"` - MinLength int `json:"min_length,omitempty"` - MaxLength int `json:"max_length,omitempty"` + Placeholder string `json:"placeholder,omitempty"` + Value string `json:"value,omitempty"` + Required bool `json:"required,omitempty"` + MinLength int `json:"min_length,omitempty"` + MaxLength int `json:"max_length,omitempty"` } // Type is a method to get the type of a component. @@ -229,11 +229,11 @@ func (m TextInput) MarshalJSON() ([]byte, error) { }) } -// TextInputStyleType is style of text in TextInput component. -type TextInputStyleType uint +// TextInputStyle is style of text in TextInput component. +type TextInputStyle uint // Text styles const ( - TextInputShort TextStyleType = 1 - TextInputParagraph TextStyleType = 2 + TextInputShort TextInputStyle = 1 + TextInputParagraph TextInputStyle = 2 ) diff --git a/examples/modals/main.go b/examples/modals/main.go index 90e9887d6..effd8f1a3 100644 --- a/examples/modals/main.go +++ b/examples/modals/main.go @@ -54,7 +54,7 @@ var ( discordgo.TextInput{ CustomID: "opinion", Label: "What is your opinion on them?", - Style: discordgo.TextStyleShort, + Style: discordgo.TextInputShort, Placeholder: "Don't be shy, share your opinion with us", Required: true, MaxLength: 300, @@ -67,7 +67,7 @@ var ( discordgo.TextInput{ CustomID: "suggestions", Label: "What would you suggest to improve them?", - Style: discordgo.TextStyleParagraph, + Style: discordgo.TextInputParagraph, Required: false, MaxLength: 2000, }, From e44c83a1fc44227fa89d07c5e4ce8fe35e416ff0 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 16 Feb 2022 02:13:29 +0300 Subject: [PATCH 09/11] feat(interactions): added note about modals to InteractionResponse --- interactions.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interactions.go b/interactions.go index b56e46ada..b71ebb130 100644 --- a/interactions.go +++ b/interactions.go @@ -470,6 +470,8 @@ type InteractionResponseData struct { // NOTE: autocomplete interaction only. Choices []*ApplicationCommandOptionChoice `json:"choices,omitempty"` + // NOTE: modal interaction only. + CustomID string `json:"custom_id,omitempty"` Title string `json:"title,omitempty"` } From 195a4c4418d10486b02df93b0488a8a449bc4892 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 16 Feb 2022 02:15:16 +0300 Subject: [PATCH 10/11] fix(interactions): typo in comment to ModalSubmitData --- interactions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interactions.go b/interactions.go index b71ebb130..1658af3b4 100644 --- a/interactions.go +++ b/interactions.go @@ -218,7 +218,7 @@ func (i Interaction) ApplicationCommandData() (data ApplicationCommandInteractio return i.Data.(ApplicationCommandInteractionData) } -// ModalSubmitData is helper function to assert the innter InteractionData to ModalSubmitInteractionData. +// ModalSubmitData is helper function to assert the inner InteractionData to ModalSubmitInteractionData. // Make sure to check that the Type of the interaction is InteractionModalSubmit before calling. func (i Interaction) ModalSubmitData() (data ModalSubmitInteractionData) { if i.Type != InteractionModalSubmit { From a731868627444345ed449b7346e636164cc97825 Mon Sep 17 00:00:00 2001 From: nitroflap Date: Wed, 16 Feb 2022 02:16:44 +0300 Subject: [PATCH 11/11] fix(interactions): unnecessary newline between field and comment --- interactions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interactions.go b/interactions.go index 1658af3b4..b65d72b2f 100644 --- a/interactions.go +++ b/interactions.go @@ -140,8 +140,8 @@ type Interaction struct { // The message on which interaction was used. // NOTE: this field is only filled when a button click triggered the interaction. Otherwise it will be nil. - Message *Message `json:"message"` + // 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.