import { Interaction, EmbedBuilder, ChatInputCommandInteraction, MessageComponentInteraction, ModalSubmitInteraction, } from 'discord.js'; import { BotError, ErrorCategory } from './BotError'; import { ErrorDefs, createBotError } from './ErrorCodes'; import { logger } from '../utils/logger'; import { t, SupportedLocale, DEFAULT_LOCALE } from '../i18n'; /** Embed color per error category */ const CATEGORY_COLORS: Record = { [ErrorCategory.USER_INPUT]: 0xffa500, // Orange [ErrorCategory.PERMISSION]: 0xff6b6b, // Red [ErrorCategory.BOT_INTERNAL]: 0xff4444, // Dark Red [ErrorCategory.DISCORD_API]: 0x7289da, // Discord Blurple }; /** Emoji per error category */ const CATEGORY_EMOJI: Record = { [ErrorCategory.USER_INPUT]: '⚠️', [ErrorCategory.PERMISSION]: '🔒', [ErrorCategory.BOT_INTERNAL]: '❌', [ErrorCategory.DISCORD_API]: '🔄', }; export type RepliableInteraction = | ChatInputCommandInteraction | MessageComponentInteraction | ModalSubmitInteraction; /** * Utility class for reporting errors as user-friendly, localized Embeds. * Error codes and stack traces are logged server-side only. */ export class ErrorReporter { /** * Reports a BotError to the user via an ephemeral Embed, * and logs the full error details to the server console. */ static async report( interaction: RepliableInteraction, error: BotError, locale: SupportedLocale = DEFAULT_LOCALE, ): Promise { // ── Server-side logging (detailed) ── logger.error( `${error.code}: ${error.message}`, error.cause ? error.cause : '', ); // ── User-facing Embed (friendly, localized, no error codes) ── const embed = this.buildEmbed(error, locale); try { if (interaction.replied || interaction.deferred) { await interaction.followUp({ embeds: [embed], ephemeral: true }); } else { await interaction.reply({ embeds: [embed], ephemeral: true }); } } catch (replyError) { // If even the error reply fails, log it silently logger.error('ErrorReporter: Failed to send error response to user', replyError); } } /** * Wraps an unknown error into a structured BotError. * Automatically maps known Discord API error codes. */ static wrap(error: unknown): BotError { // Already a BotError — return as-is if (error instanceof BotError) { return error; } const err = error instanceof Error ? error : new Error(String(error)); // Map known Discord API error codes const discordCode = (error as any)?.code; if (typeof discordCode === 'number') { switch (discordCode) { case 50013: return createBotError(ErrorDefs.DISCORD_MISSING_PERMISSIONS, err); case 50001: // Missing Access return createBotError(ErrorDefs.BOT_MISSING_MANAGE_CHANNELS, err); default: break; } } // Map rate limit errors if ((error as any)?.httpStatus === 429) { return createBotError(ErrorDefs.DISCORD_RATE_LIMITED, err); } // Fallback — unknown internal error return createBotError(ErrorDefs.UNKNOWN_ERROR, err); } /** * Builds a user-friendly, localized Embed from a BotError. * Does NOT include error codes — those stay in logs only. */ private static buildEmbed(error: BotError, locale: SupportedLocale): EmbedBuilder { const emoji = CATEGORY_EMOJI[error.category]; const title = t(locale, `errorTitles.${error.category}`); const userMessage = t(locale, `${error.messageKey}.userMessage`); const resolution = t(locale, `${error.messageKey}.resolution`); const fieldName = t(locale, 'errorFields.resolution'); const embed = new EmbedBuilder() .setColor(CATEGORY_COLORS[error.category]) .setTitle(`${emoji} ${title}`) .setDescription(userMessage) .setTimestamp(); // Only add resolution field if the translation exists (not the key itself) if (resolution && resolution !== `${error.messageKey}.resolution`) { embed.addFields({ name: fieldName, value: resolution, }); } return embed; } } /** * Wraps an async handler function with standardized, localized error handling. * Any thrown BotError is reported to the user; unknown errors are wrapped first. */ export async function withErrorHandler( interaction: RepliableInteraction, fn: () => Promise, locale: SupportedLocale = DEFAULT_LOCALE, ): Promise { try { await fn(); } catch (error) { const botError = ErrorReporter.wrap(error); await ErrorReporter.report(interaction, botError, locale); } }