framework/arguments.go

756 lines
20 KiB
Go

package framework
import (
"errors"
"fmt"
"github.com/QPixel/orderedmap"
"github.com/bwmarrin/discordgo"
"github.com/dlclark/regexp2"
"strconv"
"strings"
)
// Arguments.go
// File for all argument based functions which includes: parsing, creating, and more
// Woo
// pixel wrote this
// -- TypeDefs --
// ArgTypes
// A way to get type safety in AddArg
type ArgTypes string
var (
ArgOption ArgTypes = "option"
ArgContent ArgTypes = "content"
ArgFlag ArgTypes = "flag"
)
// ArgTypeGuards
// A way to get type safety in AddArg
type ArgTypeGuards string
var (
Int ArgTypeGuards = "int"
String ArgTypeGuards = "string"
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"
Time ArgTypeGuards = "time"
)
// ArgInfo
// Describes a CommandInfo argument
type ArgInfo struct {
Match ArgTypes
TypeGuard ArgTypeGuards
Description string
Required bool
Flag bool
DefaultOption string
Choices []string
Regex *regexp2.Regexp
}
// CommandArg
// Describes what a cmd ctx will receive
type CommandArg struct {
info ArgInfo
Value interface{}
}
// Arguments
// Type of the arguments field in the command ctx
type Arguments map[string]CommandArg
// -- Command Configuration --
// CreateCommandInfo
// Creates a pointer to a CommandInfo
func CreateCommandInfo(trigger string, description string, public bool, group Group) *CommandInfo {
cI := &CommandInfo{
Aliases: nil,
Arguments: orderedmap.New(),
Description: description,
Group: group,
Public: public,
IsTyping: false,
Trigger: trigger,
}
return cI
}
// CreateRawCmdInfo
// Creates a pointer to a CommandInfo
func CreateRawCmdInfo(cI *CommandInfo) *CommandInfo {
cI.Arguments = orderedmap.New()
return cI
}
// SetParent
// Sets the parent properties
func (cI *CommandInfo) SetParent(isParent bool, parentID string) {
if !isParent {
cI.IsChild = true
}
cI.IsParent = isParent
cI.ParentID = parentID
}
//AddCmdAlias
// Adds a list of strings as aliases for the command
func (cI *CommandInfo) AddCmdAlias(aliases []string) *CommandInfo {
if len(aliases) < 1 {
return cI
}
cI.Aliases = aliases
return cI
}
// AddArg
// Adds an arg to the CommandInfo
func (cI *CommandInfo) AddArg(argument string, typeGuard ArgTypeGuards, match ArgTypes, description string, required bool, defaultOption string) *CommandInfo {
cI.Arguments.Set(argument, &ArgInfo{
TypeGuard: typeGuard,
Description: description,
Required: required,
Match: match,
DefaultOption: defaultOption,
Choices: nil,
Regex: nil,
})
return cI
}
// AddFlagArg
// Adds a flag arg, which is a special type of argument
// This type of argument allows for the user to place the "phrase" (e.g: --debug) anywhere
// in the command string and the parser will find it.
func (cI *CommandInfo) AddFlagArg(flag string, typeGuard ArgTypeGuards, match ArgTypes, description string, required bool, defaultOption string) *CommandInfo {
regexString := flag
if match == ArgOption {
// Currently, it only supports a limited character set.
// todo figure out how to detect any character
regexString = fmt.Sprintf("--%s (([a-zA-Z0-9:/.]+)|(\"[a-zA-Z0-9:/. ]+\"))", flag)
} else {
regexString = fmt.Sprintf("--%s", flag)
}
regex, err := regexp2.Compile(regexString, 0)
if err != nil {
log.Fatalf("Unable to create regex for flag on command %s flag: %s", cI.Trigger, flag)
}
cI.Arguments.Set(flag, &ArgInfo{
Description: description,
Required: required,
Flag: true,
Match: match,
TypeGuard: typeGuard,
DefaultOption: defaultOption,
Regex: regex,
})
return cI
}
// AddChoices
// Adds SubCmd choices
func (cI *CommandInfo) AddChoices(arg string, choices []string) *CommandInfo {
v, ok := cI.Arguments.Get(arg)
if ok {
vv := v.(*ArgInfo)
vv.Choices = choices
cI.Arguments.Set(arg, vv)
} else {
log.Errorf("Unable to get argument %s in AddChoices", arg)
return cI
}
return cI
}
func (cI *CommandInfo) SetTyping(isTyping bool) *CommandInfo {
cI.IsTyping = isTyping
return cI
}
//todo subcommand stuff
//// BindToChoice
//// Bind an arg to choice (subcmd)
//func (cI *CommandInfo) BindToChoice(arg string, choice string) {
//
//}
// CreateAppOptSt
// Creates an ApplicationOptionsStruct for all the args.
func (cI *CommandInfo) CreateAppOptSt() *discordgo.ApplicationCommandOption {
return &discordgo.ApplicationCommandOption{}
}
// -- Argument Parser --
// ParseArguments
// Version two of the argument parser
func ParseArguments(args string, infoArgs *orderedmap.OrderedMap) *Arguments {
ar := make(Arguments)
if args == "" || len(infoArgs.Keys()) < 1 {
return &ar
}
// Split string on spaces to get every "phrase"
// bool to parse content strings
moreContent := false
// Keys of infoArgs
k := infoArgs.Keys()
var modK []string
// First find all flags in the string.
splitString, ar, modK := findAllFlags(args, k, infoArgs, &ar)
// Find all the option args (e.g. single 'phrases' or quoted strings)
// Then return the currentPos, so we can index k and find remaining keys.
// Also return a modified Arguments struct
ar, moreContent, splitString, modK = findAllOptionArgs(splitString, modK, infoArgs, &ar)
// If there is more content, lets find it
if moreContent == true {
v, ok := infoArgs.Get(modK[0])
if !ok {
return &ar
}
vv := v.(*ArgInfo)
commandContent, _ := createContentString(splitString, 0)
ar[modK[0]] = CommandArg{
info: *vv,
Value: commandContent,
}
return &ar
// Else return the args struct
} else {
return &ar
}
}
/* Argument Parsing Helpers */
func createContentString(splitString []string, currentPos int) (string, int) {
str := ""
for i := currentPos; i < len(splitString); i++ {
str += splitString[i] + " "
currentPos = i
}
return strings.TrimSuffix(str, " "), currentPos
}
// 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
// (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 {
break
}
}
// 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
// Return early if the argument parser has found all args
if argString == nil || len(argString) == 0 || len(modKeys) == 0 || modKeys == nil {
return *args, false, argString, modKeys
}
// Now lets find the not required args
for i, 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 {
modKeys = RemoveItems(modKeys, indexes)
return *args, true, argString, 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)
indexes = append(indexes, i)
} else if checkTypeGuard(argString[currentPos], vv.TypeGuard) {
(*args)[v] = handleArgOption(argString[currentPos], *vv)
currentPos++
indexes = append(indexes, i)
} else {
}
}
//
return *args, false, createSplitString(modifiedArgString), modKeys
}
func findTypeGuard(input string, array []string, typeguard ArgTypeGuards) (string, []string) {
switch typeguard {
case Int:
if match, isMatch := TypeGuard["int"].FindStringMatch(input); isMatch == nil && match != nil {
return match.String(), RemoveItem(array, match.String())
}
return "", array
case Boolean:
if match, isMatch := TypeGuard["boolean"].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
case Time:
match := strings.Join(FindAllString(TimeRegexes["all"], input), "")
//if match, isMatch := TimeRegexes["all"].Mat(input); isMatch == nil && match != nil {
// return match.String(), RemoveItem(array, match.String())
//}
if match != "" {
return match, RemoveItem(array, match)
}
return "", array
default:
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 {
return CommandArg{
info: info,
Value: str,
}
}
func checkTypeGuard(str string, typeguard ArgTypeGuards) bool {
switch typeguard {
case String:
return true
case Int:
if _, err := strconv.Atoi(str); err == nil {
return true
}
return false
case Boolean:
if _, err := strconv.ParseBool(str); err == nil {
return true
}
case Channel:
if isMatch, _ := MentionStringRegexes["channel"].MatchString(str); isMatch {
return true
} else if isMatch, _ := MentionStringRegexes["id"].MatchString(str); isMatch {
return true
}
case Role:
if isMatch, _ := MentionStringRegexes["role"].MatchString(str); isMatch {
return true
} else if isMatch, _ := MentionStringRegexes["id"].MatchString(str); isMatch {
return true
}
case User:
if isMatch, _ := MentionStringRegexes["user"].MatchString(str); isMatch {
return true
} else if isMatch, _ := MentionStringRegexes["id"].MatchString(str); isMatch {
return true
}
return false
case ArrString:
if isMatch, _ := TypeGuard["arrString"].MatchString(str); isMatch {
return true
}
return false
case Message:
if isMatch, _ := TypeGuard["message_url"].MatchString(str); isMatch {
return true
}
return false
}
return false
}
/* Argument Casting s*/
// StringValue
// Returns the string value of the arg
func (ag CommandArg) StringValue() string {
if ag.Value == nil {
return ""
}
if v, ok := ag.Value.(string); ok {
return v
} else if v := strconv.FormatFloat(ag.Value.(float64), 'f', 2, 64); v != "" {
return v
} else if v = strconv.FormatBool(ag.Value.(bool)); v != "" {
return v
}
return ""
}
// Int64Value
// Returns the int64 value of the arg
func (ag CommandArg) Int64Value() int64 {
if ag.Value == nil {
return 0
}
if v, ok := ag.Value.(float64); ok {
return int64(v)
} else if v, err := strconv.ParseInt(ag.StringValue(), 10, 64); err == nil {
return v
}
return 0
}
// IntValue
// Returns the int value of the arg
func (ag CommandArg) IntValue() int {
if ag.Value == nil {
return 0
}
if v, ok := ag.Value.(float64); ok {
return int(v)
} else if v, err := strconv.Atoi(ag.StringValue()); err == nil {
return v
}
return 0
}
// FloatValue
// Returns the int value of the arg
func (ag CommandArg) FloatValue() float64 {
if ag.Value == nil {
return 0.0
}
if v, ok := ag.Value.(float64); ok {
return v
} else if v, err := strconv.ParseFloat(ag.StringValue(), 64); err == nil {
return v
}
return 0.0
}
// BoolValue
// Returns the int value of the arg
func (ag CommandArg) BoolValue() bool {
if ag.Value == nil {
return false
}
stringValue := ag.StringValue()
if v, err := strconv.ParseBool(stringValue); err == nil {
return v
}
return false
}
// ChannelValue is a utility function for casting value to a channel struct
// Returns a channel struct, partial channel struct, or a nil value
func (ag CommandArg) ChannelValue(s *discordgo.Session) (*discordgo.Channel, error) {
chanID := ag.StringValue()
if chanID == "" {
return &discordgo.Channel{ID: chanID}, errors.New("no channel id")
}
if s == nil {
return &discordgo.Channel{ID: chanID}, errors.New("no session")
}
cleanedId := CleanId(chanID)
if cleanedId == "" {
return &discordgo.Channel{ID: chanID}, errors.New("not an id")
}
ch, err := s.State.Channel(cleanedId)
if err != nil {
ch, err = s.Channel(cleanedId)
if err != nil {
return &discordgo.Channel{ID: chanID}, errors.New("could not find channel")
}
}
return ch, nil
}
// MemberValue is a utility function for casting value to a member struct
// Returns a user struct, partial user struct, or a nil value
func (ag CommandArg) MemberValue(s *discordgo.Session, g string) (*discordgo.Member, error) {
userID := ag.StringValue()
if userID == "" {
return &discordgo.Member{
GuildID: g,
User: &discordgo.User{
ID: userID,
},
}, errors.New("no userid")
}
cleanedId := CleanId(userID)
if cleanedId == "" {
return &discordgo.Member{
GuildID: g,
User: &discordgo.User{
ID: userID,
},
}, errors.New("invalid userid")
}
if s == nil {
return &discordgo.Member{
GuildID: g,
User: &discordgo.User{
ID: cleanedId,
},
}, errors.New("session is nil")
}
u, err := s.State.Member(g, cleanedId)
if err != nil {
u, err = s.GuildMember(g, cleanedId)
if err != nil {
return &discordgo.Member{
GuildID: g,
User: &discordgo.User{
ID: userID,
},
}, errors.New("cant find user")
}
}
return u, nil
}
// UserValue is a utility function for casting value to a member struct
// Returns a user struct, partial user struct, or a nil value
func (ag CommandArg) UserValue(s *discordgo.Session) (*discordgo.User, error) {
userID := ag.StringValue()
if userID == "" {
return &discordgo.User{
ID: userID,
}, errors.New("no userid")
}
cleanedId := CleanId(userID)
if cleanedId == "" {
return &discordgo.User{
ID: userID,
}, errors.New("invalid userid")
}
if s == nil {
return &discordgo.User{
ID: userID,
}, errors.New("session is nil")
}
u, err := s.User(cleanedId)
if err != nil {
return &discordgo.User{
ID: cleanedId,
}, errors.New("cant find user")
}
return u, nil
}
// RoleValue is a utility function for casting value to a user struct
// Returns a user struct, partial user struct, or a nil value
func (ag CommandArg) RoleValue(s *discordgo.Session, gID string) (*discordgo.Role, error) {
roleID := ag.StringValue()
if roleID == "" {
return nil, errors.New("unable to find roleid")
}
cleanedId := CleanId(roleID)
if cleanedId == "" {
return &discordgo.Role{
ID: roleID,
}, errors.New("invalid roleid")
}
if s == nil || gID == "" {
return &discordgo.Role{ID: cleanedId}, errors.New("no session (and/or) guild id")
}
r, err := s.State.Role(cleanedId, gID)
if err != nil {
roles, err := s.GuildRoles(gID)
if err == nil {
for _, r = range roles {
if r.ID == cleanedId {
return r, nil
}
}
}
return &discordgo.Role{ID: roleID}, errors.New("could not find role")
}
return r, nil
}