blob: cbc5f177c960b276da3d0cc43e16aac6fc87dd0f [file] [log] [blame]
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: true,
track: [],
manageableBy: []
};
}
const roleSelect = new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(
new RoleSelectMenuBuilder().setCustomId("addRole").setPlaceholder("Select a role to add").setDisabled(!isAdmin)
);
let closed = false;
let previousMessage = "";
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.Danger : ButtonStyle.Success)
.setEmoji(
getEmojiByName("CONTROL." + (current.nullable ? "CROSS" : "TICK"), "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` + (previousMessage ? previousMessage + "\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 {
out.deferUpdate();
switch (out.customId) {
case "addRole": {
const role = out.values![0]!;
const roleObj = roles.get(role)!;
if (roleObj.position >= (interaction.member as GuildMember).roles.highest.position){
previousMessage = "You can't add a role that is higher than your highest role.";
} else {
if (!current.track.includes(role)) {
current.track.push(role);
await interaction.editReply({ embeds: LoadingEmbed, components: [] });
} else {
previousMessage = "That role is already on this track";
}
}
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 };