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:
@@ -53,18 +53,24 @@ client.on(Events.InteractionCreate, async interaction => {
|
|||||||
try {
|
try {
|
||||||
await command.execute(interaction);
|
await command.execute(interaction);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
// Log detailed error context
|
||||||
if (interaction.replied || interaction.deferred) {
|
logger.error(
|
||||||
await interaction.followUp({
|
`Command "${interaction.commandName}" failed for user ${interaction.user.tag}:`,
|
||||||
content: 'There was an error while executing this command!',
|
error,
|
||||||
ephemeral: true,
|
);
|
||||||
});
|
|
||||||
} else {
|
// Prepare error response
|
||||||
await interaction.reply({
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
content: 'There was an error while executing this command!',
|
const errorMessage = isProduction
|
||||||
ephemeral: true,
|
? `❌ Command failed. Please try again later.`
|
||||||
});
|
: `❌ Command failed: ${error.message}\n\n${error.stack}`;
|
||||||
}
|
|
||||||
|
const responseMethod = interaction.replied || interaction.deferred ? 'followUp' : 'reply';
|
||||||
|
|
||||||
|
await interaction[responseMethod]({
|
||||||
|
content: errorMessage,
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+113
-142
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import Command from '../utils/Command.js';
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
webSearch: {
|
webSearch: {
|
||||||
@@ -16,21 +17,23 @@ const config = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default class AskCommand extends Command {
|
||||||
data: new SlashCommandBuilder()
|
defineCommand() {
|
||||||
.setName('ask')
|
return new SlashCommandBuilder()
|
||||||
.setDescription('Ask a question to the AI')
|
.setName('ask')
|
||||||
.addStringOption(option =>
|
.setDescription('Ask a question to the AI')
|
||||||
option.setName('prompt').setDescription('Your question or prompt').setRequired(true),
|
.addStringOption(option =>
|
||||||
)
|
option.setName('prompt').setDescription('Your question or prompt').setRequired(true),
|
||||||
.addBooleanOption(option =>
|
)
|
||||||
option
|
.addBooleanOption(option =>
|
||||||
.setName('websearch')
|
option
|
||||||
.setDescription('Enable web search for more up-to-date information')
|
.setName('websearch')
|
||||||
.setRequired(false),
|
.setDescription('Enable web search for more up-to-date information')
|
||||||
),
|
.setRequired(false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async execute(interaction) {
|
async run(interaction) {
|
||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
|
|
||||||
const prompt = interaction.options.getString('prompt');
|
const prompt = interaction.options.getString('prompt');
|
||||||
@@ -42,142 +45,110 @@ export default {
|
|||||||
? userWebSearchOption // Use user's choice if override is allowed and option was provided
|
? userWebSearchOption // Use user's choice if override is allowed and option was provided
|
||||||
: config.webSearch.enabled; // Otherwise use default config
|
: config.webSearch.enabled; // Otherwise use default config
|
||||||
|
|
||||||
try {
|
const response = await axios.post(
|
||||||
const response = await axios.post(
|
'https://openrouter.ai/api/v1/chat/completions',
|
||||||
'https://openrouter.ai/api/v1/chat/completions',
|
{
|
||||||
{
|
model: webSearchEnabled
|
||||||
model: webSearchEnabled
|
? 'google/gemma-3-27b-it:free:online'
|
||||||
? 'google/gemma-3-27b-it:free:online'
|
: 'google/gemma-3-27b-it:free',
|
||||||
: 'google/gemma-3-27b-it:free',
|
messages: [
|
||||||
messages: [
|
{
|
||||||
{
|
role: 'system',
|
||||||
role: 'system',
|
content:
|
||||||
content:
|
'You are kekbot, a highly celebrated and knowledgeable computer scientist with decades of experience in various fields of computing. You are known for your ability to explain complex topics in a clear, concise, and insightful manner. Provide direct and to-the-point answers, avoiding unnecessary elaboration or repetition. Focus on delivering accurate and valuable information efficiently.',
|
||||||
'You are kekbot, a highly celebrated and knowledgeable computer scientist with decades of experience in various fields of computing. You are known for your ability to explain complex topics in a clear, concise, and insightful manner. Provide direct and to-the-point answers, avoiding unnecessary elaboration or repetition. Focus on delivering accurate and valuable information efficiently.',
|
|
||||||
},
|
|
||||||
{ role: 'user', content: prompt },
|
|
||||||
],
|
|
||||||
plugins: webSearchEnabled
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: 'web',
|
|
||||||
max_results: 3,
|
|
||||||
search_prompt:
|
|
||||||
`A web search was conducted on ${new Date().toISOString()}. ` +
|
|
||||||
'Incorporate the following web search results into your response. ' +
|
|
||||||
'IMPORTANT: Cite them using markdown links named using the domain of the source. ' +
|
|
||||||
'Example: [nytimes.com](https://nytimes.com/some-page).',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
|
||||||
'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
},
|
||||||
|
{ role: 'user', content: prompt },
|
||||||
|
],
|
||||||
|
plugins: webSearchEnabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: 'web',
|
||||||
|
max_results: 3,
|
||||||
|
search_prompt:
|
||||||
|
`A web search was conducted on ${new Date().toISOString()}. ` +
|
||||||
|
'Incorporate the following web search results into your response. ' +
|
||||||
|
'IMPORTANT: Cite them using markdown links named using the domain of the source. ' +
|
||||||
|
'Example: [nytimes.com](https://nytimes.com/some-page).',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
||||||
|
'HTTP-Referer': 'https://github.com/hllywluis/kekbot.js',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const aiResponse = response.data.choices[0].message.content;
|
const aiResponse = response.data.choices[0].message.content;
|
||||||
const webSearchStatus = webSearchEnabled ? '\n> *Web search enabled* 🔍\n' : '';
|
const webSearchStatus = webSearchEnabled ? '\n> *Web search enabled* 🔍\n' : '';
|
||||||
const formattedResponse = `> **Question:** ${prompt}${webSearchStatus}\n${aiResponse}`;
|
const formattedResponse = `> **Question:** ${prompt}${webSearchStatus}\n${aiResponse}`;
|
||||||
|
|
||||||
if (formattedResponse.length <= 2000) {
|
if (formattedResponse.length <= 2000) {
|
||||||
// Send as a single message with proper formatting
|
// Send as a single message with proper formatting
|
||||||
await interaction.followUp({
|
await interaction.followUp({
|
||||||
content: formattedResponse,
|
content: formattedResponse,
|
||||||
|
split: false,
|
||||||
|
allowedMentions: { parse: [] },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// For longer messages, split while preserving markdown
|
||||||
|
const maxLength = 2000;
|
||||||
|
const chunks = [];
|
||||||
|
let remainingText = formattedResponse;
|
||||||
|
let isFirstChunk = true;
|
||||||
|
|
||||||
|
while (remainingText.length > 0) {
|
||||||
|
let chunk = remainingText.slice(0, maxLength);
|
||||||
|
|
||||||
|
// If we're in the middle of a code block, find a safe split point
|
||||||
|
const lastCodeBlock = chunk.lastIndexOf('```');
|
||||||
|
if (lastCodeBlock !== -1 && !chunk.slice(lastCodeBlock).includes('\n```')) {
|
||||||
|
// Find the last newline before maxLength
|
||||||
|
const lastNewline = chunk.lastIndexOf('\n');
|
||||||
|
if (lastNewline !== -1) {
|
||||||
|
chunk = chunk.slice(0, lastNewline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For subsequent chunks, add continuation indicator
|
||||||
|
if (!isFirstChunk) {
|
||||||
|
chunk = `(continued)\n${chunk}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks.push({
|
||||||
|
content: chunk,
|
||||||
split: false,
|
split: false,
|
||||||
allowedMentions: { parse: [] },
|
allowedMentions: { parse: [] },
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// For longer messages, split while preserving markdown
|
|
||||||
const maxLength = 2000;
|
|
||||||
const chunks = [];
|
|
||||||
let remainingText = formattedResponse;
|
|
||||||
let isFirstChunk = true;
|
|
||||||
|
|
||||||
while (remainingText.length > 0) {
|
remainingText = remainingText.slice(chunk.length);
|
||||||
let chunk = remainingText.slice(0, maxLength);
|
isFirstChunk = false;
|
||||||
|
|
||||||
// If we're in the middle of a code block, find a safe split point
|
|
||||||
const lastCodeBlock = chunk.lastIndexOf('```');
|
|
||||||
if (lastCodeBlock !== -1 && !chunk.slice(lastCodeBlock).includes('\n```')) {
|
|
||||||
// Find the last newline before maxLength
|
|
||||||
const lastNewline = chunk.lastIndexOf('\n');
|
|
||||||
if (lastNewline !== -1) {
|
|
||||||
chunk = chunk.slice(0, lastNewline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For subsequent chunks, add continuation indicator
|
|
||||||
if (!isFirstChunk) {
|
|
||||||
chunk = `(continued)\n${chunk}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
chunks.push({
|
|
||||||
content: chunk,
|
|
||||||
split: false,
|
|
||||||
allowedMentions: { parse: [] },
|
|
||||||
});
|
|
||||||
|
|
||||||
remainingText = remainingText.slice(chunk.length);
|
|
||||||
isFirstChunk = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send chunks sequentially using reduce
|
|
||||||
await chunks.reduce(
|
|
||||||
(promise, chunk) =>
|
|
||||||
promise.then(async () => {
|
|
||||||
try {
|
|
||||||
await interaction.followUp(chunk);
|
|
||||||
return undefined;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending message chunk:', {
|
|
||||||
chunkLength: chunk.content.length,
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
// Attempt to send error notification
|
|
||||||
try {
|
|
||||||
await interaction.followUp({
|
|
||||||
content: 'Failed to send complete response. Please try again.',
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// If even the error notification fails, log it
|
|
||||||
console.error('Failed to send error notification:', e.message);
|
|
||||||
}
|
|
||||||
// Reject to stop processing remaining chunks
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
Promise.resolve(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Log detailed error information
|
|
||||||
console.error('Error in ask command:', {
|
|
||||||
message: error.message,
|
|
||||||
response: error.response?.data,
|
|
||||||
status: error.response?.status,
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Provide more specific error messages to users
|
|
||||||
let errorMessage = 'Sorry, there was an error processing your request.';
|
|
||||||
if (error.response?.status === 429) {
|
|
||||||
errorMessage = 'The AI service is currently busy. Please try again in a few moments.';
|
|
||||||
} else if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
|
||||||
errorMessage = 'The request timed out. Please try again.';
|
|
||||||
} else if (error.response?.status === 400) {
|
|
||||||
errorMessage = 'Invalid request. Please try rephrasing your question.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.followUp({
|
// Send chunks sequentially using reduce
|
||||||
content: errorMessage,
|
await chunks.reduce(
|
||||||
ephemeral: true,
|
(promise, chunk) =>
|
||||||
});
|
promise.then(async () => {
|
||||||
|
await interaction.followUp(chunk);
|
||||||
|
}),
|
||||||
|
Promise.resolve(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
};
|
|
||||||
|
getErrorMessage(error) {
|
||||||
|
if (error.response?.status === 429) {
|
||||||
|
return 'The AI service is currently busy. Please try again in a few moments.';
|
||||||
|
}
|
||||||
|
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
||||||
|
return 'The request timed out. Please try again.';
|
||||||
|
}
|
||||||
|
if (error.response?.status === 400) {
|
||||||
|
return 'Invalid request. Please try rephrasing your question.';
|
||||||
|
}
|
||||||
|
return 'Sorry, there was an error processing your request.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+9
-5
@@ -7,10 +7,14 @@
|
|||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
|
||||||
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
|
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
|
||||||
|
import Command from '../utils/Command.js';
|
||||||
|
|
||||||
export default {
|
export default class HelpCommand extends Command {
|
||||||
data: new SlashCommandBuilder().setName('help').setDescription('Lists all available commands'),
|
defineCommand() {
|
||||||
async execute(interaction) {
|
return new SlashCommandBuilder().setName('help').setDescription('Lists all available commands');
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
const { commands } = interaction.client;
|
const { commands } = interaction.client;
|
||||||
const helpEmbed = new EmbedBuilder()
|
const helpEmbed = new EmbedBuilder()
|
||||||
.setColor('#5dc67b')
|
.setColor('#5dc67b')
|
||||||
@@ -26,5 +30,5 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await interaction.reply({ embeds: [helpEmbed], ephemeral: true });
|
await interaction.reply({ embeds: [helpEmbed], ephemeral: true });
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
+22
-35
@@ -7,49 +7,36 @@
|
|||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
|
||||||
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
|
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
|
||||||
|
import Command from '../utils/Command.js';
|
||||||
|
|
||||||
// eslint-disable-next-line import/extensions
|
export default class KickCommand extends Command {
|
||||||
import logger from '../logger.js';
|
defineCommand() {
|
||||||
|
return new SlashCommandBuilder()
|
||||||
|
.setName('kick')
|
||||||
|
.setDescription('Kick a user from the server')
|
||||||
|
.addUserOption(option =>
|
||||||
|
option.setName('target').setDescription('The user to kick').setRequired(true),
|
||||||
|
)
|
||||||
|
.addStringOption(option => option.setName('reason').setDescription('Reason for kicking'))
|
||||||
|
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers);
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
async run(interaction) {
|
||||||
data: new SlashCommandBuilder()
|
|
||||||
.setName('kick')
|
|
||||||
.setDescription('Kick a user from the server')
|
|
||||||
.addUserOption(option =>
|
|
||||||
option.setName('target').setDescription('The user to kick').setRequired(true),
|
|
||||||
)
|
|
||||||
.addStringOption(option => option.setName('reason').setDescription('Reason for kicking'))
|
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers),
|
|
||||||
async execute(interaction) {
|
|
||||||
const target = interaction.options.getMember('target');
|
const target = interaction.options.getMember('target');
|
||||||
const reason = interaction.options.getString('reason') ?? 'No reason provided';
|
const reason = interaction.options.getString('reason') ?? 'No reason provided';
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return interaction.reply({
|
throw new Error('That user is not in this server!');
|
||||||
content: 'That user is not in this server!',
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!target.kickable) {
|
if (!target.kickable) {
|
||||||
return interaction.reply({
|
throw new Error('I cannot kick this user! They may have higher permissions than me.');
|
||||||
content: 'I cannot kick this user! They may have higher permissions than me.',
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await target.kick(reason);
|
||||||
await target.kick(reason);
|
await interaction.reply({
|
||||||
return await interaction.reply({
|
content: `Successfully kicked ${target.user.tag}\nReason: ${reason}`,
|
||||||
content: `Successfully kicked ${target.user.tag}\nReason: ${reason}`,
|
ephemeral: true,
|
||||||
ephemeral: true,
|
});
|
||||||
});
|
}
|
||||||
} catch (error) {
|
}
|
||||||
logger.error(error);
|
|
||||||
return interaction.reply({
|
|
||||||
content: 'There was an error trying to kick this user!',
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
+9
-5
@@ -7,10 +7,14 @@
|
|||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
|
||||||
import { SlashCommandBuilder } from 'discord.js';
|
import { SlashCommandBuilder } from 'discord.js';
|
||||||
|
import Command from '../utils/Command.js';
|
||||||
|
|
||||||
export default {
|
export default class PingCommand extends Command {
|
||||||
data: new SlashCommandBuilder().setName('ping').setDescription('Replies with Pong!'),
|
defineCommand() {
|
||||||
async execute(interaction) {
|
return new SlashCommandBuilder().setName('ping').setDescription('Replies with Pong!');
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
await interaction.reply('Pong! 🏓');
|
await interaction.reply('Pong! 🏓');
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
+25
-30
@@ -7,35 +7,30 @@
|
|||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
|
|
||||||
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
|
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
|
||||||
|
import Command from '../utils/Command.js';
|
||||||
|
|
||||||
export default {
|
export default class PruneCommand extends Command {
|
||||||
data: new SlashCommandBuilder()
|
defineCommand() {
|
||||||
.setName('prune')
|
return new SlashCommandBuilder()
|
||||||
.setDescription('Prune up to 99 messages.')
|
.setName('prune')
|
||||||
.addIntegerOption(option =>
|
.setDescription('Prune up to 99 messages.')
|
||||||
option
|
.addIntegerOption(option =>
|
||||||
.setName('amount')
|
option
|
||||||
.setDescription('Number of messages to prune')
|
.setName('amount')
|
||||||
.setMinValue(1)
|
.setDescription('Number of messages to prune')
|
||||||
.setMaxValue(99)
|
.setMinValue(1)
|
||||||
.setRequired(true),
|
.setMaxValue(99)
|
||||||
)
|
.setRequired(true),
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
|
)
|
||||||
async execute(interaction) {
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(interaction) {
|
||||||
const amount = interaction.options.getInteger('amount');
|
const amount = interaction.options.getInteger('amount');
|
||||||
|
const deleted = await interaction.channel.bulkDelete(amount, true);
|
||||||
try {
|
await interaction.reply({
|
||||||
const deleted = await interaction.channel.bulkDelete(amount, true);
|
content: `Successfully deleted ${deleted.size} message(s).`,
|
||||||
await interaction.reply({
|
ephemeral: true,
|
||||||
content: `Successfully deleted ${deleted.size} message(s).`,
|
});
|
||||||
ephemeral: true,
|
}
|
||||||
});
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
await interaction.reply({
|
|
||||||
content: 'There was an error trying to prune messages in this channel!',
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,28 +1,79 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced logger utility with timestamps and consistent formatting
|
||||||
|
*/
|
||||||
class Logger {
|
class Logger {
|
||||||
|
/**
|
||||||
|
* Create a new Logger instance
|
||||||
|
* @param {string} moduleName - Name of the module for context
|
||||||
|
*/
|
||||||
constructor(moduleName) {
|
constructor(moduleName) {
|
||||||
|
if (typeof moduleName !== 'string') {
|
||||||
|
throw new Error('Logger requires a string moduleName');
|
||||||
|
}
|
||||||
this.moduleName = moduleName;
|
this.moduleName = moduleName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get formatted timestamp
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
#getTimestamp() {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format log message with consistent structure
|
||||||
|
* @private
|
||||||
|
* @param {string} emoji - Log level emoji
|
||||||
|
* @param {string} level - Log level name
|
||||||
|
* @param {string} message - Message to log
|
||||||
|
*/
|
||||||
|
#formatMessage(emoji, level, message) {
|
||||||
|
const timestamp = this.#getTimestamp();
|
||||||
|
return `${chalk.gray(timestamp)} ${emoji} ${chalk.cyan(`[${this.moduleName}]`)} ${level}: ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log informational message
|
||||||
|
* @param {string} message - Message to log
|
||||||
|
*/
|
||||||
log(message) {
|
log(message) {
|
||||||
console.log(`${chalk.blue('📝')} ${chalk.blue(`[${this.moduleName}]`)} ${message}`);
|
console.log(this.#formatMessage(chalk.blue('📝'), 'LOG', message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log error message
|
||||||
|
* @param {string|Error} message - Error message or Error object
|
||||||
|
*/
|
||||||
error(message) {
|
error(message) {
|
||||||
console.error(`${chalk.red('❌')} ${chalk.red(`[${this.moduleName}]`)} ${message}`);
|
const msg = message instanceof Error ? message.stack || message.message : message;
|
||||||
|
console.error(this.#formatMessage(chalk.red('❌'), 'ERROR', msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log warning message
|
||||||
|
* @param {string} message - Warning message
|
||||||
|
*/
|
||||||
warn(message) {
|
warn(message) {
|
||||||
console.warn(`${chalk.yellow('⚠️')} ${chalk.yellow(`[${this.moduleName}]`)} ${message}`);
|
console.warn(this.#formatMessage(chalk.yellow('⚠️'), 'WARN', message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log info message
|
||||||
|
* @param {string} message - Info message
|
||||||
|
*/
|
||||||
info(message) {
|
info(message) {
|
||||||
console.info(`${chalk.green('ℹ️')} ${chalk.green(`[${this.moduleName}]`)} ${message}`);
|
console.info(this.#formatMessage(chalk.green('ℹ️'), 'INFO', message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log debug message
|
||||||
|
* @param {string} message - Debug message
|
||||||
|
*/
|
||||||
debug(message) {
|
debug(message) {
|
||||||
console.debug(`${chalk.gray('🔧')} ${chalk.gray(`[${this.moduleName}]`)} ${message}`);
|
console.debug(this.#formatMessage(chalk.gray('🔧'), 'DEBUG', message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { readdirSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
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) {
|
export async function loadCommands(commandsPath, logger) {
|
||||||
const commands = [];
|
const commands = [];
|
||||||
const commandFiles = readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
const commandFiles = readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||||
@@ -10,21 +23,28 @@ export async function loadCommands(commandsPath, logger) {
|
|||||||
try {
|
try {
|
||||||
const filePath = join(commandsPath, file);
|
const filePath = join(commandsPath, file);
|
||||||
const commandModule = await import(filePath);
|
const commandModule = await import(filePath);
|
||||||
const command = commandModule.default;
|
const Command = commandModule.default;
|
||||||
|
|
||||||
if (!command?.data || !command?.execute) {
|
const CommandBase = (await import('./Command.js')).default;
|
||||||
logger.warn(
|
if (Command.prototype instanceof CommandBase) {
|
||||||
`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
commands.push(command);
|
|
||||||
logger.log(`Loaded command: ${command.data.name}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error loading command from ${file}:`, error);
|
logger.error(`Error loading command from ${file}: ${error.stack}`);
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return commands;
|
return commands;
|
||||||
|
|||||||
Reference in New Issue
Block a user