diff --git a/.air.toml b/.air.toml index 9197b00..5fbe2ce 100644 --- a/.air.toml +++ b/.air.toml @@ -5,7 +5,7 @@ tmp_dir = "tmp" [build] args_bin = [] bin = "./tmp/main" - cmd = "go build -o ./tmp/main ./src/." + cmd = "go build -o ./tmp/main ./src/" delay = 1000 exclude_dir = ["assets", "tmp", "vendor", "testdata"] exclude_file = [] @@ -13,7 +13,7 @@ tmp_dir = "tmp" exclude_unchanged = false follow_symlink = false full_bin = "" - include_dir = [] + include_dir = ["src"] include_ext = ["go", "tpl", "tmpl", "html"] include_file = [] kill_delay = "0s" diff --git a/src/callbacks.go b/src/callbacks.go index d3c5bb2..cc9f8c3 100644 --- a/src/callbacks.go +++ b/src/callbacks.go @@ -1,13 +1,14 @@ package main import ( + "fmt" + "github.com/bwmarrin/discordgo" ) -// This function will be called (due to AddHandler above) every time a new +// This function will be called (due to AddHandler in main) every time a new // message is created on any channel that the authenticated bot has access to. func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { - // Ignore all messages created by the bot itself // This isn't required in this specific example but it's a good practice. if m.Author.ID == s.State.User.ID { @@ -24,3 +25,33 @@ func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { s.ChannelMessageSend(m.ChannelID, "Ping!") } } + +func userJoin(s *discordgo.Session, m *discordgo.MessageCreate) { + // Check if the message is a guild member add event + if m.Type == discordgo.MessageTypeGuildMemberJoin { + // Get the guild ID and user ID + guildID := m.GuildID + userID := m.Author.ID + + // Replace "YOUR_ROLE_ID" with the actual role ID you want to assign to the user + roleID := "738764170446503956" + + // Assign the role to the user + err := s.GuildMemberRoleAdd(guildID, userID, roleID) + if err != nil { + fmt.Println("Error assigning role to user: ", err) + } else { + fmt.Println("Role assigned to user on join.") + } + } +} + +func setBotStatus(s *discordgo.Session) { + // Replace "Playing Game" with the status you want to set + err := s.UpdateGameStatus(0, "with Golang!") + if err != nil { + fmt.Println("Error setting bot status: ", err) + } else { + fmt.Println("Bot status updated.") + } +} diff --git a/src/commands.go b/src/commands.go new file mode 100644 index 0000000..6b48082 --- /dev/null +++ b/src/commands.go @@ -0,0 +1,514 @@ +package main + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/bwmarrin/discordgo" +) + +var ( + integerOptionMinValue = 1.0 + dmPermission = false + defaultMemberPermissions int64 = discordgo.PermissionManageServer + + 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: "permission-overview", + Description: "Command for demonstration of default command permissions", + DefaultMemberPermissions: &defaultMemberPermissions, + DMPermission: &dmPermission, + }, + { + Name: "basic-command-with-files", + Description: "Basic command with files", + }, + { + Name: "localized-command", + Description: "Localized command. Description and name may vary depending on the Language setting", + NameLocalizations: &map[discordgo.Locale]string{ + discordgo.ChineseCN: "本地化的命令", + }, + DescriptionLocalizations: &map[discordgo.Locale]string{ + discordgo.ChineseCN: "这是一个本地化的命令", + }, + Options: []*discordgo.ApplicationCommandOption{ + { + Name: "localized-option", + Description: "Localized option. Description and name may vary depending on the Language setting", + NameLocalizations: map[discordgo.Locale]string{ + discordgo.ChineseCN: "一个本地化的选项", + }, + DescriptionLocalizations: map[discordgo.Locale]string{ + discordgo.ChineseCN: "这是一个本地化的选项", + }, + Type: discordgo.ApplicationCommandOptionInteger, + Choices: []*discordgo.ApplicationCommandOptionChoice{ + { + Name: "First", + NameLocalizations: map[discordgo.Locale]string{ + discordgo.ChineseCN: "一的", + }, + Value: 1, + }, + { + Name: "Second", + NameLocalizations: map[discordgo.Locale]string{ + discordgo.ChineseCN: "二的", + }, + Value: 2, + }, + }, + }, + }, + }, + { + 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: "subcommand-group", + 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: "nested-subcommand", + 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: "subcommand", + 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!!"), + }, + }, + }, + }) + }, + "localized-command": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + responses := map[discordgo.Locale]string{ + discordgo.ChineseCN: "你好! 这是一个本地化的命令", + } + response := "Hi! This is a localized message" + if r, ok := responses[i.Locale]; ok { + response = r + } + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: response, + }, + }) + if err != nil { + panic(err) + } + }, + "options": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + // Access options in the order provided by the user. + options := i.ApplicationCommandData().Options + + // Or convert the slice into a map + optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options)) + for _, opt := range options { + optionMap[opt.Name] = opt + } + + // This example stores the provided arguments in an []interface{} + // which will be used to format the bot's response + margs := make([]interface{}, 0, len(options)) + msgformat := "You learned how to use command options! " + + "Take a look at the value(s) you entered:\n" + + // Get the value from the option map. + // When the option exists, ok = true + if option, ok := optionMap["string-option"]; ok { + // Option values must be type asserted from interface{}. + // Discordgo provides utility functions to make this simple. + margs = append(margs, option.StringValue()) + msgformat += "> string-option: %s\n" + } + + if opt, ok := optionMap["integer-option"]; ok { + margs = append(margs, opt.IntValue()) + msgformat += "> integer-option: %d\n" + } + + if opt, ok := optionMap["number-option"]; ok { + margs = append(margs, opt.FloatValue()) + msgformat += "> number-option: %f\n" + } + + if opt, ok := optionMap["bool-option"]; ok { + margs = append(margs, opt.BoolValue()) + msgformat += "> bool-option: %v\n" + } + + if opt, ok := optionMap["channel-option"]; ok { + margs = append(margs, opt.ChannelValue(nil).ID) + msgformat += "> channel-option: <#%s>\n" + } + + if opt, ok := optionMap["user-option"]; ok { + margs = append(margs, opt.UserValue(nil).ID) + msgformat += "> user-option: <@%s>\n" + } + + if opt, ok := optionMap["role-option"]; ok { + margs = append(margs, opt.RoleValue(nil, "").ID) + msgformat += "> role-option: <@&%s>\n" + } + + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + // Ignore type for now, they will be discussed in "responses" + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: fmt.Sprintf( + msgformat, + margs..., + ), + }, + }) + }, + "permission-overview": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + perms, err := s.ApplicationCommandPermissions(s.State.User.ID, i.GuildID, i.ApplicationCommandData().ID) + + var restError *discordgo.RESTError + if errors.As(err, &restError) && restError.Message != nil && restError.Message.Code == discordgo.ErrCodeUnknownApplicationCommandPermissions { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: ":x: No permission overwrites", + }, + }) + return + } else if err != nil { + panic(err) + } + + if err != nil { + panic(err) + } + format := "- %s %s\n" + + channels := "" + users := "" + roles := "" + + for _, o := range perms.Permissions { + emoji := "❌" + if o.Permission { + emoji = "☑" + } + + switch o.Type { + case discordgo.ApplicationCommandPermissionTypeUser: + users += fmt.Sprintf(format, emoji, "<@!"+o.ID+">") + case discordgo.ApplicationCommandPermissionTypeChannel: + allChannels, _ := discordgo.GuildAllChannelsID(i.GuildID) + + if o.ID == allChannels { + channels += fmt.Sprintf(format, emoji, "All channels") + } else { + channels += fmt.Sprintf(format, emoji, "<#"+o.ID+">") + } + case discordgo.ApplicationCommandPermissionTypeRole: + if o.ID == i.GuildID { + roles += fmt.Sprintf(format, emoji, "@everyone") + } else { + roles += fmt.Sprintf(format, emoji, "<@&"+o.ID+">") + } + } + } + + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "Permissions overview", + Description: "Overview of permissions for this command", + Fields: []*discordgo.MessageEmbedField{ + { + Name: "Users", + Value: users, + }, + { + Name: "Channels", + Value: channels, + }, + { + Name: "Roles", + Value: roles, + }, + }, + }, + }, + AllowedMentions: &discordgo.MessageAllowedMentions{}, + }, + }) + }, + "subcommands": func(s *discordgo.Session, i *discordgo.InteractionCreate) { + options := i.ApplicationCommandData().Options + content := "" + + // As you can see, names of subcommands (nested, top-level) + // and subcommand groups are provided through the arguments. + switch options[0].Name { + case "subcommand": + content = "The top-level subcommand is executed. Now try to execute the nested one." + case "subcommand-group": + options = options[0].Options + switch options[0].Name { + case "nested-subcommand": + content = "Nice, now you know how to execute nested commands too" + default: + content = "Oops, something went 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(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(i.Interaction, true, &discordgo.WebhookParams{ + Content: "Something went wrong", + }) + return + } + time.AfterFunc(time.Second*5, func() { + 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." + _, err = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ + Content: &content, + }) + if err != nil { + s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: "Something went wrong", + }) + return + } + time.Sleep(time.Second * 10) + s.InteractionResponseDelete(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: discordgo.MessageFlagsEphemeral, + Content: "Surprise!", + }, + }) + msg, err := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: "Followup message has been created, after 5 seconds it will be edited", + }) + if err != nil { + s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: "Something went wrong", + }) + return + } + time.Sleep(time.Second * 5) + + content := "Now the original message is gone and after 10 seconds this message will ~~self-destruct~~ be deleted." + s.FollowupMessageEdit(i.Interaction, msg.ID, &discordgo.WebhookEdit{ + Content: &content, + }) + + 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", + }) + }, + } +) diff --git a/src/main.go b/src/main.go index 516f706..5b5a2e8 100644 --- a/src/main.go +++ b/src/main.go @@ -33,6 +33,13 @@ func main() { // Register the messageCreate func as a callback for MessageCreate events. bot.AddHandler(messageCreate) + bot.AddHandler(userJoin) + + bot.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { + if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { + h(s, i) + } + }) // In this example, we only care about receiving message events. bot.Identify.Intents = discordgo.IntentsGuildMessages @@ -44,6 +51,18 @@ func main() { return } + // Set discord bot's status + setBotStatus(bot) + + registeredCommands := make([]*discordgo.ApplicationCommand, len(commands)) + for i, v := range commands { + cmd, err := bot.ApplicationCommandCreate(bot.State.User.ID, bot.State.Application.GuildID, v) + if err != nil { + log.Panicf("Cannot create '%v' command: %v", v.Name, err) + } + registeredCommands[i] = cmd + } + // Wait here until CTRL-C or other term signal is received. fmt.Println("Bot is now running. Press CTRL-C to exit.") sc := make(chan os.Signal, 1) diff --git a/tmp/build-errors.log b/tmp/build-errors.log index 1cada71..0efc0f3 100644 --- a/tmp/build-errors.log +++ b/tmp/build-errors.log @@ -1 +1 @@ -exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file +exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file diff --git a/tmp/main b/tmp/main index 722926c..8f942b2 100755 Binary files a/tmp/main and b/tmp/main differ