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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/QPixel/orderedmap"
|
"github.com/QPixel/orderedmap"
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
"github.com/dlclark/regexp2"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
@ -36,7 +38,10 @@ var (
|
||||||
Channel ArgTypeGuards = "channel"
|
Channel ArgTypeGuards = "channel"
|
||||||
User ArgTypeGuards = "user"
|
User ArgTypeGuards = "user"
|
||||||
Role ArgTypeGuards = "role"
|
Role ArgTypeGuards = "role"
|
||||||
|
GuildArg ArgTypeGuards = "guild"
|
||||||
|
Message ArgTypeGuards = "message"
|
||||||
Boolean ArgTypeGuards = "bool"
|
Boolean ArgTypeGuards = "bool"
|
||||||
|
Id ArgTypeGuards = "id"
|
||||||
SubCmd ArgTypeGuards = "subcmd"
|
SubCmd ArgTypeGuards = "subcmd"
|
||||||
SubCmdGrp ArgTypeGuards = "subcmdgrp"
|
SubCmdGrp ArgTypeGuards = "subcmdgrp"
|
||||||
ArrString ArgTypeGuards = "arrString"
|
ArrString ArgTypeGuards = "arrString"
|
||||||
|
|
@ -52,7 +57,7 @@ type ArgInfo struct {
|
||||||
Flag bool
|
Flag bool
|
||||||
DefaultOption string
|
DefaultOption string
|
||||||
Choices []string
|
Choices []string
|
||||||
Regex string
|
Regex *regexp2.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommandArg
|
// CommandArg
|
||||||
|
|
@ -113,14 +118,28 @@ func (cI *CommandInfo) AddArg(argument string, typeGuard ArgTypeGuards, match Ar
|
||||||
Match: match,
|
Match: match,
|
||||||
DefaultOption: defaultOption,
|
DefaultOption: defaultOption,
|
||||||
Choices: nil,
|
Choices: nil,
|
||||||
Regex: "",
|
Regex: nil,
|
||||||
})
|
})
|
||||||
return cI
|
return cI
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddFlagArg
|
// AddFlagArg
|
||||||
// Adds a flag arg, which is a special type of argument
|
// 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 {
|
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{
|
cI.Arguments.Set(flag, &ArgInfo{
|
||||||
Description: description,
|
Description: description,
|
||||||
Required: required,
|
Required: required,
|
||||||
|
|
@ -128,7 +147,7 @@ func (cI *CommandInfo) AddFlagArg(flag string, typeGuard ArgTypeGuards, match Ar
|
||||||
Match: match,
|
Match: match,
|
||||||
TypeGuard: typeGuard,
|
TypeGuard: typeGuard,
|
||||||
DefaultOption: defaultOption,
|
DefaultOption: defaultOption,
|
||||||
Regex: "",
|
Regex: regex,
|
||||||
})
|
})
|
||||||
return cI
|
return cI
|
||||||
}
|
}
|
||||||
|
|
@ -153,87 +172,61 @@ func (cI *CommandInfo) SetTyping(isTyping bool) *CommandInfo {
|
||||||
return cI
|
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 --
|
// -- Argument Parser --
|
||||||
|
|
||||||
// ParseArguments
|
// 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 {
|
func ParseArguments(args string, infoArgs *orderedmap.OrderedMap) *Arguments {
|
||||||
ar := make(Arguments)
|
ar := make(Arguments)
|
||||||
|
|
||||||
if args == "" || len(infoArgs.Keys()) < 1 {
|
if args == "" || len(infoArgs.Keys()) < 1 {
|
||||||
return &ar
|
return &ar
|
||||||
}
|
}
|
||||||
// Split string on spaces to get every "phrase"
|
// 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
|
// Keys of infoArgs
|
||||||
k := infoArgs.Keys()
|
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++ {
|
ar, moreContent, splitString, modK = findAllOptionArgs(splitString, modK, infoArgs, &ar)
|
||||||
for n := currentPos; n <= len(k); n++ {
|
|
||||||
if n > len(k)+1 || currentPos+1 > len(k) {
|
// If there is more content, lets find it
|
||||||
break
|
if moreContent == true {
|
||||||
|
v, ok := infoArgs.Get(modK[0])
|
||||||
|
if !ok {
|
||||||
|
return &ar
|
||||||
}
|
}
|
||||||
v, _ := infoArgs.Get(k[currentPos])
|
|
||||||
vv := v.(*ArgInfo)
|
vv := v.(*ArgInfo)
|
||||||
switch vv.Match {
|
commandContent, _ := createContentString(splitString, 0)
|
||||||
case ArgOption:
|
ar[modK[0]] = CommandArg{
|
||||||
// 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{
|
|
||||||
info: *vv,
|
info: *vv,
|
||||||
Value: vv.DefaultOption,
|
Value: commandContent,
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return &ar
|
return &ar
|
||||||
|
// Else return the args struct
|
||||||
|
} else {
|
||||||
|
return &ar
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Argument Parsing Helpers */
|
/* Argument Parsing Helpers */
|
||||||
|
|
@ -247,22 +240,254 @@ func createContentString(splitString []string, currentPos int) (string, int) {
|
||||||
return strings.TrimSuffix(str, " "), currentPos
|
return strings.TrimSuffix(str, " "), currentPos
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleQuotedString(splitString []string, argInfo ArgInfo, currentPos int) (CommandArg, int) {
|
// Finds all the 'option' type args
|
||||||
str := ""
|
func findAllOptionArgs(argString []string, keys []string, infoArgs *orderedmap.OrderedMap, args *Arguments) (Arguments, bool, []string, []string) {
|
||||||
splitString[currentPos] = strings.TrimPrefix(splitString[currentPos], "\"")
|
if len(keys) == 0 || keys == nil {
|
||||||
for i := currentPos; i < len(splitString); i++ {
|
return *args, false, []string{}, []string{}
|
||||||
if !strings.HasSuffix(splitString[i], "\"") {
|
}
|
||||||
str += splitString[i] + " "
|
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 {
|
} else {
|
||||||
str += strings.TrimSuffix(splitString[i], "\"")
|
|
||||||
currentPos = i
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return CommandArg{
|
// Remove already found keys and clear the index list
|
||||||
info: argInfo,
|
// We also reset some values that we reuse
|
||||||
Value: str,
|
//if
|
||||||
}, currentPos
|
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 {
|
func handleArgOption(str string, info ArgInfo) CommandArg {
|
||||||
|
|
@ -309,8 +534,12 @@ func checkTypeGuard(str string, typeguard ArgTypeGuards) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
case Message:
|
||||||
|
if isMatch, _ := TypeGuard["message_url"].MatchString(str); isMatch {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
49
commands.go
49
commands.go
|
|
@ -2,19 +2,18 @@ package framework
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/QPixel/orderedmap"
|
"github.com/QPixel/orderedmap"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO Clean up this file
|
|
||||||
// commands.go
|
// commands.go
|
||||||
// This file contains everything required to add core commands to the bot, and parse commands from a message
|
// This file contains everything required to add core commands to the bot, and parse commands from a message
|
||||||
|
|
||||||
// GroupTypes
|
// GroupTypes
|
||||||
const (
|
const (
|
||||||
Moderation = "moderation"
|
Moderation = "moderation"
|
||||||
Module = "module"
|
|
||||||
Utility = "utility"
|
Utility = "utility"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -26,8 +25,8 @@ type CommandInfo struct {
|
||||||
Description string // A short description of what the command does
|
Description string // A short description of what the command does
|
||||||
Group string // The group this command belongs to
|
Group string // The group this command belongs to
|
||||||
ParentID string // The ID of the parent command
|
ParentID string // The ID of the parent command
|
||||||
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
|
||||||
IsTyping bool // Whether or not the command will show a typing thing when ran.
|
IsTyping bool // Whether the command will show a typing thing when ran.
|
||||||
IsParent bool // If the command is the parent of a subcommand tree
|
IsParent bool // If the command is the parent of a subcommand tree
|
||||||
IsChild bool // If the command is the child
|
IsChild bool // If the command is the child
|
||||||
Trigger string // The string that will trigger the command
|
Trigger string // The string that will trigger the command
|
||||||
|
|
@ -46,7 +45,7 @@ type Context struct {
|
||||||
|
|
||||||
// BotFunction
|
// BotFunction
|
||||||
// This type defines the functions that are called when commands are triggered
|
// 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)
|
type BotFunction func(ctx *Context)
|
||||||
|
|
||||||
// Command
|
// Command
|
||||||
|
|
@ -56,6 +55,8 @@ type Command struct {
|
||||||
Function BotFunction
|
Function BotFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChildCommand
|
||||||
|
// Defines how child commands are stored
|
||||||
type ChildCommand map[string]map[string]Command
|
type ChildCommand map[string]map[string]Command
|
||||||
|
|
||||||
// CustomCommand
|
// CustomCommand
|
||||||
|
|
@ -63,16 +64,16 @@ type ChildCommand map[string]map[string]Command
|
||||||
type CustomCommand struct {
|
type CustomCommand struct {
|
||||||
Content string // The content of the custom command. Custom commands are just special strings after all
|
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
|
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
|
// 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
|
// This is private so that other commands cannot modify it
|
||||||
var commands = make(map[string]Command)
|
var commands = make(map[string]Command)
|
||||||
|
|
||||||
// childCommands
|
// childCommands
|
||||||
// All of the registered childcommands (subcmdgrps)
|
// All the registered ChildCommands (SubCmdGrps)
|
||||||
// This is private so other commands cannot modify it
|
// This is private so other commands cannot modify it
|
||||||
var childCommands = make(ChildCommand)
|
var childCommands = make(ChildCommand)
|
||||||
|
|
||||||
|
|
@ -81,7 +82,7 @@ var childCommands = make(ChildCommand)
|
||||||
var commandAliases = make(map[string]string)
|
var commandAliases = make(map[string]string)
|
||||||
|
|
||||||
// slashCommands
|
// 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
|
// This is also private so other commands cannot modify it
|
||||||
var slashCommands = make(map[string]discordgo.ApplicationCommand)
|
var slashCommands = make(map[string]discordgo.ApplicationCommand)
|
||||||
|
|
||||||
|
|
@ -128,8 +129,16 @@ func AddChildCommand(info *CommandInfo, function BotFunction) {
|
||||||
// Adds a slash command to the bot
|
// Adds a slash command to the bot
|
||||||
// Allows for separation between normal commands and slash commands
|
// Allows for separation between normal commands and slash commands
|
||||||
func AddSlashCommand(info *CommandInfo) {
|
func AddSlashCommand(info *CommandInfo) {
|
||||||
|
if !info.IsParent || !info.IsChild {
|
||||||
s := createSlashCommandStruct(info)
|
s := createSlashCommandStruct(info)
|
||||||
slashCommands[strings.ToLower(info.Trigger)] = *s
|
slashCommands[strings.ToLower(info.Trigger)] = *s
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if info.IsParent {
|
||||||
|
s := createSlashSubCmdStruct(info, childCommands[info.Trigger])
|
||||||
|
slashCommands[strings.ToLower(info.Trigger)] = *s
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSlashCommands
|
// 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
|
// Ignore messages sent by the bot
|
||||||
if message.Author.ID == session.State.User.ID {
|
if message.Author.ID == session.State.User.ID {
|
||||||
return
|
return
|
||||||
|
|
@ -250,6 +253,7 @@ func commandHandler(session *discordgo.Session, message *discordgo.MessageCreate
|
||||||
if command.Info.IsTyping && g.Info.ResponseChannelId == "" {
|
if command.Info.IsTyping && g.Info.ResponseChannelId == "" {
|
||||||
_ = Session.ChannelTyping(message.ChannelID)
|
_ = Session.ChannelTyping(message.ChannelID)
|
||||||
}
|
}
|
||||||
|
defer handleCommandError(g.ID, channel.ID, message.Author.ID)
|
||||||
if command.Info.IsParent {
|
if command.Info.IsParent {
|
||||||
handleChildCommand(*argString, command, message.Message, g)
|
handleChildCommand(*argString, command, message.Message, g)
|
||||||
return
|
return
|
||||||
|
|
@ -312,3 +316,18 @@ func handleChildCommand(argString string, command Command, message *discordgo.Me
|
||||||
})
|
})
|
||||||
return
|
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),
|
"channel": regexp2.MustCompile("<((#?\\d+))>", 0),
|
||||||
"id": regexp2.MustCompile("^[0-9]{18}$", 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 {
|
type GuildInfo struct {
|
||||||
AddedDate int64 `json:"addedDate"` // The date the bot was added to the server
|
AddedDate int64 `json:"addedDate"` // The date the bot was added to the server
|
||||||
Prefix string `json:"prefix"` // The bot prefix
|
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
|
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
|
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
|
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
|
// 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
|
// Return a pointer to the guild object and pass that around instead, to avoid information desync
|
||||||
func getGuild(guildId string) *Guild {
|
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 {
|
if guild, ok := Guilds[guildId]; ok {
|
||||||
return guild
|
return guild
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -67,7 +90,6 @@ func getGuild(guildId string) *Guild {
|
||||||
ID: guildId,
|
ID: guildId,
|
||||||
Info: GuildInfo{
|
Info: GuildInfo{
|
||||||
AddedDate: time.Now().Unix(),
|
AddedDate: time.Now().Unix(),
|
||||||
GuildLanguage: "en",
|
|
||||||
Prefix: "!",
|
Prefix: "!",
|
||||||
DeletePolicy: false,
|
DeletePolicy: false,
|
||||||
ResponseChannelId: "",
|
ResponseChannelId: "",
|
||||||
|
|
@ -255,13 +277,6 @@ func (g *Guild) SetPrefix(newPrefix string) {
|
||||||
g.save()
|
g.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLang
|
|
||||||
// Set the prefix, then save the guild data
|
|
||||||
func (g *Guild) SetLang(lang string) {
|
|
||||||
g.Info.GuildLanguage = lang
|
|
||||||
g.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsMod
|
// IsMod
|
||||||
// Check if a given ID is a moderator or not
|
// Check if a given ID is a moderator or not
|
||||||
func (g *Guild) IsMod(checkId string) bool {
|
func (g *Guild) IsMod(checkId string) bool {
|
||||||
|
|
|
||||||
101
interaction.go
101
interaction.go
|
|
@ -1,20 +1,16 @@
|
||||||
package framework
|
package framework
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
//
|
// -- Types and Structs --
|
||||||
//// TODO clean up this file and move interaction specific functions here
|
|
||||||
//
|
// slashCommandTypes
|
||||||
//import (
|
// A map of *short hand* slash commands types to their discordgo counterparts
|
||||||
// "github.com/bwmarrin/discordgo"
|
// TODO move this over to interaction.go
|
||||||
// "strings"
|
|
||||||
//)
|
|
||||||
//
|
|
||||||
//// 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{
|
var slashCommandTypes = map[ArgTypeGuards]discordgo.ApplicationCommandOptionType{
|
||||||
Int: discordgo.ApplicationCommandOptionInteger,
|
Int: discordgo.ApplicationCommandOptionInteger,
|
||||||
String: discordgo.ApplicationCommandOptionString,
|
String: discordgo.ApplicationCommandOptionString,
|
||||||
|
|
@ -26,6 +22,8 @@ var slashCommandTypes = map[ArgTypeGuards]discordgo.ApplicationCommandOptionType
|
||||||
//SubCmdGrp: discordgo.ApplicationCommandOptionSubCommandGroup,
|
//SubCmdGrp: discordgo.ApplicationCommandOptionSubCommandGroup,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//var componentHandlers
|
||||||
|
|
||||||
//
|
//
|
||||||
// getSlashCommandStruct
|
// getSlashCommandStruct
|
||||||
// Creates a slash command struct
|
// Creates a slash command struct
|
||||||
|
|
@ -66,6 +64,29 @@ func createSlashCommandStruct(info *CommandInfo) (st *discordgo.ApplicationComma
|
||||||
return
|
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 --
|
// -- Interaction Handlers --
|
||||||
|
|
||||||
// handleInteraction
|
// handleInteraction
|
||||||
|
|
@ -84,6 +105,39 @@ func handleInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
// handleInteractionCommand
|
// handleInteractionCommand
|
||||||
// Handles a slash command
|
// Handles a slash command
|
||||||
func handleInteractionCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
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)
|
g := getGuild(i.GuildID)
|
||||||
|
|
||||||
if g.Info.DeletePolicy {
|
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) {
|
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
|
// Check if the command is public, or if the current user is a bot moderator
|
||||||
// Bot admins supercede both checks
|
// Bot admins supercede both checks
|
||||||
|
defer handleSlashCommandError(*i.Interaction)
|
||||||
command.Function(&Context{
|
command.Function(&Context{
|
||||||
Guild: g,
|
Guild: g,
|
||||||
Cmd: command.Info,
|
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
|
package framework
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
|
@ -24,6 +25,7 @@ type Response struct {
|
||||||
Success bool
|
Success bool
|
||||||
Loading bool
|
Loading bool
|
||||||
Ephemeral bool
|
Ephemeral bool
|
||||||
|
Reply bool
|
||||||
Embed *discordgo.MessageEmbed
|
Embed *discordgo.MessageEmbed
|
||||||
ResponseComponents *ResponseComponents
|
ResponseComponents *ResponseComponents
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +73,7 @@ func NewResponse(ctx *Context, messageComponents bool, ephemeral bool) *Response
|
||||||
},
|
},
|
||||||
Loading: ctx.Cmd.IsTyping,
|
Loading: ctx.Cmd.IsTyping,
|
||||||
Ephemeral: ephemeral,
|
Ephemeral: ephemeral,
|
||||||
|
Reply: ephemeral,
|
||||||
}
|
}
|
||||||
if messageComponents {
|
if messageComponents {
|
||||||
r.ResponseComponents.Components = CreateComponentFields()
|
r.ResponseComponents.Components = CreateComponentFields()
|
||||||
|
|
@ -90,8 +93,74 @@ func NewResponse(ctx *Context, messageComponents bool, ephemeral bool) *Response
|
||||||
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
|
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
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,7 +386,7 @@ func (r *Response) Send(success bool, title string, description string) {
|
||||||
Embed: r.Embed,
|
Embed: r.Embed,
|
||||||
Components: r.ResponseComponents.Components,
|
Components: r.ResponseComponents.Components,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil && r.Reply {
|
||||||
// Reply to user if no output channel
|
// Reply to user if no output channel
|
||||||
_, err = ReplyToUser(r.Ctx.Message.ChannelID, &discordgo.MessageSend{
|
_, err = ReplyToUser(r.Ctx.Message.ChannelID, &discordgo.MessageSend{
|
||||||
Embed: r.Embed,
|
Embed: r.Embed,
|
||||||
|
|
@ -334,6 +403,12 @@ func (r *Response) Send(success bool, title string, description string) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Message.ChannelID, r.Ctx.Message.Author.ID, "Ultimately failed to send bot response", err)
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
"github.com/dlclark/regexp2"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// language.go
|
// util.go
|
||||||
// This file contains utility functions, simplifying redundant tasks
|
// This file contains utility functions, simplifying redundant tasks
|
||||||
|
|
||||||
// RemoveItem
|
// RemoveItem
|
||||||
|
|
@ -23,45 +23,43 @@ func RemoveItem(slice []string, delete string) []string {
|
||||||
return newSlice
|
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
|
// EnsureNumbers
|
||||||
// Given a string, ensure it contains only numbers
|
// Given a string, ensure it contains only numbers
|
||||||
// This is useful for stripping letters and formatting characters from user/role pings
|
// This is useful for stripping letters and formatting characters from user/role pings
|
||||||
func EnsureNumbers(in string) string {
|
func EnsureNumbers(in string) string {
|
||||||
reg, err := regexp2.Compile("[^0-9]+", 0)
|
reg, err := regexp.Compile("[^0-9]+")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("An unrecoverable error occurred when compiling a regex expression: %s", err)
|
log.Errorf("An unrecoverable error occurred when compiling a regex expression: %s", err)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if ok, _ := reg.MatchString(in); ok {
|
|
||||||
str, err := reg.Replace(in, "", -1, -1)
|
return reg.ReplaceAllString(in, "")
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Unable to replace text in EnsureNumbers")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
return in
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureLetters
|
// EnsureLetters
|
||||||
// Given a string, ensure it contains only letters
|
// Given a string, ensure it contains only letters
|
||||||
// This is useful for stripping numbers from mute durations, and possibly other things
|
// This is useful for stripping numbers from mute durations, and possibly other things
|
||||||
func EnsureLetters(in string) string {
|
func EnsureLetters(in string) string {
|
||||||
reg, err := regexp2.Compile("[^a-zA-Z]+", 0)
|
reg, err := regexp.Compile("[^a-zA-Z]+")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("An unrecoverable error occurred when compiling a regex expression: %s", err)
|
log.Errorf("An unrecoverable error occurred when compiling a regex expression: %s", err)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, _ := reg.MatchString(in); ok {
|
return reg.ReplaceAllString(in, "")
|
||||||
str, err := reg.Replace(in, "", -1, -1)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Unable to replace text in EnsureLetters")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
return in
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanId
|
// CleanId
|
||||||
|
|
@ -140,15 +138,8 @@ func ExtractCommand(guild *GuildInfo, message string) (*string, *string) {
|
||||||
if content == "" {
|
if content == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
// Attempt to pull the trigger out of the command content by splitting on spaces
|
trigger := strings.ToLower(strings.Fields(content)[0])
|
||||||
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
|
|
||||||
fullArgs := strings.SplitN(content, trigger, 2)[1]
|
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
|
return &trigger, &fullArgs
|
||||||
} else {
|
} else {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
@ -190,7 +181,7 @@ func SendErrorReport(guildId string, channelId string, userId string, title stri
|
||||||
log.Errorf("[REPORT] %s (%s)", title, err)
|
log.Errorf("[REPORT] %s (%s)", title, err)
|
||||||
|
|
||||||
// Iterate through all the admins
|
// Iterate through all the admins
|
||||||
for admin, _ := range botAdmins {
|
for admin := range botAdmins {
|
||||||
|
|
||||||
// Get the channel ID of the user to DM
|
// Get the channel ID of the user to DM
|
||||||
dmChannel, dmCreateErr := Session.UserChannelCreate(admin)
|
dmChannel, dmCreateErr := Session.UserChannelCreate(admin)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue