Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slash commands options auto completion #1014

Merged
merged 2 commits into from Nov 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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