307 lines
9.2 KiB
Go
307 lines
9.2 KiB
Go
package framework
|
|
|
|
import (
|
|
"github.com/QPixel/orderedmap"
|
|
"github.com/bwmarrin/discordgo"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// commands.go
|
|
// This file contains everything required to add core commands to the bot, and parse commands from a message
|
|
|
|
// Group
|
|
// Defines different "groups" of commands for ordering in a help command
|
|
type Group string
|
|
|
|
var (
|
|
Moderation Group = "moderation"
|
|
Utility Group = "utility"
|
|
)
|
|
|
|
// 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
|
|
Trigger string // The string that will trigger the command
|
|
}
|
|
|
|
// Context
|
|
// This is a context of a single command invocation
|
|
// This gives the command function access to all the information it might need
|
|
type Context struct {
|
|
Guild *Guild // NOTE: Guild is a pointer, since we want to use the SAME instance of the guild across the program!
|
|
Cmd CommandInfo
|
|
Args Arguments
|
|
Message *discordgo.Message
|
|
Interaction *discordgo.Interaction
|
|
}
|
|
|
|
// 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
|
|
type BotFunction func(ctx *Context)
|
|
|
|
// Command
|
|
// The definition of a command, which is that command's information, along with the function it will run
|
|
type Command struct {
|
|
Info CommandInfo
|
|
Function BotFunction
|
|
}
|
|
|
|
// ChildCommand
|
|
// Defines how child commands are stored
|
|
type ChildCommand map[string]map[string]Command
|
|
|
|
// commands
|
|
// All the registered core commands (not custom commands)
|
|
// 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)
|
|
|
|
// 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
|
|
|
|
// 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
|
|
command := Command{
|
|
Info: *info,
|
|
Function: 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)
|
|
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)
|
|
}
|
|
// Add the command to the map; command triggers are case-insensitive
|
|
childCommands[parentID][command.Info.Trigger] = 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)
|
|
if err != nil {
|
|
c <- "Unable to register slash commands :/"
|
|
log.Errorf("Cannot create '%v' command: %v", v.Name, err)
|
|
log.Errorf("%v", v.Options)
|
|
return
|
|
}
|
|
}
|
|
c <- "Finished registering slash commands"
|
|
return
|
|
}
|
|
|
|
// GetCommands
|
|
// Provide a way to read commands without making it possible to modify their functions
|
|
func GetCommands() map[string]CommandInfo {
|
|
list := make(map[string]CommandInfo)
|
|
for x, y := range commands {
|
|
list[x] = y.Info
|
|
}
|
|
return list
|
|
}
|
|
|
|
// 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) {
|
|
// Try getting an object for the current channel, with a fallback in case session.state is not ready or is nil
|
|
channel, err := session.State.Channel(message.ChannelID)
|
|
if err != nil {
|
|
if channel, err = session.Channel(message.ChannelID); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Ignore messages sent by the bot
|
|
if message.Author.ID == session.State.User.ID {
|
|
return
|
|
}
|
|
|
|
g := getGuild(message.GuildID)
|
|
|
|
trigger, argString := ExtractCommand(&g.Info, message.Content)
|
|
if trigger == nil {
|
|
return
|
|
}
|
|
// Only do further checks if the user is not a bot admin
|
|
if !IsAdmin(message.Author.ID) {
|
|
// Ignore the command if it is globally disabled
|
|
if g.IsGloballyDisabled(*trigger) {
|
|
return
|
|
}
|
|
|
|
// Ignore the command if this channel has blocked the command
|
|
if g.CommandIsDisabledInChannel(*trigger, message.ChannelID) {
|
|
return
|
|
}
|
|
|
|
// Ignore any message if the user is banned from using the bot
|
|
if !g.MemberOrRoleIsWhitelisted(message.Author.ID) || g.MemberOrRoleIsIgnored(message.Author.ID) {
|
|
return
|
|
}
|
|
|
|
// Ignore the message if this channel is not whitelisted, or if it is ignored
|
|
if !g.ChannelIsWhitelisted(message.ChannelID) || g.ChannelIsIgnored(message.ChannelID) {
|
|
return
|
|
}
|
|
}
|
|
|
|
//Get the command to run
|
|
// Error Checking
|
|
command, ok := commands[commandAliases[*trigger]]
|
|
if !ok {
|
|
log.Errorf("Command was not found")
|
|
return
|
|
}
|
|
// Check if the command is public, or if the current user is a bot moderator
|
|
// Bot admins supercede both checks
|
|
if IsAdmin(message.Author.ID) || command.Info.Public || g.IsMod(message.Author.ID) {
|
|
// Run the command with the necessary context
|
|
if command.Info.IsTyping && g.Info.ResponseChannelId == "" {
|
|
_ = Session.ChannelTyping(message.ChannelID)
|
|
}
|
|
// The command is valid, so now we need to delete the invoking message if that is configured
|
|
if g.Info.DeletePolicy {
|
|
err := Session.ChannelMessageDelete(message.ChannelID, message.ID)
|
|
if err != nil {
|
|
SendErrorReport(message.GuildID, message.ChannelID, message.Author.ID, "Failed to delete message: "+message.ID, err)
|
|
}
|
|
}
|
|
|
|
defer handleCommandError(g.ID, channel.ID, message.Author.ID)
|
|
if command.Info.IsParent {
|
|
handleChildCommand(*argString, command, message.Message, g)
|
|
return
|
|
}
|
|
command.Function(&Context{
|
|
Guild: g,
|
|
Cmd: command.Info,
|
|
Args: *ParseArguments(*argString, command.Info.Arguments),
|
|
Message: message.Message,
|
|
})
|
|
// Makes sure that variables ran in ParseArguments are gone.
|
|
if commandsGC == 25 && commandsGC > 25 {
|
|
debug.FreeOSMemory()
|
|
commandsGC = 0
|
|
} else {
|
|
commandsGC++
|
|
}
|
|
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 {
|
|
log.Warningf("Recovering from panic: %s", r)
|
|
log.Warningf("Sending Error report to admins")
|
|
SendErrorReport(gID, cId, uId, "Error!", r.(runtime.Error))
|
|
message, err := Session.ChannelMessageSend(cId, "Error!")
|
|
if err != nil {
|
|
log.Errorf("err sending message %s", err)
|
|
}
|
|
time.Sleep(5 * time.Second)
|
|
_ = Session.ChannelMessageDelete(cId, message.ID)
|
|
return
|
|
}
|
|
return
|
|
}
|