framework/util.go

382 lines
11 KiB
Go

package framework
import (
"errors"
"fmt"
"github.com/bwmarrin/discordgo"
"github.com/dlclark/regexp2"
"regexp"
"runtime"
"strconv"
"strings"
)
// util.go
// This file contains utility functions, simplifying redundant tasks
// RemoveItem
// Remove an item from a slice by value
func RemoveItem(slice []string, delete string) []string {
var newSlice []string
for _, elem := range slice {
if elem != delete {
newSlice = append(newSlice, elem)
}
}
return newSlice
}
// RemoveItems
// 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 {
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
}
// EnsureNumbers
// Given a string, ensure it contains only numbers
// This is useful for stripping letters and formatting characters from user/role pings
func EnsureNumbers(in string) string {
reg, err := regexp.Compile("[^0-9]+")
if err != nil {
log.Errorf("An unrecoverable error occurred when compiling a regex expression: %s", err)
return ""
}
return reg.ReplaceAllString(in, "")
}
// EnsureLetters
// Given a string, ensure it contains only letters
// This is useful for stripping numbers from mute durations, and possibly other things
func EnsureLetters(in string) string {
reg, err := regexp.Compile("[^a-zA-Z]+")
if err != nil {
log.Errorf("An unrecoverable error occurred when compiling a regex expression: %s", err)
return ""
}
return reg.ReplaceAllString(in, "")
}
// CleanId
// Given a string, attempt to remove all numbers from it
// Additionally, ensure it is at least 17 characters in length
// This is a way of "cleaning" a Discord ping into a valid snowflake string
func CleanId(in string) string {
out := EnsureNumbers(in)
// Discord IDs must be, at minimum, 17 characters long
if len(out) < 17 {
return ""
}
return out
}
// ExtractCommand
// Given a message, attempt to extract a commands trigger and command arguments out of it
// If there is no prefix, try using a bot mention as the prefix
func ExtractCommand(guild *GuildInfo, message string) (*string, *string) {
// Check if the message starts with the bot prefix
if strings.HasPrefix(message, guild.Prefix) {
// Split the message on the prefix, but ensure only 2 fields are returned
// This ensures messages containing multiple instances of the prefix don't split multiple times
split := strings.SplitN(message, guild.Prefix, 2)
// Get everything after the prefix as the command content
content := split[1]
// If the content is blank, someone used the prefix without a trigger
if content == "" {
return nil, nil
}
// Attempt to pull the trigger out of the command content by splitting on spaces
trigger := strings.Fields(content)[0]
// With the trigger identified, split the command content on the trigger to obtain everything BUT the trigger
// Ensure only 2 fields are returned so it can be split further. Then, get only the second field
fullArgs := strings.SplitN(content, trigger, 2)[1]
fullArgs = strings.TrimPrefix(fullArgs, " ")
// Avoids issues with strings that are case sensitive
trigger = strings.ToLower(trigger)
return &trigger, &fullArgs
} else {
// The bot can only be mentioned with a space
botMention := Session.State.User.Mention() + " "
// Sanitize Discord's ridiculous formatting
message = strings.Replace(message, "!", "", 1)
// See if someone is trying to mention the bot
if strings.HasPrefix(message, botMention) {
// Same process as above prefix method, but split on a bot mention instead
split := strings.SplitN(message, botMention, 2)
content := split[1]
// If content is null someone just sent the prefix
if content == "" {
return nil, nil
}
trigger := strings.ToLower(strings.Fields(content)[0])
fullArgs := strings.SplitN(content, trigger, 2)[1]
return &trigger, &fullArgs
} else {
return nil, nil
}
}
}
// GetUser
// Given a user ID, get that user's object (global to Discord, not in a guild)
func GetUser(userId string) (*discordgo.User, error) {
cleanedId := CleanId(userId)
if cleanedId == "" {
return nil, errors.New("provided ID is invalid")
}
return Session.User(cleanedId)
}
// logErrorReportFailure
// If an error report fails to send, log the failure
func logErrorReportFailure(recipient string, dmErr error, guildId string, channelId string, userId string, errTitle string, origErr error) {
log.Errorf("[REPORT] Failed to DM report to %s: %s", recipient, dmErr)
log.Error("[REPORT] ---------- BEGIN ERROR REPORT ----------")
log.Error("[REPORT] Report title: " + errTitle)
// Can't .Error a nil error
if origErr != nil {
log.Error("[REPORT] Full error: " + origErr.Error())
}
log.Error("[REPORT] Affected guild: " + guildId)
log.Error("[REPORT] Affected channel: " + channelId)
log.Error("[REPORT] Affected user: " + userId)
log.Error("[REPORT] ----------- END ERROR REPORT -----------")
}
// SendErrorReport
// Send an error report as a DM to all of the registered bot administrators
func SendErrorReport(guildId string, channelId string, userId string, title string, err error) {
// Log a general error
log.Errorf("[REPORT] %s (%s)", title, err)
// Iterate through all the admins
for admin := range botAdmins {
// Get the channel ID of the user to DM
dmChannel, dmCreateErr := Session.UserChannelCreate(admin)
if dmCreateErr != nil {
logErrorReportFailure(admin, dmCreateErr, guildId, channelId, userId, title, err)
continue
}
// Create a generic embed
reportEmbed := CreateEmbed(ColorFailure, "ERROR REPORT", title, nil)
// Add fields if they aren't blank
if guildId != "" {
reportEmbed.Fields = append(reportEmbed.Fields, &discordgo.MessageEmbedField{
Name: "Guild ID:",
Value: guildId,
Inline: false,
})
}
if channelId != "" {
reportEmbed.Fields = append(reportEmbed.Fields, &discordgo.MessageEmbedField{
Name: "Channel ID:",
Value: channelId,
Inline: false,
})
}
if userId != "" {
reportEmbed.Fields = append(reportEmbed.Fields, &discordgo.MessageEmbedField{
Name: "User ID:",
Value: userId,
Inline: false,
})
}
if err != nil {
reportEmbed.Fields = append(reportEmbed.Fields, &discordgo.MessageEmbedField{
Name: "Full error:",
Value: err.Error(),
Inline: false,
})
}
_, dmSendErr := Session.ChannelMessageSendEmbed(dmChannel.ID, reportEmbed)
if dmSendErr != nil {
logErrorReportFailure(admin, dmSendErr, guildId, channelId, userId, title, err)
continue
}
}
}
// ParseTime
// Parses time strings
func ParseTime(content string) (int, string) {
if content == "" {
return 0, "error lol"
}
duration := 0
multiplier := 1
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
}
}
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
}
// 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)
}
}