import { Client as DiscordClient, Collection, GatewayIntentBits, REST, Routes } from "discord.js"; import { join } from "node:path"; import type { Command } from "@shared/lib/types"; import { env } from "@shared/lib/env"; import { CommandLoader } from "@lib/loaders/CommandLoader"; import { EventLoader } from "@lib/loaders/EventLoader"; 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(); console.log("♻️ Reloading commands..."); } const commandsPath = join(import.meta.dir, '../commands'); const result = await this.commandLoader.loadFromDirectory(commandsPath, reload); console.log(`📦 Command loading complete: ${result.loaded} loaded, ${result.skipped} skipped, ${result.errors.length} errors`); } async loadEvents(reload: boolean = false) { if (reload) { this.removeAllListeners(); console.log("♻️ Reloading events..."); } const eventsPath = join(import.meta.dir, '../events'); const result = await this.eventLoader.loadFromDirectory(eventsPath, reload); console.log(`📦 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) { console.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) { console.error("DISCORD_CLIENT_ID is not set."); return; } try { console.log(`Started refreshing ${commandsData.length} application (/) commands.`); let data; if (guildId) { console.log(`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 { console.log('Registering commands globally'); data = await rest.put( Routes.applicationCommands(clientId), { body: commandsData }, ); } console.log(`Successfully reloaded ${(data as any).length} application (/) commands.`); } catch (error: any) { if (error.code === 50001) { console.warn("Missing Access: The bot is not in the guild or lacks 'applications.commands' scope."); console.warn(" If you are testing locally, make sure you invited the bot with scope 'bot applications.commands'."); } else { console.error(error); } } } async shutdown() { const { setShuttingDown, waitForTransactions } = await import("./shutdown"); const { closeDatabase } = await import("@shared/db/DrizzleClient"); console.log("🛑 Shutdown signal received. Starting graceful shutdown..."); setShuttingDown(true); // Wait for transactions to complete console.log("⏳ Waiting for active transactions to complete..."); await waitForTransactions(10000); // Destroy Discord client console.log("🔌 Disconnecting from Discord..."); this.destroy(); // Close database console.log("🗄️ Closing database connection..."); await closeDatabase(); console.log("👋 Graceful shutdown complete. Exiting."); process.exit(0); } } export const AuroraClient = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMembers] });