From 75e813570b28e43c028f25b68a987d8d1430111e Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 14:10:49 -0500 Subject: [PATCH 01/25] Updating README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 122d1ae1..e168fe18 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,7 @@ OmegaBot is online │ └── omegabot.png ├── data │ ├── faqs.json +│ ├── fun-usage.json │ ├── github-assignees.json │ ├── guild-config.json │ ├── last-seen.json @@ -231,8 +232,12 @@ OmegaBot is online │ │ │ │ ├── coinflip.ts │ │ │ │ ├── dadjoke.ts │ │ │ │ ├── dice.ts +│ │ │ │ ├── java.ts +│ │ │ │ ├── leaderboard.ts +│ │ │ │ ├── poll.ts │ │ │ │ └── weather.ts -│ │ │ └── fun.ts +│ │ │ ├── fun.ts +│ │ │ └── funUsageStore.ts │ │ ├── general │ │ │ └── ping.ts │ │ ├── github From 1b5fa074ff14b9d526f7ebcb66d107731f1f50d6 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 16:15:55 -0500 Subject: [PATCH 02/25] Fixing somet things --- README.md | 5 +- src/commands/fun/fun.ts | 2 +- src/commands/fun/funUsageStore.ts | 128 ------------ src/commands/fun/subcommands/leaderboard.ts | 204 ++++++++++++-------- src/services/fun/funUsageStore.ts | 111 +++++++++++ 5 files changed, 239 insertions(+), 211 deletions(-) delete mode 100644 src/commands/fun/funUsageStore.ts create mode 100644 src/services/fun/funUsageStore.ts diff --git a/README.md b/README.md index e168fe18..58b17a7c 100644 --- a/README.md +++ b/README.md @@ -236,8 +236,7 @@ OmegaBot is online │ │ │ │ ├── leaderboard.ts │ │ │ │ ├── poll.ts │ │ │ │ └── weather.ts -│ │ │ ├── fun.ts -│ │ │ └── funUsageStore.ts +│ │ │ └── fun.ts │ │ ├── general │ │ │ └── ping.ts │ │ ├── github @@ -278,6 +277,8 @@ OmegaBot is online │ │ │ ├── store.test.ts │ │ │ ├── store.ts │ │ │ └── types.ts +│ │ ├── fun +│ │ │ └── funUsageStore.ts │ │ ├── github │ │ │ ├── githubApi.ts │ │ │ ├── githubClient.ts diff --git a/src/commands/fun/fun.ts b/src/commands/fun/fun.ts index f26ce7f7..04850744 100644 --- a/src/commands/fun/fun.ts +++ b/src/commands/fun/fun.ts @@ -25,7 +25,7 @@ import { run as runJava } from "./subcommands/java.js"; import { run as runLeaderboard } from "./subcommands/leaderboard.js"; import type { LeaderboardMode } from "./subcommands/leaderboard.js"; -import { recordFunUsage, type FunCommandKey } from "./funUsageStore.js"; +import { recordFunUsage, type FunCommandKey } from "../../services/fun/funUsageStore.js"; function parseTempUnit(raw: string | null): TempUnit { return raw?.toLowerCase() === "c" ? "c" : "f"; diff --git a/src/commands/fun/funUsageStore.ts b/src/commands/fun/funUsageStore.ts deleted file mode 100644 index a93f7630..00000000 --- a/src/commands/fun/funUsageStore.ts +++ /dev/null @@ -1,128 +0,0 @@ -// src/services/fun/funUsageStore.ts - -import { promises as fs } from "node:fs"; -import path from "node:path"; - -export type FunCommandKey = - | "chucknorris" - | "dadjoke" - | "dice" - | "weather" - | "weather7" - | "coinflip" - | "poll" - | "java" - | "leaderboard"; - -export type FunUsageSnapshot = { - updatedAt: string; - totalsByCommand: Record; - totalsByUser: Record; - byUserByCommand: Record>; -}; - -type FunUsageFile = { - version: 1; - updatedAt: string; - totalsByCommand: Record; - totalsByUser: Record; - byUserByCommand: Record>; -}; - -const DATA_DIR = path.join(process.cwd(), "data"); -const STATE_PATH = path.join(DATA_DIR, "fun-usage.json"); - -async function ensureDataDir(): Promise { - await fs.mkdir(DATA_DIR, { recursive: true }); -} - -function emptyState(): FunUsageFile { - return { - version: 1, - updatedAt: new Date().toISOString(), - totalsByCommand: {}, - totalsByUser: {}, - byUserByCommand: {}, - }; -} - -async function loadState(): Promise { - try { - const raw = await fs.readFile(STATE_PATH, "utf8"); - const parsed = JSON.parse(raw) as FunUsageFile; - - if (!parsed || typeof parsed !== "object") return emptyState(); - if (parsed.version !== 1) return emptyState(); - - return { - version: 1, - updatedAt: - typeof parsed.updatedAt === "string" - ? parsed.updatedAt - : new Date().toISOString(), - totalsByCommand: - parsed.totalsByCommand && typeof parsed.totalsByCommand === "object" - ? parsed.totalsByCommand - : {}, - totalsByUser: - parsed.totalsByUser && typeof parsed.totalsByUser === "object" - ? parsed.totalsByUser - : {}, - byUserByCommand: - parsed.byUserByCommand && typeof parsed.byUserByCommand === "object" - ? parsed.byUserByCommand - : {}, - }; - } catch { - return emptyState(); - } -} - -async function saveState(state: FunUsageFile): Promise { - await ensureDataDir(); - await fs.writeFile(STATE_PATH, JSON.stringify(state, null, 2), "utf8"); -} - -function inc(map: Record, key: string, by = 1): void { - const cur = map[key] ?? 0; - map[key] = cur + by; -} - -function incNested( - map: Record>, - outer: string, - inner: string, - by = 1, -): void { - if (!map[outer]) map[outer] = {}; - inc(map[outer], inner, by); -} - -export async function recordFunUsage(args: { - userId: string; - command: FunCommandKey; -}): Promise { - const { userId, command } = args; - - const state = await loadState(); - inc(state.totalsByCommand, command, 1); - inc(state.totalsByUser, userId, 1); - incNested(state.byUserByCommand, userId, command, 1); - - state.updatedAt = new Date().toISOString(); - await saveState(state); -} - -export async function getFunUsageSnapshot(): Promise { - const state = await loadState(); - return { - updatedAt: state.updatedAt, - totalsByCommand: state.totalsByCommand, - totalsByUser: state.totalsByUser, - byUserByCommand: state.byUserByCommand, - }; -} - -export async function resetFunUsage(): Promise { - await saveState(emptyState()); -} diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index e3ec1133..9232642d 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,128 +1,172 @@ // src/commands/fun/subcommands/leaderboard.ts -import type { ChatInputCommandInteraction, User } from "discord.js"; +import type { ChatInputCommandInteraction } from "discord.js"; import { EmbedBuilder } from "discord.js"; -import { getFunUsageSnapshot } from "../funUsageStore.js"; +import { getFunUsageSnapshot, type FunCommandKey } from "../../../services/fun/funUsageStore.js"; +import { logger } from "../../../utils/logger.js"; export type LeaderboardMode = | { kind: "users"; limit: number } | { kind: "commands"; limit: number } | { kind: "user"; userId: string }; -type RankedItem = { key: string; count: number }; +type UserRow = { + userId: string; + total: number; +}; -function rank(map: Record, limit: number): RankedItem[] { - return Object.entries(map) - .map(([key, count]) => ({ key, count })) - .sort((a, b) => b.count - a.count) - .slice(0, limit); +type CommandRow = { + command: FunCommandKey; + total: number; +}; + +function clamp(n: number, min: number, max: number): number { + return Math.max(min, Math.min(max, n)); +} + +function sortDesc(items: T[], getValue: (t: T) => number): T[] { + return [...items].sort((a, b) => getValue(b) - getValue(a)); } -function medals(idx: number): string { +function formatCmd(cmd: string): string { + // Make it a little prettier in output + // weather7 -> weather7, chucknorris -> chucknorris etc + return cmd; +} + +function medal(idx: number): string { if (idx === 0) return "🥇"; if (idx === 1) return "🥈"; if (idx === 2) return "🥉"; - return `${idx + 1}.`; + return "🏅"; } async function safeFetchUser( interaction: ChatInputCommandInteraction, userId: string, -): Promise { +): Promise<{ id: string; tag: string; avatarUrl: string } | null> { try { - return await interaction.client.users.fetch(userId); + const u = await interaction.client.users.fetch(userId); + return { + id: u.id, + tag: u.tag, + avatarUrl: u.displayAvatarURL({ size: 128 }), + }; } catch { return null; } } -function prettyCommandName(cmd: string): string { - // Keep it simple and readable in output - return cmd.startsWith("fun ") ? cmd : `fun ${cmd}`; +function buildUpdatedLine(updatedAt: string | null): string { + const ts = updatedAt ?? new Date().toISOString(); + return `Updated: ${ts}`; } export async function run( interaction: ChatInputCommandInteraction, mode: LeaderboardMode, ): Promise { - const snapshot = await getFunUsageSnapshot(); - - const totalEvents = Object.values(snapshot.totalsByCommand).reduce( - (sum: number, n: number) => sum + n, - 0, - ); - - if (totalEvents === 0) { - await interaction.editReply( - [`Updated: ${snapshot.updatedAt}`, "", "No fun command usage recorded yet."].join( - "\n", - ), - ); - return; - } + try { + const store = await getFunUsageSnapshot(); + + if (!store || Object.keys(store.users ?? {}).length === 0) { + await interaction.editReply( + ["No fun command usage recorded yet.", "", buildUpdatedLine(store?.updatedAt ?? null)].join( + "\n", + ), + ); + return; + } - if (mode.kind === "commands") { - const top = rank(snapshot.totalsByCommand, mode.limit); + if (mode.kind === "users") { + const limit = clamp(mode.limit, 1, 25); - const lines: string[] = []; - for (let i = 0; i < top.length; i += 1) { - const item = top[i]!; - lines.push(`${medals(i)} \`/${prettyCommandName(item.key)}\` — **${item.count}**`); - } + const rows: UserRow[] = Object.entries(store.users).map(([userId, stats]) => ({ + userId, + total: stats.total ?? 0, + })); - const embed = new EmbedBuilder() - .setTitle("🎉 Fun Leaderboard: Top Commands") - .setDescription(lines.join("\n")) - .setFooter({ text: `Updated: ${snapshot.updatedAt}` }); + const top = sortDesc(rows, (r) => r.total).slice(0, limit); - await interaction.editReply({ embeds: [embed] }); - return; - } + const lines: string[] = []; + for (let i = 0; i < top.length; i += 1) { + const row = top[i]; + const u = await safeFetchUser(interaction, row.userId); + const mention = `<@${row.userId}>`; + + // Avatar: we cannot show inline images per line, but we can include a clickable link. + const avatarLink = u?.avatarUrl ? `[avatar](${u.avatarUrl})` : ""; + + lines.push(`${medal(i)} ${mention} — **${row.total}** ${avatarLink}`.trim()); + } - if (mode.kind === "users") { - const top = rank(snapshot.totalsByUser, mode.limit); + const embed = new EmbedBuilder() + .setTitle("🏆 Fun Leaderboard: Top Users") + .setDescription(lines.join("\n")) + .setFooter({ text: buildUpdatedLine(store.updatedAt) }); - const lines: string[] = []; - for (let i = 0; i < top.length; i += 1) { - const item = top[i]!; - lines.push(`${medals(i)} <@${item.key}> — **${item.count}**`); + await interaction.editReply({ embeds: [embed] }); + return; } - const embed = new EmbedBuilder() - .setTitle("🏆 Fun Leaderboard: Top Users") - .setDescription(lines.join("\n")) - .setFooter({ text: `Updated: ${snapshot.updatedAt}` }); + if (mode.kind === "commands") { + const limit = clamp(mode.limit, 1, 25); - await interaction.editReply({ embeds: [embed] }); - return; - } + const totals = store.totals ?? {}; + const rows: CommandRow[] = Object.entries(totals).map(([k, v]) => ({ + command: k as FunCommandKey, + total: v ?? 0, + })); + + const top = sortDesc(rows, (r) => r.total).slice(0, limit); - // mode.kind === "user" - const userId = mode.userId; - const user = await safeFetchUser(interaction, userId); + const lines = top.map((r, idx) => `${medal(idx)} \`${formatCmd(r.command)}\` — **${r.total}**`); - const perCmd = snapshot.byUserByCommand[userId] ?? {}; - const sorted = rank(perCmd, 25); + const embed = new EmbedBuilder() + .setTitle("🏆 Fun Leaderboard: Top Commands") + .setDescription(lines.join("\n")) + .setFooter({ text: buildUpdatedLine(store.updatedAt) }); - const lines: string[] = []; - if (sorted.length === 0) { - lines.push("No recorded fun commands for this user yet."); - } else { - for (let i = 0; i < sorted.length; i += 1) { - const item = sorted[i]!; - lines.push(`${medals(i)} \`/${prettyCommandName(item.key)}\` — **${item.count}**`); + await interaction.editReply({ embeds: [embed] }); + return; } - } - const titleName = user?.username ? `${user.username}` : `User ${userId}`; - const embed = new EmbedBuilder() - .setTitle(`👤 Fun Usage: ${titleName}`) - .setDescription(lines.join("\n")) - .setFooter({ text: `Updated: ${snapshot.updatedAt}` }); + // Single user view + if (mode.kind === "user") { + const userId = mode.userId; + const stats = store.users[userId]; - if (user) { - embed.setThumbnail(user.displayAvatarURL({ size: 128 })); - } + if (!stats) { + await interaction.editReply(`No fun usage found for <@${userId}> yet.`); + return; + } - await interaction.editReply({ embeds: [embed] }); -} + const u = await safeFetchUser(interaction, userId); + const mention = `<@${userId}>`; + + // Build breakdown lines: "chucknorris x2" + const entries = Object.entries(stats.commands ?? {}) as Array<[FunCommandKey, number]>; + const sorted = sortDesc(entries, (e) => e[1]); + + const breakdown = + sorted.length === 0 + ? ["No per-command breakdown recorded yet."] + : sorted.map(([cmd, count]) => `• \`${formatCmd(cmd)}\` x**${count}**`); + + const embed = new EmbedBuilder() + .setTitle("📊 Fun Usage: User Breakdown") + .setDescription([`${mention} — **${stats.total}** total`, "", ...breakdown].join("\n")) + .setFooter({ text: buildUpdatedLine(stats.updatedAt ?? store.updatedAt) }); + + if (u?.avatarUrl) { + embed.setThumbnail(u.avatarUrl); + } + + await interaction.editReply({ embeds: [embed] }); + return; + } + } catch (err) { + logger.error({ err, mode }, "[leaderboard] failed"); + await interaction.editReply("Something went wrong building the leaderboard."); + } +} \ No newline at end of file diff --git a/src/services/fun/funUsageStore.ts b/src/services/fun/funUsageStore.ts new file mode 100644 index 00000000..e9c33314 --- /dev/null +++ b/src/services/fun/funUsageStore.ts @@ -0,0 +1,111 @@ +// src/services/fun/funUsageStore.ts + +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { logger } from "../../utils/logger.js"; + +export type FunCommandKey = + | "chucknorris" + | "dadjoke" + | "dice" + | "coinflip" + | "java" + | "poll" + | "weather" + | "weather7" + | "leaderboard"; + +export type FunUsageUserStats = { + total: number; + commands: Partial>; + updatedAt: string; +}; + +export type FunUsageStore = { + version: 1; + updatedAt: string; + totals: Partial>; + users: Record; +}; + +const DATA_DIR = path.join(process.cwd(), "data"); +const STORE_PATH = path.join(DATA_DIR, "fun-usage.json"); + +function nowIso(): string { + return new Date().toISOString(); +} + +function emptyStore(): FunUsageStore { + return { + version: 1, + updatedAt: nowIso(), + totals: {}, + users: {}, + }; +} + +async function ensureDataDir(): Promise { + await fs.mkdir(DATA_DIR, { recursive: true }); +} + +async function readStore(): Promise { + try { + const raw = await fs.readFile(STORE_PATH, "utf8"); + const parsed = JSON.parse(raw) as FunUsageStore; + + // Minimal validation + forward-safe defaults + if (!parsed || parsed.version !== 1) return emptyStore(); + if (!parsed.totals) parsed.totals = {}; + if (!parsed.users) parsed.users = {}; + if (!parsed.updatedAt) parsed.updatedAt = nowIso(); + + return parsed; + } catch (err) { + // First run or file missing is fine + return emptyStore(); + } +} + +async function writeStore(store: FunUsageStore): Promise { + await ensureDataDir(); + await fs.writeFile(STORE_PATH, JSON.stringify(store, null, 2), "utf8"); +} + +export async function recordFunUsage(args: { + userId: string; + command: FunCommandKey; +}): Promise { + const { userId, command } = args; + + try { + const store = await readStore(); + + // Totals + const prevTotal = store.totals[command] ?? 0; + store.totals[command] = prevTotal + 1; + + // User stats + const user = store.users[userId] ?? { + total: 0, + commands: {}, + updatedAt: nowIso(), + }; + + user.total += 1; + const prevCmd = user.commands[command] ?? 0; + user.commands[command] = prevCmd + 1; + user.updatedAt = nowIso(); + + store.users[userId] = user; + store.updatedAt = nowIso(); + + await writeStore(store); + } catch (err) { + // Do not crash commands if stats storage fails + logger.warn({ err }, "[funUsageStore] recordFunUsage failed"); + } +} + +export async function getFunUsageSnapshot(): Promise { + return await readStore(); +} \ No newline at end of file From 8be00d74f83a4d1dfd3e5b46fabafb8bdfcc4bb6 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 16:16:04 -0500 Subject: [PATCH 03/25] style: auto-format with Prettier [skip-precheck] --- src/commands/fun/subcommands/leaderboard.ts | 27 +++++++++++++++------ src/services/fun/funUsageStore.ts | 2 +- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index 9232642d..72c0c078 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -2,7 +2,10 @@ import type { ChatInputCommandInteraction } from "discord.js"; import { EmbedBuilder } from "discord.js"; -import { getFunUsageSnapshot, type FunCommandKey } from "../../../services/fun/funUsageStore.js"; +import { + getFunUsageSnapshot, + type FunCommandKey, +} from "../../../services/fun/funUsageStore.js"; import { logger } from "../../../utils/logger.js"; export type LeaderboardMode = @@ -71,9 +74,11 @@ export async function run( if (!store || Object.keys(store.users ?? {}).length === 0) { await interaction.editReply( - ["No fun command usage recorded yet.", "", buildUpdatedLine(store?.updatedAt ?? null)].join( - "\n", - ), + [ + "No fun command usage recorded yet.", + "", + buildUpdatedLine(store?.updatedAt ?? null), + ].join("\n"), ); return; } @@ -120,7 +125,9 @@ export async function run( const top = sortDesc(rows, (r) => r.total).slice(0, limit); - const lines = top.map((r, idx) => `${medal(idx)} \`${formatCmd(r.command)}\` — **${r.total}**`); + const lines = top.map( + (r, idx) => `${medal(idx)} \`${formatCmd(r.command)}\` — **${r.total}**`, + ); const embed = new EmbedBuilder() .setTitle("🏆 Fun Leaderboard: Top Commands") @@ -145,7 +152,9 @@ export async function run( const mention = `<@${userId}>`; // Build breakdown lines: "chucknorris x2" - const entries = Object.entries(stats.commands ?? {}) as Array<[FunCommandKey, number]>; + const entries = Object.entries(stats.commands ?? {}) as Array< + [FunCommandKey, number] + >; const sorted = sortDesc(entries, (e) => e[1]); const breakdown = @@ -155,7 +164,9 @@ export async function run( const embed = new EmbedBuilder() .setTitle("📊 Fun Usage: User Breakdown") - .setDescription([`${mention} — **${stats.total}** total`, "", ...breakdown].join("\n")) + .setDescription( + [`${mention} — **${stats.total}** total`, "", ...breakdown].join("\n"), + ) .setFooter({ text: buildUpdatedLine(stats.updatedAt ?? store.updatedAt) }); if (u?.avatarUrl) { @@ -169,4 +180,4 @@ export async function run( logger.error({ err, mode }, "[leaderboard] failed"); await interaction.editReply("Something went wrong building the leaderboard."); } -} \ No newline at end of file +} diff --git a/src/services/fun/funUsageStore.ts b/src/services/fun/funUsageStore.ts index e9c33314..3191443b 100644 --- a/src/services/fun/funUsageStore.ts +++ b/src/services/fun/funUsageStore.ts @@ -108,4 +108,4 @@ export async function recordFunUsage(args: { export async function getFunUsageSnapshot(): Promise { return await readStore(); -} \ No newline at end of file +} From 65c5407a41036f19fc10d9423822dadd6d335f85 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 16:30:21 -0500 Subject: [PATCH 04/25] Trying again --- src/commands/fun/subcommands/leaderboard.ts | 276 ++++++++------------ src/services/fun/funUsageStore.ts | 124 +++++---- 2 files changed, 186 insertions(+), 214 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index 72c0c078..f65b5d34 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,183 +1,133 @@ -// src/commands/fun/subcommands/leaderboard.ts - -import type { ChatInputCommandInteraction } from "discord.js"; -import { EmbedBuilder } from "discord.js"; -import { - getFunUsageSnapshot, - type FunCommandKey, -} from "../../../services/fun/funUsageStore.js"; -import { logger } from "../../../utils/logger.js"; - -export type LeaderboardMode = - | { kind: "users"; limit: number } - | { kind: "commands"; limit: number } - | { kind: "user"; userId: string }; - -type UserRow = { - userId: string; - total: number; +// src/services/fun/funUsageStore.ts + +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { logger } from "../../utils/logger.js"; + +export type FunCommandKey = + | "chucknorris" + | "dadjoke" + | "dice" + | "coinflip" + | "java" + | "poll" + | "weather" + | "weather7" + | "leaderboard"; + +type FunUsageStoreV1 = { + version: 1; + updatedAt: string; + totalsByUser: Record; + totalsByCommand: Record; + breakdownByUser: Record>>; }; -type CommandRow = { - command: FunCommandKey; - total: number; +export type FunUsageSnapshot = { + updatedAt: string; + totalsByUser: Array<{ userId: string; total: number }>; + totalsByCommand: Array<{ command: FunCommandKey; total: number }>; + breakdownByUser: Record>>; }; -function clamp(n: number, min: number, max: number): number { - return Math.max(min, Math.min(max, n)); -} +const DATA_DIR = path.join(process.cwd(), "data"); +const STORE_PATH = path.join(DATA_DIR, "fun-usage.json"); -function sortDesc(items: T[], getValue: (t: T) => number): T[] { - return [...items].sort((a, b) => getValue(b) - getValue(a)); +async function ensureDataDir(): Promise { + await fs.mkdir(DATA_DIR, { recursive: true }); } -function formatCmd(cmd: string): string { - // Make it a little prettier in output - // weather7 -> weather7, chucknorris -> chucknorris etc - return cmd; +function emptyStore(): FunUsageStoreV1 { + return { + version: 1, + updatedAt: new Date().toISOString(), + totalsByUser: {}, + totalsByCommand: { + chucknorris: 0, + dadjoke: 0, + dice: 0, + coinflip: 0, + java: 0, + poll: 0, + weather: 0, + weather7: 0, + leaderboard: 0, + }, + breakdownByUser: {}, + }; } -function medal(idx: number): string { - if (idx === 0) return "🥇"; - if (idx === 1) return "🥈"; - if (idx === 2) return "🥉"; - return "🏅"; -} - -async function safeFetchUser( - interaction: ChatInputCommandInteraction, - userId: string, -): Promise<{ id: string; tag: string; avatarUrl: string } | null> { +async function loadStore(): Promise { try { - const u = await interaction.client.users.fetch(userId); - return { - id: u.id, - tag: u.tag, - avatarUrl: u.displayAvatarURL({ size: 128 }), - }; + const raw = await fs.readFile(STORE_PATH, "utf8"); + const parsed = JSON.parse(raw) as FunUsageStoreV1; + + if (!parsed || typeof parsed !== "object") return emptyStore(); + if (parsed.version !== 1) return emptyStore(); + if (!parsed.totalsByUser || typeof parsed.totalsByUser !== "object") return emptyStore(); + if ( + !parsed.totalsByCommand || + typeof parsed.totalsByCommand !== "object" + ) + return emptyStore(); + if ( + !parsed.breakdownByUser || + typeof parsed.breakdownByUser !== "object" + ) + return emptyStore(); + + return parsed; } catch { - return null; + return emptyStore(); } } -function buildUpdatedLine(updatedAt: string | null): string { - const ts = updatedAt ?? new Date().toISOString(); - return `Updated: ${ts}`; +async function saveStore(store: FunUsageStoreV1): Promise { + await ensureDataDir(); + await fs.writeFile(STORE_PATH, JSON.stringify(store, null, 2), "utf8"); } -export async function run( - interaction: ChatInputCommandInteraction, - mode: LeaderboardMode, -): Promise { +export async function recordFunUsage(args: { + userId: string; + command: FunCommandKey; +}): Promise { + const { userId, command } = args; + try { - const store = await getFunUsageSnapshot(); - - if (!store || Object.keys(store.users ?? {}).length === 0) { - await interaction.editReply( - [ - "No fun command usage recorded yet.", - "", - buildUpdatedLine(store?.updatedAt ?? null), - ].join("\n"), - ); - return; - } - - if (mode.kind === "users") { - const limit = clamp(mode.limit, 1, 25); - - const rows: UserRow[] = Object.entries(store.users).map(([userId, stats]) => ({ - userId, - total: stats.total ?? 0, - })); - - const top = sortDesc(rows, (r) => r.total).slice(0, limit); - - const lines: string[] = []; - for (let i = 0; i < top.length; i += 1) { - const row = top[i]; - const u = await safeFetchUser(interaction, row.userId); - const mention = `<@${row.userId}>`; - - // Avatar: we cannot show inline images per line, but we can include a clickable link. - const avatarLink = u?.avatarUrl ? `[avatar](${u.avatarUrl})` : ""; - - lines.push(`${medal(i)} ${mention} — **${row.total}** ${avatarLink}`.trim()); - } - - const embed = new EmbedBuilder() - .setTitle("🏆 Fun Leaderboard: Top Users") - .setDescription(lines.join("\n")) - .setFooter({ text: buildUpdatedLine(store.updatedAt) }); - - await interaction.editReply({ embeds: [embed] }); - return; - } - - if (mode.kind === "commands") { - const limit = clamp(mode.limit, 1, 25); - - const totals = store.totals ?? {}; - const rows: CommandRow[] = Object.entries(totals).map(([k, v]) => ({ - command: k as FunCommandKey, - total: v ?? 0, - })); - - const top = sortDesc(rows, (r) => r.total).slice(0, limit); - - const lines = top.map( - (r, idx) => `${medal(idx)} \`${formatCmd(r.command)}\` — **${r.total}**`, - ); - - const embed = new EmbedBuilder() - .setTitle("🏆 Fun Leaderboard: Top Commands") - .setDescription(lines.join("\n")) - .setFooter({ text: buildUpdatedLine(store.updatedAt) }); - - await interaction.editReply({ embeds: [embed] }); - return; - } - - // Single user view - if (mode.kind === "user") { - const userId = mode.userId; - const stats = store.users[userId]; - - if (!stats) { - await interaction.editReply(`No fun usage found for <@${userId}> yet.`); - return; - } - - const u = await safeFetchUser(interaction, userId); - const mention = `<@${userId}>`; - - // Build breakdown lines: "chucknorris x2" - const entries = Object.entries(stats.commands ?? {}) as Array< - [FunCommandKey, number] - >; - const sorted = sortDesc(entries, (e) => e[1]); - - const breakdown = - sorted.length === 0 - ? ["No per-command breakdown recorded yet."] - : sorted.map(([cmd, count]) => `• \`${formatCmd(cmd)}\` x**${count}**`); - - const embed = new EmbedBuilder() - .setTitle("📊 Fun Usage: User Breakdown") - .setDescription( - [`${mention} — **${stats.total}** total`, "", ...breakdown].join("\n"), - ) - .setFooter({ text: buildUpdatedLine(stats.updatedAt ?? store.updatedAt) }); - - if (u?.avatarUrl) { - embed.setThumbnail(u.avatarUrl); - } - - await interaction.editReply({ embeds: [embed] }); - return; - } + const store = await loadStore(); + + store.totalsByUser[userId] = (store.totalsByUser[userId] ?? 0) + 1; + store.totalsByCommand[command] = (store.totalsByCommand[command] ?? 0) + 1; + + const currentBreakdown = store.breakdownByUser[userId] ?? {}; + currentBreakdown[command] = (currentBreakdown[command] ?? 0) + 1; + store.breakdownByUser[userId] = currentBreakdown; + + store.updatedAt = new Date().toISOString(); + + await saveStore(store); } catch (err) { - logger.error({ err, mode }, "[leaderboard] failed"); - await interaction.editReply("Something went wrong building the leaderboard."); + logger.warn({ err, userId, command }, "[funUsageStore] failed to record usage"); } } + +export async function getFunUsageSnapshot(): Promise { + const store = await loadStore(); + + const totalsByUser = Object.entries(store.totalsByUser) + .map(([userId, total]) => ({ userId, total })) + .sort((a, b) => b.total - a.total); + + const totalsByCommand = (Object.entries(store.totalsByCommand) as Array< + [FunCommandKey, number] + >) + .map(([command, total]) => ({ command, total })) + .sort((a, b) => b.total - a.total); + + return { + updatedAt: store.updatedAt, + totalsByUser, + totalsByCommand, + breakdownByUser: store.breakdownByUser, + }; +} \ No newline at end of file diff --git a/src/services/fun/funUsageStore.ts b/src/services/fun/funUsageStore.ts index 3191443b..f65b5d34 100644 --- a/src/services/fun/funUsageStore.ts +++ b/src/services/fun/funUsageStore.ts @@ -15,58 +15,74 @@ export type FunCommandKey = | "weather7" | "leaderboard"; -export type FunUsageUserStats = { - total: number; - commands: Partial>; +type FunUsageStoreV1 = { + version: 1; updatedAt: string; + totalsByUser: Record; + totalsByCommand: Record; + breakdownByUser: Record>>; }; -export type FunUsageStore = { - version: 1; +export type FunUsageSnapshot = { updatedAt: string; - totals: Partial>; - users: Record; + totalsByUser: Array<{ userId: string; total: number }>; + totalsByCommand: Array<{ command: FunCommandKey; total: number }>; + breakdownByUser: Record>>; }; const DATA_DIR = path.join(process.cwd(), "data"); const STORE_PATH = path.join(DATA_DIR, "fun-usage.json"); -function nowIso(): string { - return new Date().toISOString(); +async function ensureDataDir(): Promise { + await fs.mkdir(DATA_DIR, { recursive: true }); } -function emptyStore(): FunUsageStore { +function emptyStore(): FunUsageStoreV1 { return { version: 1, - updatedAt: nowIso(), - totals: {}, - users: {}, + updatedAt: new Date().toISOString(), + totalsByUser: {}, + totalsByCommand: { + chucknorris: 0, + dadjoke: 0, + dice: 0, + coinflip: 0, + java: 0, + poll: 0, + weather: 0, + weather7: 0, + leaderboard: 0, + }, + breakdownByUser: {}, }; } -async function ensureDataDir(): Promise { - await fs.mkdir(DATA_DIR, { recursive: true }); -} - -async function readStore(): Promise { +async function loadStore(): Promise { try { const raw = await fs.readFile(STORE_PATH, "utf8"); - const parsed = JSON.parse(raw) as FunUsageStore; - - // Minimal validation + forward-safe defaults - if (!parsed || parsed.version !== 1) return emptyStore(); - if (!parsed.totals) parsed.totals = {}; - if (!parsed.users) parsed.users = {}; - if (!parsed.updatedAt) parsed.updatedAt = nowIso(); + const parsed = JSON.parse(raw) as FunUsageStoreV1; + + if (!parsed || typeof parsed !== "object") return emptyStore(); + if (parsed.version !== 1) return emptyStore(); + if (!parsed.totalsByUser || typeof parsed.totalsByUser !== "object") return emptyStore(); + if ( + !parsed.totalsByCommand || + typeof parsed.totalsByCommand !== "object" + ) + return emptyStore(); + if ( + !parsed.breakdownByUser || + typeof parsed.breakdownByUser !== "object" + ) + return emptyStore(); return parsed; - } catch (err) { - // First run or file missing is fine + } catch { return emptyStore(); } } -async function writeStore(store: FunUsageStore): Promise { +async function saveStore(store: FunUsageStoreV1): Promise { await ensureDataDir(); await fs.writeFile(STORE_PATH, JSON.stringify(store, null, 2), "utf8"); } @@ -78,34 +94,40 @@ export async function recordFunUsage(args: { const { userId, command } = args; try { - const store = await readStore(); - - // Totals - const prevTotal = store.totals[command] ?? 0; - store.totals[command] = prevTotal + 1; + const store = await loadStore(); - // User stats - const user = store.users[userId] ?? { - total: 0, - commands: {}, - updatedAt: nowIso(), - }; + store.totalsByUser[userId] = (store.totalsByUser[userId] ?? 0) + 1; + store.totalsByCommand[command] = (store.totalsByCommand[command] ?? 0) + 1; - user.total += 1; - const prevCmd = user.commands[command] ?? 0; - user.commands[command] = prevCmd + 1; - user.updatedAt = nowIso(); + const currentBreakdown = store.breakdownByUser[userId] ?? {}; + currentBreakdown[command] = (currentBreakdown[command] ?? 0) + 1; + store.breakdownByUser[userId] = currentBreakdown; - store.users[userId] = user; - store.updatedAt = nowIso(); + store.updatedAt = new Date().toISOString(); - await writeStore(store); + await saveStore(store); } catch (err) { - // Do not crash commands if stats storage fails - logger.warn({ err }, "[funUsageStore] recordFunUsage failed"); + logger.warn({ err, userId, command }, "[funUsageStore] failed to record usage"); } } -export async function getFunUsageSnapshot(): Promise { - return await readStore(); -} +export async function getFunUsageSnapshot(): Promise { + const store = await loadStore(); + + const totalsByUser = Object.entries(store.totalsByUser) + .map(([userId, total]) => ({ userId, total })) + .sort((a, b) => b.total - a.total); + + const totalsByCommand = (Object.entries(store.totalsByCommand) as Array< + [FunCommandKey, number] + >) + .map(([command, total]) => ({ command, total })) + .sort((a, b) => b.total - a.total); + + return { + updatedAt: store.updatedAt, + totalsByUser, + totalsByCommand, + breakdownByUser: store.breakdownByUser, + }; +} \ No newline at end of file From 1318c8054d2c09b382eda10259bb44833c262e0f Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 16:30:26 -0500 Subject: [PATCH 05/25] style: auto-format with Prettier [skip-precheck] --- src/commands/fun/subcommands/leaderboard.ts | 21 ++++++++------------- src/services/fun/funUsageStore.ts | 21 ++++++++------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index f65b5d34..2987b36a 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -64,16 +64,11 @@ async function loadStore(): Promise { if (!parsed || typeof parsed !== "object") return emptyStore(); if (parsed.version !== 1) return emptyStore(); - if (!parsed.totalsByUser || typeof parsed.totalsByUser !== "object") return emptyStore(); - if ( - !parsed.totalsByCommand || - typeof parsed.totalsByCommand !== "object" - ) + if (!parsed.totalsByUser || typeof parsed.totalsByUser !== "object") return emptyStore(); - if ( - !parsed.breakdownByUser || - typeof parsed.breakdownByUser !== "object" - ) + if (!parsed.totalsByCommand || typeof parsed.totalsByCommand !== "object") + return emptyStore(); + if (!parsed.breakdownByUser || typeof parsed.breakdownByUser !== "object") return emptyStore(); return parsed; @@ -118,9 +113,9 @@ export async function getFunUsageSnapshot(): Promise { .map(([userId, total]) => ({ userId, total })) .sort((a, b) => b.total - a.total); - const totalsByCommand = (Object.entries(store.totalsByCommand) as Array< - [FunCommandKey, number] - >) + const totalsByCommand = ( + Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]> + ) .map(([command, total]) => ({ command, total })) .sort((a, b) => b.total - a.total); @@ -130,4 +125,4 @@ export async function getFunUsageSnapshot(): Promise { totalsByCommand, breakdownByUser: store.breakdownByUser, }; -} \ No newline at end of file +} diff --git a/src/services/fun/funUsageStore.ts b/src/services/fun/funUsageStore.ts index f65b5d34..2987b36a 100644 --- a/src/services/fun/funUsageStore.ts +++ b/src/services/fun/funUsageStore.ts @@ -64,16 +64,11 @@ async function loadStore(): Promise { if (!parsed || typeof parsed !== "object") return emptyStore(); if (parsed.version !== 1) return emptyStore(); - if (!parsed.totalsByUser || typeof parsed.totalsByUser !== "object") return emptyStore(); - if ( - !parsed.totalsByCommand || - typeof parsed.totalsByCommand !== "object" - ) + if (!parsed.totalsByUser || typeof parsed.totalsByUser !== "object") return emptyStore(); - if ( - !parsed.breakdownByUser || - typeof parsed.breakdownByUser !== "object" - ) + if (!parsed.totalsByCommand || typeof parsed.totalsByCommand !== "object") + return emptyStore(); + if (!parsed.breakdownByUser || typeof parsed.breakdownByUser !== "object") return emptyStore(); return parsed; @@ -118,9 +113,9 @@ export async function getFunUsageSnapshot(): Promise { .map(([userId, total]) => ({ userId, total })) .sort((a, b) => b.total - a.total); - const totalsByCommand = (Object.entries(store.totalsByCommand) as Array< - [FunCommandKey, number] - >) + const totalsByCommand = ( + Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]> + ) .map(([command, total]) => ({ command, total })) .sort((a, b) => b.total - a.total); @@ -130,4 +125,4 @@ export async function getFunUsageSnapshot(): Promise { totalsByCommand, breakdownByUser: store.breakdownByUser, }; -} \ No newline at end of file +} From 72dfc9113b898426d3771a6b4d804bbe5441c9d4 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 16:35:24 -0500 Subject: [PATCH 06/25] Trying to fix this --- src/commands/fun/subcommands/leaderboard.ts | 246 +++++++++++--------- src/services/fun/funUsageStore.ts | 143 +++++++----- 2 files changed, 215 insertions(+), 174 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index 2987b36a..cc3e60ca 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,128 +1,144 @@ -// src/services/fun/funUsageStore.ts - -import { promises as fs } from "node:fs"; -import path from "node:path"; -import { logger } from "../../utils/logger.js"; - -export type FunCommandKey = - | "chucknorris" - | "dadjoke" - | "dice" - | "coinflip" - | "java" - | "poll" - | "weather" - | "weather7" - | "leaderboard"; - -type FunUsageStoreV1 = { - version: 1; - updatedAt: string; - totalsByUser: Record; - totalsByCommand: Record; - breakdownByUser: Record>>; -}; - -export type FunUsageSnapshot = { - updatedAt: string; - totalsByUser: Array<{ userId: string; total: number }>; - totalsByCommand: Array<{ command: FunCommandKey; total: number }>; - breakdownByUser: Record>>; -}; - -const DATA_DIR = path.join(process.cwd(), "data"); -const STORE_PATH = path.join(DATA_DIR, "fun-usage.json"); - -async function ensureDataDir(): Promise { - await fs.mkdir(DATA_DIR, { recursive: true }); +// src/commands/fun/subcommands/leaderboard.ts + +import { + EmbedBuilder, + type ChatInputCommandInteraction, + type User, +} from "discord.js"; +import { + getFunUsageSnapshot, + type FunCommandKey, +} from "../../../services/fun/funUsageStore.js"; +import { logger } from "../../../utils/logger.js"; + +export type LeaderboardMode = + | { kind: "users"; limit: number } + | { kind: "commands"; limit: number } + | { kind: "user"; userId: string }; + +type Breakdown = Record; + +function formatBreakdown(b: Breakdown | undefined, maxItems: number): string { + if (!b) return ""; + + const parts = (Object.entries(b) as Array<[FunCommandKey, number]>) + .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) + .sort((a, c) => c[1] - a[1]) + .slice(0, maxItems) + .map(([k, n]) => `${n}x ${k}`); + + return parts.length ? ` (${parts.join(", ")})` : ""; } -function emptyStore(): FunUsageStoreV1 { - return { - version: 1, - updatedAt: new Date().toISOString(), - totalsByUser: {}, - totalsByCommand: { - chucknorris: 0, - dadjoke: 0, - dice: 0, - coinflip: 0, - java: 0, - poll: 0, - weather: 0, - weather7: 0, - leaderboard: 0, - }, - breakdownByUser: {}, - }; -} - -async function loadStore(): Promise { +async function safeFetchUser( + interaction: ChatInputCommandInteraction, + userId: string, +): Promise { try { - const raw = await fs.readFile(STORE_PATH, "utf8"); - const parsed = JSON.parse(raw) as FunUsageStoreV1; - - if (!parsed || typeof parsed !== "object") return emptyStore(); - if (parsed.version !== 1) return emptyStore(); - if (!parsed.totalsByUser || typeof parsed.totalsByUser !== "object") - return emptyStore(); - if (!parsed.totalsByCommand || typeof parsed.totalsByCommand !== "object") - return emptyStore(); - if (!parsed.breakdownByUser || typeof parsed.breakdownByUser !== "object") - return emptyStore(); - - return parsed; - } catch { - return emptyStore(); + return await interaction.client.users.fetch(userId); + } catch (err) { + logger.debug({ err, userId }, "[fun/leaderboard] failed to fetch user"); + return null; } } -async function saveStore(store: FunUsageStoreV1): Promise { - await ensureDataDir(); - await fs.writeFile(STORE_PATH, JSON.stringify(store, null, 2), "utf8"); -} - -export async function recordFunUsage(args: { - userId: string; - command: FunCommandKey; -}): Promise { - const { userId, command } = args; +export async function run( + interaction: ChatInputCommandInteraction, + mode: LeaderboardMode, +): Promise { + const store = await getFunUsageSnapshot(); + + const embed = new EmbedBuilder().setFooter({ + text: `Updated: ${store.updatedAt}`, + }); + + // No usage yet + const anyUsage = + Object.keys(store.totalsByUser).length > 0 || + Object.values(store.totalsByCommand).some((n) => n > 0); + + if (!anyUsage) { + embed.setTitle("Fun Leaderboard"); + embed.setDescription("No fun command usage recorded yet."); + await interaction.editReply({ embeds: [embed] }); + return; + } - try { - const store = await loadStore(); + if (mode.kind === "commands") { + const items = (Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]>) + .filter(([, n]) => n > 0) + .sort((a, c) => c[1] - a[1]) + .slice(0, mode.limit); - store.totalsByUser[userId] = (store.totalsByUser[userId] ?? 0) + 1; - store.totalsByCommand[command] = (store.totalsByCommand[command] ?? 0) + 1; + embed.setTitle("Fun Leaderboard: Top Commands"); - const currentBreakdown = store.breakdownByUser[userId] ?? {}; - currentBreakdown[command] = (currentBreakdown[command] ?? 0) + 1; - store.breakdownByUser[userId] = currentBreakdown; + const lines = items.map(([cmd, count], idx) => { + const rank = idx + 1; + return `${rank}. \`${cmd}\` — **${count}**`; + }); - store.updatedAt = new Date().toISOString(); + embed.setDescription(lines.length ? lines.join("\n") : "No command usage yet."); + await interaction.editReply({ embeds: [embed] }); + return; + } - await saveStore(store); - } catch (err) { - logger.warn({ err, userId, command }, "[funUsageStore] failed to record usage"); + if (mode.kind === "user") { + const total = store.totalsByUser[mode.userId] ?? 0; + const breakdown = store.breakdownByUser[mode.userId] as Breakdown | undefined; + + const u = await safeFetchUser(interaction, mode.userId); + const display = u ? `${u.username}` : `User ${mode.userId}`; + const avatarUrl = u?.displayAvatarURL() ?? null; + + embed.setTitle("Fun Leaderboard: User Stats"); + if (avatarUrl) embed.setThumbnail(avatarUrl); + + const breakdownLine = formatBreakdown(breakdown, 10) || " (no breakdown yet)"; + embed.setDescription( + [ + `**${display}**`, + `Total uses: **${total}**${breakdownLine}`, + avatarUrl ? `Avatar: ${avatarUrl}` : "", + ] + .filter(Boolean) + .join("\n"), + ); + + await interaction.editReply({ embeds: [embed] }); + return; } -} -export async function getFunUsageSnapshot(): Promise { - const store = await loadStore(); - - const totalsByUser = Object.entries(store.totalsByUser) - .map(([userId, total]) => ({ userId, total })) - .sort((a, b) => b.total - a.total); - - const totalsByCommand = ( - Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]> - ) - .map(([command, total]) => ({ command, total })) - .sort((a, b) => b.total - a.total); - - return { - updatedAt: store.updatedAt, - totalsByUser, - totalsByCommand, - breakdownByUser: store.breakdownByUser, - }; -} + // Top users (default) + const userItems = Object.entries(store.totalsByUser) + .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) + .sort((a, c) => c[1] - a[1]) + .slice(0, mode.limit); + + embed.setTitle("Fun Leaderboard: Top Users"); + + // Fetch users for avatar urls (best effort) + const users = await Promise.all( + userItems.map(async ([userId]) => { + const u = await safeFetchUser(interaction, userId); + return { userId, user: u }; + }), + ); + + const lines = userItems.map(([userId, total], idx) => { + const rank = idx + 1; + + const u = users.find((x) => x.userId === userId)?.user ?? null; + const avatarUrl = u?.displayAvatarURL() ?? null; + + const breakdown = store.breakdownByUser[userId] as Breakdown | undefined; + const suffix = formatBreakdown(breakdown, 4); + + // Mention + total + breakdown + avatar link + const avatarPart = avatarUrl ? ` • ${avatarUrl}` : ""; + return `${rank}. <@${userId}> — **${total}**${suffix}${avatarPart}`; + }); + + embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); + + await interaction.editReply({ embeds: [embed] }); +} \ No newline at end of file diff --git a/src/services/fun/funUsageStore.ts b/src/services/fun/funUsageStore.ts index 2987b36a..3e51357c 100644 --- a/src/services/fun/funUsageStore.ts +++ b/src/services/fun/funUsageStore.ts @@ -17,61 +17,94 @@ export type FunCommandKey = type FunUsageStoreV1 = { version: 1; + initializedAt: string; updatedAt: string; totalsByUser: Record; totalsByCommand: Record; - breakdownByUser: Record>>; + breakdownByUser: Record>; }; -export type FunUsageSnapshot = { - updatedAt: string; - totalsByUser: Array<{ userId: string; total: number }>; - totalsByCommand: Array<{ command: FunCommandKey; total: number }>; - breakdownByUser: Record>>; -}; +export type FunUsageSnapshot = FunUsageStoreV1; const DATA_DIR = path.join(process.cwd(), "data"); const STORE_PATH = path.join(DATA_DIR, "fun-usage.json"); -async function ensureDataDir(): Promise { - await fs.mkdir(DATA_DIR, { recursive: true }); -} +const ALL_COMMANDS: FunCommandKey[] = [ + "chucknorris", + "dadjoke", + "dice", + "coinflip", + "java", + "poll", + "weather", + "weather7", + "leaderboard", +]; function emptyStore(): FunUsageStoreV1 { + const totalsByCommand = Object.fromEntries( + ALL_COMMANDS.map((k) => [k, 0]), + ) as Record; + + const now = new Date().toISOString(); + return { version: 1, - updatedAt: new Date().toISOString(), + initializedAt: now, + updatedAt: now, totalsByUser: {}, - totalsByCommand: { - chucknorris: 0, - dadjoke: 0, - dice: 0, - coinflip: 0, - java: 0, - poll: 0, - weather: 0, - weather7: 0, - leaderboard: 0, - }, + totalsByCommand, breakdownByUser: {}, }; } +async function ensureDataDir(): Promise { + await fs.mkdir(DATA_DIR, { recursive: true }); +} + async function loadStore(): Promise { try { const raw = await fs.readFile(STORE_PATH, "utf8"); - const parsed = JSON.parse(raw) as FunUsageStoreV1; + const parsed = JSON.parse(raw) as Partial | null; if (!parsed || typeof parsed !== "object") return emptyStore(); if (parsed.version !== 1) return emptyStore(); - if (!parsed.totalsByUser || typeof parsed.totalsByUser !== "object") - return emptyStore(); - if (!parsed.totalsByCommand || typeof parsed.totalsByCommand !== "object") - return emptyStore(); - if (!parsed.breakdownByUser || typeof parsed.breakdownByUser !== "object") - return emptyStore(); - - return parsed; + + // Start from a known-good shape and merge in what exists + const base = emptyStore(); + + const totalsByUser = + parsed.totalsByUser && typeof parsed.totalsByUser === "object" + ? parsed.totalsByUser + : {}; + + const breakdownByUser = + parsed.breakdownByUser && typeof parsed.breakdownByUser === "object" + ? parsed.breakdownByUser + : {}; + + const totalsByCommandRaw = + parsed.totalsByCommand && typeof parsed.totalsByCommand === "object" + ? parsed.totalsByCommand + : {}; + + const totalsByCommand = { ...base.totalsByCommand }; + for (const k of ALL_COMMANDS) { + const v = (totalsByCommandRaw as Record)[k]; + if (typeof v === "number" && Number.isFinite(v) && v >= 0) { + totalsByCommand[k] = v; + } + } + + return { + ...base, + initializedAt: + typeof parsed.initializedAt === "string" ? parsed.initializedAt : base.initializedAt, + updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : base.updatedAt, + totalsByUser, + totalsByCommand, + breakdownByUser, + }; } catch { return emptyStore(); } @@ -88,41 +121,33 @@ export async function recordFunUsage(args: { }): Promise { const { userId, command } = args; - try { - const store = await loadStore(); + const store = await loadStore(); + + // totalsByUser + store.totalsByUser[userId] = (store.totalsByUser[userId] ?? 0) + 1; + + // totalsByCommand + store.totalsByCommand[command] = (store.totalsByCommand[command] ?? 0) + 1; - store.totalsByUser[userId] = (store.totalsByUser[userId] ?? 0) + 1; - store.totalsByCommand[command] = (store.totalsByCommand[command] ?? 0) + 1; + // breakdownByUser + const userBreakdown: Record = + (store.breakdownByUser[userId] as Record | undefined) ?? ({} as Record< + FunCommandKey, + number + >); - const currentBreakdown = store.breakdownByUser[userId] ?? {}; - currentBreakdown[command] = (currentBreakdown[command] ?? 0) + 1; - store.breakdownByUser[userId] = currentBreakdown; + userBreakdown[command] = (userBreakdown[command] ?? 0) + 1; + store.breakdownByUser[userId] = userBreakdown; - store.updatedAt = new Date().toISOString(); + store.updatedAt = new Date().toISOString(); + try { await saveStore(store); } catch (err) { - logger.warn({ err, userId, command }, "[funUsageStore] failed to record usage"); + logger.error({ err }, "[fun/usage] failed to save fun usage store"); } } export async function getFunUsageSnapshot(): Promise { - const store = await loadStore(); - - const totalsByUser = Object.entries(store.totalsByUser) - .map(([userId, total]) => ({ userId, total })) - .sort((a, b) => b.total - a.total); - - const totalsByCommand = ( - Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]> - ) - .map(([command, total]) => ({ command, total })) - .sort((a, b) => b.total - a.total); - - return { - updatedAt: store.updatedAt, - totalsByUser, - totalsByCommand, - breakdownByUser: store.breakdownByUser, - }; -} + return loadStore(); +} \ No newline at end of file From 1148584ca6b3b6087962c60d3cf014ab806a78c0 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 16:35:28 -0500 Subject: [PATCH 07/25] style: auto-format with Prettier [skip-precheck] --- src/commands/fun/subcommands/leaderboard.ts | 12 +++++------- src/services/fun/funUsageStore.ts | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index cc3e60ca..f306e4d3 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,10 +1,6 @@ // src/commands/fun/subcommands/leaderboard.ts -import { - EmbedBuilder, - type ChatInputCommandInteraction, - type User, -} from "discord.js"; +import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; import { getFunUsageSnapshot, type FunCommandKey, @@ -65,7 +61,9 @@ export async function run( } if (mode.kind === "commands") { - const items = (Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]>) + const items = ( + Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]> + ) .filter(([, n]) => n > 0) .sort((a, c) => c[1] - a[1]) .slice(0, mode.limit); @@ -141,4 +139,4 @@ export async function run( embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); await interaction.editReply({ embeds: [embed] }); -} \ No newline at end of file +} diff --git a/src/services/fun/funUsageStore.ts b/src/services/fun/funUsageStore.ts index 3e51357c..0dca9f24 100644 --- a/src/services/fun/funUsageStore.ts +++ b/src/services/fun/funUsageStore.ts @@ -42,9 +42,10 @@ const ALL_COMMANDS: FunCommandKey[] = [ ]; function emptyStore(): FunUsageStoreV1 { - const totalsByCommand = Object.fromEntries( - ALL_COMMANDS.map((k) => [k, 0]), - ) as Record; + const totalsByCommand = Object.fromEntries(ALL_COMMANDS.map((k) => [k, 0])) as Record< + FunCommandKey, + number + >; const now = new Date().toISOString(); @@ -99,7 +100,9 @@ async function loadStore(): Promise { return { ...base, initializedAt: - typeof parsed.initializedAt === "string" ? parsed.initializedAt : base.initializedAt, + typeof parsed.initializedAt === "string" + ? parsed.initializedAt + : base.initializedAt, updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : base.updatedAt, totalsByUser, totalsByCommand, @@ -131,10 +134,8 @@ export async function recordFunUsage(args: { // breakdownByUser const userBreakdown: Record = - (store.breakdownByUser[userId] as Record | undefined) ?? ({} as Record< - FunCommandKey, - number - >); + (store.breakdownByUser[userId] as Record | undefined) ?? + ({} as Record); userBreakdown[command] = (userBreakdown[command] ?? 0) + 1; store.breakdownByUser[userId] = userBreakdown; @@ -150,4 +151,4 @@ export async function recordFunUsage(args: { export async function getFunUsageSnapshot(): Promise { return loadStore(); -} \ No newline at end of file +} From 81772d942e12e8e44d3edf149ecfd3f95c6351b8 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 17:09:50 -0500 Subject: [PATCH 08/25] Trying again --- src/commands/fun/subcommands/leaderboard.ts | 68 ++++++++++++--------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index f306e4d3..32a1b68b 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -14,6 +14,13 @@ export type LeaderboardMode = type Breakdown = Record; +function medalForRank(rank: number): string { + if (rank === 1) return "🥇"; + if (rank === 2) return "🥈"; + if (rank === 3) return "🥉"; + return "•"; +} + function formatBreakdown(b: Breakdown | undefined, maxItems: number): string { if (!b) return ""; @@ -48,31 +55,33 @@ export async function run( text: `Updated: ${store.updatedAt}`, }); - // No usage yet const anyUsage = Object.keys(store.totalsByUser).length > 0 || - Object.values(store.totalsByCommand).some((n) => n > 0); + Object.values(store.totalsByCommand).some((n) => typeof n === "number" && n > 0); if (!anyUsage) { - embed.setTitle("Fun Leaderboard"); + embed.setTitle("🎮 Fun Leaderboard"); embed.setDescription("No fun command usage recorded yet."); await interaction.editReply({ embeds: [embed] }); return; } + // --------------------------------------------------------------------------- + // Top Commands + // --------------------------------------------------------------------------- if (mode.kind === "commands") { const items = ( Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]> ) - .filter(([, n]) => n > 0) + .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) .sort((a, c) => c[1] - a[1]) .slice(0, mode.limit); - embed.setTitle("Fun Leaderboard: Top Commands"); + embed.setTitle("🎉 Fun Leaderboard: Top Commands"); - const lines = items.map(([cmd, count], idx) => { + const lines = items.map(([cmd, count], idx: number) => { const rank = idx + 1; - return `${rank}. \`${cmd}\` — **${count}**`; + return `${medalForRank(rank)} \`/${cmd}\` — **${count}**`; }); embed.setDescription(lines.length ? lines.join("\n") : "No command usage yet."); @@ -80,41 +89,43 @@ export async function run( return; } + // --------------------------------------------------------------------------- + // Single User + // --------------------------------------------------------------------------- if (mode.kind === "user") { const total = store.totalsByUser[mode.userId] ?? 0; const breakdown = store.breakdownByUser[mode.userId] as Breakdown | undefined; const u = await safeFetchUser(interaction, mode.userId); - const display = u ? `${u.username}` : `User ${mode.userId}`; + const titleName = u?.username ?? `User ${mode.userId}`; const avatarUrl = u?.displayAvatarURL() ?? null; - embed.setTitle("Fun Leaderboard: User Stats"); + embed.setTitle(`👤 Fun Usage: ${titleName}`); if (avatarUrl) embed.setThumbnail(avatarUrl); - const breakdownLine = formatBreakdown(breakdown, 10) || " (no breakdown yet)"; + const breakdownText = formatBreakdown(breakdown, 25); embed.setDescription( [ - `**${display}**`, - `Total uses: **${total}**${breakdownLine}`, - avatarUrl ? `Avatar: ${avatarUrl}` : "", - ] - .filter(Boolean) - .join("\n"), + `**Total uses:** **${total}**`, + breakdownText ? `**Breakdown:**${breakdownText}` : "**Breakdown:** (none yet)", + ].join("\n"), ); await interaction.editReply({ embeds: [embed] }); return; } - // Top users (default) + // --------------------------------------------------------------------------- + // Top Users (default) + // --------------------------------------------------------------------------- const userItems = Object.entries(store.totalsByUser) .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) .sort((a, c) => c[1] - a[1]) .slice(0, mode.limit); - embed.setTitle("Fun Leaderboard: Top Users"); + embed.setTitle("🏆 Fun Leaderboard: Top Users"); - // Fetch users for avatar urls (best effort) + // Fetch users once (best effort) const users = await Promise.all( userItems.map(async ([userId]) => { const u = await safeFetchUser(interaction, userId); @@ -122,21 +133,20 @@ export async function run( }), ); - const lines = userItems.map(([userId, total], idx) => { - const rank = idx + 1; + // Use #1 user avatar as the embed thumbnail (Discord supports one thumbnail) + const topUser = users[0]?.user ?? null; + const topAvatarUrl = topUser?.displayAvatarURL() ?? null; + if (topAvatarUrl) embed.setThumbnail(topAvatarUrl); - const u = users.find((x) => x.userId === userId)?.user ?? null; - const avatarUrl = u?.displayAvatarURL() ?? null; + const lines = userItems.map(([userId, total], idx: number) => { + const rank = idx + 1; const breakdown = store.breakdownByUser[userId] as Breakdown | undefined; - const suffix = formatBreakdown(breakdown, 4); + const suffix = formatBreakdown(breakdown, 6); - // Mention + total + breakdown + avatar link - const avatarPart = avatarUrl ? ` • ${avatarUrl}` : ""; - return `${rank}. <@${userId}> — **${total}**${suffix}${avatarPart}`; + return `${medalForRank(rank)} <@${userId}> — **${total}**${suffix}`; }); embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); - await interaction.editReply({ embeds: [embed] }); -} +} \ No newline at end of file From 95707d8785c5eda4c68ba007e59657dae9274188 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 17:09:55 -0500 Subject: [PATCH 09/25] style: auto-format with Prettier [skip-precheck] --- src/commands/fun/subcommands/leaderboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index 32a1b68b..7318ae26 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -149,4 +149,4 @@ export async function run( embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); await interaction.editReply({ embeds: [embed] }); -} \ No newline at end of file +} From bf0976b602ab4daf91fdcf5c2b1e33a2fceac986 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 17:23:08 -0500 Subject: [PATCH 10/25] Fixing this up --- src/commands/fun/subcommands/leaderboard.ts | 71 ++++------ src/services/fun/funUsageStore.ts | 135 +++++++++++++------- 2 files changed, 113 insertions(+), 93 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index 7318ae26..374e85d3 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,10 +1,7 @@ // src/commands/fun/subcommands/leaderboard.ts import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; -import { - getFunUsageSnapshot, - type FunCommandKey, -} from "../../../services/fun/funUsageStore.js"; +import { getFunUsageSnapshot, type FunCommandKey } from "../../../services/fun/funUsageStore.js"; import { logger } from "../../../utils/logger.js"; export type LeaderboardMode = @@ -14,13 +11,6 @@ export type LeaderboardMode = type Breakdown = Record; -function medalForRank(rank: number): string { - if (rank === 1) return "🥇"; - if (rank === 2) return "🥈"; - if (rank === 3) return "🥉"; - return "•"; -} - function formatBreakdown(b: Breakdown | undefined, maxItems: number): string { if (!b) return ""; @@ -30,7 +20,7 @@ function formatBreakdown(b: Breakdown | undefined, maxItems: number): string { .slice(0, maxItems) .map(([k, n]) => `${n}x ${k}`); - return parts.length ? ` (${parts.join(", ")})` : ""; + return parts.length ? parts.join(", ") : ""; } async function safeFetchUser( @@ -57,31 +47,26 @@ export async function run( const anyUsage = Object.keys(store.totalsByUser).length > 0 || - Object.values(store.totalsByCommand).some((n) => typeof n === "number" && n > 0); + Object.values(store.totalsByCommand).some((n) => n > 0); if (!anyUsage) { - embed.setTitle("🎮 Fun Leaderboard"); + embed.setTitle("🏆 Fun Leaderboard"); embed.setDescription("No fun command usage recorded yet."); await interaction.editReply({ embeds: [embed] }); return; } - // --------------------------------------------------------------------------- - // Top Commands - // --------------------------------------------------------------------------- if (mode.kind === "commands") { - const items = ( - Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]> - ) - .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) + const items = (Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]>) + .filter(([, n]) => n > 0) .sort((a, c) => c[1] - a[1]) .slice(0, mode.limit); embed.setTitle("🎉 Fun Leaderboard: Top Commands"); - const lines = items.map(([cmd, count], idx: number) => { + const lines = items.map(([cmd, count], idx) => { const rank = idx + 1; - return `${medalForRank(rank)} \`/${cmd}\` — **${count}**`; + return `${rank}. \`${cmd}\` — **${count}**`; }); embed.setDescription(lines.length ? lines.join("\n") : "No command usage yet."); @@ -89,25 +74,22 @@ export async function run( return; } - // --------------------------------------------------------------------------- - // Single User - // --------------------------------------------------------------------------- if (mode.kind === "user") { const total = store.totalsByUser[mode.userId] ?? 0; - const breakdown = store.breakdownByUser[mode.userId] as Breakdown | undefined; + const breakdown = store.breakdownByUser[mode.userId]; const u = await safeFetchUser(interaction, mode.userId); - const titleName = u?.username ?? `User ${mode.userId}`; + const display = u ? `${u.username}` : `User ${mode.userId}`; const avatarUrl = u?.displayAvatarURL() ?? null; - embed.setTitle(`👤 Fun Usage: ${titleName}`); + embed.setTitle(`👤 Fun Usage: ${display}`); if (avatarUrl) embed.setThumbnail(avatarUrl); - const breakdownText = formatBreakdown(breakdown, 25); + const breakdownText = formatBreakdown(breakdown, 10); embed.setDescription( [ - `**Total uses:** **${total}**`, - breakdownText ? `**Breakdown:**${breakdownText}` : "**Breakdown:** (none yet)", + `Total uses: **${total}**`, + breakdownText ? `Breakdown: ${breakdownText}` : "Breakdown: (none yet)", ].join("\n"), ); @@ -115,9 +97,7 @@ export async function run( return; } - // --------------------------------------------------------------------------- - // Top Users (default) - // --------------------------------------------------------------------------- + // Top users const userItems = Object.entries(store.totalsByUser) .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) .sort((a, c) => c[1] - a[1]) @@ -125,7 +105,6 @@ export async function run( embed.setTitle("🏆 Fun Leaderboard: Top Users"); - // Fetch users once (best effort) const users = await Promise.all( userItems.map(async ([userId]) => { const u = await safeFetchUser(interaction, userId); @@ -133,20 +112,20 @@ export async function run( }), ); - // Use #1 user avatar as the embed thumbnail (Discord supports one thumbnail) - const topUser = users[0]?.user ?? null; - const topAvatarUrl = topUser?.displayAvatarURL() ?? null; - if (topAvatarUrl) embed.setThumbnail(topAvatarUrl); - - const lines = userItems.map(([userId, total], idx: number) => { + const lines = userItems.map(([userId, total], idx) => { const rank = idx + 1; + const u = users.find((x) => x.userId === userId)?.user ?? null; - const breakdown = store.breakdownByUser[userId] as Breakdown | undefined; - const suffix = formatBreakdown(breakdown, 6); + const avatarUrl = u?.displayAvatarURL() ?? null; + const breakdown = store.breakdownByUser[userId]; + const breakdownText = formatBreakdown(breakdown, 4); + + const avatarPart = avatarUrl ? ` • ${avatarUrl}` : ""; + const breakdownPart = breakdownText ? ` • ${breakdownText}` : ""; - return `${medalForRank(rank)} <@${userId}> — **${total}**${suffix}`; + return `${rank}. <@${userId}> — **${total}**${breakdownPart}${avatarPart}`; }); embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); await interaction.editReply({ embeds: [embed] }); -} +} \ No newline at end of file diff --git a/src/services/fun/funUsageStore.ts b/src/services/fun/funUsageStore.ts index 0dca9f24..16a627d9 100644 --- a/src/services/fun/funUsageStore.ts +++ b/src/services/fun/funUsageStore.ts @@ -15,13 +15,21 @@ export type FunCommandKey = | "weather7" | "leaderboard"; +type Breakdown = Record; + type FunUsageStoreV1 = { version: 1; initializedAt: string; updatedAt: string; + totalsByUser: Record; totalsByCommand: Record; - breakdownByUser: Record>; + + // Canonical field name going forward + breakdownByUser: Record; + + // Back-compat only (older shape some dev builds used) + byUserByCommand?: Record>; }; export type FunUsageSnapshot = FunUsageStoreV1; @@ -41,20 +49,22 @@ const ALL_COMMANDS: FunCommandKey[] = [ "leaderboard", ]; -function emptyStore(): FunUsageStoreV1 { - const totalsByCommand = Object.fromEntries(ALL_COMMANDS.map((k) => [k, 0])) as Record< - FunCommandKey, - number - >; +function emptyTotalsByCommand(): Record { + return Object.fromEntries(ALL_COMMANDS.map((k) => [k, 0])) as Record; +} - const now = new Date().toISOString(); +function emptyBreakdown(): Breakdown { + return Object.fromEntries(ALL_COMMANDS.map((k) => [k, 0])) as Breakdown; +} +function emptyStore(): FunUsageStoreV1 { + const now = new Date().toISOString(); return { version: 1, initializedAt: now, updatedAt: now, totalsByUser: {}, - totalsByCommand, + totalsByCommand: emptyTotalsByCommand(), breakdownByUser: {}, }; } @@ -63,50 +73,91 @@ async function ensureDataDir(): Promise { await fs.mkdir(DATA_DIR, { recursive: true }); } +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function coerceNonNegativeInt(value: unknown): number | null { + if (typeof value !== "number") return null; + if (!Number.isFinite(value)) return null; + if (!Number.isInteger(value)) return null; + if (value < 0) return null; + return value; +} + +function coerceBreakdown(value: unknown): Breakdown | null { + if (!isRecord(value)) return null; + + const out = emptyBreakdown(); + for (const k of ALL_COMMANDS) { + const n = coerceNonNegativeInt(value[k]); + if (n !== null) out[k] = n; + } + return out; +} + async function loadStore(): Promise { try { const raw = await fs.readFile(STORE_PATH, "utf8"); - const parsed = JSON.parse(raw) as Partial | null; + const parsed = JSON.parse(raw) as unknown; - if (!parsed || typeof parsed !== "object") return emptyStore(); + if (!isRecord(parsed)) return emptyStore(); if (parsed.version !== 1) return emptyStore(); - // Start from a known-good shape and merge in what exists const base = emptyStore(); - const totalsByUser = - parsed.totalsByUser && typeof parsed.totalsByUser === "object" - ? parsed.totalsByUser - : {}; - - const breakdownByUser = - parsed.breakdownByUser && typeof parsed.breakdownByUser === "object" - ? parsed.breakdownByUser - : {}; + const totalsByUser = isRecord(parsed.totalsByUser) + ? (parsed.totalsByUser as Record) + : {}; - const totalsByCommandRaw = - parsed.totalsByCommand && typeof parsed.totalsByCommand === "object" - ? parsed.totalsByCommand - : {}; + const totalsByCommandRaw = isRecord(parsed.totalsByCommand) + ? (parsed.totalsByCommand as Record) + : {}; const totalsByCommand = { ...base.totalsByCommand }; for (const k of ALL_COMMANDS) { - const v = (totalsByCommandRaw as Record)[k]; - if (typeof v === "number" && Number.isFinite(v) && v >= 0) { - totalsByCommand[k] = v; - } + const n = coerceNonNegativeInt(totalsByCommandRaw[k]); + if (n !== null) totalsByCommand[k] = n; + } + + // Canonical new field + const breakdownByUserRaw = isRecord(parsed.breakdownByUser) + ? (parsed.breakdownByUser as Record) + : {}; + + // Back-compat field (your current JSON) + const byUserByCommandRaw = isRecord(parsed.byUserByCommand) + ? (parsed.byUserByCommand as Record) + : {}; + + // Merge: breakdownByUser wins, else fallback to byUserByCommand + const breakdownByUser: Record = {}; + + const userIds = new Set([ + ...Object.keys(breakdownByUserRaw), + ...Object.keys(byUserByCommandRaw), + ...Object.keys(totalsByUser), + ]); + + for (const userId of userIds) { + const fromNew = coerceBreakdown(breakdownByUserRaw[userId]); + const fromOld = coerceBreakdown(byUserByCommandRaw[userId]); + const chosen = fromNew ?? fromOld; + + if (chosen) breakdownByUser[userId] = chosen; } return { ...base, - initializedAt: - typeof parsed.initializedAt === "string" - ? parsed.initializedAt - : base.initializedAt, + initializedAt: typeof parsed.initializedAt === "string" ? parsed.initializedAt : base.initializedAt, updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : base.updatedAt, totalsByUser, totalsByCommand, breakdownByUser, + // keep the optional field if it existed, but we won't write it anymore + byUserByCommand: isRecord(parsed.byUserByCommand) + ? (parsed.byUserByCommand as Record>) + : undefined, }; } catch { return emptyStore(); @@ -118,27 +169,17 @@ async function saveStore(store: FunUsageStoreV1): Promise { await fs.writeFile(STORE_PATH, JSON.stringify(store, null, 2), "utf8"); } -export async function recordFunUsage(args: { - userId: string; - command: FunCommandKey; -}): Promise { +export async function recordFunUsage(args: { userId: string; command: FunCommandKey }): Promise { const { userId, command } = args; const store = await loadStore(); - // totalsByUser store.totalsByUser[userId] = (store.totalsByUser[userId] ?? 0) + 1; - - // totalsByCommand store.totalsByCommand[command] = (store.totalsByCommand[command] ?? 0) + 1; - // breakdownByUser - const userBreakdown: Record = - (store.breakdownByUser[userId] as Record | undefined) ?? - ({} as Record); - - userBreakdown[command] = (userBreakdown[command] ?? 0) + 1; - store.breakdownByUser[userId] = userBreakdown; + const current = store.breakdownByUser[userId] ?? emptyBreakdown(); + current[command] = (current[command] ?? 0) + 1; + store.breakdownByUser[userId] = current; store.updatedAt = new Date().toISOString(); @@ -151,4 +192,4 @@ export async function recordFunUsage(args: { export async function getFunUsageSnapshot(): Promise { return loadStore(); -} +} \ No newline at end of file From b8370d22502efc15f787beb7f98bda71bd237eec Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 17:23:13 -0500 Subject: [PATCH 11/25] style: auto-format with Prettier [skip-precheck] --- src/commands/fun/subcommands/leaderboard.ts | 11 ++++++++--- src/services/fun/funUsageStore.ts | 17 +++++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index 374e85d3..662527f7 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,7 +1,10 @@ // src/commands/fun/subcommands/leaderboard.ts import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; -import { getFunUsageSnapshot, type FunCommandKey } from "../../../services/fun/funUsageStore.js"; +import { + getFunUsageSnapshot, + type FunCommandKey, +} from "../../../services/fun/funUsageStore.js"; import { logger } from "../../../utils/logger.js"; export type LeaderboardMode = @@ -57,7 +60,9 @@ export async function run( } if (mode.kind === "commands") { - const items = (Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]>) + const items = ( + Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]> + ) .filter(([, n]) => n > 0) .sort((a, c) => c[1] - a[1]) .slice(0, mode.limit); @@ -128,4 +133,4 @@ export async function run( embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); await interaction.editReply({ embeds: [embed] }); -} \ No newline at end of file +} diff --git a/src/services/fun/funUsageStore.ts b/src/services/fun/funUsageStore.ts index 16a627d9..a9cb93fe 100644 --- a/src/services/fun/funUsageStore.ts +++ b/src/services/fun/funUsageStore.ts @@ -50,7 +50,10 @@ const ALL_COMMANDS: FunCommandKey[] = [ ]; function emptyTotalsByCommand(): Record { - return Object.fromEntries(ALL_COMMANDS.map((k) => [k, 0])) as Record; + return Object.fromEntries(ALL_COMMANDS.map((k) => [k, 0])) as Record< + FunCommandKey, + number + >; } function emptyBreakdown(): Breakdown { @@ -149,7 +152,10 @@ async function loadStore(): Promise { return { ...base, - initializedAt: typeof parsed.initializedAt === "string" ? parsed.initializedAt : base.initializedAt, + initializedAt: + typeof parsed.initializedAt === "string" + ? parsed.initializedAt + : base.initializedAt, updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : base.updatedAt, totalsByUser, totalsByCommand, @@ -169,7 +175,10 @@ async function saveStore(store: FunUsageStoreV1): Promise { await fs.writeFile(STORE_PATH, JSON.stringify(store, null, 2), "utf8"); } -export async function recordFunUsage(args: { userId: string; command: FunCommandKey }): Promise { +export async function recordFunUsage(args: { + userId: string; + command: FunCommandKey; +}): Promise { const { userId, command } = args; const store = await loadStore(); @@ -192,4 +201,4 @@ export async function recordFunUsage(args: { userId: string; command: FunCommand export async function getFunUsageSnapshot(): Promise { return loadStore(); -} \ No newline at end of file +} From c074148696b5fefe28d0e8be93427fc184bdaaf7 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 17:33:34 -0500 Subject: [PATCH 12/25] Fixing this for once and all --- src/services/fun/funUsageStore.ts | 178 +++++++++++++++++------------- 1 file changed, 102 insertions(+), 76 deletions(-) diff --git a/src/services/fun/funUsageStore.ts b/src/services/fun/funUsageStore.ts index a9cb93fe..48f2feb9 100644 --- a/src/services/fun/funUsageStore.ts +++ b/src/services/fun/funUsageStore.ts @@ -15,8 +15,6 @@ export type FunCommandKey = | "weather7" | "leaderboard"; -type Breakdown = Record; - type FunUsageStoreV1 = { version: 1; initializedAt: string; @@ -25,11 +23,17 @@ type FunUsageStoreV1 = { totalsByUser: Record; totalsByCommand: Record; - // Canonical field name going forward - breakdownByUser: Record; - - // Back-compat only (older shape some dev builds used) - byUserByCommand?: Record>; + /** + * Canonical breakdown key. + * (We'll also accept legacy "breakdownByUser" on disk) + */ + byUserByCommand: Record>; + + /** + * Back-compat alias kept in-memory so older code can still read it. + * Do not write this separately; it mirrors byUserByCommand. + */ + breakdownByUser: Record>; }; export type FunUsageSnapshot = FunUsageStoreV1; @@ -56,19 +60,18 @@ function emptyTotalsByCommand(): Record { >; } -function emptyBreakdown(): Breakdown { - return Object.fromEntries(ALL_COMMANDS.map((k) => [k, 0])) as Breakdown; -} - function emptyStore(): FunUsageStoreV1 { const now = new Date().toISOString(); + const byUserByCommand: Record> = {}; + return { version: 1, initializedAt: now, updatedAt: now, totalsByUser: {}, totalsByCommand: emptyTotalsByCommand(), - breakdownByUser: {}, + byUserByCommand, + breakdownByUser: byUserByCommand, // alias }; } @@ -80,22 +83,52 @@ function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } -function coerceNonNegativeInt(value: unknown): number | null { - if (typeof value !== "number") return null; - if (!Number.isFinite(value)) return null; - if (!Number.isInteger(value)) return null; - if (value < 0) return null; - return value; +function normalizeTotalsByUser(raw: unknown): Record { + if (!isRecord(raw)) return {}; + const out: Record = {}; + + for (const [k, v] of Object.entries(raw)) { + if (typeof v === "number" && Number.isFinite(v) && v >= 0) out[k] = v; + } + return out; } -function coerceBreakdown(value: unknown): Breakdown | null { - if (!isRecord(value)) return null; +function normalizeTotalsByCommand(raw: unknown): Record { + const base = emptyTotalsByCommand(); + if (!isRecord(raw)) return base; - const out = emptyBreakdown(); for (const k of ALL_COMMANDS) { - const n = coerceNonNegativeInt(value[k]); - if (n !== null) out[k] = n; + const v = raw[k]; + if (typeof v === "number" && Number.isFinite(v) && v >= 0) base[k] = v; + } + return base; +} + +function normalizeByUserByCommand( + raw: unknown, +): Record> { + if (!isRecord(raw)) return {}; + + const out: Record> = {}; + + for (const [userId, perCmd] of Object.entries(raw)) { + if (!isRecord(perCmd)) continue; + + const normalized: Record = {} as Record; + + for (const cmd of ALL_COMMANDS) { + const v = perCmd[cmd]; + if (typeof v === "number" && Number.isFinite(v) && v > 0) { + normalized[cmd] = v; + } + } + + // Only store if it has at least one entry + if (Object.keys(normalized).length > 0) { + out[userId] = normalized; + } } + return out; } @@ -109,61 +142,34 @@ async function loadStore(): Promise { const base = emptyStore(); - const totalsByUser = isRecord(parsed.totalsByUser) - ? (parsed.totalsByUser as Record) - : {}; - - const totalsByCommandRaw = isRecord(parsed.totalsByCommand) - ? (parsed.totalsByCommand as Record) - : {}; - - const totalsByCommand = { ...base.totalsByCommand }; - for (const k of ALL_COMMANDS) { - const n = coerceNonNegativeInt(totalsByCommandRaw[k]); - if (n !== null) totalsByCommand[k] = n; - } + const totalsByUser = normalizeTotalsByUser(parsed.totalsByUser); + const totalsByCommand = normalizeTotalsByCommand(parsed.totalsByCommand); - // Canonical new field - const breakdownByUserRaw = isRecord(parsed.breakdownByUser) - ? (parsed.breakdownByUser as Record) - : {}; + /** + * ✅ Back-compat: accept either key from disk + * - preferred: byUserByCommand + * - legacy: breakdownByUser + */ + const byUserByCommand = normalizeByUserByCommand( + parsed.byUserByCommand ?? parsed.breakdownByUser, + ); - // Back-compat field (your current JSON) - const byUserByCommandRaw = isRecord(parsed.byUserByCommand) - ? (parsed.byUserByCommand as Record) - : {}; + const initializedAt = + typeof parsed.initializedAt === "string" ? parsed.initializedAt : base.initializedAt; - // Merge: breakdownByUser wins, else fallback to byUserByCommand - const breakdownByUser: Record = {}; - - const userIds = new Set([ - ...Object.keys(breakdownByUserRaw), - ...Object.keys(byUserByCommandRaw), - ...Object.keys(totalsByUser), - ]); - - for (const userId of userIds) { - const fromNew = coerceBreakdown(breakdownByUserRaw[userId]); - const fromOld = coerceBreakdown(byUserByCommandRaw[userId]); - const chosen = fromNew ?? fromOld; - - if (chosen) breakdownByUser[userId] = chosen; - } + const updatedAt = + typeof parsed.updatedAt === "string" ? parsed.updatedAt : base.updatedAt; return { - ...base, - initializedAt: - typeof parsed.initializedAt === "string" - ? parsed.initializedAt - : base.initializedAt, - updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : base.updatedAt, + version: 1, + initializedAt, + updatedAt, totalsByUser, totalsByCommand, - breakdownByUser, - // keep the optional field if it existed, but we won't write it anymore - byUserByCommand: isRecord(parsed.byUserByCommand) - ? (parsed.byUserByCommand as Record>) - : undefined, + + // Canonical + alias + byUserByCommand, + breakdownByUser: byUserByCommand, }; } catch { return emptyStore(); @@ -172,7 +178,18 @@ async function loadStore(): Promise { async function saveStore(store: FunUsageStoreV1): Promise { await ensureDataDir(); - await fs.writeFile(STORE_PATH, JSON.stringify(store, null, 2), "utf8"); + + // Write ONLY the canonical breakdown key to disk + const toWrite = { + version: store.version, + initializedAt: store.initializedAt, + updatedAt: store.updatedAt, + totalsByUser: store.totalsByUser, + totalsByCommand: store.totalsByCommand, + byUserByCommand: store.byUserByCommand, + }; + + await fs.writeFile(STORE_PATH, JSON.stringify(toWrite, null, 2), "utf8"); } export async function recordFunUsage(args: { @@ -183,12 +200,21 @@ export async function recordFunUsage(args: { const store = await loadStore(); + // totalsByUser store.totalsByUser[userId] = (store.totalsByUser[userId] ?? 0) + 1; + + // totalsByCommand store.totalsByCommand[command] = (store.totalsByCommand[command] ?? 0) + 1; - const current = store.breakdownByUser[userId] ?? emptyBreakdown(); - current[command] = (current[command] ?? 0) + 1; - store.breakdownByUser[userId] = current; + // byUserByCommand (canonical) + const userMap: Record = + store.byUserByCommand[userId] ?? ({} as Record); + + userMap[command] = (userMap[command] ?? 0) + 1; + store.byUserByCommand[userId] = userMap; + + // keep alias in sync + store.breakdownByUser = store.byUserByCommand; store.updatedAt = new Date().toISOString(); @@ -201,4 +227,4 @@ export async function recordFunUsage(args: { export async function getFunUsageSnapshot(): Promise { return loadStore(); -} +} \ No newline at end of file From 916ede0e3521c315f6139e435086265257a7eeee Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 17:33:40 -0500 Subject: [PATCH 13/25] style: auto-format with Prettier [skip-precheck] --- src/services/fun/funUsageStore.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/fun/funUsageStore.ts b/src/services/fun/funUsageStore.ts index 48f2feb9..01084caa 100644 --- a/src/services/fun/funUsageStore.ts +++ b/src/services/fun/funUsageStore.ts @@ -155,7 +155,9 @@ async function loadStore(): Promise { ); const initializedAt = - typeof parsed.initializedAt === "string" ? parsed.initializedAt : base.initializedAt; + typeof parsed.initializedAt === "string" + ? parsed.initializedAt + : base.initializedAt; const updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : base.updatedAt; @@ -227,4 +229,4 @@ export async function recordFunUsage(args: { export async function getFunUsageSnapshot(): Promise { return loadStore(); -} \ No newline at end of file +} From 128a14f1d1b6ecc729d0ff14a892beb142f93945 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 17:42:45 -0500 Subject: [PATCH 14/25] Adding it in --- src/services/fun/funUsageStore.ts | 303 ++++++++++-------------------- 1 file changed, 99 insertions(+), 204 deletions(-) diff --git a/src/services/fun/funUsageStore.ts b/src/services/fun/funUsageStore.ts index 01084caa..abf43663 100644 --- a/src/services/fun/funUsageStore.ts +++ b/src/services/fun/funUsageStore.ts @@ -1,232 +1,127 @@ -// src/services/fun/funUsageStore.ts - -import { promises as fs } from "node:fs"; -import path from "node:path"; -import { logger } from "../../utils/logger.js"; - -export type FunCommandKey = - | "chucknorris" - | "dadjoke" - | "dice" - | "coinflip" - | "java" - | "poll" - | "weather" - | "weather7" - | "leaderboard"; - -type FunUsageStoreV1 = { - version: 1; - initializedAt: string; - updatedAt: string; - - totalsByUser: Record; - totalsByCommand: Record; - - /** - * Canonical breakdown key. - * (We'll also accept legacy "breakdownByUser" on disk) - */ - byUserByCommand: Record>; - - /** - * Back-compat alias kept in-memory so older code can still read it. - * Do not write this separately; it mirrors byUserByCommand. - */ - breakdownByUser: Record>; -}; - -export type FunUsageSnapshot = FunUsageStoreV1; - -const DATA_DIR = path.join(process.cwd(), "data"); -const STORE_PATH = path.join(DATA_DIR, "fun-usage.json"); - -const ALL_COMMANDS: FunCommandKey[] = [ - "chucknorris", - "dadjoke", - "dice", - "coinflip", - "java", - "poll", - "weather", - "weather7", - "leaderboard", -]; - -function emptyTotalsByCommand(): Record { - return Object.fromEntries(ALL_COMMANDS.map((k) => [k, 0])) as Record< - FunCommandKey, - number - >; -} +// src/commands/fun/subcommands/leaderboard.ts -function emptyStore(): FunUsageStoreV1 { - const now = new Date().toISOString(); - const byUserByCommand: Record> = {}; - - return { - version: 1, - initializedAt: now, - updatedAt: now, - totalsByUser: {}, - totalsByCommand: emptyTotalsByCommand(), - byUserByCommand, - breakdownByUser: byUserByCommand, // alias - }; -} +import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; +import { + getFunUsageSnapshot, + type FunCommandKey, +} from "../../../services/fun/funUsageStore.js"; +import { logger } from "../../../utils/logger.js"; -async function ensureDataDir(): Promise { - await fs.mkdir(DATA_DIR, { recursive: true }); -} +export type LeaderboardMode = + | { kind: "users"; limit: number } + | { kind: "commands"; limit: number } + | { kind: "user"; userId: string }; -function isRecord(value: unknown): value is Record { - return Boolean(value) && typeof value === "object" && !Array.isArray(value); -} +type Breakdown = Record; -function normalizeTotalsByUser(raw: unknown): Record { - if (!isRecord(raw)) return {}; - const out: Record = {}; +function formatBreakdown(b: Breakdown | undefined, maxItems: number): string { + if (!b) return ""; - for (const [k, v] of Object.entries(raw)) { - if (typeof v === "number" && Number.isFinite(v) && v >= 0) out[k] = v; - } - return out; -} + const items = (Object.entries(b) as Array<[FunCommandKey, number]>) + .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) + .sort((a, c) => c[1] - a[1]) + .slice(0, maxItems) + .map(([k, n]) => `${n}x ${k}`); -function normalizeTotalsByCommand(raw: unknown): Record { - const base = emptyTotalsByCommand(); - if (!isRecord(raw)) return base; + return items.length ? ` (${items.join(", ")})` : ""; +} - for (const k of ALL_COMMANDS) { - const v = raw[k]; - if (typeof v === "number" && Number.isFinite(v) && v >= 0) base[k] = v; +async function safeFetchUser( + interaction: ChatInputCommandInteraction, + userId: string, +): Promise { + try { + return await interaction.client.users.fetch(userId); + } catch (err) { + logger.debug({ err, userId }, "[fun/leaderboard] failed to fetch user"); + return null; } - return base; } -function normalizeByUserByCommand( - raw: unknown, -): Record> { - if (!isRecord(raw)) return {}; - - const out: Record> = {}; +export async function run( + interaction: ChatInputCommandInteraction, + mode: LeaderboardMode, +): Promise { + const store = await getFunUsageSnapshot(); + + const embed = new EmbedBuilder().setFooter({ + text: `Updated: ${store.updatedAt}`, + }); + + const anyUsage = + Object.keys(store.totalsByUser).length > 0 || + Object.values(store.totalsByCommand).some((n) => typeof n === "number" && n > 0); + + if (!anyUsage) { + embed.setTitle("🏆 Fun Leaderboard"); + embed.setDescription("No fun command usage recorded yet."); + await interaction.editReply({ embeds: [embed] }); + return; + } - for (const [userId, perCmd] of Object.entries(raw)) { - if (!isRecord(perCmd)) continue; + if (mode.kind === "commands") { + const items = ( + Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]> + ) + .filter(([, n]) => typeof n === "number" && n > 0) + .sort((a, c) => c[1] - a[1]) + .slice(0, mode.limit); - const normalized: Record = {} as Record; + embed.setTitle("🎉 Fun Leaderboard: Top Commands"); - for (const cmd of ALL_COMMANDS) { - const v = perCmd[cmd]; - if (typeof v === "number" && Number.isFinite(v) && v > 0) { - normalized[cmd] = v; - } - } + const lines = items.map(([cmd, count], idx) => { + const rank = idx + 1; + return `${rank}. \`${cmd}\` — **${count}**`; + }); - // Only store if it has at least one entry - if (Object.keys(normalized).length > 0) { - out[userId] = normalized; - } + embed.setDescription(lines.length ? lines.join("\n") : "No command usage yet."); + await interaction.editReply({ embeds: [embed] }); + return; } - return out; -} - -async function loadStore(): Promise { - try { - const raw = await fs.readFile(STORE_PATH, "utf8"); - const parsed = JSON.parse(raw) as unknown; - - if (!isRecord(parsed)) return emptyStore(); - if (parsed.version !== 1) return emptyStore(); + if (mode.kind === "user") { + const total = store.totalsByUser[mode.userId] ?? 0; + const breakdown = store.byUserByCommand[mode.userId] as Breakdown | undefined; - const base = emptyStore(); + const u = await safeFetchUser(interaction, mode.userId); + const titleName = u ? u.username : `User ${mode.userId}`; + const avatarUrl = u?.displayAvatarURL() ?? null; - const totalsByUser = normalizeTotalsByUser(parsed.totalsByUser); - const totalsByCommand = normalizeTotalsByCommand(parsed.totalsByCommand); + embed.setTitle(`👤 Fun Usage: ${titleName}`); + if (avatarUrl) embed.setThumbnail(avatarUrl); - /** - * ✅ Back-compat: accept either key from disk - * - preferred: byUserByCommand - * - legacy: breakdownByUser - */ - const byUserByCommand = normalizeByUserByCommand( - parsed.byUserByCommand ?? parsed.breakdownByUser, + const breakdownText = formatBreakdown(breakdown, 25); + embed.setDescription( + [ + `User: <@${mode.userId}>`, + `Total uses: **${total}**`, + breakdownText ? `Breakdown:${breakdownText}` : "Breakdown: (none yet)", + ].join("\n"), ); - const initializedAt = - typeof parsed.initializedAt === "string" - ? parsed.initializedAt - : base.initializedAt; - - const updatedAt = - typeof parsed.updatedAt === "string" ? parsed.updatedAt : base.updatedAt; - - return { - version: 1, - initializedAt, - updatedAt, - totalsByUser, - totalsByCommand, - - // Canonical + alias - byUserByCommand, - breakdownByUser: byUserByCommand, - }; - } catch { - return emptyStore(); + await interaction.editReply({ embeds: [embed] }); + return; } -} - -async function saveStore(store: FunUsageStoreV1): Promise { - await ensureDataDir(); - - // Write ONLY the canonical breakdown key to disk - const toWrite = { - version: store.version, - initializedAt: store.initializedAt, - updatedAt: store.updatedAt, - totalsByUser: store.totalsByUser, - totalsByCommand: store.totalsByCommand, - byUserByCommand: store.byUserByCommand, - }; - - await fs.writeFile(STORE_PATH, JSON.stringify(toWrite, null, 2), "utf8"); -} - -export async function recordFunUsage(args: { - userId: string; - command: FunCommandKey; -}): Promise { - const { userId, command } = args; - - const store = await loadStore(); - // totalsByUser - store.totalsByUser[userId] = (store.totalsByUser[userId] ?? 0) + 1; + // Top users (default) + const userItems = Object.entries(store.totalsByUser) + .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) + .sort((a, c) => c[1] - a[1]) + .slice(0, mode.limit); - // totalsByCommand - store.totalsByCommand[command] = (store.totalsByCommand[command] ?? 0) + 1; + embed.setTitle("🏆 Fun Leaderboard: Top Users"); - // byUserByCommand (canonical) - const userMap: Record = - store.byUserByCommand[userId] ?? ({} as Record); + const lines = userItems.map(([userId, total], idx) => { + const rank = idx + 1; - userMap[command] = (userMap[command] ?? 0) + 1; - store.byUserByCommand[userId] = userMap; + const breakdown = store.byUserByCommand[userId] as Breakdown | undefined; + const suffix = formatBreakdown(breakdown, 6); // show up to 6 commands per user line - // keep alias in sync - store.breakdownByUser = store.byUserByCommand; + // This is the exact thing you want: + // "Nick — 10 (3x chucknorris, 2x coinflip, ...)" + return `${rank}. <@${userId}> — **${total}**${suffix}`; + }); - store.updatedAt = new Date().toISOString(); - - try { - await saveStore(store); - } catch (err) { - logger.error({ err }, "[fun/usage] failed to save fun usage store"); - } -} - -export async function getFunUsageSnapshot(): Promise { - return loadStore(); -} + embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); + await interaction.editReply({ embeds: [embed] }); +} \ No newline at end of file From 60b70faf58abefe6309a3c5371d16a0f4c665503 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 17:51:48 -0500 Subject: [PATCH 15/25] Adding stuff in --- src/commands/fun/subcommands/leaderboard.ts | 54 ++-- src/services/fun/funUsageStore.ts | 267 ++++++++++++-------- 2 files changed, 191 insertions(+), 130 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index 662527f7..5df6d047 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,10 +1,7 @@ // src/commands/fun/subcommands/leaderboard.ts import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; -import { - getFunUsageSnapshot, - type FunCommandKey, -} from "../../../services/fun/funUsageStore.js"; +import { getFunUsageSnapshot, type FunCommandKey } from "../../../services/fun/funUsageStore.js"; import { logger } from "../../../utils/logger.js"; export type LeaderboardMode = @@ -17,7 +14,8 @@ type Breakdown = Record; function formatBreakdown(b: Breakdown | undefined, maxItems: number): string { if (!b) return ""; - const parts = (Object.entries(b) as Array<[FunCommandKey, number]>) + const entries = Object.entries(b) as Array<[FunCommandKey, number]>; + const parts = entries .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) .sort((a, c) => c[1] - a[1]) .slice(0, maxItems) @@ -44,13 +42,11 @@ export async function run( ): Promise { const store = await getFunUsageSnapshot(); - const embed = new EmbedBuilder().setFooter({ - text: `Updated: ${store.updatedAt}`, - }); - const anyUsage = Object.keys(store.totalsByUser).length > 0 || - Object.values(store.totalsByCommand).some((n) => n > 0); + Object.values(store.totalsByCommand).some((n) => typeof n === "number" && n > 0); + + const embed = new EmbedBuilder().setFooter({ text: `Updated: ${store.updatedAt}` }); if (!anyUsage) { embed.setTitle("🏆 Fun Leaderboard"); @@ -60,16 +56,14 @@ export async function run( } if (mode.kind === "commands") { - const items = ( - Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]> - ) - .filter(([, n]) => n > 0) + const items = (Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]>) + .filter(([, n]) => typeof n === "number" && n > 0) .sort((a, c) => c[1] - a[1]) .slice(0, mode.limit); embed.setTitle("🎉 Fun Leaderboard: Top Commands"); - const lines = items.map(([cmd, count], idx) => { + const lines = items.map(([cmd, count], idx: number) => { const rank = idx + 1; return `${rank}. \`${cmd}\` — **${count}**`; }); @@ -81,13 +75,15 @@ export async function run( if (mode.kind === "user") { const total = store.totalsByUser[mode.userId] ?? 0; - const breakdown = store.breakdownByUser[mode.userId]; + + // canonical per-user breakdown + const breakdown = store.byUserByCommand[mode.userId] as Breakdown | undefined; const u = await safeFetchUser(interaction, mode.userId); - const display = u ? `${u.username}` : `User ${mode.userId}`; + const displayName = u ? u.username : `User ${mode.userId}`; const avatarUrl = u?.displayAvatarURL() ?? null; - embed.setTitle(`👤 Fun Usage: ${display}`); + embed.setTitle(`👤 Fun Usage: ${displayName}`); if (avatarUrl) embed.setThumbnail(avatarUrl); const breakdownText = formatBreakdown(breakdown, 10); @@ -105,32 +101,32 @@ export async function run( // Top users const userItems = Object.entries(store.totalsByUser) .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) - .sort((a, c) => c[1] - a[1]) + .sort((a, c) => (c[1] as number) - (a[1] as number)) .slice(0, mode.limit); embed.setTitle("🏆 Fun Leaderboard: Top Users"); - const users = await Promise.all( + // Best-effort fetch users for nicer labels + const fetched = await Promise.all( userItems.map(async ([userId]) => { const u = await safeFetchUser(interaction, userId); return { userId, user: u }; }), ); - const lines = userItems.map(([userId, total], idx) => { + const lines = userItems.map(([userId, total], idx: number) => { const rank = idx + 1; - const u = users.find((x) => x.userId === userId)?.user ?? null; - const avatarUrl = u?.displayAvatarURL() ?? null; - const breakdown = store.breakdownByUser[userId]; - const breakdownText = formatBreakdown(breakdown, 4); + const u = fetched.find((x) => x.userId === userId)?.user ?? null; + const namePart = u ? `**${u.username}**` : `<@${userId}>`; - const avatarPart = avatarUrl ? ` • ${avatarUrl}` : ""; - const breakdownPart = breakdownText ? ` • ${breakdownText}` : ""; + const breakdown = store.byUserByCommand[userId] as Breakdown | undefined; + const breakdownText = formatBreakdown(breakdown, 4); + const suffix = breakdownText ? ` (${breakdownText})` : ""; - return `${rank}. <@${userId}> — **${total}**${breakdownPart}${avatarPart}`; + return `${rank}. ${namePart} — **${total}**${suffix}`; }); embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); await interaction.editReply({ embeds: [embed] }); -} +} \ No newline at end of file diff --git a/src/services/fun/funUsageStore.ts b/src/services/fun/funUsageStore.ts index abf43663..8e2739ae 100644 --- a/src/services/fun/funUsageStore.ts +++ b/src/services/fun/funUsageStore.ts @@ -1,127 +1,192 @@ -// src/commands/fun/subcommands/leaderboard.ts - -import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; -import { - getFunUsageSnapshot, - type FunCommandKey, -} from "../../../services/fun/funUsageStore.js"; -import { logger } from "../../../utils/logger.js"; - -export type LeaderboardMode = - | { kind: "users"; limit: number } - | { kind: "commands"; limit: number } - | { kind: "user"; userId: string }; +// src/services/fun/funUsageStore.ts + +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { logger } from "../../utils/logger.js"; + +export type FunCommandKey = + | "chucknorris" + | "dadjoke" + | "dice" + | "coinflip" + | "java" + | "poll" + | "weather" + | "weather7" + | "leaderboard"; + +type FunUsageStoreV1 = { + version: 1; + initializedAt: string; + updatedAt: string; + + totalsByUser: Record; + totalsByCommand: Record; + + // This is the canonical per-user breakdown (matches your JSON) + byUserByCommand: Record>; + + // Backward-compat: older name some code used + breakdownByUser?: Record>; +}; + +export type FunUsageSnapshot = FunUsageStoreV1; + +const DATA_DIR = path.join(process.cwd(), "data"); +const STORE_PATH = path.join(DATA_DIR, "fun-usage.json"); + +const ALL_COMMANDS: FunCommandKey[] = [ + "chucknorris", + "dadjoke", + "dice", + "coinflip", + "java", + "poll", + "weather", + "weather7", + "leaderboard", +]; + +function nowIso(): string { + return new Date().toISOString(); +} -type Breakdown = Record; +function emptyTotalsByCommand(): Record { + return Object.fromEntries(ALL_COMMANDS.map((k) => [k, 0])) as Record; +} -function formatBreakdown(b: Breakdown | undefined, maxItems: number): string { - if (!b) return ""; +function emptyStore(): FunUsageStoreV1 { + const now = nowIso(); + return { + version: 1, + initializedAt: now, + updatedAt: now, + totalsByUser: {}, + totalsByCommand: emptyTotalsByCommand(), + byUserByCommand: {}, + }; +} - const items = (Object.entries(b) as Array<[FunCommandKey, number]>) - .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) - .sort((a, c) => c[1] - a[1]) - .slice(0, maxItems) - .map(([k, n]) => `${n}x ${k}`); +async function ensureDataDir(): Promise { + await fs.mkdir(DATA_DIR, { recursive: true }); +} - return items.length ? ` (${items.join(", ")})` : ""; +function isRecord(v: unknown): v is Record { + return Boolean(v) && typeof v === "object" && !Array.isArray(v); } -async function safeFetchUser( - interaction: ChatInputCommandInteraction, - userId: string, -): Promise { - try { - return await interaction.client.users.fetch(userId); - } catch (err) { - logger.debug({ err, userId }, "[fun/leaderboard] failed to fetch user"); - return null; +function sanitizeTotalsByUser(input: unknown): Record { + if (!isRecord(input)) return {}; + const out: Record = {}; + for (const [k, v] of Object.entries(input)) { + if (typeof v === "number" && Number.isFinite(v) && v >= 0) out[k] = v; } + return out; } -export async function run( - interaction: ChatInputCommandInteraction, - mode: LeaderboardMode, -): Promise { - const store = await getFunUsageSnapshot(); - - const embed = new EmbedBuilder().setFooter({ - text: `Updated: ${store.updatedAt}`, - }); - - const anyUsage = - Object.keys(store.totalsByUser).length > 0 || - Object.values(store.totalsByCommand).some((n) => typeof n === "number" && n > 0); - - if (!anyUsage) { - embed.setTitle("🏆 Fun Leaderboard"); - embed.setDescription("No fun command usage recorded yet."); - await interaction.editReply({ embeds: [embed] }); - return; - } +function sanitizeTotalsByCommand(input: unknown): Record { + const base = emptyTotalsByCommand(); + if (!isRecord(input)) return base; - if (mode.kind === "commands") { - const items = ( - Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]> - ) - .filter(([, n]) => typeof n === "number" && n > 0) - .sort((a, c) => c[1] - a[1]) - .slice(0, mode.limit); + for (const key of ALL_COMMANDS) { + const v = input[key]; + if (typeof v === "number" && Number.isFinite(v) && v >= 0) base[key] = v; + } - embed.setTitle("🎉 Fun Leaderboard: Top Commands"); + return base; +} - const lines = items.map(([cmd, count], idx) => { - const rank = idx + 1; - return `${rank}. \`${cmd}\` — **${count}**`; - }); +function sanitizeByUserByCommand(input: unknown): Record> { + if (!isRecord(input)) return {}; + const out: Record> = {}; - embed.setDescription(lines.length ? lines.join("\n") : "No command usage yet."); - await interaction.editReply({ embeds: [embed] }); - return; - } + for (const [userId, rawBreakdown] of Object.entries(input)) { + if (!isRecord(rawBreakdown)) continue; - if (mode.kind === "user") { - const total = store.totalsByUser[mode.userId] ?? 0; - const breakdown = store.byUserByCommand[mode.userId] as Breakdown | undefined; + const breakdown: Record = emptyTotalsByCommand(); + let any = false; - const u = await safeFetchUser(interaction, mode.userId); - const titleName = u ? u.username : `User ${mode.userId}`; - const avatarUrl = u?.displayAvatarURL() ?? null; + for (const cmd of ALL_COMMANDS) { + const v = rawBreakdown[cmd]; + if (typeof v === "number" && Number.isFinite(v) && v > 0) { + breakdown[cmd] = v; + any = true; + } + } - embed.setTitle(`👤 Fun Usage: ${titleName}`); - if (avatarUrl) embed.setThumbnail(avatarUrl); + if (any) out[userId] = breakdown; + } - const breakdownText = formatBreakdown(breakdown, 25); - embed.setDescription( - [ - `User: <@${mode.userId}>`, - `Total uses: **${total}**`, - breakdownText ? `Breakdown:${breakdownText}` : "Breakdown: (none yet)", - ].join("\n"), - ); + return out; +} - await interaction.editReply({ embeds: [embed] }); - return; +async function loadStore(): Promise { + try { + const raw = await fs.readFile(STORE_PATH, "utf8"); + const parsed = JSON.parse(raw) as unknown; + + if (!isRecord(parsed)) return emptyStore(); + if (parsed.version !== 1) return emptyStore(); + + const base = emptyStore(); + + const initializedAt = + typeof parsed.initializedAt === "string" ? parsed.initializedAt : base.initializedAt; + const updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : base.updatedAt; + + const totalsByUser = sanitizeTotalsByUser(parsed.totalsByUser); + const totalsByCommand = sanitizeTotalsByCommand(parsed.totalsByCommand); + + // Prefer canonical name + const byUserByCommand = sanitizeByUserByCommand(parsed.byUserByCommand); + + // Backward-compat: if file has breakdownByUser but not byUserByCommand + const legacyBreakdown = sanitizeByUserByCommand(parsed.breakdownByUser); + const finalByUserByCommand = + Object.keys(byUserByCommand).length > 0 ? byUserByCommand : legacyBreakdown; + + return { + ...base, + initializedAt, + updatedAt, + totalsByUser, + totalsByCommand, + byUserByCommand: finalByUserByCommand, + }; + } catch { + return emptyStore(); } +} + +async function saveStore(store: FunUsageStoreV1): Promise { + await ensureDataDir(); + await fs.writeFile(STORE_PATH, JSON.stringify(store, null, 2), "utf8"); +} - // Top users (default) - const userItems = Object.entries(store.totalsByUser) - .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) - .sort((a, c) => c[1] - a[1]) - .slice(0, mode.limit); +export async function recordFunUsage(args: { + userId: string; + command: FunCommandKey; +}): Promise { + const { userId, command } = args; - embed.setTitle("🏆 Fun Leaderboard: Top Users"); + const store = await loadStore(); - const lines = userItems.map(([userId, total], idx) => { - const rank = idx + 1; + store.totalsByUser[userId] = (store.totalsByUser[userId] ?? 0) + 1; + store.totalsByCommand[command] = (store.totalsByCommand[command] ?? 0) + 1; - const breakdown = store.byUserByCommand[userId] as Breakdown | undefined; - const suffix = formatBreakdown(breakdown, 6); // show up to 6 commands per user line + const existing = store.byUserByCommand[userId] ?? emptyTotalsByCommand(); + existing[command] = (existing[command] ?? 0) + 1; + store.byUserByCommand[userId] = existing; - // This is the exact thing you want: - // "Nick — 10 (3x chucknorris, 2x coinflip, ...)" - return `${rank}. <@${userId}> — **${total}**${suffix}`; - }); + store.updatedAt = nowIso(); + + try { + await saveStore(store); + } catch (err) { + logger.error({ err }, "[fun/usage] failed to save fun usage store"); + } +} - embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); - await interaction.editReply({ embeds: [embed] }); +export async function getFunUsageSnapshot(): Promise { + return loadStore(); } \ No newline at end of file From 8230420cb46fa498eca4bacd4264e013e068883e Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 17:51:53 -0500 Subject: [PATCH 16/25] style: auto-format with Prettier [skip-precheck] --- src/commands/fun/subcommands/leaderboard.ts | 11 ++++++++--- src/services/fun/funUsageStore.ts | 18 +++++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index 5df6d047..62a96dca 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,7 +1,10 @@ // src/commands/fun/subcommands/leaderboard.ts import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; -import { getFunUsageSnapshot, type FunCommandKey } from "../../../services/fun/funUsageStore.js"; +import { + getFunUsageSnapshot, + type FunCommandKey, +} from "../../../services/fun/funUsageStore.js"; import { logger } from "../../../utils/logger.js"; export type LeaderboardMode = @@ -56,7 +59,9 @@ export async function run( } if (mode.kind === "commands") { - const items = (Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]>) + const items = ( + Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]> + ) .filter(([, n]) => typeof n === "number" && n > 0) .sort((a, c) => c[1] - a[1]) .slice(0, mode.limit); @@ -129,4 +134,4 @@ export async function run( embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); await interaction.editReply({ embeds: [embed] }); -} \ No newline at end of file +} diff --git a/src/services/fun/funUsageStore.ts b/src/services/fun/funUsageStore.ts index 8e2739ae..8d0e14bb 100644 --- a/src/services/fun/funUsageStore.ts +++ b/src/services/fun/funUsageStore.ts @@ -52,7 +52,10 @@ function nowIso(): string { } function emptyTotalsByCommand(): Record { - return Object.fromEntries(ALL_COMMANDS.map((k) => [k, 0])) as Record; + return Object.fromEntries(ALL_COMMANDS.map((k) => [k, 0])) as Record< + FunCommandKey, + number + >; } function emptyStore(): FunUsageStoreV1 { @@ -96,7 +99,9 @@ function sanitizeTotalsByCommand(input: unknown): Record return base; } -function sanitizeByUserByCommand(input: unknown): Record> { +function sanitizeByUserByCommand( + input: unknown, +): Record> { if (!isRecord(input)) return {}; const out: Record> = {}; @@ -131,8 +136,11 @@ async function loadStore(): Promise { const base = emptyStore(); const initializedAt = - typeof parsed.initializedAt === "string" ? parsed.initializedAt : base.initializedAt; - const updatedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : base.updatedAt; + typeof parsed.initializedAt === "string" + ? parsed.initializedAt + : base.initializedAt; + const updatedAt = + typeof parsed.updatedAt === "string" ? parsed.updatedAt : base.updatedAt; const totalsByUser = sanitizeTotalsByUser(parsed.totalsByUser); const totalsByCommand = sanitizeTotalsByCommand(parsed.totalsByCommand); @@ -189,4 +197,4 @@ export async function recordFunUsage(args: { export async function getFunUsageSnapshot(): Promise { return loadStore(); -} \ No newline at end of file +} From fab360eb7bb1e6f9545ecfdf85e7689dc5589fcc Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Wed, 14 Jan 2026 17:38:21 -0500 Subject: [PATCH 17/25] Fixing this up --- src/commands/fun/subcommands/leaderboard.ts | 137 +++++++++++--------- 1 file changed, 75 insertions(+), 62 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index 62a96dca..48baef0e 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,6 +1,10 @@ // src/commands/fun/subcommands/leaderboard.ts -import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; +import { + EmbedBuilder, + type ChatInputCommandInteraction, + type User, +} from "discord.js"; import { getFunUsageSnapshot, type FunCommandKey, @@ -12,19 +16,21 @@ export type LeaderboardMode = | { kind: "commands"; limit: number } | { kind: "user"; userId: string }; -type Breakdown = Record; +type PerCommandCounts = Record; -function formatBreakdown(b: Breakdown | undefined, maxItems: number): string { - if (!b) return ""; +/** + * Convert a per-user command map into a clean, non-ranking usage list. + * Example: + * /fun coinflip — 4 + * /fun java — 1 + */ +function formatUserUsageLines(perCmd: PerCommandCounts | undefined): string[] { + if (!perCmd) return []; - const entries = Object.entries(b) as Array<[FunCommandKey, number]>; - const parts = entries + return Object.entries(perCmd) .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) - .sort((a, c) => c[1] - a[1]) - .slice(0, maxItems) - .map(([k, n]) => `${n}x ${k}`); - - return parts.length ? parts.join(", ") : ""; + .sort((a, b) => b[1] - a[1]) + .map(([cmd, count]) => `/fun ${cmd} — **${count}**`); } async function safeFetchUser( @@ -39,63 +45,85 @@ async function safeFetchUser( } } +function isAnyUsage(snapshot: { + totalsByUser?: Record; + totalsByCommand?: Record; + byUserByCommand?: Record>; +}): boolean { + const byUser = snapshot.totalsByUser ?? {}; + const byCmd = snapshot.totalsByCommand ?? {}; + const perUser = snapshot.byUserByCommand ?? {}; + + return ( + Object.keys(byUser).length > 0 || + Object.values(byCmd).some((n) => typeof n === "number" && n > 0) || + Object.keys(perUser).length > 0 + ); +} + export async function run( interaction: ChatInputCommandInteraction, mode: LeaderboardMode, ): Promise { - const store = await getFunUsageSnapshot(); + const snapshot = await getFunUsageSnapshot(); - const anyUsage = - Object.keys(store.totalsByUser).length > 0 || - Object.values(store.totalsByCommand).some((n) => typeof n === "number" && n > 0); + const updatedAt = + typeof snapshot.updatedAt === "string" ? snapshot.updatedAt : "unknown"; - const embed = new EmbedBuilder().setFooter({ text: `Updated: ${store.updatedAt}` }); + const embed = new EmbedBuilder().setFooter({ text: `Updated: ${updatedAt}` }); - if (!anyUsage) { - embed.setTitle("🏆 Fun Leaderboard"); + if (!isAnyUsage(snapshot)) { + embed.setTitle("Fun Leaderboard"); embed.setDescription("No fun command usage recorded yet."); await interaction.editReply({ embeds: [embed] }); return; } + // ------------------------------------------------------------ + // View: Top commands + // ------------------------------------------------------------ if (mode.kind === "commands") { - const items = ( - Object.entries(store.totalsByCommand) as Array<[FunCommandKey, number]> - ) - .filter(([, n]) => typeof n === "number" && n > 0) - .sort((a, c) => c[1] - a[1]) + const totals = snapshot.totalsByCommand ?? {}; + const items = (Object.entries(totals) as Array<[string, number]>) + .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) + .sort((a, b) => b[1] - a[1]) .slice(0, mode.limit); - embed.setTitle("🎉 Fun Leaderboard: Top Commands"); - - const lines = items.map(([cmd, count], idx: number) => { - const rank = idx + 1; - return `${rank}. \`${cmd}\` — **${count}**`; - }); + embed.setTitle("Fun Leaderboard: Top Commands"); + const lines = items.map(([cmd, count]) => `/fun ${cmd} — **${count}**`); embed.setDescription(lines.length ? lines.join("\n") : "No command usage yet."); + await interaction.editReply({ embeds: [embed] }); return; } + // ------------------------------------------------------------ + // View: Single user usage (includes avatar thumbnail) + // ------------------------------------------------------------ if (mode.kind === "user") { - const total = store.totalsByUser[mode.userId] ?? 0; + const totalsByUser = snapshot.totalsByUser ?? {}; + const total = totalsByUser[mode.userId] ?? 0; - // canonical per-user breakdown - const breakdown = store.byUserByCommand[mode.userId] as Breakdown | undefined; + const perCmd = + (snapshot.byUserByCommand?.[mode.userId] as Record | undefined) ?? + undefined; const u = await safeFetchUser(interaction, mode.userId); - const displayName = u ? u.username : `User ${mode.userId}`; + const titleName = u?.username ?? `User ${mode.userId}`; const avatarUrl = u?.displayAvatarURL() ?? null; - embed.setTitle(`👤 Fun Usage: ${displayName}`); + embed.setTitle(`Fun Usage: ${titleName}`); if (avatarUrl) embed.setThumbnail(avatarUrl); - const breakdownText = formatBreakdown(breakdown, 10); + const lines = formatUserUsageLines(perCmd); + + // Flat list, no ranks/medals embed.setDescription( [ `Total uses: **${total}**`, - breakdownText ? `Breakdown: ${breakdownText}` : "Breakdown: (none yet)", + "", + ...(lines.length ? lines : ["No per-command usage recorded yet."]), ].join("\n"), ); @@ -103,35 +131,20 @@ export async function run( return; } - // Top users - const userItems = Object.entries(store.totalsByUser) + // ------------------------------------------------------------ + // View: Top users (no avatars, no ranking semantics) + // ------------------------------------------------------------ + const totalsByUser = snapshot.totalsByUser ?? {}; + const userItems = Object.entries(totalsByUser) .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) - .sort((a, c) => (c[1] as number) - (a[1] as number)) + .sort((a, b) => b[1] - a[1]) .slice(0, mode.limit); - embed.setTitle("🏆 Fun Leaderboard: Top Users"); - - // Best-effort fetch users for nicer labels - const fetched = await Promise.all( - userItems.map(async ([userId]) => { - const u = await safeFetchUser(interaction, userId); - return { userId, user: u }; - }), - ); - - const lines = userItems.map(([userId, total], idx: number) => { - const rank = idx + 1; - - const u = fetched.find((x) => x.userId === userId)?.user ?? null; - const namePart = u ? `**${u.username}**` : `<@${userId}>`; - - const breakdown = store.byUserByCommand[userId] as Breakdown | undefined; - const breakdownText = formatBreakdown(breakdown, 4); - const suffix = breakdownText ? ` (${breakdownText})` : ""; - - return `${rank}. ${namePart} — **${total}**${suffix}`; - }); + embed.setTitle("Fun Leaderboard: Top Users"); + // Flat list, no 1/2/3 medals or ranks + const lines = userItems.map(([userId, total]) => `<@${userId}> — **${total}**`); embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); + await interaction.editReply({ embeds: [embed] }); -} +} \ No newline at end of file From 87e6605b517d8629947dd30ba8e781d22d11deaf Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Wed, 14 Jan 2026 17:38:27 -0500 Subject: [PATCH 18/25] style: auto-format with Prettier [skip-precheck] --- src/commands/fun/subcommands/leaderboard.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index 48baef0e..297aacd2 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,10 +1,6 @@ // src/commands/fun/subcommands/leaderboard.ts -import { - EmbedBuilder, - type ChatInputCommandInteraction, - type User, -} from "discord.js"; +import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; import { getFunUsageSnapshot, type FunCommandKey, @@ -147,4 +143,4 @@ export async function run( embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); await interaction.editReply({ embeds: [embed] }); -} \ No newline at end of file +} From 0caa86669ef11278ddc9ae58b7a578b9f65fa3fd Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Wed, 14 Jan 2026 17:53:35 -0500 Subject: [PATCH 19/25] Fixing this up --- src/commands/fun/subcommands/leaderboard.ts | 191 ++++++++++++-------- 1 file changed, 118 insertions(+), 73 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index 297aacd2..8df74a22 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,6 +1,10 @@ // src/commands/fun/subcommands/leaderboard.ts -import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; +import { + EmbedBuilder, + type ChatInputCommandInteraction, + type User, +} from "discord.js"; import { getFunUsageSnapshot, type FunCommandKey, @@ -12,21 +16,65 @@ export type LeaderboardMode = | { kind: "commands"; limit: number } | { kind: "user"; userId: string }; -type PerCommandCounts = Record; +type PerUserByCommand = Record>; + +function isFunCommandKey(value: string): value is FunCommandKey { + const allowed: ReadonlySet = new Set([ + "chucknorris", + "dadjoke", + "dice", + "coinflip", + "java", + "poll", + "weather", + "weather7", + "leaderboard", + ]); + return allowed.has(value); +} + +function formatCount(n: number): string { + return `${n}x`; +} + +function getPerCmd( + byUserByCommand: PerUserByCommand | undefined, + userId: string, +): Record { + const raw = (byUserByCommand ?? {})[userId] ?? {}; + const out: Partial> = {}; + + for (const [k, v] of Object.entries(raw)) { + if (!isFunCommandKey(k)) continue; + if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) continue; + out[k] = v; + } + + return out as Record; +} -/** - * Convert a per-user command map into a clean, non-ranking usage list. - * Example: - * /fun coinflip — 4 - * /fun java — 1 - */ -function formatUserUsageLines(perCmd: PerCommandCounts | undefined): string[] { - if (!perCmd) return []; +function formatTopCommandsInline( + perCmd: Record, + maxItems: number, +): string { + const parts = (Object.entries(perCmd) as Array<[FunCommandKey, number]>) + .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) + .sort((a, b) => b[1] - a[1]) + .slice(0, maxItems) + .map(([cmd, n]) => `${cmd} ${formatCount(n)}`); - return Object.entries(perCmd) + return parts.length ? parts.join(", ") : ""; +} + +function formatUserBreakdownLines( + perCmd: Record, + maxItems: number, +): string[] { + return (Object.entries(perCmd) as Array<[FunCommandKey, number]>) .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) .sort((a, b) => b[1] - a[1]) - .map(([cmd, count]) => `/fun ${cmd} — **${count}**`); + .slice(0, maxItems) + .map(([cmd, n]) => `• \`/fun ${cmd}\` ${formatCount(n)}`); } async function safeFetchUser( @@ -41,106 +89,103 @@ async function safeFetchUser( } } -function isAnyUsage(snapshot: { - totalsByUser?: Record; - totalsByCommand?: Record; - byUserByCommand?: Record>; -}): boolean { - const byUser = snapshot.totalsByUser ?? {}; - const byCmd = snapshot.totalsByCommand ?? {}; - const perUser = snapshot.byUserByCommand ?? {}; - - return ( - Object.keys(byUser).length > 0 || - Object.values(byCmd).some((n) => typeof n === "number" && n > 0) || - Object.keys(perUser).length > 0 - ); -} - export async function run( interaction: ChatInputCommandInteraction, mode: LeaderboardMode, ): Promise { const snapshot = await getFunUsageSnapshot(); - const updatedAt = - typeof snapshot.updatedAt === "string" ? snapshot.updatedAt : "unknown"; + const embed = new EmbedBuilder().setFooter({ + text: `Updated: ${snapshot.updatedAt}`, + }); + + const totalsByUser = snapshot.totalsByUser ?? {}; + const totalsByCommand = snapshot.totalsByCommand ?? {}; + const byUserByCommand = snapshot.byUserByCommand ?? {}; - const embed = new EmbedBuilder().setFooter({ text: `Updated: ${updatedAt}` }); + const anyUsage = + Object.keys(totalsByUser).length > 0 || + Object.values(totalsByCommand).some( + (n) => typeof n === "number" && Number.isFinite(n) && n > 0, + ); - if (!isAnyUsage(snapshot)) { - embed.setTitle("Fun Leaderboard"); - embed.setDescription("No fun command usage recorded yet."); + if (!anyUsage) { + embed.setTitle("🎉 Fun Leaderboard"); + embed.setDescription( + "No fun command usage recorded yet.\n\nTry running `/fun dadjoke` or `/fun coinflip` to get started.", + ); await interaction.editReply({ embeds: [embed] }); return; } - // ------------------------------------------------------------ - // View: Top commands - // ------------------------------------------------------------ + // Top commands if (mode.kind === "commands") { - const totals = snapshot.totalsByCommand ?? {}; - const items = (Object.entries(totals) as Array<[string, number]>) - .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) + const items = (Object.entries(totalsByCommand) as Array<[string, number]>) + .filter(([k, n]) => isFunCommandKey(k) && typeof n === "number" && n > 0) + .map(([k, n]) => [k as FunCommandKey, n] as const) .sort((a, b) => b[1] - a[1]) .slice(0, mode.limit); - embed.setTitle("Fun Leaderboard: Top Commands"); + embed.setTitle("🎉 Fun Leaderboard: Top Commands"); - const lines = items.map(([cmd, count]) => `/fun ${cmd} — **${count}**`); + const lines = items.map(([cmd, count]) => `• \`/fun ${cmd}\` ${formatCount(count)}`); embed.setDescription(lines.length ? lines.join("\n") : "No command usage yet."); await interaction.editReply({ embeds: [embed] }); return; } - // ------------------------------------------------------------ - // View: Single user usage (includes avatar thumbnail) - // ------------------------------------------------------------ + // Single-user view if (mode.kind === "user") { - const totalsByUser = snapshot.totalsByUser ?? {}; - const total = totalsByUser[mode.userId] ?? 0; + const userId = mode.userId; + const total = totalsByUser[userId] ?? 0; + const perCmd = getPerCmd(byUserByCommand, userId); - const perCmd = - (snapshot.byUserByCommand?.[mode.userId] as Record | undefined) ?? - undefined; + const u = await safeFetchUser(interaction, userId); + const titleName = u?.username ?? `User ${userId}`; - const u = await safeFetchUser(interaction, mode.userId); - const titleName = u?.username ?? `User ${mode.userId}`; - const avatarUrl = u?.displayAvatarURL() ?? null; + embed.setTitle(`👤 Fun Usage: ${titleName}`); - embed.setTitle(`Fun Usage: ${titleName}`); - if (avatarUrl) embed.setThumbnail(avatarUrl); + // Per your request: no avatar thumbnail in this view + // (keep it clean and consistent) - const lines = formatUserUsageLines(perCmd); + const lines: string[] = []; + lines.push(`Total: **${formatCount(total)}**`); - // Flat list, no ranks/medals - embed.setDescription( - [ - `Total uses: **${total}**`, - "", - ...(lines.length ? lines : ["No per-command usage recorded yet."]), - ].join("\n"), - ); + const breakdownLines = formatUserBreakdownLines(perCmd, 25); + if (breakdownLines.length) { + lines.push(""); + lines.push(...breakdownLines); + } else { + lines.push(""); + lines.push("No per-command breakdown yet. Run a few `/fun` commands!"); + } + + embed.setDescription(lines.join("\n")); await interaction.editReply({ embeds: [embed] }); return; } - // ------------------------------------------------------------ - // View: Top users (no avatars, no ranking semantics) - // ------------------------------------------------------------ - const totalsByUser = snapshot.totalsByUser ?? {}; + // Top users (default) const userItems = Object.entries(totalsByUser) .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) - .sort((a, b) => b[1] - a[1]) + .sort((a, b) => (b[1] as number) - (a[1] as number)) .slice(0, mode.limit); - embed.setTitle("Fun Leaderboard: Top Users"); + embed.setTitle("🏆 Fun Leaderboard: Top Users"); + + const lines = userItems.map(([userId, total]) => { + const perCmd = getPerCmd(byUserByCommand, userId); + const top = formatTopCommandsInline(perCmd, 3); + + // No numbers, no "—" + // Show exactly what you wanted: user X ran command Y this many times + const detail = top ? ` (${top})` : ""; + return `• <@${userId}> ${formatCount(total)}${detail}`; + }); - // Flat list, no 1/2/3 medals or ranks - const lines = userItems.map(([userId, total]) => `<@${userId}> — **${total}**`); embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); await interaction.editReply({ embeds: [embed] }); -} +} \ No newline at end of file From 66602f8c113fb34f52e1531a1e07bc0712deac8e Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Wed, 14 Jan 2026 17:53:42 -0500 Subject: [PATCH 20/25] style: auto-format with Prettier [skip-precheck] --- src/commands/fun/subcommands/leaderboard.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index 8df74a22..df02c1e4 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,10 +1,6 @@ // src/commands/fun/subcommands/leaderboard.ts -import { - EmbedBuilder, - type ChatInputCommandInteraction, - type User, -} from "discord.js"; +import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; import { getFunUsageSnapshot, type FunCommandKey, @@ -188,4 +184,4 @@ export async function run( embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); await interaction.editReply({ embeds: [embed] }); -} \ No newline at end of file +} From e04da8213c04a016cb0ac6db1efb6dac579422c7 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Wed, 14 Jan 2026 19:01:31 -0500 Subject: [PATCH 21/25] Adding this in --- README.md | 22 +- src/commands/fun/subcommands/leaderboard.ts | 10 +- src/commands/fun/subcommands/poll.ts | 219 ++++++++++++++++---- src/commands/help/helpText.ts | 60 ++++-- src/services/fun/pollStore.ts | 147 +++++++++++++ 5 files changed, 378 insertions(+), 80 deletions(-) create mode 100644 src/services/fun/pollStore.ts diff --git a/README.md b/README.md index 58b17a7c..e2650c1c 100644 --- a/README.md +++ b/README.md @@ -174,24 +174,6 @@ OmegaBot is online │ │ └── OmegaBot.yml │ └── pull_request_template.md ├── .husky -│ ├── _ -│ │ ├── .gitignore -│ │ ├── applypatch-msg -│ │ ├── commit-msg -│ │ ├── h -│ │ ├── husky.sh -│ │ ├── post-applypatch -│ │ ├── post-checkout -│ │ ├── post-commit -│ │ ├── post-merge -│ │ ├── post-rewrite -│ │ ├── pre-applypatch -│ │ ├── pre-auto-gc -│ │ ├── pre-commit -│ │ ├── pre-merge-commit -│ │ ├── pre-push -│ │ ├── pre-rebase -│ │ └── prepare-commit-msg │ ├── pre-commit │ └── pre-push ├── assets @@ -278,7 +260,8 @@ OmegaBot is online │ │ │ ├── store.ts │ │ │ └── types.ts │ │ ├── fun -│ │ │ └── funUsageStore.ts +│ │ │ ├── funUsageStore.ts +│ │ │ └── pollStore.ts │ │ ├── github │ │ │ ├── githubApi.ts │ │ │ ├── githubClient.ts @@ -326,7 +309,6 @@ OmegaBot is online ├── package.json ├── README.md ├── tsconfig.json -└── vitest.config.ts ``` diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index df02c1e4..c01dde7a 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,10 +1,14 @@ // src/commands/fun/subcommands/leaderboard.ts -import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; +import { + EmbedBuilder, + type ChatInputCommandInteraction, + type User, +} from "discord.js"; import { getFunUsageSnapshot, type FunCommandKey, -} from "../../../services/fun/funUsageStore.js"; +} from "../funUsageStore.js"; import { logger } from "../../../utils/logger.js"; export type LeaderboardMode = @@ -184,4 +188,4 @@ export async function run( embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); await interaction.editReply({ embeds: [embed] }); -} +} \ No newline at end of file diff --git a/src/commands/fun/subcommands/poll.ts b/src/commands/fun/subcommands/poll.ts index 7cb1237d..6a28c83f 100644 --- a/src/commands/fun/subcommands/poll.ts +++ b/src/commands/fun/subcommands/poll.ts @@ -1,62 +1,203 @@ // src/commands/fun/subcommands/poll.ts -import type { ChatInputCommandInteraction, Message } from "discord.js"; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + type ButtonInteraction, + type ChatInputCommandInteraction, +} from "discord.js"; +import { logger } from "../../../utils/logger.js"; +import { createPoll, recordVote, type StoredPoll } from "../../../services/fun/pollStore.js"; + +type PollOption = { + label: string; + index: number; +}; -const REACTIONS: string[] = ["1️⃣", "2️⃣", "3️⃣", "4️⃣"]; +function buildPollEmbed(poll: StoredPoll): EmbedBuilder { + const totalVotes = poll.counts.reduce((acc: number, n: number) => acc + n, 0); + + const lines = poll.options.map((opt: string, idx: number) => { + const n = poll.counts[idx] ?? 0; + return `• **${opt}**: ${n}`; + }); + + return new EmbedBuilder() + .setTitle("📊 Fun Poll") + .setDescription( + [ + `**${poll.question}**`, + "", + ...lines, + "", + `Total votes: **${totalVotes}**`, + ].join("\n"), + ) + .setFooter({ text: `Poll ID: ${poll.messageId}` }); +} -type PollArgs = { - question: string; - options: string[]; -}; +function buildPollButtons(poll: StoredPoll, opts?: { disabled?: boolean }): ActionRowBuilder[] { + const disabled = opts?.disabled ?? false; -function buildPollText(args: PollArgs): string { - const { question, options } = args; + // Discord max 5 buttons per row. We have 2–4 options: one row is fine. + const row = new ActionRowBuilder(); - const lines: string[] = []; - lines.push(`📊 **${question}**`); - lines.push(""); + for (let i = 0; i < poll.options.length; i += 1) { + const label = poll.options[i] ?? `Option ${i + 1}`; - for (let i = 0; i < options.length; i += 1) { - const emoji = REACTIONS[i] ?? "•"; - lines.push(`${emoji} ${options[i]}`); + row.addComponents( + new ButtonBuilder() + .setCustomId(`funpoll:${poll.messageId}:${i}`) + .setLabel(label.length > 80 ? `${label.slice(0, 77)}...` : label) + .setStyle(ButtonStyle.Secondary) + .setDisabled(disabled), + ); } - lines.push(""); - lines.push("_React below to vote._"); + return [row]; +} - return lines.join("\n"); +function parseOptions(interaction: ChatInputCommandInteraction): PollOption[] { + const o1 = interaction.options.getString("option1", true).trim(); + const o2 = interaction.options.getString("option2", true).trim(); + const o3 = interaction.options.getString("option3")?.trim() ?? ""; + const o4 = interaction.options.getString("option4")?.trim() ?? ""; + + const raw = [o1, o2, o3, o4].filter((s: string) => s.length > 0); + + // Deduplicate exact duplicates (case-insensitive) to avoid confusion + const seen = new Set(); + const unique: string[] = []; + for (const s of raw) { + const key = s.toLowerCase(); + if (seen.has(key)) continue; + seen.add(key); + unique.push(s); + } + + // Enforce 2–4 options (after dedupe) + if (unique.length < 2) throw new Error("Poll must have at least 2 unique options."); + if (unique.length > 4) throw new Error("Poll can have at most 4 options."); + + return unique.map((label: string, index: number) => ({ label, index })); } export async function run(interaction: ChatInputCommandInteraction): Promise { const question = interaction.options.getString("question", true).trim(); - // options are option1..option4 - const opt1 = interaction.options.getString("option1", true).trim(); - const opt2 = interaction.options.getString("option2", true).trim(); - const opt3 = interaction.options.getString("option3")?.trim() ?? ""; - const opt4 = interaction.options.getString("option4")?.trim() ?? ""; - - const options = [opt1, opt2, opt3, opt4].filter((s: string) => s.length > 0); - - if (options.length < 2 || options.length > 4) { - await interaction.editReply("Poll requires 2–4 options."); + let opts: PollOption[]; + try { + opts = parseOptions(interaction); + } catch (err) { + const msg = err instanceof Error ? err.message : "Invalid poll options."; + await interaction.editReply(msg); return; } - await interaction.editReply(buildPollText({ question, options })); + // Create a placeholder message first so we can use messageId as pollId. + const placeholder = new EmbedBuilder().setTitle("📊 Fun Poll").setDescription("Creating poll…"); + await interaction.editReply({ embeds: [placeholder] }); - // Add reactions to the sent message - const reply = (await interaction.fetchReply()) as Message; + const sent = await interaction.fetchReply(); + const messageId = sent.id; - for (let i = 0; i < options.length; i += 1) { - const emoji = REACTIONS[i]; - if (!emoji) continue; + const poll = await createPoll({ + messageId, + channelId: sent.channelId, + guildId: sent.guildId ?? null, + creatorUserId: interaction.user.id, + question, + options: opts.map((o: PollOption) => o.label), + }); + let latestPoll: StoredPoll = poll; + + await interaction.editReply({ + embeds: [buildPollEmbed(latestPoll)], + components: buildPollButtons(latestPoll), + }); + + // Collector is in-memory: if the bot restarts, existing polls won’t accept votes anymore. + const collector = sent.createMessageComponentCollector({ + time: 1000 * 60 * 60 * 24, // 24h + }); + + collector.on("collect", async (btn: ButtonInteraction) => { try { - await reply.react(emoji); - } catch { - // If bot can't add reactions, don't fail the command. - // The poll message is still usable. + if (!btn.customId.startsWith("funpoll:")) return; + + const parts = btn.customId.split(":"); + // funpoll:: + const pollId = parts[1] ?? ""; + const idxStr = parts[2] ?? ""; + const optionIndex = Number(idxStr); + + if (pollId !== messageId) return; + + if ( + !Number.isFinite(optionIndex) || + optionIndex < 0 || + optionIndex >= latestPoll.options.length + ) { + await btn.reply({ content: "Invalid poll option.", ephemeral: true }); + return; + } + + const result = await recordVote({ + messageId: pollId, + userId: btn.user.id, + optionIndex, + }); + + if (result.kind === "alreadyVoted") { + await btn.reply({ + content: `You already voted: **${ + latestPoll.options[result.previousOptionIndex] ?? "Unknown" + }**`, + ephemeral: true, + }); + return; + } + + if (result.kind === "notFound") { + await btn.reply({ content: "Poll not found (maybe it expired).", ephemeral: true }); + return; + } + + // Updated poll snapshot + latestPoll = result.poll; + + await btn.deferUpdate(); // avoid “interaction failed” and extra message spam + await interaction.editReply({ + embeds: [buildPollEmbed(latestPoll)], + components: buildPollButtons(latestPoll), + }); + } catch (err) { + logger.warn({ err }, "[fun/poll] vote handling failed"); + try { + if (!btn.replied && !btn.deferred) { + await btn.reply({ + content: "Something went wrong recording that vote.", + ephemeral: true, + }); + } + } catch { + // ignore + } } - } -} + }); + + collector.on("end", async () => { + // Disable buttons when done + try { + await interaction.editReply({ + embeds: [buildPollEmbed(latestPoll)], + components: buildPollButtons(latestPoll, { disabled: true }), + }); + } catch (err) { + logger.debug({ err }, "[fun/poll] failed to disable buttons on end"); + } + }); +} \ No newline at end of file diff --git a/src/commands/help/helpText.ts b/src/commands/help/helpText.ts index d4714402..2cb526a3 100644 --- a/src/commands/help/helpText.ts +++ b/src/commands/help/helpText.ts @@ -15,48 +15,72 @@ export function buildHelpText(args: { const lines: string[] = []; - lines.push("**🤖 OmegaBot Help**"); + lines.push("**OmegaBot Help**"); lines.push(""); - lines.push("**🚀 Start here**"); - lines.push("- Try `/help` any time you forget what I can do"); + lines.push("**Start here**"); + lines.push("- Run `/help` any time you forget what I can do"); + lines.push("- Tip: type `/fun` or `/gh` and pick a subcommand from the menu"); lines.push(""); - lines.push("**👋 Onboarding**"); - lines.push("- Welcome messages are posted when someone joins"); + lines.push("**Onboarding**"); + lines.push("- Posts a welcome message when someone joins"); lines.push(""); - lines.push("**🎉 Fun**"); - lines.push("- Use `/fun` to access lightweight, fun commands:"); - lines.push(" - `/fun chucknorris`, `/fun dadjoke`, `/fun dice` 😄"); - lines.push(" - `/fun weather`, `/fun weather7` 🌦️"); - lines.push(" - `/fun leaderboard` 🏆 see who’s having the most fun"); - lines.push("- Tip: type `/fun` and choose a subcommand from the menu"); + lines.push("**Fun**"); + lines.push("- `/fun chucknorris` — Chuck Norris facts (random, category, or search)"); + lines.push("- `/fun dadjoke` — Random dad joke (or search)"); + lines.push("- `/fun coinflip` — Heads or tails"); + lines.push("- `/fun dice` — Roll dice (custom sides/count)"); + lines.push("- `/fun poll` — Create a quick poll (2–4 options)"); + lines.push("- `/fun weather` — Today’s weather for a location"); + lines.push("- `/fun weather7` — 7-day forecast for a location"); + lines.push("- `/fun leaderboard` — Usage stats (top users, top commands, or a single user)"); + lines.push(""); + + lines.push("**GitHub**"); + lines.push("- `/gh issue` — Fetch a GitHub issue by number"); + lines.push("- `/gh issues` — List open GitHub issues"); + lines.push("- `/gh prs` — List open pull requests"); + lines.push("- `/gh status` — Show GitHub integration status (config, polling, channels)"); + lines.push("- `/pr` — Fetch a single pull request by number (legacy shortcut)"); + lines.push(""); + + lines.push("**Summary & History**"); + lines.push("- `/summary` — Summarize recent messages (local or LLM mode)"); + lines.push("- `/history` — DM recent channel history (file fallback if too long)"); + lines.push("- `/playback` — Page through recent messages using buttons"); + lines.push("- `/pagination` — Demo the reusable pagination helper"); + lines.push(""); + + lines.push("**Timezone**"); + lines.push("- `/timezone set` — Save your IANA timezone"); + lines.push("- `/timezone show` — Display your current timezone"); + lines.push("- `/timezone clear` — Remove your saved timezone"); lines.push(""); if (isAdmin) { - lines.push("**🛠️ Admin config** (Manage Server)"); + lines.push("**Admin config** (Manage Server)"); lines.push("- `/config welcome-channel set channel:#your-channel`"); lines.push("- `/config welcome-channel clear`"); lines.push(""); } else { - lines.push("**🛠️ Admin config**"); + lines.push("**Admin config**"); lines.push("- Ask a server admin to run `/config welcome-channel set` if needed"); lines.push(""); } // Optional dynamic section: list known commands if we can extract them. + // This is useful as a "sanity list" even if it doesn't include subcommands. const pretty = formatCommandList(commands, { isAdmin }); if (pretty.length) { - lines.push("**📚 Commands**"); + lines.push("**Registered commands**"); lines.push(...pretty); lines.push(""); } - lines.push( - "_Tip: If new commands don’t show up, admins may need to run the register script._", - ); + lines.push("_Tip: If new commands don’t show up, admins may need to run the register script._"); return lines.join("\n"); } @@ -103,4 +127,4 @@ function titleCase(s: string): string { .filter(Boolean) .map((w) => w[0].toUpperCase() + w.slice(1)) .join(" "); -} +} \ No newline at end of file diff --git a/src/services/fun/pollStore.ts b/src/services/fun/pollStore.ts new file mode 100644 index 00000000..2b22ab36 --- /dev/null +++ b/src/services/fun/pollStore.ts @@ -0,0 +1,147 @@ +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { logger } from "../../utils/logger.js"; + +export type StoredPoll = { + version: 1; + messageId: string; + channelId: string; + guildId: string | null; + creatorUserId: string; + createdAt: string; + updatedAt: string; + question: string; + options: string[]; + counts: number[]; // same length as options + votesByUser: Record; // userId -> optionIndex +}; + +type PollStoreFileV1 = { + version: 1; + updatedAt: string; + polls: Record; // messageId -> poll +}; + +const DATA_DIR = path.join(process.cwd(), "data"); +const STORE_PATH = path.join(DATA_DIR, "fun-polls.json"); + +function nowIso(): string { + return new Date().toISOString(); +} + +async function ensureDataDir(): Promise { + await fs.mkdir(DATA_DIR, { recursive: true }); +} + +function emptyStore(): PollStoreFileV1 { + return { + version: 1, + updatedAt: nowIso(), + polls: {}, + }; +} + +async function loadStore(): Promise { + try { + const raw = await fs.readFile(STORE_PATH, "utf8"); + const parsed = JSON.parse(raw) as Partial | null; + + if (!parsed || typeof parsed !== "object") return emptyStore(); + if (parsed.version !== 1) return emptyStore(); + + const polls = + parsed.polls && typeof parsed.polls === "object" ? parsed.polls : {}; + + return { + version: 1, + updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : nowIso(), + polls: polls as Record, + }; + } catch { + return emptyStore(); + } +} + +async function saveStore(store: PollStoreFileV1): Promise { + await ensureDataDir(); + await fs.writeFile(STORE_PATH, JSON.stringify(store, null, 2), "utf8"); +} + +export async function createPoll(args: { + messageId: string; + channelId: string; + guildId: string | null; + creatorUserId: string; + question: string; + options: string[]; +}): Promise { + const store = await loadStore(); + + const createdAt = nowIso(); + const poll: StoredPoll = { + version: 1, + messageId: args.messageId, + channelId: args.channelId, + guildId: args.guildId, + creatorUserId: args.creatorUserId, + createdAt, + updatedAt: createdAt, + question: args.question, + options: args.options, + counts: args.options.map(() => 0), + votesByUser: {}, + }; + + store.polls[poll.messageId] = poll; + store.updatedAt = nowIso(); + + try { + await saveStore(store); + } catch (err) { + logger.error({ err }, "[fun/pollStore] failed to save poll store"); + } + + return poll; +} + +export async function getPoll(messageId: string): Promise { + const store = await loadStore(); + return store.polls[messageId] ?? null; +} + +export async function recordVote(args: { + messageId: string; + userId: string; + optionIndex: number; +}): Promise< + | { kind: "ok"; poll: StoredPoll } + | { kind: "alreadyVoted"; previousOptionIndex: number } + | { kind: "notFound" } +> { + const store = await loadStore(); + const poll = store.polls[args.messageId]; + + if (!poll) return { kind: "notFound" }; + + const prev = poll.votesByUser[args.userId]; + if (typeof prev === "number" && Number.isFinite(prev)) { + return { kind: "alreadyVoted", previousOptionIndex: prev }; + } + + poll.votesByUser[args.userId] = args.optionIndex; + + const current = poll.counts[args.optionIndex] ?? 0; + poll.counts[args.optionIndex] = current + 1; + + poll.updatedAt = nowIso(); + store.updatedAt = nowIso(); + store.polls[poll.messageId] = poll; + + try { + await saveStore(store); + } catch (err) { + logger.error({ err }, "[fun/pollStore] failed to save poll store"); + } + + return { kind: "ok", poll }; +} \ No newline at end of file From 85994fc9b1746b802882fcd9715d53e9fbc2fc9f Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Wed, 14 Jan 2026 19:03:03 -0500 Subject: [PATCH 22/25] Adding more in --- src/commands/fun/subcommands/leaderboard.ts | 13 ++------- src/commands/fun/subcommands/poll.ts | 32 +++++++++++++-------- src/commands/help/helpText.ts | 14 ++++++--- src/services/fun/pollStore.ts | 5 ++-- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index c01dde7a..d7db560a 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,14 +1,7 @@ // src/commands/fun/subcommands/leaderboard.ts -import { - EmbedBuilder, - type ChatInputCommandInteraction, - type User, -} from "discord.js"; -import { - getFunUsageSnapshot, - type FunCommandKey, -} from "../funUsageStore.js"; +import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; +import { getFunUsageSnapshot, type FunCommandKey } from "../../../services/fun/funUsageStore.js"; import { logger } from "../../../utils/logger.js"; export type LeaderboardMode = @@ -188,4 +181,4 @@ export async function run( embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); await interaction.editReply({ embeds: [embed] }); -} \ No newline at end of file +} diff --git a/src/commands/fun/subcommands/poll.ts b/src/commands/fun/subcommands/poll.ts index 6a28c83f..d48eedc9 100644 --- a/src/commands/fun/subcommands/poll.ts +++ b/src/commands/fun/subcommands/poll.ts @@ -9,7 +9,11 @@ import { type ChatInputCommandInteraction, } from "discord.js"; import { logger } from "../../../utils/logger.js"; -import { createPoll, recordVote, type StoredPoll } from "../../../services/fun/pollStore.js"; +import { + createPoll, + recordVote, + type StoredPoll, +} from "../../../services/fun/pollStore.js"; type PollOption = { label: string; @@ -27,18 +31,17 @@ function buildPollEmbed(poll: StoredPoll): EmbedBuilder { return new EmbedBuilder() .setTitle("📊 Fun Poll") .setDescription( - [ - `**${poll.question}**`, - "", - ...lines, - "", - `Total votes: **${totalVotes}**`, - ].join("\n"), + [`**${poll.question}**`, "", ...lines, "", `Total votes: **${totalVotes}**`].join( + "\n", + ), ) .setFooter({ text: `Poll ID: ${poll.messageId}` }); } -function buildPollButtons(poll: StoredPoll, opts?: { disabled?: boolean }): ActionRowBuilder[] { +function buildPollButtons( + poll: StoredPoll, + opts?: { disabled?: boolean }, +): ActionRowBuilder[] { const disabled = opts?.disabled ?? false; // Discord max 5 buttons per row. We have 2–4 options: one row is fine. @@ -97,7 +100,9 @@ export async function run(interaction: ChatInputCommandInteraction): Promise w[0].toUpperCase() + w.slice(1)) .join(" "); -} \ No newline at end of file +} diff --git a/src/services/fun/pollStore.ts b/src/services/fun/pollStore.ts index 2b22ab36..1e97f6cb 100644 --- a/src/services/fun/pollStore.ts +++ b/src/services/fun/pollStore.ts @@ -49,8 +49,7 @@ async function loadStore(): Promise { if (!parsed || typeof parsed !== "object") return emptyStore(); if (parsed.version !== 1) return emptyStore(); - const polls = - parsed.polls && typeof parsed.polls === "object" ? parsed.polls : {}; + const polls = parsed.polls && typeof parsed.polls === "object" ? parsed.polls : {}; return { version: 1, @@ -144,4 +143,4 @@ export async function recordVote(args: { } return { kind: "ok", poll }; -} \ No newline at end of file +} From 2261dd0ae8c8a5a23eab78a2c8e6762c47ba2ce4 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Wed, 14 Jan 2026 19:03:09 -0500 Subject: [PATCH 23/25] style: auto-format with Prettier [skip-precheck] --- src/commands/fun/subcommands/leaderboard.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index d7db560a..df02c1e4 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,7 +1,10 @@ // src/commands/fun/subcommands/leaderboard.ts import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; -import { getFunUsageSnapshot, type FunCommandKey } from "../../../services/fun/funUsageStore.js"; +import { + getFunUsageSnapshot, + type FunCommandKey, +} from "../../../services/fun/funUsageStore.js"; import { logger } from "../../../utils/logger.js"; export type LeaderboardMode = From 5680fe84d085aca9877138a7500731372b60d296 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Wed, 14 Jan 2026 19:35:29 -0500 Subject: [PATCH 24/25] Fixing this up --- src/commands/fun/subcommands/leaderboard.ts | 190 +++++++++----------- 1 file changed, 82 insertions(+), 108 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index df02c1e4..bc5a842d 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -12,65 +12,35 @@ export type LeaderboardMode = | { kind: "commands"; limit: number } | { kind: "user"; userId: string }; -type PerUserByCommand = Record>; - -function isFunCommandKey(value: string): value is FunCommandKey { - const allowed: ReadonlySet = new Set([ - "chucknorris", - "dadjoke", - "dice", - "coinflip", - "java", - "poll", - "weather", - "weather7", - "leaderboard", - ]); - return allowed.has(value); -} +type PerCommandCounts = Record; +type PerUserByCommand = Record; -function formatCount(n: number): string { - return `${n}x`; +function toCount(n: unknown): number { + return typeof n === "number" && Number.isFinite(n) ? n : 0; } -function getPerCmd( - byUserByCommand: PerUserByCommand | undefined, - userId: string, -): Record { - const raw = (byUserByCommand ?? {})[userId] ?? {}; - const out: Partial> = {}; - - for (const [k, v] of Object.entries(raw)) { - if (!isFunCommandKey(k)) continue; - if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) continue; - out[k] = v; - } - - return out as Record; +function topCommandsFromMap( + perCmd: PerCommandCounts | undefined, + limit: number, +): Array<{ command: string; count: number }> { + if (!perCmd) return []; + + return Object.entries(perCmd) + .map(([command, raw]) => ({ command, count: toCount(raw) })) + .filter((x) => x.count > 0) + .sort((a, b) => b.count - a.count) + .slice(0, limit); } -function formatTopCommandsInline( - perCmd: Record, - maxItems: number, +function formatInlineBreakdown( + perCmd: PerCommandCounts | undefined, + limit: number, ): string { - const parts = (Object.entries(perCmd) as Array<[FunCommandKey, number]>) - .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) - .sort((a, b) => b[1] - a[1]) - .slice(0, maxItems) - .map(([cmd, n]) => `${cmd} ${formatCount(n)}`); + const top = topCommandsFromMap(perCmd, limit); + if (!top.length) return ""; - return parts.length ? parts.join(", ") : ""; -} - -function formatUserBreakdownLines( - perCmd: Record, - maxItems: number, -): string[] { - return (Object.entries(perCmd) as Array<[FunCommandKey, number]>) - .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) - .sort((a, b) => b[1] - a[1]) - .slice(0, maxItems) - .map(([cmd, n]) => `• \`/fun ${cmd}\` ${formatCount(n)}`); + // Example: "leaderboard 10x, coinflip 4x, chucknorris 2x" + return top.map((x) => `${x.command} ${x.count}x`).join(", "); } async function safeFetchUser( @@ -91,97 +61,101 @@ export async function run( ): Promise { const snapshot = await getFunUsageSnapshot(); + // Snapshot shapes can drift if a file is edited manually, so we defensive-cast. + const totalsByUser = (snapshot.totalsByUser ?? {}) as Record; + const totalsByCommand = (snapshot.totalsByCommand ?? {}) as Record; + const byUserByCommand = (snapshot.byUserByCommand ?? {}) as PerUserByCommand; + + const anyUserUsage = Object.keys(totalsByUser).length > 0; + const anyCommandUsage = (Object.values(totalsByCommand) as unknown[]).some( + (n) => toCount(n) > 0, + ); + const anyUsage = anyUserUsage || anyCommandUsage; + const embed = new EmbedBuilder().setFooter({ text: `Updated: ${snapshot.updatedAt}`, }); - const totalsByUser = snapshot.totalsByUser ?? {}; - const totalsByCommand = snapshot.totalsByCommand ?? {}; - const byUserByCommand = snapshot.byUserByCommand ?? {}; - - const anyUsage = - Object.keys(totalsByUser).length > 0 || - Object.values(totalsByCommand).some( - (n) => typeof n === "number" && Number.isFinite(n) && n > 0, - ); - if (!anyUsage) { - embed.setTitle("🎉 Fun Leaderboard"); + embed.setTitle("Fun Leaderboard"); embed.setDescription( - "No fun command usage recorded yet.\n\nTry running `/fun dadjoke` or `/fun coinflip` to get started.", + "No fun command usage recorded yet. Try `/fun dadjoke` to get started.", ); await interaction.editReply({ embeds: [embed] }); return; } - // Top commands if (mode.kind === "commands") { - const items = (Object.entries(totalsByCommand) as Array<[string, number]>) - .filter(([k, n]) => isFunCommandKey(k) && typeof n === "number" && n > 0) - .map(([k, n]) => [k as FunCommandKey, n] as const) - .sort((a, b) => b[1] - a[1]) + const items = (Object.entries(totalsByCommand) as Array<[string, unknown]>) + .map(([cmd, raw]) => ({ cmd, count: toCount(raw) })) + .filter((x) => x.count > 0) + .sort((a, b) => b.count - a.count) .slice(0, mode.limit); - embed.setTitle("🎉 Fun Leaderboard: Top Commands"); + embed.setTitle("Fun Leaderboard: Top Commands"); - const lines = items.map(([cmd, count]) => `• \`/fun ${cmd}\` ${formatCount(count)}`); - embed.setDescription(lines.length ? lines.join("\n") : "No command usage yet."); + if (!items.length) { + embed.setDescription("No command usage yet."); + await interaction.editReply({ embeds: [embed] }); + return; + } + // No numbering, no dashes + const lines = items.map((x) => `• \`/fun ${x.cmd}\` ${x.count}x`); + embed.setDescription(lines.join("\n")); await interaction.editReply({ embeds: [embed] }); return; } - // Single-user view if (mode.kind === "user") { - const userId = mode.userId; - const total = totalsByUser[userId] ?? 0; - const perCmd = getPerCmd(byUserByCommand, userId); - - const u = await safeFetchUser(interaction, userId); - const titleName = u?.username ?? `User ${userId}`; + const total = toCount(totalsByUser[mode.userId]); + const perCmd = byUserByCommand[mode.userId] ?? {}; - embed.setTitle(`👤 Fun Usage: ${titleName}`); + const u = await safeFetchUser(interaction, mode.userId); + const displayName = u?.username ?? `User ${mode.userId}`; - // Per your request: no avatar thumbnail in this view - // (keep it clean and consistent) + embed.setTitle(`Fun Usage: ${displayName}`); - const lines: string[] = []; - lines.push(`Total: **${formatCount(total)}**`); - - const breakdownLines = formatUserBreakdownLines(perCmd, 25); - if (breakdownLines.length) { - lines.push(""); - lines.push(...breakdownLines); - } else { - lines.push(""); - lines.push("No per-command breakdown yet. Run a few `/fun` commands!"); - } - - embed.setDescription(lines.join("\n")); + const top = topCommandsFromMap(perCmd, 25); + const commandLines = top.length + ? top.map((x) => `• \`/fun ${x.command}\` ${x.count}x`) + : ["• No per-command data yet."]; + embed.setDescription([`Total uses: ${total}x`, "", ...commandLines].join("\n")); await interaction.editReply({ embeds: [embed] }); return; } // Top users (default) - const userItems = Object.entries(totalsByUser) - .filter(([, n]) => typeof n === "number" && Number.isFinite(n) && n > 0) - .sort((a, b) => (b[1] as number) - (a[1] as number)) + const userItems = (Object.entries(totalsByUser) as Array<[string, unknown]>) + .map(([userId, raw]) => ({ userId, total: toCount(raw) })) + .filter((x) => x.total > 0) + .sort((a, b) => b.total - a.total) .slice(0, mode.limit); - embed.setTitle("🏆 Fun Leaderboard: Top Users"); + embed.setTitle("Fun Leaderboard: Top Users"); - const lines = userItems.map(([userId, total]) => { - const perCmd = getPerCmd(byUserByCommand, userId); - const top = formatTopCommandsInline(perCmd, 3); + if (!userItems.length) { + embed.setDescription("No user usage yet."); + await interaction.editReply({ embeds: [embed] }); + return; + } - // No numbers, no "—" - // Show exactly what you wanted: user X ran command Y this many times - const detail = top ? ` (${top})` : ""; - return `• <@${userId}> ${formatCount(total)}${detail}`; - }); + // Show “user X ran fun command Y this many times” + // No emojis, no numbering, no dash separators + const lines: string[] = []; + + for (const item of userItems) { + const perCmd = byUserByCommand[item.userId] ?? {}; + const breakdown = formatInlineBreakdown(perCmd, 3); - embed.setDescription(lines.length ? lines.join("\n") : "No user usage yet."); + if (breakdown) { + lines.push(`• <@${item.userId}> ${item.total}x (top: ${breakdown})`); + } else { + lines.push(`• <@${item.userId}> ${item.total}x`); + } + } + embed.setDescription(lines.join("\n")); await interaction.editReply({ embeds: [embed] }); } From d84e20a1a1144d42c5d7ceeed7fa9bce09cc9b10 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Wed, 14 Jan 2026 19:39:33 -0500 Subject: [PATCH 25/25] Fixing this up --- src/commands/fun/subcommands/leaderboard.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index bc5a842d..d9b394c7 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -1,10 +1,7 @@ // src/commands/fun/subcommands/leaderboard.ts import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; -import { - getFunUsageSnapshot, - type FunCommandKey, -} from "../../../services/fun/funUsageStore.js"; +import { getFunUsageSnapshot } from "../../../services/fun/funUsageStore.js"; import { logger } from "../../../utils/logger.js"; export type LeaderboardMode =