Instascrub

This commit is contained in:
Daniel Mason 2025-07-15 19:21:11 +12:00
parent bd38b0d91c
commit 659a4a3176
Signed by: idanoo
GPG key ID: 387387CDBC02F132
7 changed files with 169 additions and 225 deletions

View file

@ -1,113 +0,0 @@
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.GetRecentEmojisForUser(guildID, userID, 1)
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
emo := e.EmojiID
if emo == "" {
emo = e.EmojiName
}
err := b.DiscordSession.MessageReactionRemove(e.ChannelID, e.MessageID, emo, e.UserID)
if err != nil {
slog.Error("Error removing emoji reaction", "err", err, "emoji", emo, "user", e.UserID)
}
// 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,7 +5,6 @@ import (
"log/slog" "log/slog"
"sort" "sort"
"strings" "strings"
"time"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
) )
@ -47,8 +46,8 @@ var (
}, },
}, },
{ {
Name: "add-auto-scrubber", Name: "add-magic-tool",
Description: "Auto Scrub Emoijis after a set period", Description: "Runs a script on reaction for user",
DefaultMemberPermissions: &defaultRunCommandPermissions, DefaultMemberPermissions: &defaultRunCommandPermissions,
Options: []*discordgo.ApplicationCommandOption{ Options: []*discordgo.ApplicationCommandOption{
{ {
@ -57,17 +56,11 @@ var (
Description: "Select user", Description: "Select user",
Required: true, Required: true,
}, },
{
Type: discordgo.ApplicationCommandOptionInteger,
Name: "minutes",
Description: "Every X minutes",
Required: true,
},
}, },
}, },
{ {
Name: "remove-auto-scrubber", Name: "remove-magic-tool",
Description: "Stop autoscrubbing emojis after a set period", Description: "Stops running a script on reaction for user",
DefaultMemberPermissions: &defaultRunCommandPermissions, DefaultMemberPermissions: &defaultRunCommandPermissions,
Options: []*discordgo.ApplicationCommandOption{ Options: []*discordgo.ApplicationCommandOption{
{ {
@ -81,10 +74,10 @@ var (
} }
commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"show-top-emojis": showTopEmojis, "show-top-emojis": showTopEmojis,
"show-top-users": showTopUsers, "show-top-users": showTopUsers,
"add-auto-scrubber": addAutoScrubber, "add-magic-tool": addAutoScrubber,
"remove-auto-scrubber": removeAutoScrubber, "remove-magic-tool": removeAutoScrubber,
} }
) )
@ -195,7 +188,7 @@ func showTopUsers(s *discordgo.Session, i *discordgo.InteractionCreate) {
// Sort keys // Sort keys
keys := make([]int, 0) keys := make([]int, 0)
for k, _ := range top { for k := range top {
keys = append(keys, k) keys = append(keys, k)
} }
sort.Ints(keys) sort.Ints(keys)
@ -260,29 +253,13 @@ func addAutoScrubber(s *discordgo.Session, i *discordgo.InteractionCreate) {
return return
} }
var minutes int64 err := startScrubbingUser(i.GuildID, user.ID)
if opt, ok := optionMap["minutes"]; ok {
minutes = opt.IntValue()
} else {
slog.Error("Invalid time option provided")
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "No minutes specified",
AllowedMentions: &discordgo.MessageAllowedMentions{},
},
})
return
}
dur := time.Duration(minutes) * time.Minute
err := startScrubbingUser(i.GuildID, user.ID, dur)
if err != nil { if err != nil {
slog.Error("Error starting auto scrubber", "err", err, "guild_id", i.GuildID, "user_id", user.ID, "duration", dur) slog.Error("Error starting auto scrubber", "err", err, "guild_id", i.GuildID, "user_id", user.ID)
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Error auto scrubbing user %s: %+v", user.Username, err.Error()), Content: ":x:",
AllowedMentions: &discordgo.MessageAllowedMentions{}, AllowedMentions: &discordgo.MessageAllowedMentions{},
}, },
}) })
@ -293,7 +270,7 @@ func addAutoScrubber(s *discordgo.Session, i *discordgo.InteractionCreate) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Will remove %s's emojis after %d minutes", user.Username, minutes), Content: ":white_check_mark:",
AllowedMentions: &discordgo.MessageAllowedMentions{}, AllowedMentions: &discordgo.MessageAllowedMentions{},
}, },
}) })
@ -328,7 +305,7 @@ func removeAutoScrubber(s *discordgo.Session, i *discordgo.InteractionCreate) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Error stopping scrub on %s: %+v", user.Username, err.Error()), Content: ":x:",
AllowedMentions: &discordgo.MessageAllowedMentions{}, AllowedMentions: &discordgo.MessageAllowedMentions{},
}, },
}) })
@ -339,7 +316,7 @@ func removeAutoScrubber(s *discordgo.Session, i *discordgo.InteractionCreate) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Stopped scrubbing %s's emojis", user.Username), Content: ":white_check_mark:",
AllowedMentions: &discordgo.MessageAllowedMentions{}, AllowedMentions: &discordgo.MessageAllowedMentions{},
}, },
}) })

View file

@ -4,6 +4,7 @@ import (
"log/slog" "log/slog"
"os" "os"
"os/signal" "os/signal"
"sync"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/idanoo/GoDiscMoji/internal/db" "github.com/idanoo/GoDiscMoji/internal/db"
@ -19,6 +20,10 @@ type Bot struct {
registeredCommands []*discordgo.ApplicationCommand registeredCommands []*discordgo.ApplicationCommand
Db *db.Database Db *db.Database
// Scrub map[GuildID][UserID]bool
scrubs *map[string]map[string]bool
scrubsMutex sync.RWMutex
} }
// New - Return new instance of *Bot // New - Return new instance of *Bot
@ -69,8 +74,8 @@ func (bot *Bot) Start() error {
bot.RegisterCommands() bot.RegisterCommands()
b = bot b = bot
// Add scrubber // Add scrubs
initScrubber() initScrub()
// Keep running untill there is NO os interruption (ctrl + C) // Keep running untill there is NO os interruption (ctrl + C)
slog.Info("Bot is now running. Press CTRL-C to exit.") slog.Info("Bot is now running. Press CTRL-C to exit.")
@ -81,9 +86,6 @@ func (bot *Bot) Start() error {
// Deregister any commands we created // Deregister any commands we created
bot.DeregisterCommands() bot.DeregisterCommands()
// Stop scrubbin!
close(scrubberStop)
return nil return nil
} }
@ -94,6 +96,15 @@ func (bot *Bot) HandleAddReaction(discord *discordgo.Session, reaction *discordg
return return
} }
if scrub.shouldScrub(reaction.GuildID, reaction.UserID) {
err := b.DiscordSession.MessageReactionRemove(reaction.ChannelID, reaction.MessageID, reaction.Emoji.ID, reaction.UserID)
if err != nil {
slog.Error("Error removing emoji reaction", "err", err, "reaction", reaction)
}
return
}
err := bot.Db.LogEmojiUsage(reaction.GuildID, reaction.ChannelID, reaction.MessageID, reaction.UserID, reaction.Emoji.ID, reaction.Emoji.Name) err := bot.Db.LogEmojiUsage(reaction.GuildID, reaction.ChannelID, reaction.MessageID, reaction.UserID, reaction.Emoji.ID, reaction.Emoji.Name)
if err != nil { if err != nil {
slog.Error("Failed to log emoji usage", "err", err) slog.Error("Failed to log emoji usage", "err", err)

84
src/internal/bot/scrub.go Normal file
View file

@ -0,0 +1,84 @@
package bot
import (
"log/slog"
"sync"
)
type Scrubber struct {
scrubs map[string]map[string]bool
mutex sync.RWMutex
}
var scrub *Scrubber
// shouldScrub - Check if a user is scrubbing in a guild
func (s *Scrubber) shouldScrub(guildID string, userID string) bool {
s.mutex.RLock()
defer s.mutex.RUnlock()
if _, ok := s.scrubs[guildID]; !ok {
return false
}
if _, ok := s.scrubs[guildID][userID]; !ok {
return false
}
return true
}
// initScrub - Loads the scrubber configs from the DB
func initScrub() error {
s := Scrubber{
scrubs: make(map[string]map[string]bool),
mutex: sync.RWMutex{},
}
scrub = &s
// Load config
allScrubbers, err := b.Db.GetAllScrubs()
if err != nil {
return err
}
// map[GuildID][UserID]bool
scrubs := make(map[string]map[string]bool)
for _, scrubber := range allScrubbers {
if _, ok := scrubs[scrubber.GuildID]; !ok {
scrubs[scrubber.GuildID] = make(map[string]bool)
}
scrubs[scrubber.GuildID][scrubber.UserID] = true
}
scrub.scrubs = scrubs
return nil
}
// startScrubbingUser - Start an auto scrubber for a user in a guild
func (s *Scrubber) startScrubbingUser(guildID string, userID string) error {
err := b.Db.AddScrub(guildID, userID)
if err != nil {
slog.Error("Failed to add auto scrubber", "err", err)
return err
}
// Add to map
if _, ok := s.scrubs[guildID]; !ok {
s.scrubs[guildID] = make(map[string]bool)
}
s.scrubs[guildID][userID] = true
return nil
}
// stopScrubbingUser - Stop an auto scrubber for a user in a guild
func (s *Scrubber) stopScrubbingUser(guildID string, userID string) error {
s.mutex.Lock()
defer s.mutex.Unlock()
delete(s.scrubs[guildID], userID)
return b.Db.RemoveScrub(guildID, userID)
}

View file

@ -1,51 +0,0 @@
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 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

@ -60,35 +60,26 @@ func (db *Database) runMigrations() (*Database, error) {
return db, err return db, err
} }
_, err = db.db.Exec("CREATE TABLE IF NOT EXISTS `auto_scrubber` (" + // Clean up old tables
_, err = db.db.Exec("DROP TABLE IF EXISTS `auto_scrubber`")
if err != nil {
return db, err
}
_, err = db.db.Exec("CREATE TABLE IF NOT EXISTS `scrub` (" +
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, " + "`id` INTEGER PRIMARY KEY AUTOINCREMENT, " +
"`guild_id` TEXT, " + "`guild_id` TEXT, " +
"`user_id` TEXT, " + "`user_id` TEXT " +
"`duration` INT " +
")") ")")
if err != nil { if err != nil {
return db, err return db, err
} }
_, err = db.db.Exec("CREATE INDEX IF NOT EXISTS `idx_auto_scrubber_guild_user` ON `auto_scrubber` (`guild_id`, `user_id`)") _, err = db.db.Exec("CREATE INDEX IF NOT EXISTS `idx_scrub_guildid_userid` ON `scrub` (`guild_id`, `user_id`)")
if err != nil { if err != nil {
return db, err return db, err
} }
// emojitest := map[string]string{
// "pepe_analyze": "579431592624390147",
// "kekw": "1317987954102112347",
// "pepe_kek": "1300985103182336010",
// "KEKW": "649918246119669770",
// }
// for name, id := range emojitest {
// _, err = db.db.Exec("UPDATE `emoji_usage` SET `emoji_id` = '" + id + "' WHERE `emoji_name` = '" + name + "'")
// if err != nil {
// return db, err
// }
// }
return db, nil return db, nil
} }

45
src/internal/db/scrub.go Normal file
View file

@ -0,0 +1,45 @@
package db
type Scrub struct {
GuildID string `json:"guild_id"`
UserID string `json:"user_id"`
}
// AddScrub - Add an auto scrubber for a guild/user
func (db *Database) AddScrub(guildID, userID string) error {
_, err := db.db.Exec(
"INSERT INTO `scrub` (`guild_id`, `user_id`) VALUES (?,?)",
guildID, userID,
)
return err
}
// RemoveScrub - Delete for guild/channel/message/user
func (db *Database) RemoveScrub(guildID, userID string) error {
_, err := db.db.Exec(
"DELETE FROM `scrub` WHERE `guild_id` = ? AND `user_id` = ?",
guildID, userID,
)
return err
}
// GetAllScrubs - Get all scrubbers
func (db *Database) GetAllScrubs() ([]Scrub, error) {
data := make([]Scrub, 0)
row, err := db.db.Query("SELECT guild_id, user_id from `scrub`")
if err != nil {
return data, err
}
defer row.Close()
for row.Next() {
var guildID string
var userID string
row.Scan(&guildID, &userID)
data = append(data, Scrub{GuildID: guildID, UserID: userID})
}
return data, nil
}