diff --git a/src/commands/misc/disableping.js b/src/commands/admin/disableping.js similarity index 99% rename from src/commands/misc/disableping.js rename to src/commands/admin/disableping.js index fcd622a..6929ccf 100644 --- a/src/commands/misc/disableping.js +++ b/src/commands/admin/disableping.js @@ -26,6 +26,7 @@ export default { nsfwMode: false, testMode: false, devOnly: false, + category: 'Admin', run: async (client, interaction) => { try { diff --git a/src/commands/misc/jointoCreate.js b/src/commands/admin/jointoCreate.js similarity index 99% rename from src/commands/misc/jointoCreate.js rename to src/commands/admin/jointoCreate.js index 2f4bd9b..10c6617 100644 --- a/src/commands/misc/jointoCreate.js +++ b/src/commands/admin/jointoCreate.js @@ -60,6 +60,7 @@ export default { PermissionFlagsBits.ManageChannels, PermissionFlagsBits.ManageRoles, ], + category: 'Admin', run: async (client, interaction) => { const subcommand = interaction.options.getSubcommand(); diff --git a/src/commands/misc/purge.js b/src/commands/admin/purge.js similarity index 98% rename from src/commands/misc/purge.js rename to src/commands/admin/purge.js index 2781a0b..e9165d5 100644 --- a/src/commands/misc/purge.js +++ b/src/commands/admin/purge.js @@ -29,6 +29,8 @@ export default { nwfwMode: false, testMode: false, devOnly: false, + category: 'Admin', + prefix: true, run: async (client, interaction) => { const amount = interaction.options.getInteger('amount'); diff --git a/src/commands/misc/welcome.js b/src/commands/admin/welcome.js similarity index 99% rename from src/commands/misc/welcome.js rename to src/commands/admin/welcome.js index 74c896e..0480762 100644 --- a/src/commands/misc/welcome.js +++ b/src/commands/admin/welcome.js @@ -51,6 +51,7 @@ export default { userPermissions: [PermissionsBitField.ADMINISTRATOR], botPermissions: [PermissionsBitField.ManageRoles], cooldown: 5, + category: 'Admin', run: async (client, interaction) => { await interaction.deferReply({ ephemeral: true }); diff --git a/src/commands/developer/createitem.js b/src/commands/developer/createitem.js index ce5425e..78aec1b 100644 --- a/src/commands/developer/createitem.js +++ b/src/commands/developer/createitem.js @@ -48,6 +48,8 @@ export default { .toJSON(), userPermissions: [], botPermissions: [], + category: 'Devloper', + cooldown: 10, nsfwMode: false, testMode: false, diff --git a/src/commands/developer/delitem.js b/src/commands/developer/delitem.js index 5df50d6..873dfab 100644 --- a/src/commands/developer/delitem.js +++ b/src/commands/developer/delitem.js @@ -17,6 +17,7 @@ export default { .toJSON(), userPermissions: [], botPermissions: [], + category: 'Devloper', cooldown: 10, nsfwMode: false, testMode: false, diff --git a/src/commands/developer/dm.js b/src/commands/developer/dm.js index b169a2b..8c26fed 100644 --- a/src/commands/developer/dm.js +++ b/src/commands/developer/dm.js @@ -64,6 +64,7 @@ export default { nwfwMode: false, testMode: false, devOnly: true, + category: 'Devloper', run: async (client, interaction) => { const subcommand = interaction.options.getSubcommand(); diff --git a/src/commands/developer/eco.js b/src/commands/developer/eco.js index d30d153..a6b3eb4 100644 --- a/src/commands/developer/eco.js +++ b/src/commands/developer/eco.js @@ -104,6 +104,7 @@ export default { nsfwMode: false, testMode: false, devOnly: true, + category: 'Devloper', run: async (client, interaction) => { const subcommand = interaction.options.getSubcommand(); diff --git a/src/commands/developer/servers.js b/src/commands/developer/servers.js index da84e69..92b998f 100644 --- a/src/commands/developer/servers.js +++ b/src/commands/developer/servers.js @@ -74,6 +74,7 @@ export default { nsfwMode: false, testMode: false, devOnly: true, + category: 'Devloper', run: async (client, interaction) => { await interaction.deferReply({ ephemeral: true }); diff --git a/src/commands/developer/setPrefix.js b/src/commands/developer/setPrefix.js new file mode 100644 index 0000000..6dc88ab --- /dev/null +++ b/src/commands/developer/setPrefix.js @@ -0,0 +1,107 @@ +import { UserPrefix } from '../../schemas/prefix.js'; +import { EmbedBuilder, SlashCommandBuilder } from 'discord.js'; + +const DISALLOWED_PREFIXES = [ + '/', + '\\', + '@', + '#', + '$', + '&', + '(', + ')', + '{', + '}', + '[', + ']', +]; + +export default { + data: new SlashCommandBuilder() + .setName('setprefix') + .setDescription('Set a prefix for a user or remove it (dev only)') + .addUserOption((option) => + option + .setName('user') + .setDescription('The user to set the prefix for') + .setRequired(true) + ) + .addStringOption((option) => + option + .setName('prefix') + .setDescription('The new prefix to set (use "noprefix" to remove)') + .setRequired(true) + ), + userPermissions: [], + botPermissions: [], + category: 'Misc', + cooldown: 5, + nsfwMode: false, + testMode: false, + devOnly: true, + category: 'Devloper', + + run: async (client, interaction) => { + try { + const targetUser = interaction.options.getUser('user'); + const newPrefix = interaction.options.getString('prefix'); + + if (!targetUser) { + return interaction.reply({ + content: 'Please provide a valid user.', + ephemeral: true, + }); + } + + await updatePrefix(interaction, targetUser, newPrefix); + } catch (error) { + console.error('Error in setprefix command:', error); + await interaction.reply({ + content: + 'An error occurred while processing the command. Please try again later.', + ephemeral: true, + }); + } + }, +}; + +async function updatePrefix(interaction, targetUser, newPrefix) { + if (DISALLOWED_PREFIXES.includes(newPrefix) && newPrefix !== 'noprefix') { + return interaction.reply({ + content: `The prefix "${newPrefix}" is not allowed as it may conflict with Discord or bot functionality.`, + ephemeral: true, + }); + } + + const finalPrefix = newPrefix === 'noprefix' ? '' : newPrefix; + let updateData; + let responseMessage; + + if (newPrefix === 'noprefix') { + updateData = { exemptFromPrefix: true, prefix: '' }; + responseMessage = `Prefix for ${targetUser.tag} has been removed and they are now exempt from using a prefix.`; + } else { + updateData = { exemptFromPrefix: false, prefix: finalPrefix }; + responseMessage = `Prefix for ${targetUser.tag} has been updated to \`${finalPrefix}\`.`; + } + + try { + await UserPrefix.findOneAndUpdate( + { userId: targetUser.id }, + { $set: { ...updateData, userId: targetUser.id } }, + { upsert: true, new: true, runValidators: true } + ); + + await interaction.reply({ + content: responseMessage, + ephemeral: true, + }); + } catch (error) { + console.error('Error updating prefix:', error); + await interaction.reply({ + content: + 'An error occurred while updating the prefix. Please try again later.', + ephemeral: true, + }); + } +} diff --git a/src/commands/economy/balance.js b/src/commands/economy/balance.js index e4f3760..6b11e2f 100644 --- a/src/commands/economy/balance.js +++ b/src/commands/economy/balance.js @@ -15,6 +15,7 @@ export default { testMode: false, devOnly: false, dmAllowed: true, + category: 'economy', run: async (client, interaction) => { const userId = interaction.user.id; diff --git a/src/commands/economy/bank.js b/src/commands/economy/bank.js index 38776d3..5504cf9 100644 --- a/src/commands/economy/bank.js +++ b/src/commands/economy/bank.js @@ -13,6 +13,7 @@ export default { nwfwMode: false, testMode: false, devOnly: false, + category: 'economy', run: async (client, interaction) => { try { diff --git a/src/commands/economy/beg.js b/src/commands/economy/beg.js index 07e817f..f1846cd 100644 --- a/src/commands/economy/beg.js +++ b/src/commands/economy/beg.js @@ -14,6 +14,7 @@ export default { nsfwMode: false, testMode: false, devOnly: false, + category: 'economy', run: async (client, interaction) => { const userId = interaction.user.id; diff --git a/src/commands/economy/coinflip.js b/src/commands/economy/coinflip.js index 2708c12..764f056 100644 --- a/src/commands/economy/coinflip.js +++ b/src/commands/economy/coinflip.js @@ -36,6 +36,7 @@ export default { nsfwMode: false, testMode: false, devOnly: false, + category: 'economy', run: async (client, interaction) => { const userId = interaction.user.id; diff --git a/src/commands/economy/crime.js b/src/commands/economy/crime.js index 1593a86..41c91a7 100644 --- a/src/commands/economy/crime.js +++ b/src/commands/economy/crime.js @@ -11,6 +11,7 @@ export default { nsfwMode: false, testMode: false, devOnly: false, + category: 'economy', run: async (client, interaction) => { const userId = interaction.user.id; diff --git a/src/commands/economy/daily.js b/src/commands/economy/daily.js index 2d29c4a..2dcd47f 100644 --- a/src/commands/economy/daily.js +++ b/src/commands/economy/daily.js @@ -14,6 +14,7 @@ export default { nsfwMode: false, testMode: false, devOnly: false, + category: 'economy', run: async (client, interaction) => { const userId = interaction.user.id; diff --git a/src/commands/economy/deposit.js b/src/commands/economy/deposit.js index a8a38fc..9028152 100644 --- a/src/commands/economy/deposit.js +++ b/src/commands/economy/deposit.js @@ -22,6 +22,7 @@ export default { nwfwMode: false, testMode: false, devOnly: false, + category: 'economy', run: async (client, interaction) => { const userId = interaction.user.id; diff --git a/src/commands/economy/hourly.js b/src/commands/economy/hourly.js index fcc58d2..b9c155f 100644 --- a/src/commands/economy/hourly.js +++ b/src/commands/economy/hourly.js @@ -12,6 +12,7 @@ export default { nwfwMode: false, testMode: false, devOnly: false, + category: 'economy', run: async (client, interaction) => { const userId = interaction.user.id; diff --git a/src/commands/economy/inventory.js b/src/commands/economy/inventory.js index 78e0da2..a7bb729 100644 --- a/src/commands/economy/inventory.js +++ b/src/commands/economy/inventory.js @@ -12,6 +12,7 @@ export default { nsfwMode: false, testMode: false, devOnly: false, + category: 'economy', run: async (client, interaction) => { const userId = interaction.user.id; diff --git a/src/commands/economy/items.js b/src/commands/economy/items.js index 67d82c1..67f2208 100644 --- a/src/commands/economy/items.js +++ b/src/commands/economy/items.js @@ -15,6 +15,7 @@ export default { nsfwMode: false, testMode: false, devOnly: false, + category: 'economy', run: async (client, interaction) => { // Fetch all items from the database diff --git a/src/commands/economy/leaderboard.js b/src/commands/economy/leaderboard.js index afa297a..af85d17 100644 --- a/src/commands/economy/leaderboard.js +++ b/src/commands/economy/leaderboard.js @@ -15,6 +15,7 @@ export default { nsfwMode: false, testMode: false, devOnly: false, + category: 'economy', run: async (client, interaction) => { // Defer the interaction diff --git a/src/commands/economy/shop.js b/src/commands/economy/shop.js index aed5dfd..fa56f3e 100644 --- a/src/commands/economy/shop.js +++ b/src/commands/economy/shop.js @@ -21,6 +21,8 @@ export default { nsfwMode: false, testMode: false, devOnly: false, + category: 'economy', + run: async (client, interaction) => { try { const items = await Item.find().lean(); diff --git a/src/commands/economy/slots.js b/src/commands/economy/slots.js index 65d8f93..c7d4640 100644 --- a/src/commands/economy/slots.js +++ b/src/commands/economy/slots.js @@ -21,6 +21,7 @@ export default { nsfwMode: false, testMode: false, devOnly: false, + category: 'economy', run: async (client, interaction) => { const userId = interaction.user.id; diff --git a/src/commands/economy/weekly.js b/src/commands/economy/weekly.js index 7808b79..df85f7d 100644 --- a/src/commands/economy/weekly.js +++ b/src/commands/economy/weekly.js @@ -14,6 +14,7 @@ export default { nsfwMode: false, testMode: false, devOnly: false, + category: 'economy', run: async (client, interaction) => { const userId = interaction.user.id; diff --git a/src/commands/economy/withdraw.js b/src/commands/economy/withdraw.js index 5f28183..dfee776 100644 --- a/src/commands/economy/withdraw.js +++ b/src/commands/economy/withdraw.js @@ -24,6 +24,7 @@ export default { nsfwMode: false, testMode: false, devOnly: false, + category: 'economy', run: async (client, interaction) => { const userId = interaction.user.id; diff --git a/src/commands/image/cat.js b/src/commands/image/cat.js index d8f26da..e6fef86 100644 --- a/src/commands/image/cat.js +++ b/src/commands/image/cat.js @@ -12,9 +12,11 @@ export default { .setDescription('send random cat img') .toJSON(), + category: 'Image', nwfwMode: false, testMode: false, devOnly: false, + prefix: true, userPermissionsBitField: [], bot: [], diff --git a/src/commands/image/dog.js b/src/commands/image/dog.js index d2bd5c7..904496c 100644 --- a/src/commands/image/dog.js +++ b/src/commands/image/dog.js @@ -11,9 +11,11 @@ export default { .setName('dog') .setDescription('send random dog image') .toJSON(), + category: 'Image', nwfwMode: false, testMode: false, devOnly: false, + prefix: true, userPermissionsBitField: [], bot: [], diff --git a/src/commands/image/magik.js b/src/commands/image/magik.js index 305c15b..2dc36a6 100644 --- a/src/commands/image/magik.js +++ b/src/commands/image/magik.js @@ -32,6 +32,7 @@ export default { cooldown: 10, nsfwMode: false, testMode: false, + category: 'Image', devOnly: false, userPermissionsBitField: [], bot: [], diff --git a/src/commands/misc/avatar.js b/src/commands/misc/avatar.js index 0dff144..56c86de 100644 --- a/src/commands/misc/avatar.js +++ b/src/commands/misc/avatar.js @@ -19,11 +19,13 @@ export default { userPermissions: [], botPermissions: [], + category: 'Misc', cooldown: 5, deleted: false, nwfwMode: false, testMode: false, devOnly: false, + prefix: true, run: async (client, interaction) => { try { diff --git a/src/commands/misc/fact.js b/src/commands/misc/fact.js index 912793d..54b65a9 100644 --- a/src/commands/misc/fact.js +++ b/src/commands/misc/fact.js @@ -14,10 +14,13 @@ export default { userPermissionsBitField: [], bot: [], + category: 'Misc', cooldown: 19, // Cooldown of 5 seconds nwfwMode: false, testMode: false, devOnly: false, + prefix: true, + run: async (client, interaction) => { try { const res = await axios.get( diff --git a/src/commands/misc/guild.js b/src/commands/misc/guild.js index 67e7fb6..beed323 100644 --- a/src/commands/misc/guild.js +++ b/src/commands/misc/guild.js @@ -21,6 +21,7 @@ export default { cooldown: 5, userPermissionsBitField: [], bot: [], + category: 'Misc', run: async (client, interaction) => { if (interaction.options.getSubcommand() === 'join') { diff --git a/src/commands/misc/help.js b/src/commands/misc/help.js index 0edc723..0ae52b2 100644 --- a/src/commands/misc/help.js +++ b/src/commands/misc/help.js @@ -1,30 +1,53 @@ -import { SlashCommandBuilder, EmbedBuilder } from 'discord.js'; +import { + SlashCommandBuilder, + EmbedBuilder, + ActionRowBuilder, + StringSelectMenuBuilder, + ButtonBuilder, + ButtonStyle, + ComponentType, +} from 'discord.js'; import getLocalCommands from '../../utils/getLocalCommands.js'; import mConfig from '../../config/messageConfig.js'; -import paginate from '../../utils/buttonPagination.js'; -const MAX_DESCRIPTION_LENGTH = 2048; // Max length of a single field in an embed +const MAX_DESCRIPTION_LENGTH = 2048; +const COMMANDS_PER_PAGE = 10; +const MAX_FIELD_LENGTH = 1024; +const INTERACTION_TIMEOUT = 300000; // 5 minutes -// Function to split text into chunks of a specified length const splitText = (text, length) => { - const result = []; - for (let i = 0; i < text.length; i += length) { - result.push(text.slice(i, i + length)); - } - return result; + return text.match(new RegExp(`.{1,${length}}`, 'g')) || []; +}; + +const categorizeCommands = (commands) => { + const categories = {}; + commands.forEach((cmd) => { + const category = cmd.category || 'Uncategorized'; + if (!categories[category]) { + categories[category] = []; + } + categories[category].push(cmd); + }); + return categories; }; export default { data: new SlashCommandBuilder() .setName('help') - .setDescription( - 'Displays a list of available commands or info about a specific command' - ) + .setDescription('Displays information about commands') .addStringOption((option) => option .setName('command') .setDescription('Specific command to get info about') .setRequired(false) + .setAutocomplete(true) + ) + .addStringOption((option) => + option + .setName('category') + .setDescription('View commands by category') + .setRequired(false) + .setAutocomplete(true) ) .toJSON(), @@ -34,75 +57,360 @@ export default { nsfwMode: false, testMode: false, devOnly: false, + category: 'Misc', + + async autocomplete(client, interaction) { + const focusedOption = interaction.options.getFocused(true); + const localCommands = await getLocalCommands(); + + if (focusedOption.name === 'command') { + const filtered = localCommands.filter((cmd) => + cmd.data.name + .toLowerCase() + .startsWith(focusedOption.value.toLowerCase()) + ); + await interaction.respond( + filtered.map((cmd) => ({ + name: cmd.data.name, + value: cmd.data.name, + })) + ); + } else if (focusedOption.name === 'category') { + const categories = [ + ...new Set( + localCommands.map((cmd) => cmd.category || 'Uncategorized') + ), + ]; + const filtered = categories.filter((cat) => + cat.toLowerCase().startsWith(focusedOption.value.toLowerCase()) + ); + await interaction.respond( + filtered.map((cat) => ({ + name: cat, + value: cat, + })) + ); + } + }, run: async (client, interaction) => { try { const localCommands = await getLocalCommands(); const commandName = interaction.options.getString('command'); - const embedColor = mConfig.embedColorDefault || '#0099ff'; // Default fallback color + const category = interaction.options.getString('category'); + const embedColor = mConfig.embedColorDefault || '#0099ff'; if (commandName) { - // Provide detailed info about a specific command - const command = localCommands.find( - (cmd) => cmd.data.name === commandName + return await showCommandDetails( + interaction, + localCommands, + commandName, + embedColor + ); + } else if (category) { + return await showCategoryCommands( + interaction, + localCommands, + category, + embedColor ); - if (!command) { - return interaction.reply({ - content: 'Command not found.', - ephemeral: true, - }); - } - - const embed = new EmbedBuilder() - .setTitle(`Command: ${command.data.name}`) - .setDescription( - command.data.description || 'No description available.' - ) - .setColor(embedColor); - - if (command.data.options) { - command.data.options.forEach((option) => { - embed.addFields({ - name: option.name, - value: option.description, - inline: true, - }); - }); - } - - return interaction.reply({ embeds: [embed] }); } else { - // Provide a list of all commands - const commandsText = localCommands - .map( - (cmd) => - `\`${cmd.data.name}\`: ${cmd.data.description || 'No description available.'}` - ) - .join('\n'); - - // Split the commandsText into chunks to fit within embed length limits - const textChunks = splitText(commandsText, MAX_DESCRIPTION_LENGTH); - - // Create an array of embeds - const pages = textChunks.map((chunk, index) => - new EmbedBuilder() - .setTitle('Available Commands') - .setDescription(chunk) - .setColor(embedColor) - .setFooter({ - text: `Page ${index + 1} of ${textChunks.length}`, - }) + return await showCommandOverview( + interaction, + localCommands, + embedColor ); - - // Use the pagination utility - return paginate(interaction, pages); } } catch (error) { + console.error('Error in help command:', error); return interaction.reply({ content: 'An error occurred while fetching help information.', ephemeral: true, }); - throw error; } }, }; + +async function showCommandDetails( + interaction, + localCommands, + commandName, + embedColor +) { + const command = localCommands.find( + (cmd) => cmd.data.name.toLowerCase() === commandName.toLowerCase() + ); + if (!command) { + return interaction.reply({ + content: 'Command not found.', + ephemeral: true, + }); + } + + const embed = new EmbedBuilder() + .setTitle(`📖 Command: ${command.data.name}`) + .setDescription(command.data.description || 'No description available.') + .setColor(embedColor) + .addFields( + { + name: '🏷ī¸ Category', + value: command.category || 'Uncategorized', + inline: true, + }, + { + name: 'âŗ Cooldown', + value: `${command.cooldown || 0}s`, + inline: true, + }, + { + name: '🔒 Permissions', + value: command.userPermissions?.join(', ') || 'None', + inline: true, + } + ); + + if (command.aliases?.length > 0) { + embed.addFields({ + name: '🔀 Aliases', + value: command.aliases.join(', '), + inline: true, + }); + } + + if (command.usage) { + embed.addFields({ + name: '💡 Usage', + value: `\`${command.usage}\``, + inline: false, + }); + } + + if (command.data.options?.length > 0) { + const optionsText = command.data.options + .map( + (opt) => + `â€ĸ **${opt.name}**: ${opt.description} ${ + opt.required ? '*(Required)*' : '' + }` + ) + .join('\n'); + embed.addFields({ + name: '🔧 Options', + value: optionsText, + inline: false, + }); + } + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('return_to_overview') + .setLabel('Return to Overview') + .setStyle(ButtonStyle.Secondary) + ); + + const message = await interaction.reply({ + embeds: [embed], + components: [row], + }); + + const collector = message.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: INTERACTION_TIMEOUT, + }); + + collector.on('collect', async (i) => { + if (i.customId === 'return_to_overview') { + await showCommandOverview(i, localCommands, embedColor); + } + }); + + collector.on('end', () => { + row.components.forEach((component) => component.setDisabled(true)); + interaction.editReply({ components: [row] }); + }); +} + +async function showCategoryCommands( + interaction, + localCommands, + category, + embedColor +) { + const categoryCommands = localCommands.filter( + (cmd) => (cmd.category || 'Uncategorized') === category + ); + const pages = createCommandPages(categoryCommands, category, embedColor); + + let currentPage = 0; + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('prev_page') + .setLabel('Previous') + .setStyle(ButtonStyle.Primary) + .setDisabled(true), + new ButtonBuilder() + .setCustomId('next_page') + .setLabel('Next') + .setStyle(ButtonStyle.Primary) + .setDisabled(pages.length <= 1), + new ButtonBuilder() + .setCustomId('return_to_overview') + .setLabel('Return to Overview') + .setStyle(ButtonStyle.Secondary) + ); + + const message = await interaction.reply({ + embeds: [pages[currentPage]], + components: [row], + }); + + const collector = message.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: INTERACTION_TIMEOUT, + }); + + collector.on('collect', async (i) => { + if (i.user.id !== interaction.user.id) { + return i.reply({ + content: "You can't use this button.", + ephemeral: true, + }); + } + + if (i.customId === 'prev_page') { + currentPage = Math.max(0, currentPage - 1); + } else if (i.customId === 'next_page') { + currentPage = Math.min(pages.length - 1, currentPage + 1); + } else if (i.customId === 'return_to_overview') { + await showCommandOverview(i, localCommands, embedColor); + return; + } + + row.components[0].setDisabled(currentPage === 0); + row.components[1].setDisabled(currentPage === pages.length - 1); + + await i.update({ embeds: [pages[currentPage]], components: [row] }); + }); + + collector.on('end', () => { + row.components.forEach((component) => component.setDisabled(true)); + interaction.editReply({ components: [row] }); + }); +} + +async function showCommandOverview(interaction, localCommands, embedColor) { + const categorizedCommands = categorizeCommands(localCommands); + const categories = Object.keys(categorizedCommands); + + const overviewEmbed = createOverviewEmbed(categorizedCommands, embedColor); + + const categorySelect = new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() + .setCustomId('category_select') + .setPlaceholder('Select a category') + .addOptions( + categories.map((cat) => ({ + label: cat, + value: cat, + emoji: getCategoryEmoji(cat), + })) + ) + ); + + const message = await interaction.reply({ + embeds: [overviewEmbed], + components: [categorySelect], + }); + + const collector = message.createMessageComponentCollector({ + componentType: ComponentType.StringSelect, + time: INTERACTION_TIMEOUT, + }); + + collector.on('collect', async (i) => { + if (i.user.id !== interaction.user.id) { + return i.reply({ + content: "You can't use this menu.", + ephemeral: true, + }); + } + + if (i.customId === 'category_select') { + const selectedCategory = i.values[0]; + await showCategoryCommands( + i, + localCommands, + selectedCategory, + embedColor + ); + } + }); + + collector.on('end', () => { + categorySelect.components[0].setDisabled(true); + interaction.editReply({ components: [categorySelect] }); + }); +} + +function createOverviewEmbed(categorizedCommands, embedColor) { + const embed = new EmbedBuilder() + .setTitle('📚 Command Overview') + .setColor(embedColor) + .setDescription( + "Here's an overview of all command categories. Select a category from the dropdown menu to view detailed information." + ); + + Object.entries(categorizedCommands).forEach(([category, commands]) => { + embed.addFields({ + name: `${getCategoryEmoji(category)} ${category}`, + value: `${commands.length} command${commands.length !== 1 ? 's' : ''}`, + inline: true, + }); + }); + + return embed; +} + +function createCommandPages(commands, category, embedColor) { + const pages = []; + for (let i = 0; i < commands.length; i += COMMANDS_PER_PAGE) { + const pageCommands = commands.slice(i, i + COMMANDS_PER_PAGE); + const embed = new EmbedBuilder() + .setTitle(`${getCategoryEmoji(category)} ${category} Commands`) + .setColor(embedColor) + .setFooter({ + text: `Page ${pages.length + 1}/${Math.ceil(commands.length / COMMANDS_PER_PAGE)}`, + }); + + const description = pageCommands + .map((cmd) => { + const usage = cmd.usage ? ` - Usage: \`${cmd.usage}\`` : ''; + return `â€ĸ **${cmd.data.name}**: ${cmd.data.description}${usage}`; + }) + .join('\n'); + + embed.setDescription(description.substring(0, MAX_FIELD_LENGTH)); + pages.push(embed); + } + return pages; +} + +function getCategoryEmoji(category) { + const emojiMap = { + Uncategorized: '📁', + ticket: '🎟ī¸', + Admin: '🛡ī¸', + Misc: '🎉', + image: '🔧', + economy: '💰', + Music: 'đŸŽĩ', + Developer: '👩🏾‍đŸ’ģ', + Moderation: '🚨', + Fun: '🎮', + Utility: '🛠ī¸', + Information: 'ℹī¸', + Configuration: '⚙ī¸', + }; + return emojiMap[category] || '📁'; +} diff --git a/src/commands/misc/ping.js b/src/commands/misc/ping.js index 5ac040a..4876247 100644 --- a/src/commands/misc/ping.js +++ b/src/commands/misc/ping.js @@ -13,10 +13,12 @@ export default { userPermissions: [], botPermissions: [], + category: 'Misc', cooldown: 5, nwfwMode: false, testMode: false, devOnly: false, + prefix: true, run: async (client, interaction) => { try { diff --git a/src/commands/misc/prefix.js b/src/commands/misc/prefix.js new file mode 100644 index 0000000..da4fb7a --- /dev/null +++ b/src/commands/misc/prefix.js @@ -0,0 +1,98 @@ +import { UserPrefix } from '../../schemas/prefix.js'; +import { EmbedBuilder, SlashCommandBuilder } from 'discord.js'; + +const DISALLOWED_PREFIXES = [ + '/', + '\\', + '@', + '#', + '$', + '&', + '(', + ')', + '{', + '}', + '[', + ']', +]; + +export default { + data: new SlashCommandBuilder() + .setName('prefix') + .setDescription('Shows or sets your prefix') + .addStringOption((option) => + option + .setName('prefix') + .setDescription('The new prefix to set') + .setRequired(false) + ), + userPermissions: [], + botPermissions: [], + category: 'Misc', + cooldown: 5, + nsfwMode: false, + testMode: false, + devOnly: false, + + run: async (client, interaction) => { + try { + const newPrefix = interaction.options.getString('prefix'); + + if (newPrefix !== null) { + await updatePrefix(interaction, newPrefix); + } else { + await showCurrentPrefix(interaction); + } + } catch (error) { + console.error('Error in prefix command:', error); + await interaction.reply({ + content: + 'An error occurred while processing the command. Please try again later.', + ephemeral: true, + }); + } + }, +}; + +async function updatePrefix(interaction, newPrefix) { + if (DISALLOWED_PREFIXES.includes(newPrefix)) { + return interaction.reply({ + content: `The prefix "${newPrefix}" is not allowed as it may conflict with Discord or bot functionality.`, + ephemeral: true, + }); + } + + const finalPrefix = newPrefix === 'noprefix' ? '' : newPrefix; + + await UserPrefix.findOneAndUpdate( + { userId: interaction.user.id }, + { prefix: finalPrefix }, + { upsert: true } + ); + + await interaction.reply({ + content: `Your prefix has been updated to \`${finalPrefix || 'no prefix'}\`.`, + ephemeral: true, + }); +} + +async function showCurrentPrefix(interaction) { + const userPrefixData = await UserPrefix.findOne({ + userId: interaction.user.id, + }); + const userPrefix = userPrefixData ? userPrefixData.prefix : '!'; + + const embed = new EmbedBuilder() + .setTitle('Your Prefix') + .setDescription( + `Your current prefix is \`${userPrefix || 'no prefix'}\`. You can set a new prefix by using \`/prefix [newPrefix]\`.` + ) + .setColor('#00FF00') + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.avatarURL(), + }) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); +} diff --git a/src/commands/misc/social.js b/src/commands/misc/social.js index eaa0b00..e6ba0da 100644 --- a/src/commands/misc/social.js +++ b/src/commands/misc/social.js @@ -9,6 +9,7 @@ export default { .toJSON(), userPermissions: [], botPermissions: [], + category: 'Misc', cooldown: 5, nwfwMode: false, testMode: false, diff --git a/src/commands/ticket/close-all-tickets.js b/src/commands/ticket/close-all-tickets.js index c32ba19..0100fe3 100644 --- a/src/commands/ticket/close-all-tickets.js +++ b/src/commands/ticket/close-all-tickets.js @@ -14,6 +14,7 @@ export default { ), userPermissions: [PermissionFlagsBits.ManageChannels], botPermissions: [PermissionFlagsBits.ManageChannels], + category: 'ticket', run: async (client, interaction) => { await interaction.deferReply({ ephemeral: true }); diff --git a/src/commands/ticket/ticket-close.js b/src/commands/ticket/ticket-close.js index 454bfc0..2a7124a 100644 --- a/src/commands/ticket/ticket-close.js +++ b/src/commands/ticket/ticket-close.js @@ -14,6 +14,7 @@ export default { userPermissions: [PermissionFlagsBits.ManageChannels], botPermissions: [PermissionFlagsBits.ManageChannels], + category: 'ticket', run: async (client, interaction) => { await interaction.deferReply({ ephemeral: true }); diff --git a/src/commands/ticket/ticketAddMember.js b/src/commands/ticket/ticketAddMember.js index bc76bd5..cd8b9cf 100644 --- a/src/commands/ticket/ticketAddMember.js +++ b/src/commands/ticket/ticketAddMember.js @@ -15,6 +15,7 @@ export default { userPermissions: [PermissionFlagsBits.ManageChannels], botPermissions: [PermissionFlagsBits.ManageChannels], + category: 'ticket', run: async (client, interaction) => { await interaction.deferReply({ ephemeral: true }); diff --git a/src/commands/ticket/ticketRemoveMenber.js b/src/commands/ticket/ticketRemoveMenber.js index 4c74e53..087e3c8 100644 --- a/src/commands/ticket/ticketRemoveMenber.js +++ b/src/commands/ticket/ticketRemoveMenber.js @@ -15,6 +15,7 @@ export default { userPermissions: [PermissionFlagsBits.ManageChannels], botPermissions: [PermissionFlagsBits.ManageChannels], + category: 'ticket', run: async (client, interaction) => { await interaction.deferReply({ ephemeral: true }); diff --git a/src/commands/ticket/ticketSetup.js b/src/commands/ticket/ticketSetup.js index 8c18c2b..218566a 100644 --- a/src/commands/ticket/ticketSetup.js +++ b/src/commands/ticket/ticketSetup.js @@ -73,6 +73,7 @@ export default { PermissionFlagsBits.ManageChannels, PermissionFlagsBits.ManageRoles, ], + category: 'ticket', run: async (client, interaction) => { const subcommand = interaction.options.getSubcommand(); diff --git a/src/events/messageCreate/command.js b/src/events/messageCreate/command.js new file mode 100644 index 0000000..4b8c74c --- /dev/null +++ b/src/events/messageCreate/command.js @@ -0,0 +1,218 @@ +import { Collection, EmbedBuilder, PermissionsBitField } from 'discord.js'; +import { config } from '../../config/config.js'; +import mConfig from '../../config/messageConfig.js'; +import { convertSlashCommandsToPrefix } from '../../utils/SlashCommandtoPrifix.js'; +import { UserPrefix } from '../../schemas/prefix.js'; + +const { maintenance, developersId, testServerId } = config; +const cooldowns = new Collection(); +const commandMap = new Map(); + +let prefixCommandsLoaded = false; + +export default async (client, errorHandler, message) => { + try { + const { prefix, isExempt } = await getUserPrefixInfo(message.author.id); + + if (!isExempt && !message.content.startsWith(prefix)) return; + + const commandContent = isExempt + ? message.content + : message.content.slice(prefix.length); + + await loadPrefixCommandsIfNeeded(client); + + const { commandName, args } = parseCommand(commandContent); + const command = findCommand(commandName); + + if (!command) return; + + await executeCommand(client, message, command, args, errorHandler); + } catch (error) { + console.error('Error in command handler:', error); + await sendEmbedReply( + message, + mConfig.embedColorError, + 'An unexpected error occurred. Please try again later.' + ); + } +}; + +async function getUserPrefixInfo(userId) { + try { + const userPrefixData = await UserPrefix.findOne({ userId }); + return { + prefix: userPrefixData?.prefix || '!', + isExempt: userPrefixData?.exemptFromPrefix || false, + }; + } catch (error) { + console.error('Error fetching user prefix:', error); + return { prefix: '!', isExempt: false }; + } +} + +async function loadPrefixCommandsIfNeeded(client) { + if (prefixCommandsLoaded) return; + + try { + client.prefixCommands = await convertSlashCommandsToPrefix(); + client.prefixCommands.forEach((cmd) => commandMap.set(cmd.name, cmd)); + prefixCommandsLoaded = true; + } catch (error) { + console.error('Error loading prefix commands:', error); + throw new Error('Failed to load prefix commands'); + } +} + +function parseCommand(content) { + const args = content.trim().split(/ +/); + const commandName = args.shift().toLowerCase(); + return { commandName, args }; +} + +function findCommand(commandName) { + return ( + commandMap.get(commandName) || + Array.from(commandMap.values()).find( + (cmd) => cmd.aliases && cmd.aliases.includes(commandName) + ) + ); +} + +async function executeCommand(client, message, command, args, errorHandler) { + try { + if (!checkCommandPrerequisites(message, command)) return; + + const cooldown = applyCooldown( + message.author.id, + command.name, + (command.cooldown || 3) * 1000 + ); + if (cooldown.active) { + return sendEmbedReply( + message, + mConfig.embedColorError, + mConfig.commandCooldown.replace('{time}', cooldown.timeLeft) + ); + } + + await command.run(client, message, args); + console.log(`Command executed: ${command.name} by ${message.author.tag}`); + } catch (err) { + await handleCommandError(err, command, message, errorHandler); + } +} + +function checkCommandPrerequisites(message, command) { + if (maintenance && !developersId.includes(message.author.id)) { + sendEmbedReply( + message, + mConfig.embedColorError, + 'Bot is currently in maintenance mode. Please try again later.' + ); + return false; + } + + if (!message.guild && !command.dmAllowed) { + sendEmbedReply( + message, + mConfig.embedColorError, + 'This command can only be used within a server.' + ); + return false; + } + + if (command.devOnly && !developersId.includes(message.author.id)) { + sendEmbedReply(message, mConfig.embedColorError, mConfig.commandDevOnly); + return false; + } + + if (command.testMode && message.guild?.id !== testServerId) { + sendEmbedReply(message, mConfig.embedColorError, mConfig.commandTestMode); + return false; + } + + if (command.nsfwMode && !message.channel.nsfw) { + sendEmbedReply(message, mConfig.embedColorError, mConfig.nsfw); + return false; + } + + if ( + command.userPermissions?.length && + !checkPermissions(message, command.userPermissions, 'user') + ) { + sendEmbedReply( + message, + mConfig.embedColorError, + mConfig.userNoPermissions + ); + return false; + } + + if ( + command.botPermissions?.length && + !checkPermissions(message, command.botPermissions, 'bot') + ) { + sendEmbedReply( + message, + mConfig.embedColorError, + mConfig.botNoPermissions + ); + return false; + } + + return true; +} + +async function handleCommandError(err, command, message, errorHandler) { + await errorHandler.handleError(err, { + type: 'commandError', + commandName: command.name, + userId: message.author.id, + guildId: message.guild?.id, + }); + + sendEmbedReply( + message, + mConfig.embedColorError, + 'An error occurred while executing the command.' + ); +} + +function sendEmbedReply(message, color, description) { + const embed = new EmbedBuilder().setColor(color).setDescription(description); + return message.reply({ embeds: [embed] }); +} + +function checkPermissions(message, requiredPermissions, type) { + if (!message.guild) return true; + + const member = type === 'user' ? message.member : message.guild.members.me; + return requiredPermissions.every((permission) => + member.permissions.has(PermissionsBitField.Flags[permission]) + ); +} + +function applyCooldown(userId, commandName, cooldownTime) { + if (!cooldowns.has(commandName)) { + cooldowns.set(commandName, new Collection()); + } + + const now = Date.now(); + const timestamps = cooldowns.get(commandName); + const cooldownAmount = cooldownTime; + + if (timestamps.has(userId)) { + const expirationTime = timestamps.get(userId) + cooldownAmount; + + if (now < expirationTime) { + const timeLeft = (expirationTime - now) / 1000; + return { active: true, timeLeft: timeLeft.toFixed(1) }; + } + } + + timestamps.set(userId, now); + setTimeout(() => timestamps.delete(userId), cooldownAmount); + + return { active: false }; +} diff --git a/src/events/validations/ModalCommandValidator.js b/src/events/validations/ModalCommandValidator.js index f3d6db7..d55971f 100644 --- a/src/events/validations/ModalCommandValidator.js +++ b/src/events/validations/ModalCommandValidator.js @@ -6,6 +6,7 @@ import mConfig from '../../config/messageConfig.js'; import getModals from '../../utils/getModals.js'; const modals = new Collection(); +const cooldowns = new Map(); let modalsLoaded = false; const sendEmbedReply = async ( @@ -14,22 +15,38 @@ const sendEmbedReply = async ( description, ephemeral = true ) => { - const embed = new EmbedBuilder().setColor(color).setDescription(description); - await interaction.reply({ embeds: [embed], ephemeral }); + try { + const embed = new EmbedBuilder() + .setColor(color) + .setDescription(description); + await interaction.reply({ embeds: [embed], ephemeral }); + } catch (error) { + console.error('Error sending embed reply:'.red, error); + } }; -const checkPermissions = (interaction, permissions, type) => { - const member = - type === 'user' ? interaction.member : interaction.guild.members.me; - return permissions.every((permission) => +const checkPermissions = (member, permissions) => + permissions.every((permission) => member.permissions.has(PermissionsBitField.Flags[permission]) ); -}; -const loadModals = async (errorHandler) => { +const loadModals = async (errorHandler, retryCount = 0) => { try { const modalFiles = await getModals(); for (const modal of modalFiles) { + modal.compiledChecks = { + userPermissions: modal.userPermissions + ? (interaction) => + checkPermissions(interaction.member, modal.userPermissions) + : () => true, + botPermissions: modal.botPermissions + ? (interaction) => + checkPermissions( + interaction.guild.members.me, + modal.botPermissions + ) + : () => true, + }; modals.set(modal.customId, modal); } console.log(`Loaded ${modals.size} modal commands`.green); @@ -37,6 +54,16 @@ const loadModals = async (errorHandler) => { } catch (error) { errorHandler.handleError(error, { type: 'modalLoad' }); console.error('Error loading modals:'.red, error); + + if (retryCount < 3) { + console.log( + `Retrying modal load... (Attempt ${retryCount + 1})`.yellow + ); + await new Promise((resolve) => setTimeout(resolve, 5000)); // Wait 5 seconds before retry + await loadModals(errorHandler, retryCount + 1); + } else { + console.error('Failed to load modals after 3 attempts'.red); + } } }; @@ -67,10 +94,7 @@ const handleModal = async (client, errorHandler, interaction) => { } // Check user permissions - if ( - modalObject.userPermissions?.length && - !checkPermissions(interaction, modalObject.userPermissions, 'user') - ) { + if (!modalObject.compiledChecks.userPermissions(interaction)) { return sendEmbedReply( interaction, mConfig.embedColorError, @@ -79,10 +103,7 @@ const handleModal = async (client, errorHandler, interaction) => { } // Check bot permissions - if ( - modalObject.botPermissions?.length && - !checkPermissions(interaction, modalObject.botPermissions, 'bot') - ) { + if (!modalObject.compiledChecks.botPermissions(interaction)) { return sendEmbedReply( interaction, mConfig.embedColorError, @@ -93,7 +114,7 @@ const handleModal = async (client, errorHandler, interaction) => { // Check cooldown if (modalObject.cooldown) { const cooldownKey = `${interaction.user.id}-${customId}`; - const cooldownTime = modals.get(cooldownKey); + const cooldownTime = cooldowns.get(cooldownKey); if (cooldownTime && Date.now() < cooldownTime) { const remainingTime = Math.ceil((cooldownTime - Date.now()) / 1000); return sendEmbedReply( @@ -102,7 +123,7 @@ const handleModal = async (client, errorHandler, interaction) => { `Please wait ${remainingTime} seconds before submitting this modal again.` ); } - modals.set(cooldownKey, Date.now() + modalObject.cooldown * 1000); + cooldowns.set(cooldownKey, Date.now() + modalObject.cooldown * 1000); } try { @@ -111,7 +132,6 @@ const handleModal = async (client, errorHandler, interaction) => { } catch (error) { console.error(`Error executing modal ${customId}:`.red, error); - // Use errorHandler to handle the error await errorHandler.handleError(error, { type: 'modalError', modalId: customId, @@ -134,7 +154,6 @@ export default async (client, errorHandler, interaction) => { await loadModals(errorHandler); } - // Log modal usage console.log( `Modal ${interaction.customId} submitted by ${interaction.user.tag} in ${interaction.guild.name}` .yellow diff --git a/src/events/validations/buttonValidator.js b/src/events/validations/buttonValidator.js index 3f5e732..a337698 100644 --- a/src/events/validations/buttonValidator.js +++ b/src/events/validations/buttonValidator.js @@ -1,120 +1,159 @@ /** @format */ import 'colors'; -import { EmbedBuilder, Collection, PermissionsBitField } from 'discord.js'; +import { EmbedBuilder, PermissionsBitField } from 'discord.js'; import { config } from '../../config/config.js'; import mConfig from '../../config/messageConfig.js'; import getButtons from '../../utils/getButtons.js'; -const buttons = new Collection(); +class LRUCache { + constructor(capacity) { + this.capacity = capacity; + this.cache = new Map(); + } + + get(key) { + if (!this.cache.has(key)) return undefined; + const item = this.cache.get(key); + this.cache.delete(key); + this.cache.set(key, item); + return item; + } + + set(key, value) { + if (this.cache.size >= this.capacity) { + const oldestKey = this.cache.keys().next().value; + this.cache.delete(oldestKey); + } + this.cache.set(key, value); + } +} + +const buttons = new Map(); +const cooldowns = new Map(); +const buttonCache = new LRUCache(100); // Adjust capacity as needed +let buttonsLoaded = false; + +const sendEmbedReply = async ( + interaction, + color, + description, + ephemeral = false +) => { + const embed = new EmbedBuilder() + .setColor('Yellow') + .setDescription(description); + await interaction.reply({ embeds: [embed], ephemeral }); +}; + +const checkPermissions = (member, permissions) => + permissions.every((permission) => + member.permissions.has(PermissionsBitField.Flags[permission]) + ); -const loadButtons = async (errorHandler) => { +const loadButtons = async (errorHandler, retryCount = 0) => { try { const buttonFiles = await getButtons(); for (const button of buttonFiles) { + button.compiledChecks = { + userPermissions: button.userPermissions + ? (interaction) => + checkPermissions(interaction.member, button.userPermissions) + : () => true, + botPermissions: button.botPermissions + ? (interaction) => + checkPermissions( + interaction.guild.members.me, + button.botPermissions + ) + : () => true, + }; buttons.set(button.customId, button); } console.log(`Loaded ${buttons.size} buttons`.green); + buttonsLoaded = true; } catch (error) { errorHandler.handleError(error, { type: 'buttonLoad' }); console.error('Error loading buttons:'.red, error); - } -}; - -const sendEmbedReply = (interaction, color, description, ephemeral = false) => { - const embed = new EmbedBuilder() - .setColor(mConfig.embedColors[color]) - .setDescription(description); - return interaction.reply({ embeds: [embed], ephemeral }); -}; -const checkPermissions = (interaction, permissions, type) => { - const member = - type === 'user' ? interaction.member : interaction.guild.members.me; - return permissions.every((permission) => - member.permissions.has(PermissionsBitField.Flags[permission]) - ); + if (retryCount < 3) { + console.log( + `Retrying button load... (Attempt ${retryCount + 1})`.yellow + ); + await new Promise((resolve) => setTimeout(resolve, 5000)); + await loadButtons(errorHandler, retryCount + 1); + } else { + console.error('Failed to load buttons after 3 attempts'.red); + } + } }; const handleButton = async (client, errorHandler, interaction) => { const { customId } = interaction; - const button = buttons.get(customId); + let button = buttonCache.get(customId); + if (!button) { + button = buttons.get(customId); + if (button) buttonCache.set(customId, button); + } if (!button) return; const { developersId, testServerId } = config; - // Check if the button is developer-only if (button.devOnly && !developersId.includes(interaction.user.id)) { - return sendEmbedReply( - interaction, - mConfig.embedColorError, - mConfig.commandDevOnly, - true - ); + return sendEmbedReply(interaction, 'error', mConfig.commandDevOnly, true); } - // Check if the button is in test mode if (button.testMode && interaction.guild.id !== testServerId) { return sendEmbedReply( interaction, - mConfig.embedColorError, + 'error', mConfig.commandTestMode, true ); } - // Check user permissions - if ( - button.userPermissions?.length && - !checkPermissions(interaction, button.userPermissions, 'user') - ) { + if (!button.compiledChecks.userPermissions(interaction)) { return sendEmbedReply( interaction, - mConfig.embedColorError, + 'error', mConfig.userNoPermissions, true ); } - // Check bot permissions - if ( - button.botPermissions?.length && - !checkPermissions(interaction, button.botPermissions, 'bot') - ) { + if (!button.compiledChecks.botPermissions(interaction)) { return sendEmbedReply( interaction, - mConfig.embedColorError, + 'error', mConfig.botNoPermissions, true ); } - // Check if the user is the original interaction user if ( interaction.message.interaction && interaction.message.interaction.user.id !== interaction.user.id ) { return sendEmbedReply( interaction, - mConfig.embedColorError, + 'error', mConfig.cannotUseButton, true ); } - // Check cooldown if (button.cooldown) { const cooldownKey = `${interaction.user.id}-${customId}`; - const cooldownTime = buttons.get(cooldownKey); + const cooldownTime = cooldowns.get(cooldownKey); if (cooldownTime && Date.now() < cooldownTime) { const remainingTime = Math.ceil((cooldownTime - Date.now()) / 1000); return sendEmbedReply( interaction, - mConfig.embedColorError, + 'error', `Please wait ${remainingTime} seconds before using this button again.`, true ); } - buttons.set(cooldownKey, Date.now() + button.cooldown * 1000); + cooldowns.set(cooldownKey, Date.now() + button.cooldown * 1000); } try { @@ -124,31 +163,26 @@ const handleButton = async (client, errorHandler, interaction) => { await button.run(client, interaction); } catch (error) { console.error(`Error executing button ${customId}:`.red, error); - - // Use errorHandler to handle the error await errorHandler.handleError(error, { type: 'buttonError', buttonId: customId, userId: interaction.user.id, guildId: interaction.guild.id, }); - sendEmbedReply( interaction, - mConfig.embedColorError, + 'error', 'There was an error while executing this button!', true ); } }; -let buttonsLoaded = false; export default async (client, errorHandler, interaction) => { if (!interaction.isButton()) return; if (!buttonsLoaded) { await loadButtons(errorHandler); - buttonsLoaded = true; } await handleButton(client, errorHandler, interaction); diff --git a/src/events/validations/chatInputCommandValidator.js b/src/events/validations/chatInputCommandValidator.js index d03ba88..73e8340 100644 --- a/src/events/validations/chatInputCommandValidator.js +++ b/src/events/validations/chatInputCommandValidator.js @@ -6,8 +6,33 @@ import { config } from '../../config/config.js'; import mConfig from '../../config/messageConfig.js'; import getLocalCommands from '../../utils/getLocalCommands.js'; +class LRUCache { + constructor(capacity) { + this.capacity = capacity; + this.cache = new Map(); + } + + get(key) { + if (!this.cache.has(key)) return undefined; + const value = this.cache.get(key); + this.cache.delete(key); + this.cache.set(key, value); + return value; + } + + set(key, value) { + if (this.cache.has(key)) this.cache.delete(key); + else if (this.cache.size >= this.capacity) { + const firstKey = this.cache.keys().next().value; + this.cache.delete(firstKey); + } + this.cache.set(key, value); + } +} + +const cache = new LRUCache(100); // Adjust capacity as needed const cooldowns = new Collection(); -const cache = new Map(); +const commandMap = new Map(); const sendEmbedReply = async ( interaction, @@ -26,22 +51,27 @@ const sendEmbedReply = async ( }; const getCachedData = async (key, fetchFunction) => { - const now = Date.now(); - const cacheDuration = config.cacheDuration * 1000; const cachedItem = cache.get(key); - - if (cachedItem && now - cachedItem.timestamp < cacheDuration) { - return cachedItem.data; - } + if (cachedItem) return cachedItem; const data = await fetchFunction(); - cache.set(key, { data, timestamp: now }); + cache.set(key, data); return data; }; const getCachedLocalCommands = () => getCachedData('localCommands', getLocalCommands); +const initializeCommandMap = async () => { + const localCommands = await getCachedLocalCommands(); + localCommands.forEach((cmd) => { + commandMap.set(cmd.data.name, cmd); + if (cmd.aliases) { + cmd.aliases.forEach((alias) => commandMap.set(alias, cmd)); + } + }); +}; + const applyCooldown = (interaction, commandName, cooldownAmount) => { const userCooldowns = cooldowns.get(commandName) || new Collection(); const now = Date.now(); @@ -75,15 +105,14 @@ export default async (client, errorHandler, interaction) => { if (!interaction.isChatInputCommand() && !interaction.isAutocomplete()) return; - const localCommands = await getCachedLocalCommands(); + if (commandMap.size === 0) { + await initializeCommandMap(); // Initialize command map if it's empty + } + const { developersId, testServerId, maintenance } = config; try { - const commandObject = localCommands.find( - (cmd) => - cmd.data.name === interaction.commandName || - cmd.aliases?.includes(interaction.commandName) - ); + const commandObject = commandMap.get(interaction.commandName); if (!commandObject) { return sendEmbedReply( @@ -175,11 +204,9 @@ export default async (client, errorHandler, interaction) => { ); } - // Run the command in a try-catch block to ensure errors are handled try { await commandObject.run(client, interaction); } catch (err) { - // Use errorHandler to handle the error await errorHandler.handleError(err, { type: 'commandError', commandName: interaction.commandName, @@ -199,7 +226,6 @@ export default async (client, errorHandler, interaction) => { .green ); } catch (err) { - // Use errorHandler to handle the error await errorHandler.handleError(err, { type: 'commandError', commandName: interaction.commandName, diff --git a/src/events/validations/contextMenuCommandValidator.js b/src/events/validations/contextMenuCommandValidator.js index 6eba28f..e1e83cd 100644 --- a/src/events/validations/contextMenuCommandValidator.js +++ b/src/events/validations/contextMenuCommandValidator.js @@ -1,12 +1,36 @@ /** @format */ import 'colors'; -import { EmbedBuilder, Collection } from 'discord.js'; +import { EmbedBuilder } from 'discord.js'; import { config } from '../../config/config.js'; import mConfig from '../../config/messageConfig.js'; import getLocalContextMenus from '../../utils/getLocalContextMenus.js'; -const contextMenus = new Collection(); +class LRUCache { + constructor(capacity) { + this.capacity = capacity; + this.cache = new Map(); + } + + get(key) { + if (!this.cache.has(key)) return undefined; + const value = this.cache.get(key); + this.cache.delete(key); + this.cache.set(key, value); + return value; + } + + set(key, value) { + if (this.cache.has(key)) this.cache.delete(key); + else if (this.cache.size >= this.capacity) { + const firstKey = this.cache.keys().next().value; + this.cache.delete(firstKey); + } + this.cache.set(key, value); + } +} + +const contextMenus = new LRUCache(100); // Adjust capacity as needed const sendEmbedReply = async ( interaction, @@ -25,11 +49,17 @@ const checkPermissions = (interaction, permissions, type) => { }; const loadContextMenus = async () => { - const menuFiles = await getLocalContextMenus(); - for (const menu of menuFiles) { - contextMenus.set(menu.data.name, menu); + try { + const menuFiles = await getLocalContextMenus(); + for (const menu of menuFiles) { + contextMenus.set(menu.data.name, menu); + } + console.log( + `Loaded ${contextMenus.cache.size} context menu commands`.green + ); + } catch (error) { + console.error('Error loading context menus:'.red, error); } - console.log(`Loaded ${contextMenus.size} context menu commands`.green); }; const handleContextMenu = async (client, errorHandler, interaction) => { @@ -90,7 +120,6 @@ const handleContextMenu = async (client, errorHandler, interaction) => { error ); - // Use errorHandler to handle the error await errorHandler.handleError(error, { type: 'contextMenuError', commandName: commandName, @@ -106,9 +135,11 @@ const handleContextMenu = async (client, errorHandler, interaction) => { } }; +// Call this function during bot initialization +await loadContextMenus(); + export default async (client, errorHandler, interaction) => { if (!interaction.isContextMenuCommand()) return; - await loadContextMenus(); await handleContextMenu(client, errorHandler, interaction); }; diff --git a/src/events/validations/seclectMenuValidator.js b/src/events/validations/seclectMenuValidator.js index e275afd..586cf21 100644 --- a/src/events/validations/seclectMenuValidator.js +++ b/src/events/validations/seclectMenuValidator.js @@ -31,7 +31,10 @@ const loadSelects = async (errorHandler) => { try { const selectFiles = await getSelects(); for (const select of selectFiles) { - selects.set(select.customId, select); + if (select && select.customId) { + selects.set(select.customId, select); + } else { + } } console.log(`Loaded ${selects.size} select menu commands`.green); selectsLoaded = true; @@ -44,20 +47,7 @@ const loadSelects = async (errorHandler) => { const handleSelect = async (client, errorHandler, interaction) => { const { customId } = interaction; const selectObject = selects.get(customId); - - if (!selectObject) { - errorHandler.handleError(new Error(`Unknown select menu: ${customId}`), { - type: 'unknownSelect', - selectId: customId, - userId: interaction.user.id, - guildId: interaction.guild.id, - }); - return sendEmbedReply( - interaction, - mConfig.embedColorError, - 'This select menu is not recognized.' - ); - } + if (!selectObject) return; const { developersId, testServerId } = config; diff --git a/src/events/voiceStateUpdate/joinToCreate.js b/src/events/voiceStateUpdate/joinToCreate.js index dee6c56..bc7b990 100644 --- a/src/events/voiceStateUpdate/joinToCreate.js +++ b/src/events/voiceStateUpdate/joinToCreate.js @@ -66,24 +66,81 @@ async function handleLeave(oldState) { } async function handleMove(oldState, newState) { - const newChannel = await JoinToSystem.findOne({ - channelId: newState.channelId, + const setup = await JoinToSystemSetup.findOne({ + guildId: newState.guild.id, }); - if (newChannel) { - const canJoin = await checkJoinPermission( - newState.member, - newState.channel - ); - if (!canJoin) { - await newState.setChannel(oldState.channel); - return; + const isMovingFromJoinToCreate = + setup && setup.joinToCreateChannelId === oldState.channelId; + const isMovingToJoinToCreate = + setup && setup.joinToCreateChannelId === newState.channelId; + + if (isMovingToJoinToCreate) { + try { + const newChannel = await createVoiceChannel( + newState.member, + newState.guild, + newState.client + ); + if (newChannel) { + await JoinToSystem.create({ + guildId: newState.guild.id, + channelId: newChannel.id, + ownerId: newState.member.id, + }); + await newState.setChannel(newChannel); + // Delete the Join to Create channel + const joinToCreateChannel = newState.channel; + setTimeout(async () => { + try { + await joinToCreateChannel.delete(); + } catch (error) { + console.error( + 'Error deleting Join to Create channel:', + error + ); + } + }, 1000); // Delay to ensure the member has moved to the new channel + } + } catch (error) { + console.error('Error creating new channel:', error); + } + } else { + const newChannel = await JoinToSystem.findOne({ + channelId: newState.channelId, + }); + if (newChannel) { + const canJoin = await checkJoinPermission( + newState.member, + newState.channel + ); + if (!canJoin) { + await newState.setChannel(oldState.channel); + return; + } } + await handleLeave(oldState); } - await handleLeave(oldState); + // Handle leaving the old channel if it's a created channel + const oldChannel = await JoinToSystem.findOne({ + channelId: oldState.channelId, + }); + if (oldChannel && oldState.channel) { + try { + const updatedOldChannel = await oldState.channel.fetch(); + if (updatedOldChannel.members.size === 0) { + await updatedOldChannel.delete(); + await JoinToSystem.deleteOne({ channelId: oldState.channelId }); + } + } catch (error) { + console.error('Error handling old channel leave:', error); + } + } } async function checkJoinPermission(member, channel) { + if (!channel) return true; + const channelDoc = await JoinToSystem.findOne({ channelId: channel.id }); if (!channelDoc) return true; diff --git a/src/handlers/eventHandler.js b/src/handlers/eventHandler.js index 6bf2934..dd828b6 100644 --- a/src/handlers/eventHandler.js +++ b/src/handlers/eventHandler.js @@ -7,49 +7,79 @@ import fs from 'fs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -export default async (client, errorHandler) => { - const eventRegistry = new Map(); - const loadedEvents = new Set(); +/** + * Registers an event and its handler in the event registry. + * @param {Map} eventRegistry - The event registry. + * @param {string} eventName - The name of the event. + * @param {Object} eventInfo - Information about the event handler. + */ +const registerEvent = (eventRegistry, eventName, eventInfo) => { + if (!eventRegistry.has(eventName)) { + eventRegistry.set(eventName, []); + } + eventRegistry.get(eventName).push(eventInfo); +}; - const registerEvent = (eventName, eventInfo) => { - if (!eventRegistry.has(eventName)) { - eventRegistry.set(eventName, []); - } - eventRegistry.get(eventName).push(eventInfo); - }; +/** + * Loads an event file and registers it in the event registry. + * @param {string} eventFile - The path to the event file. + * @param {string} eventName - The name of the event. + * @param {Function} errorHandler - The error handler function. + */ +const loadEventFile = async ( + eventFile, + eventName, + errorHandler, + eventRegistry +) => { + try { + const { default: eventFunction } = await import(`file://${eventFile}`); + const eventInfo = { + function: eventFunction, + fileName: path.basename(eventFile), + priority: eventFunction.priority || 0, + }; + registerEvent(eventRegistry, eventName, eventInfo); + } catch (error) { + errorHandler.handleError(error, { + type: 'loadingEventFile', + eventFile, + eventName, + }); + } +}; - const loadEventFile = async (eventFile, eventName) => { - try { - const { default: eventFunction } = await import(`file://${eventFile}`); - const eventInfo = { - function: eventFunction, - fileName: path.basename(eventFile), - priority: eventFunction.priority || 0, - }; - registerEvent(eventName, eventInfo); - } catch (error) { - errorHandler.handleError(error, { - type: 'loadingEventFile', - eventFile, - eventName, - }); - } - }; +/** + * Processes an event folder and loads all event files within it. + * @param {string} eventFolder - The path to the event folder. + * @param {Function} errorHandler - The error handler function. + * @param {Map} eventRegistry - The event registry. + */ +const processEventFolder = async (eventFolder, errorHandler, eventRegistry) => { + const eventFiles = getAllFiles(eventFolder); + let eventName = path.basename(eventFolder); + + if (eventName === 'validations') { + eventName = 'interactionCreate'; + } - const processEventFolder = async (eventFolder) => { - const eventFiles = getAllFiles(eventFolder); - let eventName = eventFolder.replace(/\\/g, '/').split('/').pop(); + const loadPromises = eventFiles + .filter((eventFile) => fs.lstatSync(eventFile).isFile()) + .map((eventFile) => + loadEventFile(eventFile, eventName, errorHandler, eventRegistry) + ); - if (eventName === 'validations') { - eventName = 'interactionCreate'; - } + await Promise.all(loadPromises); +}; - for (const eventFile of eventFiles) { - if (fs.lstatSync(eventFile).isFile()) { - await loadEventFile(eventFile, eventName); - } - } - }; +/** + * Main function to load and register all event handlers. + * @param {Object} client - The Discord client. + * @param {Function} errorHandler - The error handler function. + */ +const loadEventHandlers = async (client, errorHandler) => { + const eventRegistry = new Map(); + const loadedEvents = new Set(); try { const eventFolders = getAllFiles( @@ -57,9 +87,11 @@ export default async (client, errorHandler) => { true ); - for (const eventFolder of eventFolders) { - await processEventFolder(eventFolder); - } + await Promise.all( + eventFolders.map((eventFolder) => + processEventFolder(eventFolder, errorHandler, eventRegistry) + ) + ); for (const [eventName, eventHandlers] of eventRegistry) { eventHandlers.sort((a, b) => b.priority - a.priority); @@ -88,6 +120,7 @@ export default async (client, errorHandler) => { } } catch (error) { errorHandler.handleError(error, { type: 'settingUpEventHandlers' }); - console.error('Error setting up event handlers:'.red, error); } }; + +export default loadEventHandlers; diff --git a/src/schemas/prefix.js b/src/schemas/prefix.js new file mode 100644 index 0000000..1c44484 --- /dev/null +++ b/src/schemas/prefix.js @@ -0,0 +1,18 @@ +import mongoose from 'mongoose'; + +const userPrefixSchema = new mongoose.Schema({ + userId: { + type: String, + required: true, + }, + prefix: { + type: String, + default: '!', + }, + exemptFromPrefix: { + type: Boolean, + default: false, + }, +}); + +export const UserPrefix = mongoose.model('UserPrefixs', userPrefixSchema); diff --git a/src/utils/SlashCommandtoPrifix.js b/src/utils/SlashCommandtoPrifix.js new file mode 100644 index 0000000..1d61cdc --- /dev/null +++ b/src/utils/SlashCommandtoPrifix.js @@ -0,0 +1,218 @@ +import { Collection } from 'discord.js'; +import loadCommands from './getLocalCommands.js'; + +export async function convertSlashCommandsToPrefix() { + const prefixCommands = new Collection(); + const slashCommands = await loadCommands(); + + slashCommands.forEach((slashCommand) => { + if (!slashCommand.prefix) return; + + const prefixCommand = { + ...slashCommand, + name: slashCommand.data.name, + description: slashCommand.data.description, + aliases: slashCommand.data.aliases || [], + run: async (client, message, args) => { + // Handle subcommands + const { subcommand, remainingArgs, error } = handleSubcommands( + slashCommand, + args, + message + ); + if (error) return; + + const mockOptions = {}; + const options = subcommand + ? slashCommand.data.options.find( + (opt) => opt.name === subcommand + )?.options || [] + : slashCommand.data.options || []; + + options.forEach((option, index) => { + const value = remainingArgs[index]; + mockOptions[option.name] = parseOptionValue( + option, + value, + message + ); + }); + + const mockInteraction = createMockInteraction( + message, + subcommand, + mockOptions + ); + + try { + await slashCommand.run(client, mockInteraction); + } catch (error) { + console.error( + `Error executing command ${slashCommand.data.name}:`, + error + ); + await message.reply( + 'An error occurred while executing the command. Please try again later.' + ); + } + }, + }; + + prefixCommands.set(prefixCommand.name, prefixCommand); + prefixCommand.aliases.forEach((alias) => + prefixCommands.set(alias, prefixCommand) + ); + }); + + return prefixCommands; +} + +function handleSubcommands(slashCommand, args, message) { + if (!slashCommand.data.options?.some((opt) => opt.type === 1)) { + return { subcommand: null, remainingArgs: args }; + } + + const subcommandName = args[0]?.toLowerCase(); + const subcommands = slashCommand.data.options.filter( + (opt) => opt.type === 1 + ); + + if (!subcommandName) { + const subcommandList = subcommands + .map((sc) => `\`${sc.name}\``) + .join(', '); + message.reply( + `This command requires a subcommand. Available subcommands: ${subcommandList}` + ); + return { error: true }; + } + + const subcommand = subcommands.find( + (sc) => sc.name.toLowerCase() === subcommandName + ); + + if (!subcommand) { + const subcommandList = subcommands + .map((sc) => `\`${sc.name}\``) + .join(', '); + message.reply( + `Invalid subcommand \`${subcommandName}\`. Available subcommands: ${subcommandList}` + ); + return { error: true }; + } + + return { + subcommand: subcommand.name, + remainingArgs: args.slice(1), + }; +} + +function parseOptionValue(option, value, message) { + switch (option.type) { + case 3: // STRING + return parseStringOption(option, value); + case 4: // INTEGER + return parseInt(value) || null; + case 10: // NUMBER + return parseFloat(value) || null; + case 5: // BOOLEAN + return value?.toLowerCase() === 'true'; + case 6: // USER + return ( + message.mentions.users.first() || + message.guild.members.cache.get(value)?.user || + null + ); + case 7: // CHANNEL + return ( + message.mentions.channels.first() || + message.guild.channels.cache.get(value) || + null + ); + case 8: // ROLE + return ( + message.mentions.roles.first() || + message.guild.roles.cache.get(value) || + null + ); + case 9: // MENTIONABLE + return ( + message.mentions.members.first() || + message.mentions.roles.first() || + message.mentions.users.first() || + message.guild.members.cache.get(value) || + message.guild.roles.cache.get(value) || + null + ); + case 11: // ATTACHMENT + return message.attachments.first() || null; + default: + return null; + } +} + +function parseStringOption(option, value) { + if (option.choices) { + const validChoice = option.choices.find( + (choice) => + choice.name.toLowerCase() === value?.toLowerCase() || + choice.value.toLowerCase() === value?.toLowerCase() + ); + return validChoice ? validChoice.value : null; + } + return value || null; +} + +function createMockInteraction(message, subcommand, mockOptions) { + return { + reply: async (options) => handleReply(message, options), + deferReply: async () => message.channel.sendTyping(), + editReply: async (options) => handleReply(message, options, true), + followUp: async (options) => handleReply(message, options), + fetchReply: async () => message, + guild: message.guild, + channel: message.channel, + member: message.member, + user: message.author, + options: { + getSubcommand: () => subcommand, + getString: (name) => mockOptions[name], + getInteger: (name) => mockOptions[name], + getNumber: (name) => mockOptions[name], + getBoolean: (name) => mockOptions[name], + getUser: (name) => mockOptions[name], + getMember: (name) => + message.guild.members.cache.get(mockOptions[name]?.id), + getChannel: (name) => mockOptions[name], + getRole: (name) => mockOptions[name], + getMentionable: (name) => mockOptions[name], + getAttachment: (name) => mockOptions[name], + }, + }; +} + +async function handleReply(message, options, isEdit = false) { + const content = options.content || ''; + const embeds = options.embeds || []; + + if (!content && embeds.length === 0) return; + + const replyOptions = { content, embeds }; + + if (options.ephemeral) { + return message.author.send(replyOptions); + } + + if (isEdit) { + if (message.lastBotReply && !message.lastBotReply.deleted) { + return message.lastBotReply.edit(replyOptions); + } else { + return message.channel.send(replyOptions); + } + } else { + // For new replies, store the sent message + const sentMessage = await message.reply(replyOptions); + message.lastBotReply = sentMessage; + return sentMessage; + } +} diff --git a/src/utils/getAllFiles.js b/src/utils/getAllFiles.js index 6ed7152..5907f0d 100644 --- a/src/utils/getAllFiles.js +++ b/src/utils/getAllFiles.js @@ -9,28 +9,28 @@ import path from 'path'; * @returns {string[]} - List of file or directory paths. */ const getAllFiles = (directory, foldersOnly = false) => { - const items = []; + const stack = [directory]; + const result = []; - const files = fs.readdirSync(directory, { withFileTypes: true }); + while (stack.length > 0) { + const currentPath = stack.pop(); + const items = fs.readdirSync(currentPath, { withFileTypes: true }); - for (const file of files) { - const filePath = path.join(directory, file.name); + for (const item of items) { + const fullPath = path.join(currentPath, item.name); - if (foldersOnly) { - if (file.isDirectory()) { - items.push(filePath); - items.push(...getAllFiles(filePath, foldersOnly)); // Recursively get subfolders - } - } else { - if (file.isDirectory()) { - items.push(...getAllFiles(filePath, foldersOnly)); // Recursively get files and subfolders - } else if (file.isFile()) { - items.push(filePath); + if (item.isDirectory()) { + if (foldersOnly) { + result.push(fullPath); + } + stack.push(fullPath); + } else if (!foldersOnly && item.isFile()) { + result.push(fullPath); } } } - return items; + return result; }; export default getAllFiles; diff --git a/src/utils/getLocalCommands.js b/src/utils/getLocalCommands.js index b55132b..48b73bb 100644 --- a/src/utils/getLocalCommands.js +++ b/src/utils/getLocalCommands.js @@ -4,62 +4,55 @@ import getAllFiles from './getAllFiles.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -export default async (exceptions = []) => { - const localCommands = []; - const commandCategories = getAllFiles( - path.join(__dirname, '..', 'commands'), - true - ); - - const importCommandFiles = async (commandCategory) => { - const commandFiles = getAllFiles(commandCategory); - - return Promise.all( - commandFiles.map(async (commandFile) => { - try { - const commandFileURL = pathToFileURL(commandFile).href; - const commandModule = await import(commandFileURL); +/** + * Imports a single command file and returns the command object if valid. + * @param {string} commandFile - Path to the command file. + * @param {string[]} exceptions - List of command names to exclude. + * @returns {Object|null} The command object or null if invalid. + */ +async function importCommandFile(commandFile, exceptions) { + try { + const commandFileURL = pathToFileURL(commandFile).href; + const commandModule = await import(commandFileURL); + + if (!commandModule?.default?.data?.name) { + console.warn(`Command file ${commandFile} is invalid.`); + return null; + } - if (!commandModule || !commandModule.default) { - console.warn( - `Command file ${commandFile} does not have a default export.` - ); - return null; - } + const commandObject = commandModule.default; - const commandObject = commandModule.default; + if (exceptions.includes(commandObject.data.name)) { + console.warn( + `Command ${commandObject.data.name} is in the exception list.` + ); + return null; + } - if ( - commandObject.data && - commandObject.data.name && - !exceptions.includes(commandObject.data.name) - ) { - return commandObject; - } else { - console.warn( - `Command file ${commandFile} does not have a valid name property or is in the exc list` - ); - return null; - } - } catch (error) { - console.error( - `Error importing command file ${commandFile}: ${error.message}` - ); - return null; - } - }) + return commandObject; + } catch (error) { + console.error( + `Error importing command file ${commandFile}: ${error.message}` ); - }; - - const allCommands = await Promise.all( - commandCategories.map(importCommandFiles) + return null; + } +} + +/** + * Loads all valid command files from the commands directory. + * @param {string[]} exceptions - List of command names to exclude. + * @returns {Promise} Array of valid command objects. + */ +export default async function loadCommands(exceptions = []) { + const commandsDir = path.join(__dirname, '..', 'commands'); + const allCommandFiles = getAllFiles(commandsDir).filter((file) => + file.endsWith('.js') ); - allCommands.flat().forEach((command) => { - if (command) { - localCommands.push(command); - } - }); + const commandPromises = allCommandFiles.map((file) => + importCommandFile(file, exceptions) + ); + const commands = await Promise.all(commandPromises); - return localCommands; -}; + return commands.filter(Boolean); +} diff --git a/src/utils/stock/generateInitialPrice.js b/src/utils/stock/generateInitialPrice.js deleted file mode 100644 index 9064e38..0000000 --- a/src/utils/stock/generateInitialPrice.js +++ /dev/null @@ -1,56 +0,0 @@ -import axios from 'axios'; - -// Alpha Vantage API key (replace with your own API key) -const ALPHA_VANTAGE_API_KEY = ''; - -// List of example stock symbols to fetch data from -const stockSymbols = [ - 'AAPL', - 'MSFT', - 'GOOGL', - 'AMZN', - 'FB', - 'TSLA', - 'BRK.A', - 'V', - 'JNJ', - 'WMT', -]; - -// Function to fetch a random stock price -const fetchRandomStockPrice = async () => { - try { - // Select a random stock symbol from the list - const randomSymbol = - stockSymbols[Math.floor(Math.random() * stockSymbols.length)]; - - // Fetch stock data from Alpha Vantage - const response = await axios.get(`https://www.alphavantage.co/query`, { - params: { - function: 'TIME_SERIES_INTRADAY', - symbol: randomSymbol, - interval: '5min', - apikey: ALPHA_VANTAGE_API_KEY, - }, - }); - - // Extract the latest stock price - const timeSeries = response.data['Time Series (5min)']; - const latestTime = Object.keys(timeSeries)[0]; - const latestPrice = parseFloat(timeSeries[latestTime]['4. close']); - - return latestPrice; - } catch (error) { - console.error('Error fetching stock price:', error); - // Return a default random price in case of an error - return Math.floor(Math.random() * (500 - 50 + 1)) + 50; - } -}; - -// Function to generate an initial price for a stock -const generateInitialPrice = async () => { - const price = await fetchRandomStockPrice(); - return price; -}; - -export default generateInitialPrice;