From 31cd996fd93ed125bb2acbde492f696e2f37ca5f Mon Sep 17 00:00:00 2001 From: Nick Clark Date: Sun, 18 Jan 2026 12:43:12 -0500 Subject: [PATCH 1/2] Adding this stuff in --- docs/commands.md | 37 ---- src/commands/fun/fun.ts | 67 +----- src/commands/fun/subcommands/chucknorris.ts | 220 -------------------- src/commands/fun/subcommands/java.ts | 21 -- src/commands/help/helpText.ts | 9 - src/services/fun/funUsageStore.ts | 4 +- 6 files changed, 13 insertions(+), 345 deletions(-) delete mode 100644 src/commands/fun/subcommands/chucknorris.ts delete mode 100644 src/commands/fun/subcommands/java.ts diff --git a/docs/commands.md b/docs/commands.md index 30f4bbf9..faaf7826 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -87,43 +87,6 @@ Fetch a single pull request by number. Entertainment and engagement commands. All fun commands track usage for leaderboards. -### `/fun chucknorris` - -Get Chuck Norris facts. - -**Options:** - -- `category` (optional) — Fetch from a specific category -- `query` (optional) — Search facts by keyword -- `ephemeral` (optional) — Show result only to you - -**Modes:** - -- Random fact (no options) -- Category-based fact -- Keyword search - -**Use case:** Lighten the mood with absurd humor. - ---- - -### `/fun dadjoke` - -Get dad jokes. - -**Options:** - -- `query` (optional) — Search jokes by keyword -- `ephemeral` (optional) — Show result only to you - -**Modes:** - -- Random joke (no query) -- Search results (with query) - -**Use case:** Classic groan-worthy humor. - ---- ### `/fun joke` diff --git a/src/commands/fun/fun.ts b/src/commands/fun/fun.ts index ea450e8f..ef93ead5 100644 --- a/src/commands/fun/fun.ts +++ b/src/commands/fun/fun.ts @@ -7,9 +7,6 @@ import { } from "discord.js"; import { logger } from "../../utils/logger.js"; -import { run as runChuckNorris } from "./subcommands/chucknorris.js"; -import type { ChuckNorrisMode } from "./subcommands/chucknorris.js"; - import { run as runDice } from "./subcommands/dice.js"; import { run as runWeather } from "./subcommands/weather.js"; @@ -17,11 +14,13 @@ 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 { handleJoke, buildJokeSubcommands } from "./subcommands/joke/index.js"; + import { recordFunUsage, type FunCommandKey } from "../../services/fun/funUsageStore.js"; function parseTempUnit(raw: string | null): TempUnit { @@ -32,24 +31,8 @@ export const data = new SlashCommandBuilder() .setName("fun") .setDescription("Fun commands") - // /fun chucknorris - .addSubcommand((s) => - s - .setName("chucknorris") - .setDescription("Chuck Norris facts (random, category, or search)") - .addStringOption((o) => - o.setName("category").setDescription("Category (optional)").setRequired(false), - ) - .addStringOption((o) => - o.setName("query").setDescription("Search term (optional)").setRequired(false), - ) - .addBooleanOption((o) => - o - .setName("ephemeral") - .setDescription("Only show the result to you") - .setRequired(false), - ), - ) + // /fun joke + .addSubcommandGroup(buildJokeSubcommands) // /fun dice .addSubcommand((s) => @@ -93,19 +76,6 @@ export const data = new SlashCommandBuilder() ), ) - // /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 @@ -214,15 +184,13 @@ export const data = new SlashCommandBuilder() 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", + joke: "joke", }; return allowed[sub] ?? null; @@ -245,8 +213,8 @@ async function maybeRecordUsage( export async function execute(interaction: ChatInputCommandInteraction): Promise { const sub = interaction.options.getSubcommand(true); - // Only some subcommands have the "ephemeral" option (poll does not). - const supportsEphemeral = sub !== "poll"; + // Only some subcommands have the "ephemeral" option (poll and joke do not). + const supportsEphemeral = sub !== "poll" && sub !== "joke"; const ephemeral = supportsEphemeral ? (interaction.options.getBoolean("ephemeral") ?? false) : false; @@ -254,17 +222,8 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise await interaction.deferReply(ephemeral ? { flags: MessageFlags.Ephemeral } : undefined); try { - if (sub === "chucknorris") { - const category = interaction.options.getString("category")?.trim(); - const query = interaction.options.getString("query")?.trim(); - - const mode: ChuckNorrisMode = query - ? { kind: "search", query } - : category - ? { kind: "category", category } - : { kind: "random" }; - - await runChuckNorris(interaction, mode); + if (sub === "joke") { + await handleJoke(interaction); await maybeRecordUsage(interaction, sub); return; } @@ -281,12 +240,6 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise return; } - if (sub === "java") { - await runJava(interaction); - await maybeRecordUsage(interaction, sub); - return; - } - if (sub === "poll") { await runPoll(interaction); await maybeRecordUsage(interaction, sub); diff --git a/src/commands/fun/subcommands/chucknorris.ts b/src/commands/fun/subcommands/chucknorris.ts deleted file mode 100644 index 6b3ce038..00000000 --- a/src/commands/fun/subcommands/chucknorris.ts +++ /dev/null @@ -1,220 +0,0 @@ -// src/commands/fun/subcommands/chucknorris.ts - -import type { ChatInputCommandInteraction } from "discord.js"; -import { logger } from "../../../utils/logger.js"; - -type ChuckNorrisApiResponse = { - value: string; -}; - -type ChuckNorrisSearchResponse = { - total?: number; - result?: ChuckNorrisApiResponse[]; -}; - -export type ChuckNorrisMode = - | { kind: "random" } - | { kind: "category"; category: string } - | { kind: "search"; query: string }; - -/** - * Run handler for /fun chucknorris - * - * IMPORTANT: - * - This file is NOT a slash command by itself - * - It must NOT call reply() or deferReply() - * - The parent command (fun.ts) owns the interaction lifecycle - */ -export async function run( - interaction: ChatInputCommandInteraction, - mode: ChuckNorrisMode, -): Promise { - try { - const joke = await fetchChuckNorris(mode); - - await interaction.editReply(joke); - - logger.debug( - { userId: interaction.user.id, mode: mode.kind }, - "[fun/chucknorris] sent", - ); - } catch (err) { - const userMessage = toUserMessage(err); - - logger.error({ err, mode, userMessage }, "[fun/chucknorris] failed"); - await interaction.editReply(userMessage); - } -} - -class UserFacingError extends Error { - constructor(message: string) { - super(message); - this.name = "UserFacingError"; - } -} - -function toUserMessage(err: unknown): string { - if (err instanceof UserFacingError) return err.message; - - // Keep the fun fallback, but also provide a tiny bit of “why” for non-user errors. - if (err instanceof Error) { - // Avoid leaking raw internals; give a short category of failure. - if (err.message.toLowerCase().includes("network")) { - return "I couldn’t reach the Chuck Norris API (network issue). Try again in a bit."; - } - if (err.message.toLowerCase().includes("api error")) { - return "The Chuck Norris API returned an error. Try again later."; - } - } - - return "Chuck Norris is currently roundhouse kicking the API. Try again later."; -} - -async function fetchChuckNorris(mode: ChuckNorrisMode): Promise { - if (mode.kind === "random") - return fetchSingle("https://api.chucknorris.io/jokes/random"); - - if (mode.kind === "category") { - const category = mode.category.trim(); - if (!category) { - throw new UserFacingError("Please provide a category."); - } - - const url = `https://api.chucknorris.io/jokes/random?category=${encodeURIComponent( - category, - )}`; - - // fetchSingle throws on non-OK; we intercept 404 here to explain “why”. - try { - return await fetchSingle(url); - } catch (err) { - if (isHttpError(err, 404)) { - const categories = await safeFetchCategories(); - const hint = categories?.length - ? `Valid categories include: ${categories - .slice(0, 12) - .map((c) => `\`${c}\``) - .join(", ")}` - : "Try something like `dev`, `movie`, or `science`."; - - throw new UserFacingError( - `That category was not found: \`${category}\`. ${hint}`, - ); - } - throw err; - } - } - - // mode.kind === "search" - const query = mode.query.trim(); - if (!query) { - throw new UserFacingError("Please provide a search query."); - } - - const url = `https://api.chucknorris.io/jokes/search?query=${encodeURIComponent( - query, - )}`; - - const res = await fetch(url, { - headers: { - Accept: "application/json", - "User-Agent": "OmegaBot", - }, - }); - - if (!res.ok) { - // For search, a 400 usually means bad query; treat as user-facing - if (res.status === 400) { - throw new UserFacingError( - "That search query didn’t work. Try a simpler word or phrase.", - ); - } - throw new Error(`Chuck Norris API error: ${res.status} ${res.statusText}`); - } - - const data = (await res.json()) as ChuckNorrisSearchResponse; - const results = Array.isArray(data.result) ? data.result : []; - - if (!results.length) { - throw new UserFacingError(`No results for \`${query}\`. Try something broader.`); - } - - // Pick a random result so it feels fresh - const pick = results[Math.floor(Math.random() * results.length)]; - const joke = pick?.value; - - if (!joke || typeof joke !== "string") { - throw new Error("Chuck Norris API returned an unexpected response shape"); - } - - return joke.trim(); -} - -async function fetchSingle(url: string): Promise { - let res: Response; - - try { - res = await fetch(url, { - headers: { - Accept: "application/json", - "User-Agent": "OmegaBot", - }, - }); - } catch (err) { - throw new Error( - `Network error calling Chuck Norris API: ${(err as Error)?.message ?? String(err)}`, - ); - } - - if (!res.ok) { - throw new HttpError( - `Chuck Norris API error: ${res.status} ${res.statusText}`, - res.status, - ); - } - - const data = (await res.json()) as ChuckNorrisApiResponse; - - if (!data?.value || typeof data.value !== "string") { - throw new Error("Chuck Norris API returned an unexpected response shape"); - } - - return data.value.trim(); -} - -class HttpError extends Error { - constructor( - message: string, - public status: number, - ) { - super(message); - this.name = "HttpError"; - } -} - -function isHttpError(err: unknown, status: number): boolean { - return err instanceof HttpError && err.status === status; -} - -async function safeFetchCategories(): Promise { - try { - const res = await fetch("https://api.chucknorris.io/jokes/categories", { - headers: { - Accept: "application/json", - "User-Agent": "OmegaBot", - }, - }); - - if (!res.ok) return null; - - const data = (await res.json()) as unknown; - - if (!Array.isArray(data) || !data.every((x) => typeof x === "string")) { - return null; - } - - return data as string[]; - } catch { - return null; - } -} diff --git a/src/commands/fun/subcommands/java.ts b/src/commands/fun/subcommands/java.ts deleted file mode 100644 index 0dc4b976..00000000 --- a/src/commands/fun/subcommands/java.ts +++ /dev/null @@ -1,21 +0,0 @@ -// 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)}`); -} diff --git a/src/commands/help/helpText.ts b/src/commands/help/helpText.ts index b3a52cd4..a311e308 100644 --- a/src/commands/help/helpText.ts +++ b/src/commands/help/helpText.ts @@ -92,15 +92,6 @@ function buildFunHelp(): string { ); lines.push(""); - lines.push("**Chuck Norris**"); - lines.push("`/fun chucknorris`"); - lines.push("Examples"); - lines.push("`/fun chucknorris`"); - lines.push("`/fun chucknorris category:dev`"); - lines.push("`/fun chucknorris query:roundhouse`"); - lines.push("`/fun chucknorris query:docker ephemeral:true`"); - lines.push(""); - lines.push("**Joke (Community)**"); lines.push("`/fun joke` User-submitted jokes with 13 categories"); lines.push("Subcommands"); diff --git a/src/services/fun/funUsageStore.ts b/src/services/fun/funUsageStore.ts index 8d0e14bb..4a7bc1a0 100644 --- a/src/services/fun/funUsageStore.ts +++ b/src/services/fun/funUsageStore.ts @@ -13,7 +13,8 @@ export type FunCommandKey = | "poll" | "weather" | "weather7" - | "leaderboard"; + | "leaderboard" + | "joke"; type FunUsageStoreV1 = { version: 1; @@ -45,6 +46,7 @@ const ALL_COMMANDS: FunCommandKey[] = [ "weather", "weather7", "leaderboard", + "joke", ]; function nowIso(): string { From ac267391198ff7e913305a0832df3599d5ba096d Mon Sep 17 00:00:00 2001 From: Nick Clark Date: Sun, 18 Jan 2026 12:43:16 -0500 Subject: [PATCH 2/2] style: auto-format with Prettier [skip-precheck] --- docs/commands.md | 1 - src/commands/fun/fun.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index faaf7826..6c230c36 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -87,7 +87,6 @@ Fetch a single pull request by number. Entertainment and engagement commands. All fun commands track usage for leaderboards. - ### `/fun joke` Community-submitted jokes organized by generation. diff --git a/src/commands/fun/fun.ts b/src/commands/fun/fun.ts index ef93ead5..212045fe 100644 --- a/src/commands/fun/fun.ts +++ b/src/commands/fun/fun.ts @@ -18,7 +18,6 @@ import { run as runPoll } from "./subcommands/poll.js"; import { run as runLeaderboard } from "./subcommands/leaderboard.js"; import type { LeaderboardMode } from "./subcommands/leaderboard.js"; - import { handleJoke, buildJokeSubcommands } from "./subcommands/joke/index.js"; import { recordFunUsage, type FunCommandKey } from "../../services/fun/funUsageStore.js";