new argument parser
This commit is contained in:
parent
98d784a5bd
commit
08183b82d9
387
arguments.go
387
arguments.go
|
|
@ -2,8 +2,10 @@ package framework
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/QPixel/orderedmap"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/dlclark/regexp2"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
|
@ -36,7 +38,10 @@ var (
|
|||
Channel ArgTypeGuards = "channel"
|
||||
User ArgTypeGuards = "user"
|
||||
Role ArgTypeGuards = "role"
|
||||
GuildArg ArgTypeGuards = "guild"
|
||||
Message ArgTypeGuards = "message"
|
||||
Boolean ArgTypeGuards = "bool"
|
||||
Id ArgTypeGuards = "id"
|
||||
SubCmd ArgTypeGuards = "subcmd"
|
||||
SubCmdGrp ArgTypeGuards = "subcmdgrp"
|
||||
ArrString ArgTypeGuards = "arrString"
|
||||
|
|
@ -52,7 +57,7 @@ type ArgInfo struct {
|
|||
Flag bool
|
||||
DefaultOption string
|
||||
Choices []string
|
||||
Regex string
|
||||
Regex *regexp2.Regexp
|
||||
}
|
||||
|
||||
// CommandArg
|
||||
|
|
@ -113,14 +118,28 @@ func (cI *CommandInfo) AddArg(argument string, typeGuard ArgTypeGuards, match Ar
|
|||
Match: match,
|
||||
DefaultOption: defaultOption,
|
||||
Choices: nil,
|
||||
Regex: "",
|
||||
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,
|
||||
|
|
@ -128,7 +147,7 @@ func (cI *CommandInfo) AddFlagArg(flag string, typeGuard ArgTypeGuards, match Ar
|
|||
Match: match,
|
||||
TypeGuard: typeGuard,
|
||||
DefaultOption: defaultOption,
|
||||
Regex: "",
|
||||
Regex: regex,
|
||||
})
|
||||
return cI
|
||||
}
|
||||
|
|
@ -153,87 +172,61 @@ func (cI *CommandInfo) SetTyping(isTyping bool) *CommandInfo {
|
|||
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
|
||||
// Parses the arguments into a pointer to an Arguments struct
|
||||
// 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"
|
||||
splitString := strings.Split(args, " ")
|
||||
|
||||
// Current Position in the infoArgs map
|
||||
currentPos := 0
|
||||
|
||||
// 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
|
||||
|
||||
for i := 0; i < len(splitString); i++ {
|
||||
for n := currentPos; n <= len(k); n++ {
|
||||
if n > len(k)+1 || currentPos+1 > len(k) {
|
||||
break
|
||||
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
|
||||
}
|
||||
v, _ := infoArgs.Get(k[currentPos])
|
||||
vv := v.(*ArgInfo)
|
||||
switch vv.Match {
|
||||
case ArgOption:
|
||||
// Lets first check the typeguard to see if the str matches the arg
|
||||
if checkTypeGuard(splitString[i], vv.TypeGuard) {
|
||||
// todo abstract this into handleArgOption
|
||||
// Handle quoted ArgOptions separately
|
||||
if strings.Contains(splitString[i], "\"") {
|
||||
st := CommandArg{}
|
||||
st, i = handleQuotedString(splitString, *vv, i)
|
||||
ar[k[currentPos]] = st
|
||||
currentPos++
|
||||
break
|
||||
}
|
||||
// Handle ArgOption
|
||||
ar[k[currentPos]] = handleArgOption(splitString[i], *vv)
|
||||
currentPos++
|
||||
break
|
||||
}
|
||||
if n+1 > len(splitString) {
|
||||
break
|
||||
}
|
||||
// If the TypeGuard does not match check to see if the Arg is required or not
|
||||
if vv.Required {
|
||||
// Set the CommandArg to the default option, which is usually ""
|
||||
ar[k[currentPos]] = CommandArg{
|
||||
commandContent, _ := createContentString(splitString, 0)
|
||||
ar[modK[0]] = CommandArg{
|
||||
info: *vv,
|
||||
Value: vv.DefaultOption,
|
||||
}
|
||||
currentPos++
|
||||
break
|
||||
} else {
|
||||
// If it's not required, we set the CommandArg to ""
|
||||
ar[k[currentPos]] = CommandArg{
|
||||
info: *vv,
|
||||
Value: "",
|
||||
}
|
||||
currentPos++
|
||||
break
|
||||
}
|
||||
case ArgContent:
|
||||
// Takes the splitString and currentPos to find how many more elements in the slice
|
||||
// need to join together
|
||||
contentString := ""
|
||||
contentString, i = createContentString(splitString, i)
|
||||
ar[k[currentPos]] = CommandArg{
|
||||
info: *vv,
|
||||
Value: contentString,
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
Value: commandContent,
|
||||
}
|
||||
return &ar
|
||||
// Else return the args struct
|
||||
} else {
|
||||
return &ar
|
||||
}
|
||||
}
|
||||
|
||||
/* Argument Parsing Helpers */
|
||||
|
|
@ -247,22 +240,254 @@ func createContentString(splitString []string, currentPos int) (string, int) {
|
|||
return strings.TrimSuffix(str, " "), currentPos
|
||||
}
|
||||
|
||||
func handleQuotedString(splitString []string, argInfo ArgInfo, currentPos int) (CommandArg, int) {
|
||||
str := ""
|
||||
splitString[currentPos] = strings.TrimPrefix(splitString[currentPos], "\"")
|
||||
for i := currentPos; i < len(splitString); i++ {
|
||||
if !strings.HasSuffix(splitString[i], "\"") {
|
||||
str += splitString[i] + " "
|
||||
// Finds all the 'option' type args
|
||||
func findAllOptionArgs(argString []string, keys []string, infoArgs *orderedmap.OrderedMap, args *Arguments) (Arguments, bool, []string, []string) {
|
||||
if len(keys) == 0 || keys == nil {
|
||||
return *args, false, []string{}, []string{}
|
||||
}
|
||||
modifiedArgString := ""
|
||||
var modKeys []string
|
||||
var indexes []int
|
||||
// If the length of the argString is equal to the length of keys
|
||||
// We can just set each value in argString to a CommandArg
|
||||
// If a match field is equal to the content we will return early.
|
||||
// This is a rare occurrence. But this allows for a faster result
|
||||
if len(argString) == len(keys) {
|
||||
for i, v := range argString {
|
||||
// error handling
|
||||
iA, ok := infoArgs.Get(keys[i])
|
||||
if !ok {
|
||||
err := errors.New(fmt.Sprintf("Unable to find map relating to key: %s", keys[i]))
|
||||
SendErrorReport("", "", "", "Argument Parsing error", err)
|
||||
continue
|
||||
}
|
||||
vv := iA.(*ArgInfo)
|
||||
// ArgContent type should always be the last item in the slice
|
||||
// Should be safe to return early
|
||||
if vv.Match == ArgContent {
|
||||
modifiedArgString = strings.Join(argString[i:], " ")
|
||||
modKeys = RemoveItems(keys, indexes)
|
||||
return *args, true, createSplitString(modifiedArgString), modKeys
|
||||
}
|
||||
if checkTypeGuard(v, vv.TypeGuard) {
|
||||
(*args)[keys[i]] = handleArgOption(v, *vv)
|
||||
indexes = append(indexes, i)
|
||||
}
|
||||
}
|
||||
return *args, false, createSplitString(modifiedArgString), modKeys
|
||||
}
|
||||
// (semi) Brute force method
|
||||
// First lets find all required args
|
||||
currentPos := 0
|
||||
for i, v := range keys {
|
||||
// error handling
|
||||
iA, ok := infoArgs.Get(v)
|
||||
if !ok {
|
||||
err := errors.New(fmt.Sprintf("Unable to find map relating to key: %s", keys[i]))
|
||||
SendErrorReport("", "", "", "Argument Parsing error", err)
|
||||
continue
|
||||
}
|
||||
vv := iA.(*ArgInfo)
|
||||
if vv.Required {
|
||||
if vv.TypeGuard != String {
|
||||
var value string
|
||||
value, argString = findTypeGuard(strings.Join(argString, " "), argString, vv.TypeGuard)
|
||||
(*args)[v] = handleArgOption(value, *vv)
|
||||
indexes = append(indexes, i)
|
||||
} else if checkTypeGuard(argString[currentPos], vv.TypeGuard) {
|
||||
(*args)[v] = handleArgOption(argString[currentPos], *vv)
|
||||
currentPos++
|
||||
indexes = append(indexes, i)
|
||||
} else {
|
||||
(*args)[v] = handleArgOption(vv.DefaultOption, *vv)
|
||||
indexes = append(indexes, i)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
str += strings.TrimSuffix(splitString[i], "\"")
|
||||
currentPos = i
|
||||
break
|
||||
}
|
||||
}
|
||||
return CommandArg{
|
||||
info: argInfo,
|
||||
Value: str,
|
||||
}, currentPos
|
||||
// Remove already found keys and clear the index list
|
||||
// We also reset some values that we reuse
|
||||
//if
|
||||
modKeys = RemoveItems(keys, indexes)
|
||||
argString = argString[currentPos:]
|
||||
indexes = nil
|
||||
currentPos = 0
|
||||
// Now lets find the not required args
|
||||
for _, v := range modKeys {
|
||||
// error handling
|
||||
iA, ok := infoArgs.Get(v)
|
||||
if !ok {
|
||||
err := errors.New(fmt.Sprintf("Unable to find map relating to key: %s", v))
|
||||
SendErrorReport("", "", "", "Argument Parsing error", err)
|
||||
continue
|
||||
}
|
||||
vv := iA.(*ArgInfo)
|
||||
// If we find an arg that is required send an error and return
|
||||
if vv.Required {
|
||||
err := errors.New(fmt.Sprintf("Found a required arg where there is supposed to be none %s", v))
|
||||
SendErrorReport("", "", "", "Argument Parsing error", err)
|
||||
break
|
||||
}
|
||||
if vv.Match == ArgContent {
|
||||
modifiedArgString = strings.Join(argString, " ")
|
||||
return *args, true, createSplitString(modifiedArgString), modKeys
|
||||
}
|
||||
// Break early if current pos is the length of the array
|
||||
if currentPos == len(argString) {
|
||||
break
|
||||
}
|
||||
if vv.TypeGuard != String {
|
||||
var value string
|
||||
value, argString = findTypeGuard(strings.Join(argString, " "), argString, vv.TypeGuard)
|
||||
(*args)[v] = handleArgOption(value, *vv)
|
||||
} else if checkTypeGuard(argString[currentPos], vv.TypeGuard) {
|
||||
(*args)[v] = handleArgOption(argString[currentPos], *vv)
|
||||
currentPos++
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
//
|
||||
return *args, false, createSplitString(modifiedArgString), modKeys
|
||||
}
|
||||
|
||||
func findTypeGuard(input string, array []string, typeguard ArgTypeGuards) (string, []string) {
|
||||
switch typeguard {
|
||||
case Int:
|
||||
if match, isMatch := Misc["int"].FindStringMatch(input); isMatch == nil && match != nil {
|
||||
return match.String(), RemoveItem(array, match.String())
|
||||
}
|
||||
return "", array
|
||||
case Channel:
|
||||
if match, isMatch := MentionStringRegexes["channel"].FindStringMatch(input); isMatch == nil && match != nil {
|
||||
return match.String(), RemoveItem(array, match.String())
|
||||
} else if match, isMatch := MentionStringRegexes["id"].FindStringMatch(input); isMatch == nil && match != nil {
|
||||
return match.String(), RemoveItem(array, match.String())
|
||||
}
|
||||
return "", array
|
||||
case Role:
|
||||
if match, isMatch := MentionStringRegexes["role"].FindStringMatch(input); isMatch == nil && match != nil {
|
||||
return match.String(), RemoveItem(array, match.String())
|
||||
} else if match, isMatch := MentionStringRegexes["id"].FindStringMatch(input); isMatch == nil && match != nil {
|
||||
return match.String(), RemoveItem(array, match.String())
|
||||
}
|
||||
return "", array
|
||||
case User:
|
||||
if match, isMatch := MentionStringRegexes["user"].FindStringMatch(input); isMatch == nil && match != nil {
|
||||
return match.String(), RemoveItem(array, match.String())
|
||||
} else if match, isMatch := MentionStringRegexes["id"].FindStringMatch(input); isMatch == nil && match != nil {
|
||||
return match.String(), RemoveItem(array, match.String())
|
||||
}
|
||||
return "", array
|
||||
case ArrString:
|
||||
if match, isMatch := TypeGuard["arrString"].FindStringMatch(input); isMatch == nil && match != nil {
|
||||
return match.String(), RemoveItem(array, match.String())
|
||||
}
|
||||
return "", array
|
||||
case Message:
|
||||
if match, isMatch := TypeGuard["message_url"].FindStringMatch(input); isMatch == nil && match != nil {
|
||||
return match.String(), RemoveItem(array, match.String())
|
||||
}
|
||||
return "", array
|
||||
}
|
||||
return "", array
|
||||
}
|
||||
|
||||
func findAllFlags(argString string, keys []string, infoArgs *orderedmap.OrderedMap, args *Arguments) ([]string, Arguments, []string) {
|
||||
modifiedArgString := argString
|
||||
var indexes []int
|
||||
var modKeys []string
|
||||
for index, a := range keys {
|
||||
v, _ := infoArgs.Get(a)
|
||||
vv := v.(*ArgInfo)
|
||||
// Skip because the argument has no flag
|
||||
if !vv.Flag {
|
||||
continue
|
||||
}
|
||||
// Use the compiled regex to search the arg string for a matching result.
|
||||
match, err := vv.Regex.FindStringMatch(argString)
|
||||
// Error handling/no match
|
||||
if err != nil || match == nil {
|
||||
if vv.Match == ArgOption {
|
||||
(*args)[a] = handleArgOption(vv.DefaultOption, *vv)
|
||||
} else {
|
||||
(*args)[a] = CommandArg{info: *vv, Value: "false"}
|
||||
}
|
||||
// Set the modified arg string to the mod string
|
||||
indexes = append(indexes, index)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check to see if the flag is a string 'option' or a boolean 'flag'
|
||||
if vv.Match == ArgOption {
|
||||
val := strings.Trim(strings.SplitN(match.String(), " ", 2)[1], "\"")
|
||||
if checkTypeGuard(val, vv.TypeGuard) {
|
||||
(*args)[a] = handleArgOption(val, *vv)
|
||||
}
|
||||
} else if vv.Match == ArgFlag {
|
||||
(*args)[a] = CommandArg{info: *vv, Value: "true"}
|
||||
} // todo figure out if indexes need to put an else statement here
|
||||
|
||||
// Replace all reference to the flag in the string.
|
||||
modString, err := vv.Regex.Replace(modifiedArgString, "", -1, -1)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Set the modified arg string to the mod string
|
||||
modifiedArgString = modString
|
||||
indexes = append(indexes, index)
|
||||
}
|
||||
if len(indexes) > 0 {
|
||||
// set keys to nil if flags have already gotten all the args
|
||||
if len(indexes) == len(keys) {
|
||||
modKeys = nil
|
||||
return []string{}, *args, keys
|
||||
}
|
||||
modKeys = RemoveItems(keys, indexes)
|
||||
}
|
||||
if modifiedArgString == "" {
|
||||
modifiedArgString = argString
|
||||
}
|
||||
if len(modKeys) == 0 || modKeys == nil {
|
||||
modKeys = keys
|
||||
}
|
||||
return createSplitString(modifiedArgString), *args, modKeys
|
||||
}
|
||||
|
||||
// Creates a "split" string (array of strings that is split off of spaces
|
||||
func createSplitString(argString string) []string {
|
||||
splitStr := strings.SplitAfter(argString, " ")
|
||||
var newSplitStr []string
|
||||
quotedStringBuffer := ""
|
||||
isQuotedString := false
|
||||
for _, v := range splitStr {
|
||||
if v == "" || v == " " {
|
||||
continue
|
||||
}
|
||||
// Checks to see if the string is a quoted argument.
|
||||
// If so, it will combine it into one string
|
||||
if strings.Contains(v, "\"") || isQuotedString {
|
||||
if strings.HasSuffix(strings.Trim(v, " "), "\"") {
|
||||
// Trim quotes and trim space suffix
|
||||
quotedStringBuffer = strings.TrimSuffix(strings.Trim(quotedStringBuffer+strings.Trim(v, " "), "\""), " ")
|
||||
newSplitStr = append(newSplitStr, quotedStringBuffer)
|
||||
|
||||
isQuotedString = false
|
||||
quotedStringBuffer = ""
|
||||
continue
|
||||
}
|
||||
isQuotedString = true
|
||||
quotedStringBuffer = quotedStringBuffer + v
|
||||
continue
|
||||
} else {
|
||||
// If the string suffix contains a whitespace character, we need to remove that
|
||||
v = strings.TrimSuffix(v, " ")
|
||||
newSplitStr = append(newSplitStr, v)
|
||||
}
|
||||
}
|
||||
return newSplitStr
|
||||
}
|
||||
|
||||
func handleArgOption(str string, info ArgInfo) CommandArg {
|
||||
|
|
@ -309,8 +534,12 @@ func checkTypeGuard(str string, typeguard ArgTypeGuards) bool {
|
|||
return true
|
||||
}
|
||||
return false
|
||||
case Message:
|
||||
if isMatch, _ := TypeGuard["message_url"].MatchString(str); isMatch {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
|||
49
commands.go
49
commands.go
|
|
@ -2,19 +2,18 @@ package framework
|
|||
|
||||
import (
|
||||
"github.com/QPixel/orderedmap"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
// TODO Clean up this file
|
||||
// commands.go
|
||||
// This file contains everything required to add core commands to the bot, and parse commands from a message
|
||||
|
||||
// GroupTypes
|
||||
const (
|
||||
Moderation = "moderation"
|
||||
Module = "module"
|
||||
Utility = "utility"
|
||||
)
|
||||
|
||||
|
|
@ -26,8 +25,8 @@ type CommandInfo struct {
|
|||
Description string // A short description of what the command does
|
||||
Group string // The group this command belongs to
|
||||
ParentID string // The ID of the parent command
|
||||
Public bool // Whether or not non-admins and non-mods can use this command
|
||||
IsTyping bool // Whether or not the command will show a typing thing when ran.
|
||||
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
|
||||
|
|
@ -46,7 +45,7 @@ type Context struct {
|
|||
|
||||
// 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
|
||||
// Contexts are also passed as pointers, so they are not re-allocated when passed through
|
||||
type BotFunction func(ctx *Context)
|
||||
|
||||
// Command
|
||||
|
|
@ -56,6 +55,8 @@ type Command struct {
|
|||
Function BotFunction
|
||||
}
|
||||
|
||||
// ChildCommand
|
||||
// Defines how child commands are stored
|
||||
type ChildCommand map[string]map[string]Command
|
||||
|
||||
// CustomCommand
|
||||
|
|
@ -63,16 +64,16 @@ type ChildCommand map[string]map[string]Command
|
|||
type CustomCommand struct {
|
||||
Content string // The content of the custom command. Custom commands are just special strings after all
|
||||
InvokeCount int64 // How many times the command has been invoked; int64 for easier use with json
|
||||
Public bool // Whether or not non-admins and non-mods can use this command
|
||||
Public bool // Whether non-admins and non-mods can use this command
|
||||
}
|
||||
|
||||
// commands
|
||||
// All of the registered core commands (not custom 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 of the registered childcommands (subcmdgrps)
|
||||
// All the registered ChildCommands (SubCmdGrps)
|
||||
// This is private so other commands cannot modify it
|
||||
var childCommands = make(ChildCommand)
|
||||
|
||||
|
|
@ -81,7 +82,7 @@ var childCommands = make(ChildCommand)
|
|||
var commandAliases = make(map[string]string)
|
||||
|
||||
// slashCommands
|
||||
// All of the registered core commands that are also slash commands
|
||||
// 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)
|
||||
|
||||
|
|
@ -128,8 +129,16 @@ func AddChildCommand(info *CommandInfo, function BotFunction) {
|
|||
// 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
|
||||
|
|
@ -176,12 +185,6 @@ func commandHandler(session *discordgo.Session, message *discordgo.MessageCreate
|
|||
}
|
||||
}
|
||||
|
||||
// If we are in DMs, ignore the message
|
||||
// In the future, this can be used to handle special DM-only commands
|
||||
if channel.Type == discordgo.ChannelTypeDM {
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore messages sent by the bot
|
||||
if message.Author.ID == session.State.User.ID {
|
||||
return
|
||||
|
|
@ -250,6 +253,7 @@ func commandHandler(session *discordgo.Session, message *discordgo.MessageCreate
|
|||
if command.Info.IsTyping && g.Info.ResponseChannelId == "" {
|
||||
_ = Session.ChannelTyping(message.ChannelID)
|
||||
}
|
||||
defer handleCommandError(g.ID, channel.ID, message.Author.ID)
|
||||
if command.Info.IsParent {
|
||||
handleChildCommand(*argString, command, message.Message, g)
|
||||
return
|
||||
|
|
@ -312,3 +316,18 @@ func handleChildCommand(argString string, command Command, message *discordgo.Me
|
|||
})
|
||||
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)
|
||||
}
|
||||
_ = Session.ChannelMessageDelete(cId, message.ID)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,5 +20,11 @@ var (
|
|||
"channel": regexp2.MustCompile("<((#?\\d+))>", 0),
|
||||
"id": regexp2.MustCompile("^[0-9]{18}$", 0),
|
||||
}
|
||||
TypeGuard = regex{}
|
||||
TypeGuard = regex{
|
||||
"message_url": regexp2.MustCompile("((https:\\/\\/canary.discord.com\\/channels\\/)+([0-9]{18})\\/+([0-9]{18})\\/+([0-9]{18})$)", regexp2.IgnoreCase|regexp2.Multiline),
|
||||
}
|
||||
Misc = regex{
|
||||
"quoted_string": regexp2.MustCompile("^\"[a-zA-Z0-9]+\"$", 0),
|
||||
"int": regexp2.MustCompile("\\b(0*(?:[0-9]{1,8}))\\b", 0),
|
||||
}
|
||||
)
|
||||
|
|
|
|||
33
guilds.go
33
guilds.go
|
|
@ -17,7 +17,6 @@ import (
|
|||
type GuildInfo struct {
|
||||
AddedDate int64 `json:"addedDate"` // The date the bot was added to the server
|
||||
Prefix string `json:"prefix"` // The bot prefix
|
||||
GuildLanguage string `json:"guildLanguage"` // The guilds language
|
||||
ModeratorIds []string `json:"moderatorIds"` // The list of user/role IDs allowed to run mod-only commands
|
||||
WhitelistIds []string `json:"whitelistIds"` // List of user/role Ids that a user MUST have one of in order to run any commands, including public ones
|
||||
IgnoredIds []string `json:"ignoredIds"` // List of user/role IDs that can never run commands, even public ones
|
||||
|
|
@ -59,6 +58,30 @@ var muteLock = make(map[string]*sync.Mutex)
|
|||
// If the guild doesn't exist, initialize a new guild and save it before returning
|
||||
// Return a pointer to the guild object and pass that around instead, to avoid information desync
|
||||
func getGuild(guildId string) *Guild {
|
||||
// The command is being ran as a dm, send back an empty guild object with default fields
|
||||
if guildId == "" {
|
||||
return &Guild{
|
||||
ID: "",
|
||||
Info: GuildInfo{
|
||||
AddedDate: time.Now().Unix(),
|
||||
Prefix: "!",
|
||||
DeletePolicy: false,
|
||||
ResponseChannelId: "",
|
||||
MuteRoleId: "",
|
||||
GlobalDisabledTriggers: nil,
|
||||
ChannelDisabledTriggers: make(map[string][]string),
|
||||
CustomCommands: make(map[string]CustomCommand),
|
||||
ModeratorIds: nil,
|
||||
IgnoredIds: nil,
|
||||
BannedWordDetector: false,
|
||||
GuildBannedWords: nil,
|
||||
BannedWordDetectorRoles: nil,
|
||||
BannedWordDetectorChannels: nil,
|
||||
MutedUsers: make(map[string]int64),
|
||||
Storage: make(map[string]interface{}),
|
||||
},
|
||||
}
|
||||
}
|
||||
if guild, ok := Guilds[guildId]; ok {
|
||||
return guild
|
||||
} else {
|
||||
|
|
@ -67,7 +90,6 @@ func getGuild(guildId string) *Guild {
|
|||
ID: guildId,
|
||||
Info: GuildInfo{
|
||||
AddedDate: time.Now().Unix(),
|
||||
GuildLanguage: "en",
|
||||
Prefix: "!",
|
||||
DeletePolicy: false,
|
||||
ResponseChannelId: "",
|
||||
|
|
@ -255,13 +277,6 @@ func (g *Guild) SetPrefix(newPrefix string) {
|
|||
g.save()
|
||||
}
|
||||
|
||||
// SetLang
|
||||
// Set the prefix, then save the guild data
|
||||
func (g *Guild) SetLang(lang string) {
|
||||
g.Info.GuildLanguage = lang
|
||||
g.save()
|
||||
}
|
||||
|
||||
// IsMod
|
||||
// Check if a given ID is a moderator or not
|
||||
func (g *Guild) IsMod(checkId string) bool {
|
||||
|
|
|
|||
101
interaction.go
101
interaction.go
|
|
@ -1,20 +1,16 @@
|
|||
package framework
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
//
|
||||
//// TODO clean up this file and move interaction specific functions here
|
||||
//
|
||||
//import (
|
||||
// "github.com/bwmarrin/discordgo"
|
||||
// "strings"
|
||||
//)
|
||||
//
|
||||
//// slashCommandTypes
|
||||
//// A map of *short hand* slash commands types to their discordgo counterparts
|
||||
//// TODO move this over to interaction.go
|
||||
// -- Types and Structs --
|
||||
|
||||
// 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,
|
||||
|
|
@ -26,6 +22,8 @@ var slashCommandTypes = map[ArgTypeGuards]discordgo.ApplicationCommandOptionType
|
|||
//SubCmdGrp: discordgo.ApplicationCommandOptionSubCommandGroup,
|
||||
}
|
||||
|
||||
//var componentHandlers
|
||||
|
||||
//
|
||||
// getSlashCommandStruct
|
||||
// Creates a slash command struct
|
||||
|
|
@ -66,6 +64,29 @@ func createSlashCommandStruct(info *CommandInfo) (st *discordgo.ApplicationComma
|
|||
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 {
|
||||
|
||||
} 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 --
|
||||
|
||||
// handleInteraction
|
||||
|
|
@ -84,6 +105,39 @@ func handleInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|||
// handleInteractionCommand
|
||||
// Handles a slash command
|
||||
func handleInteractionCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if i.ApplicationCommandData().Name == "rickroll-em" {
|
||||
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: "Operation rickroll has begun",
|
||||
Flags: 1 << 6,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ch, err := s.UserChannelCreate(
|
||||
i.ApplicationCommandData().TargetID,
|
||||
)
|
||||
if err != nil {
|
||||
_, err = s.FollowupMessageCreate(Session.State.User.ID, i.Interaction, true, &discordgo.WebhookParams{
|
||||
Content: fmt.Sprintf("Mission failed. Cannot send a message to this user: %q", err.Error()),
|
||||
Flags: 1 << 6,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
_, err = s.ChannelMessageSend(
|
||||
ch.ID,
|
||||
fmt.Sprintf("%s sent you this: https://youtu.be/dQw4w9WgXcQ", i.Member.Mention()),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
g := getGuild(i.GuildID)
|
||||
|
||||
if g.Info.DeletePolicy {
|
||||
|
|
@ -121,6 +175,7 @@ func handleInteractionCommand(s *discordgo.Session, i *discordgo.InteractionCrea
|
|||
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.Function(&Context{
|
||||
Guild: g,
|
||||
Cmd: command.Info,
|
||||
|
|
@ -204,3 +259,27 @@ func RemoveGuildSlashCommands(guildID string) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleSlashCommandError(i discordgo.Interaction) {
|
||||
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))
|
||||
message, err := Session.InteractionResponseEdit(Session.State.User.ID, &i, &discordgo.WebhookEdit{
|
||||
Content: "error executing command",
|
||||
})
|
||||
if err != nil {
|
||||
Session.InteractionRespond(&i, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Flags: 1 << 6,
|
||||
Content: "error executing command",
|
||||
},
|
||||
})
|
||||
log.Errorf("err sending message %s", err)
|
||||
}
|
||||
Session.ChannelMessageDelete(i.ChannelID, message.ID)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
79
response.go
79
response.go
|
|
@ -1,6 +1,7 @@
|
|||
package framework
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
|
|
@ -24,6 +25,7 @@ type Response struct {
|
|||
Success bool
|
||||
Loading bool
|
||||
Ephemeral bool
|
||||
Reply bool
|
||||
Embed *discordgo.MessageEmbed
|
||||
ResponseComponents *ResponseComponents
|
||||
}
|
||||
|
|
@ -71,6 +73,7 @@ func NewResponse(ctx *Context, messageComponents bool, ephemeral bool) *Response
|
|||
},
|
||||
Loading: ctx.Cmd.IsTyping,
|
||||
Ephemeral: ephemeral,
|
||||
Reply: ephemeral,
|
||||
}
|
||||
if messageComponents {
|
||||
r.ResponseComponents.Components = CreateComponentFields()
|
||||
|
|
@ -90,8 +93,74 @@ func NewResponse(ctx *Context, messageComponents bool, ephemeral bool) *Response
|
|||
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
// If the command context is not empty, append the command
|
||||
if ctx.Cmd.Trigger != "" {
|
||||
// Get the command used as a string, and all interpreted arguments, so it can be a part of the output
|
||||
commandUsed := ""
|
||||
if r.Ctx.Cmd.IsChild {
|
||||
commandUsed = fmt.Sprintf("%s%s %s", r.Ctx.Guild.Info.Prefix, r.Ctx.Cmd.ParentID, r.Ctx.Cmd.Trigger)
|
||||
} else {
|
||||
commandUsed = r.Ctx.Guild.Info.Prefix + r.Ctx.Cmd.Trigger
|
||||
}
|
||||
// Just makes the thing prettier
|
||||
if ctx.Interaction != nil {
|
||||
commandUsed = "/" + r.Ctx.Cmd.Trigger
|
||||
}
|
||||
for _, k := range r.Ctx.Cmd.Arguments.Keys() {
|
||||
arg := ctx.Args[k]
|
||||
if arg.StringValue() == "" {
|
||||
continue
|
||||
}
|
||||
vv, ok := r.Ctx.Cmd.Arguments.Get(k)
|
||||
|
||||
if ok {
|
||||
argInfo := vv.(*ArgInfo)
|
||||
switch argInfo.TypeGuard {
|
||||
case Int:
|
||||
fallthrough
|
||||
case Boolean:
|
||||
fallthrough
|
||||
case String:
|
||||
commandUsed += " " + arg.StringValue()
|
||||
break
|
||||
case User:
|
||||
user, err := arg.UserValue(Session)
|
||||
if err != nil {
|
||||
commandUsed += " " + arg.StringValue()
|
||||
} else {
|
||||
commandUsed += " " + user.Mention()
|
||||
}
|
||||
case Role:
|
||||
role, err := arg.RoleValue(Session, r.Ctx.Guild.ID)
|
||||
if err != nil {
|
||||
commandUsed += " " + arg.StringValue()
|
||||
} else {
|
||||
commandUsed += " " + role.Mention()
|
||||
}
|
||||
case Channel:
|
||||
channel, err := arg.ChannelValue(Session)
|
||||
if err != nil {
|
||||
commandUsed += " " + arg.StringValue()
|
||||
} else {
|
||||
commandUsed += " " + channel.Mention()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
commandUsed += " " + arg.StringValue()
|
||||
}
|
||||
}
|
||||
|
||||
commandUsed = "```\n" + commandUsed + "\n```"
|
||||
|
||||
r.AppendField("Command used:", commandUsed, false)
|
||||
}
|
||||
|
||||
// If the message is not nil, append an invoker
|
||||
if ctx.Message != nil {
|
||||
r.AppendField("Invoked by:", r.Ctx.Message.Author.Mention(), false)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
|
|
@ -317,7 +386,7 @@ func (r *Response) Send(success bool, title string, description string) {
|
|||
Embed: r.Embed,
|
||||
Components: r.ResponseComponents.Components,
|
||||
})
|
||||
if err != nil {
|
||||
if err != nil && r.Reply {
|
||||
// Reply to user if no output channel
|
||||
_, err = ReplyToUser(r.Ctx.Message.ChannelID, &discordgo.MessageSend{
|
||||
Embed: r.Embed,
|
||||
|
|
@ -334,6 +403,12 @@ func (r *Response) Send(success bool, title string, description string) {
|
|||
if err != nil {
|
||||
SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Message.ChannelID, r.Ctx.Message.Author.ID, "Ultimately failed to send bot response", err)
|
||||
}
|
||||
} else if !r.Reply {
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
53
util.go
53
util.go
|
|
@ -3,12 +3,12 @@ package framework
|
|||
import (
|
||||
"errors"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/dlclark/regexp2"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// language.go
|
||||
// util.go
|
||||
// This file contains utility functions, simplifying redundant tasks
|
||||
|
||||
// RemoveItem
|
||||
|
|
@ -23,45 +23,43 @@ func RemoveItem(slice []string, delete string) []string {
|
|||
return newSlice
|
||||
}
|
||||
|
||||
// RemoveItems
|
||||
// Removes items from a slice by index
|
||||
func RemoveItems(slice []string, indexes []int) []string {
|
||||
newSlice := make([]string, len(slice))
|
||||
copy(newSlice, slice)
|
||||
for _, v := range indexes {
|
||||
newSlice[v] = newSlice[len(newSlice)-1]
|
||||
newSlice[len(newSlice)-1] = ""
|
||||
newSlice = newSlice[:len(newSlice)-1]
|
||||
}
|
||||
return newSlice
|
||||
}
|
||||
|
||||
// EnsureNumbers
|
||||
// Given a string, ensure it contains only numbers
|
||||
// This is useful for stripping letters and formatting characters from user/role pings
|
||||
func EnsureNumbers(in string) string {
|
||||
reg, err := regexp2.Compile("[^0-9]+", 0)
|
||||
reg, err := regexp.Compile("[^0-9]+")
|
||||
if err != nil {
|
||||
log.Errorf("An unrecoverable error occurred when compiling a regex expression: %s", err)
|
||||
return ""
|
||||
}
|
||||
if ok, _ := reg.MatchString(in); ok {
|
||||
str, err := reg.Replace(in, "", -1, -1)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to replace text in EnsureNumbers")
|
||||
return ""
|
||||
}
|
||||
return str
|
||||
}
|
||||
return in
|
||||
|
||||
return reg.ReplaceAllString(in, "")
|
||||
}
|
||||
|
||||
// EnsureLetters
|
||||
// Given a string, ensure it contains only letters
|
||||
// This is useful for stripping numbers from mute durations, and possibly other things
|
||||
func EnsureLetters(in string) string {
|
||||
reg, err := regexp2.Compile("[^a-zA-Z]+", 0)
|
||||
reg, err := regexp.Compile("[^a-zA-Z]+")
|
||||
if err != nil {
|
||||
log.Errorf("An unrecoverable error occurred when compiling a regex expression: %s", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
if ok, _ := reg.MatchString(in); ok {
|
||||
str, err := reg.Replace(in, "", -1, -1)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to replace text in EnsureLetters")
|
||||
return ""
|
||||
}
|
||||
return str
|
||||
}
|
||||
return in
|
||||
return reg.ReplaceAllString(in, "")
|
||||
}
|
||||
|
||||
// CleanId
|
||||
|
|
@ -140,15 +138,8 @@ func ExtractCommand(guild *GuildInfo, message string) (*string, *string) {
|
|||
if content == "" {
|
||||
return nil, nil
|
||||
}
|
||||
// Attempt to pull the trigger out of the command content by splitting on spaces
|
||||
trigger := strings.Fields(content)[0]
|
||||
|
||||
// With the trigger identified, split the command content on the trigger to obtain everything BUT the trigger
|
||||
// Ensure only 2 fields are returned so it can be split further. Then, get only the second field
|
||||
trigger := strings.ToLower(strings.Fields(content)[0])
|
||||
fullArgs := strings.SplitN(content, trigger, 2)[1]
|
||||
fullArgs = strings.TrimPrefix(fullArgs, " ")
|
||||
// Avoids issues with strings that are case sensitive
|
||||
trigger = strings.ToLower(trigger)
|
||||
return &trigger, &fullArgs
|
||||
} else {
|
||||
return nil, nil
|
||||
|
|
@ -190,7 +181,7 @@ func SendErrorReport(guildId string, channelId string, userId string, title stri
|
|||
log.Errorf("[REPORT] %s (%s)", title, err)
|
||||
|
||||
// Iterate through all the admins
|
||||
for admin, _ := range botAdmins {
|
||||
for admin := range botAdmins {
|
||||
|
||||
// Get the channel ID of the user to DM
|
||||
dmChannel, dmCreateErr := Session.UserChannelCreate(admin)
|
||||
|
|
|
|||
Loading…
Reference in New Issue