discordmuffin/examples/slash_commands/main.go
Fedor Lapshin b0fa920925
Slash commands (#856)
* UnknownBan error code addition

* GuildBan method implementation

* Gofmt fix

Gofmt fix

* Interactions: application commands basic API and gateway integration

* Some gitignore update

* Application commands and interactions API implementation

* Some fixes

* Some improvements of slash-commands example and slash-commands API

* OAuth2 endpoints backward compatibility

* Gofmt fix

* Requested fixes and documentation improvement for application commands

* Some fixes

* New and more interesting example of slash-commands usage, merging "interaction.go" and "interactions.go" into a single file. And some fixes.

* Gofmt and documentation fixes

* More fixes

* Gofmt fixes

* More fixes!

* Doc and endpoint fixes

* Gofmt fix

* Remove dependence on open gateway connection

* Remove redundant command ID checks

* Fix typo in ApplicationCommandCreate comment

* Tidy up function calls returning body

* Add upcoming API changes

* Correct return value name, swap parameter order

* Add Version field to ApplicationCommand

* Fix up language in comments

* Remove redundant conversion to float64

Co-authored-by: Carson Hoffman <c@rsonhoffman.com>
2021-02-28 21:54:02 -05:00

375 lines
12 KiB
Go

package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"time"
"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: "basic-command",
// All commands and options must have an description
// Commands/options without description will fail the registration
// of the command.
Description: "Basic command",
},
{
Name: "options",
Description: "Command for demonstrating options",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "string-option",
Description: "String option",
Required: true,
},
{
Type: discordgo.ApplicationCommandOptionInteger,
Name: "integer-option",
Description: "Integer option",
Required: true,
},
{
Type: discordgo.ApplicationCommandOptionBoolean,
Name: "bool-option",
Description: "Boolean option",
Required: true,
},
// Required options must be listed first, because
// like everyone knows - optional parameters is on the back.
// The same concept applies to Discord's Slash-commands API
{
Type: discordgo.ApplicationCommandOptionChannel,
Name: "channel-option",
Description: "Channel option",
Required: false,
},
{
Type: discordgo.ApplicationCommandOptionUser,
Name: "user-option",
Description: "User option",
Required: false,
},
{
Type: discordgo.ApplicationCommandOptionRole,
Name: "role-option",
Description: "Role option",
Required: false,
},
},
},
{
Name: "subcommands",
Description: "Subcommands and command groups example",
Options: []*discordgo.ApplicationCommandOption{
// When command have subcommands/subcommand groups
// It must not have top-level options, they aren't accesible in the UI
// in this case (at least, yet), so if command is with
// subcommands/subcommand groups registering top-level options
// will fail the registration of the command
{
Name: "scmd-grp",
Description: "Subcommands group",
Options: []*discordgo.ApplicationCommandOption{
// Also, subcommand groups isn't capable of
// containg options, by the name of them, you can see
// they can contain only subcommands
{
Name: "nst-subcmd",
Description: "Nested subcommand",
Type: discordgo.ApplicationCommandOptionSubCommand,
},
},
Type: discordgo.ApplicationCommandOptionSubCommandGroup,
},
// Also, you can create both subcommand groups and subcommands
// in the command at the same time. But, there's some limits to
// nesting, count of subcommands (top level and nested) and options.
// Read the intro of slash-commands docs on Discord dev portal
// to get more information
{
Name: "subcmd",
Description: "Top-level subcommand",
Type: discordgo.ApplicationCommandOptionSubCommand,
},
},
},
{
Name: "responses",
Description: "Interaction responses testing initiative",
Options: []*discordgo.ApplicationCommandOption{
{
Name: "resp-type",
Description: "Response type",
Type: discordgo.ApplicationCommandOptionInteger,
Choices: []*discordgo.ApplicationCommandOptionChoice{
{
Name: "Acknowledge",
Value: 2,
},
{
Name: "Channel message",
Value: 3,
},
{
Name: "Channel message with source",
Value: 4,
},
{
Name: "Acknowledge with source",
Value: 5,
},
},
},
},
},
{
Name: "followups",
Description: "Followup messages",
},
}
commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"basic-command": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionApplicationCommandResponseData{
Content: "Hey there! Congratulations, you just executed your first slash command",
},
})
},
"options": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
margs := []interface{}{
// Here we need to convert raw interface{} value to wanted type.
// Also, as you can see, here is used utility functions to convert the value
// to particular type. Yeah, you can use just switch type,
// but this is much simpler
i.Data.Options[0].StringValue(),
i.Data.Options[1].IntValue(),
i.Data.Options[2].BoolValue(),
}
msgformat :=
` Now you just leared how to use command options. Take a look to the value of which you've just entered:
> string_option: %s
> integer_option: %d
> bool_option: %v
`
if len(i.Data.Options) >= 4 {
margs = append(margs, i.Data.Options[3].ChannelValue(nil).ID)
msgformat += "> channel-option: <#%s>\n"
}
if len(i.Data.Options) >= 5 {
margs = append(margs, i.Data.Options[4].UserValue(nil).ID)
msgformat += "> user-option: <@%s>\n"
}
if len(i.Data.Options) >= 6 {
margs = append(margs, i.Data.Options[5].RoleValue(nil, "").ID)
msgformat += "> role-option: <@&%s>\n"
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
// Ignore type for now, we'll discuss them in "responses" part
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionApplicationCommandResponseData{
Content: fmt.Sprintf(
msgformat,
margs...,
),
},
})
},
"subcommands": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
content := ""
// As you can see, the name of subcommand (nested, top-level) or subcommand group
// is provided through arguments.
switch i.Data.Options[0].Name {
case "subcmd":
content =
"The top-level subcommand is executed. Now try to execute nested one."
default:
if i.Data.Options[0].Name != "scmd-grp" {
return
}
switch i.Data.Options[0].Options[0].Name {
case "nst-subcmd":
content = "Nice, now you know how to execute nested commands too"
default:
// I added this in the case something might go wrong
content = "Oops, something gone wrong.\n" +
"Hol' up, you aren't supposed to see this message."
}
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionApplicationCommandResponseData{
Content: content,
},
})
},
"responses": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
// Responses to a command is really important thing.
// First of all, because you need to react to the interaction
// by sending the response in 3 seconds after receiving, otherwise
// interaction will be considered invalid and you can no longer
// use interaction token and ID for responding to the user's request
content := ""
// As you can see, response type names saying by themselvs
// how they're used, but for those who want to get
// more information - read the official documentation
switch i.Data.Options[0].IntValue() {
case int64(discordgo.InteractionResponseChannelMessage):
content =
"Well, you just responded to an interaction, and sent a message.\n" +
"That's all what I wanted to say, yeah."
content +=
"\nAlso... you can edit your response, wait 5 seconds and this message will be changed"
case int64(discordgo.InteractionResponseChannelMessageWithSource):
content =
"You just responded to an interaction, sent a message and showed the original one. " +
"Congratulations!"
content +=
"\nAlso... you can edit your response, wait 5 seconds and this message will be changed"
default:
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseType(i.Data.Options[0].IntValue()),
})
if err != nil {
s.FollowupMessageCreate("", i.Interaction, true, &discordgo.WebhookParams{
Content: "Something gone wrong",
})
}
return
}
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseType(i.Data.Options[0].IntValue()),
Data: &discordgo.InteractionApplicationCommandResponseData{
Content: content,
},
})
if err != nil {
s.FollowupMessageCreate("", i.Interaction, true, &discordgo.WebhookParams{
Content: "Something gone wrong",
})
return
}
time.AfterFunc(time.Second*5, func() {
err = s.InteractionResponseEdit("", i.Interaction, &discordgo.WebhookEdit{
Content: content + "\n\nWell, now you know how to create and edit responses. " +
"But you still don't know how to delete them... so... wait 10 seconds and this " +
"message will be deleted.",
})
if err != nil {
s.FollowupMessageCreate("", i.Interaction, true, &discordgo.WebhookParams{
Content: "Something gone wrong",
})
return
}
time.Sleep(time.Second * 10)
s.InteractionResponseDelete("", i.Interaction)
})
},
"followups": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
// Followup messages is basically regular messages (you can create as many of them as you wish),
// but working as they is created by webhooks and their functional
// is for handling additional messages after sending response.
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionApplicationCommandResponseData{
// Note: this isn't documented, but you can use that if you want to.
// This flag just allows to create messages visible only for the caller (user who triggered the command)
// of the command
Flags: 1 << 6,
Content: "Surprise!",
},
})
msg, err := s.FollowupMessageCreate("", i.Interaction, true, &discordgo.WebhookParams{
Content: "Followup message has created, after 5 seconds it will be edited",
})
if err != nil {
s.FollowupMessageCreate("", i.Interaction, true, &discordgo.WebhookParams{
Content: "Something gone wrong",
})
return
}
time.Sleep(time.Second * 5)
s.FollowupMessageEdit("", i.Interaction, msg.ID, &discordgo.WebhookEdit{
Content: "Now original message is gone and after 10 seconds this message will ~~self-destruct~~ be deleted.",
})
time.Sleep(time.Second * 10)
s.FollowupMessageDelete("", i.Interaction, msg.ID)
s.FollowupMessageCreate("", i.Interaction, true, &discordgo.WebhookParams{
Content: "For those, who didn't skip anything and followed tutorial along fairly, " +
"take a unicorn :unicorn: as reward!\n" +
"Also, as bonus..., look at the original interaction response :D",
})
},
}
)
func init() {
s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
if h, ok := commandHandlers[i.Data.Name]; ok {
h(s, i)
}
})
}
func main() {
s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) {
log.Println("Bot is up!")
})
err := s.Open()
if err != nil {
log.Fatalf("Cannot open the session: %v", err)
}
for _, v := range commands {
_, err := s.ApplicationCommandCreate("", *GuildID, v)
if err != nil {
log.Panicf("Cannot create '%v' command: %v", v.Name, err)
}
}
defer s.Close()
stop := make(chan os.Signal)
signal.Notify(stop, os.Interrupt)
<-stop
log.Println("Gracefully shutdowning")
}