forked from syntaxbullet/AuroraBot-discord
refactor(lib): simplify BotClient using loader classes
- Delegate command loading to CommandLoader - Delegate event loading to EventLoader - Remove readCommandsRecursively and readEventsRecursively methods - Remove isValidCommand and isValidEvent methods (moved to loaders) - Add summary logging with load statistics - Export Client class for better type safety - Reduce file from 188 to 97 lines (48% reduction) BREAKING CHANGE: Client class is now exported as a named export
This commit is contained in:
@@ -1,17 +1,21 @@
|
|||||||
import { Client as DiscordClient, Collection, GatewayIntentBits, REST, Routes } from "discord.js";
|
import { Client as DiscordClient, Collection, GatewayIntentBits, REST, Routes } from "discord.js";
|
||||||
import { readdir } from "node:fs/promises";
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { Command, Event } from "@lib/types";
|
import type { Command } from "@lib/types";
|
||||||
import { env } from "@lib/env";
|
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<string, Command>;
|
commands: Collection<string, Command>;
|
||||||
|
private commandLoader: CommandLoader;
|
||||||
|
private eventLoader: EventLoader;
|
||||||
|
|
||||||
constructor({ intents }: { intents: number[] }) {
|
constructor({ intents }: { intents: number[] }) {
|
||||||
super({ intents });
|
super({ intents });
|
||||||
this.commands = new Collection<string, Command>();
|
this.commands = new Collection<string, Command>();
|
||||||
|
this.commandLoader = new CommandLoader(this);
|
||||||
|
this.eventLoader = new EventLoader(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadCommands(reload: boolean = false) {
|
async loadCommands(reload: boolean = false) {
|
||||||
@@ -21,7 +25,9 @@ class Client extends DiscordClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const commandsPath = join(import.meta.dir, '../commands');
|
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) {
|
async loadEvents(reload: boolean = false) {
|
||||||
@@ -31,109 +37,12 @@ class Client extends DiscordClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const eventsPath = join(import.meta.dir, '../events');
|
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<any> {
|
|
||||||
return event && typeof event === 'object' && 'name' in event && 'execute' in event;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deployCommands() {
|
async deployCommands() {
|
||||||
// We use env.DISCORD_BOT_TOKEN directly so this can run without client.login()
|
// We use env.DISCORD_BOT_TOKEN directly so this can run without client.login()
|
||||||
|
|||||||
Reference in New Issue
Block a user