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} */ 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); });