From cd248dcaef9c1c1a045ea6d3a90b689a6a57f916 Mon Sep 17 00:00:00 2001 From: Daniel Mason Date: Mon, 23 Jun 2025 21:14:01 +1200 Subject: [PATCH] Add command --- README.md | 1 + src/go.mod | 8 +-- src/go.sum | 8 +++ src/internal/bot/commands.go | 107 ++++++++++++++++++++++++++++++--- src/internal/bot/main.go | 4 +- src/internal/db/database.go | 1 + src/internal/db/emoji_usage.go | 65 ++++++++++++++++---- 7 files changed, 166 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index e029932..d9beb48 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,4 @@ Discord bot to measure emoji usage /show-top-emojis # Shows top 5 emojis and their 3 biggest users ``` + diff --git a/src/go.mod b/src/go.mod index 43443be..dd030b9 100644 --- a/src/go.mod +++ b/src/go.mod @@ -3,13 +3,13 @@ module github.com/idanoo/GoDiscMoji go 1.23.4 require ( - github.com/bwmarrin/discordgo v0.28.1 + github.com/bwmarrin/discordgo v0.29.0 github.com/golang-migrate/migrate v3.5.4+incompatible - github.com/mattn/go-sqlite3 v1.14.24 + github.com/mattn/go-sqlite3 v1.14.28 ) require ( github.com/gorilla/websocket v1.5.3 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/sys v0.31.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/sys v0.33.0 // indirect ) diff --git a/src/go.sum b/src/go.sum index 0e05d0b..03b4a85 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,5 +1,7 @@ github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno= +github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= @@ -8,6 +10,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= @@ -16,6 +20,8 @@ golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -25,6 +31,8 @@ golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/src/internal/bot/commands.go b/src/internal/bot/commands.go index 9f9531e..2bfbca2 100644 --- a/src/internal/bot/commands.go +++ b/src/internal/bot/commands.go @@ -11,7 +11,6 @@ import ( var ( integerOptionMinValue = 1.0 - amountKey = "amount" defaultRunCommandPermissions int64 = discordgo.PermissionKickMembers @@ -23,7 +22,7 @@ var ( Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionInteger, - Name: amountKey, + Name: "amount", Description: "Amount to show", MinValue: &integerOptionMinValue, MaxValue: 20, @@ -38,7 +37,7 @@ var ( Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionInteger, - Name: amountKey, + Name: "amount", Description: "Amount to show", MinValue: &integerOptionMinValue, MaxValue: 20, @@ -46,11 +45,31 @@ var ( }, }, }, + { + Name: "purge-recent-emojis", + Description: "Purges recent emojis", + DefaultMemberPermissions: &defaultRunCommandPermissions, + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionUser, + Name: "user", + Description: "Select user", + Required: true, + }, + { + Type: discordgo.ApplicationCommandOptionInteger, + Name: "hours", + Description: "Hours to purge", + Required: true, + }, + }, + }, } commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ - "show-top-emojis": showTopEmojis, - "show-top-users": showTopUsers, + "show-top-emojis": showTopEmojis, + "show-top-users": showTopUsers, + "purge-recent-emojis": purgeRecentEmojis, } ) @@ -87,7 +106,7 @@ func showTopEmojis(s *discordgo.Session, i *discordgo.InteractionCreate) { } amount := int64(5) - if opt, ok := optionMap[amountKey]; ok { + if opt, ok := optionMap["amount"]; ok { amount = opt.IntValue() } @@ -118,7 +137,7 @@ func showTopEmojis(s *discordgo.Session, i *discordgo.InteractionCreate) { sort.Ints(subkeys) users := []string{} - msg += fmt.Sprintf("%s: %d", top[v].EmojiID, top[v].Count) + msg += fmt.Sprintf("<:%s:%s> %d", top[v].EmojiName, top[v].EmojiID, top[v].Count) for _, sv := range subkeys { users = append(users, fmt.Sprintf("<@%s>: %d", topUsers[sv].EmojiID, topUsers[sv].Count)) } @@ -144,7 +163,7 @@ func showTopUsers(s *discordgo.Session, i *discordgo.InteractionCreate) { } amount := int64(5) - if opt, ok := optionMap[amountKey]; ok { + if opt, ok := optionMap["amount"]; ok { amount = opt.IntValue() } @@ -178,7 +197,7 @@ func showTopUsers(s *discordgo.Session, i *discordgo.InteractionCreate) { users := []string{} msg += fmt.Sprintf("<@%s>: %d", top[v].EmojiID, top[v].Count) for _, sv := range subkeys { - users = append(users, fmt.Sprintf("%s: %d", topUsers[sv].EmojiID, topUsers[sv].Count)) + users = append(users, fmt.Sprintf("<:%s:%s> %d", topUsers[sv].EmojiName, topUsers[sv].EmojiID, topUsers[sv].Count)) } msg += " (" + strings.Join(users, ", ") + ")\n" } @@ -191,3 +210,73 @@ func showTopUsers(s *discordgo.Session, i *discordgo.InteractionCreate) { }, }) } + +// purgeRecentEmojis - Purges recent emojis for a user +func purgeRecentEmojis(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 + } + + hours := int64(24) + if opt, ok := optionMap["hours"]; ok { + hours = opt.IntValue() + } else { + slog.Error("Invalid hours option provided") + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "No hours specified", + AllowedMentions: &discordgo.MessageAllowedMentions{}, + }, + }) + return + } + + emojis, err := b.Db.GetRecentEmojisForUser(i.GuildID, user.ID, hours) + if err != nil { + slog.Error("Error getting recent emojis for user", "err", err) + return + } + + x := 0 + for _, emoji := range emojis { + err := s.MessageReactionRemove(emoji.ChannelID, emoji.MessageID, emoji.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), + AllowedMentions: &discordgo.MessageAllowedMentions{}, + }, + }) +} diff --git a/src/internal/bot/main.go b/src/internal/bot/main.go index bc1e0ce..cf49644 100644 --- a/src/internal/bot/main.go +++ b/src/internal/bot/main.go @@ -88,7 +88,7 @@ func (bot *Bot) HandleAddReaction(discord *discordgo.Session, reaction *discordg return } - err := bot.Db.LogEmojiUsage(reaction.GuildID, reaction.ChannelID, reaction.MessageID, reaction.UserID, reaction.Emoji.Name) + err := bot.Db.LogEmojiUsage(reaction.GuildID, reaction.ChannelID, reaction.MessageID, reaction.UserID, reaction.Emoji.ID, reaction.Emoji.Name) if err != nil { slog.Error("Failed to log emoji usage", "err", err) } @@ -101,7 +101,7 @@ func (bot *Bot) HandleRemoveReaction(discord *discordgo.Session, reaction *disco return } - err := bot.Db.DeleteEmojiUsage(reaction.GuildID, reaction.ChannelID, reaction.MessageID, reaction.UserID, reaction.Emoji.Name) + err := bot.Db.DeleteEmojiUsage(reaction.GuildID, reaction.ChannelID, reaction.MessageID, reaction.UserID, reaction.Emoji.ID) if err != nil { slog.Error("Failed to delete single emoji usage", "err", err) } diff --git a/src/internal/db/database.go b/src/internal/db/database.go index 691ac40..6366cda 100644 --- a/src/internal/db/database.go +++ b/src/internal/db/database.go @@ -44,6 +44,7 @@ func (db *Database) runMigrations() (*Database, error) { "`message_id` TEXT, " + "`user_id` TEXT, " + "`emoji_id` TEXT, " + + "`emoji_name` TEXT, " + "`timestamp` DATETIME" + ")") if err != nil { diff --git a/src/internal/db/emoji_usage.go b/src/internal/db/emoji_usage.go index 4c388c0..325121f 100644 --- a/src/internal/db/emoji_usage.go +++ b/src/internal/db/emoji_usage.go @@ -1,15 +1,27 @@ package db +import "fmt" + type EmojiMap struct { - EmojiID string - Count int64 + EmojiID string + EmojiName string + Count int64 +} + +type EmojiUsage struct { + GuildID string + ChannelID string + MessageID string + UserID string + EmojiID string + Timestamp string } // LogEmojiUsage - Log usage -func (db *Database) LogEmojiUsage(guildID, channelID, messageID, userID, emojiID string) error { +func (db *Database) LogEmojiUsage(guildID, channelID, messageID, userID, emojiID, emojiName string) error { _, err := db.db.Exec( - "INSERT INTO `emoji_usage` (`guild_id`, `channel_id`, `message_id`, `user_id`, `emoji_id`, `timestamp`) VALUES (?,?,?,?,?, datetime())", - guildID, channelID, messageID, userID, emojiID, + "INSERT INTO `emoji_usage` (`guild_id`, `channel_id`, `message_id`, `user_id`, `emoji_id`, `emoji_name`, `timestamp`) VALUES (?,?,?,?,?,?, datetime())", + guildID, channelID, messageID, userID, emojiID, emojiName, ) return err @@ -91,7 +103,7 @@ func (db *Database) GetTopUsersForGuildEmoji(guildID string, emojiID string, num func (db *Database) GetTopEmojisForGuild(guildID string, num int64) (map[int]EmojiMap, error) { data := make(map[int]EmojiMap) row, err := db.db.Query( - "SELECT emoji_id, count(*) FROM `emoji_usage` WHERE `guild_id` = ? GROUP BY emoji_id ORDER BY count(*) DESC LIMIT ?", + "SELECT emoji_name, emoji_id, count(*) FROM `emoji_usage` WHERE `guild_id` = ? GROUP BY emoji_id ORDER BY count(*) DESC LIMIT ?", guildID, num, ) @@ -103,10 +115,11 @@ func (db *Database) GetTopEmojisForGuild(guildID string, num int64) (map[int]Emo defer row.Close() i := 0 for row.Next() { - var emoji string + var emojiName string + var emojiID string var count int64 - row.Scan(&emoji, &count) - data[i] = EmojiMap{EmojiID: emoji, Count: count} + row.Scan(&emojiName, &emojiID, &count) + data[i] = EmojiMap{EmojiID: emojiID, EmojiName: emojiName, Count: count} i++ } @@ -117,7 +130,7 @@ func (db *Database) GetTopEmojisForGuild(guildID string, num int64) (map[int]Emo func (db *Database) GetTopEmojisForGuildUser(guildID string, userID string, num int) (map[int]EmojiMap, error) { data := make(map[int]EmojiMap) row, err := db.db.Query( - "SELECT emoji_id, count(*) FROM `emoji_usage` WHERE `guild_id` = ? AND `user_id` = ? GROUP BY emoji_id ORDER BY count(*) DESC LIMIT ?", + "SELECT emoji_name, emoji_id, count(*) FROM `emoji_usage` WHERE `guild_id` = ? AND `user_id` = ? GROUP BY emoji_name ORDER BY count(*) DESC LIMIT ?", guildID, userID, num, @@ -130,12 +143,38 @@ func (db *Database) GetTopEmojisForGuildUser(guildID string, userID string, num defer row.Close() i := 0 for row.Next() { - var emoji string + var emojiName string + var emojiID string var count int64 - row.Scan(&emoji, &count) - data[i] = EmojiMap{EmojiID: emoji, Count: count} + row.Scan(&emojiName, &emojiID, &count) + data[i] = EmojiMap{EmojiID: emojiID, EmojiName: emojiName, Count: count} i++ } return data, nil } + +// GetRecentEmojisForUser - Get recent emojis used by user map[] +func (db *Database) GetRecentEmojisForUser(guildID string, userID string, hours int64) ([]EmojiUsage, error) { + var data []EmojiUsage + row, err := db.db.Query( + "SELECT guild_id, channel_id, message_id, user_id, emoji_id, timestamp "+ + "FROM `emoji_usage` WHERE `guild_id` = ? AND `user_id` = ? AND timestamp >= datetime('now', '-"+fmt.Sprintf("%d", hours)+" hours') "+ + "ORDER BY timestamp DESC", + 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.Timestamp) + data = append(data, usage) + } + + return data, nil +}