| import type Discord from "discord.js"; |
| import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonInteraction, ButtonStyle, CommandInteraction, Message, ModalBuilder, RoleSelectMenuBuilder, RoleSelectMenuInteraction, StringSelectMenuBuilder, StringSelectMenuInteraction, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; |
| import type { SlashCommandSubcommandBuilder } from "discord.js"; |
| import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; |
| import { LoadingEmbed } from "../../utils/defaults.js"; |
| import client from "../../utils/client.js"; |
| import getEmojiByName from "../../utils/getEmojiByName.js"; |
| import createPageIndicator from "../../utils/createPageIndicator.js"; |
| import { configToDropdown } from "../../actions/roleMenu.js"; |
| import { modalInteractionCollector } from "../../utils/dualCollector.js"; |
| import ellipsis from "../../utils/ellipsis.js"; |
| import lodash from 'lodash'; |
| |
| const isEqual = lodash.isEqual; |
| |
| const command = (builder: SlashCommandSubcommandBuilder) => |
| builder |
| .setName("rolemenu") |
| .setDescription("rolemenu") |
| |
| interface ObjectSchema { |
| name: string; |
| description: string; |
| min: number; |
| max: number; |
| options: { |
| name: string; |
| description: string | null; |
| role: string; |
| }[]; |
| } |
| |
| const defaultRolePageConfig = { |
| name: "Role Menu Page", |
| description: "A new role menu page", |
| min: 0, |
| max: 0, |
| options: [ |
| {name: "Role 1", description: null, role: "No role set"} |
| ] |
| } |
| |
| const reorderRoleMenuPages = async (interaction: CommandInteraction, m: Message, currentObj: ObjectSchema[]) => { |
| let reorderRow = new ActionRowBuilder<StringSelectMenuBuilder>() |
| .addComponents( |
| new StringSelectMenuBuilder() |
| .setCustomId("reorder") |
| .setPlaceholder("Select all pages in the order you want them to appear.") |
| .setMinValues(currentObj.length) |
| .setMaxValues(currentObj.length) |
| .addOptions( |
| currentObj.map((o, i) => new StringSelectMenuOptionBuilder() |
| .setLabel(o.name) |
| .setValue(i.toString()) |
| ) |
| ) |
| ); |
| let buttonRow = new ActionRowBuilder<ButtonBuilder>() |
| .addComponents( |
| new ButtonBuilder() |
| .setCustomId("back") |
| .setLabel("Back") |
| .setStyle(ButtonStyle.Secondary) |
| .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji) |
| ) |
| await interaction.editReply({ |
| embeds: [ |
| new EmojiEmbed() |
| .setTitle("Role Menu") |
| .setDescription("Select pages in the order you want them to appear.") |
| .setStatus("Success") |
| ], |
| components: [reorderRow, buttonRow] |
| }); |
| let out: StringSelectMenuInteraction | ButtonInteraction | null; |
| try { |
| out = await m.awaitMessageComponent({ |
| filter: (i) => i.channel!.id === interaction.channel!.id, |
| time: 300000 |
| }) as StringSelectMenuInteraction | ButtonInteraction | null; |
| } catch (e) { |
| console.error(e); |
| out = null; |
| } |
| if(!out) return; |
| out.deferUpdate(); |
| if (out.isButton()) return; |
| if(!out.values) return; |
| const values = out.values; |
| |
| const newOrder: ObjectSchema[] = currentObj.map((_, i) => { |
| const index = values.findIndex(v => v === i.toString()); |
| return currentObj[index]; |
| }) as ObjectSchema[]; |
| |
| return newOrder; |
| } |
| |
| const editNameDescription = async (i: ButtonInteraction, interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data: {name?: string, description?: string}) => { |
| |
| let {name, description} = data; |
| const modal = new ModalBuilder() |
| .setTitle("Edit Name and Description") |
| .setCustomId("editNameDescription") |
| .addComponents( |
| new ActionRowBuilder<TextInputBuilder>() |
| .addComponents( |
| new TextInputBuilder() |
| .setLabel("Name") |
| .setCustomId("name") |
| .setPlaceholder("Name here...") // TODO: Make better placeholder |
| .setStyle(TextInputStyle.Short) |
| .setValue(name ?? "") |
| .setRequired(true) |
| ), |
| new ActionRowBuilder<TextInputBuilder>() |
| .addComponents( |
| new TextInputBuilder() |
| .setLabel("Description") |
| .setCustomId("description") |
| .setPlaceholder("Description here...") // TODO: Make better placeholder |
| .setStyle(TextInputStyle.Short) |
| .setValue(description ?? "") |
| ) |
| ) |
| const button = new ActionRowBuilder<ButtonBuilder>() |
| .addComponents( |
| new ButtonBuilder() |
| .setCustomId("back") |
| .setLabel("Back") |
| .setStyle(ButtonStyle.Secondary) |
| .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji) |
| ) |
| |
| await i.showModal(modal) |
| await interaction.editReply({ |
| embeds: [ |
| new EmojiEmbed() |
| .setTitle("Role Menu") |
| .setDescription("Modal opened. If you can't see it, click back and try again.") |
| .setStatus("Success") |
| ], |
| components: [button] |
| }); |
| |
| let out: Discord.ModalSubmitInteraction | null; |
| try { |
| out = await modalInteractionCollector( |
| m, |
| (m) => m.channel!.id === interaction.channel!.id, |
| (_) => true |
| ) as Discord.ModalSubmitInteraction | null; |
| } catch (e) { |
| console.error(e); |
| out = null; |
| } |
| if(!out) return [name, description]; |
| if (out.isButton()) return [name, description]; |
| if(!out.fields) return [name, description]; |
| name = out.fields.fields.find((f) => f.customId === "name")?.value ?? name; |
| description = out.fields.fields.find((f) => f.customId === "description")?.value ?? description; |
| return [name, description] |
| |
| } |
| |
| const editRoleMenuPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: ObjectSchema): Promise<ObjectSchema | null> => { |
| if (!data) data = { |
| name: "Role Menu Page", |
| description: "A new role menu page", |
| min: 0, |
| max: 0, |
| options: [] |
| }; |
| const buttons = new ActionRowBuilder<ButtonBuilder>() |
| .addComponents( |
| new ButtonBuilder() |
| .setCustomId("back") |
| .setLabel("Back") |
| .setStyle(ButtonStyle.Secondary) |
| .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji), |
| new ButtonBuilder() |
| .setCustomId("edit") |
| .setLabel("Edit") |
| .setStyle(ButtonStyle.Primary) |
| .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji), |
| new ButtonBuilder() |
| .setCustomId("addRole") |
| .setLabel("Add Role") |
| .setStyle(ButtonStyle.Secondary) |
| .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji) |
| ); |
| |
| let back = false |
| if(data.options.length === 0) { |
| data.options = [ |
| {name: "Role 1", description: null, role: "No role set"} |
| ] |
| } |
| do { |
| const previewSelect = configToDropdown("Edit Roles", {name: data.name, description: data.description, min: 1, max: 1, options: data.options}); |
| const embed = new EmojiEmbed() |
| .setTitle(`${data.name}`) |
| .setStatus("Success") |
| .setDescription( |
| `**Description:**\n> ${data.description}\n\n` + |
| `**Min:** ${data.min}` + (data.min === 0 ? " (Members will be given a skip button)" : "") + "\n" + |
| `**Max:** ${data.max}\n` |
| ) |
| |
| interaction.editReply({embeds: [embed], components: [previewSelect, buttons]}); |
| let i: StringSelectMenuInteraction | ButtonInteraction; |
| try { |
| i = await m.awaitMessageComponent({ time: 300000, filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id && i.channelId === interaction.channelId}) as ButtonInteraction | StringSelectMenuInteraction; |
| } catch (e) { |
| back = true; |
| break; |
| } |
| |
| if (i.isStringSelectMenu()) { |
| if(i.customId === "roles") { |
| await i.deferUpdate(); |
| await createRoleMenuOptionPage(interaction, m, data.options.find((o) => o.role === (i as StringSelectMenuInteraction).values[0])); |
| } |
| } else if (i.isButton()) { |
| switch (i.customId) { |
| case "back": |
| await i.deferUpdate(); |
| back = true; |
| break; |
| case "edit": |
| let [name, description] = await editNameDescription(i, interaction, m, data); |
| data.name = name ? name : data.name; |
| data.description = description ? description : data.description; |
| break; |
| case "addRole": |
| await i.deferUpdate(); |
| data.options.push(await createRoleMenuOptionPage(interaction, m)); |
| break; |
| } |
| } |
| |
| } while (!back); |
| if(isEqual(data, defaultRolePageConfig)) return null; |
| return data; |
| } |
| |
| const createRoleMenuOptionPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: {name: string; description: string | null; role: string}) => { |
| const { renderRole} = client.logger; |
| if (!data) data = { |
| name: "Role Menu Option", |
| description: null, |
| role: "No role set" |
| }; |
| let back = false; |
| const buttons = new ActionRowBuilder<ButtonBuilder>() |
| .addComponents( |
| new ButtonBuilder() |
| .setCustomId("back") |
| .setLabel("Back") |
| .setStyle(ButtonStyle.Secondary) |
| .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji), |
| new ButtonBuilder() |
| .setCustomId("edit") |
| .setLabel("Edit Details") |
| .setStyle(ButtonStyle.Primary) |
| .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji) |
| ); |
| do { |
| const roleSelect = new RoleSelectMenuBuilder().setCustomId("role").setPlaceholder(data.role ? "Set role to" : "Set the role"); |
| const embed = new EmojiEmbed() |
| .setTitle(`${data.name ?? "New Role Menu Option"}`) |
| .setStatus("Success") |
| .setDescription( |
| `**Description:**\n> ${data.description ?? "No description set"}\n\n` + |
| `**Role:** ${renderRole((await interaction.guild!.roles.fetch(data.role))!) ?? "No role set"}\n` |
| ) |
| |
| interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(roleSelect), buttons]}); |
| |
| let i: RoleSelectMenuInteraction | ButtonInteraction; |
| try { |
| i = await m.awaitMessageComponent({ time: 300000, filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id && i.channelId === interaction.channelId}) as ButtonInteraction | RoleSelectMenuInteraction; |
| } catch (e) { |
| back = true; |
| break; |
| } |
| |
| if (i.isRoleSelectMenu()) { |
| if(i.customId === "role") { |
| await i.deferUpdate(); |
| data.role = (i as RoleSelectMenuInteraction).values[0]!; |
| } |
| } else if (i.isButton()) { |
| switch (i.customId) { |
| case "back": |
| await i.deferUpdate(); |
| back = true; |
| break; |
| case "edit": |
| await i.deferUpdate(); |
| let [name, description] = await editNameDescription(i, interaction, m, data as {name: string; description: string}); |
| data.name = name ? name : data.name; |
| data.description = description ? description : data.description; |
| break; |
| } |
| } |
| } while (!back); |
| return data; |
| } |
| |
| const callback = async (interaction: CommandInteraction): Promise<void> => { |
| if (!interaction.guild) return; |
| const m = await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true}); |
| |
| let page = 0; |
| let closed = false; |
| const config = await client.database.guilds.read(interaction.guild.id); |
| let currentObject: ObjectSchema[] = config.roleMenu.options; |
| let modified = false; |
| do { |
| const embed = new EmojiEmbed() |
| .setTitle("Role Menu") |
| .setEmoji("GUILD.GREEN") |
| .setStatus("Success"); |
| const noRoleMenus = currentObject.length === 0; |
| let current: ObjectSchema; |
| |
| const pageSelect = new StringSelectMenuBuilder() |
| .setCustomId("page") |
| .setPlaceholder("Select a Role Menu page to manage"); |
| const actionSelect = new StringSelectMenuBuilder() |
| .setCustomId("action") |
| .setPlaceholder("Perform an action") |
| .addOptions( |
| new StringSelectMenuOptionBuilder() |
| .setLabel("Edit") |
| .setDescription("Edit this page") |
| .setValue("edit") |
| .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji), |
| new StringSelectMenuOptionBuilder() |
| .setLabel("Delete") |
| .setDescription("Delete this page") |
| .setValue("delete") |
| .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji) |
| ); |
| const buttonRow = new ActionRowBuilder<ButtonBuilder>() |
| .addComponents( |
| new ButtonBuilder() |
| .setCustomId("back") |
| .setStyle(ButtonStyle.Primary) |
| .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji) |
| .setDisabled(page === 0), |
| new ButtonBuilder() |
| .setCustomId("next") |
| .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji) |
| .setStyle(ButtonStyle.Primary) |
| .setDisabled(page === Object.keys(currentObject).length - 1), |
| new ButtonBuilder() |
| .setCustomId("add") |
| .setLabel("New Page") |
| .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji) |
| .setStyle(ButtonStyle.Secondary) |
| .setDisabled(Object.keys(currentObject).length >= 24), |
| new ButtonBuilder() |
| .setCustomId("reorder") |
| .setLabel("Reorder Pages") |
| .setEmoji(getEmojiByName("ICONS.SHUFFLE", "id") as APIMessageComponentEmoji) |
| .setStyle(ButtonStyle.Secondary) |
| .setDisabled(Object.keys(currentObject).length <= 1), |
| new ButtonBuilder() |
| .setCustomId("save") |
| .setLabel("Save") |
| .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji) |
| .setStyle(ButtonStyle.Success) |
| .setDisabled(!modified), |
| ); |
| if(noRoleMenus) { |
| embed.setDescription("No role menu pages have been set up yet. Use the button below to add one.\n\n" + |
| createPageIndicator(1, 1, undefined, true) |
| ); |
| pageSelect.setDisabled(true); |
| actionSelect.setDisabled(true); |
| pageSelect.addOptions(new StringSelectMenuOptionBuilder() |
| .setLabel("No role menu pages") |
| .setValue("none") |
| ); |
| } else { |
| page = Math.min(page, Object.keys(currentObject).length - 1); |
| current = currentObject[page]!; |
| embed.setDescription(`**Currently Editing:** ${current.name}\n\n` + |
| `**Description:**\n> ${current.description}\n` + |
| `\n\n${createPageIndicator(Object.keys(config.roleMenu.options).length, page)}` |
| ); |
| |
| pageSelect.addOptions( |
| currentObject.map((key: ObjectSchema, index) => { |
| return new StringSelectMenuOptionBuilder() |
| .setLabel(ellipsis(key.name, 50)) |
| .setDescription(ellipsis(key.description, 50)) |
| .setValue(index.toString()); |
| }) |
| ); |
| |
| } |
| |
| await interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect), new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect), buttonRow]}); |
| let i: StringSelectMenuInteraction | ButtonInteraction; |
| try { |
| i = await m.awaitMessageComponent({ time: 300000, filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id && i.channelId === interaction.channelId}) as ButtonInteraction | StringSelectMenuInteraction; |
| } catch (e) { |
| closed = true; |
| break; |
| } |
| |
| await i.deferUpdate(); |
| if (i.isButton()) { |
| switch (i.customId) { |
| case "back": |
| page--; |
| break; |
| case "next": |
| page++; |
| break; |
| case "add": |
| let newPage = await editRoleMenuPage(i, m) |
| if(!newPage) break; |
| currentObject.push(); |
| page = currentObject.length - 1; |
| break; |
| case "reorder": |
| let reordered = await reorderRoleMenuPages(interaction, m, currentObject); |
| if(!reordered) break; |
| currentObject = reordered; |
| break; |
| case "save": |
| client.database.guilds.write(interaction.guild.id, {"roleMenu.options": currentObject}); |
| modified = false; |
| break; |
| } |
| } else if (i.isStringSelectMenu()) { |
| switch (i.customId) { |
| case "action": |
| switch(i.values[0]) { |
| case "edit": |
| let edited = await editRoleMenuPage(i, m, current!); |
| if(!edited) break; |
| currentObject[page] = edited; |
| modified = true; |
| break; |
| case "delete": |
| if(page === 0 && currentObject.keys.length - 1 > 0) page++; |
| else page--; |
| currentObject.splice(page, 1); |
| break; |
| } |
| break; |
| case "page": |
| page = parseInt(i.values[0]!); |
| break; |
| } |
| } |
| |
| } while (!closed) |
| }; |
| |
| const check = (interaction: CommandInteraction, _partial: boolean = false) => { |
| const member = interaction.member as Discord.GuildMember; |
| if (!member.permissions.has("ManageRoles")) |
| return "You must have the *Manage Roles* permission to use this command"; |
| return true; |
| }; |
| |
| export { command }; |
| export { callback }; |
| export { check }; |