diff --git a/README.md b/README.md index 86fa581d..63b6c763 100644 --- a/README.md +++ b/README.md @@ -214,12 +214,47 @@ OmegaBot uses discord.js v14 which includes: ``` . +├── .github +│ ├── ISSUE_TEMPLATE +│ │ ├── bug.yml +│ │ ├── config.yml +│ │ ├── documentation.yml +│ │ ├── enhancement_refactor.yml +│ │ ├── feature_request.yml +│ │ └── question_discussion.yml +│ ├── workflows +│ │ └── 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 │ ├── banner.png │ └── omegabot.png -├── CHANGELOG.md -├── CONTRIBUTORS.md ├── data +│ ├── faqs.json +│ ├── fun-usage.json +│ ├── github-assignees.json +│ ├── guild-config.json +│ ├── last-seen.json │ └── omegabot.db ├── docs │ ├── commands.md @@ -228,60 +263,35 @@ OmegaBot uses discord.js v14 which includes: │ ├── setup-discord.md │ ├── setup-env.md │ └── transcripts.md -├── .env -├── .env.example -├── eslint.config.ts -├── .github -│ ├── ISSUE_TEMPLATE -│ │ ├── bug.yml -│ │ ├── config.yml -│ │ ├── documentation.yml -│ │ ├── enhancement_refactor.yml -│ │ ├── feature_request.yml -│ │ └── question_discussion.yml -│ ├── pull_request_template.md -│ └── workflows -│ └── OmegaBot.yml -├── .gitignore -├── .husky -│ ├── pre-commit -│ └── pre-push -├── LICENSE -├── package.json -├── package-lock.json -├── .prettierignore -├── .prettierrc.yml -├── README.md ├── scripts │ └── precheck.sh ├── src -│ ├── bot.ts │ ├── commands │ │ ├── changelog │ │ │ └── changelog.ts │ │ ├── config │ │ │ └── config.ts │ │ ├── faq -│ │ │ ├── faq.ts -│ │ │ └── subcommands -│ │ │ ├── add.ts -│ │ │ ├── get.ts -│ │ │ ├── list.ts -│ │ │ └── remove.ts +│ │ │ ├── subcommands +│ │ │ │ ├── add.ts +│ │ │ │ ├── get.ts +│ │ │ │ ├── list.ts +│ │ │ │ └── remove.ts +│ │ │ └── faq.ts │ │ ├── fun -│ │ │ ├── fun.ts -│ │ │ └── subcommands -│ │ │ ├── coinflip.ts -│ │ │ ├── dice.ts -│ │ │ ├── joke -│ │ │ │ ├── add.ts -│ │ │ │ ├── index.ts -│ │ │ │ ├── list.ts -│ │ │ │ ├── random.ts -│ │ │ │ └── remove.ts -│ │ │ ├── leaderboard.ts -│ │ │ ├── poll.ts -│ │ │ └── weather.ts +│ │ │ ├── subcommands +│ │ │ │ ├── joke +│ │ │ │ │ ├── add.ts +│ │ │ │ │ ├── index.ts +│ │ │ │ │ ├── list.ts +│ │ │ │ │ ├── random.ts +│ │ │ │ │ └── remove.ts +│ │ │ │ ├── coinflip.ts +│ │ │ │ ├── dice.ts +│ │ │ │ ├── leaderboard.ts +│ │ │ │ ├── poll.ts +│ │ │ │ └── weather.ts +│ │ │ └── fun.ts │ │ ├── general │ │ │ └── ping.ts │ │ ├── github @@ -289,8 +299,8 @@ OmegaBot uses discord.js v14 which includes: │ │ │ ├── pr.ts │ │ │ └── status.ts │ │ ├── help -│ │ │ ├── helpText.ts -│ │ │ └── help.ts +│ │ │ ├── help.ts +│ │ │ └── helpText.ts │ │ ├── history │ │ │ └── history.ts │ │ ├── pagination @@ -303,7 +313,6 @@ OmegaBot uses discord.js v14 which includes: │ │ └── timezone.ts │ ├── config │ │ └── env.ts -│ ├── registerCommands.ts │ ├── services │ │ ├── cache │ │ │ └── simpleCache.ts @@ -322,11 +331,11 @@ OmegaBot uses discord.js v14 which includes: │ │ │ ├── interactionHandler.ts │ │ │ └── safeReply.ts │ │ ├── faq +│ │ │ ├── _shared.ts │ │ │ ├── faqService.ts │ │ │ ├── permissions.ts │ │ │ ├── services.test.ts │ │ │ ├── services.ts -│ │ │ ├── _shared.ts │ │ │ ├── store.test.ts │ │ │ ├── store.test.ts.disabled │ │ │ ├── store.ts @@ -349,7 +358,6 @@ OmegaBot uses discord.js v14 which includes: │ │ │ └── types.ts │ │ ├── joke │ │ │ └── jokeStore.ts -│ │ ├── permissions │ │ ├── roles │ │ │ └── autoRoleHandler.ts │ │ ├── summary @@ -370,8 +378,23 @@ OmegaBot uses discord.js v14 which includes: │ │ └── welcome │ │ ├── welcomeHandler.ts │ │ └── welcomeMessage.ts -│ └── utils -│ └── logger.ts +│ ├── utils +│ │ ├── colors.ts +│ │ ├── interactions.ts +│ │ └── logger.ts +│ ├── bot.ts +│ └── registerCommands.ts +├── .env.example +├── .gitignore +├── .prettierignore +├── .prettierrc.yml +├── CHANGELOG.md +├── CONTRIBUTORS.md +├── eslint.config.ts +├── LICENSE +├── package-lock.json +├── package.json +├── README.md ├── tsconfig.json └── vitest.config.ts diff --git a/src/commands/config/config.ts b/src/commands/config/config.ts index f65023e9..75c689e0 100644 --- a/src/commands/config/config.ts +++ b/src/commands/config/config.ts @@ -1,3 +1,4 @@ +import { MessageFlags } from "discord.js"; // src/commands/config/config.ts import { @@ -53,7 +54,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { if (!interaction.guildId) { await interaction.reply({ content: "This command can only be used in a server (not in DMs).", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -62,7 +63,10 @@ export async function execute(interaction: ChatInputCommandInteraction) { const sub = interaction.options.getSubcommand(); if (group !== "welcome-channel") { - await interaction.reply({ content: "Unknown config group.", ephemeral: true }); + await interaction.reply({ + content: "Unknown config group.", + flags: MessageFlags.Ephemeral, + }); return; } @@ -73,7 +77,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { if (channel.type !== ChannelType.GuildText) { await interaction.reply({ content: "Please choose a normal text channel (not a thread or DM).", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -90,7 +94,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { await interaction.reply({ content: `✅ Welcome messages will be posted in <#${updated.welcomeChannelId}>.`, - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -103,7 +107,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { await interaction.reply({ content: "Welcome channel is already not set. I will use the system channel or first text channel as a fallback.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -118,7 +122,7 @@ export async function execute(interaction: ChatInputCommandInteraction) { await interaction.reply({ content: "✅ Cleared the welcome channel. I will use the system channel or first text channel as a fallback.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } } diff --git a/src/commands/fun/fun.ts b/src/commands/fun/fun.ts index 821a9b44..e214665a 100644 --- a/src/commands/fun/fun.ts +++ b/src/commands/fun/fun.ts @@ -226,7 +226,7 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise if (!interaction.replied && !interaction.deferred) { await interaction.reply({ content: "Something went wrong. Try again in a bit.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } else { await interaction.editReply("Something went wrong. Try again in a bit."); diff --git a/src/commands/fun/subcommands/joke/add.ts b/src/commands/fun/subcommands/joke/add.ts index 234bf0bd..8f6d2fc3 100644 --- a/src/commands/fun/subcommands/joke/add.ts +++ b/src/commands/fun/subcommands/joke/add.ts @@ -1,3 +1,4 @@ +import { MessageFlags } from "discord.js"; // src/commands/fun/subcommands/joke/add.ts import type { ChatInputCommandInteraction } from "discord.js"; import { addJoke, type JokeCategory } from "../../../../services/joke/jokeStore.js"; @@ -11,7 +12,7 @@ export async function handleJokeAdd( if (jokeText.length > 1000) { await interaction.reply({ content: "Joke is too long! Keep it under 1000 characters.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -27,6 +28,6 @@ export async function handleJokeAdd( "", joke.joke_text, ].join("\n"), - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } diff --git a/src/commands/fun/subcommands/joke/index.ts b/src/commands/fun/subcommands/joke/index.ts index d3a7cb1e..b11433aa 100644 --- a/src/commands/fun/subcommands/joke/index.ts +++ b/src/commands/fun/subcommands/joke/index.ts @@ -1,3 +1,4 @@ +import { MessageFlags } from "discord.js"; // src/commands/fun/subcommands/joke/index.ts import { SlashCommandSubcommandBuilder, @@ -87,7 +88,7 @@ export async function handleJoke( default: await interaction.reply({ content: "Unknown subcommand", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } } diff --git a/src/commands/fun/subcommands/joke/list.ts b/src/commands/fun/subcommands/joke/list.ts index ff9569e9..6ae4338c 100644 --- a/src/commands/fun/subcommands/joke/list.ts +++ b/src/commands/fun/subcommands/joke/list.ts @@ -1,5 +1,5 @@ // src/commands/fun/subcommands/joke/list.ts -import { EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { MessageFlags, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; import { listJokes, getJokeStats, @@ -35,7 +35,7 @@ export async function handleJokeList( content: category ? `No jokes in the ${category} category yet.` : "No jokes added yet. Be the first with `/fun joke add`!", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -63,5 +63,5 @@ export async function handleJokeList( }) .setColor(0xffd700); // Gold color for fun commands - await interaction.reply({ embeds: [embed], ephemeral: true }); + await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral }); } diff --git a/src/commands/fun/subcommands/joke/random.ts b/src/commands/fun/subcommands/joke/random.ts index d7ff1b29..47a48e3f 100644 --- a/src/commands/fun/subcommands/joke/random.ts +++ b/src/commands/fun/subcommands/joke/random.ts @@ -1,3 +1,4 @@ +import { MessageFlags } from "discord.js"; // src/commands/fun/subcommands/joke/random.ts import type { ChatInputCommandInteraction } from "discord.js"; import { getRandomJoke, type JokeCategory } from "../../../../services/joke/jokeStore.js"; @@ -30,7 +31,7 @@ export async function handleJokeRandom( content: category ? `No jokes found in the ${category} category. Add some with \`/fun joke add\`!` : "No jokes available yet. Add some with `/fun joke add`!", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } diff --git a/src/commands/fun/subcommands/joke/remove.ts b/src/commands/fun/subcommands/joke/remove.ts index 6203e132..c80c5231 100644 --- a/src/commands/fun/subcommands/joke/remove.ts +++ b/src/commands/fun/subcommands/joke/remove.ts @@ -1,3 +1,4 @@ +import { MessageFlags } from "discord.js"; // src/commands/fun/subcommands/joke/remove.ts import type { ChatInputCommandInteraction } from "discord.js"; import { removeJoke, getJoke } from "../../../../services/joke/jokeStore.js"; @@ -11,7 +12,7 @@ export async function handleJokeRemove( if (!interaction.inGuild() || !interaction.member) { await interaction.reply({ content: "This command can only be used in a server.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -23,7 +24,7 @@ export async function handleJokeRemove( if (typeof member === "string") { await interaction.reply({ content: "Could not verify your permissions.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -38,7 +39,7 @@ export async function handleJokeRemove( if (!hasModRole) { await interaction.reply({ content: "❌ Only joke moderators can remove jokes.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -48,7 +49,7 @@ export async function handleJokeRemove( if (!joke) { await interaction.reply({ content: `Joke #${jokeId} not found.`, - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -63,12 +64,12 @@ export async function handleJokeRemove( `Category: ${joke.category}`, `Text: ${joke.joke_text.substring(0, 100)}${joke.joke_text.length > 100 ? "..." : ""}`, ].join("\n"), - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } else { await interaction.reply({ content: `Failed to remove joke #${jokeId}.`, - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } } diff --git a/src/commands/fun/subcommands/poll.ts b/src/commands/fun/subcommands/poll.ts index d48eedc9..4dc07962 100644 --- a/src/commands/fun/subcommands/poll.ts +++ b/src/commands/fun/subcommands/poll.ts @@ -1,3 +1,4 @@ +import { MessageFlags } from "discord.js"; // src/commands/fun/subcommands/poll.ts import { @@ -146,7 +147,10 @@ export async function run(interaction: ChatInputCommandInteraction): Promise= latestPoll.options.length ) { - await btn.reply({ content: "Invalid poll option.", ephemeral: true }); + await btn.reply({ + content: "Invalid poll option.", + flags: MessageFlags.Ephemeral, + }); return; } @@ -161,7 +165,7 @@ export async function run(interaction: ChatInputCommandInteraction): Promise { const sub = interaction.options.getSubcommand(true); - await interaction.deferReply({ ephemeral: true }); + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); try { if (sub === "set") return await handleSet(interaction); diff --git a/src/services/discord/interactionHandler.ts b/src/services/discord/interactionHandler.ts index c3fb2f1e..e28e779f 100644 --- a/src/services/discord/interactionHandler.ts +++ b/src/services/discord/interactionHandler.ts @@ -1,3 +1,4 @@ +import { MessageFlags } from "discord.js"; // src/services/discord/interactionHandler.ts // // Central interaction router for Discord. @@ -69,7 +70,7 @@ export async function handleInteraction( await safeReply(interaction, { content: "Command not found.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); return; } @@ -98,7 +99,7 @@ export async function handleInteraction( await safeReply(interaction, { content: "Something went wrong while running this command.", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } } diff --git a/src/services/discord/safeReply.ts b/src/services/discord/safeReply.ts index c525b85e..0f58b308 100644 --- a/src/services/discord/safeReply.ts +++ b/src/services/discord/safeReply.ts @@ -1,48 +1,58 @@ // src/services/discord/safeReply.ts -// -// Safe reply helper for Discord interactions. -// -// Goal: -// - Avoid "Interaction already replied" and "Unknown interaction" pitfalls -// - Let callers write one consistent "respond" call -// -// Behavior: -// - If interaction already has a reply or was deferred, use editReply(content) -// - Otherwise use reply({ content, flags? }) -// -// Notes: -// - Discord does not allow setting Ephemeral on editReply. -// So we only apply ephemeral flags on the initial reply. - -import { +import type { + ChatInputCommandInteraction, + InteractionEditReplyOptions, + InteractionReplyOptions, MessageFlags, - type InteractionReplyOptions, - type RepliableInteraction, } from "discord.js"; +import { logger } from "../../utils/logger.js"; -export type SafeReplyOptions = { +export interface SafeReplyOptions { content: string; + flags?: MessageFlags; ephemeral?: boolean; -}; - -function toReplyOptions(opts: SafeReplyOptions): InteractionReplyOptions { - if (opts.ephemeral) { - return { content: opts.content, flags: MessageFlags.Ephemeral }; - } - return { content: opts.content }; } /** - * Safely respond to an interaction exactly once. + * Safely reply to an interaction, handling already-replied cases. */ export async function safeReply( - interaction: RepliableInteraction, - opts: SafeReplyOptions, + interaction: ChatInputCommandInteraction, + options: SafeReplyOptions, ): Promise { - if (interaction.replied || interaction.deferred) { - await interaction.editReply(opts.content); - return; + const replyOptions: InteractionReplyOptions = { + content: options.content, + }; + + // Add flags or ephemeral - prefer flags + if (options.flags !== undefined) { + replyOptions.flags = options.flags as number; // Cast to number for bitfield + } else if (options.ephemeral) { + replyOptions.ephemeral = options.ephemeral; } - await interaction.reply(toReplyOptions(opts)); + try { + if (interaction.replied || interaction.deferred) { + const editOptions: InteractionEditReplyOptions = { + content: options.content, + }; + await interaction.editReply(editOptions); + } else { + await interaction.reply(replyOptions); + } + } catch (error) { + logger.warn( + { error, commandName: interaction.commandName }, + "Failed to reply to interaction, trying followUp", + ); + // If reply fails, try followUp as last resort + try { + await interaction.followUp(replyOptions); + } catch (followUpError) { + logger.error( + { error: followUpError, commandName: interaction.commandName }, + "Failed to followUp on interaction - interaction may be expired", + ); + } + } } diff --git a/src/services/joke/jokeStore.ts b/src/services/joke/jokeStore.ts index 6cd548fc..048a2019 100644 --- a/src/services/joke/jokeStore.ts +++ b/src/services/joke/jokeStore.ts @@ -1,5 +1,6 @@ // src/services/joke/jokeStore.ts import { getDb } from "../database/db.js"; +import { logger } from "../../utils/logger.js"; export type JokeCategory = | "boomer" @@ -44,102 +45,145 @@ export interface Joke { export function addJoke(jokeText: string, category: JokeCategory, userId: string): Joke { const db = getDb(); - const result = db - .prepare( - ` - INSERT INTO jokes (joke_text, category, added_by, added_at, usage_count) - VALUES (?, ?, ?, ?, 0) - `, - ) - .run(jokeText, category, userId, Date.now()); - - return { - id: result.lastInsertRowid as number, - joke_text: jokeText, - category, - added_by: userId, - added_at: Date.now(), - usage_count: 0, - }; + try { + const result = db + .prepare( + ` + INSERT INTO jokes (joke_text, category, added_by, added_at, usage_count) + VALUES (?, ?, ?, ?, 0) + `, + ) + .run(jokeText, category, userId, Date.now()); + + const joke: Joke = { + id: result.lastInsertRowid as number, + joke_text: jokeText, + category, + added_by: userId, + added_at: Date.now(), + usage_count: 0, + }; + + logger.info({ jokeId: joke.id, category, userId }, "Joke added"); + return joke; + } catch (error) { + logger.error({ error, category, userId }, "Failed to add joke"); + throw error; + } } export function getRandomJoke(category?: JokeCategory): Joke | null { const db = getDb(); - let query = "SELECT * FROM jokes"; - const params: (string | number)[] = []; + try { + let query = "SELECT * FROM jokes"; + const params: (string | number)[] = []; - if (category && category !== "random") { - query += " WHERE category = ?"; - params.push(category); - } + if (category && category !== "random") { + query += " WHERE category = ?"; + params.push(category); + } - query += " ORDER BY RANDOM() LIMIT 1"; + query += " ORDER BY RANDOM() LIMIT 1"; - const joke = db.prepare(query).get(...params) as Joke | undefined; + const joke = db.prepare(query).get(...params) as Joke | undefined; - if (joke) { - // Increment usage count - db.prepare("UPDATE jokes SET usage_count = usage_count + 1 WHERE id = ?").run( - joke.id, - ); - } + if (joke) { + // Increment usage count + db.prepare("UPDATE jokes SET usage_count = usage_count + 1 WHERE id = ?").run( + joke.id, + ); + } - return joke || null; + return joke || null; + } catch (error) { + logger.error({ error, category }, "Failed to get random joke"); + throw error; + } } export function removeJoke(jokeId: number): boolean { const db = getDb(); - const result = db.prepare("DELETE FROM jokes WHERE id = ?").run(jokeId); - return result.changes > 0; + + try { + const result = db.prepare("DELETE FROM jokes WHERE id = ?").run(jokeId); + const success = result.changes > 0; + + if (success) { + logger.info({ jokeId }, "Joke removed"); + } else { + logger.warn({ jokeId }, "Attempted to remove non-existent joke"); + } + + return success; + } catch (error) { + logger.error({ error, jokeId }, "Failed to remove joke"); + throw error; + } } export function getJoke(jokeId: number): Joke | null { const db = getDb(); - return db.prepare("SELECT * FROM jokes WHERE id = ?").get(jokeId) as Joke | null; + + try { + return db.prepare("SELECT * FROM jokes WHERE id = ?").get(jokeId) as Joke | null; + } catch (error) { + logger.error({ error, jokeId }, "Failed to get joke"); + throw error; + } } export function listJokes(category?: JokeCategory, limit: number = 50): Joke[] { const db = getDb(); - let query = "SELECT * FROM jokes"; - const params: (string | number)[] = []; + try { + let query = "SELECT * FROM jokes"; + const params: (string | number)[] = []; - if (category && category !== "random") { - query += " WHERE category = ?"; - params.push(category); - } + if (category && category !== "random") { + query += " WHERE category = ?"; + params.push(category); + } - query += " ORDER BY added_at DESC LIMIT ?"; - params.push(limit); + query += " ORDER BY added_at DESC LIMIT ?"; + params.push(limit); - return db.prepare(query).all(...params) as Joke[]; + return db.prepare(query).all(...params) as Joke[]; + } catch (error) { + logger.error({ error, category, limit }, "Failed to list jokes"); + throw error; + } } export function getJokeStats(): { total: number; byCategory: Record } { const db = getDb(); - const total = db.prepare("SELECT COUNT(*) as count FROM jokes").get() as { - count: number; - }; - - const byCategory = db - .prepare( - ` - SELECT category, COUNT(*) as count - FROM jokes - GROUP BY category - `, - ) - .all() as { category: string; count: number }[]; - - const categoryMap: Record = {}; - byCategory.forEach((row) => { - categoryMap[row.category] = row.count; - }); - - return { - total: total.count, - byCategory: categoryMap, - }; + try { + const total = db.prepare("SELECT COUNT(*) as count FROM jokes").get() as { + count: number; + }; + + const byCategory = db + .prepare( + ` + SELECT category, COUNT(*) as count + FROM jokes + GROUP BY category + `, + ) + .all() as { category: string; count: number }[]; + + const categoryMap: Record = {}; + byCategory.forEach((row) => { + categoryMap[row.category] = row.count; + }); + + return { + total: total.count, + byCategory: categoryMap, + }; + } catch (error) { + logger.error({ error }, "Failed to get joke stats"); + throw error; + } } diff --git a/src/utils/colors.ts b/src/utils/colors.ts new file mode 100644 index 00000000..4bb8dfb3 --- /dev/null +++ b/src/utils/colors.ts @@ -0,0 +1,20 @@ +// src/utils/colors.ts +// Consistent embed colors across the bot + +export const EmbedColors = { + // Primary colors + Success: 0x00ae86, // Green - successful operations + Error: 0xff0000, // Red - errors and failures + Warning: 0xffa500, // Orange - warnings + Info: 0x3498db, // Blue - informational + + // Feature-specific colors + GitHub: 0x6e5494, // GitHub purple + Fun: 0xffd700, // Gold for fun commands + Moderation: 0xe74c3c, // Red for mod actions + + // Utility + Default: 0x00ae86, // Default if unsure +} as const; + +export type EmbedColor = (typeof EmbedColors)[keyof typeof EmbedColors]; diff --git a/src/utils/interactions.ts b/src/utils/interactions.ts new file mode 100644 index 00000000..259b59d4 --- /dev/null +++ b/src/utils/interactions.ts @@ -0,0 +1,57 @@ +// src/utils/interactions.ts +// Helper utilities for interaction handling + +import type { ChatInputCommandInteraction, EmbedBuilder } from "discord.js"; + +/** + * Helper for commands that might take a while. + * Shows "thinking..." state, then edits with result. + */ +export async function deferredReply( + interaction: ChatInputCommandInteraction, + handler: () => Promise, +): Promise { + await interaction.deferReply(); + + try { + const result = await handler(); + + if (typeof result === "string") { + await interaction.editReply({ content: result }); + } else { + await interaction.editReply(result); + } + } catch (error) { + await interaction.editReply({ + content: "❌ An error occurred while processing your request.", + }); + throw error; + } +} + +/** + * Standard error reply format + */ +export async function errorReply( + interaction: ChatInputCommandInteraction, + message: string, + suggestions?: string[], +): Promise { + const parts = [`❌ ${message}`]; + + if (suggestions && suggestions.length > 0) { + parts.push(""); + parts.push("💡 **Suggestions:**"); + suggestions.forEach((s) => parts.push(`• ${s}`)); + } + + const replied = interaction.replied || interaction.deferred; + + if (replied) { + await interaction.editReply({ content: parts.join("\n") }); + } else { + await interaction.reply({ + content: parts.join("\n"), + }); + } +}