unbot/features/xdtracker.go
2026-06-02 04:22:00 +02:00

220 lines
5.3 KiB
Go

package features
import (
"fmt"
"log/slog"
"strings"
"time"
"github.com/disgoorg/disgo/bot"
"github.com/disgoorg/disgo/discord"
"github.com/disgoorg/disgo/events"
"github.com/disgoorg/snowflake/v2"
"github.com/elisiei/unbot/db"
)
type XDTracker struct {
db *db.DB
config XDTrackerConfig
client *bot.Client
guildID snowflake.ID
}
type XDTrackerConfig struct {
Enabled bool
}
func NewXDTracker(database *db.DB, client *bot.Client, guildID snowflake.ID, enabled bool) *XDTracker {
return &XDTracker{
db: database,
client: client,
guildID: guildID,
config: XDTrackerConfig{Enabled: enabled},
}
}
func (x *XDTracker) HandleMessage(e *events.MessageCreate) {
if e.Message.Author.Bot {
return
}
msgID := e.Message.ID.String()
processed, err := x.db.IsXDMessageProcessed(msgID)
if err != nil || processed {
return
}
content := strings.ToLower(e.Message.Content)
count := strings.Count(content, "xd")
if count == 0 {
return
}
if err := x.db.IncrementXD(e.Message.Author.ID.String(), count); err != nil {
slog.Error("failed to increment xd count", slog.Any("err", err))
return
}
if err := x.db.MarkXDMessageProcessed(msgID); err != nil {
slog.Error("failed to mark message as processed", slog.Any("err", err))
}
}
func (x *XDTracker) Command() discord.SlashCommandCreate {
return discord.SlashCommandCreate{
Name: "xd",
Description: "XD tracker commands",
Options: []discord.ApplicationCommandOption{
discord.ApplicationCommandOptionSubCommand{
Name: "leaderboard",
Description: "Show XD leaderboard",
},
discord.ApplicationCommandOptionSubCommand{
Name: "stats",
Description: "Show your XD stats",
},
},
}
}
func (x *XDTracker) HandleLeaderboard(e *events.ApplicationCommandInteractionCreate) {
entries, err := x.db.GetXDLeaderboard(10)
if err != nil {
e.CreateMessage(discord.NewMessageCreate().WithContent("Failed to get leaderboard.").WithEphemeral(true))
return
}
if len(entries) == 0 {
e.CreateMessage(discord.NewMessageCreate().WithContent("No XD data yet.").WithEphemeral(true))
return
}
total, err := x.db.GetTotalXDCount()
if err != nil {
total = 0
}
userCount, err := x.db.GetXDUserCount()
if err != nil {
userCount = 0
}
var sb strings.Builder
for i, entry := range entries {
sb.WriteString(fmt.Sprintf("%d. <@%s> - **%d** xd\n", i+1, entry.UserID, entry.Count))
}
sb.WriteString(fmt.Sprintf("\nTotal: **%d** xd across **%d** users", total, userCount))
e.CreateMessage(discord.NewMessageCreate().WithEmbeds(discord.NewEmbed().WithTitle("xd leaderboard").WithDescription(sb.String())))
}
func (x *XDTracker) HandleStats(e *events.ApplicationCommandInteractionCreate) {
userID := e.User().ID.String()
count, err := x.db.GetXDCount(userID)
if err != nil {
e.CreateMessage(discord.NewMessageCreate().WithContent("Failed to get stats.").WithEphemeral(true))
return
}
total, err := x.db.GetTotalXDCount()
if err != nil {
total = 0
}
userCount, err := x.db.GetXDUserCount()
if err != nil {
userCount = 0
}
rank, err := x.db.GetUserRank(userID)
if err != nil {
rank = 0
}
var pct string
if total > 0 {
pct = fmt.Sprintf(" (%.1f%%)", float64(count)/float64(total)*100)
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("**<@%s>'s XD Stats**\n", userID))
sb.WriteString(fmt.Sprintf("Rank: **#%d** of **%d**\n", rank, userCount))
sb.WriteString(fmt.Sprintf("Messages: **%d** xd%s\n", count, pct))
sb.WriteString(fmt.Sprintf("Server total: **%d** xd", total))
e.CreateMessage(discord.NewMessageCreate().WithContent(sb.String()))
}
func (x *XDTracker) BackfillHistory() {
if !x.config.Enabled {
return
}
slog.Info("starting XD history backfill")
channels, err := x.client.Rest.GetGuildChannels(x.guildID)
if err != nil {
slog.Error("failed to get guild channels for backfill", slog.Any("err", err))
return
}
for _, ch := range channels {
switch ch.Type() {
case discord.ChannelTypeGuildText, discord.ChannelTypeGuildNews:
go x.backfillChannel(ch.ID())
}
}
slog.Info("XD history backfill complete")
}
func (x *XDTracker) backfillChannel(channelID snowflake.ID) {
var lastID snowflake.ID
total := 0
for {
messages, err := x.client.Rest.GetMessages(channelID, 0, lastID, 0, 100)
if err != nil {
slog.Error("failed to get messages for backfill", slog.String("channel_id", channelID.String()), slog.Any("err", err))
return
}
if len(messages) == 0 {
break
}
for _, msg := range messages {
if msg.Author.Bot {
continue
}
msgID := msg.ID.String()
processed, err := x.db.IsXDMessageProcessed(msgID)
if err != nil || processed {
continue
}
content := strings.ToLower(msg.Content)
if count := strings.Count(content, "xd"); count > 0 {
if err := x.db.IncrementXD(msg.Author.ID.String(), count); err != nil {
slog.Error("failed to increment xd count during backfill", slog.Any("err", err))
continue
}
}
if err := x.db.MarkXDMessageProcessed(msgID); err != nil {
slog.Error("failed to mark message as processed", slog.Any("err", err))
}
}
total += len(messages)
lastID = messages[len(messages)-1].ID
time.Sleep(200 * time.Millisecond)
}
if total > 0 {
slog.Info("backfilled channel", slog.String("channel_id", channelID.String()), slog.Int("messages", total))
}
}