mirror of https://github.com/clickscodes/nucleus
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
461 lines
19 KiB
461 lines
19 KiB
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 };
|