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:
+113
-142
@@ -8,6 +8,7 @@
|
||||
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import axios from 'axios';
|
||||
import Command from '../utils/Command.js';
|
||||
|
||||
const config = {
|
||||
webSearch: {
|
||||
@@ -16,21 +17,23 @@ const config = {
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('ask')
|
||||
.setDescription('Ask a question to the AI')
|
||||
.addStringOption(option =>
|
||||
option.setName('prompt').setDescription('Your question or prompt').setRequired(true),
|
||||
)
|
||||
.addBooleanOption(option =>
|
||||
option
|
||||
.setName('websearch')
|
||||
.setDescription('Enable web search for more up-to-date information')
|
||||
.setRequired(false),
|
||||
),
|
||||
export default class AskCommand extends Command {
|
||||
defineCommand() {
|
||||
return new SlashCommandBuilder()
|
||||
.setName('ask')
|
||||
.setDescription('Ask a question to the AI')
|
||||
.addStringOption(option =>
|
||||
option.setName('prompt').setDescription('Your question or prompt').setRequired(true),
|
||||
)
|
||||
.addBooleanOption(option =>
|
||||
option
|
||||
.setName('websearch')
|
||||
.setDescription('Enable web search for more up-to-date information')
|
||||
.setRequired(false),
|
||||
);
|
||||
}
|
||||
|
||||
async execute(interaction) {
|
||||
async run(interaction) {
|
||||
await interaction.deferReply();
|
||||
|
||||
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
|
||||
: config.webSearch.enabled; // Otherwise use default config
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
'https://openrouter.ai/api/v1/chat/completions',
|
||||
{
|
||||
model: webSearchEnabled
|
||||
? 'google/gemma-3-27b-it:free:online'
|
||||
: 'google/gemma-3-27b-it:free',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
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.',
|
||||
},
|
||||
{ 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 response = await axios.post(
|
||||
'https://openrouter.ai/api/v1/chat/completions',
|
||||
{
|
||||
model: webSearchEnabled
|
||||
? 'google/gemma-3-27b-it:free:online'
|
||||
: 'google/gemma-3-27b-it:free',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
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.',
|
||||
},
|
||||
{ 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 webSearchStatus = webSearchEnabled ? '\n> *Web search enabled* 🔍\n' : '';
|
||||
const formattedResponse = `> **Question:** ${prompt}${webSearchStatus}\n${aiResponse}`;
|
||||
const aiResponse = response.data.choices[0].message.content;
|
||||
const webSearchStatus = webSearchEnabled ? '\n> *Web search enabled* 🔍\n' : '';
|
||||
const formattedResponse = `> **Question:** ${prompt}${webSearchStatus}\n${aiResponse}`;
|
||||
|
||||
if (formattedResponse.length <= 2000) {
|
||||
// Send as a single message with proper formatting
|
||||
await interaction.followUp({
|
||||
content: formattedResponse,
|
||||
if (formattedResponse.length <= 2000) {
|
||||
// Send as a single message with proper formatting
|
||||
await interaction.followUp({
|
||||
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,
|
||||
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,
|
||||
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.';
|
||||
remainingText = remainingText.slice(chunk.length);
|
||||
isFirstChunk = false;
|
||||
}
|
||||
|
||||
await interaction.followUp({
|
||||
content: errorMessage,
|
||||
ephemeral: true,
|
||||
});
|
||||
// Send chunks sequentially using reduce
|
||||
await chunks.reduce(
|
||||
(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.
|
||||
|
||||
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
|
||||
import Command from '../utils/Command.js';
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder().setName('help').setDescription('Lists all available commands'),
|
||||
async execute(interaction) {
|
||||
export default class HelpCommand extends Command {
|
||||
defineCommand() {
|
||||
return new SlashCommandBuilder().setName('help').setDescription('Lists all available commands');
|
||||
}
|
||||
|
||||
async run(interaction) {
|
||||
const { commands } = interaction.client;
|
||||
const helpEmbed = new EmbedBuilder()
|
||||
.setColor('#5dc67b')
|
||||
@@ -26,5 +30,5 @@ export default {
|
||||
});
|
||||
|
||||
await interaction.reply({ embeds: [helpEmbed], ephemeral: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+22
-35
@@ -7,49 +7,36 @@
|
||||
// (at your option) any later version.
|
||||
|
||||
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
|
||||
import Command from '../utils/Command.js';
|
||||
|
||||
// eslint-disable-next-line import/extensions
|
||||
import logger from '../logger.js';
|
||||
export default class KickCommand extends Command {
|
||||
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 {
|
||||
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) {
|
||||
async run(interaction) {
|
||||
const target = interaction.options.getMember('target');
|
||||
const reason = interaction.options.getString('reason') ?? 'No reason provided';
|
||||
|
||||
if (!target) {
|
||||
return interaction.reply({
|
||||
content: 'That user is not in this server!',
|
||||
ephemeral: true,
|
||||
});
|
||||
throw new Error('That user is not in this server!');
|
||||
}
|
||||
|
||||
if (!target.kickable) {
|
||||
return interaction.reply({
|
||||
content: 'I cannot kick this user! They may have higher permissions than me.',
|
||||
ephemeral: true,
|
||||
});
|
||||
throw new Error('I cannot kick this user! They may have higher permissions than me.');
|
||||
}
|
||||
|
||||
try {
|
||||
await target.kick(reason);
|
||||
return await interaction.reply({
|
||||
content: `Successfully kicked ${target.user.tag}\nReason: ${reason}`,
|
||||
ephemeral: true,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return interaction.reply({
|
||||
content: 'There was an error trying to kick this user!',
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
await target.kick(reason);
|
||||
await interaction.reply({
|
||||
content: `Successfully kicked ${target.user.tag}\nReason: ${reason}`,
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+9
-5
@@ -7,10 +7,14 @@
|
||||
// (at your option) any later version.
|
||||
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import Command from '../utils/Command.js';
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder().setName('ping').setDescription('Replies with Pong!'),
|
||||
async execute(interaction) {
|
||||
export default class PingCommand extends Command {
|
||||
defineCommand() {
|
||||
return new SlashCommandBuilder().setName('ping').setDescription('Replies with Pong!');
|
||||
}
|
||||
|
||||
async run(interaction) {
|
||||
await interaction.reply('Pong! 🏓');
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+25
-30
@@ -7,35 +7,30 @@
|
||||
// (at your option) any later version.
|
||||
|
||||
import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js';
|
||||
import Command from '../utils/Command.js';
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('prune')
|
||||
.setDescription('Prune up to 99 messages.')
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('amount')
|
||||
.setDescription('Number of messages to prune')
|
||||
.setMinValue(1)
|
||||
.setMaxValue(99)
|
||||
.setRequired(true),
|
||||
)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
|
||||
async execute(interaction) {
|
||||
export default class PruneCommand extends Command {
|
||||
defineCommand() {
|
||||
return new SlashCommandBuilder()
|
||||
.setName('prune')
|
||||
.setDescription('Prune up to 99 messages.')
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('amount')
|
||||
.setDescription('Number of messages to prune')
|
||||
.setMinValue(1)
|
||||
.setMaxValue(99)
|
||||
.setRequired(true),
|
||||
)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages);
|
||||
}
|
||||
|
||||
async run(interaction) {
|
||||
const amount = interaction.options.getInteger('amount');
|
||||
|
||||
try {
|
||||
const deleted = await interaction.channel.bulkDelete(amount, true);
|
||||
await interaction.reply({
|
||||
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,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
const deleted = await interaction.channel.bulkDelete(amount, true);
|
||||
await interaction.reply({
|
||||
content: `Successfully deleted ${deleted.size} message(s).`,
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user