package main import ( "flag" "fmt" "log" "os" "os/signal" "strings" "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 ( integerOptionMinValue = 1.0 commands = []*discordgo.ApplicationCommand{ { Name: "basic-command", // All commands and options must have a description // Commands/options without description will fail the registration // of the command. Description: "Basic command", }, { Name: "basic-command-with-files", Description: "Basic command with files", }, { 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", MinValue: &integerOptionMinValue, MaxValue: 10, Required: true, }, { Type: discordgo.ApplicationCommandOptionNumber, Name: "number-option", Description: "Float option", MaxValue: 10.1, Required: true, }, { Type: discordgo.ApplicationCommandOptionBoolean, Name: "bool-option", Description: "Boolean option", Required: true, }, // Required options must be listed first since optional parameters // always come after when they're used. // The same concept applies to Discord's Slash-commands API { Type: discordgo.ApplicationCommandOptionChannel, Name: "channel-option", Description: "Channel option", // Channel type mask ChannelTypes: []discordgo.ChannelType{ discordgo.ChannelTypeGuildText, discordgo.ChannelTypeGuildVoice, }, 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 a command has subcommands/subcommand groups // It must not have top-level options, they aren't accesible in the UI // in this case (at least not yet), so if a command has // subcommands/subcommand any groups registering top-level options // will cause the registration of the command to fail { Name: "scmd-grp", Description: "Subcommands group", Options: []*discordgo.ApplicationCommandOption{ // Also, subcommand groups aren't capable of // containing options, by the name of them, you can see // they can only contain 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: "Channel message with source", Value: 4, }, { Name: "Deferred response With Source", Value: 5, }, }, Required: true, }, }, }, { 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.InteractionResponseData{ Content: "Hey there! Congratulations, you just executed your first slash command", }, }) }, "basic-command-with-files": func(s *discordgo.Session, i *discordgo.InteractionCreate) { s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: "Hey there! Congratulations, you just executed your first slash command with a file in the response", Files: []*discordgo.File{ { ContentType: "text/plain", Name: "test.txt", Reader: strings.NewReader("Hello Discord!!"), }, }, }, }) }, "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.ApplicationCommandData().Options[0].StringValue(), i.ApplicationCommandData().Options[1].IntValue(), i.ApplicationCommandData().Options[2].FloatValue(), i.ApplicationCommandData().Options[3].BoolValue(), } msgformat := ` Now you just learned how to use command options. Take a look to the value of which you've just entered: > string_option: %s > integer_option: %d > number_option: %f > bool_option: %v ` if len(i.ApplicationCommandData().Options) >= 5 { margs = append(margs, i.ApplicationCommandData().Options[4].ChannelValue(nil).ID) msgformat += "> channel-option: <#%s>\n" } if len(i.ApplicationCommandData().Options) >= 6 { margs = append(margs, i.ApplicationCommandData().Options[5].UserValue(nil).ID) msgformat += "> user-option: <@%s>\n" } if len(i.ApplicationCommandData().Options) >= 7 { margs = append(margs, i.ApplicationCommandData().Options[6].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.InteractionResponseData{ 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.ApplicationCommandData().Options[0].Name { case "subcmd": content = "The top-level subcommand is executed. Now try to execute the nested one." default: if i.ApplicationCommandData().Options[0].Name != "scmd-grp" { return } switch i.ApplicationCommandData().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.InteractionResponseData{ Content: content, }, }) }, "responses": func(s *discordgo.Session, i *discordgo.InteractionCreate) { // Responses to a command are very important. // 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 the interaction token and ID for responding to the user's request content := "" // As you can see, the response type names used here are pretty self-explanatory, // but for those who want more information see the official documentation switch i.ApplicationCommandData().Options[0].IntValue() { 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.ApplicationCommandData().Options[0].IntValue()), }) if err != nil { s.FollowupMessageCreate(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{ Content: "Something went wrong", }) } return } err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseType(i.ApplicationCommandData().Options[0].IntValue()), Data: &discordgo.InteractionResponseData{ Content: content, }, }) if err != nil { s.FollowupMessageCreate(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{ Content: "Something went 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 went 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 are basically regular messages (you can create as many of them as you wish) // but work as they are created by webhooks and their functionality // is for handling additional messages after sending a response. s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ // Note: this isn't documented, but you can use that if you want to. // This flag just allows you to create messages visible only for the caller of the command // (user who triggered the command) Flags: 1 << 6, Content: "Surprise!", }, }) msg, err := s.FollowupMessageCreate(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{ Content: "Followup message has been created, after 5 seconds it will be edited", }) if err != nil { s.FollowupMessageCreate(s.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{ Content: "Something went wrong", }) return } time.Sleep(time.Second * 5) s.FollowupMessageEdit(s.State.User.ID, i.Interaction, msg.ID, &discordgo.WebhookEdit{ Content: "Now the 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.ApplicationCommandData().Name]; ok { h(s, i) } }) } func main() { s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { log.Printf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator) }) err := s.Open() if err != nil { log.Fatalf("Cannot open the session: %v", err) } log.Println("Adding commands...") registeredCommands := make([]*discordgo.ApplicationCommand, len(commands)) for i, v := range commands { cmd, err := s.ApplicationCommandCreate(s.State.User.ID, *GuildID, v) if err != nil { log.Panicf("Cannot create '%v' command: %v", v.Name, err) } registeredCommands[i] = cmd } defer s.Close() stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt) log.Println("Press Ctrl+C to exit") <-stop if *RemoveCommands { log.Println("Removing commands...") // // We need to fetch the commands, since deleting requires the command ID. // // We are doing this from the returned commands on line 375, because using // // this will delete all the commands, which might not be desirable, so we // // are deleting only the commands that we added. // registeredCommands, err := s.ApplicationCommands(s.State.User.ID, *GuildID) // if err != nil { // log.Fatalf("Could not fetch registered commands: %v", err) // } for _, v := range registeredCommands { err := s.ApplicationCommandDelete(s.State.User.ID, *GuildID, v.ID) if err != nil { log.Panicf("Cannot delete '%v' command: %v", v.Name, err) } } } log.Println("Gracefully shutdowning") }