refactor: implement command class structure for better organization and error handling; update existing commands to extend from the new base class
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
// utils/Command.js - Base command class for Discord bot commands
|
||||
// Copyright (C) 2025 Luis Bauza
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import logger from '../logger.js';
|
||||
|
||||
export default class Command {
|
||||
constructor() {
|
||||
if (this.constructor === Command) {
|
||||
throw new Error('Abstract class Command cannot be instantiated');
|
||||
}
|
||||
|
||||
this._data = this.defineCommand();
|
||||
this._logger = new logger(this.constructor.name);
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
// Abstract method - must be implemented by subclasses
|
||||
defineCommand() {
|
||||
throw new Error('Method defineCommand() must be implemented');
|
||||
}
|
||||
|
||||
// Common execute method with standardized error handling
|
||||
async execute(interaction) {
|
||||
try {
|
||||
this._logger.info(`Executing command: ${interaction.commandName}`);
|
||||
await this.run(interaction);
|
||||
} catch (error) {
|
||||
this.handleError(interaction, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Abstract method - must be implemented by subclasses
|
||||
async run(interaction) {
|
||||
throw new Error('Method run() must be implemented');
|
||||
}
|
||||
|
||||
// Standardized error handling
|
||||
handleError(interaction, error) {
|
||||
this._logger.error(`Command ${interaction.commandName} failed:`, error);
|
||||
|
||||
const response = interaction.deferred ? 'followUp' : 'reply';
|
||||
const errorMessage = this.getErrorMessage(error);
|
||||
|
||||
interaction[response]({
|
||||
content: errorMessage,
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Customizable error message handling
|
||||
getErrorMessage(error) {
|
||||
if (error.response?.status === 429) {
|
||||
return 'The service is currently busy. Please try again later.';
|
||||
}
|
||||
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
||||
return 'The request timed out. Please try again.';
|
||||
}
|
||||
return `Command failed: ${error.message}`;
|
||||
}
|
||||
}
|
||||
+30
-10
@@ -1,6 +1,19 @@
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
/**
|
||||
* Asynchronously loads all command modules from a directory
|
||||
* @async
|
||||
* @param {string} commandsPath - Path to the commands directory
|
||||
* @param {Logger} logger - Logger instance for logging loading progress
|
||||
* @returns {Promise<Array<Object>>} Array of command objects with:
|
||||
* @property {SlashCommandBuilder} data - Command definition
|
||||
* @property {Function} execute - Command execution function
|
||||
* @throws {TypeError} If commandsPath is not a string
|
||||
* @example
|
||||
* // Load all commands from './commands'
|
||||
* const commands = await loadCommands('./commands', logger);
|
||||
*/
|
||||
export async function loadCommands(commandsPath, logger) {
|
||||
const commands = [];
|
||||
const commandFiles = readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||
@@ -10,21 +23,28 @@ export async function loadCommands(commandsPath, logger) {
|
||||
try {
|
||||
const filePath = join(commandsPath, file);
|
||||
const commandModule = await import(filePath);
|
||||
const command = commandModule.default;
|
||||
const Command = commandModule.default;
|
||||
|
||||
if (!command?.data || !command?.execute) {
|
||||
logger.warn(
|
||||
`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`
|
||||
);
|
||||
const CommandBase = (await import('./Command.js')).default;
|
||||
if (Command.prototype instanceof CommandBase) {
|
||||
// New style - class extending Command
|
||||
const commandInstance = new Command();
|
||||
// Wrap run() in execute() for backward compatibility
|
||||
commandInstance.execute = commandInstance.run.bind(commandInstance);
|
||||
commands.push(commandInstance);
|
||||
logger.log(`Loaded command: ${commandInstance.data.name}`);
|
||||
} else if (Command?.data && Command?.execute) {
|
||||
// Old style - plain object (maintain backward compatibility)
|
||||
commands.push(Command);
|
||||
logger.log(`Loaded command: ${Command.data.name}`);
|
||||
} else {
|
||||
logger.warn(`[WARNING] The command at ${filePath} is missing required properties.`);
|
||||
return;
|
||||
}
|
||||
|
||||
commands.push(command);
|
||||
logger.log(`Loaded command: ${command.data.name}`);
|
||||
} catch (error) {
|
||||
logger.error(`Error loading command from ${file}:`, error);
|
||||
logger.error(`Error loading command from ${file}: ${error.stack}`);
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
return commands;
|
||||
|
||||
Reference in New Issue
Block a user