framework/core/arguments.go

524 lines
12 KiB
Go

package core
import (
"errors"
"github.com/QPixel/orderedmap"
"github.com/bwmarrin/discordgo"
"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"
Boolean ArgTypeGuards = "bool"
SubCmd ArgTypeGuards = "subcmd"
SubCmdGrp ArgTypeGuards = "subcmdgrp"
ArrString ArgTypeGuards = "arrString"
)
// ArgInfo
// Describes a CommandInfo argument
type ArgInfo struct {
Match ArgTypes
TypeGuard ArgTypeGuards
Description string
Required bool
Flag bool
DefaultOption string
Choices []string
Regex string
}
// 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 string) *CommandInfo {
cI := &CommandInfo{
Aliases: nil,
Arguments: orderedmap.New(),
Description: description,
Group: group,
Public: public,
IsTyping: false,
Trigger: trigger,
}
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: "",
})
return cI
}
// AddFlagArg
// Adds a flag arg, which is a special type of argument
func (cI *CommandInfo) AddFlagArg(flag string, typeGuard ArgTypeGuards, match ArgTypes, description string, required bool, defaultOption string) *CommandInfo {
cI.Arguments.Set(flag, &ArgInfo{
Description: description,
Required: required,
Flag: true,
Match: match,
TypeGuard: typeGuard,
DefaultOption: defaultOption,
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
}
// -- Argument Parser --
// ParseArguments
// Parses the arguments into a pointer to an Arguments struct
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
// Keys of infoArgs
k := infoArgs.Keys()
for i := 0; i < len(splitString); i++ {
for n := currentPos; n <= len(k); n++ {
if n > len(k)+1 || currentPos+1 > len(k) {
break
}
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{
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
}
}
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
}
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] + " "
} else {
str += strings.TrimSuffix(splitString[i], "\"")
currentPos = i
break
}
}
return CommandArg{
info: argInfo,
Value: str,
}, currentPos
}
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
}
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
}