From c222962106ed063b70b0de85b6732eae1b0c80ce Mon Sep 17 00:00:00 2001 From: NorySight <114584253+GrishMahat@users.noreply.github.com> Date: Sat, 27 Jul 2024 12:18:32 +0000 Subject: [PATCH] Add prefix command --- src/commands/{misc => admin}/disableping.js | 1 + src/commands/{misc => admin}/jointoCreate.js | 1 + src/commands/{misc => admin}/purge.js | 2 + src/commands/{misc => admin}/welcome.js | 1 + src/commands/developer/createitem.js | 2 + src/commands/developer/delitem.js | 1 + src/commands/developer/dm.js | 1 + src/commands/developer/eco.js | 1 + src/commands/developer/servers.js | 1 + src/commands/developer/setPrefix.js | 107 +++++++++ src/commands/economy/balance.js | 1 + src/commands/economy/bank.js | 1 + src/commands/economy/beg.js | 1 + src/commands/economy/coinflip.js | 1 + src/commands/economy/crime.js | 1 + src/commands/economy/daily.js | 1 + src/commands/economy/deposit.js | 1 + src/commands/economy/hourly.js | 1 + src/commands/economy/inventory.js | 1 + src/commands/economy/items.js | 1 + src/commands/economy/leaderboard.js | 1 + src/commands/economy/shop.js | 2 + src/commands/economy/slots.js | 1 + src/commands/economy/weekly.js | 1 + src/commands/economy/withdraw.js | 1 + src/commands/image/cat.js | 2 + src/commands/image/dog.js | 2 + src/commands/image/magik.js | 1 + src/commands/misc/avatar.js | 1 + src/commands/misc/fact.js | 2 + src/commands/misc/guild.js | 1 + src/commands/misc/help.js | 220 ++++++++++++++----- src/commands/misc/ping.js | 1 + src/commands/misc/prefix.js | 98 +++++++++ src/commands/ticket/close-all-tickets.js | 1 + src/commands/ticket/ticket-close.js | 1 + src/commands/ticket/ticketAddMember.js | 1 + src/commands/ticket/ticketRemoveMenber.js | 1 + src/commands/ticket/ticketSetup.js | 1 + src/events/messageCreate/command.js | 218 ++++++++++++++++++ src/events/voiceStateUpdate/joinToCreate.js | 79 ++++++- src/schemas/prefix.js | 18 ++ src/utils/SlashCommandtoPrifix.js | 218 ++++++++++++++++++ src/utils/stock/generateInitialPrice.js | 56 ----- 44 files changed, 935 insertions(+), 121 deletions(-) rename src/commands/{misc => admin}/disableping.js (99%) rename src/commands/{misc => admin}/jointoCreate.js (99%) rename src/commands/{misc => admin}/purge.js (98%) rename src/commands/{misc => admin}/welcome.js (99%) create mode 100644 src/commands/developer/setPrefix.js create mode 100644 src/commands/misc/prefix.js create mode 100644 src/events/messageCreate/command.js create mode 100644 src/schemas/prefix.js create mode 100644 src/utils/SlashCommandtoPrifix.js delete mode 100644 src/utils/stock/generateInitialPrice.js 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 7169fb4..56c86de 100644 --- a/src/commands/misc/avatar.js +++ b/src/commands/misc/avatar.js @@ -25,6 +25,7 @@ export default { 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 918518a..54b65a9 100644 --- a/src/commands/misc/fact.js +++ b/src/commands/misc/fact.js @@ -19,6 +19,8 @@ export default { 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 cd0acee..0ae52b2 100644 --- a/src/commands/misc/help.js +++ b/src/commands/misc/help.js @@ -1,24 +1,24 @@ import { SlashCommandBuilder, EmbedBuilder, - AutocompleteInteraction, 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; -const COMMANDS_PER_PAGE = 10; // Adjust as necessary +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) => { return text.match(new RegExp(`.{1,${length}}`, 'g')) || []; }; -// Function to categorize commands const categorizeCommands = (commands) => { const categories = {}; commands.forEach((cmd) => { @@ -57,6 +57,7 @@ export default { nsfwMode: false, testMode: false, devOnly: false, + category: 'Misc', async autocomplete(client, interaction) { const focusedOption = interaction.options.getFocused(true); @@ -64,7 +65,9 @@ export default { if (focusedOption.name === 'command') { const filtered = localCommands.filter((cmd) => - cmd.data.name.startsWith(focusedOption.value) + cmd.data.name + .toLowerCase() + .startsWith(focusedOption.value.toLowerCase()) ); await interaction.respond( filtered.map((cmd) => ({ @@ -105,30 +108,14 @@ export default { embedColor ); } else if (category) { - const categoryCommands = localCommands.filter( - (cmd) => (cmd.category || 'Uncategorized') === category - ); - const commandsText = categoryCommands - .map( - (cmd) => - `\`${cmd.data.name}\`: ${cmd.data.description || 'No description available.'}` - ) - .join('\n'); - - const textChunks = splitText(commandsText, MAX_DESCRIPTION_LENGTH); - const pages = textChunks.map((chunk, index) => - new EmbedBuilder() - .setTitle(`Commands in ${category}`) - .setDescription(chunk) - .setColor(embedColor) - .setFooter({ - text: `Page ${index + 1} of ${textChunks.length}`, - }) + return await showCategoryCommands( + interaction, + localCommands, + category, + embedColor ); - - return paginate(interaction, pages); } else { - return await showCommandList( + return await showCommandOverview( interaction, localCommands, embedColor @@ -150,7 +137,9 @@ async function showCommandDetails( commandName, embedColor ) { - const command = localCommands.find((cmd) => cmd.data.name === commandName); + const command = localCommands.find( + (cmd) => cmd.data.name.toLowerCase() === commandName.toLowerCase() + ); if (!command) { return interaction.reply({ content: 'Command not found.', @@ -200,7 +189,9 @@ async function showCommandDetails( const optionsText = command.data.options .map( (opt) => - `â€ĸ **${opt.name}**: ${opt.description} ${opt.required ? '*(Required)*' : ''}` + `â€ĸ **${opt.name}**: ${opt.description} ${ + opt.required ? '*(Required)*' : '' + }` ) .join('\n'); embed.addFields({ @@ -210,13 +201,109 @@ async function showCommandDetails( }); } - return interaction.reply({ embeds: [embed], ephemeral: true }); + 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 showCommandList(interaction, localCommands, embedColor) { - const categories = [ - ...new Set(localCommands.map((cmd) => cmd.category || 'Uncategorized')), - ]; +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() @@ -231,41 +318,60 @@ async function showCommandList(interaction, localCommands, embedColor) { ) ); - const initialEmbed = new EmbedBuilder() - .setTitle('📚 Command Categories') - .setDescription( - 'Select a category from the dropdown menu below to view its commands.' - ) - .setColor(embedColor); - const message = await interaction.reply({ - embeds: [initialEmbed], + embeds: [overviewEmbed], components: [categorySelect], - ephemeral: true, }); - const collector = message.createMessageComponentCollector({ time: 300000 }); // 5 minutes + 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]; - const categoryCommands = localCommands.filter( - (cmd) => (cmd.category || 'Uncategorized') === selectedCategory - ); - const pages = createCommandPages( - categoryCommands, + await showCategoryCommands( + i, + localCommands, selectedCategory, embedColor ); - await paginate(i, pages); } }); collector.on('end', () => { - interaction.editReply({ components: [] }); + 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) { @@ -291,14 +397,20 @@ function createCommandPages(commands, category, embedColor) { } function getCategoryEmoji(category) { - // Add more emojis for different categories const emojiMap = { Uncategorized: '📁', - ticket: '🛡ī¸', + 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 0c224fe..4876247 100644 --- a/src/commands/misc/ping.js +++ b/src/commands/misc/ping.js @@ -18,6 +18,7 @@ export default { 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/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/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/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/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;