import { Client as DiscordClient, Collection, GatewayIntentBits, REST, Routes } from "discord.js"; import { join } from "node:path"; import type { Command } from "@lib/types"; import { env } from "@lib/env"; import { CommandLoader } from "@lib/loaders/CommandLoader"; import { EventLoader } from "@lib/loaders/EventLoader"; import { logger } from "@lib/logger"; export class Client extends DiscordClient { commands: Collection; lastCommandTimestamp: number | null = null; private commandLoader: CommandLoader; private eventLoader: EventLoader; constructor({ intents }: { intents: number[] }) { super({ intents }); this.commands = new Collection(); this.commandLoader = new CommandLoader(this); this.eventLoader = new EventLoader(this); } async loadCommands(reload: boolean = false) { if (reload) { this.commands.clear(); logger.info("♻️ Reloading commands..."); } const commandsPath = join(import.meta.dir, '../commands'); const result = await this.commandLoader.loadFromDirectory(commandsPath, reload); logger.info(`📦 Command loading complete: ${result.loaded} loaded, ${result.skipped} skipped, ${result.errors.length} errors`); } async loadEvents(reload: boolean = false) { if (reload) { this.removeAllListeners(); logger.info("♻️ Reloading events..."); } const eventsPath = join(import.meta.dir, '../events'); const result = await this.eventLoader.loadFromDirectory(eventsPath, reload); logger.info(`📦 Event loading complete: ${result.loaded} loaded, ${result.skipped} skipped, ${result.errors.length} errors`); } async deployCommands() { // We use env.DISCORD_BOT_TOKEN directly so this can run without client.login() const token = env.DISCORD_BOT_TOKEN; if (!token) { logger.error("DISCORD_BOT_TOKEN is not set."); return; } const rest = new REST().setToken(token); const commandsData = this.commands.map(c => c.data.toJSON()); const guildId = env.DISCORD_GUILD_ID; const clientId = env.DISCORD_CLIENT_ID; if (!clientId) { logger.error("DISCORD_CLIENT_ID is not set."); return; } try { logger.info(`Started refreshing ${commandsData.length} application (/) commands.`); let data; if (guildId) { logger.info(`Registering commands to guild: ${guildId}`); data = await rest.put( Routes.applicationGuildCommands(clientId, guildId), { body: commandsData }, ); // Clear global commands to avoid duplicates await rest.put(Routes.applicationCommands(clientId), { body: [] }); } else { logger.info('Registering commands globally'); data = await rest.put( Routes.applicationCommands(clientId), { body: commandsData }, ); } logger.success(`Successfully reloaded ${(data as any).length} application (/) commands.`); } catch (error: any) { if (error.code === 50001) { logger.warn("Missing Access: The bot is not in the guild or lacks 'applications.commands' scope."); logger.warn(" If you are testing locally, make sure you invited the bot with scope 'bot applications.commands'."); } else { logger.error(error); } } } async shutdown() { const { setShuttingDown, waitForTransactions } = await import("./shutdown"); const { closeDatabase } = await import("./DrizzleClient"); logger.info("🛑 Shutdown signal received. Starting graceful shutdown..."); setShuttingDown(true); // Wait for transactions to complete logger.info("⏳ Waiting for active transactions to complete..."); await waitForTransactions(10000); // Destroy Discord client logger.info("🔌 Disconnecting from Discord..."); this.destroy(); // Close database logger.info("🗄️ Closing database connection..."); await closeDatabase(); logger.success("👋 Graceful shutdown complete. Exiting."); process.exit(0); } } export const AuroraClient = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMembers] });