Initial setup of a modular Discord bot featuring reaction-role management. Includes slash commands for creating, listing, removing, and clearing reaction roles with support for unicode and custom emojis. Features persistent SQLite storage, automatic command and event loading, graceful shutdown handling, and structured logging. Designed with extensibility in mind for future commands and events.
99 lines
3.1 KiB
JavaScript
99 lines
3.1 KiB
JavaScript
import {
|
|
DiscordAPIError,
|
|
MessageFlags,
|
|
} from 'discord.js';
|
|
import logger from './logger.js';
|
|
|
|
/**
|
|
* Replies or follows up with an ephemeral error message.
|
|
* @param {import('discord.js').ChatInputCommandInteraction} interaction
|
|
* @param {string} content
|
|
*/
|
|
export async function replyEphemeralError(interaction, content) {
|
|
try {
|
|
if (interaction.replied || interaction.deferred) {
|
|
await interaction.followUp({ content, flags: MessageFlags.Ephemeral });
|
|
} else {
|
|
await interaction.reply({ content, flags: MessageFlags.Ephemeral });
|
|
}
|
|
} catch (error) {
|
|
logger.error('Failed to send ephemeral error reply', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
commandName: interaction.commandName,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maps known failure modes to user-facing messages.
|
|
* @param {unknown} error
|
|
* @returns {string}
|
|
*/
|
|
export function describeCommandError(error) {
|
|
if (error instanceof Error) {
|
|
switch (error.message) {
|
|
case 'DUPLICATE_MAPPING':
|
|
return 'A reaction role for that emoji already exists on this message.';
|
|
case 'MESSAGE_NOT_FOUND':
|
|
return 'Message not found in this channel. Make sure the message ID is correct and the message is in the current channel.';
|
|
case 'INVALID_EMOJI':
|
|
return 'Invalid emoji. Use a unicode emoji (🎮) or custom emoji markup such as `<:name:id>` or `<a:name:id>`.';
|
|
case 'CUSTOM_EMOJI_UNAVAILABLE':
|
|
return 'That custom emoji is unavailable to the bot. Use an emoji from a server the bot is in.';
|
|
case 'ROLE_NOT_FOUND':
|
|
return 'That role could not be found in this server.';
|
|
case 'MAPPING_NOT_FOUND':
|
|
return 'No reaction-role mapping was found for that message and emoji.';
|
|
case 'NO_MAPPINGS_FOR_MESSAGE':
|
|
return 'No reaction-role mappings were found for that message.';
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (error.message.startsWith('PERMISSION:')) {
|
|
return error.message.slice('PERMISSION:'.length).trim();
|
|
}
|
|
}
|
|
|
|
if (error instanceof DiscordAPIError) {
|
|
logger.error('Discord API error', {
|
|
code: error.code,
|
|
message: error.message,
|
|
status: error.status,
|
|
});
|
|
|
|
if (error.code === 50013) {
|
|
return 'Missing bot permissions. I need Manage Roles, Add Reactions, View Channels, and Read Message History.';
|
|
}
|
|
|
|
if (error.code === 10008) {
|
|
return 'Message not found. It may have been deleted.';
|
|
}
|
|
|
|
if (error.code === 10014) {
|
|
return 'Unknown emoji. The emoji may be invalid or unavailable.';
|
|
}
|
|
|
|
return `Discord API error: ${error.message}`;
|
|
}
|
|
|
|
logger.error('Unexpected command error', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
|
|
return 'An unexpected error occurred. Please try again later.';
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} error
|
|
* @returns {boolean}
|
|
*/
|
|
export function isDatabaseError(error) {
|
|
return (
|
|
error instanceof Error &&
|
|
(error.message.includes('SQLITE_') ||
|
|
error.message.toLowerCase().includes('database'))
|
|
);
|
|
}
|