blob: 333dbdcdb530290b3aafcda57eb668329ad34d63 [file] [log] [blame]
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 _ from "lodash";
const cross = getEmojiByName("CONTROL.CROSS");
const tick = getEmojiByName("CONTROL.TICK");
const isEqual = _.isEqual;
const command = (builder: SlashCommandSubcommandBuilder) =>
builder.setName("rolemenu").setDescription("Allows you to change settings for the servers rolemenu");
interface ObjectSchema {
enabled?: boolean;
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[]) => {
const 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()))
)
);
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("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;
await out.deferUpdate();
if (out.isButton()) 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; bounds?: { min: number; max: number } },
addBounds: boolean = false
): Promise<[string | undefined, string | undefined, { min: number, max: number } | undefined]> => {
let { name, description, bounds } = data;
const components = [
new ActionRowBuilder<TextInputBuilder>().addComponents(
new TextInputBuilder()
.setLabel("Name")
.setCustomId("name")
.setPlaceholder("The name of the role (e.g. Programmer)")
.setStyle(TextInputStyle.Short)
.setValue(name ?? "")
.setRequired(true)
.setMaxLength(100)
),
new ActionRowBuilder<TextInputBuilder>().addComponents(
new TextInputBuilder()
.setLabel("Description")
.setCustomId("description")
.setPlaceholder("A short description of the role (e.g. A role for people who code)")
.setStyle(TextInputStyle.Short)
.setValue(description ?? "")
.setRequired(false)
.setMaxLength(100)
)
]
if (addBounds && bounds) {
components.push(
new ActionRowBuilder<TextInputBuilder>().addComponents(
new TextInputBuilder()
.setLabel("Minimum")
.setCustomId("min")
.setPlaceholder("The minimum number of roles a user can select")
.setStyle(TextInputStyle.Short)
.setValue(bounds.min.toString())
.setRequired(true)
.setMaxLength(2)
),
new ActionRowBuilder<TextInputBuilder>().addComponents(
new TextInputBuilder()
.setLabel("Maximum")
.setCustomId("max")
.setPlaceholder("The maximum number of roles a user can select (0 for no limit)")
.setStyle(TextInputStyle.Short)
.setValue(bounds.max.toString())
.setRequired(true)
.setMaxLength(2)
)
)
}
const modal = new ModalBuilder()
.setTitle("Edit Name and Description")
.setCustomId("editNameDescription")
.addComponents(
...components
);
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, interaction.user)) as Discord.ModalSubmitInteraction | null;
} catch (e) {
console.error(e);
out = null;
}
if (!out) return [name, description, bounds];
if (out.isButton()) return [name, description, bounds];
name = out.fields.fields.find((f) => f.customId === "name")?.value ?? name;
description = out.fields.fields.find((f) => f.customId === "description")?.value;
if (addBounds) {
const min = parseInt(out.fields.fields.find((f) => f.customId === "min")?.value!);
const max = parseInt(out.fields.fields.find((f) => f.customId === "max")?.value!);
bounds = { min, max };
}
return [name, description, bounds];
};
const defaultRoleMenuData = {
enabled: true,
name: "New Page",
description: "",
min: 0,
max: 1,
options: []
};
const editRoleMenuPage = async (
interaction: StringSelectMenuInteraction | ButtonInteraction,
m: Message,
data?: ObjectSchema
): Promise<ObjectSchema | null> => {
if (!data) data = _.cloneDeep(defaultRoleMenuData);
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;
do {
const noRoles = data.options.length === 0;
const previewSelect = configToDropdown(
"Edit Roles",
data.description ? {
name: data.name,
description: data.description,
min: 1,
max: 1,
options: noRoles ? [{ name: "Role 1", description: null, role: "No role set" }] : data.options
} : {
name: data.name,
min: 1,
max: 1,
options: noRoles ? [{ name: "Role 1", description: null, role: "No role set" }] : data.options
},
undefined,
noRoles
);
const embed = new EmojiEmbed()
.setTitle(`${data.name}`)
.setStatus("Success")
.setDescription(
`**Description:**\n> ${data.description ?? "*No description set*"}\n\n` +
`**Min:** ${data.min}` +
(data.min === 0 ? " (Members will be given a skip button)" : "") +
"\n" +
`**Max:** ${data.max}\n` +
`\n**Roles:** ${data.options.length === 0 ? "*No roles set*" : data.options.length}`
);
await 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]),
false
);
}
} else if (i.isButton()) {
switch (i.customId) {
case "back": {
await i.deferUpdate();
back = true;
break;
}
case "edit": {
const [name, description, _bounds] = await editNameDescription(i, interaction, m, data);
data.name = name ? name : data.name;
description ? data.description = description : null;
break;
}
case "addRole": {
await i.deferUpdate();
const out = await createRoleMenuOptionPage(interaction, m, undefined, true);
if (out) data.options.push(out);
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 },
newRole: boolean = false
) => {
const initialData = _.cloneDeep(data);
const { renderRole } = client.logger;
if (!data)
data = {
name: "New role Menu Option",
description: null,
role: ""
};
let back = false;
const buttons = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId("back")
.setLabel(newRole ? "Add" : "Back")
.setStyle(newRole ? ButtonStyle.Success : ButtonStyle.Secondary)
.setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji),
new ButtonBuilder()
.setCustomId("edit")
.setLabel("Edit Details")
.setStyle(ButtonStyle.Primary)
.setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
new ButtonBuilder()
.setCustomId("delete")
.setLabel("Delete")
.setStyle(ButtonStyle.Danger)
.setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji),
new ButtonBuilder()
.setCustomId("cancel")
.setLabel("Cancel")
.setStyle(ButtonStyle.Secondary)
.setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
);
do {
const roleSelect = new RoleSelectMenuBuilder()
.setCustomId("role")
.setPlaceholder(data.role ? "Change role to" : "Select a role");
const embed = new EmojiEmbed()
.setTitle(`${data.name}`)
.setStatus("Success")
.setDescription(
`**Description:**\n> ${data.description ?? "No description set"}\n\n` +
`**Role:** ${
data.role ? renderRole((await interaction.guild!.roles.fetch(data.role))!) : "No role set"
}\n`
);
await 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]!;
await interaction.editReply({
embeds: [
new EmojiEmbed().setTitle(`Applying changes`).setStatus("Danger").setEmoji("NUCLEUS.LOADING")
],
components: []
});
}
} else if (i.isButton()) {
switch (i.customId) {
case "back": {
await i.deferUpdate();
back = true;
break;
}
case "edit": {
const [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;
}
case "delete": {
await i.deferUpdate();
return null;
}
case "cancel": {
await i.deferUpdate();
if (newRole) return null;
else return initialData;
}
}
}
} 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);
const currentObject: typeof config.roleMenu = _.cloneDeep(config.roleMenu);
let modified = false;
do {
const embed = new EmojiEmbed().setTitle("Role Menu").setEmoji("GUILD.GREEN").setStatus("Success");
const noRoleMenus = currentObject.options.length === 0;
let current: ObjectSchema;
const pageSelect = new StringSelectMenuBuilder()
.setCustomId("page")
.setPlaceholder("Select a Role Menu page to manage");
let actionSelect;
if (page === 0) {
actionSelect = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId("switch")
.setLabel(currentObject.enabled ? "Enabled" : "Disabled")
.setStyle(currentObject.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
.setEmoji(
getEmojiByName(
currentObject.enabled ? "CONTROL.TICK" : "CONTROL.CROSS",
"id"
) as APIMessageComponentEmoji
)
);
} else {
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.options).length || noRoleMenus),
new ButtonBuilder()
.setCustomId("add")
.setLabel("New Page")
.setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
.setStyle(ButtonStyle.Secondary)
.setDisabled(Object.keys(currentObject.options).length >= 24),
new ButtonBuilder()
.setCustomId("reorder")
.setLabel("Reorder Pages")
.setEmoji(getEmojiByName("ICONS.REORDER", "id") as APIMessageComponentEmoji)
.setStyle(ButtonStyle.Secondary)
.setDisabled(Object.keys(currentObject.options).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);
if (page > 0) (actionSelect as StringSelectMenuBuilder).setDisabled(true);
pageSelect.addOptions(new StringSelectMenuOptionBuilder().setLabel("No role menu pages").setValue("none"));
} else if (page === 0) {
embed.setDescription(
`**Enabled:** ${currentObject.enabled ? `${tick} Yes` : `${cross} No`}\n\n` +
`**Pages:** ${currentObject.options.length}\n` +
(currentObject.options.length > 0
? currentObject.options
.map((key: ObjectSchema) => {
const crossOut = key.enabled ? "" : "~~";
return `> ${crossOut}**${key.name}:** ${key.description ?? "*No description set*"}${crossOut}`;
})
.join("\n")
: "")
);
if (currentObject.options.length > 0) {
pageSelect.addOptions(
currentObject.options.map((key: ObjectSchema, index) => {
return new StringSelectMenuOptionBuilder()
.setLabel(ellipsis(key.name, 50))
.setDescription(
ellipsis(
key.description?.length
? key.description.length > 0
? key.description
: "No description set"
: "No description set",
50
)
)
.setValue((index + 1).toString());
})
);
} else {
pageSelect.setDisabled(true);
pageSelect.addOptions(
new StringSelectMenuOptionBuilder().setLabel("No role menu pages").setValue("none")
);
}
} else {
page = Math.max(Math.min(page, currentObject.options.length), 0);
current = currentObject.options[page - 1]!;
(actionSelect as StringSelectMenuBuilder).addOptions(
new StringSelectMenuOptionBuilder()
.setLabel(current.enabled ? "Disable" : "Enable")
.setDescription("Enable or disable this page")
.setValue("switch")
.setEmoji(
getEmojiByName(
current.enabled ? "CONTROL.CROSS" : "CONTROL.TICK",
"id"
) as APIMessageComponentEmoji
)
)
embed.setDescription(
`**Currently Editing:** ${current.name}\n\n` +
`**Visible:** ${current.enabled ? `${tick} Yes` : `${cross} No`}\n\n` +
`**Description:**\n> ${current.description ?? "*No description set*"}\n` +
`\n\n${createPageIndicator(Object.keys(currentObject.options).length, page - 1, false, Object.values(currentObject.options).map((o) => !(o.enabled ?? true)))}`
);
pageSelect.addOptions(
currentObject.options.map((key: ObjectSchema, index) => {
return new StringSelectMenuOptionBuilder()
.setLabel(ellipsis(key.name, 50))
.setDescription(
ellipsis(
key.description?.length
? key.description.length > 0
? key.description
: "No description set"
: "No description set",
50
)
)
.setValue((index + 1).toString());
})
);
}
await interaction.editReply({
embeds: [embed],
components: [
page === 0
? (actionSelect as ActionRowBuilder<ButtonBuilder>)
: new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
actionSelect as StringSelectMenuBuilder
),
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect),
buttonRow as ActionRowBuilder<ButtonBuilder>
]
});
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 editRoleMenuPage(i, m);
if (_.isEqual(newPage, defaultRoleMenuData)) {
break;
}
if (newPage) {
currentObject.options.push(newPage);
page = currentObject.options.length;
modified = true;
}
break;
}
case "reorder": {
const reordered = await reorderRoleMenuPages(interaction, m, currentObject.options);
if (!reordered) break;
currentObject.options = reordered;
modified = true;
break;
}
case "save": {
await client.database.guilds.write(interaction.guild.id, {
"roleMenu": currentObject
});
modified = false;
await client.memory.forceUpdate(interaction.guild.id);
break;
}
case "switch": {
currentObject.enabled = !currentObject.enabled;
modified = true;
break;
}
}
} else if (i.isStringSelectMenu()) {
switch (i.customId) {
case "action": {
switch (i.values[0]) {
case "edit": {
const edited = await editRoleMenuPage(i, m, current!);
if (!edited) break;
currentObject.options[page - 1] = edited;
modified = true;
break;
}
case "delete": {
if (page === 0 && currentObject.options.keys.length - 1 > 0) page++;
else page--;
currentObject.options.splice(page, 1);
modified = true;
break;
}
case "switch": {
currentObject.options[page - 1]!.enabled = !currentObject.options[page - 1]!.enabled;
modified = true;
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 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 };