// Source code of the slash command "help" const { SlashCommandBuilder, EmbedBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, } = require("@discordjs/builders"); const { ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageButton, MessageActionRow, } = require("discord.js"); const fs = require("node:fs"); const path = require("node:path"); const { handleError } = require("../../utils/errorHandling.js"); // These values will be used in the menu object const author = { name: "/help", iconURL: "https://i.postimg.cc/rwXj33rv/sonnnn.png", }; const color = [50, 222, 138]; // Absolute path of the command directory const commandsDir = path.resolve("./src/commands/"); // An object that stores all the categories and pages of the Help Menu let menu = { init: [ new EmbedBuilder() .setAuthor(author) .setTitle("Select a category of commands") .setColor([50, 222, 138]) .addFields( { name: "Fun Commands", value: "Slash commands with fun purposes, such as /kill, /nuke and /yeet.", }, { name: "Utility Commands", value: "Useful slash commands, such as /info and /invites.", }, { name: "Welcome-Goodbye Commands", value: "Slash commands dedicated to personalize the welcome-goodbye features, such as the Welcome Image and the Goodbye message.", } ) .setFooter({ text: "Page 1/1. \nHave you found any issues? Or... any suggestions? Join this server to post your reports and your ideas! https://discord.gg/wRtZ6fRhZC", }), ], }; // ======================================================================================================================================================================== // Dynamically adding the commands in the help menu // This part is executed when the bot is launched try { const commandFolders = fs.readdirSync(commandsDir); for (const folder of commandFolders) { const commandsPath = path.join(commandsDir, folder); // commands folder path const files = fs // Array of all .js command files in the folder .readdirSync(commandsPath) .filter((file) => file.endsWith(".js")); // A file index to help me easily track the number of files fetched let fileIndex = 0; // A variable that keeps me tracked with the embed page in which I should add command details let embedIndex = 0; // Retrieve the number of files in a folder const folderLength = fs.readdirSync(commandsPath).length; for (const file of files) { fileIndex += 1; // The following part will be IGNORED if the fileIndex does not match the conditions to create a new page // Create a new page at each time we reach 5 commands // e.g : 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 // ^ ^ ^ if (fileIndex % 5 == 1) { // Embed page creation // Create an empty array if not created if (menu[`${folder}`] == undefined) menu[`${folder}`] = []; menu[`${folder}`].push( new EmbedBuilder() .setAuthor(author) .setTitle( folder.charAt(0).toUpperCase().toString() + folder.slice(1).replace("_", "-") + " Commands" ) .setColor(color) ); // Update the embed index if the current index is not 0 // (increasing it in this case would cause it to be 1 index // higher than the expected one and would cause errors) menu[`${folder}`].length <= 1 ? (embedIndex = embedIndex) : embedIndex++; // The setFooter part is seperated from the main embed creation code in order to avoid incorrect paging issues menu[`${folder}`][embedIndex].setFooter({ text: `Page ${embedIndex + 1}/${ // Determine the number of pages that will be created folderLength.length % 5 == 0 ? folderLength / 5 : Math.ceil(folderLength / 5) }. \nTo report bugs or to post ideas, please visit the following server at #bugs-report. https://discord.gg/wRtZ6fRhZC`, }); } // Import datas of the command file const command = require(path.join(commandsPath, file)); // Add fields that contain the name and description of every command into embeds if ("name" in command && "description" in command) { menu[`${folder}`][embedIndex].addFields({ name: `/${command.name}`, value: command.description, }); } else { // In case the command file is incomplete console.log( `[WARNING] Help Generation - Name and Description not found in ${file} \n` ); } } } console.log( `${new Date().toString()} [HELP COMMAND] Help menu generated successfully. \n` ); } catch (err) { handleError(undefined, err); } // ============================================================================================================================= module.exports = { data: new SlashCommandBuilder() .setName("help") .setDescription( "Are you confused about how to use this bot? Well, this command will help you out!" ), async execute(interaction) { // Two values that will help me to easily track the page index and help category let currentPage = 0; let currentCategory = menu.init; // It is temporarily put with an array of one value in order to make its length 1. The category will change to a real category (e.g: menu.init) with further interactions. // Build the select menu const selectMenu = new StringSelectMenuBuilder() .setCustomId("menu") .setPlaceholder("Select the category of help you want to receive.") .addOptions( (selection_fun = new StringSelectMenuOptionBuilder() .setLabel("Fun") .setDescription( "Fun slash commands, such as /nuke and /yeet." ) .setValue("fun")), (selection_utils = new StringSelectMenuOptionBuilder() .setLabel("Utilities") .setDescription( "Utility commands, such as /invites and /info." ) .setValue("utils")), (selection_welcomeBye = new StringSelectMenuOptionBuilder() .setLabel("Welcome-Goodbye") .setDescription( "Commands dedicated to setup the welcome-goodbye system." ) .setValue("welcomebye")) ); // Place the select menu into the action row const selectMenuRow = new ActionRowBuilder().addComponents(selectMenu); // Creating the buttons and place them into the action row const buttonsRow = new ActionRowBuilder().addComponents( (firstBtn = new ButtonBuilder() .setCustomId("first") .setLabel("First Page") .setStyle(ButtonStyle.Primary) .setDisabled(true)), (previousBtn = new ButtonBuilder() .setCustomId("previous") .setLabel("⬅️") .setStyle(ButtonStyle.Primary) .setDisabled(true)), (nextBtn = new ButtonBuilder() .setCustomId("next") .setLabel("➡️") .setStyle(ButtonStyle.Primary)).setDisabled(true), (lastBtn = new ButtonBuilder() .setCustomId("last") .setLabel("Last Page") .setStyle(ButtonStyle.Primary) .setDisabled(true)) ); // An array of buttons to ease the loops const buttons = [previousBtn, nextBtn, firstBtn, lastBtn]; // Filter for the interaction collector const filter = (i) => i.deferred == false && !i.user.bot; try { // Sending the embed message, the drop menu and the buttons as a reply const response = await interaction.reply({ embeds: [menu.init[0]], components: [selectMenuRow, buttonsRow], }); // Interaction collector const collector = response.createMessageComponentCollector({ filter: filter, time: 120000, }); // Handling received interactions from the collector collector.on("collect", async (i) => { if (i.user.id == interaction.user.id) { // If the interaction received is not from the buttons nor from the select menu, do nothing. if (!i.isButton() && !i.isStringSelectMenu()) return; // Defer the reply since the command takes longer time than usual to complete await i.deferUpdate(); // Button handling starts here if (i.isButton()) { // If the user is still on the starting menu, disable all the buttons if (currentCategory == menu.init) { for (const i of buttons) { await i.setDisabled(true); } // User is not on the starting menu } else { // Update the page according to the interaction of the user switch (i.customId) { case "previous": currentPage -= 1; break; case "next": currentPage += 1; break; case "first": currentPage = 0; break; case "last": currentPage = currentCategory.length - 1; break; default: handleError( i, "Unknown button interaction." ); } // Disabled and enable certain buttons to avoid errors // First page if (currentPage == 0) { await previousBtn.setDisabled(true); await firstBtn.setDisabled(true); // Re-enable in case disabled await nextBtn.setDisabled(false); await lastBtn.setDisabled(false); // Last page } else if ( currentPage == currentCategory.length - 1 ) { await nextBtn.setDisabled(true); await lastBtn.setDisabled(true); // Re-enable in case disabled await previousBtn.setDisabled(false); await firstBtn.setDisabled(false); // Middle pages } else { // Reenable all buttons in case disabled for (const button of buttons) { await button.setDisabled(false); } } console.log("BTN:", currentPage); // Update the embed message; await i.editReply({ embeds: [currentCategory[currentPage]], components: [selectMenuRow, buttonsRow], }); } // Select Menu Handling } else if (i.isStringSelectMenu()) { // i.values[0] is the command category that the user had chosen switch (i.values[0]) { case "fun": currentCategory = menu.fun; break; case "utils": currentCategory = menu.utilities; break; case "welcomebye": currentCategory = menu.welcome_goodbye; break; default: currentCategory = menu.init; break; } currentPage = 0; // Reset the page number // Disable the previous and next buttons to avoid unexpected interactions await firstBtn.setDisabled(true); await previousBtn.setDisabled(true); // If there are more than one page of help in that category, allow the user to move on the next pages if (currentCategory.length > 1) { await nextBtn.setDisabled(false); await lastBtn.setDisabled(false); // If there is only one page of help, disable all the buttons to prevent causing errors } else { await nextBtn.setDisabled(true); await lastBtn.setDisabled(true); } await interaction.editReply({ embeds: [currentCategory[currentPage]], components: [selectMenuRow, buttonsRow], }); } // A non-command-caller user tries to interact with the command } else { await i.deferUpdate(); await i.followUp({ content: `This is not for you.`, ephemeral: true, }); } }); collector.on("end", async () => { // Disable the buttons to avoid making the users interact with buttons which can no longer receiving interactions for (const button of buttons) await button .setStyle(ButtonStyle.Secondary) .setDisabled(true); // Refresh page await interaction.editReply({ embeds: [currentCategory[currentPage]], components: [selectMenuRow, buttonsRow], }); return; }); } catch (err) { // An error has occured; handleError(interaction, err); } }, };