From 07ba944979e0f3ee8c0028bfc483749a4e03afbc Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 13:54:21 -0500 Subject: [PATCH 1/4] Adding in the features --- README.md | 25 +-- docs/commands.md | 26 ++- src/commands/fun/fun.ts | 170 +++++++++++++++++++- src/commands/fun/funUsageStore.ts | 125 ++++++++++++++ src/commands/fun/subcommands/java.ts | 21 +++ src/commands/fun/subcommands/leaderboard.ts | 126 +++++++++++++++ src/commands/fun/subcommands/poll.ts | 62 +++++++ src/commands/help/helpText.ts | 23 ++- 8 files changed, 548 insertions(+), 30 deletions(-) create mode 100644 src/commands/fun/funUsageStore.ts create mode 100644 src/commands/fun/subcommands/java.ts create mode 100644 src/commands/fun/subcommands/leaderboard.ts create mode 100644 src/commands/fun/subcommands/poll.ts diff --git a/README.md b/README.md index 07183036..122d1ae1 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,9 @@ OmegaBot is a modular Discord bot designed to support development projects with - Structured logging (pino) - Welcome and onboarding flows triggered on member join (`guildMemberAdd`) - Optional auto-role assignment for new members (`DISCORD_AUTO_ROLE_ID`) -- GitHub integration: issue and PR lookups plus polling-based announcements - - Check if Github is working +- GitHub integration with polling-based automation + - Health/status checks + - Issue and PR lookups - New PR announcements - Issue and PR assignee change announcements - Issue and PR closed announcements @@ -65,17 +66,21 @@ OmegaBot is a modular Discord bot designed to support development projects with ### Fun / utility commands -- chucknorris -- dadjoke -- coinflip -- dice -- weather +All fun commands are available under `/fun`: + +- `/fun chucknorris` — Chuck Norris facts (random, category, or search) +- `/fun dadjoke` — Random or searched dad jokes +- `/fun coinflip` — Heads or tails +- `/fun dice` — Custom dice rolls +- `/fun weather` — Daily weather +- `/fun weather7` — 7-day forecast +- `/fun leaderboard` — Track fun command usage and top users ## Planned features -- /docs command for documentation lookups -- GitHub issues and pull request lookups -- Pull request announcements +- `/docs` command for documentation lookups +- Expanded GitHub automation (labels, reviews, merge events) +- Enhanced fun leaderboard views and stats - Improved summary output (highlights, action items, structured sections) --- diff --git a/docs/commands.md b/docs/commands.md index 4e4654a0..2582e2dd 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -4,13 +4,6 @@ - `/ping` — Verify the bot is online and measure latency -## Summary & History - -- `/summary` — Summarize recent messages (local or LLM mode) -- `/history` — DM recent channel history (file fallback if too long) -- `/playback` — Page through recent messages using buttons -- `/pagination` — Inline paginated view of recent messages - ## GitHub - `/gh issue` — Fetch a GitHub issue by number @@ -19,6 +12,25 @@ - `/gh status` — Show GitHub integration status (configuration, polling, and channels) - `/pr` — Fetch a single pull request by number (legacy shortcut) +## Fun + +All fun commands are grouped under `/fun`. + +- `/fun chucknorris` — Chuck Norris facts +- `/fun dadjoke` — Dad jokes (random or search) +- `/fun coinflip` — Flip a coin +- `/fun dice` — Roll dice with custom sides/count +- `/fun weather` — Today’s weather +- `/fun weather7` — 7-day forecast +- `/fun leaderboard` — Fun command usage leaderboard + +## Summary & History + +- `/summary` — Summarize recent messages (local or LLM mode) +- `/history` — DM recent channel history (file fallback if too long) +- `/playback` — Page through recent messages using buttons +- `/pagination` — Inline paginated view of recent messages + ## Timezone - `/timezone set` — Save your IANA timezone diff --git a/src/commands/fun/fun.ts b/src/commands/fun/fun.ts index 9cfa976d..9a2672a8 100644 --- a/src/commands/fun/fun.ts +++ b/src/commands/fun/fun.ts @@ -18,6 +18,15 @@ import { run as runDice } from "./subcommands/dice.js"; import { run as runWeather } from "./subcommands/weather.js"; import type { TempUnit, WeatherMode } from "../../services/weather/types.js"; +import { run as runCoinflip } from "./subcommands/coinflip.js"; +import { run as runPoll } from "./subcommands/poll.js"; +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 "../../services/fun/funUsageStore.js"; + function parseTempUnit(raw: string | null): TempUnit { return raw?.toLowerCase() === "c" ? "c" : "f"; } @@ -90,6 +99,54 @@ export const data = new SlashCommandBuilder() ), ) + // /fun coinflip + .addSubcommand((s) => + s + .setName("coinflip") + .setDescription("Flip a coin") + .addBooleanOption((o) => + o + .setName("ephemeral") + .setDescription("Only show the result to you") + .setRequired(false), + ), + ) + + // /fun java + .addSubcommand((s) => + s + .setName("java") + .setDescription("Random Java jokes ☕") + .addBooleanOption((o) => + o + .setName("ephemeral") + .setDescription("Only show the result to you") + .setRequired(false), + ), + ) + + // /fun poll (2–4 options) + .addSubcommand((s) => + s + .setName("poll") + .setDescription("Create a quick poll (2–4 options)") + .addStringOption((o) => + o.setName("question").setDescription("Poll question").setRequired(true), + ) + .addStringOption((o) => + o.setName("option1").setDescription("Option 1").setRequired(true), + ) + .addStringOption((o) => + o.setName("option2").setDescription("Option 2").setRequired(true), + ) + .addStringOption((o) => + o.setName("option3").setDescription("Option 3 (optional)").setRequired(false), + ) + .addStringOption((o) => + o.setName("option4").setDescription("Option 4 (optional)").setRequired(false), + ), + ) + // /fun weather (daily) .addSubcommand((s) => s @@ -140,10 +197,60 @@ export const data = new SlashCommandBuilder() .setDescription("Only show the result to you") .setRequired(false), ), + ) + + // /fun leaderboard + .addSubcommand((s) => + s + .setName("leaderboard") + .setDescription("Show fun command leaderboard") + .addStringOption((o) => + o + .setName("view") + .setDescription("What leaderboard view to show") + .setRequired(false) + .addChoices( + { name: "Top users", value: "users" }, + { name: "Top commands", value: "commands" }, + { name: "Single user", value: "user" }, + ), + ) + .addUserOption((o) => + o + .setName("user") + .setDescription("User to inspect (used with view: Single user)") + .setRequired(false), + ) + .addIntegerOption((o) => + o + .setName("limit") + .setDescription("How many results to show (default 10, max 25)") + .setMinValue(1) + .setMaxValue(25) + .setRequired(false), + ), ); +function funKeyFromSub(sub: string): FunCommandKey | null { + const allowed: Record = { + chucknorris: "chucknorris", + dadjoke: "dadjoke", + dice: "dice", + coinflip: "coinflip", + java: "java", + poll: "poll", + weather: "weather", + weather7: "weather7", + leaderboard: "leaderboard", + }; + + return allowed[sub] ?? null; +} + export async function execute(interaction: ChatInputCommandInteraction): Promise { const sub = interaction.options.getSubcommand(true); + + // NOTE: most subcommands accept "ephemeral". Poll does not. const ephemeral = interaction.options.getBoolean("ephemeral") ?? false; // Parent command owns the interaction lifecycle @@ -160,19 +267,65 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise ? { kind: "category", category } : { kind: "random" }; - return await runChuckNorris(interaction, mode); + await runChuckNorris(interaction, mode); + await recordFunUsage({ userId: interaction.user.id, command: "chucknorris" }); + return; } if (sub === "dadjoke") { - const query = interaction.options.getString("search")?.trim() ?? ""; + // NOTE: option name is "query" (not "search") + const query = interaction.options.getString("query")?.trim() ?? ""; const mode: DadJokeMode = query ? { kind: "search", query } : { kind: "random" }; - return await runDadJoke(interaction, mode); + await runDadJoke(interaction, mode); + await recordFunUsage({ userId: interaction.user.id, command: "dadjoke" }); + return; } if (sub === "dice") { - return await runDice(interaction); + await runDice(interaction); + await recordFunUsage({ userId: interaction.user.id, command: "dice" }); + return; + } + + if (sub === "coinflip") { + await runCoinflip(interaction); + await recordFunUsage({ userId: interaction.user.id, command: "coinflip" }); + return; + } + + if (sub === "java") { + await runJava(interaction); + await recordFunUsage({ userId: interaction.user.id, command: "java" }); + return; + } + + if (sub === "poll") { + await runPoll(interaction); + await recordFunUsage({ userId: interaction.user.id, command: "poll" }); + return; + } + + if (sub === "leaderboard") { + const view = interaction.options.getString("view") ?? "users"; + const limit = interaction.options.getInteger("limit") ?? 10; + + if (view === "commands") { + const mode: LeaderboardMode = { kind: "commands", limit }; + await runLeaderboard(interaction, mode); + } else if (view === "user") { + const u = interaction.options.getUser("user"); + const targetId = (u?.id ?? interaction.user.id) as string; + const mode: LeaderboardMode = { kind: "user", userId: targetId }; + await runLeaderboard(interaction, mode); + } else { + const mode: LeaderboardMode = { kind: "users", limit }; + await runLeaderboard(interaction, mode); + } + + await recordFunUsage({ userId: interaction.user.id, command: "leaderboard" }); + return; } if (sub === "weather" || sub === "weather7") { @@ -184,7 +337,12 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise ? { kind: "daily", location, unit } : { kind: "7day", location, unit }; - return await runWeather(interaction, mode); + await runWeather(interaction, mode); + await recordFunUsage({ + userId: interaction.user.id, + command: sub === "weather" ? "weather" : "weather7", + }); + return; } await interaction.editReply("Unknown subcommand."); @@ -192,4 +350,4 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise logger.error({ err, sub }, "[fun] subcommand failed"); await interaction.editReply("Something went wrong. Try again in a bit."); } -} +} \ No newline at end of file diff --git a/src/commands/fun/funUsageStore.ts b/src/commands/fun/funUsageStore.ts new file mode 100644 index 00000000..ed546714 --- /dev/null +++ b/src/commands/fun/funUsageStore.ts @@ -0,0 +1,125 @@ +// 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()); +} \ No newline at end of file diff --git a/src/commands/fun/subcommands/java.ts b/src/commands/fun/subcommands/java.ts new file mode 100644 index 00000000..028cdb66 --- /dev/null +++ b/src/commands/fun/subcommands/java.ts @@ -0,0 +1,21 @@ +// src/commands/fun/subcommands/java.ts + +import type { ChatInputCommandInteraction } from "discord.js"; + +const JOKES: string[] = [ + "Why do Java developers wear glasses? Because they don’t C#.", + "A SQL query walks into a bar, walks up to two tables and asks: 'Can I join you?'", + "I told my Java program a joke… it didn’t laugh. It just threw an exception.", + "Java: Write once, debug everywhere.", + "Why was the Java developer broke? Because they used up all their cache.", + "My Java app is really secure. It has *private* everything.", + "How many Java devs does it take to change a light bulb? None, it’s a hardware problem.", +]; + +function pick(arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)] as T; +} + +export async function run(interaction: ChatInputCommandInteraction): Promise { + await interaction.editReply(`☕ ${pick(JOKES)}`); +} \ No newline at end of file diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts new file mode 100644 index 00000000..05db9bfb --- /dev/null +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -0,0 +1,126 @@ +// src/commands/fun/subcommands/leaderboard.ts + +import type { ChatInputCommandInteraction, User } from "discord.js"; +import { EmbedBuilder } from "discord.js"; +import { getFunUsageSnapshot } from "../funUsageStore.js"; + +export type LeaderboardMode = + | { kind: "users"; limit: number } + | { kind: "commands"; limit: number } + | { kind: "user"; userId: string }; + +type RankedItem = { key: string; count: 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); +} + +function medals(idx: number): string { + if (idx === 0) return "🥇"; + if (idx === 1) return "🥈"; + if (idx === 2) return "🥉"; + return `${idx + 1}.`; +} + +async function safeFetchUser( + interaction: ChatInputCommandInteraction, + userId: string, +): Promise { + try { + return await interaction.client.users.fetch(userId); + } catch { + return null; + } +} + +function prettyCommandName(cmd: string): string { + // Keep it simple and readable in output + return cmd.startsWith("fun ") ? cmd : `fun ${cmd}`; +} + +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; + } + + if (mode.kind === "commands") { + const top = rank(snapshot.totalsByCommand, mode.limit); + + 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 embed = new EmbedBuilder() + .setTitle("🎉 Fun Leaderboard: Top Commands") + .setDescription(lines.join("\n")) + .setFooter({ text: `Updated: ${snapshot.updatedAt}` }); + + await interaction.editReply({ embeds: [embed] }); + return; + } + + if (mode.kind === "users") { + const top = rank(snapshot.totalsByUser, mode.limit); + + const lines: string[] = []; + for (let i = 0; i < top.length; i += 1) { + const item = top[i]!; + lines.push(`${medals(i)} <@${item.key}> — **${item.count}**`); + } + + const embed = new EmbedBuilder() + .setTitle("🏆 Fun Leaderboard: Top Users") + .setDescription(lines.join("\n")) + .setFooter({ text: `Updated: ${snapshot.updatedAt}` }); + + await interaction.editReply({ embeds: [embed] }); + return; + } + + // mode.kind === "user" + const userId = mode.userId; + const user = await safeFetchUser(interaction, userId); + + const perCmd = snapshot.byUserByCommand[userId] ?? {}; + const sorted = rank(perCmd, 25); + + 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}**`); + } + } + + 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}` }); + + if (user) { + embed.setThumbnail(user.displayAvatarURL({ size: 128 })); + } + + 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 new file mode 100644 index 00000000..4ca4deb0 --- /dev/null +++ b/src/commands/fun/subcommands/poll.ts @@ -0,0 +1,62 @@ +// src/commands/fun/subcommands/poll.ts + +import type { ChatInputCommandInteraction, Message } from "discord.js"; + +const REACTIONS: string[] = ["1️⃣", "2️⃣", "3️⃣", "4️⃣"]; + +type PollArgs = { + question: string; + options: string[]; +}; + +function buildPollText(args: PollArgs): string { + const { question, options } = args; + + const lines: string[] = []; + lines.push(`📊 **${question}**`); + lines.push(""); + + for (let i = 0; i < options.length; i += 1) { + const emoji = REACTIONS[i] ?? "•"; + lines.push(`${emoji} ${options[i]}`); + } + + lines.push(""); + lines.push("_React below to vote._"); + + return lines.join("\n"); +} + +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."); + return; + } + + await interaction.editReply(buildPollText({ question, options })); + + // Add reactions to the sent message + const reply = (await interaction.fetchReply()) as Message; + + for (let i = 0; i < options.length; i += 1) { + const emoji = REACTIONS[i]; + if (!emoji) continue; + + try { + await reply.react(emoji); + } catch { + // If bot can't add reactions, don't fail the command. + // The poll message is still usable. + } + } +} \ No newline at end of file diff --git a/src/commands/help/helpText.ts b/src/commands/help/helpText.ts index 5708d407..da0d0036 100644 --- a/src/commands/help/helpText.ts +++ b/src/commands/help/helpText.ts @@ -15,23 +15,32 @@ export function buildHelpText(args: { const lines: string[] = []; - lines.push("**OmegaBot Help**"); + lines.push("**🤖 OmegaBot Help**"); lines.push(""); - lines.push("**Start here**"); + + lines.push("**🚀 Start here**"); lines.push("- Try `/help` any time you forget what I can do"); lines.push(""); - lines.push("**Onboarding**"); + lines.push("**👋 Onboarding**"); lines.push("- Welcome messages are posted 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(""); + 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(""); } @@ -40,7 +49,7 @@ export function buildHelpText(args: { const pretty = formatCommandList(commands, { isAdmin }); if (pretty.length) { - lines.push("**Commands**"); + lines.push("**📚 Commands**"); lines.push(...pretty); lines.push(""); } @@ -94,4 +103,4 @@ function titleCase(s: string): string { .filter(Boolean) .map((w) => w[0].toUpperCase() + w.slice(1)) .join(" "); -} +} \ No newline at end of file From 92f2deb87ce0dbc3843e33152244265d12bfcf89 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 13:54:59 -0500 Subject: [PATCH 2/4] Adding in files --- src/commands/fun/fun.ts | 2 +- src/commands/fun/funUsageStore.ts | 7 +++++-- src/commands/fun/subcommands/java.ts | 2 +- src/commands/fun/subcommands/leaderboard.ts | 6 ++++-- src/commands/fun/subcommands/poll.ts | 2 +- src/commands/help/helpText.ts | 2 +- 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/commands/fun/fun.ts b/src/commands/fun/fun.ts index 9a2672a8..fdcbbd1b 100644 --- a/src/commands/fun/fun.ts +++ b/src/commands/fun/fun.ts @@ -350,4 +350,4 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise logger.error({ err, sub }, "[fun] subcommand failed"); await interaction.editReply("Something went wrong. Try again in a bit."); } -} \ No newline at end of file +} diff --git a/src/commands/fun/funUsageStore.ts b/src/commands/fun/funUsageStore.ts index ed546714..a93f7630 100644 --- a/src/commands/fun/funUsageStore.ts +++ b/src/commands/fun/funUsageStore.ts @@ -56,7 +56,10 @@ async function loadState(): Promise { return { version: 1, - updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : new Date().toISOString(), + updatedAt: + typeof parsed.updatedAt === "string" + ? parsed.updatedAt + : new Date().toISOString(), totalsByCommand: parsed.totalsByCommand && typeof parsed.totalsByCommand === "object" ? parsed.totalsByCommand @@ -122,4 +125,4 @@ export async function getFunUsageSnapshot(): Promise { export async function resetFunUsage(): Promise { await saveState(emptyState()); -} \ No newline at end of file +} diff --git a/src/commands/fun/subcommands/java.ts b/src/commands/fun/subcommands/java.ts index 028cdb66..0dc4b976 100644 --- a/src/commands/fun/subcommands/java.ts +++ b/src/commands/fun/subcommands/java.ts @@ -18,4 +18,4 @@ function pick(arr: T[]): T { export async function run(interaction: ChatInputCommandInteraction): Promise { await interaction.editReply(`☕ ${pick(JOKES)}`); -} \ No newline at end of file +} diff --git a/src/commands/fun/subcommands/leaderboard.ts b/src/commands/fun/subcommands/leaderboard.ts index 05db9bfb..e3ec1133 100644 --- a/src/commands/fun/subcommands/leaderboard.ts +++ b/src/commands/fun/subcommands/leaderboard.ts @@ -54,7 +54,9 @@ export async function run( if (totalEvents === 0) { await interaction.editReply( - [`Updated: ${snapshot.updatedAt}`, "", "No fun command usage recorded yet."].join("\n"), + [`Updated: ${snapshot.updatedAt}`, "", "No fun command usage recorded yet."].join( + "\n", + ), ); return; } @@ -123,4 +125,4 @@ export async function run( } 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 4ca4deb0..7cb1237d 100644 --- a/src/commands/fun/subcommands/poll.ts +++ b/src/commands/fun/subcommands/poll.ts @@ -59,4 +59,4 @@ export async function run(interaction: ChatInputCommandInteraction): Promise w[0].toUpperCase() + w.slice(1)) .join(" "); -} \ No newline at end of file +} From 6a144d1065d2896da45883906b71ffdc8215bc68 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 14:04:13 -0500 Subject: [PATCH 3/4] fixing it up --- src/commands/fun/fun.ts | 43 +++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/commands/fun/fun.ts b/src/commands/fun/fun.ts index fdcbbd1b..efd2ccfd 100644 --- a/src/commands/fun/fun.ts +++ b/src/commands/fun/fun.ts @@ -25,7 +25,10 @@ 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 "../../services/fun/funUsageStore.js"; +import { + recordFunUsage, + type FunCommandKey, +} from "../../services/fun/funUsageStore.js"; function parseTempUnit(raw: string | null): TempUnit { return raw?.toLowerCase() === "c" ? "c" : "f"; @@ -247,6 +250,21 @@ function funKeyFromSub(sub: string): FunCommandKey | null { return allowed[sub] ?? null; } +async function maybeRecordUsage( + interaction: ChatInputCommandInteraction, + sub: string, +): Promise { + const key = funKeyFromSub(sub); + if (!key) return; + + try { + await recordFunUsage({ userId: interaction.user.id, command: key }); + } catch (err) { + // Do not fail the command if usage tracking fails + logger.warn({ err, sub }, "[fun] failed to record usage"); + } +} + export async function execute(interaction: ChatInputCommandInteraction): Promise { const sub = interaction.options.getSubcommand(true); @@ -268,7 +286,7 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise : { kind: "random" }; await runChuckNorris(interaction, mode); - await recordFunUsage({ userId: interaction.user.id, command: "chucknorris" }); + await maybeRecordUsage(interaction, sub); return; } @@ -279,31 +297,31 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise const mode: DadJokeMode = query ? { kind: "search", query } : { kind: "random" }; await runDadJoke(interaction, mode); - await recordFunUsage({ userId: interaction.user.id, command: "dadjoke" }); + await maybeRecordUsage(interaction, sub); return; } if (sub === "dice") { await runDice(interaction); - await recordFunUsage({ userId: interaction.user.id, command: "dice" }); + await maybeRecordUsage(interaction, sub); return; } if (sub === "coinflip") { await runCoinflip(interaction); - await recordFunUsage({ userId: interaction.user.id, command: "coinflip" }); + await maybeRecordUsage(interaction, sub); return; } if (sub === "java") { await runJava(interaction); - await recordFunUsage({ userId: interaction.user.id, command: "java" }); + await maybeRecordUsage(interaction, sub); return; } if (sub === "poll") { await runPoll(interaction); - await recordFunUsage({ userId: interaction.user.id, command: "poll" }); + await maybeRecordUsage(interaction, sub); return; } @@ -316,7 +334,7 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise await runLeaderboard(interaction, mode); } else if (view === "user") { const u = interaction.options.getUser("user"); - const targetId = (u?.id ?? interaction.user.id) as string; + const targetId = u?.id ?? interaction.user.id; const mode: LeaderboardMode = { kind: "user", userId: targetId }; await runLeaderboard(interaction, mode); } else { @@ -324,7 +342,7 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise await runLeaderboard(interaction, mode); } - await recordFunUsage({ userId: interaction.user.id, command: "leaderboard" }); + await maybeRecordUsage(interaction, sub); return; } @@ -338,10 +356,7 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise : { kind: "7day", location, unit }; await runWeather(interaction, mode); - await recordFunUsage({ - userId: interaction.user.id, - command: sub === "weather" ? "weather" : "weather7", - }); + await maybeRecordUsage(interaction, sub); return; } @@ -350,4 +365,4 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise logger.error({ err, sub }, "[fun] subcommand failed"); await interaction.editReply("Something went wrong. Try again in a bit."); } -} +} \ No newline at end of file From ee516ddde72d6c58ce7df2357ebe64c642757716 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Tue, 13 Jan 2026 14:05:09 -0500 Subject: [PATCH 4/4] fixing it up --- src/commands/fun/fun.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/commands/fun/fun.ts b/src/commands/fun/fun.ts index efd2ccfd..f26ce7f7 100644 --- a/src/commands/fun/fun.ts +++ b/src/commands/fun/fun.ts @@ -25,10 +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 "../../services/fun/funUsageStore.js"; +import { recordFunUsage, type FunCommandKey } from "./funUsageStore.js"; function parseTempUnit(raw: string | null): TempUnit { return raw?.toLowerCase() === "c" ? "c" : "f"; @@ -365,4 +362,4 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise logger.error({ err, sub }, "[fun] subcommand failed"); await interaction.editReply("Something went wrong. Try again in a bit."); } -} \ No newline at end of file +}