|
@@ -0,0 +1,375 @@
|
|
|
+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")
|
|
|
+}
|