Add scrubber :>

This commit is contained in:
Daniel Mason 2025-06-26 20:10:20 +12:00
parent de5ae550e0
commit d4244e0f4a
Signed by: idanoo
GPG key ID: 387387CDBC02F132
6 changed files with 306 additions and 41 deletions

View file

@ -0,0 +1,110 @@
package bot
import (
"log/slog"
"time"
)
// scrubberInterval - How often we check
const scrubberInterval = 1 * time.Minute
var (
// scrubbers - Map of auto scrubbers to monitor
scrubbers map[string]map[string]time.Duration
// Chan to use on shutdown
scrubberStop = make(chan struct{})
)
// initScrubber - Loads the scrubber configs from the DB
func initScrubber() error {
// Load config
allScrubbers, err := b.Db.GetAllAutoScrubbers()
if err != nil {
return err
}
// map[GuildID][UserID]Interval
scrubbers = make(map[string]map[string]time.Duration)
for _, scrubber := range allScrubbers {
if _, ok := scrubbers[scrubber.GuildID]; !ok {
scrubbers[scrubber.GuildID] = make(map[string]time.Duration)
}
scrubbers[scrubber.GuildID][scrubber.UserID] = scrubber.Duration
}
runScrubber()
return nil
}
// runScrubber - Start the auto scrubber system
func runScrubber() {
ticker := time.NewTicker(scrubberInterval)
go func() {
for {
select {
case <-ticker.C:
// Loop through all scrubbers, clone for funsies
tmpScrub := scrubbers
for guildID, users := range tmpScrub {
for userID, interval := range users {
emojis, err := b.Db.GetAllEmojisForUser(guildID, userID)
if err != nil {
slog.Error("Error getting recent emojis for user", "guild_id", guildID, "user_id", userID, "err", err)
continue
}
// If older creation time + interval is before now, remove it
for _, e := range emojis {
if e.Timestamp.Add(interval).Before(time.Now()) {
// Ignore errors here as it's likely bad data
err := b.DiscordSession.MessageReactionRemove(e.ChannelID, e.MessageID, e.EmojiID, e.UserID)
if err != nil {
slog.Error("Error removing emoji reaction", "err", err, "emoji", e.EmojiID, "user", e.UserID)
continue
}
// We care if we can't delete from our DB..
err = b.Db.DeleteEmojiUsageById(e.ID)
if err != nil {
slog.Error("Error deleting emoji usage", "err", err, "emoji", e)
continue
}
}
}
}
}
// Handle shutdown
case <-scrubberStop:
ticker.Stop()
return
}
}
}()
}
// startScrubbingUser - Start an auto scrubber for a user in a guild
func startScrubbingUser(guildID string, userID string, interval time.Duration) error {
err := b.Db.AddAutoScrubber(guildID, userID, interval)
if err != nil {
slog.Error("Failed to add auto scrubber", "err", err)
return err
}
// Add to map
if _, ok := scrubbers[guildID]; !ok {
scrubbers[guildID] = make(map[string]time.Duration)
}
scrubbers[guildID][userID] = interval
return nil
}
// stopScrubbingUser - Stop an auto scrubber for a user in a guild
func stopScrubbingUser(guildID string, userID string) error {
// Remove from instant
delete(scrubbers[guildID], userID)
return b.Db.RemoveAutoScrubber(guildID, userID)
}

View file

@ -5,6 +5,7 @@ import (
"log/slog"
"sort"
"strings"
"time"
"github.com/bwmarrin/discordgo"
)
@ -46,8 +47,8 @@ var (
},
},
{
Name: "purge-recent-emojis",
Description: "Purges recent emojis",
Name: "add-auto-scrubber",
Description: "Auto Scrub Emoijis after a set period",
DefaultMemberPermissions: &defaultRunCommandPermissions,
Options: []*discordgo.ApplicationCommandOption{
{
@ -58,8 +59,21 @@ var (
},
{
Type: discordgo.ApplicationCommandOptionInteger,
Name: "hours",
Description: "Hours to purge",
Name: "minutes",
Description: "Every X minutes",
Required: true,
},
},
},
{
Name: "remove-auto-scrubber",
Description: "Stop autoscrubbing emojis after a set period",
DefaultMemberPermissions: &defaultRunCommandPermissions,
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionUser,
Name: "user",
Description: "Select user",
Required: true,
},
},
@ -67,9 +81,10 @@ var (
}
commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"show-top-emojis": showTopEmojis,
"show-top-users": showTopUsers,
"purge-recent-emojis": purgeRecentEmojis,
"show-top-emojis": showTopEmojis,
"show-top-users": showTopUsers,
"add-auto-scrubber": addAutoScrubber,
"remove-auto-scrubber": removeAutoScrubber,
}
)
@ -221,8 +236,8 @@ func showTopUsers(s *discordgo.Session, i *discordgo.InteractionCreate) {
})
}
// purgeRecentEmojis - Purges recent emojis for a user
func purgeRecentEmojis(s *discordgo.Session, i *discordgo.InteractionCreate) {
// addAutoScrubber - Scrubs emojis after a set period
func addAutoScrubber(s *discordgo.Session, i *discordgo.InteractionCreate) {
// Access options in the order provided by the user.
options := i.ApplicationCommandData().Options
optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options))
@ -245,52 +260,86 @@ func purgeRecentEmojis(s *discordgo.Session, i *discordgo.InteractionCreate) {
return
}
hours := int64(24)
if opt, ok := optionMap["hours"]; ok {
hours = opt.IntValue()
var minutes int64
if opt, ok := optionMap["minutes"]; ok {
minutes = opt.IntValue()
} else {
slog.Error("Invalid hours option provided")
slog.Error("Invalid time option provided")
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "No hours specified",
Content: "No minutes specified",
AllowedMentions: &discordgo.MessageAllowedMentions{},
},
})
return
}
emojis, err := b.Db.GetRecentEmojisForUser(i.GuildID, user.ID, hours)
dur := time.Duration(minutes) * time.Minute
err := startScrubbingUser(i.GuildID, user.ID, dur)
if err != nil {
slog.Error("Error getting recent emojis for user", "err", err)
slog.Error("Error starting auto scrubber", "err", err, "guild_id", i.GuildID, "user_id", user.ID, "duration", dur)
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Error auto scrubbing user %s", user.Username),
AllowedMentions: &discordgo.MessageAllowedMentions{},
},
})
return
}
x := 0
for _, emoji := range emojis {
emojiID := emoji.EmojiID
if emojiID == "" {
emojiID = emoji.EmojiName
}
err := s.MessageReactionRemove(emoji.ChannelID, emoji.MessageID, emojiID, emoji.UserID)
if err != nil {
slog.Error("Error removing emoji reaction", "err", err, "emoji", emoji.EmojiID, "user", user.ID)
continue
}
err = b.Db.DeleteEmojiUsage(i.GuildID, emoji.ChannelID, emoji.MessageID, emoji.UserID, emoji.EmojiID)
if err != nil {
slog.Error("Error deleting emoji usage", "err", err)
continue
}
x++
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Purged %d emojis for user %s", x, user.Username),
Content: fmt.Sprintf("Will remove %s's emojis every %d minutes", user.Username, minutes),
AllowedMentions: &discordgo.MessageAllowedMentions{},
},
})
}
// removeAutoScrubber - Stops scrubbing emojis
func removeAutoScrubber(s *discordgo.Session, i *discordgo.InteractionCreate) {
// Access options in the order provided by the user.
options := i.ApplicationCommandData().Options
optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options))
for _, opt := range options {
optionMap[opt.Name] = opt
}
user := &discordgo.User{}
if opt, ok := optionMap["user"]; ok {
user = opt.UserValue(s)
} else {
slog.Error("Invalid user option provided")
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "No user specified",
AllowedMentions: &discordgo.MessageAllowedMentions{},
},
})
return
}
err := stopScrubbingUser(i.GuildID, user.ID)
if err != nil {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Error stopping scrub on %s: %+v", user.Username, err.Error()),
AllowedMentions: &discordgo.MessageAllowedMentions{},
},
})
return
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Stopped scrubbing %s's emojis", user.Username),
AllowedMentions: &discordgo.MessageAllowedMentions{},
},
})

View file

@ -78,6 +78,9 @@ func (bot *Bot) Start() error {
// Deregister any commands we created
bot.DeregisterCommands()
// Stop scrubbin!
close(scrubberStop)
return nil
}

View file

@ -0,0 +1,51 @@
package db
import (
"time"
)
type AutoScrubber struct {
GuildID string `json:"guild_id"`
UserID string `json:"user_id"`
Duration time.Duration `json:"duration"`
}
// AddAutoScrubber - Add an auto scrubber for a guild/user
func (db *Database) AddAutoScrubber(guildID, userID string, duration time.Duration) error {
_, err := db.db.Exec(
"INSERT IGNORE INTO `auto_scrubber` (`guild_id`, `user_id`, `duration`) VALUES (?,?,?)",
guildID, userID, duration,
)
return err
}
// RemoveAutoScrubber - Delete for guild/channel/message/user
func (db *Database) RemoveAutoScrubber(guildID, userID string) error {
_, err := db.db.Exec(
"DELETE FROM `auto_scrubber` WHERE `guild_id` = ? AND `user_id` = ?",
guildID, userID,
)
return err
}
// DeleteEmojiAll - Delete for whole message
func (db *Database) GetAllAutoScrubbers() ([]AutoScrubber, error) {
data := make([]AutoScrubber, 0)
row, err := db.db.Query("SELECT guild_id, user_id, duration from `auto_scrubber`")
if err != nil {
return data, err
}
defer row.Close()
for row.Next() {
var guildID string
var userID string
var duration time.Duration
row.Scan(&guildID, &userID, &duration)
data = append(data, AutoScrubber{GuildID: guildID, UserID: userID, Duration: duration})
}
return data, nil
}

View file

@ -36,7 +36,6 @@ func InitDb() (*Database, error) {
// runMigrations - Run migrations for connection
func (db *Database) runMigrations() (*Database, error) {
// Hacked up af - Rerunnable
_, err := db.db.Exec("CREATE TABLE IF NOT EXISTS `emoji_usage` (" +
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, " +
"`guild_id` TEXT, " +
@ -61,6 +60,21 @@ func (db *Database) runMigrations() (*Database, error) {
return db, err
}
_, err = db.db.Exec("CREATE TABLE IF NOT EXISTS `auto_scrubber` (" +
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, " +
"`guild_id` TEXT, " +
"`user_id` TEXT, " +
"`duration` INT " +
")")
if err != nil {
return db, err
}
_, err = db.db.Exec("CREATE INDEX IF NOT EXISTS `idx_auto_scrubber_guild_user` ON `auto_scrubber` (`guild_id`, `user_id`)")
if err != nil {
return db, err
}
// emojitest := map[string]string{
// "pepe_analyze": "579431592624390147",
// "kekw": "1317987954102112347",

View file

@ -1,6 +1,9 @@
package db
import "fmt"
import (
"fmt"
"time"
)
type EmojiMap struct {
EmojiID string
@ -9,13 +12,14 @@ type EmojiMap struct {
}
type EmojiUsage struct {
ID int64
GuildID string
ChannelID string
MessageID string
UserID string
EmojiID string
EmojiName string
Timestamp string
Timestamp time.Time
}
// LogEmojiUsage - Log usage
@ -38,6 +42,16 @@ func (db *Database) DeleteEmojiUsage(guildID, channelID, messageID, userID, emoj
return err
}
// DeleteEmojiUsageById - Delete for guild/channel/message/user
func (db *Database) DeleteEmojiUsageById(id int64) error {
_, err := db.db.Exec(
"DELETE FROM `emoji_usage` WHERE `id` = ?",
id,
)
return err
}
// DeleteEmojiAll - Delete for whole message
func (db *Database) DeleteEmojiAll(guildID, channelID, messageID string) error {
_, err := db.db.Exec(
@ -179,3 +193,27 @@ func (db *Database) GetRecentEmojisForUser(guildID string, userID string, hours
return data, nil
}
// GetAllEmojisForUser - Get all emojis used by user map[]
func (db *Database) GetAllEmojisForUser(guildID string, userID string) ([]EmojiUsage, error) {
var data []EmojiUsage
row, err := db.db.Query(
"SELECT guild_id, channel_id, message_id, user_id, emoji_id, emoji_name, timestamp "+
"FROM `emoji_usage` WHERE `guild_id` = ? AND `user_id` = ?",
guildID,
userID,
)
if err != nil {
return data, err
}
defer row.Close()
for row.Next() {
usage := EmojiUsage{}
row.Scan(&usage.GuildID, &usage.ChannelID, &usage.MessageID, &usage.UserID, &usage.EmojiID, &usage.EmojiName, &usage.Timestamp)
data = append(data, usage)
}
return data, nil
}