diff --git a/components.go b/components.go index 2ad19a07d..4537bb26a 100644 --- a/components.go +++ b/components.go @@ -13,6 +13,7 @@ const ( ActionsRowComponent ComponentType = 1 ButtonComponent ComponentType = 2 SelectMenuComponent ComponentType = 3 + TextInputComponent 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 TextInputComponent: + umc.MessageComponent = &TextInput{} default: return fmt.Errorf("unknown component type: %d", v.Type) } @@ -195,3 +198,42 @@ func (m SelectMenu) MarshalJSON() ([]byte, error) { Type: m.Type(), }) } + +// TextInput represents text input component. +type TextInput struct { + 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"` +} + +// Type is a method to get the type of a component. +func (TextInput) Type() ComponentType { + return TextInputComponent +} + +// 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 + Type ComponentType `json:"type"` + }{ + inputText: inputText(m), + Type: m.Type(), + }) +} + +// TextInputStyle is style of text in TextInput component. +type TextInputStyle uint + +// Text styles +const ( + TextInputShort TextInputStyle = 1 + TextInputParagraph TextInputStyle = 2 +) diff --git a/examples/modals/main.go b/examples/modals/main.go new file mode 100644 index 000000000..effd8f1a3 --- /dev/null +++ b/examples/modals/main.go @@ -0,0 +1,160 @@ +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.TextInput{ + CustomID: "opinion", + Label: "What is your opinion on them?", + Style: discordgo.TextInputShort, + Placeholder: "Don't be shy, share your opinion with us", + Required: true, + MaxLength: 300, + MinLength: 10, + }, + }, + }, + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.TextInput{ + CustomID: "suggestions", + Label: "What would you suggest to improve them?", + Style: discordgo.TextInputParagraph, + 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.TextInput).Value, + data.Components[1].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).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..b65d72b2f 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) } @@ -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 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 { + 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,11 @@ 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"` } // VerifyInteraction implements message verification of the discord interactions api