From 0a1b2fb6073c33e6b5c4914f492c1d5f6bdae7aa Mon Sep 17 00:00:00 2001 From: nitroflap Date: Fri, 1 Oct 2021 18:25:50 +0300 Subject: [PATCH 1/2] feat(interactions): options autocompletion --- examples/autocomplete/main.go | 255 ++++++++++++++++++++++++++++++++++ interactions.go | 32 +++-- 2 files changed, 276 insertions(+), 11 deletions(-) create mode 100644 examples/autocomplete/main.go diff --git a/examples/autocomplete/main.go b/examples/autocomplete/main.go new file mode 100644 index 000000000..a81bdbe57 --- /dev/null +++ b/examples/autocomplete/main.go @@ -0,0 +1,255 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "os/signal" + + "github.com/bwmarrin/discordgo" +) + +// Bot parameters +var ( + GuildID = flag.String("guild", "", "Test guild ID. If not passed - bot registers commands globally") + BotToken = flag.String("token", "", "Bot access token") + RemoveCommands = flag.Bool("rmcmd", true, "Remove all commands after shutdowning or not") +) + +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: "single-autocomplete", + Description: "Showcase of single autocomplete option", + Type: discordgo.ChatApplicationCommand, + Options: []*discordgo.ApplicationCommandOption{ + { + Name: "autocomplete-option", + Description: "Autocomplete option", + Type: discordgo.ApplicationCommandOptionString, + Required: true, + Autocomplete: true, + }, + }, + }, + { + Name: "multi-autocomplete", + Description: "Showcase of multiple autocomplete option", + Type: discordgo.ChatApplicationCommand, + Options: []*discordgo.ApplicationCommandOption{ + { + Name: "autocomplete-option-1", + Description: "Autocomplete option 1", + Type: discordgo.ApplicationCommandOptionString, + Required: true, + Autocomplete: true, + }, + { + Name: "autocomplete-option-2", + Description: "Autocomplete option 2", + Type: discordgo.ApplicationCommandOptionString, + Required: true, + Autocomplete: true, + }, + }, + }, + } + + commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ + "single-autocomplete": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + switch i.Type { + case discordgo.InteractionApplicationCommand: + data := i.ApplicationCommandData() + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: fmt.Sprintf( + "You picked %q autocompletion", + // Autocompleted options do not affect usual flow of handling application command. They are ordinary options at this stage + data.Options[0].StringValue(), + ), + }, + }) + if err != nil { + panic(err) + } + // Autocomplete options introduce a new interaction type (8) for returining custom autocomplete results. + case discordgo.InteractionApplicationCommandAutocomplete: + data := i.ApplicationCommandData() + choices := []*discordgo.ApplicationCommandOptionChoice{ + { + Name: "Autocomplete", + Value: "autocomplete", + }, + { + Name: "Autocomplete is best!", + Value: "autocomplete_is_best", + }, + { + Name: "Choice 3", + Value: "choice3", + }, + { + Name: "Choice 4", + Value: "choice4", + }, + { + Name: "Choice 5", + Value: "choice5", + }, + // And so on, up to 25 choices + } + + if data.Options[0].StringValue() != "" { + choices = append(choices, &discordgo.ApplicationCommandOptionChoice{ + Name: data.Options[0].StringValue(), // To get user input you just get value of the autocomplete option. + Value: "choice_custom", + }) + } + + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionApplicationCommandAutocompleteResult, + Data: &discordgo.InteractionResponseData{ + Choices: choices, // This is basically the whole purpose of autocomplete interaction - return custom options to the user. + }, + }) + if err != nil { + panic(err) + } + } + }, + "multi-autocomplete": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + switch i.Type { + case discordgo.InteractionApplicationCommand: + data := i.ApplicationCommandData() + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: fmt.Sprintf( + "Option 1: %s\nOption 2: %s", + data.Options[0].StringValue(), + data.Options[1].StringValue(), + ), + }, + }) + if err != nil { + panic(err) + } + case discordgo.InteractionApplicationCommandAutocomplete: + data := i.ApplicationCommandData() + var choices []*discordgo.ApplicationCommandOptionChoice + switch { + // In this case there are multiple autocomplete options. The Focused field shows which option user is focused on. + case data.Options[0].Focused: + choices = []*discordgo.ApplicationCommandOptionChoice{ + { + Name: "Autocomplete 4 first option", + Value: "autocomplete_default", + }, + { + Name: "Choice 3", + Value: "choice3", + }, + { + Name: "Choice 4", + Value: "choice4", + }, + { + Name: "Choice 5", + Value: "choice5", + }, + } + if data.Options[0].StringValue() != "" { + choices = append(choices, &discordgo.ApplicationCommandOptionChoice{ + Name: data.Options[0].StringValue(), + Value: "choice_custom", + }) + } + + case data.Options[1].Focused: + choices = []*discordgo.ApplicationCommandOptionChoice{ + { + Name: "Autocomplete 4 second option", + Value: "autocomplete_1_default", + }, + { + Name: "Choice 3.1", + Value: "choice3_1", + }, + { + Name: "Choice 4.1", + Value: "choice4_1", + }, + { + Name: "Choice 5.1", + Value: "choice5_1", + }, + } + if data.Options[1].StringValue() != "" { + choices = append(choices, &discordgo.ApplicationCommandOptionChoice{ + Name: data.Options[1].StringValue(), + Value: "choice_custom_2", + }) + } + } + + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionApplicationCommandAutocompleteResult, + Data: &discordgo.InteractionResponseData{ + Choices: choices, + }, + }) + 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) { + if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { + h(s, i) + } + }) + err := s.Open() + if err != nil { + log.Fatalf("Cannot open the session: %v", err) + } + defer s.Close() + + createdCommands, err := s.ApplicationCommandBulkOverwrite(s.State.User.ID, *GuildID, commands) + + if err != nil { + log.Fatalf("Cannot register commands: %v", err) + } + + stop := make(chan os.Signal) + signal.Notify(stop, os.Interrupt) //nolint: staticcheck + <-stop + log.Println("Gracefully shutting down") + + if *RemoveCommands { + for _, cmd := range createdCommands { + err := s.ApplicationCommandDelete(s.State.User.ID, *GuildID, cmd.ID) + if err != nil { + log.Fatalf("Cannot delete %q command: %v", cmd.Name, err) + } + } + } +} diff --git a/interactions.go b/interactions.go index 49e62151b..ecc5b008c 100644 --- a/interactions.go +++ b/interactions.go @@ -89,10 +89,13 @@ type ApplicationCommandOption struct { // NOTE: This feature was on the API, but at some point developers decided to remove it. // So I commented it, until it will be officially on the docs. // Default bool `json:"default"` - Required bool `json:"required"` + ChannelTypes []ChannelType `json:"channel_types"` + Required bool `json:"required"` + Options []*ApplicationCommandOption `json:"options"` + + // NOTE: mutually exclusive with Choices. + Autocomplete bool `json:"autocomplete"` Choices []*ApplicationCommandOptionChoice `json:"choices"` - Options []*ApplicationCommandOption `json:"options"` - ChannelTypes []ChannelType `json:"channel_types"` } // ApplicationCommandOptionChoice represents a slash command option choice. @@ -106,9 +109,10 @@ type InteractionType uint8 // Interaction types const ( - InteractionPing InteractionType = 1 - InteractionApplicationCommand InteractionType = 2 - InteractionMessageComponent InteractionType = 3 + InteractionPing InteractionType = 1 + InteractionApplicationCommand InteractionType = 2 + InteractionMessageComponent InteractionType = 3 + InteractionApplicationCommandAutocomplete InteractionType = 4 ) func (t InteractionType) String() string { @@ -168,7 +172,7 @@ func (i *Interaction) UnmarshalJSON(raw []byte) error { *i = Interaction(tmp.interaction) switch tmp.Type { - case InteractionApplicationCommand: + case InteractionApplicationCommand, InteractionApplicationCommandAutocomplete: v := ApplicationCommandInteractionData{} err = json.Unmarshal(tmp.Data, &v) if err != nil { @@ -198,7 +202,7 @@ func (i Interaction) MessageComponentData() (data MessageComponentInteractionDat // ApplicationCommandData is helper function to assert the inner InteractionData to ApplicationCommandInteractionData. // Make sure to check that the Type of the interaction is InteractionApplicationCommand before calling. func (i Interaction) ApplicationCommandData() (data ApplicationCommandInteractionData) { - if i.Type != InteractionApplicationCommand { + if i.Type != InteractionApplicationCommand && i.Type != InteractionApplicationCommandAutocomplete { panic("ApplicationCommandData called on interaction of type " + i.Type.String()) } return i.Data.(ApplicationCommandInteractionData) @@ -259,6 +263,9 @@ type ApplicationCommandInteractionDataOption struct { // NOTE: Contains the value specified by Type. Value interface{} `json:"value,omitempty"` Options []*ApplicationCommandInteractionDataOption `json:"options,omitempty"` + + // NOTE: autocomplete interaction only. + Focused bool `json:"focused,omitempty"` } // IntValue is a utility function for casting option value to integer @@ -389,6 +396,8 @@ const ( InteractionResponseDeferredMessageUpdate InteractionResponseType = 6 // InteractionResponseUpdateMessage is for updating the message to which message component was attached. InteractionResponseUpdateMessage InteractionResponseType = 7 + // InteractionApplicationCommandAutocompleteResult shows autocompletion results. Autocomplete interaction only. + InteractionApplicationCommandAutocompleteResult InteractionResponseType = 8 ) // InteractionResponse represents a response for an interaction event. @@ -404,10 +413,11 @@ type InteractionResponseData struct { Components []MessageComponent `json:"components"` Embeds []*MessageEmbed `json:"embeds,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` + Flags uint64 `json:"flags,omitempty"` + Files []*File `json:"-"` - Flags uint64 `json:"flags,omitempty"` - - Files []*File `json:"-"` + // NOTE: autocomplete interaction only. + Choices []*ApplicationCommandOptionChoice `json:"choices,omitempty"` } // VerifyInteraction implements message verification of the discord interactions api From 7daab1134e1adc4786bfd1d607d7dd59a73414d1 Mon Sep 17 00:00:00 2001 From: Fedor Lapshin Date: Sun, 31 Oct 2021 02:40:24 +0300 Subject: [PATCH 2/2] fix(examples/autocomplete): typo in comment Replaced "returining" with "returning" --- examples/autocomplete/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/autocomplete/main.go b/examples/autocomplete/main.go index a81bdbe57..3e509af4d 100644 --- a/examples/autocomplete/main.go +++ b/examples/autocomplete/main.go @@ -86,7 +86,7 @@ var ( if err != nil { panic(err) } - // Autocomplete options introduce a new interaction type (8) for returining custom autocomplete results. + // Autocomplete options introduce a new interaction type (8) for returning custom autocomplete results. case discordgo.InteractionApplicationCommandAutocomplete: data := i.ApplicationCommandData() choices := []*discordgo.ApplicationCommandOptionChoice{