diff --git a/src/commands/misc/help.js b/src/commands/misc/help.js index 361a607..3c63a64 100644 --- a/src/commands/misc/help.js +++ b/src/commands/misc/help.js @@ -11,7 +11,7 @@ import getLocalCommands from '../../utils/getLocalCommands.js'; import mConfig from '../../config/messageConfig.js'; const COMMANDS_PER_PAGE = 8; -const INTERACTION_TIMEOUT = 300000; // 5 minutes +const INTERACTION_TIMEOUT = 300000; const categorizeCommands = (commands) => { const categorized = commands.reduce((acc, cmd) => { @@ -48,7 +48,7 @@ export default { run: async (client, interaction) => { try { const localCommands = await getLocalCommands(); - const embedColor = mConfig.embedColorDefault || '#0099ff'; + const embedColor = mConfig.embedColorDefault ?? '#0099ff'; const prefix = '!'; const commandOption = interaction.options.getString('command'); @@ -65,6 +65,11 @@ export default { embedColor, prefix ); + } else { + return await interaction.reply({ + content: `Command "${commandOption}" not found.`, + ephemeral: true, + }); } } @@ -81,6 +86,11 @@ export default { embedColor, prefix ); + } else { + return await interaction.reply({ + content: `Category "${categoryOption}" not found.`, + ephemeral: true, + }); } } @@ -103,22 +113,22 @@ export default { async function showCommandDetails(interaction, command, embedColor, prefix) { const embed = new EmbedBuilder() .setTitle(`📖 Command: ${command.name}`) - .setDescription(command.description || 'No description available.') + .setDescription(command.description ?? 'No description available.') .setColor(embedColor) .addFields( { name: '🏷️ Category', - value: command.category || 'Uncategorized', + value: command.category ?? 'Uncategorized', inline: true, }, { name: '⏳ Cooldown', - value: `${command.cooldown || 0}s`, + value: `${command.cooldown ?? 0}s`, inline: true, }, { name: '🔒 Permissions', - value: command.userPermissions?.join(', ') || 'None', + value: command.userPermissions?.join(', ') ?? 'None', inline: true, }, { @@ -182,7 +192,7 @@ async function showCommandDetails(interaction, command, embedColor, prefix) { .setTitle(`📝 Examples for ${command.name}`) .setColor(embedColor) .setDescription( - command.examples?.join('\n') || 'No examples available.' + command.examples?.join('\n') ?? 'No examples available.' ); await i.reply({ embeds: [examplesEmbed], ephemeral: true }); } @@ -193,7 +203,6 @@ async function showCommandDetails(interaction, command, embedColor, prefix) { interaction.editReply({ components: [row] }); }); } - async function showCategoryCommands( interaction, localCommands, @@ -202,7 +211,15 @@ async function showCategoryCommands( prefix ) { const categorizedCommands = categorizeCommands(localCommands); - const categoryCommands = categorizedCommands[category] || []; + const categoryCommands = categorizedCommands[category] ?? []; + + if (categoryCommands.length === 0) { + return interaction.reply({ + content: `No commands found in the "${category}" category.`, + ephemeral: true, + }); + } + const pages = createCommandPages( categoryCommands, category, @@ -268,7 +285,6 @@ async function showCategoryCommands( interaction.editReply({ components: [row] }); }); } - async function showCommandOverview( interaction, localCommands, diff --git a/src/events/ready/registerCommands.js b/src/events/ready/registerCommands.js index 8309e24..aef956c 100644 --- a/src/events/ready/registerCommands.js +++ b/src/events/ready/registerCommands.js @@ -56,7 +56,9 @@ async function deleteUnusedCommands( ); await Promise.all( - commandsToDelete.map(deleteCommand(applicationCommands, errorHandler)) + commandsToDelete.map((cmd) => + deleteCommand(applicationCommands, errorHandler)(cmd) + ) ); } @@ -100,6 +102,7 @@ const deleteCommand = (applicationCommands, errorHandler) => async (cmd) => { } }; + /** * Processes a local command, updating or creating it as needed. * @param {Collection} applicationCommands - The current application commands. diff --git a/src/events/validations/chatInputCommandValidator.js b/src/events/validations/chatInputCommandValidator.js index d2655fc..df31663 100644 --- a/src/events/validations/chatInputCommandValidator.js +++ b/src/events/validations/chatInputCommandValidator.js @@ -51,9 +51,11 @@ const sendEmbedReply = async ( .setTimestamp(); await interaction.reply({ embeds: [embed], ephemeral }); - } catch (err) {} + } catch (err) { + } }; + const getCachedData = async (key, fetchFunction) => { const cachedItem = cache.get(key); if (cachedItem) return cachedItem; @@ -77,11 +79,13 @@ const initializeCommandMap = async () => { }; const applyCooldown = (interaction, commandName, cooldownAmount) => { + if (isNaN(cooldownAmount) || cooldownAmount <= 0) { + throw new Error('Invalid cooldown amount'); + } + const userCooldowns = cooldowns.get(commandName) || new Collection(); const now = Date.now(); - const userId = `${interaction.user.id}-${ - interaction.guild ? interaction.guild.id : 'DM' - }`; + const userId = `${interaction.user.id}-${interaction.guild ? interaction.guild.id : 'DM'}`; if (userCooldowns.has(userId)) { const expirationTime = userCooldowns.get(userId) + cooldownAmount; @@ -118,13 +122,14 @@ export default async (client, errorHandler, interaction) => { try { const commandObject = commandMap.get(interaction.commandName); - if (!commandObject) { - return sendEmbedReply( - interaction, - mConfig.embedColorError, - 'Command not found.' - ); - } +if (!commandObject) { + return sendEmbedReply( + interaction, + mConfig.embedColorError, + 'Command not found.' + ); +} + if (interaction.isAutocomplete()) { return await commandObject.autocomplete(client, interaction); diff --git a/src/handlers/eventHandler.js b/src/handlers/eventHandler.js index dd828b6..1b5c1b6 100644 --- a/src/handlers/eventHandler.js +++ b/src/handlers/eventHandler.js @@ -1,7 +1,6 @@ import path from 'path'; import { fileURLToPath } from 'url'; import getAllFiles from '../utils/getAllFiles.js'; -import 'colors'; import fs from 'fs'; const __filename = fileURLToPath(import.meta.url); @@ -33,7 +32,12 @@ const loadEventFile = async ( eventRegistry ) => { try { - const { default: eventFunction } = await import(`file://${eventFile}`); + const { default: eventFunction } = await import( + encodeURI(`file://${eventFile}`) + ); + if (typeof eventFunction !== 'function') { + throw new Error(`Invalid or missing event function in ${eventFile}`); + } const eventInfo = { function: eventFunction, fileName: path.basename(eventFile), @@ -56,20 +60,27 @@ const loadEventFile = async ( * @param {Map} eventRegistry - The event registry. */ const processEventFolder = async (eventFolder, errorHandler, eventRegistry) => { - const eventFiles = getAllFiles(eventFolder); - let eventName = path.basename(eventFolder); + try { + const eventFiles = getAllFiles(eventFolder); + let eventName = path.basename(eventFolder); - if (eventName === 'validations') { - eventName = 'interactionCreate'; - } + if (eventName === 'validations') { + eventName = 'interactionCreate'; + } - const loadPromises = eventFiles - .filter((eventFile) => fs.lstatSync(eventFile).isFile()) - .map((eventFile) => - loadEventFile(eventFile, eventName, errorHandler, eventRegistry) - ); + const loadPromises = eventFiles + .filter((eventFile) => fs.lstatSync(eventFile).isFile()) + .map((eventFile) => + loadEventFile(eventFile, eventName, errorHandler, eventRegistry) + ); - await Promise.all(loadPromises); + await Promise.all(loadPromises); + } catch (error) { + errorHandler.handleError(error, { + type: 'processingEventFolder', + eventFolder, + }); + } }; /** @@ -107,11 +118,6 @@ const loadEventHandlers = async (client, errorHandler) => { handler: handler.fileName, eventName, }); - console.error( - `Error executing event handler ${handler.fileName} for event ${eventName}:` - .red, - error - ); } } }); diff --git a/src/utils/buttonPagination.js b/src/utils/buttonPagination.js index 9346714..2bcc39a 100644 --- a/src/utils/buttonPagination.js +++ b/src/utils/buttonPagination.js @@ -21,7 +21,6 @@ const updateButtons = (buttons, index, pagesLength) => { switch (button.data.custom_id) { case 'first': case 'prev': - case 'home': button.setDisabled(index === 0); break; case 'next': @@ -44,7 +43,7 @@ const animateTransition = async (msg, newEmbed) => { }; // LRU Cache implementation for efficient embed caching -class LRUCache { +class EnhancedLRUCache { constructor(capacity) { this.capacity = capacity; this.cache = new Map(); @@ -65,6 +64,17 @@ class LRUCache { } this.cache.set(key, value); } + + prefetch(key, getValue) { + if (!this.cache.has(key)) { + const value = getValue(key); + this.put(key, value); + } + } + + clear() { + this.cache.clear(); + } } /** @@ -80,18 +90,16 @@ export default async (interaction, pages, options = {}) => { throw new Error('Invalid pages array'); const defaultOptions = { - time: 5 * 60 * 1000, // 5 minutes + time: 5 * 60 * 1000, buttonEmojis: { first: '⏮️', prev: '⬅️', - home: '🏠', next: '➡️', last: '⏭️', }, buttonStyles: { first: ButtonStyle.Primary, prev: ButtonStyle.Primary, - home: ButtonStyle.Secondary, next: ButtonStyle.Primary, last: ButtonStyle.Primary, }, @@ -122,20 +130,12 @@ export default async (interaction, pages, options = {}) => { createButton( 'first', mergedOptions.buttonEmojis.first, - mergedOptions.buttonStyles.first, - true + mergedOptions.buttonStyles.first ), createButton( 'prev', mergedOptions.buttonEmojis.prev, - mergedOptions.buttonStyles.prev, - true - ), - createButton( - 'home', - mergedOptions.buttonEmojis.home, - mergedOptions.buttonStyles.home, - true + mergedOptions.buttonStyles.prev ), createButton( 'next', @@ -152,20 +152,29 @@ export default async (interaction, pages, options = {}) => { const row = new ActionRowBuilder().addComponents(buttons); let index = 0; - // Implement LRU Cache for embed caching - const embedCache = new LRUCache(mergedOptions.cacheSize); + const embedCache = new EnhancedLRUCache(mergedOptions.cacheSize); const getEmbed = (index) => { let embed = embedCache.get(index); if (!embed) { embed = pages[index].setFooter({ - text: `Page ${index + 1} of ${pages.length} | Total Items: ${pages.length}`, + text: `Page ${index + 1} of ${pages.length}`, }); embedCache.put(index, embed); } return embed; }; + const prefetchAdjacentPages = (currentIndex) => { + const prefetchIndexes = [currentIndex - 1, currentIndex + 1]; + prefetchIndexes.forEach((idx) => { + if (idx >= 0 && idx < pages.length) { + embedCache.prefetch(idx, getEmbed); + } + }); + }; + + updateButtons(buttons, index, pages.length); const msg = await interaction.editReply({ embeds: [getEmbed(index)], components: [row], @@ -199,9 +208,6 @@ export default async (interaction, pages, options = {}) => { case 'prev': index = Math.max(0, index - 1); break; - case 'home': - index = 0; - break; case 'next': index = Math.min(pages.length - 1, index + 1); break; @@ -220,6 +226,7 @@ export default async (interaction, pages, options = {}) => { components: [row], }); } + prefetchAdjacentPages(index); collector.resetTimer(); }); @@ -233,8 +240,8 @@ export default async (interaction, pages, options = {}) => { }) .catch(() => null); - // Log usage statistics console.log(`Pagination ended. Total interactions: ${usageCount}`); + embedCache.clear(); }); return msg; @@ -250,9 +257,5 @@ export default async (interaction, pages, options = {}) => { content: 'An error occurred while setting up pagination.', }); } - // Assuming you have an error handling system - if (typeof client !== 'undefined' && client.errorHandler) { - await client.errorHandler.handleError(err, { type: 'Pagination' }); - } } };