Skip to content

Commit

Permalink
Slash commands options auto completion (#1014)
Browse files Browse the repository at this point in the history
* feat(interactions): options autocompletion

* fix(examples/autocomplete): typo in comment

Replaced "returining" with "returning"
  • Loading branch information
FedorLap2006 committed Nov 17, 2021
1 parent 007bf76 commit fd6228c
Show file tree
Hide file tree
Showing 2 changed files with 276 additions and 11 deletions.
255 changes: 255 additions & 0 deletions 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 returning 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)
}
}
}
}
32 changes: 21 additions & 11 deletions interactions.go
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down

0 comments on commit fd6228c

Please sign in to comment.