From e19717e6511215ee43cfd9134de12cf1ab741bf3 Mon Sep 17 00:00:00 2001 From: Riley Smith Date: Fri, 22 Mar 2024 12:14:38 -0700 Subject: [PATCH 01/10] update discordgo, commands, interactions --- .github/workflows/go.yml | 23 ++- .gitignore | 2 + arguments.go | 176 +---------------- commands.go | 398 +++++++++++++++++++++++++++++---------- go.mod | 13 +- go.sum | 19 +- guilds.go | 6 +- handlers.go | 2 +- interaction.go | 84 +++------ 9 files changed, 375 insertions(+), 348 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f869ba3..4b7146e 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,24 +2,23 @@ name: Go on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: - build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.17 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.18 - - name: Build - run: go build -v ./... + - name: Build + run: go build -v ./... - - name: Test - run: go test -v ./... + - name: Test + run: go test -v ./... diff --git a/.gitignore b/.gitignore index 2881224..80b4be2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ # ignore IDE related files .idea .vscode + +.DS_Store \ No newline at end of file diff --git a/arguments.go b/arguments.go index c358d2a..4b352e4 100644 --- a/arguments.go +++ b/arguments.go @@ -3,11 +3,12 @@ package framework import ( "errors" "fmt" + "strconv" + "strings" + "github.com/QPixel/orderedmap" "github.com/bwmarrin/discordgo" "github.com/dlclark/regexp2" - "strconv" - "strings" ) // Arguments.go @@ -49,16 +50,18 @@ var ( ) // ArgInfo -// Describes a CommandInfo argument +// Describes the argument that a command will receive type ArgInfo struct { + Name string Match ArgTypes TypeGuard ArgTypeGuards Description string Required bool Flag bool DefaultOption string - Choices []string + Choices []*discordgo.ApplicationCommandOptionChoice Regex *regexp2.Regexp + AutoComplete bool } // CommandArg @@ -72,171 +75,6 @@ type CommandArg struct { // Type of the arguments field in the command ctx type Arguments map[string]CommandArg -// -- Command Configuration -- - -// CreateCommandInfo -// Creates a pointer to a CommandInfo -func CreateCommandInfo(trigger string, description string, public bool, group Group) *CommandInfo { - cI := &CommandInfo{ - Aliases: nil, - Arguments: orderedmap.New(), - Description: description, - Group: group, - Public: public, - IsTyping: false, - Trigger: trigger, - } - return cI -} - -// CreateRawCmdInfo -// Creates a pointer to a CommandInfo -func CreateRawCmdInfo(cI *CommandInfo) *CommandInfo { - cI.Arguments = orderedmap.New() - return cI -} - -// SetParent -// Sets the parent properties -func (cI *CommandInfo) SetParent(isParent bool, parentID string) { - if !isParent { - cI.IsChild = true - } - cI.IsParent = isParent - cI.ParentID = parentID -} - -//AddCmdAlias -// Adds a list of strings as aliases for the command -func (cI *CommandInfo) AddCmdAlias(aliases []string) *CommandInfo { - if len(aliases) < 1 { - return cI - } - cI.Aliases = aliases - return cI -} - -// AddArg -// Adds an arg to the CommandInfo -func (cI *CommandInfo) AddArg(argument string, typeGuard ArgTypeGuards, match ArgTypes, description string, required bool, defaultOption string) *CommandInfo { - cI.Arguments.Set(argument, &ArgInfo{ - TypeGuard: typeGuard, - Description: description, - Required: required, - Match: match, - DefaultOption: defaultOption, - Choices: nil, - Regex: nil, - }) - return cI -} - -// AddFlagArg -// Adds a flag arg, which is a special type of argument -// This type of argument allows for the user to place the "phrase" (e.g: --debug) anywhere -// in the command string and the parser will find it. -func (cI *CommandInfo) AddFlagArg(flag string, typeGuard ArgTypeGuards, match ArgTypes, description string, required bool, defaultOption string) *CommandInfo { - regexString := flag - if match == ArgOption { - // Currently, it only supports a limited character set. - // todo figure out how to detect any character - regexString = fmt.Sprintf("--%s (([a-zA-Z0-9:/.]+)|(\"[a-zA-Z0-9:/. ]+\"))", flag) - } else { - regexString = fmt.Sprintf("--%s", flag) - } - regex, err := regexp2.Compile(regexString, 0) - if err != nil { - log.Fatalf("Unable to create regex for flag on command %s flag: %s", cI.Trigger, flag) - } - cI.Arguments.Set(flag, &ArgInfo{ - Description: description, - Required: required, - Flag: true, - Match: match, - TypeGuard: typeGuard, - DefaultOption: defaultOption, - Regex: regex, - }) - return cI -} - -// AddChoices -// Adds SubCmd choices -func (cI *CommandInfo) AddChoices(arg string, choices []string) *CommandInfo { - v, ok := cI.Arguments.Get(arg) - if ok { - vv := v.(*ArgInfo) - vv.Choices = choices - cI.Arguments.Set(arg, vv) - } else { - log.Errorf("Unable to get argument %s in AddChoices", arg) - return cI - } - return cI -} - -func (cI *CommandInfo) SetTyping(isTyping bool) *CommandInfo { - cI.IsTyping = isTyping - return cI -} - -//todo subcommand stuff -//// BindToChoice -//// Bind an arg to choice (subcmd) -//func (cI *CommandInfo) BindToChoice(arg string, choice string) { -// -//} - -// CreateAppOptSt -// Creates an ApplicationOptionsStruct for all the args. -func (cI *CommandInfo) CreateAppOptSt() *discordgo.ApplicationCommandOption { - return &discordgo.ApplicationCommandOption{} -} - -// -- Argument Parser -- - -// ParseArguments -// Version two of the argument parser -func ParseArguments(args string, infoArgs *orderedmap.OrderedMap) *Arguments { - ar := make(Arguments) - - if args == "" || len(infoArgs.Keys()) < 1 { - return &ar - } - // Split string on spaces to get every "phrase" - - // bool to parse content strings - moreContent := false - // Keys of infoArgs - k := infoArgs.Keys() - var modK []string - // First find all flags in the string. - splitString, ar, modK := findAllFlags(args, k, infoArgs, &ar) - // Find all the option args (e.g. single 'phrases' or quoted strings) - // Then return the currentPos, so we can index k and find remaining keys. - // Also return a modified Arguments struct - - ar, moreContent, splitString, modK = findAllOptionArgs(splitString, modK, infoArgs, &ar) - - // If there is more content, lets find it - if moreContent == true { - v, ok := infoArgs.Get(modK[0]) - if !ok { - return &ar - } - vv := v.(*ArgInfo) - commandContent, _ := createContentString(splitString, 0) - ar[modK[0]] = CommandArg{ - info: *vv, - Value: commandContent, - } - return &ar - // Else return the args struct - } else { - return &ar - } -} - /* Argument Parsing Helpers */ func createContentString(splitString []string, currentPos int) (string, int) { diff --git a/commands.go b/commands.go index 2954ddf..fc130be 100644 --- a/commands.go +++ b/commands.go @@ -1,12 +1,15 @@ package framework import ( - "github.com/QPixel/orderedmap" - "github.com/bwmarrin/discordgo" + "fmt" "runtime" "runtime/debug" "strings" "time" + + "github.com/QPixel/orderedmap" + "github.com/bwmarrin/discordgo" + "github.com/dlclark/regexp2" ) // commands.go @@ -33,7 +36,7 @@ type CommandInfo struct { IsTyping bool // Whether the command will show a typing thing when ran. IsParent bool // If the command is the parent of a subcommand tree IsChild bool // If the command is the child - Trigger string // The string that will trigger the command + Name string // The name of the command } // Context @@ -53,108 +56,309 @@ type Context struct { type BotFunction func(ctx *Context) // Command -// The definition of a command, which is that command's information, along with the function it will run +// The definition of a command, which is that command's information, along with the functions it will run +// Handlers is a map of strings to BotFunctions, so that different handlers can be used for different situations type Command struct { - Info CommandInfo - Function BotFunction + Info *CommandInfo + Handlers map[string]BotFunction + ApplicationCommand *discordgo.ApplicationCommand } -// ChildCommand -// Defines how child commands are stored -type ChildCommand map[string]map[string]Command - // commands -// All the registered core commands (not custom commands) +// All commands that are registered with the bot are stored here // This is private so that other commands cannot modify it -var commands = make(map[string]Command) - -// childCommands -// All the registered ChildCommands (SubCmdGrps) -// This is private so other commands cannot modify it -var childCommands = make(ChildCommand) +var commands = make(map[string]*Command) // Command Aliases // A map of aliases to command triggers var commandAliases = make(map[string]string) -// slashCommands -// All the registered core commands that are also slash commands -// This is also private so other commands cannot modify it -var slashCommands = make(map[string]discordgo.ApplicationCommand) - // commandsGC var commandsGC = 0 +// -- Command Configuration -- + +// CreateCommandInfo +// Creates a pointer to a CommandInfo +func CreateCommandInfo(name string, description string, public bool, group Group) *CommandInfo { + cI := &CommandInfo{ + Aliases: make([]string, 0), + Arguments: orderedmap.New(), + Description: description, + Group: group, + Public: public, + IsTyping: false, + Name: name, + IsParent: true, + IsChild: false, + } + cI.Aliases = append(cI.Aliases, name) + return cI +} + +// Sets the parent properties +func (cI *CommandInfo) SetParent(isParent bool, parentID string) { + if !isParent { + cI.IsChild = true + } + cI.IsParent = isParent + cI.ParentID = parentID +} + +// AddCmdAlias +// Adds a list of strings as aliases for the command +func (cI *CommandInfo) AddCmdAlias(aliases []string) *CommandInfo { + if len(aliases) < 1 { + return cI + } + cI.Aliases = aliases + return cI +} + +// AddArg +// Adds an arg to the CommandInfo +func (cI *CommandInfo) AddArg(argument string, typeGuard ArgTypeGuards, match ArgTypes, description string, required bool) *CommandInfo { + cI.Arguments.Set(argument, &ArgInfo{ + TypeGuard: typeGuard, + Description: description, + Required: required, + Match: match, + DefaultOption: "", + Choices: make([]*discordgo.ApplicationCommandOptionChoice, 0), + Regex: nil, + AutoComplete: false, + }) + return cI +} + +// AddFlagArg +// Adds a flag arg, which is a special type of argument +// This type of argument allows for the user to place the "phrase" (e.g: --debug) anywhere +// in the command string and the parser will find it. +func (cI *CommandInfo) AddFlagArg(flag string, typeGuard ArgTypeGuards, match ArgTypes, description string, required bool, defaultOption string) *CommandInfo { + regexString := flag + if match == ArgOption { + // Currently, it only supports a limited character set. + // todo figure out how to detect any character + regexString = fmt.Sprintf("--%s (([a-zA-Z0-9:/.]+)|(\"[a-zA-Z0-9:/. ]+\"))", flag) + } else { + regexString = fmt.Sprintf("--%s", flag) + } + regex, err := regexp2.Compile(regexString, 0) + if err != nil { + log.Fatalf("Unable to create regex for flag on command %s flag: %s", cI.Name, flag) + } + cI.Arguments.Set(flag, &ArgInfo{ + Description: description, + Required: required, + Flag: true, + Match: match, + TypeGuard: typeGuard, + DefaultOption: defaultOption, + Regex: regex, + }) + return cI +} + +// AddChoice +// Adds an argument choice +func (cI *CommandInfo) AddChoice(arg string, choice string) *CommandInfo { + v, ok := cI.Arguments.Get(arg) + if ok { + vv := v.(*ArgInfo) + vv.Choices = append(vv.Choices, &discordgo.ApplicationCommandOptionChoice{ + Name: choice, + Value: choice, + }) + cI.Arguments.Set(arg, vv) + } else { + log.Errorf("Unable to get argument %s in AddChoice", arg) + return cI + } + return cI +} + +// AddChoices +// Adds SubCmd choices +func (cI *CommandInfo) AddChoices(arg string, choices []string) *CommandInfo { + v, ok := cI.Arguments.Get(arg) + if ok { + vv := v.(*ArgInfo) + optionChoice := make([]*discordgo.ApplicationCommandOptionChoice, 0) + for _, v := range choices { + optionChoice = append(optionChoice, &discordgo.ApplicationCommandOptionChoice{ + Name: v, + Value: v, + }) + } + vv.Choices = append(vv.Choices, optionChoice...) + cI.Arguments.Set(arg, vv) + } else { + log.Errorf("Unable to get argument %s in AddChoices", arg) + return cI + } + return cI +} + +// AddChoices +// Adds SubCmd choices +func (cI *CommandInfo) AddChoicesManual(arg string, choices []*discordgo.ApplicationCommandOptionChoice) *CommandInfo { + v, ok := cI.Arguments.Get(arg) + if ok { + vv := v.(*ArgInfo) + vv.Choices = append(vv.Choices, choices...) + cI.Arguments.Set(arg, vv) + } else { + log.Errorf("Unable to get argument %s in AddChoices", arg) + return cI + } + return cI +} + +func (cI *CommandInfo) SetTyping(isTyping bool) *CommandInfo { + cI.IsTyping = isTyping + return cI +} + +func (cI *CommandInfo) SetAutocomplete(arg string, autocomplete bool) *CommandInfo { + v, ok := cI.Arguments.Get(arg) + if ok { + vv := v.(*ArgInfo) + vv.AutoComplete = autocomplete + cI.Arguments.Set(arg, vv) + } else { + log.Errorf("Unable to get argument %s in SetAutocomplete", arg) + return cI + } + return cI +} + +// -- Argument Parser -- + +// ParseArguments +// Version two of the argument parser +func ParseArguments(args string, infoArgs *orderedmap.OrderedMap) *Arguments { + ar := make(Arguments) + + if args == "" || len(infoArgs.Keys()) < 1 { + return &ar + } + // Split string on spaces to get every "phrase" + + // bool to parse content strings + moreContent := false + // Keys of infoArgs + k := infoArgs.Keys() + var modK []string + // First find all flags in the string. + splitString, ar, modK := findAllFlags(args, k, infoArgs, &ar) + // Find all the option args (e.g. single 'phrases' or quoted strings) + // Then return the currentPos, so we can index k and find remaining keys. + // Also return a modified Arguments struct + + ar, moreContent, splitString, modK = findAllOptionArgs(splitString, modK, infoArgs, &ar) + + // If there is more content, lets find it + if moreContent == true { + v, ok := infoArgs.Get(modK[0]) + if !ok { + return &ar + } + vv := v.(*ArgInfo) + commandContent, _ := createContentString(splitString, 0) + ar[modK[0]] = CommandArg{ + info: *vv, + Value: commandContent, + } + return &ar + // Else return the args struct + } else { + return &ar + } +} + // AddCommand // Add a command to the bot func AddCommand(info *CommandInfo, function BotFunction) { - // Add Trigger to the alias - info.Aliases = append(info.Aliases, info.Trigger) // Build a Command object for this command + appCommand := createApplicationChatCommand(info) command := Command{ - Info: *info, - Function: function, + Info: info, + Handlers: make(map[string]BotFunction), + ApplicationCommand: appCommand, } + + command.Handlers["default"] = function + // adds a alias to a map; command aliases are case-sensitive for _, alias := range info.Aliases { if _, ok := commandAliases[alias]; ok { - log.Errorf("Alias was already registered %s for command %s", alias, info.Trigger) + log.Errorf("Alias was already registered %s for command %s", alias, info.Name) continue } alias = strings.ToLower(alias) - commandAliases[alias] = info.Trigger - } - // Add the command to the map; command triggers are case-insensitive - commands[strings.ToLower(info.Trigger)] = command -} - -// AddChildCommand -// Adds a child command to the bot. -func AddChildCommand(info *CommandInfo, function BotFunction) { - // Build a Command object for this command - command := Command{ - Info: *info, - Function: function, - } - parentID := strings.ToLower(info.ParentID) - if childCommands[parentID] == nil { - childCommands[parentID] = make(map[string]Command) + commandAliases[alias] = info.Name } // Add the command to the map; command triggers are case-insensitive - childCommands[parentID][command.Info.Trigger] = command + commands[strings.ToLower(info.Name)] = &command } -// AddSlashCommand -// Adds a slash command to the bot -// Allows for separation between normal commands and slash commands -func AddSlashCommand(info *CommandInfo) { - if !info.IsParent || !info.IsChild { - s := createSlashCommandStruct(info) - slashCommands[strings.ToLower(info.Trigger)] = *s - return - } - if info.IsParent { - s := createSlashSubCmdStruct(info, childCommands[info.Trigger]) - slashCommands[strings.ToLower(info.Trigger)] = *s +// AddCommandHandler +// Adds a command handler to the bot +func AddCommandHandler(info *CommandInfo, function BotFunction, handler string) { + if _, ok := commands[strings.ToLower(info.Name)]; !ok { + log.Errorf("Command was not found") return } + commands[strings.ToLower(info.Name)].Handlers[handler] = function } +// // AddChildCommand +// // Adds a child command to the bot. +// func AddChildCommand(info *CommandInfo, function BotFunction) { +// // Build a Command object for this command +// command := Command{ +// Info: *info, +// Handlers: make(map[string]BotFunction), +// } +// command.Handlers["default"] = function +// parentID := strings.ToLower(info.ParentID) + +// // Add the command to the map; command triggers are case-insensitive +// commands[fmt.Sprintf("%s:%s", strings.ToLower(parentID), strings.ToLower(info.Name))] = command +// } + +// // AddSlashCommand +// // Adds a slash command to the bot +// // Allows for separation between normal commands and slash commands +// func AddSlashCommand(info *CommandInfo) { +// if !info.IsParent || !info.IsChild { +// s := createSlashCommandStruct(info) +// slashCommands[strings.ToLower(info.Trigger)] = *s +// return +// } +// if info.IsParent { +// s := createSlashSubCmdStruct(info, childCommands[info.Trigger]) +// slashCommands[strings.ToLower(info.Trigger)] = *s +// return +// } +// } + // AddSlashCommands // Defaults to adding Global slash commands // Currently hard coded to guild commands for testing func AddSlashCommands(guildId string, c chan string) { - for _, v := range slashCommands { - _, err := Session.ApplicationCommandCreate(Session.State.User.ID, guildId, &v) + for _, v := range commands { + log.Debugf("Adding slash command %s", v.ApplicationCommand.Name) + _, err := Session.ApplicationCommandCreate(Session.State.User.ID, guildId, v.ApplicationCommand) if err != nil { c <- "Unable to register slash commands :/" - log.Errorf("Cannot create '%v' command: %v", v.Name, err) - log.Errorf("%v", v.Options) + log.Errorf("Cannot create '%v' command: %v", v.Info, err) + log.Errorf("%v", v.ApplicationCommand) return } } c <- "Finished registering slash commands" - return } // GetCommands @@ -162,7 +366,7 @@ func AddSlashCommands(guildId string, c chan string) { func GetCommands() map[string]CommandInfo { list := make(map[string]CommandInfo) for x, y := range commands { - list[x] = y.Info + list[x] = *y.Info } return list } @@ -236,12 +440,12 @@ func commandHandler(session *discordgo.Session, message *discordgo.MessageCreate defer handleCommandError(g.ID, channel.ID, message.Author.ID) if command.Info.IsParent { - handleChildCommand(*argString, command, message.Message, g) + // handleChildCommand(*argString, command, message.Message, g) return } - command.Function(&Context{ + command.Handlers["default"](&Context{ Guild: g, - Cmd: command.Info, + Cmd: *command.Info, Args: *ParseArguments(*argString, command.Info.Arguments), Message: message.Message, }) @@ -257,37 +461,37 @@ func commandHandler(session *discordgo.Session, message *discordgo.MessageCreate } -// -- Helper Methods -func handleChildCommand(argString string, command Command, message *discordgo.Message, g *Guild) { - split := strings.SplitN(argString, " ", 2) - - childCmd, ok := childCommands[command.Info.Trigger][split[0]] - if !ok { - command.Function(&Context{ - Guild: g, - Cmd: command.Info, - Args: nil, - Message: message, - }) - return - } - if len(split) < 2 { - childCmd.Function(&Context{ - Guild: g, - Cmd: childCmd.Info, - Args: *ParseArguments("", childCmd.Info.Arguments), - Message: message, - }) - return - } - childCmd.Function(&Context{ - Guild: g, - Cmd: childCmd.Info, - Args: *ParseArguments(split[1], childCmd.Info.Arguments), - Message: message, - }) - return -} +// // -- Helper Methods +// func handleChildCommand(argString string, command Command, message *discordgo.Message, g *Guild) { +// split := strings.SplitN(argString, " ", 2) + +// childCmd, ok := childCommands[command.Info.Trigger][split[0]] +// if !ok { +// command.Function(&Context{ +// Guild: g, +// Cmd: command.Info, +// Args: nil, +// Message: message, +// }) +// return +// } +// if len(split) < 2 { +// childCmd.Function(&Context{ +// Guild: g, +// Cmd: childCmd.Info, +// Args: *ParseArguments("", childCmd.Info.Arguments), +// Message: message, +// }) +// return +// } +// childCmd.Function(&Context{ +// Guild: g, +// Cmd: childCmd.Info, +// Args: *ParseArguments(split[1], childCmd.Info.Arguments), +// Message: message, +// }) +// return +// } func handleCommandError(gID string, cId string, uId string) { if r := recover(); r != nil { diff --git a/go.mod b/go.mod index 6c8580d..cb5fc56 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,17 @@ module github.com/qpixel/framework -go 1.18 +go 1.21 require ( github.com/QPixel/orderedmap v0.2.0 - github.com/bwmarrin/discordgo v0.26.1 - github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 + github.com/bwmarrin/discordgo v0.27.1 + github.com/dlclark/regexp2 v1.11.0 github.com/ubergeek77/tinylog v1.0.0 - golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 + golang.org/x/sys v0.18.0 ) require ( - github.com/gorilla/websocket v1.4.2 // indirect - golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect + github.com/gorilla/websocket v1.5.1 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 7421b8f..7ee01a0 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,23 @@ github.com/QPixel/orderedmap v0.2.0 h1:qGTSj7i1YP7dhhUmOZ5/p2OX3NGHtJW/FLT2eDCHJak= github.com/QPixel/orderedmap v0.2.0/go.mod h1:4cAVROPCVsOwbmwg3hDwZcfiAqYVr3FsOHAVy9ntErE= -github.com/bwmarrin/discordgo v0.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE= -github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= -github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E= -github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= +github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/ubergeek77/tinylog v1.0.0 h1:gsq98mbig3LDWhsizOe2tid12wHUz/mrkDlmgJ0MZG4= github.com/ubergeek77/tinylog v1.0.0/go.mod h1:NzUi4PkRG2hACL4cGgmW7db6EaKjAeqrqlVQnJdw78Q= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/guilds.go b/guilds.go index 100fda6..4271749 100644 --- a/guilds.go +++ b/guilds.go @@ -28,7 +28,7 @@ type GuildInfo struct { WhitelistIds []string `json:"whitelist_ids"` } -//GuildProvider +// GuildProvider // Type that holds functions that can be easily modified to support a wide range // of storage types type GuildProvider struct { @@ -950,10 +950,10 @@ func (g *Guild) GetMap(key string) (map[string]interface{}, error) { } // GetCommandUsage -//// Compile the usage information for a single command, so it can be printed out +// // Compile the usage information for a single command, so it can be printed out func (g *Guild) GetCommandUsage(cmd CommandInfo) string { // Get the trigger for the command, and add the prefix to it - trigger := g.Info.Prefix + cmd.Trigger + trigger := g.Info.Prefix + cmd.Name // If there are no usage examples, we only need to print the trigger, wrapped in code formatting if len(cmd.Arguments.Keys()) == 0 { diff --git a/handlers.go b/handlers.go index 5a3e4e4..c62e1dd 100644 --- a/handlers.go +++ b/handlers.go @@ -3,7 +3,7 @@ package framework // handlers.go // Everything required for commands to pass their own handlers to discordgo and the framework itself. -// handlers +// dGOhandlers // This list stores all the handlers that can be added to the bot // It's basically a passthroughs for discordgo.AddHandler, but having a list // allows them to be collected ahead of time and then added all at once diff --git a/interaction.go b/interaction.go index ca98d94..bc3baac 100644 --- a/interaction.go +++ b/interaction.go @@ -10,35 +10,33 @@ import ( // slashCommandTypes // A map of *short hand* slash commands types to their discordgo counterparts -// TODO move this over to interaction.go var slashCommandTypes = map[ArgTypeGuards]discordgo.ApplicationCommandOptionType{ - Int: discordgo.ApplicationCommandOptionInteger, - String: discordgo.ApplicationCommandOptionString, - Channel: discordgo.ApplicationCommandOptionChannel, - User: discordgo.ApplicationCommandOptionUser, - Role: discordgo.ApplicationCommandOptionRole, - Boolean: discordgo.ApplicationCommandOptionBoolean, - //SubCmd: discordgo.ApplicationCommandOptionSubCommand, - //SubCmdGrp: discordgo.ApplicationCommandOptionSubCommandGroup, + Int: discordgo.ApplicationCommandOptionInteger, + String: discordgo.ApplicationCommandOptionString, + Channel: discordgo.ApplicationCommandOptionChannel, + User: discordgo.ApplicationCommandOptionUser, + Role: discordgo.ApplicationCommandOptionRole, + Boolean: discordgo.ApplicationCommandOptionBoolean, + SubCmd: discordgo.ApplicationCommandOptionSubCommand, + SubCmdGrp: discordgo.ApplicationCommandOptionSubCommandGroup, } var genericError = "error executing command" -// getSlashCommandStruct -// Creates a slash command struct -// todo work on sub command stuff -func createSlashCommandStruct(info *CommandInfo) (st *discordgo.ApplicationCommand) { +func createApplicationChatCommand(info *CommandInfo) (st *discordgo.ApplicationCommand) { if info.Arguments == nil || len(info.Arguments.Keys()) < 1 { st = &discordgo.ApplicationCommand{ - Name: info.Trigger, + Name: info.Name, Description: info.Description, + Type: discordgo.ChatApplicationCommand, } return } st = &discordgo.ApplicationCommand{ - Name: info.Trigger, + Name: info.Name, Description: info.Description, Options: make([]*discordgo.ApplicationCommandOption, len(info.Arguments.Keys())), + Type: discordgo.ChatApplicationCommand, } for i, k := range info.Arguments.Keys() { v, _ := info.Arguments.Get(k) @@ -50,47 +48,23 @@ func createSlashCommandStruct(info *CommandInfo) (st *discordgo.ApplicationComma sType = slashCommandTypes["String"] } optionStruct := discordgo.ApplicationCommandOption{ - Type: sType, - Name: k, - Description: vv.Description, - Required: vv.Required, + Type: sType, + Name: k, + Description: vv.Description, + Required: vv.Required, + Autocomplete: vv.AutoComplete, } - if vv.Choices != nil { - optionStruct.Choices = make([]*discordgo.ApplicationCommandOptionChoice, len(vv.Choices)) - for i, k := range vv.Choices { - optionStruct.Choices[i] = &discordgo.ApplicationCommandOptionChoice{ - Name: k, - Value: k, - } - } + if len(vv.Choices) > 0 { + optionStruct.Choices = vv.Choices } st.Options[i] = &optionStruct } return } -// Creates a slash subcmd struct -func createSlashSubCmdStruct(info *CommandInfo, childCmds map[string]Command) (st *discordgo.ApplicationCommand) { - st = &discordgo.ApplicationCommand{ - Name: info.Trigger, - Description: info.Description, - Options: make([]*discordgo.ApplicationCommandOption, len(childCmds)), - } - currentPos := 0 - for _, v := range childCmds { - // Stupid inline thing - if ar, _ := v.Info.Arguments.Get(v.Info.Arguments.Keys()[0]); ar.(*ArgInfo).TypeGuard == SubCmdGrp { +// func createApplicationContextCommand(info *CommandInfo, context_type discordgo.ApplicationCommandType) (st *discordgo.ApplicationCommand) { - } else { - //Pixel: - //Yes I know this is O(N^2). Most likely I could get something better - //todo: refactor so this isn't as bad for performance - st.Options[currentPos] = v.Info.CreateAppOptSt() - currentPos++ - } - } - return st -} +// } // -- Interaction Handlers -- @@ -100,11 +74,11 @@ func handleInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { switch i.Type { case discordgo.InteractionApplicationCommand: handleInteractionCommand(s, i) - break case discordgo.InteractionMessageComponent: handleMessageComponents(s, i) + case discordgo.InteractionApplicationCommandAutocomplete: + handleAutoComplete(s, i) } - return } // handleInteractionCommand @@ -143,9 +117,9 @@ func handleInteractionCommand(s *discordgo.Session, i *discordgo.InteractionCrea // Bot admins supercede both checks defer handleSlashCommandError(*i.Interaction) - command.Function(&Context{ + command.Handlers["default"](&Context{ Guild: g, - Cmd: command.Info, + Cmd: *command.Info, Args: *ParseInteractionArgs(i.ApplicationCommandData().Options), Interaction: i.Interaction, Message: &discordgo.Message{ @@ -173,7 +147,11 @@ func handleMessageComponents(s *discordgo.Session, i *discordgo.InteractionCreat Embeds: i.Message.Embeds, }, }) - return +} + +func handleAutoComplete(s *discordgo.Session, i *discordgo.InteractionCreate) { + // Currently only supports autocomplete for the first option + // id = i.ApplicationCommandData().Options[0].Name } // -- Slash Argument Parsing Helpers -- From 822597d89d3e728b76e6a31d54902ab45314e7d9 Mon Sep 17 00:00:00 2001 From: Riley Smith Date: Sat, 23 Mar 2024 01:47:44 -0700 Subject: [PATCH 02/10] more work on handler impl --- commands.go | 38 ++++++++++++++- go.mod | 4 +- go.sum | 18 +++++++ interaction.go | 80 ++++++++++++++++++++++++------- response.go | 125 ++++++++++++++++++++++++++++++++++++++++++++++--- util.go | 15 +++++- 6 files changed, 252 insertions(+), 28 deletions(-) diff --git a/commands.go b/commands.go index fc130be..262f901 100644 --- a/commands.go +++ b/commands.go @@ -73,6 +73,9 @@ var commands = make(map[string]*Command) // A map of aliases to command triggers var commandAliases = make(map[string]string) +// component handlers +var componentHandlers = make(map[string]BotFunction) + // commandsGC var commandsGC = 0 @@ -313,6 +316,26 @@ func AddCommandHandler(info *CommandInfo, function BotFunction, handler string) commands[strings.ToLower(info.Name)].Handlers[handler] = function } +// AddAutoCompleteHandler +// Adds an autocomplete handler to the bot +func AddAutoCompleteHandler(info *CommandInfo, function BotFunction, handler string) { + if _, ok := commands[strings.ToLower(info.Name)]; !ok { + log.Errorf("Command was not found") + return + } + commands[info.Name].Handlers[fmt.Sprintf("ac:%s", strings.ToLower(handler))] = function +} + +// AddComponentHandler +// Adds a component handler to the bot +func AddComponentHandler(handler string, function BotFunction) { + if _, ok := componentHandlers[handler]; ok { + log.Errorf("Component handler was already registered %s", handler) + return + } + componentHandlers[handler] = function +} + // // AddChildCommand // // Adds a child command to the bot. // func AddChildCommand(info *CommandInfo, function BotFunction) { @@ -349,7 +372,6 @@ func AddCommandHandler(info *CommandInfo, function BotFunction, handler string) // Currently hard coded to guild commands for testing func AddSlashCommands(guildId string, c chan string) { for _, v := range commands { - log.Debugf("Adding slash command %s", v.ApplicationCommand.Name) _, err := Session.ApplicationCommandCreate(Session.State.User.ID, guildId, v.ApplicationCommand) if err != nil { c <- "Unable to register slash commands :/" @@ -371,6 +393,20 @@ func GetCommands() map[string]CommandInfo { return list } +// SendAutocompleteChoices +// Sends the choices to the user +func (ctx *Context) SendAutocompleteChoices(choices []*discordgo.ApplicationCommandOptionChoice) { + err := Session.InteractionRespond(ctx.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionApplicationCommandAutocompleteResult, + Data: &discordgo.InteractionResponseData{ + Choices: choices, + }, + }) + if err != nil { + log.Errorf("Error sending autocomplete choices %s", err) + } +} + // commandHandler // This handler will be added to a *discordgo.Session, and will scan an incoming messages for commands to run func commandHandler(session *discordgo.Session, message *discordgo.MessageCreate) { diff --git a/go.mod b/go.mod index cb5fc56..a67cce8 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,16 @@ go 1.21 require ( github.com/QPixel/orderedmap v0.2.0 - github.com/bwmarrin/discordgo v0.27.1 + github.com/bwmarrin/discordgo v0.27.2-0.20240315152229-33ee38cbf271 github.com/dlclark/regexp2 v1.11.0 github.com/ubergeek77/tinylog v1.0.0 + gitlab.com/tozd/go/errors v0.8.1 golang.org/x/sys v0.18.0 ) require ( github.com/gorilla/websocket v1.5.1 // indirect + github.com/pkg/errors v0.9.1 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/net v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 7ee01a0..7407260 100644 --- a/go.sum +++ b/go.sum @@ -2,13 +2,28 @@ github.com/QPixel/orderedmap v0.2.0 h1:qGTSj7i1YP7dhhUmOZ5/p2OX3NGHtJW/FLT2eDCHJ github.com/QPixel/orderedmap v0.2.0/go.mod h1:4cAVROPCVsOwbmwg3hDwZcfiAqYVr3FsOHAVy9ntErE= github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/bwmarrin/discordgo v0.27.2-0.20240315152229-33ee38cbf271 h1:BuDtVy29wfi7XFRYUP2c65EJuMWWNCWhkiP+jYIU4eY= +github.com/bwmarrin/discordgo v0.27.2-0.20240315152229-33ee38cbf271/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/ubergeek77/tinylog v1.0.0 h1:gsq98mbig3LDWhsizOe2tid12wHUz/mrkDlmgJ0MZG4= github.com/ubergeek77/tinylog v1.0.0/go.mod h1:NzUi4PkRG2hACL4cGgmW7db6EaKjAeqrqlVQnJdw78Q= +gitlab.com/tozd/go/errors v0.8.1 h1:RfylffRAsl3PbDdHNUBEkTleTCiL/RIT+Ef8p0HRNCI= +gitlab.com/tozd/go/errors v0.8.1/go.mod h1:PvIdUMLpPwxr+KEBxghQaCMydHXGYdJQn/PhdMqYREY= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= @@ -21,3 +36,6 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/interaction.go b/interaction.go index bc3baac..7cac320 100644 --- a/interaction.go +++ b/interaction.go @@ -1,9 +1,12 @@ package framework import ( + "fmt" "runtime" + "strings" "github.com/bwmarrin/discordgo" + errors "gitlab.com/tozd/go/errors" ) // -- Types and Structs -- @@ -77,7 +80,7 @@ func handleInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { case discordgo.InteractionMessageComponent: handleMessageComponents(s, i) case discordgo.InteractionApplicationCommandAutocomplete: - handleAutoComplete(s, i) + handleAutoComplete(i) } } @@ -135,23 +138,57 @@ func handleInteractionCommand(s *discordgo.Session, i *discordgo.InteractionCrea } func handleMessageComponents(s *discordgo.Session, i *discordgo.InteractionCreate) { - content := "Currently testing customid " + i.MessageComponentData().CustomID - i.Message.Embeds[0].Description = content - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - // Buttons also may update the message which they was attached to. - // Or may just acknowledge (InteractionResponseDeferredMessageUpdate) that the event was received and not update the message. - // To update it later you need to use interaction response edit endpoint. - Type: discordgo.InteractionResponseUpdateMessage, - Data: &discordgo.InteractionResponseData{ - TTS: false, - Embeds: i.Message.Embeds, + componentName := i.MessageComponentData().CustomID + if _, ok := componentHandlers[componentName]; !ok { + log.Errorf("No component found for %s", componentName) + return + } + + defer handleSlashCommandError(*i.Interaction) + componentHandlers[componentName](&Context{ + Guild: getGuild(i.GuildID), + Cmd: CommandInfo{}, + Args: map[string]CommandArg{}, + Interaction: i.Interaction, + Message: &discordgo.Message{ + Member: i.Member, + Author: i.Member.User, + ChannelID: i.ChannelID, + GuildID: i.GuildID, + Content: "", }, }) } -func handleAutoComplete(s *discordgo.Session, i *discordgo.InteractionCreate) { - // Currently only supports autocomplete for the first option - // id = i.ApplicationCommandData().Options[0].Name +func handleAutoComplete(i *discordgo.InteractionCreate) { + commandName := i.ApplicationCommandData().Name + for _, option := range i.ApplicationCommandData().Options { + if option.Focused { + command := commands[strings.ToLower(commandName)] + if command == nil { + log.Errorf("No command found for autocomplete %s", commandName) + return + } + + // All AutoComplete handlers are prefixed with "ac:" + handler := command.Handlers[fmt.Sprintf("ac:%s", strings.ToLower(option.Name))] + + if handler == nil { + log.Errorf("No handler found for autocomplete %s", commandName) + return + } + + defer handleAutoCompleteError(*i.Interaction, "Error executing autocomplete") + + handler(&Context{ + Guild: getGuild(i.GuildID), + Cmd: *command.Info, + Args: *ParseInteractionArgs(i.ApplicationCommandData().Options), + Interaction: i.Interaction, + }) + } + } + } // -- Slash Argument Parsing Helpers -- @@ -207,9 +244,10 @@ func RemoveGuildSlashCommands(guildID string) { func handleSlashCommandError(i discordgo.Interaction) { if r := recover(); r != nil { - log.Warningf("Recovering from panic: %s", r) + e := errors.WithStack(r.(error)) + log.Warningf("Recovering from panic: %s", e) log.Warningf("Sending Error report to admins") - SendErrorReport(i.GuildID, i.ChannelID, i.Member.User.ID, "Error!", r.(runtime.Error)) + SendErrorReport(i.GuildID, i.ChannelID, i.Member.User.ID, "Error!", e) message, err := Session.InteractionResponseEdit(&i, &discordgo.WebhookEdit{ Content: &genericError, }) @@ -224,7 +262,13 @@ func handleSlashCommandError(i discordgo.Interaction) { log.Errorf("err sending message %s", err) } Session.ChannelMessageDelete(i.ChannelID, message.ID) - return } - return +} + +func handleAutoCompleteError(i discordgo.Interaction, message string) { + if r := recover(); r != nil { + log.Warningf("Recovering from panic: %s", r) + log.Warningf("Sending Error report to admins") + SendErrorReport(i.GuildID, i.ChannelID, i.Member.User.ID, "Error!", r.(runtime.Error)) + } } diff --git a/response.go b/response.go index 941afb6..a78b6a1 100644 --- a/response.go +++ b/response.go @@ -13,8 +13,8 @@ import ( // Stores the components for response // allows for functions to add data type ResponseComponents struct { - Components []discordgo.MessageComponent - SelectMenuOptions []discordgo.SelectMenuOption + Components []*discordgo.MessageComponent + SelectMenuOptions []*discordgo.SelectMenuOption } // Response @@ -58,6 +58,39 @@ func CreateComponentFields() []discordgo.MessageComponent { } } +func (c *ResponseComponents) FindButton(customID string) (*discordgo.Button, bool) { + for _, row := range c.Components { + for _, component := range row.(*discordgo.ActionsRow).Components { + if component.(discordgo.Button).CustomID == customID { + return component.(*discordgo.Button), true + } + } + } + return nil, false +} + +func (c *ResponseComponents) FindDropDown(customID string) (*discordgo.SelectMenu, bool) { + for _, row := range c.Components { + for _, component := range row.(*discordgo.ActionsRow).Components { + if component.(discordgo.SelectMenu).CustomID == customID { + return component.(*discordgo.SelectMenu), true + } + } + } + return nil, false +} + +func (c *ResponseComponents) ReplaceButton(customID string, button discordgo.Button) { + for i, row := range c.Components { + for j, component := range row.(discordgo.ActionsRow).Components { + if component.(discordgo.Button).CustomID == customID { + c.Components[i].(discordgo.ActionsRow).Components[j] = button + return + } + } + } +} + // NewResponse // Create a response object for a guild, which starts off as an empty Embed which will have fields added to it // The response starts with some "auditing" information @@ -97,6 +130,35 @@ func NewResponse(ctx *Context, messageComponents bool, ephemeral bool) *Response return r } +// ReconstructResponse +// Reconstruct a response object from a given context. Only for interactions +func ReconstructResponse(ctx *Context) *Response { + if ctx.Interaction == nil { + log.Errorf("Tried to reconstruct response from context without interaction") + return nil + } + if ctx.Interaction.Message == nil { + log.Errorf("Tried to reconstruct response from context without interaction message") + return nil + } + if len(ctx.Interaction.Message.Embeds) == 0 { + log.Errorf("Tried to reconstruct response from context without embeds") + return nil + } + log.Debugf("Reconstructing response from context %#v", ctx.Interaction.Message.Embeds) + r := &Response{ + Ctx: ctx, + Embed: ctx.Interaction.Message.Embeds[0], + ResponseComponents: &ResponseComponents{ + Components: ctx.Interaction.Message.Components, + }, + Loading: ctx.Cmd.IsTyping, + Ephemeral: ctx.Interaction.Message.Flags == 1<<6, + Reply: false, + } + return r +} + // -- Fields -- // AppendField @@ -131,7 +193,7 @@ func CreateButton(label string, style discordgo.ButtonStyle, customID string, ur Label: label, Style: style, Disabled: disabled, - Emoji: discordgo.ComponentEmoji{}, + Emoji: nil, URL: url, CustomID: customID, } @@ -150,12 +212,15 @@ func CreateDropDown(customID string, placeholder string, options []discordgo.Sel // AppendButton // Appends a button func (r *Response) AppendButton(label string, style discordgo.ButtonStyle, url string, customID string, rowID int) { + if r.ResponseComponents.Components == nil { + r.ResponseComponents.Components = CreateComponentFields() + } row := r.ResponseComponents.Components[rowID].(discordgo.ActionsRow) row.Components = append(row.Components, CreateButton(label, style, customID, url, false)) r.ResponseComponents.Components[rowID] = row } -//AppendDropDown +// AppendDropDown // Adds a DropDown component func (r *Response) AppendDropDown(customID string, placeholder string, noNewRow bool) { if noNewRow { @@ -258,11 +323,10 @@ func (r *Response) Send(success bool, title string, description string) { }) // Just in case the interaction gets removed. if err != nil { + log.Errorf("Error sending interaction response: %s", err) _, err := Session.ChannelMessageSendEmbed(r.Ctx.Guild.Info.ResponseChannelId, r.Embed) if err != nil { - _, err = Session.ChannelMessageSendEmbed(r.Ctx.Message.ChannelID, r.Embed) - if err != nil { - } + _, _ = Session.ChannelMessageSendEmbed(r.Ctx.Message.ChannelID, r.Embed) } } } @@ -345,6 +409,53 @@ func (r *Response) Send(success bool, title string, description string) { } } +// -- Response Editing -- + +// EditButtonDisabled +// Edit a button to be disabled +func (r *Response) EditButtonDisabled(buttonID string) { + r.EditButtonComplex(buttonID, "", 0, "", true) +} + +// EditButtonComplex +// Edit a button +func (r *Response) EditButtonComplex(buttonID string, label string, style discordgo.ButtonStyle, url string, disabled bool) { + button, ok := r.ResponseComponents.FindButton(buttonID) + if !ok { + log.Errorf("Could not find button with ID %s", buttonID) + return + } + + if label != "" { + button.Label = label + } + if style != 0 { + button.Style = style + } + if url != "" { + button.URL = url + } + + if disabled { + button.Disabled = true + } + r.ResponseComponents.ReplaceButton(buttonID, *button) +} + +// Edit +// Edit a response +func (r *Response) Edit() { + _, err := Session.ChannelMessageEditComplex(&discordgo.MessageEdit{ + Channel: r.Ctx.Message.ChannelID, + ID: r.Ctx.Message.ID, + Embed: r.Embed, + Components: &r.ResponseComponents.Components, + }) + if err != nil { + SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Message.ChannelID, r.Ctx.Message.Author.ID, "Failed to edit message", err) + } +} + func ErrorResponse(i *discordgo.Interaction, errorMsg string, trigger string) { var errorEmbed = CreateEmbed(0xff3232, "Error", errorMsg, []*discordgo.MessageEmbedField{ { diff --git a/util.go b/util.go index 6be4658..31f9e33 100644 --- a/util.go +++ b/util.go @@ -348,6 +348,19 @@ func createDisplayDurationString(content string) (str string) { return } +// CreateOptionChoiceFromSlice +// Given a slice of strings, create a slice of ApplicationCommandOptionChoice objects +func CreateOptionChoiceFromSlice(slice []string) []*discordgo.ApplicationCommandOptionChoice { + choices := make([]*discordgo.ApplicationCommandOptionChoice, 0) + for _, v := range slice { + choices = append(choices, &discordgo.ApplicationCommandOptionChoice{ + Name: v, + Value: v, + }) + } + return choices +} + func FindAllString(re *regexp2.Regexp, s string) []string { var matches []string m, _ := re.FindStringMatch(s) @@ -385,4 +398,4 @@ func dgoLog(msgL, caller int, format string, a ...interface{}) { // quick func to turn anything into a pointer func ToPtr[T any](v T) *T { return &v -} \ No newline at end of file +} From 71984c67d0d8b49cbbd847d01381d4b87f9efdca Mon Sep 17 00:00:00 2001 From: Riley Smith Date: Sat, 23 Mar 2024 17:56:31 -0700 Subject: [PATCH 03/10] More fixes to button code --- core.go | 19 ++++++- interaction.go | 2 +- response.go | 146 ++++++++++++++++++++++++++++++++++++------------- 3 files changed, 126 insertions(+), 41 deletions(-) diff --git a/core.go b/core.go index 7fceb8e..4a030df 100644 --- a/core.go +++ b/core.go @@ -1,13 +1,14 @@ package framework import ( - "github.com/bwmarrin/discordgo" - tlog "github.com/ubergeek77/tinylog" "os" "os/signal" "strconv" "strings" "syscall" + + "github.com/bwmarrin/discordgo" + tlog "github.com/ubergeek77/tinylog" ) // core.go @@ -61,6 +62,10 @@ var botPresence discordgo.GatewayStatusUpdate // Stores and allows for the calling of the chosen GuildProvider var initProvider func() GuildProvider +// debugMode +// A boolean that tells the bot to log debug messages +var debugMode = false + // SetInitProvider // Sets the init provider func SetInitProvider(provider func() GuildProvider) { @@ -109,6 +114,12 @@ func IsCommand(trigger string) bool { return false } +// SetDebugMode +// Set the log level to debug +func SetDebugMode() { + debugMode = true +} + // Start the bot. func Start() { discordgo.Logger = dgoLog @@ -132,6 +143,10 @@ func Start() { if err != nil { log.Fatalf("Failed to create Discord session: %s", err) } + if debugMode { + Session.LogLevel = discordgo.LogDebug + Session.Debug = true + } // Setup State specific variables Session.State.MaxMessageCount = MessageState Session.LogLevel = discordgo.LogWarning diff --git a/interaction.go b/interaction.go index 7cac320..e3a4607 100644 --- a/interaction.go +++ b/interaction.go @@ -144,7 +144,7 @@ func handleMessageComponents(s *discordgo.Session, i *discordgo.InteractionCreat return } - defer handleSlashCommandError(*i.Interaction) + // defer handleSlashCommandError(*i.Interaction) componentHandlers[componentName](&Context{ Guild: getGuild(i.GuildID), Cmd: CommandInfo{}, diff --git a/response.go b/response.go index a78b6a1..df8a7f9 100644 --- a/response.go +++ b/response.go @@ -1,6 +1,7 @@ package framework import ( + "reflect" "time" "github.com/bwmarrin/discordgo" @@ -13,8 +14,8 @@ import ( // Stores the components for response // allows for functions to add data type ResponseComponents struct { - Components []*discordgo.MessageComponent - SelectMenuOptions []*discordgo.SelectMenuOption + Components []discordgo.ActionsRow + SelectMenuOptions []discordgo.SelectMenuOption } // Response @@ -50,19 +51,23 @@ func CreateEmbed(color int, title string, description string, fields []*discordg } } -// CreateComponentFields -// Returns a slice of a Message Component, containing a singular ActionsRow -func CreateComponentFields() []discordgo.MessageComponent { - return []discordgo.MessageComponent{ - discordgo.ActionsRow{}, - } -} - func (c *ResponseComponents) FindButton(customID string) (*discordgo.Button, bool) { + log.Debugf("%#v", c.Components) for _, row := range c.Components { - for _, component := range row.(*discordgo.ActionsRow).Components { - if component.(discordgo.Button).CustomID == customID { - return component.(*discordgo.Button), true + for _, component := range row.Components { + log.Debugf("%#v", component) + if component.Type() == discordgo.ButtonComponent { + ctype := reflect.TypeOf(component).Kind() + if ctype == reflect.Ptr { + if component.(*discordgo.Button).CustomID == customID { + return component.(*discordgo.Button), true + } + } else { + if component.(discordgo.Button).CustomID == customID { + return component.(*discordgo.Button), true + } + } + } } } @@ -71,7 +76,7 @@ func (c *ResponseComponents) FindButton(customID string) (*discordgo.Button, boo func (c *ResponseComponents) FindDropDown(customID string) (*discordgo.SelectMenu, bool) { for _, row := range c.Components { - for _, component := range row.(*discordgo.ActionsRow).Components { + for _, component := range row.Components { if component.(discordgo.SelectMenu).CustomID == customID { return component.(*discordgo.SelectMenu), true } @@ -80,12 +85,25 @@ func (c *ResponseComponents) FindDropDown(customID string) (*discordgo.SelectMen return nil, false } +func (c *ResponseComponents) SetButton(customID string, button discordgo.Button, row ...int) { + if len(row) == 0 { + row = append(row, 0) + } + c.Components[row[0]].Components = append(c.Components[row[0]].Components, &button) +} + func (c *ResponseComponents) ReplaceButton(customID string, button discordgo.Button) { for i, row := range c.Components { - for j, component := range row.(discordgo.ActionsRow).Components { - if component.(discordgo.Button).CustomID == customID { - c.Components[i].(discordgo.ActionsRow).Components[j] = button - return + for j, component := range row.Components { + ctype := reflect.TypeOf(component).Kind() + if ctype == reflect.Ptr { + if component.(*discordgo.Button).CustomID == customID { + c.Components[i].Components[j] = &button + } + } else { + if component.(discordgo.Button).CustomID == customID { + c.Components[i].Components[j] = &button + } } } } @@ -108,7 +126,7 @@ func NewResponse(ctx *Context, messageComponents bool, ephemeral bool) *Response Reply: ephemeral, } if messageComponents { - r.ResponseComponents.Components = CreateComponentFields() + r.ResponseComponents.Components = MakeActionRow() r.ResponseComponents.SelectMenuOptions = []discordgo.SelectMenuOption{} } if r.Loading && ctx.Interaction != nil { @@ -145,12 +163,12 @@ func ReconstructResponse(ctx *Context) *Response { log.Errorf("Tried to reconstruct response from context without embeds") return nil } - log.Debugf("Reconstructing response from context %#v", ctx.Interaction.Message.Embeds) + log.Debugf("Reconstructing response from context %#v", ctx.Interaction) r := &Response{ Ctx: ctx, Embed: ctx.Interaction.Message.Embeds[0], ResponseComponents: &ResponseComponents{ - Components: ctx.Interaction.Message.Components, + Components: ConvertMessageComponent(ctx.Interaction.Message.Components), }, Loading: ctx.Cmd.IsTyping, Ephemeral: ctx.Interaction.Message.Flags == 1<<6, @@ -159,6 +177,50 @@ func ReconstructResponse(ctx *Context) *Response { return r } +// ConvertComponent +// Properly Type Asserts a MessageComponent to any of the possible types, and returns it +// func ConvertComponent[K discordgo.MessageComponent](component discordgo.MessageComponent) (K, bool) {} + +// ConvertMessageComponent +// Converts the components on the message struct to an array of ActionsRow +func ConvertMessageComponent(components []discordgo.MessageComponent) []discordgo.ActionsRow { + var rows []discordgo.ActionsRow + log.Debugf("Converting message components: %#v", components) + for _, component := range components { + if row, ok := component.(discordgo.ActionsRow); ok { + rows = append(rows, row) + } else if row, ok := component.(*discordgo.ActionsRow); ok { + rows = append(rows, *row) + } + } + return rows +} + +// MakeActionRow +// Returns a slice of a Message Component, containing a singular ActionsRow +func MakeActionRow() []discordgo.ActionsRow { + return make([]discordgo.ActionsRow, 1) +} + +// SerializeActionRow +// Converts a slice of ActionsRow to a slice of Message Components +func SerializeActionRow(row []discordgo.ActionsRow) *[]discordgo.MessageComponent { + var components []discordgo.MessageComponent + for _, r := range row { + components = append(components, r) + } + return &components +} + +// ConvertToMessageComponent +// Converts a slice of Message Components to a slice of Message Components +func ConvertToMessageComponent[T []discordgo.MessageComponent](component T) *[]discordgo.MessageComponent { + if c, ok := (any(component).([]discordgo.MessageComponent)); ok { + return &c + } + return nil +} + // -- Fields -- // AppendField @@ -211,20 +273,22 @@ func CreateDropDown(customID string, placeholder string, options []discordgo.Sel // AppendButton // Appends a button -func (r *Response) AppendButton(label string, style discordgo.ButtonStyle, url string, customID string, rowID int) { +func (r *Response) AppendButton(label string, style discordgo.ButtonStyle, url string, customID string, rowID ...int) { + if len(rowID) == 0 { + rowID = append(rowID, 0) + } if r.ResponseComponents.Components == nil { - r.ResponseComponents.Components = CreateComponentFields() + r.ResponseComponents.Components = MakeActionRow() } - row := r.ResponseComponents.Components[rowID].(discordgo.ActionsRow) - row.Components = append(row.Components, CreateButton(label, style, customID, url, false)) - r.ResponseComponents.Components[rowID] = row + button := CreateButton(label, style, customID, url, false) + r.ResponseComponents.SetButton(customID, *button, rowID...) } // AppendDropDown // Adds a DropDown component func (r *Response) AppendDropDown(customID string, placeholder string, noNewRow bool) { if noNewRow { - row := r.ResponseComponents.Components[0].(discordgo.ActionsRow) + row := r.ResponseComponents.Components[0] row.Components = append(row.Components, CreateDropDown(customID, placeholder, r.ResponseComponents.SelectMenuOptions)) r.ResponseComponents.Components[0] = row } else { @@ -271,7 +335,7 @@ func (r *Response) Send(success bool, title string, description string) { } _, dmSendErr := Session.ChannelMessageSendComplex(dmChannel.ID, &discordgo.MessageSend{ Embed: r.Embed, - Components: r.ResponseComponents.Components, + Components: *SerializeActionRow(r.ResponseComponents.Components), }) if dmSendErr != nil { // Since error reports also use DMs, sending this as an error report would be redundant @@ -291,8 +355,10 @@ func (r *Response) Send(success bool, title string, description string) { if r.Loading { // Check to see if the command is ephemeral (only shown to the user) if r.Ephemeral { + components := SerializeActionRow(r.ResponseComponents.Components) + log.Debugf("Sending interaction response with components: %#v", components) _, err := Session.InteractionResponseEdit(r.Ctx.Interaction, &discordgo.WebhookEdit{ - Components: &r.ResponseComponents.Components, + Components: components, Embeds: &[]*discordgo.MessageEmbed{ r.Embed, }, @@ -314,12 +380,14 @@ func (r *Response) Send(success bool, title string, description string) { } } } else { + components := SerializeActionRow(r.ResponseComponents.Components) + log.Debugf("Sending interaction response with components: %#v", components) _, err := Session.InteractionResponseEdit(r.Ctx.Interaction, &discordgo.WebhookEdit{ Content: ToPtr[string](""), Embeds: &[]*discordgo.MessageEmbed{ r.Embed, }, - Components: &r.ResponseComponents.Components, + Components: components, }) // Just in case the interaction gets removed. if err != nil { @@ -343,7 +411,7 @@ func (r *Response) Send(success bool, title string, description string) { Embeds: []*discordgo.MessageEmbed{ r.Embed, }, - Components: r.ResponseComponents.Components, + Components: *SerializeActionRow(r.ResponseComponents.Components), }, }) return @@ -356,7 +424,7 @@ func (r *Response) Send(success bool, title string, description string) { Embeds: []*discordgo.MessageEmbed{ r.Embed, }, - Components: r.ResponseComponents.Components, + Components: *SerializeActionRow(r.ResponseComponents.Components), }, }) if err != nil { @@ -381,13 +449,13 @@ func (r *Response) Send(success bool, title string, description string) { // If THAT fails, send an error report _, err := Session.ChannelMessageSendComplex(r.Ctx.Guild.Info.ResponseChannelId, &discordgo.MessageSend{ Embed: r.Embed, - Components: r.ResponseComponents.Components, + Components: *SerializeActionRow(r.ResponseComponents.Components), }) if err != nil && r.Reply { // Reply to user if no output channel _, err = ReplyToUser(r.Ctx.Message.ChannelID, &discordgo.MessageSend{ Embed: r.Embed, - Components: r.ResponseComponents.Components, + Components: *SerializeActionRow(r.ResponseComponents.Components), Reference: &discordgo.MessageReference{ MessageID: r.Ctx.Message.ID, ChannelID: r.Ctx.Message.ChannelID, @@ -404,7 +472,7 @@ func (r *Response) Send(success bool, title string, description string) { // If the command does not want to reply lets just send it to the channel the command was invoked _, err = Session.ChannelMessageSendComplex(r.Ctx.Message.ChannelID, &discordgo.MessageSend{ Embed: r.Embed, - Components: r.ResponseComponents.Components, + Components: *SerializeActionRow(r.ResponseComponents.Components), }) } } @@ -445,11 +513,13 @@ func (r *Response) EditButtonComplex(buttonID string, label string, style discor // Edit // Edit a response func (r *Response) Edit() { + component := SerializeActionRow(r.ResponseComponents.Components) + log.Debugf("Editing response with components: %#v", component) _, err := Session.ChannelMessageEditComplex(&discordgo.MessageEdit{ - Channel: r.Ctx.Message.ChannelID, - ID: r.Ctx.Message.ID, + Channel: r.Ctx.Interaction.Message.ChannelID, + ID: r.Ctx.Interaction.Message.ID, Embed: r.Embed, - Components: &r.ResponseComponents.Components, + Components: component, }) if err != nil { SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Message.ChannelID, r.Ctx.Message.Author.ID, "Failed to edit message", err) From 93a13497184531723ef47bd9d87a9693dcacdf65 Mon Sep 17 00:00:00 2001 From: Riley Smith Date: Sat, 23 Mar 2024 21:19:56 -0700 Subject: [PATCH 04/10] move to discordgo fork --- .editorconfig | 12 +++++ commands.go | 41 ++++++++++----- core.go | 7 ++- go.mod | 2 + guilds.go | 17 ++----- interaction.go | 132 +++++++++++++++++++++++++++++++------------------ response.go | 19 ++++--- util.go | 9 ---- 8 files changed, 143 insertions(+), 96 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f03c4b4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[{Makefile,go.mod,go.sum,*.go,.gitmodules}] +indent_style = tab +indent_size = 4 \ No newline at end of file diff --git a/commands.go b/commands.go index 262f901..b900874 100644 --- a/commands.go +++ b/commands.go @@ -27,16 +27,18 @@ var ( // CommandInfo // The definition of a command's info. This is everything about the command, besides the function it will run type CommandInfo struct { - Aliases []string // Aliases for the normal trigger - Arguments *orderedmap.OrderedMap // Arguments for the command - Description string // A short description of what the command does - Group Group // The group this command belongs to - ParentID string // The ID of the parent command - Public bool // Whether non-admins and non-mods can use this command - IsTyping bool // Whether the command will show a typing thing when ran. - IsParent bool // If the command is the parent of a subcommand tree - IsChild bool // If the command is the child - Name string // The name of the command + Aliases []string // Aliases for the normal trigger + Arguments *orderedmap.OrderedMap // Arguments for the command + Description string // A short description of what the command does + Group Group // The group this command belongs to + ParentID string // The ID of the parent command + Public bool // Whether non-admins and non-mods can use this command + IsTyping bool // Whether the command will show a typing thing when ran. + IsParent bool // If the command is the parent of a subcommand tree + IsChild bool // If the command is the child + Name string // The name of the command + IntegrationTypes []discordgo.ApplicationIntegrationType + InstallationContexts []discordgo.InteractionContextType } // Context @@ -94,18 +96,25 @@ func CreateCommandInfo(name string, description string, public bool, group Group Name: name, IsParent: true, IsChild: false, + IntegrationTypes: []discordgo.ApplicationIntegrationType{ + discordgo.ApplicationIntegrationGuildInstall, + }, + InstallationContexts: []discordgo.InteractionContextType{ + discordgo.InteractionContextGuild, + }, } cI.Aliases = append(cI.Aliases, name) return cI } // Sets the parent properties -func (cI *CommandInfo) SetParent(isParent bool, parentID string) { +func (cI *CommandInfo) SetParent(isParent bool, parentID string) *CommandInfo { if !isParent { cI.IsChild = true } cI.IsParent = isParent cI.ParentID = parentID + return cI } // AddCmdAlias @@ -236,6 +245,16 @@ func (cI *CommandInfo) SetAutocomplete(arg string, autocomplete bool) *CommandIn return cI } +func (cI *CommandInfo) SetIntegrationType(integrationType ...discordgo.ApplicationIntegrationType) *CommandInfo { + cI.IntegrationTypes = integrationType + return cI +} + +func (cI *CommandInfo) SetInstallationContext(installationContext ...discordgo.InteractionContextType) *CommandInfo { + cI.InstallationContexts = installationContext + return cI +} + // -- Argument Parser -- // ParseArguments diff --git a/core.go b/core.go index 4a030df..1a90dcb 100644 --- a/core.go +++ b/core.go @@ -70,14 +70,12 @@ var debugMode = false // Sets the init provider func SetInitProvider(provider func() GuildProvider) { initProvider = provider - return } // SetPresence // Sets the gateway field for bot presence func SetPresence(presence discordgo.GatewayStatusUpdate) { botPresence = presence - return } // AddAdmin @@ -208,7 +206,7 @@ func Start() { // Set up a sigterm channel, so we can detect when the application receives a TERM signal sigChannel := make(chan os.Signal, 1) - signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL, os.Interrupt, os.Kill) + signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) // Keep this thread blocked forever, until a TERM signal is received <-sigChannel @@ -220,7 +218,7 @@ func Start() { // Make a second sig channel that will respond to user term signal immediately sigInstant := make(chan os.Signal, 1) - signal.Notify(sigInstant, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + signal.Notify(sigInstant, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) // Make a goroutine that will wait for all background workers to be unlocked go func() { @@ -230,6 +228,7 @@ func Start() { // If we are able to lock it, then it means the worker has stopped. lock.Lock() log.Info("Stopped worker " + strconv.Itoa(i)) + lock.Unlock() } diff --git a/go.mod b/go.mod index a67cce8..d901d13 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/qpixel/framework go 1.21 +replace github.com/bwmarrin/discordgo => ../discordgo + require ( github.com/QPixel/orderedmap v0.2.0 github.com/bwmarrin/discordgo v0.27.2-0.20240315152229-33ee38cbf271 diff --git a/guilds.go b/guilds.go index 4271749..3ee0866 100644 --- a/guilds.go +++ b/guilds.go @@ -141,10 +141,7 @@ func (g *Guild) GetMember(userId string) (*discordgo.Member, error) { // Determine whether or not a given userId is a member in this guild func (g *Guild) IsMember(userId string) bool { _, err := g.GetMember(userId) - if err != nil { - return false - } - return true + return err == nil } // GetRole @@ -175,10 +172,7 @@ func (g *Guild) GetRole(roleId string) (*discordgo.Role, error) { // Determine whether or not a given roleId is a valid role in this guild func (g *Guild) IsRole(roleId string) bool { _, err := g.GetRole(roleId) - if err != nil { - return false - } - return true + return err == nil } // HasRole @@ -230,10 +224,7 @@ func (g *Guild) GetChannel(channelId string) (*discordgo.Channel, error) { // Determine whether or not a given channelId is a valid channel in this guild func (g *Guild) IsChannel(channelId string) bool { _, err := g.GetChannel(channelId) - if err != nil { - return false - } - return true + return err == nil } // MemberOrRoleInList @@ -609,7 +600,7 @@ func (g *Guild) RemoveChannelFromIgnored(channelId string) error { // Check if a given command is globally disabled func (g *Guild) IsGloballyDisabled(trigger string) bool { for _, disabled := range g.Info.GlobalDisabledCommands { - if strings.ToLower(disabled) == strings.ToLower(trigger) { + if strings.EqualFold(disabled, trigger) { return true } } diff --git a/interaction.go b/interaction.go index e3a4607..e592859 100644 --- a/interaction.go +++ b/interaction.go @@ -29,17 +29,21 @@ var genericError = "error executing command" func createApplicationChatCommand(info *CommandInfo) (st *discordgo.ApplicationCommand) { if info.Arguments == nil || len(info.Arguments.Keys()) < 1 { st = &discordgo.ApplicationCommand{ - Name: info.Name, - Description: info.Description, - Type: discordgo.ChatApplicationCommand, + Name: info.Name, + Description: info.Description, + Type: discordgo.ChatApplicationCommand, + IntegrationTypes: &info.IntegrationTypes, + Contexts: &info.InstallationContexts, } return } st = &discordgo.ApplicationCommand{ - Name: info.Name, - Description: info.Description, - Options: make([]*discordgo.ApplicationCommandOption, len(info.Arguments.Keys())), - Type: discordgo.ChatApplicationCommand, + Name: info.Name, + Description: info.Description, + Options: make([]*discordgo.ApplicationCommandOption, len(info.Arguments.Keys())), + Type: discordgo.ChatApplicationCommand, + IntegrationTypes: &info.IntegrationTypes, + Contexts: &info.InstallationContexts, } for i, k := range info.Arguments.Keys() { v, _ := info.Arguments.Get(k) @@ -87,54 +91,84 @@ func handleInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { // handleInteractionCommand // Handles a slash command func handleInteractionCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { + // Let's check if this is a user command, if so lets handle it separately + if i.Interaction.Member == nil && i.Interaction.GuildID == "" { + handleUserApplicationCommand(s, i) + return + } + g := getGuild(i.GuildID) trigger := i.ApplicationCommandData().Name - if !IsAdmin(i.Member.User.ID) { - // Ignore the command if it is globally disabled - if g.IsGloballyDisabled(trigger) { - ErrorResponse(i.Interaction, "Command is globally disabled", trigger) - return - } - - // Ignore the command if this channel has blocked the command - if g.CommandIsDisabledInChannel(trigger, i.ChannelID) { - ErrorResponse(i.Interaction, "Command is disabled in this channel!", trigger) - return - } - - // Ignore any message if the user is banned from using the bot - if !g.MemberOrRoleIsWhitelisted(i.Member.User.ID) || g.MemberOrRoleIsIgnored(i.Member.User.ID) { - return - } + log.Debugf("Handling command %s", trigger) + // if !IsAdmin(i.Member.User.ID) { + // // Ignore the command if it is globally disabled + // if g.IsGloballyDisabled(trigger) { + // ErrorResponse(i.Interaction, "Command is globally disabled", trigger) + // return + // } + + // // Ignore the command if this channel has blocked the command + // if g.CommandIsDisabledInChannel(trigger, i.ChannelID) { + // ErrorResponse(i.Interaction, "Command is disabled in this channel!", trigger) + // return + // } + + // // Ignore any message if the user is banned from using the bot + // if !g.MemberOrRoleIsWhitelisted(i.Member.User.ID) || g.MemberOrRoleIsIgnored(i.Member.User.ID) { + // return + // } + + // // Ignore the message if this channel is not whitelisted, or if it is ignored + // if !g.ChannelIsWhitelisted(i.ChannelID) || g.ChannelIsIgnored(i.ChannelID) { + // return + // } + // } - // Ignore the message if this channel is not whitelisted, or if it is ignored - if !g.ChannelIsWhitelisted(i.ChannelID) || g.ChannelIsIgnored(i.ChannelID) { - return - } - } + command := commands[trigger] + log.Debugf("Command %s found %#v", trigger, command) + // if IsAdmin(i.Member.User.ID) || command.Info.Public || g.IsMod(i.Member.User.ID) { + // Check if the command is public, or if the current user is a bot moderator + // Bot admins supercede both checks + // } + log.Debugf("%#v", i.Interaction) + defer handleSlashCommandError(*i.Interaction) + command.Handlers["default"](&Context{ + Guild: g, + Cmd: *command.Info, + Args: *ParseInteractionArgs(i.ApplicationCommandData().Options), + Interaction: i.Interaction, + Message: &discordgo.Message{ + Member: i.Member, + Author: i.Member.User, + ChannelID: i.ChannelID, + GuildID: i.GuildID, + Content: "", + }, + }) +} +func handleUserApplicationCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { + trigger := i.ApplicationCommandData().Name + log.Debugf("Handling user command %s", trigger) command := commands[trigger] - if IsAdmin(i.Member.User.ID) || command.Info.Public || g.IsMod(i.Member.User.ID) { - // Check if the command is public, or if the current user is a bot moderator - // Bot admins supercede both checks - - defer handleSlashCommandError(*i.Interaction) - command.Handlers["default"](&Context{ - Guild: g, - Cmd: *command.Info, - Args: *ParseInteractionArgs(i.ApplicationCommandData().Options), - Interaction: i.Interaction, - Message: &discordgo.Message{ - Member: i.Member, - Author: i.Member.User, - ChannelID: i.ChannelID, - GuildID: i.GuildID, - Content: "", + log.Debugf("Command %s found %#v", trigger, command) + defer handleSlashCommandError(*i.Interaction) + command.Handlers["default"](&Context{ + Guild: nil, + Cmd: *command.Info, + Args: *ParseInteractionArgs(i.ApplicationCommandData().Options), + Interaction: i.Interaction, + Message: &discordgo.Message{ + Member: &discordgo.Member{ + User: i.User, }, - }) - return - } + Author: i.User, + ChannelID: i.ChannelID, + Content: "", + }, + }) + } func handleMessageComponents(s *discordgo.Session, i *discordgo.InteractionCreate) { @@ -144,7 +178,7 @@ func handleMessageComponents(s *discordgo.Session, i *discordgo.InteractionCreat return } - // defer handleSlashCommandError(*i.Interaction) + defer handleSlashCommandError(*i.Interaction) componentHandlers[componentName](&Context{ Guild: getGuild(i.GuildID), Cmd: CommandInfo{}, diff --git a/response.go b/response.go index df8a7f9..54a3ebe 100644 --- a/response.go +++ b/response.go @@ -225,27 +225,28 @@ func ConvertToMessageComponent[T []discordgo.MessageComponent](component T) *[]d // AppendField // Create a new basic field and append it to an existing Response -func (r *Response) AppendField(name string, value string, inline bool) { +func (r *Response) AppendField(name string, value string, inline bool) *Response { r.Embed.Fields = append(r.Embed.Fields, CreateField(name, value, inline)) + return r } // PrependField // Create a new basic field and prepend it to an existing Response -func (r *Response) PrependField(name string, value string, inline bool) { +func (r *Response) PrependField(name string, value string, inline bool) *Response { fields := []*discordgo.MessageEmbedField{CreateField(name, value, inline)} r.Embed.Fields = append(fields, r.Embed.Fields...) + return r } // AppendUsage // Add the command usage to the response. Intended for syntax error responses -func (r *Response) AppendUsage() { +func (r *Response) AppendUsage() *Response { if r.Ctx.Cmd.Description == "" { r.AppendField("Command description:", "no description", false) - return + return r } r.AppendField("Command description:", r.Ctx.Cmd.Description, false) - //r.AppendField("Command usage:", r.Ctx.Guild.GetCommandUsage(r.Ctx.Cmd), false) - + return r } // -- Message Components -- @@ -324,7 +325,7 @@ func (r *Response) Send(success bool, title string, description string) { r.Embed.Color = color // If guild is nil, this is intended to be sent to Bot Admins - if r.Ctx.Guild == nil { + if r.Ctx.Guild == nil && r.Ctx.Interaction == nil { for admin := range botAdmins { dmChannel, dmCreateErr := Session.UserChannelCreate(admin) if dmCreateErr != nil { @@ -428,9 +429,7 @@ func (r *Response) Send(success bool, title string, description string) { }, }) if err != nil { - if err != nil { - SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Interaction.ChannelID, r.Ctx.Message.Author.ID, "Unable to send interaction messages", err) - } + SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Interaction.ChannelID, r.Ctx.Message.Author.ID, "Unable to send interaction messages", err) if r.Ctx.Guild.Info.ResponseChannelId != "" { _, err = Session.ChannelMessageSendEmbed(r.Ctx.Guild.Info.ResponseChannelId, r.Embed) diff --git a/util.go b/util.go index 31f9e33..f9feaae 100644 --- a/util.go +++ b/util.go @@ -305,44 +305,35 @@ func createDisplayDurationString(content string) (str string) { break } str += prefixChar + "Second" - break case "m": if multiplier > 1 { str += prefixChar + fmt.Sprintf("%d Minutes", multiplier) - break } str += prefixChar + fmt.Sprintf("%d Minute", multiplier) - break case "h": if multiplier > 1 { str += prefixChar + fmt.Sprintf("%d Hours", multiplier) break } str += prefixChar + fmt.Sprintf("%d Hours", multiplier) - break case "d": if multiplier > 1 { str += prefixChar + fmt.Sprintf("%d Days", multiplier) break } str += prefixChar + fmt.Sprintf("%d Day", multiplier) - break case "w": if multiplier > 1 { str += prefixChar + fmt.Sprintf("%d Weeks", multiplier) break } str += prefixChar + fmt.Sprintf("%d Week", multiplier) - break case "y": if multiplier > 1 { str += prefixChar + fmt.Sprintf("%d Years", multiplier) break } str += prefixChar + fmt.Sprintf("%d Year", multiplier) - break - default: - break } } return From 477b64fa529da8016da6069332c7a427ed780869 Mon Sep 17 00:00:00 2001 From: Riley Smith Date: Sat, 23 Mar 2024 22:43:29 -0700 Subject: [PATCH 05/10] add context menu command support --- commands.go | 117 ++++++++++++++++++++----------------------------- core.go | 2 +- interaction.go | 105 +++++++++++++++++++++++++++++++++++++------- response.go | 16 ++++++- 4 files changed, 154 insertions(+), 86 deletions(-) diff --git a/commands.go b/commands.go index b900874..f3c77ad 100644 --- a/commands.go +++ b/commands.go @@ -20,13 +20,24 @@ import ( type Group string var ( - Moderation Group = "moderation" - Utility Group = "utility" + Moderation Group = "moderation" + Utility Group = "utility" + UserContext Group = "context" + MessageContext Group = "message" +) + +type CommandType string + +var ( + ChatCommand CommandType = "CHAT" + UserCommand CommandType = "USER" + MessageCommand CommandType = "MESSAGE" ) // CommandInfo // The definition of a command's info. This is everything about the command, besides the function it will run type CommandInfo struct { + Type CommandType // The type of command Aliases []string // Aliases for the normal trigger Arguments *orderedmap.OrderedMap // Arguments for the command Description string // A short description of what the command does @@ -85,8 +96,12 @@ var commandsGC = 0 // CreateCommandInfo // Creates a pointer to a CommandInfo -func CreateCommandInfo(name string, description string, public bool, group Group) *CommandInfo { +func CreateCommandInfo(name string, description string, public bool, group Group, command_type ...CommandType) *CommandInfo { + if len(command_type) < 1 { + command_type = append(command_type, ChatCommand) + } cI := &CommandInfo{ + Type: command_type[0], Aliases: make([]string, 0), Arguments: orderedmap.New(), Description: description, @@ -302,6 +317,17 @@ func ParseArguments(args string, infoArgs *orderedmap.OrderedMap) *Arguments { // AddCommand // Add a command to the bot func AddCommand(info *CommandInfo, function BotFunction) { + switch info.Type { + case ChatCommand: + AddChatCommand(info, function) + case UserCommand, MessageCommand: + AddContextCommand(info, function) + } +} + +// AddChatCommand +// Add a chat command to the bot +func AddChatCommand(info *CommandInfo, function BotFunction) { // Build a Command object for this command appCommand := createApplicationChatCommand(info) command := Command{ @@ -325,6 +351,23 @@ func AddCommand(info *CommandInfo, function BotFunction) { commands[strings.ToLower(info.Name)] = &command } +// AddContextCommand +// Add a context command to the bot +func AddContextCommand(info *CommandInfo, function BotFunction) { + appCommand := createApplicationContextCommand(info) + // Build a Command object for this command + command := Command{ + Info: info, + Handlers: make(map[string]BotFunction), + ApplicationCommand: appCommand, + } + + command.Handlers["default"] = function + + // Add the command to the map; command triggers are case-insensitive + commands[strings.ToLower(info.Name)] = &command +} + // AddCommandHandler // Adds a command handler to the bot func AddCommandHandler(info *CommandInfo, function BotFunction, handler string) { @@ -355,41 +398,10 @@ func AddComponentHandler(handler string, function BotFunction) { componentHandlers[handler] = function } -// // AddChildCommand -// // Adds a child command to the bot. -// func AddChildCommand(info *CommandInfo, function BotFunction) { -// // Build a Command object for this command -// command := Command{ -// Info: *info, -// Handlers: make(map[string]BotFunction), -// } -// command.Handlers["default"] = function -// parentID := strings.ToLower(info.ParentID) - -// // Add the command to the map; command triggers are case-insensitive -// commands[fmt.Sprintf("%s:%s", strings.ToLower(parentID), strings.ToLower(info.Name))] = command -// } - -// // AddSlashCommand -// // Adds a slash command to the bot -// // Allows for separation between normal commands and slash commands -// func AddSlashCommand(info *CommandInfo) { -// if !info.IsParent || !info.IsChild { -// s := createSlashCommandStruct(info) -// slashCommands[strings.ToLower(info.Trigger)] = *s -// return -// } -// if info.IsParent { -// s := createSlashSubCmdStruct(info, childCommands[info.Trigger]) -// slashCommands[strings.ToLower(info.Trigger)] = *s -// return -// } -// } - -// AddSlashCommands +// RegisterSlashCommands // Defaults to adding Global slash commands // Currently hard coded to guild commands for testing -func AddSlashCommands(guildId string, c chan string) { +func RegisterSlashCommands(guildId string, c chan string) { for _, v := range commands { _, err := Session.ApplicationCommandCreate(Session.State.User.ID, guildId, v.ApplicationCommand) if err != nil { @@ -516,38 +528,6 @@ func commandHandler(session *discordgo.Session, message *discordgo.MessageCreate } -// // -- Helper Methods -// func handleChildCommand(argString string, command Command, message *discordgo.Message, g *Guild) { -// split := strings.SplitN(argString, " ", 2) - -// childCmd, ok := childCommands[command.Info.Trigger][split[0]] -// if !ok { -// command.Function(&Context{ -// Guild: g, -// Cmd: command.Info, -// Args: nil, -// Message: message, -// }) -// return -// } -// if len(split) < 2 { -// childCmd.Function(&Context{ -// Guild: g, -// Cmd: childCmd.Info, -// Args: *ParseArguments("", childCmd.Info.Arguments), -// Message: message, -// }) -// return -// } -// childCmd.Function(&Context{ -// Guild: g, -// Cmd: childCmd.Info, -// Args: *ParseArguments(split[1], childCmd.Info.Arguments), -// Message: message, -// }) -// return -// } - func handleCommandError(gID string, cId string, uId string) { if r := recover(); r != nil { log.Warningf("Recovering from panic: %s", r) @@ -561,5 +541,4 @@ func handleCommandError(gID string, cId string, uId string) { _ = Session.ChannelMessageDelete(cId, message.ID) return } - return } diff --git a/core.go b/core.go index 1a90dcb..3159e3b 100644 --- a/core.go +++ b/core.go @@ -194,7 +194,7 @@ func Start() { //Register slash commands slashChannel := make(chan string) log.Info("Registering slash commands") - go AddSlashCommands(botTestingId, slashChannel) + go RegisterSlashCommands(botTestingId, slashChannel) // Bot ready log.Info("Initialization complete! The bot is now ready.") diff --git a/interaction.go b/interaction.go index e592859..455b88a 100644 --- a/interaction.go +++ b/interaction.go @@ -69,9 +69,23 @@ func createApplicationChatCommand(info *CommandInfo) (st *discordgo.ApplicationC return } -// func createApplicationContextCommand(info *CommandInfo, context_type discordgo.ApplicationCommandType) (st *discordgo.ApplicationCommand) { +func createApplicationContextCommand(info *CommandInfo) (st *discordgo.ApplicationCommand) { + var context_type discordgo.ApplicationCommandType + switch info.Type { + case UserCommand: + context_type = discordgo.UserApplicationCommand + case MessageCommand: + context_type = discordgo.MessageApplicationCommand + } -// } + st = &discordgo.ApplicationCommand{ + Name: info.Name, + Type: context_type, + IntegrationTypes: &info.IntegrationTypes, + Contexts: &info.InstallationContexts, + } + return +} // -- Interaction Handlers -- @@ -80,7 +94,7 @@ func createApplicationChatCommand(info *CommandInfo) (st *discordgo.ApplicationC func handleInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { switch i.Type { case discordgo.InteractionApplicationCommand: - handleInteractionCommand(s, i) + handleApplicationCommand(s, i) case discordgo.InteractionMessageComponent: handleMessageComponents(s, i) case discordgo.InteractionApplicationCommandAutocomplete: @@ -88,19 +102,16 @@ func handleInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { } } -// handleInteractionCommand -// Handles a slash command -func handleInteractionCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { - // Let's check if this is a user command, if so lets handle it separately - if i.Interaction.Member == nil && i.Interaction.GuildID == "" { - handleUserApplicationCommand(s, i) - return +// handleApplicationCommand +// Handles a ApplicationCommand +func handleApplicationCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { + switch i.ApplicationCommandData().CommandType { + case discordgo.ChatApplicationCommand: + handleChatApplicationCommand(s, i) + case discordgo.UserApplicationCommand, discordgo.MessageApplicationCommand: + handleApplicationContextCommand(i) } - g := getGuild(i.GuildID) - - trigger := i.ApplicationCommandData().Name - log.Debugf("Handling command %s", trigger) // if !IsAdmin(i.Member.User.ID) { // // Ignore the command if it is globally disabled // if g.IsGloballyDisabled(trigger) { @@ -125,6 +136,21 @@ func handleInteractionCommand(s *discordgo.Session, i *discordgo.InteractionCrea // } // } +} + +// handleChatApplicationCommand +// Handles a slash command +func handleChatApplicationCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { + // Let's check if this is a user command, if so lets handle it separately + if i.Interaction.Member == nil && i.Interaction.GuildID == "" { + handleUserApplicationChatCommand(s, i) + return + } + + g := getGuild(i.GuildID) + + trigger := i.ApplicationCommandData().Name + log.Debugf("Handling command %s", trigger) command := commands[trigger] log.Debugf("Command %s found %#v", trigger, command) // if IsAdmin(i.Member.User.ID) || command.Info.Public || g.IsMod(i.Member.User.ID) { @@ -148,7 +174,56 @@ func handleInteractionCommand(s *discordgo.Session, i *discordgo.InteractionCrea }) } -func handleUserApplicationCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { +// handleApplicationContextCommand +// Handles a context menu command +func handleApplicationContextCommand(i *discordgo.InteractionCreate) { + switch i.ApplicationCommandData().CommandType { + case discordgo.UserApplicationCommand: + handleUserContextCommand(i) + case discordgo.MessageApplicationCommand: + handleMessageContextCommand(i) + } +} + +// handleUserContextCommand +// Handles a user context command +func handleUserContextCommand(i *discordgo.InteractionCreate) { + trigger := i.ApplicationCommandData().Name + log.Debugf("Handling command %s", trigger) + command := commands[trigger] + log.Debugf("Command %s found %#v", trigger, command) + defer handleSlashCommandError(*i.Interaction) + command.Handlers["default"](&Context{ + Guild: getGuild(i.GuildID), + Cmd: *command.Info, + Interaction: i.Interaction, + Message: &discordgo.Message{ + Author: i.User, + ChannelID: i.ChannelID, + Content: "", + }, + }) +} + +// handleMessageContextCommand +// Handles a message context command +func handleMessageContextCommand(i *discordgo.InteractionCreate) { + trigger := i.ApplicationCommandData().Name + log.Debugf("Handling command %s", trigger) + command := commands[trigger] + log.Debugf("Command %s found %#v", trigger, command) + defer handleSlashCommandError(*i.Interaction) + command.Handlers["default"](&Context{ + Guild: getGuild(i.GuildID), + Cmd: *command.Info, + Interaction: i.Interaction, + Message: i.Message, + }) +} + +// handleUserApplicationChatCommand +// Handles a user application slash command +func handleUserApplicationChatCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { trigger := i.ApplicationCommandData().Name log.Debugf("Handling user command %s", trigger) command := commands[trigger] diff --git a/response.go b/response.go index 54a3ebe..97741ed 100644 --- a/response.go +++ b/response.go @@ -28,6 +28,7 @@ type Response struct { Reply bool Embed *discordgo.MessageEmbed ResponseComponents *ResponseComponents + Content string } // CreateField @@ -306,6 +307,13 @@ func (r *Response) AppendDropDown(customID string, placeholder string, noNewRow } } +// PrependContent +// Prepend content to the response +func (r *Response) PrependContent(content string) *Response { + r.Content = content + r.Content + return r +} + // Send // Send a compiled response func (r *Response) Send(success bool, title string, description string) { @@ -363,6 +371,7 @@ func (r *Response) Send(success bool, title string, description string) { Embeds: &[]*discordgo.MessageEmbed{ r.Embed, }, + Content: ToPtr[string](r.Content), }) // Just in case the interaction gets removed. if err != nil { @@ -384,7 +393,7 @@ func (r *Response) Send(success bool, title string, description string) { components := SerializeActionRow(r.ResponseComponents.Components) log.Debugf("Sending interaction response with components: %#v", components) _, err := Session.InteractionResponseEdit(r.Ctx.Interaction, &discordgo.WebhookEdit{ - Content: ToPtr[string](""), + Content: ToPtr[string](r.Content), Embeds: &[]*discordgo.MessageEmbed{ r.Embed, }, @@ -413,6 +422,7 @@ func (r *Response) Send(success bool, title string, description string) { r.Embed, }, Components: *SerializeActionRow(r.ResponseComponents.Components), + Content: r.Content, }, }) return @@ -426,6 +436,7 @@ func (r *Response) Send(success bool, title string, description string) { r.Embed, }, Components: *SerializeActionRow(r.ResponseComponents.Components), + Content: r.Content, }, }) if err != nil { @@ -449,6 +460,7 @@ func (r *Response) Send(success bool, title string, description string) { _, err := Session.ChannelMessageSendComplex(r.Ctx.Guild.Info.ResponseChannelId, &discordgo.MessageSend{ Embed: r.Embed, Components: *SerializeActionRow(r.ResponseComponents.Components), + Content: r.Content, }) if err != nil && r.Reply { // Reply to user if no output channel @@ -463,6 +475,7 @@ func (r *Response) Send(success bool, title string, description string) { AllowedMentions: &discordgo.MessageAllowedMentions{ Parse: []discordgo.AllowedMentionType{}, }, + Content: r.Content, }) if err != nil { SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Message.ChannelID, r.Ctx.Message.Author.ID, "Ultimately failed to send bot response", err) @@ -472,6 +485,7 @@ func (r *Response) Send(success bool, title string, description string) { _, err = Session.ChannelMessageSendComplex(r.Ctx.Message.ChannelID, &discordgo.MessageSend{ Embed: r.Embed, Components: *SerializeActionRow(r.ResponseComponents.Components), + Content: r.Content, }) } } From 592f46b2c8720014b403d5371c657001c11f02cf Mon Sep 17 00:00:00 2001 From: Riley Smith Date: Sun, 24 Mar 2024 23:29:34 -0700 Subject: [PATCH 06/10] added support for log stuff, fixed some crashes --- commands.go | 4 + interaction.go | 46 +++++++--- response.go | 221 ++++++++++++++++++++++++++----------------------- 3 files changed, 159 insertions(+), 112 deletions(-) diff --git a/commands.go b/commands.go index f3c77ad..3ea3745 100644 --- a/commands.go +++ b/commands.go @@ -10,6 +10,7 @@ import ( "github.com/QPixel/orderedmap" "github.com/bwmarrin/discordgo" "github.com/dlclark/regexp2" + "github.com/ubergeek77/tinylog" ) // commands.go @@ -61,8 +62,11 @@ type Context struct { Args Arguments Message *discordgo.Message Interaction *discordgo.Interaction + Log *tinylog.Logger } +var botContextLoggerColor = tinylog.NewColor("38;5;13") + // BotFunction // This type defines the functions that are called when commands are triggered // Contexts are also passed as pointers, so they are not re-allocated when passed through diff --git a/interaction.go b/interaction.go index 455b88a..2254d03 100644 --- a/interaction.go +++ b/interaction.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/bwmarrin/discordgo" + "github.com/ubergeek77/tinylog" errors "gitlab.com/tozd/go/errors" ) @@ -171,6 +172,7 @@ func handleChatApplicationCommand(s *discordgo.Session, i *discordgo.Interaction GuildID: i.GuildID, Content: "", }, + Log: tinylog.NewTaggedLogger(fmt.Sprintf("Module: %s", command.Info.Name), tinylog.NewColor("38;5;111")), }) } @@ -202,22 +204,27 @@ func handleUserContextCommand(i *discordgo.InteractionCreate) { ChannelID: i.ChannelID, Content: "", }, + Log: tinylog.NewTaggedLogger(fmt.Sprintf("Module: %s", command.Info.Name), botContextLoggerColor), }) } // handleMessageContextCommand // Handles a message context command func handleMessageContextCommand(i *discordgo.InteractionCreate) { - trigger := i.ApplicationCommandData().Name + commandData := i.ApplicationCommandData() + trigger := commandData.Name + message := commandData.Resolved.Messages[commandData.TargetID] + log.Debugf("Handling command %s", trigger) command := commands[trigger] log.Debugf("Command %s found %#v", trigger, command) - defer handleSlashCommandError(*i.Interaction) + // defer handleSlashCommandError(*i.Interaction) command.Handlers["default"](&Context{ Guild: getGuild(i.GuildID), Cmd: *command.Info, Interaction: i.Interaction, - Message: i.Message, + Message: message, + Log: tinylog.NewTaggedLogger(fmt.Sprintf("Module: %s", command.Info.Name), botContextLoggerColor), }) } @@ -242,6 +249,7 @@ func handleUserApplicationChatCommand(s *discordgo.Session, i *discordgo.Interac ChannelID: i.ChannelID, Content: "", }, + Log: tinylog.NewTaggedLogger(fmt.Sprintf("Module: %s", command.Info.Name), botContextLoggerColor), }) } @@ -252,6 +260,28 @@ func handleMessageComponents(s *discordgo.Session, i *discordgo.InteractionCreat log.Errorf("No component found for %s", componentName) return } + message := &discordgo.Message{ + Content: "", + } + + if i.Message != nil { + message = i.Message + } + if i.Member != nil { + message.Member = i.Member + } + if i.GuildID != "" { + message.GuildID = i.GuildID + } + if i.ChannelID != "" { + message.ChannelID = i.ChannelID + } + if i.User != nil { + message.Author = i.User + } + if i.Member != nil { + message.Member = i.Member + } defer handleSlashCommandError(*i.Interaction) componentHandlers[componentName](&Context{ @@ -259,13 +289,8 @@ func handleMessageComponents(s *discordgo.Session, i *discordgo.InteractionCreat Cmd: CommandInfo{}, Args: map[string]CommandArg{}, Interaction: i.Interaction, - Message: &discordgo.Message{ - Member: i.Member, - Author: i.Member.User, - ChannelID: i.ChannelID, - GuildID: i.GuildID, - Content: "", - }, + Message: message, + Log: tinylog.NewTaggedLogger(fmt.Sprintf("Component: %s", componentName), botContextLoggerColor), }) } @@ -294,6 +319,7 @@ func handleAutoComplete(i *discordgo.InteractionCreate) { Cmd: *command.Info, Args: *ParseInteractionArgs(i.ApplicationCommandData().Options), Interaction: i.Interaction, + Log: tinylog.NewTaggedLogger(fmt.Sprintf("Handler: %s", commandName), botContextLoggerColor), }) } } diff --git a/response.go b/response.go index 97741ed..87ac45c 100644 --- a/response.go +++ b/response.go @@ -114,10 +114,17 @@ func (c *ResponseComponents) ReplaceButton(customID string, button discordgo.But // Create a response object for a guild, which starts off as an empty Embed which will have fields added to it // The response starts with some "auditing" information // The embed will be finalized in .Send() -func NewResponse(ctx *Context, messageComponents bool, ephemeral bool) *Response { +func NewResponse(ctx *Context, messageComponents bool, ephemeral bool, use_embed ...bool) *Response { + var embed *discordgo.MessageEmbed + if len(use_embed) == 0 { + use_embed = append(use_embed, true) + } + if use_embed[0] { + embed = CreateEmbed(0, "", "", nil) + } r := &Response{ Ctx: ctx, - Embed: CreateEmbed(0, "", "", nil), + Embed: embed, ResponseComponents: &ResponseComponents{ Components: nil, SelectMenuOptions: nil, @@ -316,21 +323,38 @@ func (r *Response) PrependContent(content string) *Response { // Send // Send a compiled response -func (r *Response) Send(success bool, title string, description string) { +func (r *Response) Send(success bool, title, description string) { + r.SendComplex(SendComplex{ + Success: success, + Title: title, + Description: description, + }) +} + +type SendComplex struct { + Success bool + Title string + Description string +} + +// SendComplex +// SendComplex a compiled response +func (r *Response) SendComplex(options SendComplex) { // Determine what color to use based on the success state var color int - if success { + if options.Success { color = ColorSuccess } else { // On failure, also append the command usage r.AppendUsage() color = ColorFailure } - - // Fill out the main embed - r.Embed.Title = title - r.Embed.Description = description - r.Embed.Color = color + if r.Embed != nil { + // Fill out the main embed + r.Embed.Title = options.Title + r.Embed.Description = options.Description + r.Embed.Color = color + } // If guild is nil, this is intended to be sent to Bot Admins if r.Ctx.Guild == nil && r.Ctx.Interaction == nil { @@ -359,101 +383,10 @@ func (r *Response) Send(success bool, title string, description string) { // If this is a interaction (slash command) // Run it as a interaction response and then return early if r.Ctx.Interaction != nil { - // Some commands take a while to load - // Slash commands expect a response in 3 seconds or the interaction gets invalidated - if r.Loading { - // Check to see if the command is ephemeral (only shown to the user) - if r.Ephemeral { - components := SerializeActionRow(r.ResponseComponents.Components) - log.Debugf("Sending interaction response with components: %#v", components) - _, err := Session.InteractionResponseEdit(r.Ctx.Interaction, &discordgo.WebhookEdit{ - Components: components, - Embeds: &[]*discordgo.MessageEmbed{ - r.Embed, - }, - Content: ToPtr[string](r.Content), - }) - // Just in case the interaction gets removed. - if err != nil { - if err != nil { - SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Interaction.ChannelID, r.Ctx.Message.Author.ID, "Unable to send interaction messages", err) - } - if r.Ctx.Guild.Info.ResponseChannelId != "" { - _, err = Session.ChannelMessageSendEmbed(r.Ctx.Guild.Info.ResponseChannelId, r.Embed) - - } else { - _, err = Session.ChannelMessageSendEmbed(r.Ctx.Message.ChannelID, r.Embed) - } - - if err != nil { - SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Interaction.ChannelID, r.Ctx.Message.Author.ID, "Unable to send message", err) - } - } - } else { - components := SerializeActionRow(r.ResponseComponents.Components) - log.Debugf("Sending interaction response with components: %#v", components) - _, err := Session.InteractionResponseEdit(r.Ctx.Interaction, &discordgo.WebhookEdit{ - Content: ToPtr[string](r.Content), - Embeds: &[]*discordgo.MessageEmbed{ - r.Embed, - }, - Components: components, - }) - // Just in case the interaction gets removed. - if err != nil { - log.Errorf("Error sending interaction response: %s", err) - _, err := Session.ChannelMessageSendEmbed(r.Ctx.Guild.Info.ResponseChannelId, r.Embed) - if err != nil { - _, _ = Session.ChannelMessageSendEmbed(r.Ctx.Message.ChannelID, r.Embed) - } - } - } - r.Loading = false - return - } - // Check to see if the command is ephemeral (only shown to the user) - if r.Ephemeral { - Session.InteractionRespond(r.Ctx.Interaction, &discordgo.InteractionResponse{ - // Ephemeral is type 64 don't ask why - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Flags: 1 << 6, - Embeds: []*discordgo.MessageEmbed{ - r.Embed, - }, - Components: *SerializeActionRow(r.ResponseComponents.Components), - Content: r.Content, - }, - }) - return - } - - // Default response for interaction - err := Session.InteractionRespond(r.Ctx.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Embeds: []*discordgo.MessageEmbed{ - r.Embed, - }, - Components: *SerializeActionRow(r.ResponseComponents.Components), - Content: r.Content, - }, - }) - if err != nil { - SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Interaction.ChannelID, r.Ctx.Message.Author.ID, "Unable to send interaction messages", err) - if r.Ctx.Guild.Info.ResponseChannelId != "" { - _, err = Session.ChannelMessageSendEmbed(r.Ctx.Guild.Info.ResponseChannelId, r.Embed) - - } else { - _, err = Session.ChannelMessageSendEmbed(r.Ctx.Message.ChannelID, r.Embed) - } - - if err != nil { - SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Interaction.ChannelID, r.Ctx.Message.Author.ID, "Unable to send message", err) - } - } + r.SendInteraction() return } + // Try sending the response in the configured output channel // If that fails, try sending the response in the current channel // If THAT fails, send an error report @@ -490,6 +423,90 @@ func (r *Response) Send(success bool, title string, description string) { } } +// SendInteraction +// Send a response to an interaction +func (r *Response) SendInteraction() { + var embeds []*discordgo.MessageEmbed + + if r.Embed != nil { + embeds = []*discordgo.MessageEmbed{r.Embed} + } + + // Some commands take a while to load + // Slash commands expect a response in 3 seconds or the interaction gets invalidated + if r.Loading { + var err error + // Check to see if the command is ephemeral (only shown to the user) + components := SerializeActionRow(r.ResponseComponents.Components) + webhookEdit := &discordgo.WebhookEdit{ + Content: ToPtr(r.Content), + Components: components, + } + + if embeds != nil { + webhookEdit.Embeds = &embeds + } + + if r.Ephemeral { + _, err = Session.InteractionResponseEdit(r.Ctx.Interaction, webhookEdit) + } else { + _, err = Session.InteractionResponseEdit(r.Ctx.Interaction, webhookEdit) + } + + if err != nil { + SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Interaction.ChannelID, r.Ctx.Message.Author.ID, "Unable to send interaction messages", err) + } + + r.Loading = false + return + } + // Check to see if the command is ephemeral (only shown to the user) + if r.Ephemeral { + responseData := &discordgo.InteractionResponseData{ + Flags: 1 << 6, + Components: *SerializeActionRow(r.ResponseComponents.Components), + Content: r.Content, + } + + if embeds != nil { + responseData.Embeds = embeds + } + + err := Session.InteractionRespond(r.Ctx.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: responseData, + }) + if err != nil { + SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Interaction.ChannelID, r.Ctx.Message.Author.ID, "Unable to send interaction messages", err) + } + return + } + + // Default response for interaction + err := Session.InteractionRespond(r.Ctx.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: embeds, + Components: *SerializeActionRow(r.ResponseComponents.Components), + Content: r.Content, + }, + }) + + if err != nil { + SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Interaction.ChannelID, r.Ctx.Message.Author.ID, "Unable to send interaction messages", err) + if r.Ctx.Guild.Info.ResponseChannelId != "" { + _, err = Session.ChannelMessageSendEmbed(r.Ctx.Guild.Info.ResponseChannelId, r.Embed) + + } else { + _, err = Session.ChannelMessageSendEmbed(r.Ctx.Message.ChannelID, r.Embed) + } + + if err != nil { + SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Interaction.ChannelID, r.Ctx.Message.Author.ID, "Unable to send message", err) + } + } +} + // -- Response Editing -- // EditButtonDisabled From fed0d9607e353bcc4d669cd83bc9517c3e2abdcb Mon Sep 17 00:00:00 2001 From: Riley Smith Date: Sun, 24 Mar 2024 23:56:46 -0700 Subject: [PATCH 07/10] feat: migrate workers module over from uberbot v2 --- .github/workflows/go.yml | 2 +- core.go | 25 ++++---- go.mod | 7 +++ go.sum | 29 ++++++++++ workers.go | 120 +++++++++++++++++++++++++-------------- 5 files changed, 125 insertions(+), 58 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 4b7146e..657e995 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.18 + go-version: 1.21 - name: Build run: go build -v ./... diff --git a/core.go b/core.go index 3159e3b..d02aace 100644 --- a/core.go +++ b/core.go @@ -3,9 +3,9 @@ package framework import ( "os" "os/signal" - "strconv" "strings" "syscall" + "time" "github.com/bwmarrin/discordgo" tlog "github.com/ubergeek77/tinylog" @@ -66,6 +66,10 @@ var initProvider func() GuildProvider // A boolean that tells the bot to log debug messages var debugMode = false +// workerManager +// The worker manager for the bot +var workerManager *WorkerManager + // SetInitProvider // Sets the init provider func SetInitProvider(provider func() GuildProvider) { @@ -134,6 +138,8 @@ func Start() { log.Fatalf("You have not specified a Discord bot token!") } + workerManager = InitializeManager(time.UTC) + // Use the token to create a new session var err error Session, err = discordgo.New("Bot " + botToken) @@ -174,7 +180,6 @@ func Start() { log.Infof("Bot logged in as \"" + Session.State.Ready.User.Username + "#" + Session.State.Ready.User.Discriminator + "\"") // Start workers - startWorkers() // Print information about the current bot admins numAdmins := 0 @@ -213,9 +218,6 @@ func Start() { log.Info("Received TERM signal, terminating gracefully.") - // Set the global loop variable to false so all background loops terminate - continueLoop = false - // Make a second sig channel that will respond to user term signal immediately sigInstant := make(chan os.Signal, 1) signal.Notify(sigInstant, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) @@ -223,15 +225,8 @@ func Start() { // Make a goroutine that will wait for all background workers to be unlocked go func() { log.Info("Waiting for workers to exit... (interrupt to kill immediately; not recommended!!!)") - for i, lock := range workerLock { - // Try locking the worker mutex. This will block if the mutex is already locked - // If we are able to lock it, then it means the worker has stopped. - lock.Lock() - log.Info("Stopped worker " + strconv.Itoa(i)) - - lock.Unlock() - } - + // Stop all workers + workerManager.StopWorkers() log.Info("All routines exited gracefully.") // Send our own signal to the instant sig channel @@ -242,7 +237,7 @@ func Start() { <-sigInstant log.Info("Closing the Discord session...") - closeErr := Session.Close() + closeErr := Session.CloseWithCode(1000) if closeErr != nil { log.Errorf("An error occurred when closing the Discord session: %s", err) return diff --git a/go.mod b/go.mod index d901d13..63ec4ca 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,13 @@ require ( ) require ( + github.com/google/uuid v1.4.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + go.uber.org/atomic v1.9.0 // indirect +) + +require ( + github.com/go-co-op/gocron v1.37.0 github.com/gorilla/websocket v1.5.1 // indirect github.com/pkg/errors v0.9.1 // indirect golang.org/x/crypto v0.21.0 // indirect diff --git a/go.sum b/go.sum index 7407260..edd7111 100644 --- a/go.sum +++ b/go.sum @@ -4,26 +4,51 @@ github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4Ho github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/bwmarrin/discordgo v0.27.2-0.20240315152229-33ee38cbf271 h1:BuDtVy29wfi7XFRYUP2c65EJuMWWNCWhkiP+jYIU4eY= github.com/bwmarrin/discordgo v0.27.2-0.20240315152229-33ee38cbf271/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= +github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/ubergeek77/tinylog v1.0.0 h1:gsq98mbig3LDWhsizOe2tid12wHUz/mrkDlmgJ0MZG4= github.com/ubergeek77/tinylog v1.0.0/go.mod h1:NzUi4PkRG2hACL4cGgmW7db6EaKjAeqrqlVQnJdw78Q= gitlab.com/tozd/go/errors v0.8.1 h1:RfylffRAsl3PbDdHNUBEkTleTCiL/RIT+Ef8p0HRNCI= gitlab.com/tozd/go/errors v0.8.1/go.mod h1:PvIdUMLpPwxr+KEBxghQaCMydHXGYdJQn/PhdMqYREY= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= @@ -37,5 +62,9 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/workers.go b/workers.go index 614a31a..de50ab7 100644 --- a/workers.go +++ b/workers.go @@ -1,55 +1,91 @@ package framework +// workers.go +// This package contains the necessary code to schedule reoccurring events +// Workers can manipulate different parts of the bot and are responsible for +// Mutes, TempBans, Presence updates, and other required things +// Commands can also register workers with the manager + +// todo clean up the documentation + +// WORKERS RUN MULTIPLE TIMES WHILE THE BOT IS RUNNING +// JOBS ARE THE ACTUAL GOCRON VERSION OF the WORKER + import ( - "sync" "time" + + "github.com/go-co-op/gocron" + tlog "github.com/ubergeek77/tinylog" ) -// workers.go -// This file contains everything for adding and managing workers +var wlog = tlog.NewTaggedLogger("WorkerManager", tlog.NewColor("38;5;111")) -// workerLock -// A map that stores mutexes for the background workers -// These will be used to determine when the workers have exited gracefully -// If a worker is still locked, then it has not exited -var workerLock = make(map[int]*sync.Mutex) +// WorkerManager is an easy way to manage workers. +type WorkerManager struct { + Scheduler *gocron.Scheduler + Workers map[string]Worker + Jobs []*gocron.Job + IsRunning bool +} + +// Worker +// Describes a worker. +type Worker struct { + Duration string + WorkerFunc func() +} + +func InitializeManager(loc *time.Location) *WorkerManager { + wrk := &WorkerManager{ + Scheduler: gocron.NewScheduler(loc), + Workers: make(map[string]Worker), + IsRunning: false, + } + wrk.Scheduler.TagsUnique() + return wrk +} -// workers -// The list of workers that are to be pre-registered before the bot starts, then all executed in the background -var workers []func() +// Start +// Will start all the workers via the scheduler. +func (m *WorkerManager) Start() { + m.Scheduler.StartAsync() + m.IsRunning = true +} -// continueLoop -// This boolean will be changed to false when the bot is trying to shut down -// All the background workers are looping on this being true, meaning they will stop when it is false -var continueLoop = true +// StopWorkers +// Will stop all the workers via the scheduler. +func (m *WorkerManager) StopWorkers() { + m.Scheduler.StopBlockingChan() + m.IsRunning = false +} // AddWorker -// Given a function that is passed through, append it to the list of worker functions -func AddWorker(worker func()) { - workers = append(workers, worker) -} - -// startWorkers -// Go through the list of workers than have been added to the list, and execute them all in the background -func startWorkers() { - // Iterate over all the workers - for i, worker := range workers { - // Create a mutex for this worker - workerLock[i] = &sync.Mutex{} - - // Start a goroutine for this worker, which starts it in the background - go func(worker func(), i int) { - // Lock the worker; this will be used in graceful termination - workerLock[i].Lock() - - // Run the worker once per second, forever, until a TERM signal breaks this loop - for continueLoop { - worker() - time.Sleep(time.Second) - } - - // The loop has stopped. Unlock the worker - workerLock[i].Unlock() - }(worker, i) +// Adds a worker to the internal worker map. +func (m *WorkerManager) AddWorker(tag string, worker Worker) { + m.Workers[tag] = worker +} + +// AddWorkers +// registers all the workers to the scheduler. +func (m *WorkerManager) AddWorkers() { + for tag, worker := range m.Workers { + job, err := m.Scheduler.Cron(worker.Duration).Tag(tag).Do(worker.WorkerFunc) + if err != nil { + wlog.Errorf("Unable to register worker %s", tag) + wlog.Fatal(err.Error()) + } + m.Jobs = append(m.Jobs, job) } } + +// RemoveWorker +// Removes a worker from the scheduler. +func (m *WorkerManager) RemoveWorker() { + +} + +// AddWorkerOnce +// Easy way to add a single job to the scheduler. +func (m *WorkerManager) AddWorkerOnce() { + +} From cf94347f01e3b8adb50e5462db054430a51f6e46 Mon Sep 17 00:00:00 2001 From: Riley Smith Date: Sun, 24 Mar 2024 23:59:53 -0700 Subject: [PATCH 08/10] remove tests from ci --- .github/workflows/go.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 657e995..fb4c350 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,6 +19,3 @@ jobs: - name: Build run: go build -v ./... - - - name: Test - run: go test -v ./... From 999fa38f3586deabe5649899fec1516dd9e1855a Mon Sep 17 00:00:00 2001 From: Riley Smith Date: Mon, 25 Mar 2024 00:49:52 -0700 Subject: [PATCH 09/10] move workers system to separate package, fix handler registration --- core.go | 46 ++++++++++++++------------------ workers.go => workers/workers.go | 2 +- 2 files changed, 21 insertions(+), 27 deletions(-) rename workers.go => workers/workers.go (99%) diff --git a/core.go b/core.go index d02aace..f972787 100644 --- a/core.go +++ b/core.go @@ -8,6 +8,7 @@ import ( "time" "github.com/bwmarrin/discordgo" + "github.com/qpixel/framework/workers" tlog "github.com/ubergeek77/tinylog" ) @@ -20,7 +21,7 @@ var MessageState = 500 // log // The logger for the core bot -var log = tlog.NewTaggedLogger("BotCore", tlog.NewColor("38;5;111")) +var log = tlog.NewTaggedLogger("Framework", tlog.NewColor("38;5;111")) // dlog // The logger for discordgo @@ -54,10 +55,6 @@ var ColorSuccess = 0x55F485 // The color to use for response embeds reporting failure var ColorFailure = 0xF45555 -// BotPresence -// Presence data to send when the bot is logging in -var botPresence discordgo.GatewayStatusUpdate - // initProvider // Stores and allows for the calling of the chosen GuildProvider var initProvider func() GuildProvider @@ -68,7 +65,7 @@ var debugMode = false // workerManager // The worker manager for the bot -var workerManager *WorkerManager +var WorkerManager *workers.WorkerManager // SetInitProvider // Sets the init provider @@ -76,12 +73,6 @@ func SetInitProvider(provider func() GuildProvider) { initProvider = provider } -// SetPresence -// Sets the gateway field for bot presence -func SetPresence(presence discordgo.GatewayStatusUpdate) { - botPresence = presence -} - // AddAdmin // A function that allows admins to be added, but not removed func AddAdmin(userId string) { @@ -138,7 +129,7 @@ func Start() { log.Fatalf("You have not specified a Discord bot token!") } - workerManager = InitializeManager(time.UTC) + WorkerManager = workers.InitializeManager(time.UTC) // Use the token to create a new session var err error @@ -148,25 +139,21 @@ func Start() { log.Fatalf("Failed to create Discord session: %s", err) } if debugMode { - Session.LogLevel = discordgo.LogDebug + Session.LogLevel = discordgo.LogInformational Session.Debug = true + } else { + Session.LogLevel = discordgo.LogWarning + } + + if os.Getenv("LOG_LEVEL") != "" && os.Getenv("LOG_LEVEL") == "DEBUG" { + Session.LogLevel = discordgo.LogDebug } + // Setup State specific variables Session.State.MaxMessageCount = MessageState - Session.LogLevel = discordgo.LogWarning Session.SyncEvents = false Session.Identify.Intents = discordgo.IntentsAllWithoutPrivileged | discordgo.IntentMessageContent - // Set the bots status - Session.Identify.Presence = botPresence - - // Open the session - log.Info("Connecting to Discord...") - err = Session.Open() - if err != nil { - log.Fatalf("Failed to connect to Discord: %s", err) - } - // Add the commandHandler to the list of user-defined handlers AddDGOHandler(commandHandler) @@ -176,6 +163,13 @@ func Start() { // Add the handlers to the session addDGoHandlers() + // Open the session + log.Info("Connecting to Discord...") + err = Session.Open() + if err != nil { + log.Fatalf("Failed to connect to Discord: %s", err) + } + // Log that the login succeeded log.Infof("Bot logged in as \"" + Session.State.Ready.User.Username + "#" + Session.State.Ready.User.Discriminator + "\"") @@ -226,7 +220,7 @@ func Start() { go func() { log.Info("Waiting for workers to exit... (interrupt to kill immediately; not recommended!!!)") // Stop all workers - workerManager.StopWorkers() + WorkerManager.StopWorkers() log.Info("All routines exited gracefully.") // Send our own signal to the instant sig channel diff --git a/workers.go b/workers/workers.go similarity index 99% rename from workers.go rename to workers/workers.go index de50ab7..ab06960 100644 --- a/workers.go +++ b/workers/workers.go @@ -1,4 +1,4 @@ -package framework +package workers // workers.go // This package contains the necessary code to schedule reoccurring events From 85b2a5ffa54064547f5e046daad448ea0b41b79c Mon Sep 17 00:00:00 2001 From: Riley Smith Date: Mon, 25 Mar 2024 22:51:39 -0700 Subject: [PATCH 10/10] added the ability to remove slash commands, expanded debug mode --- commands.go | 11 +++++++++ core.go | 37 +++++++++++++++++++++++++--- interaction.go | 66 +++++++++++++++++++++++--------------------------- response.go | 8 ++++++ util.go | 12 +++++++++ 5 files changed, 94 insertions(+), 40 deletions(-) diff --git a/commands.go b/commands.go index 3ea3745..9536dbe 100644 --- a/commands.go +++ b/commands.go @@ -351,6 +351,12 @@ func AddChatCommand(info *CommandInfo, function BotFunction) { alias = strings.ToLower(alias) commandAliases[alias] = info.Name } + + if _, ok := commands[strings.ToLower(info.Name)]; ok { + log.Errorf("Command was already registered %s", info.Name) + return + + } // Add the command to the map; command triggers are case-insensitive commands[strings.ToLower(info.Name)] = &command } @@ -369,6 +375,11 @@ func AddContextCommand(info *CommandInfo, function BotFunction) { command.Handlers["default"] = function // Add the command to the map; command triggers are case-insensitive + if _, ok := commands[strings.ToLower(info.Name)]; ok { + log.Errorf("Command was already registered %s", info.Name) + return + } + commands[strings.ToLower(info.Name)] = &command } diff --git a/core.go b/core.go index f972787..7462fa5 100644 --- a/core.go +++ b/core.go @@ -113,8 +113,8 @@ func SetDebugMode() { debugMode = true } -// Start the bot. -func Start() { +// Run the bot. +func Run() { discordgo.Logger = dgoLog // Load all the guilds @@ -141,8 +141,10 @@ func Start() { if debugMode { Session.LogLevel = discordgo.LogInformational Session.Debug = true + log.LogLevel = tlog.DebugLevel } else { Session.LogLevel = discordgo.LogWarning + log.LogLevel = tlog.WarningLevel } if os.Getenv("LOG_LEVEL") != "" && os.Getenv("LOG_LEVEL") == "DEBUG" { @@ -205,7 +207,7 @@ func Start() { // Set up a sigterm channel, so we can detect when the application receives a TERM signal sigChannel := make(chan os.Signal, 1) - signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, syscall.SIGKILL, syscall.SIGQUIT, syscall.SIGABRT) // Keep this thread blocked forever, until a TERM signal is received <-sigChannel @@ -214,7 +216,7 @@ func Start() { // Make a second sig channel that will respond to user term signal immediately sigInstant := make(chan os.Signal, 1) - signal.Notify(sigInstant, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + signal.Notify(sigInstant, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, syscall.SIGKILL, syscall.SIGQUIT, syscall.SIGABRT) // Make a goroutine that will wait for all background workers to be unlocked go func() { @@ -238,4 +240,31 @@ func Start() { } log.Info("Session closed.") + +} + +func DeleteSlashCommands() { + var err error + Session, err = discordgo.New("Bot " + botToken) + if err != nil { + log.Fatalf("Failed to create Discord session: %s", err) + } + if debugMode { + Session.LogLevel = discordgo.LogInformational + Session.Debug = true + log.LogLevel = tlog.DebugLevel + } else { + Session.LogLevel = discordgo.LogWarning + log.LogLevel = tlog.WarningLevel + } + err = Session.Open() + if err != nil { + log.Fatalf("Failed to connect to Discord: %s", err) + } + + log.Infof("%#v", Session) + RemoveAllSlashCommands() + + log.Info("Slash commands deleted.") + Session.Close() } diff --git a/interaction.go b/interaction.go index 2254d03..b053b36 100644 --- a/interaction.go +++ b/interaction.go @@ -112,31 +112,6 @@ func handleApplicationCommand(s *discordgo.Session, i *discordgo.InteractionCrea case discordgo.UserApplicationCommand, discordgo.MessageApplicationCommand: handleApplicationContextCommand(i) } - - // if !IsAdmin(i.Member.User.ID) { - // // Ignore the command if it is globally disabled - // if g.IsGloballyDisabled(trigger) { - // ErrorResponse(i.Interaction, "Command is globally disabled", trigger) - // return - // } - - // // Ignore the command if this channel has blocked the command - // if g.CommandIsDisabledInChannel(trigger, i.ChannelID) { - // ErrorResponse(i.Interaction, "Command is disabled in this channel!", trigger) - // return - // } - - // // Ignore any message if the user is banned from using the bot - // if !g.MemberOrRoleIsWhitelisted(i.Member.User.ID) || g.MemberOrRoleIsIgnored(i.Member.User.ID) { - // return - // } - - // // Ignore the message if this channel is not whitelisted, or if it is ignored - // if !g.ChannelIsWhitelisted(i.ChannelID) || g.ChannelIsIgnored(i.ChannelID) { - // return - // } - // } - } // handleChatApplicationCommand @@ -151,14 +126,13 @@ func handleChatApplicationCommand(s *discordgo.Session, i *discordgo.Interaction g := getGuild(i.GuildID) trigger := i.ApplicationCommandData().Name + log.Debugf("Handling command %s", trigger) + command := commands[trigger] + log.Debugf("Command %s found %#v", trigger, command) - // if IsAdmin(i.Member.User.ID) || command.Info.Public || g.IsMod(i.Member.User.ID) { - // Check if the command is public, or if the current user is a bot moderator - // Bot admins supercede both checks - // } - log.Debugf("%#v", i.Interaction) + defer handleSlashCommandError(*i.Interaction) command.Handlers["default"](&Context{ Guild: g, @@ -172,7 +146,7 @@ func handleChatApplicationCommand(s *discordgo.Session, i *discordgo.Interaction GuildID: i.GuildID, Content: "", }, - Log: tinylog.NewTaggedLogger(fmt.Sprintf("Module: %s", command.Info.Name), tinylog.NewColor("38;5;111")), + Log: MakeModuleLogger(command.Info.Name), }) } @@ -194,6 +168,7 @@ func handleUserContextCommand(i *discordgo.InteractionCreate) { log.Debugf("Handling command %s", trigger) command := commands[trigger] log.Debugf("Command %s found %#v", trigger, command) + defer handleSlashCommandError(*i.Interaction) command.Handlers["default"](&Context{ Guild: getGuild(i.GuildID), @@ -204,7 +179,7 @@ func handleUserContextCommand(i *discordgo.InteractionCreate) { ChannelID: i.ChannelID, Content: "", }, - Log: tinylog.NewTaggedLogger(fmt.Sprintf("Module: %s", command.Info.Name), botContextLoggerColor), + Log: MakeModuleLogger(command.Info.Name), }) } @@ -218,13 +193,14 @@ func handleMessageContextCommand(i *discordgo.InteractionCreate) { log.Debugf("Handling command %s", trigger) command := commands[trigger] log.Debugf("Command %s found %#v", trigger, command) - // defer handleSlashCommandError(*i.Interaction) + + defer handleSlashCommandError(*i.Interaction) command.Handlers["default"](&Context{ Guild: getGuild(i.GuildID), Cmd: *command.Info, Interaction: i.Interaction, Message: message, - Log: tinylog.NewTaggedLogger(fmt.Sprintf("Module: %s", command.Info.Name), botContextLoggerColor), + Log: MakeModuleLogger(command.Info.Name), }) } @@ -235,6 +211,7 @@ func handleUserApplicationChatCommand(s *discordgo.Session, i *discordgo.Interac log.Debugf("Handling user command %s", trigger) command := commands[trigger] log.Debugf("Command %s found %#v", trigger, command) + defer handleSlashCommandError(*i.Interaction) command.Handlers["default"](&Context{ Guild: nil, @@ -249,7 +226,7 @@ func handleUserApplicationChatCommand(s *discordgo.Session, i *discordgo.Interac ChannelID: i.ChannelID, Content: "", }, - Log: tinylog.NewTaggedLogger(fmt.Sprintf("Module: %s", command.Info.Name), botContextLoggerColor), + Log: MakeModuleLogger(command.Info.Name), }) } @@ -353,7 +330,7 @@ func ParseInteractionArgsR(options []*discordgo.ApplicationCommandInteractionDat Value: v.StringValue(), } if v.Options != nil { - ParseInteractionArgsR(v.Options, *&args) + ParseInteractionArgsR(v.Options, args) } } } @@ -377,6 +354,23 @@ func RemoveGuildSlashCommands(guildID string) { } } +// RemoveAllSlashCommands +// Removes all slash commands. +func RemoveAllSlashCommands() { + commands, err := Session.ApplicationCommands(Session.State.User.ID, "") + if err != nil { + log.Errorf("Error getting all slash commands %s", err) + return + } + for _, k := range commands { + err = Session.ApplicationCommandDelete(Session.State.User.ID, "", k.ID) + if err != nil { + log.Errorf("error deleting slash command %s %s %s", k.Name, k.ID, err) + continue + } + } +} + func handleSlashCommandError(i discordgo.Interaction) { if r := recover(); r != nil { e := errors.WithStack(r.(error)) diff --git a/response.go b/response.go index 87ac45c..6150f25 100644 --- a/response.go +++ b/response.go @@ -543,7 +543,15 @@ func (r *Response) EditButtonComplex(buttonID string, label string, style discor // Edit // Edit a response func (r *Response) Edit() { + // Stupid fix for https://arc.net/l/quote/rawqeoye + // Follow up messages have to be ephemeral, even if they are just edits... + if r.Ctx.Message.GuildID == "" { + log.Errorf("Cannot edit a message in a DM (blame Discord)") + return + } + component := SerializeActionRow(r.ResponseComponents.Components) + log.Debugf("Editing response with components: %#v", component) _, err := Session.ChannelMessageEditComplex(&discordgo.MessageEdit{ Channel: r.Ctx.Interaction.Message.ChannelID, diff --git a/util.go b/util.go index f9feaae..2b3aeb2 100644 --- a/util.go +++ b/util.go @@ -10,6 +10,7 @@ import ( "github.com/bwmarrin/discordgo" "github.com/dlclark/regexp2" + "github.com/ubergeek77/tinylog" ) // util.go @@ -385,6 +386,17 @@ func dgoLog(msgL, caller int, format string, a ...interface{}) { } } +// MakeModuleLogger +// Makes a logger for a module +func MakeModuleLogger(module string) *tinylog.Logger { + cfg := tinylog.NewConfig() + if debugMode { + cfg.LogLevel = tinylog.DebugLevel + } + cfg.LogPrefix = tinylog.GenerateTag(fmt.Sprintf("Module: %s", module), botContextLoggerColor, cfg) + return tinylog.NewLogger(cfg) +} + // ToPtr // quick func to turn anything into a pointer func ToPtr[T any](v T) *T {