import dotenv from 'dotenv'; import { Client, GatewayIntentBits, Collection, Events, BaseInteraction, // Use a base type for now, refine later if needed SlashCommandBuilder, // Assuming commands use this } from 'discord.js'; import { Shoukaku, Connectors, NodeOption, ShoukakuOptions } from 'shoukaku'; import logger from './utils/logger'; // Assuming logger uses export default or similar import fs from 'fs'; import path from 'path'; // import { fileURLToPath } from 'url'; // Needed for __dirname in ES Modules if module is not CommonJS // Define Command structure interface Command { data: Omit; // Or appropriate type for your command data execute: (interaction: BaseInteraction, client: BotClient) => Promise; // Adjust interaction type if needed } // Define Event structure interface BotEvent { name: string; // Should match discord.js event names or custom names once?: boolean; execute: (...args: any[]) => void; // Use specific types later if possible } // Extend the discord.js Client class to include custom properties export interface BotClient extends Client { // Add export keyword commands: Collection; shoukaku: Shoukaku; } // --- Setup --- dotenv.config(); // __dirname is available in CommonJS modules, which is set in tsconfig.json // Validate essential environment variables if (!process.env.DISCORD_TOKEN) { logger.error('DISCORD_TOKEN is missing in the .env file!'); process.exit(1); } if (!process.env.LAVALINK_HOST || !process.env.LAVALINK_PORT || !process.env.LAVALINK_PASSWORD) { logger.warn('Lavalink connection details (HOST, PORT, PASSWORD) are missing or incomplete in .env. Music functionality will be limited.'); // Decide if the bot should exit or continue without music // process.exit(1); // Uncomment to exit if Lavalink is mandatory } // Create a new Discord client instance with necessary intents const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMessages, // Add if needed for prefix commands or message content GatewayIntentBits.MessageContent, // Add if needed for message content ], }) as BotClient; // Assert the type here // Define Shoukaku nodes const Nodes: NodeOption[] = [ { name: process.env.LAVALINK_NAME || 'lavalink-node-1', // Use an env var or default name url: `${process.env.LAVALINK_HOST || 'localhost'}:${process.env.LAVALINK_PORT || 2333}`, // Use || 2333 for default port number auth: process.env.LAVALINK_PASSWORD || 'youshallnotpass', // Password from your Lavalink server config secure: process.env.LAVALINK_SECURE === 'true', // Set to true if using HTTPS/WSS }, ]; // Shoukaku options const shoukakuOptions: ShoukakuOptions = { moveOnDisconnect: false, // Whether to move players to another node when a node disconnects resume: true, // Whether to resume players session after Lavalink restarts reconnectTries: 3, // Number of attempts to reconnect to Lavalink reconnectInterval: 5000, // Interval between reconnect attempts in milliseconds // Add other options as needed }; // Initialize Shoukaku client.shoukaku = new Shoukaku(new Connectors.DiscordJS(client), Nodes, shoukakuOptions); // Show the actual Lavalink connection details (without exposing the actual password) logger.info(`Lavalink connection configured to: ${process.env.LAVALINK_HOST}:${process.env.LAVALINK_PORT} (Password: ${process.env.LAVALINK_PASSWORD ? '[SET]' : '[NOT SET]'})`); // Collections for commands client.commands = new Collection(); // --- Command Loading --- const commandsPath = path.join(__dirname, 'commands'); // Read .ts files now const commandFiles = fs.readdirSync(commandsPath).filter((file: string) => file.endsWith('.ts')); const loadCommands = async () => { for (const file of commandFiles) { const filePath = path.join(commandsPath, file); try { // Use dynamic import for ES Modules/CommonJS interop const commandModule = await import(filePath); const command: Command = commandModule.default || commandModule; // Handle default exports if (command && typeof command === 'object' && 'data' in command && 'execute' in command) { client.commands.set(command.data.name, command); logger.info(`Loaded command: ${command.data.name}`); } else { logger.warn(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property or is not structured correctly.`); } } catch (error: unknown) { // Type the error as unknown const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Error loading command at ${filePath}: ${errorMessage}`, error); } } }; // --- Event Handling --- const eventsPath = path.join(__dirname, 'events'); // Read .ts files now const eventFiles = fs.readdirSync(eventsPath).filter((file: string) => file.endsWith('.ts')); const loadEvents = async () => { for (const file of eventFiles) { const filePath = path.join(eventsPath, file); try { const eventModule = await import(filePath); const event: BotEvent = eventModule.default || eventModule; // Handle default exports if (event && typeof event === 'object' && 'name' in event && 'execute' in event) { if (event.once) { client.once(event.name, (...args: any[]) => event.execute(...args, client)); // Pass client logger.info(`Loaded event ${event.name} (once)`); } else { client.on(event.name, (...args: any[]) => event.execute(...args, client)); // Pass client logger.info(`Loaded event ${event.name}`); } } else { logger.warn(`[WARNING] The event at ${filePath} is missing a required "name" or "execute" property or is not structured correctly.`); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Error loading event at ${filePath}: ${errorMessage}`, error); } } }; // --- Shoukaku Event Handling --- client.shoukaku.on('ready', (name: string) => logger.info(`Lavalink Node: ${name} is now connected`)); client.shoukaku.on('error', (name: string, error: Error) => logger.error(`Lavalink Node: ${name} emitted an error: ${error.message}`)); client.shoukaku.on('close', (name: string, code: number, reason: string | undefined) => logger.warn(`Lavalink Node: ${name} closed with code ${code}. Reason: ${reason || 'No reason'}`)); // Corrected disconnect event signature based on common usage and error TS148 client.shoukaku.on('disconnect', (name: string, count: number) => { logger.warn(`Lavalink Node: ${name} disconnected. ${count} players were disconnected from this node.`); }); // --- Main Execution --- async function main() { await loadCommands(); await loadEvents(); // Log in to Discord with your client's token try { await client.login(process.env.DISCORD_TOKEN); logger.info('Successfully logged in to Discord.'); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Failed to log in: ${errorMessage}`); process.exit(1); // Exit if login fails } } main().catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Error during bot initialization: ${errorMessage}`, error); process.exit(1); }); // Basic error handling process.on('unhandledRejection', (reason: unknown, promise: Promise) => { const reasonMessage = reason instanceof Error ? reason.message : String(reason); logger.error('Unhandled promise rejection:', { reason: reasonMessage, promise }); }); process.on('uncaughtException', (error: Error, origin: NodeJS.UncaughtExceptionOrigin) => { logger.error(`Uncaught exception: ${error.message}`, { error, origin }); // Optional: exit process on critical uncaught exceptions // process.exit(1); });