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(s.State.User.ID, 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(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{ Content: "Something gone wrong", }) return } time.AfterFunc(time.Second*5, func() { err = s.InteractionResponseEdit(s.State.User.ID, 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(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{ Content: "Something gone wrong", }) return } time.Sleep(time.Second * 10) s.InteractionResponseDelete(s.State.User.ID, 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(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{ Content: "Followup message has created, after 5 seconds it will be edited", }) if err != nil { s.FollowupMessageCreate(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{ Content: "Something gone wrong", }) return } time.Sleep(time.Second * 5) s.FollowupMessageEdit(s.State.User.ID, 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(s.State.User.ID, i.Interaction, msg.ID) s.FollowupMessageCreate(s.State.User.ID, 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(s.State.User.ID, *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") }