diff --git a/src/lib/BotClient.ts b/src/lib/BotClient.ts index 8b51037..9acc052 100644 --- a/src/lib/BotClient.ts +++ b/src/lib/BotClient.ts @@ -1,17 +1,21 @@ import { Client as DiscordClient, Collection, GatewayIntentBits, REST, Routes } from "discord.js"; -import { readdir } from "node:fs/promises"; import { join } from "node:path"; -import type { Command, Event } from "@lib/types"; +import type { Command } from "@lib/types"; import { env } from "@lib/env"; -import { config } from "@lib/config"; +import { CommandLoader } from "@lib/loaders/CommandLoader"; +import { EventLoader } from "@lib/loaders/EventLoader"; -class Client extends DiscordClient { +export class Client extends DiscordClient { commands: Collection; + 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) { @@ -21,7 +25,9 @@ class Client extends DiscordClient { } const commandsPath = join(import.meta.dir, '../commands'); - await this.readCommandsRecursively(commandsPath, reload); + 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) { @@ -31,109 +37,12 @@ class Client extends DiscordClient { } const eventsPath = join(import.meta.dir, '../events'); - await this.readEventsRecursively(eventsPath, reload); + const result = await this.eventLoader.loadFromDirectory(eventsPath, reload); + + console.log(`📦 Event loading complete: ${result.loaded} loaded, ${result.skipped} skipped, ${result.errors.length} errors`); } - private async readCommandsRecursively(dir: string, reload: boolean = false) { - try { - const files = await readdir(dir, { withFileTypes: true }); - for (const file of files) { - const filePath = join(dir, file.name); - - if (file.isDirectory()) { - await this.readCommandsRecursively(filePath, reload); - continue; - } - - if (!file.name.endsWith('.ts') && !file.name.endsWith('.js')) continue; - - try { - const importPath = reload ? `${filePath}?t=${Date.now()}` : filePath; - const commandModule = await import(importPath); - const commands = Object.values(commandModule); - if (commands.length === 0) { - console.warn(`⚠️ No commands found in ${file.name}`); - continue; - } - - // Extract category from parent directory name - // filePath is like /path/to/commands/admin/features.ts - // we want "admin" - const pathParts = filePath.split('/'); - const category = pathParts[pathParts.length - 2]; - - for (const command of commands) { - if (this.isValidCommand(command)) { - command.category = category; // Inject category - - const isEnabled = config.commands[command.data.name] !== false; // Default true if undefined - - if (!isEnabled) { - console.log(`🚫 Skipping disabled command: ${command.data.name}`); - continue; - } - - this.commands.set(command.data.name, command); - console.log(`✅ Loaded command: ${command.data.name}`); - } else { - console.warn(`⚠️ Skipping invalid command in ${file.name}`); - } - } - } catch (error) { - console.error(`❌ Failed to load command from ${filePath}:`, error); - } - } - } catch (error) { - console.error(`Error reading directory ${dir}:`, error); - } - } - - private async readEventsRecursively(dir: string, reload: boolean = false) { - try { - const files = await readdir(dir, { withFileTypes: true }); - - for (const file of files) { - const filePath = join(dir, file.name); - - if (file.isDirectory()) { - await this.readEventsRecursively(filePath, reload); - continue; - } - - if (!file.name.endsWith('.ts') && !file.name.endsWith('.js')) continue; - - try { - const importPath = reload ? `${filePath}?t=${Date.now()}` : filePath; - const eventModule = await import(importPath); - const event = eventModule.default; - - if (this.isValidEvent(event)) { - if (event.once) { - this.once(event.name, (...args) => event.execute(...args)); - } else { - this.on(event.name, (...args) => event.execute(...args)); - } - console.log(`✅ Loaded event: ${event.name}`); - } else { - console.warn(`⚠️ Skipping invalid event in ${file.name}`); - } - } catch (error) { - console.error(`❌ Failed to load event from ${filePath}:`, error); - } - } - } catch (error) { - console.error(`Error reading directory ${dir}:`, error); - } - } - - private isValidCommand(command: any): command is Command { - return command && typeof command === 'object' && 'data' in command && 'execute' in command; - } - - private isValidEvent(event: any): event is Event { - return event && typeof event === 'object' && 'name' in event && 'execute' in event; - } async deployCommands() { // We use env.DISCORD_BOT_TOKEN directly so this can run without client.login()