framework/util.go

287 lines
8.7 KiB
Go

package framework
import (
"errors"
"github.com/bwmarrin/discordgo"
"github.com/dlclark/regexp2"
"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
}
// 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 := regexp2.Compile("[^0-9]+", 0)
if err != nil {
log.Errorf("An unrecoverable error occurred when compiling a regex expression: %s", err)
return ""
}
if ok, _ := reg.MatchString(in); ok {
str, err := reg.Replace(in, "", -1, -1)
if err != nil {
log.Errorf("Unable to replace text in EnsureNumbers")
return ""
}
return str
}
return 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 := regexp2.Compile("[^a-zA-Z]+", 0)
if err != nil {
log.Errorf("An unrecoverable error occurred when compiling a regex expression: %s", err)
return ""
}
if ok, _ := reg.MatchString(in); ok {
str, err := reg.Replace(in, "", -1, -1)
if err != nil {
log.Errorf("Unable to replace text in EnsureLetters")
return ""
}
return str
}
return 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 command 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 trigger
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 {
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() + " "
// 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
}
// 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 {
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
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"
}
}
}
// Plurals matter!
if multiplier != 1 {
displayDuration += "s"
}
displayDuration = strconv.Itoa(multiplier) + " " + displayDuration
return duration, displayDuration
}