From 3e3f761b019f21120319e081eb6a9f8e538dddef Mon Sep 17 00:00:00 2001 From: Riley Date: Mon, 16 Aug 2021 22:05:33 -0700 Subject: [PATCH] update from upstream --- arguments.go | 71 ++++++++++---------- commands.go | 71 ++++++++++---------- consts.go | 11 ++- core.go | 68 ++++++++++++++----- handlers.go | 4 +- interaction.go | 54 +++------------ util.go | 178 +++++++++++++++++++++++++++++++++++-------------- 7 files changed, 267 insertions(+), 190 deletions(-) diff --git a/arguments.go b/arguments.go index 589c964..c5c5c83 100644 --- a/arguments.go +++ b/arguments.go @@ -45,6 +45,7 @@ var ( SubCmd ArgTypeGuards = "subcmd" SubCmdGrp ArgTypeGuards = "subcmdgrp" ArrString ArgTypeGuards = "arrString" + Time ArgTypeGuards = "time" ) // ArgInfo @@ -75,7 +76,7 @@ type Arguments map[string]CommandArg // CreateCommandInfo // Creates a pointer to a CommandInfo -func CreateCommandInfo(trigger string, description string, public bool, group string) *CommandInfo { +func CreateCommandInfo(trigger string, description string, public bool, group Group) *CommandInfo { cI := &CommandInfo{ Aliases: nil, Arguments: orderedmap.New(), @@ -88,6 +89,13 @@ func CreateCommandInfo(trigger string, description string, public bool, group st 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) { @@ -248,34 +256,7 @@ func findAllOptionArgs(argString []string, keys []string, infoArgs *orderedmap.O 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 @@ -314,8 +295,13 @@ func findAllOptionArgs(argString []string, keys []string, infoArgs *orderedmap.O 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 _, v := range modKeys { + for i, v := range modKeys { // error handling iA, ok := infoArgs.Get(v) if !ok { @@ -331,8 +317,8 @@ func findAllOptionArgs(argString []string, keys []string, infoArgs *orderedmap.O break } if vv.Match == ArgContent { - modifiedArgString = strings.Join(argString, " ") - return *args, true, createSplitString(modifiedArgString), modKeys + modKeys = RemoveItems(modKeys, indexes) + return *args, true, argString, modKeys } // Break early if current pos is the length of the array if currentPos == len(argString) { @@ -342,9 +328,11 @@ func findAllOptionArgs(argString []string, keys []string, infoArgs *orderedmap.O 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 { } @@ -356,7 +344,12 @@ func findAllOptionArgs(argString []string, keys []string, infoArgs *orderedmap.O 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 { + 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 @@ -391,8 +384,18 @@ func findTypeGuard(input string, array []string, typeguard ArgTypeGuards) (strin 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 } - return "", array } func findAllFlags(argString string, keys []string, infoArgs *orderedmap.OrderedMap, args *Arguments) ([]string, Arguments, []string) { diff --git a/commands.go b/commands.go index 9806d10..05cdaf8 100644 --- a/commands.go +++ b/commands.go @@ -2,19 +2,22 @@ package framework import ( "github.com/QPixel/orderedmap" - "runtime" - "strings" - "github.com/bwmarrin/discordgo" + "runtime" + "runtime/debug" + "strings" + "time" ) // commands.go // This file contains everything required to add core commands to the bot, and parse commands from a message // GroupTypes -const ( - Moderation = "moderation" - Utility = "utility" +type Group string + +var ( + Moderation Group = "moderation" + Utility Group = "utility" ) // CommandInfo @@ -23,7 +26,7 @@ type CommandInfo struct { Aliases []string // Aliases for the normal trigger Arguments *orderedmap.OrderedMap // Arguments for the command Description string // A short description of what the command does - Group string // The group this command belongs to + Group Group // The group this command belongs to ParentID string // The ID of the parent command Public bool // Whether non-admins and non-mods can use this command IsTyping bool // Whether the command will show a typing thing when ran. @@ -86,6 +89,9 @@ var commandAliases = make(map[string]string) // This is also private so other commands cannot modify it var slashCommands = make(map[string]discordgo.ApplicationCommand) +// commandsGC +var commandsGC = 0 + // AddCommand // Add a command to the bot func AddCommand(info *CommandInfo, function BotFunction) { @@ -227,13 +233,6 @@ func commandHandler(session *discordgo.Session, message *discordgo.MessageCreate } } - // The command is valid, so now we need to delete the invoking message if that is configured - if g.Info.DeletePolicy { - err := Session.ChannelMessageDelete(message.ChannelID, message.ID) - if err != nil { - SendErrorReport(message.GuildID, message.ChannelID, message.Author.ID, "Failed to delete message: "+message.ID, err) - } - } if !isCustom { //Get the command to run // Error Checking @@ -253,6 +252,14 @@ func commandHandler(session *discordgo.Session, message *discordgo.MessageCreate if command.Info.IsTyping && g.Info.ResponseChannelId == "" { _ = Session.ChannelTyping(message.ChannelID) } + // The command is valid, so now we need to delete the invoking message if that is configured + if g.Info.DeletePolicy { + err := Session.ChannelMessageDelete(message.ChannelID, message.ID) + if err != nil { + SendErrorReport(message.GuildID, message.ChannelID, message.Author.ID, "Failed to delete message: "+message.ID, err) + } + } + defer handleCommandError(g.ID, channel.ID, message.Author.ID) if command.Info.IsParent { handleChildCommand(*argString, command, message.Message, g) @@ -264,6 +271,13 @@ func commandHandler(session *discordgo.Session, message *discordgo.MessageCreate Args: *ParseArguments(*argString, command.Info.Arguments), Message: message.Message, }) + // Makes sure that variables ran in ParseArguments are gone. + if commandsGC == 25 && commandsGC > 25 { + debug.FreeOSMemory() + commandsGC = 0 + } else { + commandsGC++ + } return } } @@ -272,24 +286,9 @@ func commandHandler(session *discordgo.Session, message *discordgo.MessageCreate // -- Helper Methods func handleChildCommand(argString string, command Command, message *discordgo.Message, g *Guild) { split := strings.SplitN(argString, " ", 2) - // First lets see if this subcmd even exists - v, ok := command.Info.Arguments.Get("subcmdgrp") - // the command doesn't even have a subcmdgrp arg, return - if !ok { - return - } - choices := v.(*ArgInfo).Choices - subCmdExist := false - for _, choice := range choices { - if split[0] != choice { - continue - } else { - subCmdExist = true - break - } - } - if !subCmdExist { + childCmd, ok := childCommands[command.Info.Trigger][split[0]] + if !ok { command.Function(&Context{ Guild: g, Cmd: command.Info, @@ -298,12 +297,11 @@ func handleChildCommand(argString string, command Command, message *discordgo.Me }) return } - childCmd, ok := childCommands[command.Info.Trigger][split[0]] - if !ok || len(split) < 2 { - command.Function(&Context{ + if len(split) < 2 { + childCmd.Function(&Context{ Guild: g, - Cmd: command.Info, - Args: nil, + Cmd: childCmd.Info, + Args: *ParseArguments("", childCmd.Info.Arguments), Message: message, }) return @@ -326,6 +324,7 @@ func handleCommandError(gID string, cId string, uId string) { if err != nil { log.Errorf("err sending message %s", err) } + time.Sleep(5 * time.Second) _ = Session.ChannelMessageDelete(cId, message.ID) return } diff --git a/consts.go b/consts.go index 960fa52..ccdd988 100644 --- a/consts.go +++ b/consts.go @@ -11,20 +11,19 @@ var ( "hours": regexp2.MustCompile("^[0-9]+h$", 0), "days": regexp2.MustCompile("^[0-9]+d$", 0), "weeks": regexp2.MustCompile("^[0-9]+w$", 0), - "years": regexp2.MustCompile("^[0-9]+y$", 0), + "years": regexp2.MustCompile("[0-9]+y", 0), + "all": regexp2.MustCompile("(([0-9]+)(s|m|h|d|w|y))", 0), } MentionStringRegexes = regex{ "all": regexp2.MustCompile("<((@!?\\d+)|(#?\\d+)|(@&?\\d+))>", 0), "role": regexp2.MustCompile("<((@&?\\d+))>", 0), "user": 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{ "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), + "int": regexp2.MustCompile("\\b(0*(?:[0-9]{1,8}))\\b", 0), + "boolean": regexp2.MustCompile("\\b((?:true|false))\\b", 0), } ) diff --git a/core.go b/core.go index fc7de05..ebc90b8 100644 --- a/core.go +++ b/core.go @@ -1,11 +1,12 @@ package framework import ( + "fmt" "github.com/bwmarrin/discordgo" tlog "github.com/ubergeek77/tinylog" "os" "os/signal" - + "runtime" "strconv" "strings" "syscall" @@ -15,13 +16,17 @@ import ( // This file contains the main code responsible for driving core bot functionality // messageState -// Tells discordgo the amount of mesesages to cache -var messageState = 20 +// Tells discordgo the amount of messages to cache +var messageState = 500 // log // The logger for the core bot var log = tlog.NewTaggedLogger("BotCore", tlog.NewColor("38;5;111")) +// dlog +// The logger for discordgo +var dlog = tlog.NewTaggedLogger("DG", tlog.NewColor("38;5;111")) + // Session // The Discord session, made public so commands can use it var Session *discordgo.Session @@ -33,9 +38,6 @@ var Session *discordgo.Session // This is a boolean map, because checking its values is dead simple this way var botAdmins = make(map[string]bool) -// BotPresence -var botPresence discordgo.GatewayStatusUpdate - // BotToken // A string of the current bot token, usually set by the main method // Similar to BotAdmins, this isn't saved to .json and is added programmatically @@ -77,15 +79,32 @@ func IsCommand(trigger string) bool { return false } -// SetPresence -// Sets the presence struct after a session has been created -func SetPresence(presence discordgo.GatewayStatusUpdate) { - botPresence = presence - return +// dgoLog +// Allows for discordgo to call tinylog +func dgoLog(msgL, caller int, format string, a ...interface{}) { + pc, file, line, _ := runtime.Caller(caller) + files := strings.Split(file, "/") + file = files[len(files)-1] + + name := runtime.FuncForPC(pc).Name() + fns := strings.Split(name, ".") + name = fns[len(fns)-1] + msg := fmt.Sprintf(format, a...) + switch msgL { + case discordgo.LogError: + dlog.Errorf("%s:%d:%s() %s", file, line, name, msg) + case discordgo.LogWarning: + dlog.Warningf("%s:%d:%s() %s", file, line, name, msg) + case discordgo.LogInformational: + dlog.Infof("%s:%d:%s() %s", file, line, name, msg) + case discordgo.LogDebug: + dlog.Debugf("%s:%d:%s() %s", file, line, name, msg) + } } -// Start the bot! +// Start uberbot! func Start() { + discordgo.Logger = dgoLog // Load all the guilds loadGuilds() @@ -103,8 +122,19 @@ func Start() { } // Setup State specific variables Session.State.MaxMessageCount = messageState + Session.LogLevel = discordgo.LogWarning + Session.SyncEvents = false Session.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAll) - Session.Identify.Presence = botPresence + // Set the bots status + Session.Identify.Presence = discordgo.GatewayStatusUpdate{ + Game: discordgo.Activity{ + Name: "Mega Man Battle Network", + Type: 3, + }, + Status: "dnd", + AFK: true, + Since: 91879201, + } // Open the session log.Info("Connecting to Discord...") err = Session.Open() @@ -117,6 +147,7 @@ func Start() { // Add the slash command handler to the list of user-defined handlers AddHandler(handleInteraction) + // Add the handlers to the session addHandlers() @@ -141,18 +172,19 @@ func Start() { log.Warning("You have not added any bot admins! Only moderators will be able to run commands, and permissions cannot be changed!") } // Register slash commands - slashChannel := make(chan string) - log.Info("Registering slash commands") - go AddSlashCommands("833901685054242846", slashChannel) + //slashChannel := make(chan string) + //log.Info("Registering slash commands") + //go AddSlashCommands("833901685054242846", slashChannel) // Bot ready log.Info("Initialization complete! The bot is now ready.") // Info about slash commands - log.Info(<-slashChannel) + //log.Info(<-slashChannel) + // -- GRACEFUL TERMINATION -- // - // Set up a sigterm channel so we can detect when the application receives a TERM signal + // Set up a sigterm channel, so we can detect when the application receives a TERM signal sigChannel := make(chan os.Signal, 1) signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL, os.Interrupt, os.Kill) diff --git a/handlers.go b/handlers.go index 5ee0d69..131a110 100644 --- a/handlers.go +++ b/handlers.go @@ -4,8 +4,8 @@ package framework // Everything required for commands to pass their own handlers to discordgo // handlers -// This list stores all of the handlers that can be added to the bot -// It's basically a passthrough for discordgo.AddHandler, but having a list +// This list stores all the handlers that can be added to the bot +// It's basically a passthroughs for discordgo.AddHandler, but having a list // allows them to be collected ahead of time and then added all at once var handlers []interface{} diff --git a/interaction.go b/interaction.go index 6f6f6a8..918fa94 100644 --- a/interaction.go +++ b/interaction.go @@ -1,7 +1,6 @@ package framework import ( - "fmt" "github.com/bwmarrin/discordgo" "runtime" ) @@ -22,9 +21,6 @@ var slashCommandTypes = map[ArgTypeGuards]discordgo.ApplicationCommandOptionType //SubCmdGrp: discordgo.ApplicationCommandOptionSubCommandGroup, } -//var componentHandlers - -// // getSlashCommandStruct // Creates a slash command struct // todo work on sub command stuff @@ -44,8 +40,14 @@ func createSlashCommandStruct(info *CommandInfo) (st *discordgo.ApplicationComma for i, k := range info.Arguments.Keys() { v, _ := info.Arguments.Get(k) vv := v.(*ArgInfo) + var sType discordgo.ApplicationCommandOptionType + if val, ok := slashCommandTypes[vv.TypeGuard]; ok { + sType = val + } else { + sType = slashCommandTypes["String"] + } optionStruct := discordgo.ApplicationCommandOption{ - Type: slashCommandTypes[vv.TypeGuard], + Type: sType, Name: k, Description: vv.Description, Required: vv.Required, @@ -105,47 +107,8 @@ 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 { - err := Session.ChannelMessageDelete(i.ChannelID, i.ID) - if err != nil { - SendErrorReport(i.GuildID, i.ChannelID, i.Member.User.ID, "Failed to delete message: "+i.ID, err) - } - } trigger := i.ApplicationCommandData().Name if !IsAdmin(i.Member.User.ID) { // Ignore the command if it is globally disabled @@ -175,6 +138,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, @@ -198,7 +162,7 @@ func handleMessageComponents(s *discordgo.Session, i *discordgo.InteractionCreat i.Message.Embeds[0].Description = content s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ // Buttons also may update the message which they was attached to. - // Or may just acknowledge (InteractionResponseDredeferMessageUpdate) that the event was received and not update the message. + // Or may just acknowledge (InteractionResponseDeferredMessageUpdate) that the event was received and not update the message. // To update it later you need to use interaction response edit endpoint. Type: discordgo.InteractionResponseUpdateMessage, Data: &discordgo.InteractionResponseData{ diff --git a/util.go b/util.go index 3d7a04b..77047cf 100644 --- a/util.go +++ b/util.go @@ -2,7 +2,9 @@ package framework import ( "errors" + "fmt" "github.com/bwmarrin/discordgo" + "github.com/dlclark/regexp2" "regexp" "strconv" "strings" @@ -27,11 +29,20 @@ func RemoveItem(slice []string, delete string) []string { // Removes items from a slice by index func RemoveItems(slice []string, indexes []int) []string { newSlice := make([]string, len(slice)) + if len(indexes) >= len(slice) { + return newSlice + } copy(newSlice, slice) for _, v := range indexes { - newSlice[v] = newSlice[len(newSlice)-1] - newSlice[len(newSlice)-1] = "" - newSlice = newSlice[:len(newSlice)-1] + if len(newSlice) > v+1 && v != 0 { + v = v - 1 + } + //newSlice[v] = newSlice[len(newSlice)-1] + //newSlice[len(newSlice)-1] = "" + //newSlice = newSlice[:len(newSlice)-1] + copy(newSlice[v:], newSlice[v+1:]) // Shift a[i+1:] left one index. + newSlice[len(newSlice)-1] = "" // Erase last element (write zero value). + newSlice = newSlice[:len(newSlice)-1] // Truncate slice. } return newSlice } @@ -107,22 +118,6 @@ func ExtractCommand(guild *GuildInfo, message string) (*string, *string) { return &trigger, &fullArgs } else { - if strings.Contains(message, "uber") { - // Same process as above prefix method, but split on a bot mention instead - split := strings.SplitN(message, "uber", 2) - content := strings.TrimPrefix(split[1], " ") - // If content is null someone just sent the prefix - if content == "" { - return nil, nil - } - // Attempt to pull the trigger out of the command content by splitting on spaces - trigger := 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 - } // The bot can only be mentioned with a space botMention := Session.State.User.Mention() + " " @@ -241,37 +236,122 @@ func ParseTime(content string) (int, string) { return 0, "error lol" } duration := 0 - displayDuration := "Indefinite" + multiplier := 1 - for k, v := range TimeRegexes { - if isMatch, _ := v.MatchString(content); isMatch { - multiplier, _ = strconv.Atoi(EnsureNumbers(v.String())) - switch k { - case "seconds": - duration = multiplier + duration - displayDuration = "Second" - case "minutes": - duration = multiplier*60 + duration - displayDuration = "Minute" - case "hours": - duration = multiplier*60*60 + duration - displayDuration = "Hour" - case "days": - duration = multiplier*60*60*24 + duration - displayDuration = "Day" - case "weeks": - duration = multiplier*60*60*24*7 + duration - displayDuration = "Week" - case "years": - duration = multiplier*60*60*24*7*365 + duration - displayDuration = "Year" - } + + matches := FindAllString(TimeRegexes["all"], content) + if len(matches) <= 0 { + return 0, "error lol" + } + for _, v := range matches { + // Grab only the letters out of the duration, to detect the unit + muteUnit := strings.ToLower(EnsureLetters(v)) + + // Grab the number out of the duration + // Errors shouldn't be possible due to EnsureNumbers + multiplier, _ = strconv.Atoi(EnsureNumbers(v)) + + // Use the string next to the number to check how long the mute should be for + switch muteUnit { + case "s": + duration = multiplier + duration + case "m": + duration = multiplier*60 + duration + case "h": + duration = multiplier*60*60 + duration + case "d": + duration = multiplier*60*60*24 + duration + case "w": + duration = multiplier*60*60*24*7 + duration + case "y": + duration = multiplier*60*60*24*7*52 + duration + default: + break } } - // Plurals matter! - if multiplier != 1 { - displayDuration += "s" - } - displayDuration = strconv.Itoa(multiplier) + " " + displayDuration - return duration, displayDuration + + return duration, createDisplayDurationString(content) +} + +func createDisplayDurationString(content string) (str string) { + // First tokenize + str = "" + matches := FindAllString(TimeRegexes["all"], content) + if matches == nil || len(matches) == 0 { + str = "Indefinite" + return + } + for i, v := range matches { + prefixChar := "" + if i+1 == len(matches) && len(matches) > 1 { + prefixChar = " & " + } else if i != 0 { + prefixChar = ", " + } + // Grab only the letters out of the duration, to detect the unit + muteUnit := strings.ToLower(EnsureLetters(v)) + + // Grab the number out of the duration + // Errors shouldn't be possible due to EnsureNumbers + multiplier, _ := strconv.Atoi(EnsureNumbers(v)) + + // clean this up + switch muteUnit { + case "s": + if multiplier > 1 { + str += prefixChar + fmt.Sprintf("%d Seconds", multiplier) + break + } + str += prefixChar + "Second" + break + case "m": + if multiplier > 1 { + str += prefixChar + fmt.Sprintf("%d Minutes", multiplier) + break + } + str += prefixChar + fmt.Sprintf("%d Minute", multiplier) + break + case "h": + if multiplier > 1 { + str += prefixChar + fmt.Sprintf("%d Hours", multiplier) + break + } + str += prefixChar + fmt.Sprintf("%d Hours", multiplier) + break + case "d": + if multiplier > 1 { + str += prefixChar + fmt.Sprintf("%d Days", multiplier) + break + } + str += prefixChar + fmt.Sprintf("%d Day", multiplier) + break + case "w": + if multiplier > 1 { + str += prefixChar + fmt.Sprintf("%d Weeks", multiplier) + break + } + str += prefixChar + fmt.Sprintf("%d Week", multiplier) + break + case "y": + if multiplier > 1 { + str += prefixChar + fmt.Sprintf("%d Years", multiplier) + break + } + str += prefixChar + fmt.Sprintf("%d Year", multiplier) + break + default: + break + } + } + return +} + +func FindAllString(re *regexp2.Regexp, s string) []string { + var matches []string + m, _ := re.FindStringMatch(s) + for m != nil { + matches = append(matches, m.String()) + m, _ = re.FindNextMatch(m) + } + return matches }