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)) } }