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.
136 lines
3.5 KiB
JavaScript
136 lines
3.5 KiB
JavaScript
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
Client,
|
|
Collection,
|
|
GatewayIntentBits,
|
|
Partials,
|
|
} from 'discord.js';
|
|
import env from './config/environment.js';
|
|
import { initDatabase, closeDatabase } from './database/database.js';
|
|
import { loadCommands } from './utilities/loadCommands.js';
|
|
import { loadEvents } from './utilities/loadEvents.js';
|
|
import logger from './utilities/logger.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
/** @type {boolean} */
|
|
let isShuttingDown = false;
|
|
|
|
/**
|
|
* Creates and starts the Discord client with modular command and event loaders.
|
|
*/
|
|
async function main() {
|
|
logger.info('Bot startup initiated');
|
|
|
|
initDatabase(env.databasePath);
|
|
|
|
const client = new Client({
|
|
intents: [
|
|
GatewayIntentBits.Guilds,
|
|
GatewayIntentBits.GuildMembers,
|
|
GatewayIntentBits.GuildMessageReactions,
|
|
],
|
|
partials: [
|
|
Partials.Message,
|
|
Partials.Channel,
|
|
Partials.Reaction,
|
|
Partials.User,
|
|
],
|
|
});
|
|
|
|
/** @type {Collection<string, import('./utilities/loadCommands.js').BotCommand>} */
|
|
client.commands = new Collection();
|
|
|
|
const commandsPath = path.join(__dirname, 'commands');
|
|
const eventsPath = path.join(__dirname, 'events');
|
|
|
|
const commands = await loadCommands(commandsPath);
|
|
client.commands = commands;
|
|
|
|
await loadEvents(client, eventsPath);
|
|
|
|
registerProcessHandlers(client);
|
|
|
|
try {
|
|
await client.login(env.token);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
logger.error('Discord login failed', { error: message });
|
|
|
|
if (message.toLowerCase().includes('disallowed intents')) {
|
|
console.error(
|
|
'\nLogin failed: Used disallowed intents.\n' +
|
|
'Enable "Server Members Intent" in the Discord Developer Portal:\n' +
|
|
' https://discord.com/developers/applications → Your App → Bot → Privileged Gateway Intents\n' +
|
|
'Then restart the bot with: npm start\n',
|
|
);
|
|
}
|
|
|
|
closeDatabase();
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registers graceful shutdown and process-level error handlers.
|
|
* @param {import('discord.js').Client} client
|
|
*/
|
|
function registerProcessHandlers(client) {
|
|
const shutdown = async (signal) => {
|
|
if (isShuttingDown) {
|
|
return;
|
|
}
|
|
|
|
isShuttingDown = true;
|
|
logger.info('Shutdown signal received; stopping new work', { signal });
|
|
|
|
try {
|
|
client.destroy();
|
|
logger.info('Discord client closed');
|
|
} catch (error) {
|
|
logger.error('Error while closing Discord client', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
|
|
closeDatabase();
|
|
logger.info('Bot has shut down');
|
|
process.exit(0);
|
|
};
|
|
|
|
process.on('SIGINT', () => {
|
|
void shutdown('SIGINT');
|
|
});
|
|
|
|
process.on('SIGTERM', () => {
|
|
void shutdown('SIGTERM');
|
|
});
|
|
|
|
process.on('unhandledRejection', (reason) => {
|
|
logger.error('Unhandled promise rejection', {
|
|
error: reason instanceof Error ? reason.message : String(reason),
|
|
stack: reason instanceof Error ? reason.stack : undefined,
|
|
});
|
|
});
|
|
|
|
process.on('uncaughtException', (error) => {
|
|
logger.error('Uncaught exception', {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
});
|
|
|
|
void shutdown('uncaughtException');
|
|
});
|
|
}
|
|
|
|
main().catch((error) => {
|
|
logger.error('Fatal startup error', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
closeDatabase();
|
|
process.exit(1);
|
|
});
|