| import { |
| ActionRowBuilder, |
| APIMessageComponentEmoji, |
| ButtonBuilder, |
| ButtonInteraction, |
| ButtonStyle, |
| Collection, |
| CommandInteraction, |
| GuildMember, |
| Message, |
| ModalBuilder, |
| ModalSubmitInteraction, |
| PermissionsBitField, |
| Role, |
| RoleSelectMenuBuilder, |
| RoleSelectMenuInteraction, |
| SlashCommandSubcommandBuilder, |
| StringSelectMenuBuilder, |
| StringSelectMenuInteraction, |
| StringSelectMenuOptionBuilder, |
| TextInputBuilder, |
| TextInputStyle |
| } from "discord.js"; |
| import client from "../../utils/client.js"; |
| import createPageIndicator, { createVerticalTrack } from "../../utils/createPageIndicator.js"; |
| import { LoadingEmbed } from "../../utils/defaults.js"; |
| import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; |
| import getEmojiByName from "../../utils/getEmojiByName.js"; |
| import ellipsis from "../../utils/ellipsis.js"; |
| import { modalInteractionCollector } from "../../utils/dualCollector.js"; |
| |
| const { renderRole } = client.logger; |
| |
| const command = (builder: SlashCommandSubcommandBuilder) => |
| builder.setName("tracks").setDescription("Manage the tracks for the server"); |
| |
| interface ObjectSchema { |
| name: string; |
| retainPrevious: boolean; |
| nullable: boolean; |
| track: string[]; |
| manageableBy: string[]; |
| } |
| |
| const editName = async ( |
| i: ButtonInteraction, |
| interaction: StringSelectMenuInteraction | ButtonInteraction, |
| m: Message, |
| current?: string |
| ) => { |
| let name = current ?? ""; |
| const modal = new ModalBuilder() |
| .setTitle("Edit Name and Description") |
| .setCustomId("editNameDescription") |
| .addComponents( |
| new ActionRowBuilder<TextInputBuilder>().addComponents( |
| new TextInputBuilder() |
| .setLabel("Name") |
| .setCustomId("name") |
| .setPlaceholder("The name of the track (e.g. Moderators)") |
| .setStyle(TextInputStyle.Short) |
| .setValue(name) |
| .setRequired(true) |
| ) |
| ); |
| 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("Tracks") |
| .setDescription("Modal opened. If you can't see it, click back and try again.") |
| .setStatus("Success") |
| ], |
| components: [button] |
| }); |
| |
| let out: ModalSubmitInteraction | null; |
| try { |
| out = (await modalInteractionCollector(m, interaction.user)) as ModalSubmitInteraction | null; |
| } catch (e) { |
| console.error(e); |
| out = null; |
| } |
| if (!out) return name; |
| if (out.isButton()) return name; |
| name = out.fields.fields.find((f) => f.customId === "name")?.value ?? name; |
| return name; |
| }; |
| |
| const reorderTracks = async ( |
| interaction: ButtonInteraction, |
| m: Message, |
| roles: Collection<string, Role>, |
| currentObj: string[] |
| ) => { |
| const reorderRow = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents( |
| new StringSelectMenuBuilder() |
| .setCustomId("reorder") |
| .setPlaceholder("Select all roles in the order you want users to gain them (Lowest to highest rank).") |
| .setMinValues(currentObj.length) |
| .setMaxValues(currentObj.length) |
| .addOptions( |
| currentObj.map((o, i) => |
| new StringSelectMenuOptionBuilder().setLabel(roles.get(o)!.name).setValue(i.toString()) |
| ) |
| ) |
| ); |
| const 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("Tracks") |
| .setDescription("Select all roles in the order you want users to gain them (Lowest to highest rank).") |
| .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; |
| const values = out.values; |
| |
| const newOrder: string[] = currentObj.map((_, i) => { |
| const index = values.findIndex((v) => v === i.toString()); |
| return currentObj[index]; |
| }) as string[]; |
| |
| return newOrder; |
| }; |
| |
| const editTrack = async ( |
| interaction: ButtonInteraction | StringSelectMenuInteraction, |
| message: Message, |
| roles: Collection<string, Role>, |
| current?: ObjectSchema |
| ) => { |
| const isAdmin = (interaction.member!.permissions as PermissionsBitField).has("Administrator"); |
| if (!current) { |
| current = { |
| name: "", |
| retainPrevious: false, |
| nullable: false, |
| track: [], |
| manageableBy: [] |
| }; |
| } |
| |
| const roleSelect = new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents( |
| new RoleSelectMenuBuilder().setCustomId("addRole").setPlaceholder("Select a role to add").setDisabled(!isAdmin) |
| ); |
| let closed = false; |
| do { |
| const editableRoles: string[] = current.track |
| .map((r) => { |
| if ( |
| !(roles.get(r)!.position >= (interaction.member as GuildMember).roles.highest.position) || |
| interaction.user.id === interaction.guild?.ownerId |
| ) |
| return roles.get(r)!.name; |
| }) |
| .filter((v) => v !== undefined) as string[]; |
| const selectMenu = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents( |
| new StringSelectMenuBuilder() |
| .setCustomId("removeRole") |
| .setPlaceholder("Select a role to remove") |
| .setDisabled(!isAdmin) |
| .addOptions( |
| editableRoles.map((r, i) => { |
| return new StringSelectMenuOptionBuilder().setLabel(r).setValue(i.toString()); |
| }) |
| ) |
| ); |
| 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 Name") |
| .setStyle(ButtonStyle.Primary) |
| .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji), |
| new ButtonBuilder() |
| .setCustomId("reorder") |
| .setLabel("Reorder") |
| .setDisabled(!isAdmin) |
| .setStyle(ButtonStyle.Primary) |
| .setEmoji(getEmojiByName("ICONS.REORDER", "id") as APIMessageComponentEmoji), |
| new ButtonBuilder() |
| .setCustomId("retainPrevious") |
| .setLabel("Retain Previous") |
| .setStyle(current.retainPrevious ? ButtonStyle.Success : ButtonStyle.Danger) |
| .setEmoji( |
| getEmojiByName( |
| "CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"), |
| "id" |
| ) as APIMessageComponentEmoji |
| ), |
| new ButtonBuilder() |
| .setCustomId("nullable") |
| .setLabel(`Role ${current.nullable ? "Not " : ""}Required`) |
| .setStyle(current.nullable ? ButtonStyle.Success : ButtonStyle.Danger) |
| .setEmoji( |
| getEmojiByName("CONTROL." + (current.nullable ? "TICK" : "CROSS"), "id") as APIMessageComponentEmoji |
| ) |
| ); |
| |
| const allowed: boolean[] = []; |
| for (const role of current.track) { |
| const disabled: boolean = |
| roles.get(role)!.position >= (interaction.member as GuildMember).roles.highest.position; |
| allowed.push(disabled); |
| } |
| const mapped = current.track.map((role) => roles.find((aRole) => aRole.id === role)!); |
| |
| const embed = new EmojiEmbed() |
| .setTitle("Tracks") |
| .setDescription( |
| `**Currently Editing:** ${current.name}\n\n` + |
| `${getEmojiByName("CONTROL." + (current.nullable ? "CROSS" : "TICK"))} Members ${ |
| current.nullable ? "don't " : "" |
| }need a role in this track\n` + |
| `${getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"))} Members ${ |
| current.retainPrevious ? "" : "don't " |
| }keep all roles below their current highest\n\n` + |
| createVerticalTrack( |
| mapped.map((role) => renderRole(role)), |
| new Array(current.track.length).fill(false), |
| allowed |
| ) |
| ) |
| .setStatus("Success"); |
| |
| const comps: ActionRowBuilder<RoleSelectMenuBuilder | ButtonBuilder | StringSelectMenuBuilder>[] = [ |
| roleSelect, |
| buttons |
| ]; |
| if (current.track.length >= 1) comps.splice(1, 0, selectMenu); |
| |
| interaction.editReply({ embeds: [embed], components: comps }); |
| |
| let out: ButtonInteraction | RoleSelectMenuInteraction | StringSelectMenuInteraction | null; |
| |
| try { |
| out = (await message.awaitMessageComponent({ |
| filter: (i) => i.channel!.id === interaction.channel!.id, |
| time: 300000 |
| })) as ButtonInteraction | RoleSelectMenuInteraction | StringSelectMenuInteraction | null; |
| } catch (e) { |
| console.error(e); |
| out = null; |
| } |
| |
| if (!out) return; |
| if (out.isButton()) { |
| switch (out.customId) { |
| case "back": { |
| out.deferUpdate(); |
| closed = true; |
| break; |
| } |
| case "edit": { |
| current.name = (await editName(out, interaction, message, current.name))!; |
| break; |
| } |
| case "reorder": { |
| out.deferUpdate(); |
| current.track = (await reorderTracks(out, message, roles, current.track))!; |
| break; |
| } |
| case "retainPrevious": { |
| out.deferUpdate(); |
| current.retainPrevious = !current.retainPrevious; |
| break; |
| } |
| case "nullable": { |
| out.deferUpdate(); |
| current.nullable = !current.nullable; |
| break; |
| } |
| } |
| } else if (out.isStringSelectMenu()) { |
| out.deferUpdate(); |
| switch (out.customId) { |
| case "removeRole": { |
| const index = current.track.findIndex( |
| (v) => v === editableRoles[parseInt((out! as StringSelectMenuInteraction).values![0]!)] |
| ); |
| current.track.splice(index, 1); |
| break; |
| } |
| } |
| } else { |
| switch (out.customId) { |
| case "addRole": { |
| const role = out.values![0]!; |
| if (!current.track.includes(role)) { |
| current.track.push(role); |
| } else { |
| out.reply({ content: "That role is already on this track", ephemeral: true }); |
| } |
| break; |
| } |
| } |
| } |
| } while (!closed); |
| return current; |
| }; |
| |
| const callback = async (interaction: CommandInteraction) => { |
| const m = await interaction.reply({ embeds: LoadingEmbed, fetchReply: true, ephemeral: true }); |
| const config = await client.database.guilds.read(interaction.guild!.id); |
| const tracks: ObjectSchema[] = config.tracks; |
| const roles = await interaction.guild!.roles.fetch(); |
| |
| let page = 0; |
| let closed = false; |
| let modified = false; |
| |
| do { |
| const embed = new EmojiEmbed().setTitle("Track Settings").setEmoji("TRACKS.ICON").setStatus("Success"); |
| const noTracks = config.tracks.length === 0; |
| let current: ObjectSchema; |
| |
| const pageSelect = new StringSelectMenuBuilder().setCustomId("page").setPlaceholder("Select a track to manage"); |
| const actionSelect = new StringSelectMenuBuilder() |
| .setCustomId("action") |
| .setPlaceholder("Perform an action") |
| .addOptions( |
| new StringSelectMenuOptionBuilder() |
| .setLabel("Edit") |
| .setDescription("Edit this track") |
| .setValue("edit") |
| .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji), |
| new StringSelectMenuOptionBuilder() |
| .setLabel("Delete") |
| .setDescription("Delete this track") |
| .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 === tracks.length - 1), |
| new ButtonBuilder() |
| .setCustomId("add") |
| .setLabel("New Track") |
| .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji) |
| .setStyle(ButtonStyle.Secondary) |
| .setDisabled(Object.keys(tracks).length >= 24), |
| new ButtonBuilder() |
| .setCustomId("save") |
| .setLabel("Save") |
| .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji) |
| .setStyle(ButtonStyle.Success) |
| .setDisabled(!modified) |
| ); |
| if (noTracks) { |
| embed.setDescription( |
| "No tracks 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 tracks").setValue("none")); |
| } else { |
| page = Math.min(page, Object.keys(tracks).length - 1); |
| current = tracks[page]!; |
| const mapped = current.track.map((role) => roles.find((aRole) => aRole.id === role)!); |
| embed.setDescription( |
| `**Currently Editing:** ${current.name}\n\n` + |
| `${getEmojiByName("CONTROL." + (current.nullable ? "CROSS" : "TICK"))} Members ${ |
| current.nullable ? "don't " : "" |
| }need a role in this track\n` + |
| `${getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"))} Members ${ |
| current.retainPrevious ? "" : "don't " |
| }keep all roles below their current highest\n\n` + |
| createVerticalTrack( |
| mapped.map((role) => renderRole(role)), |
| new Array(current.track.length).fill(false) |
| ) + |
| `\n${createPageIndicator(config.tracks.length, page)}` |
| ); |
| |
| pageSelect.addOptions( |
| tracks.map((key: ObjectSchema, index) => { |
| return new StringSelectMenuOptionBuilder() |
| .setLabel(ellipsis(key.name, 50)) |
| .setDescription(ellipsis(roles.get(key.track[0]!)?.name!, 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; |
| continue; |
| } |
| |
| await i.deferUpdate(); |
| if (i.isButton()) { |
| switch (i.customId) { |
| case "back": { |
| page--; |
| break; |
| } |
| case "next": { |
| page++; |
| break; |
| } |
| case "add": { |
| const newPage = await editTrack(i, m, roles); |
| if (!newPage) break; |
| tracks.push(); |
| page = tracks.length - 1; |
| break; |
| } |
| case "save": { |
| client.database.guilds.write(interaction.guild!.id, { tracks: tracks }); |
| modified = false; |
| await client.memory.forceUpdate(interaction.guild!.id); |
| break; |
| } |
| } |
| } else if (i.isStringSelectMenu()) { |
| switch (i.customId) { |
| case "action": { |
| switch (i.values[0]) { |
| case "edit": { |
| const edited = await editTrack(i, m, roles, current!); |
| if (!edited) break; |
| tracks[page] = edited; |
| modified = true; |
| break; |
| } |
| case "delete": { |
| if (page === 0 && tracks.keys.length - 1 > 0) page++; |
| else page--; |
| tracks.splice(page, 1); |
| break; |
| } |
| } |
| break; |
| } |
| case "page": { |
| page = parseInt(i.values[0]!); |
| break; |
| } |
| } |
| } |
| } while (!closed); |
| await interaction.deleteReply(); |
| }; |
| |
| const check = (interaction: CommandInteraction, _partial: boolean = false) => { |
| const member = interaction.member as GuildMember; |
| if (!member.permissions.has("ManageRoles")) |
| return "You must have the *Manage Server* permission to use this command"; |
| return true; |
| }; |
| |
| export { command }; |
| export { callback }; |
| export { check }; |