Development (#11)
We need this NOW.
---------
Co-authored-by: PineaFan <ash@pinea.dev>
Co-authored-by: pineafan <pineapplefanyt@gmail.com>
Co-authored-by: PineappleFan <PineaFan@users.noreply.github.com>
Co-authored-by: Skyler <skyler3665@gmail.com>
diff --git a/src/commands/settings/automod.ts b/src/commands/settings/automod.ts
new file mode 100644
index 0000000..09b8914
--- /dev/null
+++ b/src/commands/settings/automod.ts
@@ -0,0 +1,922 @@
+import type Discord from "discord.js";
+import { ActionRowBuilder,
+ AnySelectMenuInteraction,
+ APIMessageComponentEmoji,
+ ButtonBuilder,
+ ButtonInteraction,
+ ButtonStyle,
+ ChannelSelectMenuBuilder,
+ ChannelSelectMenuInteraction,
+ CommandInteraction,
+ Message,
+ ModalBuilder,
+ RoleSelectMenuBuilder,
+ RoleSelectMenuInteraction,
+ StringSelectMenuBuilder,
+ StringSelectMenuInteraction,
+ StringSelectMenuOptionBuilder,
+ TextInputBuilder,
+ TextInputStyle,
+ UserSelectMenuBuilder,
+ UserSelectMenuInteraction
+} from "discord.js";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
+import { LoadingEmbed } from "../../utils/defaults.js";
+import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
+import client from "../../utils/client.js";
+import getEmojiByName from "../../utils/getEmojiByName.js";
+import { modalInteractionCollector } from "../../utils/dualCollector.js";
+import listToAndMore from "../../utils/listToAndMore.js";
+
+
+const command = (builder: SlashCommandSubcommandBuilder) =>
+ builder.setName("automod").setDescription("Setting for automatic moderation features");
+
+
+const emojiFromBoolean = (bool: boolean, id?: string) => bool ? getEmojiByName("CONTROL.TICK", id) : getEmojiByName("CONTROL.CROSS", id);
+
+const toSelectMenu = async (interaction: StringSelectMenuInteraction, m: Message, ids: string[], type: "member" | "role" | "channel", title: string): Promise<string[]> => {
+
+ const back = new ActionRowBuilder<ButtonBuilder>().addComponents(new ButtonBuilder().setCustomId("back").setLabel("Back").setStyle(ButtonStyle.Secondary).setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji));
+ let closed;
+ do {
+ let render: string[] = []
+ let mapped: string[] = [];
+ let menu: UserSelectMenuBuilder | RoleSelectMenuBuilder | ChannelSelectMenuBuilder;
+ switch(type) {
+ case "member": {
+ menu = new UserSelectMenuBuilder().setCustomId("user").setPlaceholder("Select users").setMaxValues(25);
+ mapped = await Promise.all(ids.map(async (id) => { return (await client.users.fetch(id).then(user => user.tag)) || "Unknown User" }));
+ render = ids.map(id => client.logger.renderUser(id))
+ break;
+ }
+ case "role": {
+ menu = new RoleSelectMenuBuilder().setCustomId("role").setPlaceholder("Select roles").setMaxValues(25);
+ mapped = await Promise.all(ids.map(async (id) => { return (await interaction.guild!.roles.fetch(id).then(role => role?.name ?? "Unknown Role"))}));
+ render = ids.map(id => client.logger.renderRole(id, interaction.guild!))
+ break;
+ }
+ case "channel": {
+ menu = new ChannelSelectMenuBuilder().setCustomId("channel").setPlaceholder("Select channels").setMaxValues(25);
+ mapped = await Promise.all(ids.map(async (id) => { return (await interaction.guild!.channels.fetch(id).then(channel => channel?.name ?? "Unknown Role")) }));
+ render = ids.map(id => client.logger.renderChannel(id))
+ break;
+ }
+ }
+ const removeOptions = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("remove")
+ .setPlaceholder("Remove")
+ .addOptions(mapped.map((name, i) => new StringSelectMenuOptionBuilder().setLabel(name).setValue(ids[i]!)))
+ .setDisabled(ids.length === 0)
+ );
+
+ const embed = new EmojiEmbed()
+ .setTitle(title)
+ .setEmoji(getEmojiByName("GUILD.SETTINGS.GREEN"))
+ .setDescription(`Select ${type}s:\n\nCurrent:\n` + (render.length > 0 ? render.join("\n") : "None"))
+ .setStatus("Success");
+ const components: ActionRowBuilder<
+ StringSelectMenuBuilder |
+ ButtonBuilder |
+ ChannelSelectMenuBuilder |
+ UserSelectMenuBuilder |
+ RoleSelectMenuBuilder
+ >[] = [new ActionRowBuilder<typeof menu>().addComponents(menu)]
+ if(ids.length > 0) components.push(removeOptions);
+ components.push(back);
+
+ await interaction.editReply({embeds: [embed], components: components})
+
+ let i: AnySelectMenuInteraction | ButtonInteraction;
+ try {
+ i = await m.awaitMessageComponent({filter: i => i.user.id === interaction.user.id, time: 300000});
+ } catch(e) {
+ closed = true;
+ continue;
+ }
+
+ if(i.isButton()) {
+ await i.deferUpdate();
+ if(i.customId === "back") {
+ closed = true;
+ break;
+ }
+ } else if(i.isStringSelectMenu()) {
+ await i.deferUpdate();
+ if(i.customId === "remove") {
+ ids = ids.filter(id => id !== (i as StringSelectMenuInteraction).values[0]);
+ if(ids.length === 0) {
+ menu.data.disabled = true;
+ }
+ }
+ } else {
+ await i.deferUpdate();
+ if(i.customId === "user") {
+ ids = ids.concat((i as UserSelectMenuInteraction).values);
+ } else if(i.customId === "role") {
+ ids = ids.concat((i as RoleSelectMenuInteraction).values);
+ } else if(i.customId === "channel") {
+ ids = ids.concat((i as ChannelSelectMenuInteraction).values);
+ }
+ }
+
+ } while(!closed)
+ return ids;
+}
+
+const imageMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
+ NSFW: boolean,
+ size: boolean
+}): Promise<{NSFW: boolean, size: boolean}> => {
+ let closed = false;
+ do {
+ const options = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Secondary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("nsfw")
+ .setLabel("NSFW")
+ .setStyle(current.NSFW ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(emojiFromBoolean(current.NSFW, "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("size")
+ .setLabel("Size")
+ .setStyle(current.size ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(emojiFromBoolean(current.size, "id") as APIMessageComponentEmoji)
+ )
+
+ const embed = new EmojiEmbed()
+ .setTitle("Image Settings")
+ .setDescription(
+ `${emojiFromBoolean(current.NSFW)} **NSFW**\n` +
+ `${emojiFromBoolean(current.size)} **Size**\n`
+ )
+
+ await interaction.editReply({embeds: [embed], components: [options]});
+
+ let i: ButtonInteraction;
+ try {
+ i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction;
+ } catch (e) {
+ return current;
+ }
+ await i.deferUpdate();
+ switch(i.customId) {
+ case "back": {
+ closed = true;
+ break;
+ }
+ case "nsfw": {
+ current.NSFW = !current.NSFW;
+ break;
+ }
+ case "size": {
+ current.size = !current.size;
+ break;
+ }
+ }
+ } while(!closed);
+ return current;
+}
+
+const wordMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
+ enabled: boolean,
+ words: {strict: string[], loose: string[]},
+ allowed: {users: string[], roles: string[], channels: string[]}
+}): Promise<{
+ enabled: boolean,
+ words: {strict: string[], loose: string[]},
+ allowed: {users: string[], roles: string[], channels: string[]}
+}> => {
+ let closed = false;
+ do {
+ 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("enabled")
+ .setLabel("Enabled")
+ .setStyle(current.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(emojiFromBoolean(current.enabled, "id") as APIMessageComponentEmoji),
+ );
+
+ const selectMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("edit")
+ .setPlaceholder("Edit... ")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Words")
+ .setDescription("Edit your list of words to filter")
+ .setValue("words"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Allowed Users")
+ .setDescription("Users who will be unaffected by the word filter")
+ .setValue("allowedUsers"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Allowed Roles")
+ .setDescription("Roles that will be unaffected by the word filter")
+ .setValue("allowedRoles"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Allowed Channels")
+ .setDescription("Channels where the word filter will not apply")
+ .setValue("allowedChannels")
+ )
+ .setDisabled(!current.enabled)
+ );
+
+ const embed = new EmojiEmbed()
+ .setTitle("Word Filters")
+ .setDescription(
+ `${emojiFromBoolean(current.enabled)} **Enabled**\n` +
+ `**Strict Words:** ${listToAndMore(current.words.strict, 5)}\n` +
+ `**Loose Words:** ${listToAndMore(current.words.loose, 5)}\n\n` +
+ `**Users:** ` + listToAndMore(current.allowed.users.map(user => `<@${user}>`), 5) + `\n` +
+ `**Roles:** ` + listToAndMore(current.allowed.roles.map(role => `<@&${role}>`), 5) + `\n` +
+ `**Channels:** ` + listToAndMore(current.allowed.channels.map(channel => `<#${channel}>`), 5)
+ )
+ .setStatus("Success")
+ .setEmoji("GUILD.SETTINGS.GREEN")
+
+ await interaction.editReply({embeds: [embed], components: [selectMenu, buttons]});
+
+ let i: ButtonInteraction | StringSelectMenuInteraction;
+ try {
+ i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ break;
+ }
+
+ if(i.isButton()) {
+ await i.deferUpdate();
+ switch(i.customId) {
+ case "back": {
+ closed = true;
+ break;
+ }
+ case "enabled": {
+ current.enabled = !current.enabled;
+ break;
+ }
+ }
+ } else {
+ switch(i.values[0]) {
+ case "words": {
+ await interaction.editReply({embeds: [new EmojiEmbed()
+ .setTitle("Word Filter")
+ .setDescription("Modal opened. If you can't see it, click back and try again.")
+ .setStatus("Success")
+ .setEmoji("GUILD.SETTINGS.GREEN")
+ ], components: [new ActionRowBuilder<ButtonBuilder>().addComponents(new ButtonBuilder()
+ .setLabel("Back")
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId("back")
+ )]})
+ const modal = new ModalBuilder()
+ .setTitle("Word Filter")
+ .setCustomId("wordFilter")
+ .addComponents(
+ new ActionRowBuilder<TextInputBuilder>()
+ .addComponents(
+ new TextInputBuilder()
+ .setCustomId("wordStrict")
+ .setLabel("Strict Words")
+ .setPlaceholder("Matches anywhere in the message, including surrounded by other characters")
+ .setValue(current.words.strict.join(", "))
+ .setStyle(TextInputStyle.Paragraph)
+ .setRequired(false)
+ ),
+ new ActionRowBuilder<TextInputBuilder>()
+ .addComponents(
+ new TextInputBuilder()
+ .setCustomId("wordLoose")
+ .setLabel("Loose Words")
+ .setPlaceholder("Matches only if the word is by itself, surrounded by spaces or punctuation")
+ .setValue(current.words.loose.join(", "))
+ .setStyle(TextInputStyle.Paragraph)
+ .setRequired(false)
+ )
+ )
+
+ await i.showModal(modal);
+ let out;
+ try {
+ out = await modalInteractionCollector(m, interaction.user);
+ } catch (e) {
+ break;
+ }
+ if (!out) break;
+ if(out.isButton()) break;
+ current.words.strict = out.fields.getTextInputValue("wordStrict")
+ .split(",").map(s => s.trim()).filter(s => s.length > 0);
+ current.words.loose = out.fields.getTextInputValue("wordLoose")
+ .split(",").map(s => s.trim()).filter(s => s.length > 0);
+ break;
+ }
+ case "allowedUsers": {
+ await i.deferUpdate();
+ current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Word Filter");
+ break;
+ }
+ case "allowedRoles": {
+ await i.deferUpdate();
+ current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Word Filter");
+ break;
+ }
+ case "allowedChannels": {
+ await i.deferUpdate();
+ current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Word Filter");
+ break;
+ }
+ }
+ }
+ } while(!closed);
+ return current;
+}
+
+const inviteMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
+ enabled: boolean,
+ allowed: {users: string[], roles: string[], channels: string[]}
+}): Promise<{
+ enabled: boolean,
+ allowed: {users: string[], roles: string[], channels: string[]}
+}> => {
+
+ let closed = false;
+ do {
+ 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("enabled")
+ .setLabel(current.enabled ? "Enabled" : "Disabled")
+ .setStyle(current.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(emojiFromBoolean(current.enabled, "id") as APIMessageComponentEmoji)
+ );
+ const menu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("toEdit")
+ .setPlaceholder("Edit your allow list")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Users")
+ .setDescription("Users that are allowed to send invites")
+ .setValue("users"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Roles")
+ .setDescription("Roles that are allowed to send invites")
+ .setValue("roles"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Channels")
+ .setDescription("Channels that anyone is allowed to send invites in")
+ .setValue("channels")
+ ).setDisabled(!current.enabled)
+ )
+
+ const embed = new EmojiEmbed()
+ .setTitle("Invite Settings")
+ .setDescription(
+ "Automatically deletes invites sent by users (outside of staff members and self promotion channels)" + `\n\n` +
+ `${emojiFromBoolean(current.enabled)} **${current.enabled ? "Enabled" : "Disabled"}**\n\n` +
+ `**Users:** ` + listToAndMore(current.allowed.users.map(user => `<@${user}>`), 5) + `\n` +
+ `**Roles:** ` + listToAndMore(current.allowed.roles.map(role => `<@&${role}>`), 5) + `\n` +
+ `**Channels:** ` + listToAndMore(current.allowed.channels.map(channel => `<#${channel}>`), 5)
+ )
+ .setStatus("Success")
+ .setEmoji("GUILD.SETTINGS.GREEN")
+
+
+ await interaction.editReply({embeds: [embed], components: [menu, buttons]});
+
+ let i: ButtonInteraction | StringSelectMenuInteraction;
+ try {
+ i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction;
+ } catch (e) {
+ return current;
+ }
+
+ if(i.isButton()) {
+ await i.deferUpdate();
+ switch(i.customId) {
+ case "back": {
+ closed = true;
+ break;
+ }
+ case "enabled": {
+ current.enabled = !current.enabled;
+ break;
+ }
+ }
+ } else {
+ await i.deferUpdate();
+ switch(i.values[0]) {
+ case "users": {
+ current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Invite Settings");
+ break;
+ }
+ case "roles": {
+ current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Invite Settings");
+ break;
+ }
+ case "channels": {
+ current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Invite Settings");
+ break;
+ }
+ }
+ }
+
+ } while(!closed);
+ return current;
+}
+
+const mentionMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
+ mass: number,
+ everyone: boolean,
+ roles: boolean,
+ allowed: {
+ roles: string[],
+ rolesToMention: string[],
+ users: string[],
+ channels: string[]
+ }
+}): Promise<{
+ mass: number,
+ everyone: boolean,
+ roles: boolean,
+ allowed: {
+ roles: string[],
+ rolesToMention: string[],
+ users: string[],
+ channels: string[]
+ }
+}> => {
+ let closed = false;
+
+ do {
+
+ 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("everyone")
+ .setLabel(current.everyone ? "Everyone" : "No one")
+ .setStyle(current.everyone ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(emojiFromBoolean(current.everyone, "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("roles")
+ .setLabel(current.roles ? "Roles" : "No roles")
+ .setStyle(current.roles ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(emojiFromBoolean(current.roles, "id") as APIMessageComponentEmoji)
+ );
+ const menu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("toEdit")
+ .setPlaceholder("Edit mention settings")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Mass Mention Amount")
+ .setDescription("The amount of mentions before the bot will delete the message")
+ .setValue("mass"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Roles")
+ .setDescription("Roles that are able to be mentioned")
+ .setValue("roles"),
+ )
+ )
+
+ const allowedMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("allowed")
+ .setPlaceholder("Edit exceptions")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Users")
+ .setDescription("Users that are unaffected by the mention filter")
+ .setValue("users"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Roles")
+ .setDescription("Roles that are unaffected by the mention filter")
+ .setValue("roles"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Channels")
+ .setDescription("Channels where anyone is unaffected by the mention filter")
+ .setValue("channels")
+ )
+ )
+
+ const embed = new EmojiEmbed()
+ .setTitle("Mention Settings")
+ .setDescription(
+ `Log when members mention:\n` +
+ `${emojiFromBoolean(true)} **${current.mass}+ members** in one message\n` +
+ `${emojiFromBoolean(current.everyone)} **Everyone**\n` +
+ `${emojiFromBoolean(current.roles)} **Roles**\n` +
+ (current.allowed.rolesToMention.length > 0 ? `> *Except for ${listToAndMore(current.allowed.rolesToMention.map(r => `<@&${r}>`), 3)}*\n` : "") +
+ "\n" +
+ `Except if...\n` +
+ `> ${current.allowed.users.length > 0 ? `Member is: ${listToAndMore(current.allowed.users.map(u => `<@${u}>`), 3)}\n` : ""}` +
+ `> ${current.allowed.roles.length > 0 ? `Member has role: ${listToAndMore(current.allowed.roles.map(r => `<@&${r}>`), 3)}\n` : ""}` +
+ `> ${current.allowed.channels.length > 0 ? `In channel: ${listToAndMore(current.allowed.channels.map(c => `<#${c}>`), 3)}\n` : ""}`
+ )
+ .setStatus("Success")
+ .setEmoji("GUILD.SETTINGS.GREEN")
+
+ await interaction.editReply({embeds: [embed], components: [menu, allowedMenu, buttons]});
+
+ let i: ButtonInteraction | StringSelectMenuInteraction;
+ try {
+ i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ break;
+ }
+
+ if(i.isButton()) {
+ await i.deferUpdate();
+ switch (i.customId) {
+ case "back": {
+ closed = true;
+ break;
+ }
+ case "everyone": {
+ current.everyone = !current.everyone;
+ break;
+ }
+ case "roles": {
+ current.roles = !current.roles;
+ break;
+ }
+ }
+ } else {
+ switch (i.customId) {
+ case "toEdit": {
+ switch (i.values[0]) {
+ case "mass": {
+ await interaction.editReply({embeds: [new EmojiEmbed()
+ .setTitle("Word Filter")
+ .setDescription("Modal opened. If you can't see it, click back and try again.")
+ .setStatus("Success")
+ .setEmoji("GUILD.SETTINGS.GREEN")
+ ], components: [new ActionRowBuilder<ButtonBuilder>().addComponents(new ButtonBuilder()
+ .setLabel("Back")
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId("back")
+ )]})
+ const modal = new ModalBuilder()
+ .setTitle("Mass Mention Amount")
+ .setCustomId("mass")
+ .addComponents(
+ new ActionRowBuilder<TextInputBuilder>()
+ .addComponents(
+ new TextInputBuilder()
+ .setCustomId("mass")
+ .setPlaceholder("Amount")
+ .setMinLength(1)
+ .setMaxLength(3)
+ .setStyle(TextInputStyle.Short)
+ )
+ )
+ await i.showModal(modal);
+ let out;
+ try {
+ out = await modalInteractionCollector(m, interaction.user);
+ } catch (e) {
+ break;
+ }
+ if (!out) break;
+ if(out.isButton()) break;
+ current.mass = parseInt(out.fields.getTextInputValue("mass"));
+ break;
+ }
+ case "roles": {
+ await i.deferUpdate();
+ current.allowed.rolesToMention = await toSelectMenu(interaction, m, current.allowed.rolesToMention, "role", "Mention Settings");
+ break;
+ }
+ }
+ break;
+ }
+ case "allowed": {
+ await i.deferUpdate();
+ switch (i.values[0]) {
+ case "users": {
+ current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Mention Settings");
+ break;
+ }
+ case "roles": {
+ current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Mention Settings");
+ break;
+ }
+ case "channels": {
+ current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Mention Settings");
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ } while(!closed);
+ return current
+}
+
+const cleanMenu = async (interaction: StringSelectMenuInteraction, m: Message, current?: {
+ channels?: string[],
+ allowed?: {
+ roles: string[],
+ users: string[]
+ }
+}): Promise<{
+ channels: string[],
+ allowed: {
+ roles: string[],
+ users: string[]
+ }
+}> => {
+ let closed = false;
+ if(!current) current = {channels: [], allowed: {roles: [], users: []}};
+ if(!current.channels) current.channels = [];
+ if(!current.allowed) current.allowed = {roles: [], users: []};
+
+ const channelMenu = new ActionRowBuilder<ChannelSelectMenuBuilder>()
+ .addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("toAdd")
+ .setPlaceholder("Select a channel")
+ )
+
+ const allowedMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("allowed")
+ .setPlaceholder("Edit exceptions")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Users")
+ .setDescription("Users that are unaffected by the mention filter")
+ .setValue("users"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Roles")
+ .setDescription("Roles that are unaffected by the mention filter")
+ .setValue("roles")
+ )
+ )
+
+ do {
+
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Primary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
+ )
+
+ const embed = new EmojiEmbed()
+ .setTitle("Clean Settings")
+ .setEmoji("GUILD.SETTINGS.GREEN")
+ .setDescription(
+ `Current clean channels:\n\n` +
+ `${current.channels.length > 0 ? listToAndMore(current.channels.map(c => `<#${c}>`), 10) : "None"}\n\n`
+ )
+ .setStatus("Success")
+
+
+ await interaction.editReply({embeds: [embed], components: [channelMenu, allowedMenu, buttons]});
+
+ let i: ButtonInteraction | ChannelSelectMenuInteraction;
+ try {
+ i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | ChannelSelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ break;
+ }
+ await i.deferUpdate();
+ if(i.isButton()) {
+ switch (i.customId) {
+ case "back": {
+ closed = true;
+ break;
+ }
+ }
+ } else {
+ switch (i.customId) {
+ case "toAdd": {
+ const channelEmbed = new EmojiEmbed()
+ .setTitle("Clean Settings")
+ .setDescription(`Editing <#${i.values[0]}>`)
+ .setEmoji("GUILD.SETTINGS.GREEN")
+ .setStatus("Success")
+ const channelButtons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Primary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id")),
+ new ButtonBuilder()
+ .setCustomId("switch")
+ .setLabel(current.channels.includes(i.values[0]!) ? "Remove" : "Add")
+ .setStyle(current.channels.includes(i.values[0]!) ? ButtonStyle.Danger : ButtonStyle.Success)
+ )
+
+ await i.editReply({embeds: [channelEmbed], components: [channelButtons]});
+ let j: ButtonInteraction;
+ try {
+ j = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction;
+ } catch (e) {
+ closed = true;
+ break;
+ }
+ await j.deferUpdate();
+ switch (j.customId) {
+ case "back": {
+ break;
+ }
+ case "switch": {
+ if(current.channels.includes(i.values[0]!)) {
+ current.channels.splice(current.channels.indexOf(i.values[0]!), 1);
+ } else {
+ current.channels.push(i.values[0]!);
+ }
+ }
+ }
+ break;
+ }
+ case "allowed": {
+ switch (i.values[0]) {
+ case "users": {
+ current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Mention Settings");
+ break;
+ }
+ case "roles": {
+ current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Mention Settings");
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ } while(!closed);
+
+ return current as {
+ channels: string[],
+ allowed: {
+ roles: string[],
+ users: string[]
+ }
+ };
+
+}
+
+const callback = async (interaction: CommandInteraction): Promise<void> => {
+ if (!interaction.guild) return;
+ const m = await interaction.reply({embeds: LoadingEmbed, fetchReply: true, ephemeral: true});
+ const config = (await client.database.guilds.read(interaction.guild.id)).filters;
+
+ let closed = false;
+
+ const button = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setStyle(ButtonStyle.Success)
+ )
+
+ do {
+
+ const selectMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("filter")
+ .setPlaceholder("Select a filter to edit")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Invites")
+ .setDescription("Automatically delete messages containing server invites")
+ .setValue("invites"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Mentions")
+ .setDescription("Deletes messages with excessive mentions")
+ .setValue("mentions"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Words")
+ .setDescription("Delete messages containing filtered words")
+ .setValue("words"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Malware")
+ .setDescription("Automatically delete files and links containing malware")
+ .setValue("malware"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Images")
+ .setDescription("Checks performed on images (NSFW, size checking, etc.)")
+ .setValue("images"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Clean")
+ .setDescription("Automatically delete new messages in specific channels")
+ .setValue("clean")
+ )
+ );
+
+ const embed = new EmojiEmbed()
+ .setTitle("Automod Settings")
+ .setDescription(
+ `${emojiFromBoolean(config.invite.enabled)} **Invites**\n` +
+ `${emojiFromBoolean(config.pings.everyone || config.pings.mass > 0 || config.pings.roles)} **Mentions**\n` +
+ `${emojiFromBoolean(config.wordFilter.enabled)} **Words**\n` +
+ `${emojiFromBoolean(config.malware)} **Malware**\n` +
+ `${emojiFromBoolean(config.images.NSFW || config.images.size)} **Images**\n` +
+ `${emojiFromBoolean(config.clean.channels.length > 0)} **Clean**\n`
+ )
+ .setStatus("Success")
+ .setEmoji("GUILD.SETTINGS.GREEN")
+
+
+ await interaction.editReply({embeds: [embed], components: [selectMenu, button]});
+
+ let i: StringSelectMenuInteraction | ButtonInteraction;
+ try {
+ i = await m.awaitMessageComponent({filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id, time: 300000}) as StringSelectMenuInteraction | ButtonInteraction;
+ } catch (e) {
+ closed = true;
+ continue;
+ }
+ await i.deferUpdate();
+ if(i.isButton()) {
+ await client.database.guilds.write(interaction.guild.id, {filters: config});
+ } else {
+ switch(i.values[0]) {
+ case "invites": {
+ config.invite = await inviteMenu(i, m, config.invite);
+ break;
+ }
+ case "mentions": {
+ config.pings = await mentionMenu(i, m, config.pings);
+ break;
+ }
+ case "words": {
+ config.wordFilter = await wordMenu(i, m, config.wordFilter);
+ break;
+ }
+ case "malware": {
+ config.malware = !config.malware;
+ break;
+ }
+ case "images": {
+ const next = await imageMenu(i, m, config.images);
+ config.images = next;
+ break;
+ }
+ case "clean": {
+ const next = await cleanMenu(i, m, config.clean);
+ config.clean = next;
+ break;
+ }
+ }
+ }
+
+ } while(!closed);
+ await interaction.deleteReply()
+
+};
+
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
+ const member = interaction.member as Discord.GuildMember;
+ if (!member.permissions.has("ManageMessages"))
+ return "You must have the *Manage Messages* permission to use this command";
+ return true;
+};
+
+export { command };
+export { callback };
+export { check };
diff --git a/src/commands/settings/autopublish.ts b/src/commands/settings/autopublish.ts
new file mode 100644
index 0000000..1dc97e0
--- /dev/null
+++ b/src/commands/settings/autopublish.ts
@@ -0,0 +1,96 @@
+import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, CommandInteraction, SlashCommandSubcommandBuilder } from "discord.js";
+import type Discord from "discord.js";
+import client from "../../utils/client.js";
+import { LoadingEmbed } from "../../utils/defaults.js";
+import compare from "lodash"
+import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
+
+export const command = new SlashCommandSubcommandBuilder()
+ .setName("autopublish")
+ .setDescription("Automatically publish messages posted in announcement channels");
+
+export const callback = async (interaction: CommandInteraction): Promise<void> => {
+ await interaction.reply({
+ embeds: LoadingEmbed,
+ ephemeral: true,
+ fetchReply: true
+ });
+
+ let closed = false;
+ let config = await client.database.guilds.read(interaction.guild!.id);
+ let data = Object.assign({}, config.autoPublish);
+ do {
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("switch")
+ .setLabel(data.enabled ? "Disabled" : "Enabled")
+ .setStyle(data.enabled ? ButtonStyle.Danger : ButtonStyle.Success)
+ .setEmoji(data.enabled ? "✅" : "❌"),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setStyle(ButtonStyle.Success)
+ .setEmoji("💾")
+ .setDisabled(compare.isEqual(data, config.autoPublish))
+ );
+
+ const channelSelect = new ActionRowBuilder<ChannelSelectMenuBuilder>()
+ .addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("channel")
+ .setPlaceholder("Select a channel")
+ .setMinValues(1)
+ );
+
+ const embed = new EmojiEmbed()
+
+ await interaction.editReply({
+ embeds: [embed],
+ components: [channelSelect, buttons]
+ });
+
+ let i: Discord.ButtonInteraction | Discord.ChannelSelectMenuInteraction;
+ try {
+ i = await interaction.channel!.awaitMessageComponent({
+ filter: (i: Discord.Interaction) => i.user.id === interaction.user.id,
+ time: 300000
+ }) as Discord.ButtonInteraction | Discord.ChannelSelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ continue;
+ }
+
+ if(i.isButton()) {
+ switch(i.customId) {
+ case "switch": {
+ data.enabled = !data.enabled;
+ break;
+ }
+ case "save": {
+ await client.database.guilds.write(interaction.guild!.id, { "autoPublish": data });
+ config = await client.database.guilds.read(interaction.guild!.id);
+ data = Object.assign({}, config.autoPublish);
+ break;
+ }
+ }
+ } else {
+ for(const channel of i.values) {
+ data.channels.includes(channel) ? data.channels.splice(data.channels.indexOf(channel), 1) : data.channels.push(channel);
+ }
+ }
+
+ } while (!closed);
+
+ await interaction.deleteReply();
+}
+
+export const check = (interaction: CommandInteraction, _partial: boolean = false) => {
+ const member = interaction.member as Discord.GuildMember;
+ const me = interaction.guild!.members.me!;
+ if (!member.permissions.has("ManageMessages"))
+ return "You must have the *Manage Messages* permission to use this command";
+ if (_partial) return true;
+ if (!me.permissions.has("ManageMessages")) return "I do not have the *Manage Messages* permission";
+ return true;
+};
diff --git a/src/commands/settings/filters.ts b/src/commands/settings/filters.ts
deleted file mode 100644
index 2e6f4c5..0000000
--- a/src/commands/settings/filters.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type Discord from "discord.js";
-import type { CommandInteraction } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
-
-const command = (builder: SlashCommandSubcommandBuilder) =>
- builder.setName("filter").setDescription("Setting for message filters");
-
-const callback = async (_interaction: CommandInteraction): Promise<void> => {
- console.log("Filters");
-};
-
-const check = (interaction: CommandInteraction) => {
- const member = interaction.member as Discord.GuildMember;
- if (!member.permissions.has("ManageMessages"))
- return "You must have the *Manage Messages* permission to use this command";
- return true;
-};
-
-export { command };
-export { callback };
-export { check };
diff --git a/src/commands/settings/logs/attachment.ts b/src/commands/settings/logs/attachment.ts
index 2709bee..238b8b9 100644
--- a/src/commands/settings/logs/attachment.ts
+++ b/src/commands/settings/logs/attachment.ts
@@ -1,198 +1,110 @@
import { LoadingEmbed } from "../../../utils/defaults.js";
-import { ChannelType } from "discord-api-types/v9";
-import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonInteraction } from "discord.js";
+import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, ChannelType } from "discord.js";
import EmojiEmbed from "../../../utils/generateEmojiEmbed.js";
-import confirmationMessage from "../../../utils/confirmationMessage.js";
import getEmojiByName from "../../../utils/getEmojiByName.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import client from "../../../utils/client.js";
+import { getCommandMentionByName } from "../../../utils/getCommandDataByName.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
.setName("attachments")
.setDescription("Where attachments should be logged to (Premium only)")
- .addChannelOption((option) =>
- option
- .setName("channel")
- .setDescription("The channel to log attachments in")
- .addChannelTypes(ChannelType.GuildText)
- .setRequired(false)
- );
const callback = async (interaction: CommandInteraction): Promise<unknown> => {
- const m = (await interaction.reply({
+ if (interaction.guild) client.database.premium.hasPremium(interaction.guild.id).finally(() => {});
+ await interaction.reply({
embeds: LoadingEmbed,
ephemeral: true,
fetchReply: true
- })) as Discord.Message;
- if (interaction.options.get("channel")?.channel) {
- let channel;
- try {
- channel = interaction.options.get("channel")?.channel;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("CHANNEL.TEXT.DELETE")
- .setTitle("Attachment Log Channel")
- .setDescription("The channel you provided is not a valid channel")
- .setStatus("Danger")
- ]
- });
- }
- channel = channel as Discord.TextChannel;
- if (channel.guild.id !== interaction.guild!.id) {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Attachment Log Channel")
- .setDescription("You must choose a channel in this server")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ]
- });
- }
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("CHANNEL.TEXT.EDIT")
- .setTitle("Attachment Log Channel")
- .setDescription(
- "This will be the channel all attachments will be sent to.\n\n" +
- `Are you sure you want to set the attachment log channel to <#${channel.id}>?`
- )
- .setColor("Warning")
- .setFailedMessage("Attachment log channel not set", "Warning", "CHANNEL.TEXT.DELETE")
- .setInverted(true)
- .send(true);
- if (confirmation.cancelled) return;
- if (confirmation.success) {
- try {
- await client.database.guilds.write(interaction.guild!.id, {
- "logging.attachments.channel": channel.id
- });
- const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
- const data = {
- meta: {
- type: "attachmentChannelUpdate",
- displayName: "Attachment Log Channel Updated",
- calculateType: "nucleusSettingsUpdated",
- color: NucleusColors.yellow,
- emoji: "CHANNEL.TEXT.EDIT",
- timestamp: new Date().getTime()
- },
- list: {
- memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
- changedBy: entry(interaction.user.id, renderUser(interaction.user)),
- channel: entry(channel.id, renderChannel(channel))
- },
- hidden: {
- guild: interaction.guild!.id
- }
- };
- log(data);
- } catch (e) {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Attachment Log Channel")
- .setDescription("Something went wrong and the attachment log channel could not be set")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ],
- components: []
- });
- }
- } else {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Attachment Log Channel")
- .setDescription("No changes were made")
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: []
- });
- }
- }
- let clicks = 0;
- const data = await client.database.guilds.read(interaction.guild!.id);
- let channel = data.logging.staff.channel;
+ })
- let timedOut = false;
- while (!timedOut) {
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Attachment Log Channel")
- .setDescription(
- channel
- ? `Your attachment log channel is currently set to <#${channel}>`
- : "This server does not have an attachment log channel" +
- (await client.database.premium.hasPremium(interaction.guild!.id)
- ? ""
- : "\n\nThis server does not have premium, so this feature is disabled")
- )
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("clear")
- .setLabel(clicks ? "Click again to confirm" : "Reset channel")
- .setEmoji(getEmojiByName(clicks ? "TICKETS.ISSUE" : "CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Danger)
- .setDisabled(!channel)
- ])
- ]
- });
- let i;
- try {
- i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- });
- } catch (e) {
- timedOut = true;
- continue;
- }
- i.deferUpdate();
- if ((i.component as unknown as ButtonInteraction).customId === "clear") {
- clicks += 1;
- if (clicks === 2) {
- clicks = 0;
- await client.database.guilds.write(interaction.guild!.id, null, ["logging.announcements.channel"]);
- channel = null;
- }
- }
- }
- await interaction.editReply({
+ if(!await client.database.premium.hasPremium(interaction.guild!.id)) return interaction.editReply({
embeds: [
new EmojiEmbed()
- .setTitle("Attachment Log Channel")
- .setDescription(
- channel
- ? `Your attachment log channel is currently set to <#${channel}>`
- : "This server does not have an attachment log channel"
- )
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- .setFooter({ text: "Message closed" })
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
+ .setTitle("Premium Required")
+ .setDescription(`This feature is exclusive to ${getCommandMentionByName("nucleus/premium")} servers.`)
+ .setStatus("Danger")
+ .setEmoji("NUCLEUS.PREMIUM")
+ ]
+ });
+
+ let data = await client.database.guilds.read(interaction.guild!.id);
+ let channel = data.logging.staff.channel;
+
+ let closed = false;
+ do {
+ const channelMenu = new ActionRowBuilder<ChannelSelectMenuBuilder>()
+ .addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("channel")
+ .setPlaceholder("Select a channel")
+ .setChannelTypes(ChannelType.GuildText)
+ );
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
new ButtonBuilder()
.setCustomId("clear")
.setLabel("Clear")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Secondary)
- .setDisabled(true)
- ])
- ]
- });
+ .setStyle(ButtonStyle.Danger)
+ .setEmoji(getEmojiByName("CONTROL.CROSS", "id") as Discord.APIMessageComponentEmoji)
+ .setDisabled(!channel),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setStyle(ButtonStyle.Success)
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id") as Discord.APIMessageComponentEmoji)
+ .setDisabled(channel === data.logging.staff.channel)
+ );
+
+ const embed = new EmojiEmbed()
+ .setTitle("Attachments")
+ .setDescription(
+ `The channel to send all attachments from the server, allowing you to check them if they are deleted\n` +
+ `**Channel:** ${channel ? `<#${channel}>` : "*None*"}\n`
+ )
+ .setStatus("Success")
+ .setEmoji("CHANNEL.TEXT.CREATE")
+
+ await interaction.editReply({
+ embeds: [embed],
+ components: [channelMenu, buttons]
+ });
+
+ let i: Discord.ButtonInteraction | Discord.SelectMenuInteraction;
+ try {
+ i = (await interaction.channel!.awaitMessageComponent({
+ filter: (i: Discord.Interaction) => i.user.id === interaction.user.id,
+ time: 300000
+ })) as Discord.ButtonInteraction | Discord.SelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ continue;
+ }
+ await i.deferUpdate();
+ if(i.isButton()) {
+ switch (i.customId) {
+ case "clear": {
+ channel = null;
+ break;
+ }
+ case "save": {
+ await client.database.guilds.write(interaction.guild!.id, {
+ "logging.attachments.channel": channel
+ });
+ data = await client.database.guilds.read(interaction.guild!.id);
+ break;
+ }
+ }
+ } else {
+ channel = i.values[0]!;
+ }
+
+ } while (!closed);
+ await interaction.deleteReply()
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageGuild"))
return "You must have the *Manage Server* permission to use this command";
diff --git a/src/commands/settings/logs/channel.ts b/src/commands/settings/logs/channel.ts
deleted file mode 100644
index 992491a..0000000
--- a/src/commands/settings/logs/channel.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-import { LoadingEmbed } from "../../../utils/defaults.js";
-import { ChannelType } from "discord-api-types/v9";
-import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonInteraction, ButtonComponent } from "discord.js";
-import EmojiEmbed from "../../../utils/generateEmojiEmbed.js";
-import confirmationMessage from "../../../utils/confirmationMessage.js";
-import getEmojiByName from "../../../utils/getEmojiByName.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
-import client from "../../../utils/client.js";
-
-const command = (builder: SlashCommandSubcommandBuilder) =>
- builder
- .setName("channel")
- .setDescription("Sets or shows the log channel")
- .addChannelOption((option) =>
- option
- .setName("channel")
- .setDescription("The channel to set the log channel to")
- .addChannelTypes(ChannelType.GuildText)
- );
-
-const callback = async (interaction: CommandInteraction): Promise<unknown> => {
- const m = (await interaction.reply({
- embeds: LoadingEmbed,
- ephemeral: true,
- fetchReply: true
- })) as Discord.Message;
- if (interaction.options.get("channel")?.channel) {
- let channel;
- try {
- channel = interaction.options.get("channel")?.channel;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("CHANNEL.TEXT.DELETE")
- .setTitle("Log Channel")
- .setDescription("The channel you provided is not a valid channel")
- .setStatus("Danger")
- ]
- });
- }
- channel = channel as Discord.TextChannel;
- if (channel.guild.id !== interaction.guild!.id) {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Log Channel")
- .setDescription("You must choose a channel in this server")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ]
- });
- }
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("CHANNEL.TEXT.EDIT")
- .setTitle("Log Channel")
- .setDescription(`Are you sure you want to set the log channel to <#${channel.id}>?`)
- .setColor("Warning")
- .setFailedMessage("The log channel was not changed", "Danger", "CHANNEL.TEXT.DELETE")
- .setInverted(true)
- .send(true);
- if (confirmation.cancelled) return;
- if (confirmation.success) {
- try {
- await client.database.guilds.write(interaction.guild!.id, {
- "logging.logs.channel": channel.id
- });
- const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
- const data = {
- meta: {
- type: "logChannelUpdate",
- displayName: "Log Channel Changed",
- calculateType: "nucleusSettingsUpdated",
- color: NucleusColors.yellow,
- emoji: "CHANNEL.TEXT.EDIT",
- timestamp: new Date().getTime()
- },
- list: {
- memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
- changedBy: entry(interaction.user.id, renderUser(interaction.user)),
- channel: entry(channel.id, renderChannel(channel))
- },
- hidden: {
- guild: channel.guild.id
- }
- };
- log(data);
- } catch (e) {
- console.log(e);
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Log Channel")
- .setDescription("Something went wrong and the log channel could not be set")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ],
- components: []
- });
- }
- } else {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Log Channel")
- .setDescription("No changes were made")
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: []
- });
- }
- }
- let clicks = 0;
- const data = await client.database.guilds.read(interaction.guild!.id);
- let channel = data.logging.logs.channel;
- let timedOut = false;
- while (!timedOut) {
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Log channel")
- .setDescription(
- channel
- ? `Your log channel is currently set to <#${channel}>`
- : "This server does not have a log channel"
- )
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("clear")
- .setLabel(clicks ? "Click again to confirm" : "Reset channel")
- .setEmoji(getEmojiByName(clicks ? "TICKETS.ISSUE" : "CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Danger)
- .setDisabled(!channel)
- ])
- ]
- });
- let i: ButtonInteraction;
- try {
- i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- }) as ButtonInteraction;
- } catch (e) {
- timedOut = true;
- }
- i = i!
- i.deferUpdate();
- if ((i.component as ButtonComponent).customId === "clear") {
- clicks += 1;
- if (clicks === 2) {
- clicks = 0;
- await client.database.guilds.write(interaction.guild!.id, null, ["logging.logs.channel"]);
- channel = null;
- }
- }
- }
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Log channel")
- .setDescription(
- channel
- ? `Your log channel is currently set to <#${channel}>`
- : "This server does not have a log channel"
- )
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- .setFooter({ text: "Message closed" })
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("clear")
- .setLabel("Clear")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Secondary)
- .setDisabled(true)
- ])
- ]
- });
-};
-
-const check = (interaction: CommandInteraction) => {
- const member = interaction.member as Discord.GuildMember;
- if (!member.permissions.has("ManageGuild"))
- return "You must have the *Manage Server* permission to use this command";
- return true;
-};
-
-export { command };
-export { callback };
-export { check };
diff --git a/src/commands/settings/logs/events.ts b/src/commands/settings/logs/events.ts
index fbe79fa..eeef8fb 100644
--- a/src/commands/settings/logs/events.ts
+++ b/src/commands/settings/logs/events.ts
@@ -1,9 +1,11 @@
import { LoadingEmbed } from "../../../utils/defaults.js";
-import Discord, { CommandInteraction, Message, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, EmbedBuilder, StringSelectMenuInteraction } from "discord.js";
-import { SlashCommandSubcommandBuilder, StringSelectMenuOptionBuilder } from "@discordjs/builders";
-import EmojiEmbed from "../../../utils/generateEmojiEmbed.js";
+import Discord, { CommandInteraction, ActionRowBuilder, ChannelSelectMenuBuilder, ChannelType, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, ButtonInteraction, StringSelectMenuInteraction, ChannelSelectMenuInteraction, APIMessageComponentEmoji } from "discord.js";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import client from "../../../utils/client.js";
+import compare from "lodash";
import { toHexArray, toHexInteger } from "../../../utils/calculate.js";
+import EmojiEmbed from "../../../utils/generateEmojiEmbed.js";
+import getEmojiByName from "../../../utils/getEmojiByName.js";
const logs: Record<string, string> = {
channelUpdate: "Channels created, deleted or modified",
@@ -24,88 +26,138 @@
webhookUpdate: "Webhooks created or deleted",
guildMemberVerify: "Member runs verify",
autoModeratorDeleted: "Messages auto deleted by Nucleus",
- nucleusSettingsUpdated: "Nucleus' settings updated by a moderator",
- ticketUpdate: "Tickets created or deleted"
+ ticketUpdate: "Tickets created or deleted",
+ //nucleusSettingsUpdated: "Nucleus' settings updated by a moderator" // TODO
};
const command = (builder: SlashCommandSubcommandBuilder) =>
- builder.setName("events").setDescription("Sets what events should be logged");
+ builder
+ .setName("events")
+ .setDescription("The general log channel for the server, and setting what events to show")
const callback = async (interaction: CommandInteraction): Promise<void> => {
- await interaction.reply({
+ const m = (await interaction.reply({
embeds: LoadingEmbed,
- fetchReply: true,
- ephemeral: true
- });
- let m: Message;
- let timedOut = false;
+ ephemeral: true,
+ fetchReply: true
+ })) as Discord.Message;
+
+ let config = await client.database.guilds.read(interaction.guild!.id);
+ let data = Object.assign({}, config.logging.logs);
+ let closed = false;
+ let show = false;
do {
- const config = await client.database.guilds.read(interaction.guild!.id);
- const converted = toHexArray(config.logging.logs.toLog);
- const selectPane = new StringSelectMenuBuilder()
- .setPlaceholder("Set events to log")
- .setMaxValues(Object.keys(logs).length)
- .setCustomId("logs")
- .setMinValues(0)
- Object.keys(logs).map((e, i) => {
- selectPane.addOptions(new StringSelectMenuOptionBuilder()
- .setLabel(logs[e]!)
- .setValue(i.toString())
- .setDefault(converted.includes(e))
+ const channelMenu = new ActionRowBuilder<ChannelSelectMenuBuilder>()
+ .addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("channel")
+ .setPlaceholder("Select a channel")
+ .setChannelTypes(ChannelType.GuildText)
+ )
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("switch")
+ .setLabel(data.enabled ? "Enabled" : "Disabled")
+ .setStyle(data.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(getEmojiByName((data.enabled ? "CONTROL.TICK" : "CONTROL.CROSS"), "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("remove")
+ .setLabel("Remove")
+ .setStyle(ButtonStyle.Danger)
+ .setDisabled(!data.channel),
+ new ButtonBuilder()
+ .setCustomId("show")
+ .setLabel("Manage Events")
+ .setStyle(ButtonStyle.Primary),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setStyle(ButtonStyle.Success)
+ .setDisabled(compare.isEqual(data, config.logging.logs))
+ )
+
+ const converted = toHexArray(data.toLog);
+ const toLogMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setPlaceholder("Set events to log")
+ .setMaxValues(Object.keys(logs).length)
+ .setCustomId("logs")
+ .setMinValues(0)
+ )
+ Object.keys(logs).map((e) => {
+ toLogMenu.components[0]!.addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel(logs[e]!)
+ .setValue(e)
+ .setDefault(converted.includes(e))
)
});
- m = (await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Logging Events")
- .setDescription(
- "Below are the events being logged in the server. You can toggle them on and off in the dropdown"
- )
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: [
- new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectPane),
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder().setLabel("Select all").setStyle(ButtonStyle.Primary).setCustomId("all"),
- new ButtonBuilder().setLabel("Select none").setStyle(ButtonStyle.Danger).setCustomId("none")
- ])
- ]
- })) as Message;
- let i;
+
+ const embed = new EmojiEmbed()
+ .setTitle("General Log Channel")
+ .setStatus("Success")
+ .setEmoji("CHANNEL.TEXT.CREATE")
+ .setDescription(
+ `This is the channel that all events you set to be logged will be stored\n` +
+ `**Channel:** ${data.channel ? `<#${data.channel}>` : "None"}\n`
+ )
+
+ const components: ActionRowBuilder<ButtonBuilder | ChannelSelectMenuBuilder | StringSelectMenuBuilder>[] = [channelMenu, buttons];
+ if(show) components.push(toLogMenu);
+
+ await interaction.editReply({
+ embeds: [embed],
+ components: components
+ });
+
+ let i: ButtonInteraction | StringSelectMenuInteraction | ChannelSelectMenuInteraction;
try {
i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- });
+ filter: (i) => i.user.id === interaction.user.id,
+ time: 300000
+ }) as ButtonInteraction | StringSelectMenuInteraction | ChannelSelectMenuInteraction;
} catch (e) {
- timedOut = true;
+ closed = true;
continue;
}
- i.deferUpdate();
- if (i.customId === "logs") {
- const selected = (i as StringSelectMenuInteraction).values;
- const newLogs = toHexInteger(selected.map((e: string) => Object.keys(logs)[parseInt(e)]!));
- await client.database.guilds.write(interaction.guild!.id, {
- "logging.logs.toLog": newLogs
- });
- } else if (i.customId === "all") {
- const newLogs = toHexInteger(Object.keys(logs).map((e) => e));
- await client.database.guilds.write(interaction.guild!.id, {
- "logging.logs.toLog": newLogs
- });
- } else if (i.customId === "none") {
- await client.database.guilds.write(interaction.guild!.id, {
- "logging.logs.toLog": 0
- });
- }
- } while (!timedOut);
- await interaction.editReply({ embeds: [new EmbedBuilder(m.embeds[0]!.data).setFooter({ text: "Message timed out" })] });
- return;
+ await i.deferUpdate();
+
+ if(i.isButton()) {
+ switch(i.customId) {
+ case "show": {
+ show = !show;
+ break;
+ }
+ case "switch": {
+ data.enabled = !data.enabled;
+ break;
+ }
+ case "save": {
+ await client.database.guilds.write(interaction.guild!.id, {"logging.logs": data});
+ config = await client.database.guilds.read(interaction.guild!.id);
+ data = Object.assign({}, config.logging.logs);
+ break;
+ }
+ case "remove": {
+ data.channel = null;
+ break;
+ }
+ }
+ } else if(i.isStringSelectMenu()) {
+ const hex = toHexInteger(i.values);
+ data.toLog = hex;
+ } else if(i.isChannelSelectMenu()) {
+ data.channel = i.values[0]!;
+ }
+
+ } while (!closed);
+ await interaction.deleteReply()
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageGuild"))
return "You must have the *Manage Server* permission to use this command";
diff --git a/src/commands/settings/logs/staff.ts b/src/commands/settings/logs/staff.ts
deleted file mode 100644
index 13125ef..0000000
--- a/src/commands/settings/logs/staff.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import { LoadingEmbed } from "../../../utils/defaults.js";
-import { ChannelType } from "discord-api-types/v9";
-import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonComponent } from "discord.js";
-import EmojiEmbed from "../../../utils/generateEmojiEmbed.js";
-import confirmationMessage from "../../../utils/confirmationMessage.js";
-import getEmojiByName from "../../../utils/getEmojiByName.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
-import client from "../../../utils/client.js";
-
-const command = (builder: SlashCommandSubcommandBuilder) =>
- builder
- .setName("staff")
- .setDescription("Settings for the staff notifications channel")
- .addChannelOption((option) =>
- option
- .setName("channel")
- .setDescription("The channel to set the staff notifications channel to")
- .addChannelTypes(ChannelType.GuildText)
- .setRequired(false)
- );
-
-const callback = async (interaction: CommandInteraction): Promise<unknown> => {
- if (!interaction.guild) return;
- const m = (await interaction.reply({
- embeds: LoadingEmbed,
- ephemeral: true,
- fetchReply: true
- })) as Discord.Message;
- if (interaction.options.get("channel")?.channel) {
- let channel;
- try {
- channel = interaction.options.get("channel")?.channel;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("CHANNEL.TEXT.DELETE")
- .setTitle("Staff Notifications Channel")
- .setDescription("The channel you provided is not a valid channel")
- .setStatus("Danger")
- ]
- });
- }
- channel = channel as Discord.TextChannel;
- if (channel.guild.id !== interaction.guild.id) {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Staff Notifications Channel")
- .setDescription("You must choose a channel in this server")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ]
- });
- }
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("CHANNEL.TEXT.EDIT")
- .setTitle("Staff Notifications Channel")
- .setDescription(
- "This will be the channel all notifications, updates, user reports etc. will be sent to.\n\n" +
- `Are you sure you want to set the staff notifications channel to <#${channel.id}>?`
- )
- .setColor("Warning")
- .setFailedMessage("Staff notifications channel not set", "Warning", "CHANNEL.TEXT.DELETE")
- .setInverted(true)
- .send(true);
- if (confirmation.cancelled) return;
- if (confirmation.success) {
- try {
- await client.database.guilds.write(interaction.guild.id, {
- "logging.staff.channel": channel.id
- });
- const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
- const data = {
- meta: {
- type: "staffChannelUpdate",
- displayName: "Staff Notifications Channel Updated",
- calculateType: "nucleusSettingsUpdated",
- color: NucleusColors.yellow,
- emoji: "CHANNEL.TEXT.EDIT",
- timestamp: new Date().getTime()
- },
- list: {
- memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
- changedBy: entry(interaction.user.id, renderUser(interaction.user)),
- channel: entry(channel.id, renderChannel(channel))
- },
- hidden: {
- guild: interaction.guild.id
- }
- };
- log(data);
- } catch (e) {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Staff Notifications Channel")
- .setDescription("Something went wrong and the staff notifications channel could not be set")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ],
- components: []
- });
- }
- } else {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Staff Notifications Channel")
- .setDescription("No changes were made")
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: []
- });
- }
- }
- let clicks = 0;
- const data = await client.database.guilds.read(interaction.guild.id);
- let channel = data.logging.staff.channel;
- let timedOut = false;
- while (!timedOut) {
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Staff Notifications channel")
- .setDescription(
- channel
- ? `Your staff notifications channel is currently set to <#${channel}>`
- : "This server does not have a staff notifications channel"
- )
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("clear")
- .setLabel(clicks ? "Click again to confirm" : "Reset channel")
- .setEmoji(getEmojiByName(clicks ? "TICKETS.ISSUE" : "CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Danger)
- .setDisabled(!channel)
- ])
- ]
- });
- let i;
- try {
- i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- });
- } catch (e) {
- timedOut = true;
- continue;
- }
- i.deferUpdate();
- if ((i.component as ButtonComponent).customId === "clear") {
- clicks += 1;
- if (clicks === 2) {
- clicks = 0;
- await client.database.guilds.write(interaction.guild.id, null, ["logging.staff.channel"]);
- channel = null;
- }
- }
- }
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Staff Notifications channel")
- .setDescription(
- channel
- ? `Your staff notifications channel is currently set to <#${channel}>`
- : "This server does not have a staff notifications channel"
- )
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- .setFooter({ text: "Message closed" })
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("clear")
- .setLabel("Clear")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Secondary)
- .setDisabled(true)
- ])
- ]
- });
-};
-
-const check = (interaction: CommandInteraction) => {
- const member = interaction.member as Discord.GuildMember;
- if (!member.permissions.has("ManageGuild"))
- return "You must have the *Manage Server* permission to use this command";
- return true;
-};
-
-export { command };
-export { callback };
-export { check };
diff --git a/src/commands/settings/logs/warnings.ts b/src/commands/settings/logs/warnings.ts
new file mode 100644
index 0000000..84772e6
--- /dev/null
+++ b/src/commands/settings/logs/warnings.ts
@@ -0,0 +1,104 @@
+import { LoadingEmbed } from "../../../utils/defaults.js";
+import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, ChannelType } from "discord.js";
+import EmojiEmbed from "../../../utils/generateEmojiEmbed.js";
+import getEmojiByName from "../../../utils/getEmojiByName.js";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
+import client from "../../../utils/client.js";
+
+const command = (builder: SlashCommandSubcommandBuilder) =>
+ builder
+ .setName("warnings")
+ .setDescription("Settings for the staff notifications channel")
+
+const callback = async (interaction: CommandInteraction): Promise<unknown> => {
+ if (!interaction.guild) return;
+ await interaction.reply({
+ embeds: LoadingEmbed,
+ ephemeral: true,
+ fetchReply: true
+ })
+
+ let data = await client.database.guilds.read(interaction.guild.id);
+ let channel = data.logging.staff.channel;
+ let closed = false;
+ do {
+ const channelMenu = new ActionRowBuilder<ChannelSelectMenuBuilder>()
+ .addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("channel")
+ .setPlaceholder("Select a channel")
+ .setChannelTypes(ChannelType.GuildText)
+ );
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("clear")
+ .setLabel("Clear")
+ .setStyle(ButtonStyle.Danger)
+ .setEmoji(getEmojiByName("CONTROL.CROSS", "id") as Discord.APIMessageComponentEmoji)
+ .setDisabled(!channel),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setStyle(ButtonStyle.Success)
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id") as Discord.APIMessageComponentEmoji)
+ .setDisabled(channel === data.logging.staff.channel)
+ );
+
+ const embed = new EmojiEmbed()
+ .setTitle("Staff Notifications Channel")
+ .setStatus("Success")
+ .setEmoji("CHANNEL.TEXT.CREATE")
+ .setDescription(
+ `Logs which require an action from a moderator or administrator will be sent to this channel.\n` +
+ `**Channel:** ${channel ? `<#${channel}>` : "*None*"}\n`
+ )
+
+ await interaction.editReply({
+ embeds: [embed],
+ components: [channelMenu, buttons]
+ });
+
+ let i: Discord.ButtonInteraction | Discord.SelectMenuInteraction;
+ try {
+ i = (await interaction.channel!.awaitMessageComponent({
+ filter: (i: Discord.Interaction) => i.user.id === interaction.user.id,
+ time: 300000
+ })) as Discord.ButtonInteraction | Discord.SelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ continue;
+ }
+ await i.deferUpdate();
+ if(i.isButton()) {
+ switch (i.customId) {
+ case "clear": {
+ channel = null;
+ break;
+ }
+ case "save": {
+ await client.database.guilds.write(interaction.guild!.id, {
+ "logging.warnings.channel": channel
+ });
+ data = await client.database.guilds.read(interaction.guild!.id);
+ break;
+ }
+ }
+ } else {
+ channel = i.values[0]!;
+ }
+ } while (!closed);
+
+ await interaction.deleteReply()
+};
+
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
+ const member = interaction.member as Discord.GuildMember;
+ if (!member.permissions.has("ManageGuild"))
+ return "You must have the *Manage Server* permission to use this command";
+ return true;
+};
+
+export { command };
+export { callback };
+export { check };
diff --git a/src/commands/settings/commands.ts b/src/commands/settings/moderation.ts
similarity index 73%
rename from src/commands/settings/commands.ts
rename to src/commands/settings/moderation.ts
index 25034b2..336e53a 100644
--- a/src/commands/settings/commands.ts
+++ b/src/commands/settings/moderation.ts
@@ -1,50 +1,28 @@
import { LoadingEmbed } from "../../utils/defaults.js";
-import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, Role, ButtonStyle, ButtonComponent, TextInputBuilder } from "discord.js";
+import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonComponent, TextInputBuilder, RoleSelectMenuBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import client from "../../utils/client.js";
import { modalInteractionCollector } from "../../utils/dualCollector.js";
-import confirmationMessage from "../../utils/confirmationMessage.js";
-import keyValueList from "../../utils/generateKeyValueList.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
- .setName("commands")
+ .setName("moderation")
.setDescription("Links and text shown to a user after a moderator action is performed")
- .addRoleOption((o) => o.setName("role").setDescription("The role given when a member is muted"));
-const callback = async (interaction: CommandInteraction): Promise<unknown> => {
- await interaction.reply({
+const callback = async (interaction: CommandInteraction): Promise<void> => {
+ const m = await interaction.reply({
embeds: LoadingEmbed,
ephemeral: true,
fetchReply: true
});
- let m;
- let clicked = "";
- if (interaction.options.get("role")) {
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("GUILD.ROLES.DELETE")
- .setTitle("Moderation Commands")
- .setDescription(
- keyValueList({
- role: `<@&${(interaction.options.get("role") as unknown as Role).id}>`
- })
- )
- .setColor("Danger")
- .send(true);
- if (confirmation.cancelled) return
- if (confirmation.success) {
- await client.database.guilds.write(interaction.guild!.id, {
- ["moderation.mute.role"]: (interaction.options.get("role") as unknown as Role).id
- });
- }
- }
let timedOut = false;
while (!timedOut) {
const config = await client.database.guilds.read(interaction.guild!.id);
const moderation = config.moderation;
- m = await interaction.editReply({
+ console.log(moderation)
+ await interaction.editReply({
embeds: [
new EmojiEmbed()
.setTitle("Moderation Commands")
@@ -52,8 +30,7 @@
.setStatus("Success")
.setDescription(
"These links are shown below the message sent in a user's DM when they are punished.\n\n" +
- "**Mute Role:** " +
- (moderation.mute.role ? `<@&${moderation.mute.role}>` : "*None set*")
+ "**Mute Role:** " + (moderation.mute.role ? `<@&${moderation.mute.role}>` : "*None set*")
)
],
components: [
@@ -93,24 +70,23 @@
]),
new ActionRowBuilder<ButtonBuilder>().addComponents([
new ButtonBuilder()
- .setLabel(clicked === "clearMuteRole" ? "Click again to confirm" : "Clear mute role")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setCustomId("clearMuteRole")
- .setStyle(ButtonStyle.Danger)
- .setDisabled(!moderation.mute.role),
- new ButtonBuilder()
.setCustomId("timeout")
.setLabel("Mute timeout " + (moderation.mute.timeout ? "Enabled" : "Disabled"))
.setStyle(moderation.mute.timeout ? ButtonStyle.Success : ButtonStyle.Danger)
.setEmoji(getEmojiByName("CONTROL." + (moderation.mute.timeout ? "TICK" : "CROSS"), "id"))
- ])
+ ]),
+ new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(
+ new RoleSelectMenuBuilder()
+ .setCustomId("muteRole")
+ .setPlaceholder("Select a new mute role")
+ )
]
});
let i;
try {
i = await m.awaitMessageComponent({
time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
+ filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id }
});
} catch (e) {
timedOut = true;
@@ -118,20 +94,13 @@
}
type modIDs = "mute" | "kick" | "ban" | "softban" | "warn" | "role";
let chosen = moderation[i.customId as modIDs];
- if ((i.component as ButtonComponent).customId === "clearMuteRole") {
- i.deferUpdate();
- if (clicked === "clearMuteRole") {
- await client.database.guilds.write(interaction.guild!.id, {
- "moderation.mute.role": null
- });
- } else {
- clicked = "clearMuteRole";
- }
+ if (i.isRoleSelectMenu()) {
+ await i.deferUpdate();
+ await client.database.guilds.write(interaction.guild!.id, {
+ "moderation.mute.role": i.values[0]!
+ });
continue;
- } else {
- clicked = "";
- }
- if ((i.component as ButtonComponent).customId === "timeout") {
+ } else if ((i.component as ButtonComponent).customId === "timeout") {
await i.deferUpdate();
await client.database.guilds.write(interaction.guild!.id, {
"moderation.mute.timeout": !moderation.mute.timeout
@@ -183,15 +152,11 @@
});
let out: Discord.ModalSubmitInteraction | null;
try {
- out = await modalInteractionCollector(
- m,
- (m) => m.channel!.id === interaction.channel!.id,
- (_) => true
- ) as Discord.ModalSubmitInteraction | null;
+ out = await modalInteractionCollector(m, interaction.user) as Discord.ModalSubmitInteraction | null;
} catch (e) {
continue;
}
- if (!out) continue
+ if (!out || out.isButton()) continue
const buttonText = out.fields.getTextInputValue("name");
const buttonLink = out.fields.getTextInputValue("url").replace(/{id}/gi, "{id}");
const current = chosen;
@@ -206,9 +171,10 @@
}
}
}
+ await interaction.deleteReply()
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageGuild"))
return "You must have the *Manage Server* permission to use this command";
diff --git a/src/commands/settings/rolemenu.ts b/src/commands/settings/rolemenu.ts
index b62d962..cccb6f6 100644
--- a/src/commands/settings/rolemenu.ts
+++ b/src/commands/settings/rolemenu.ts
@@ -1,19 +1,478 @@
import type Discord from "discord.js";
-import type { CommandInteraction } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+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") // TODO
- .addRoleOption((option) => option.setName("role").setDescription("The role to give after verifying")); // FIXME FOR FUCK SAKE
+ .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[]) => {
+ 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;
+ 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}) => {
+
+ 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("The name of the role (e.g. Programmer)")
+ .setStyle(TextInputStyle.Short)
+ .setValue(name ?? "")
+ .setRequired(true)
+ ),
+ 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 ?? "")
+ )
+ )
+ 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];
+ if (out.isButton()) 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": {
+ const [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: "New role Menu Option",
+ description: null,
+ role: ""
+ };
+ 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}`)
+ .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`
+ )
+
+ 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();
+ 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;
+ }
+ }
+ }
+ } while (!back);
+ return data;
+}
const callback = async (interaction: CommandInteraction): Promise<void> => {
- console.log("we changed the charger again because fuck you");
- await interaction.reply("You're mum");
+ 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.REORDER", "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;
+ 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(!newPage) break;
+ currentObject.push();
+ page = currentObject.length - 1;
+ break;
+ }
+ case "reorder": {
+ const 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": {
+ const 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);
+ await interaction.deleteReply()
};
-const check = (interaction: CommandInteraction) => {
+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";
diff --git a/src/commands/settings/stats.ts b/src/commands/settings/stats.ts
index cdd218b..d46b57e 100644
--- a/src/commands/settings/stats.ts
+++ b/src/commands/settings/stats.ts
@@ -1,249 +1,403 @@
import { LoadingEmbed } from "../../utils/defaults.js";
-import Discord, { CommandInteraction, Message, ActionRowBuilder, GuildMember, StringSelectMenuBuilder, StringSelectMenuInteraction, AutocompleteInteraction } from "discord.js";
+import Discord, { CommandInteraction, Message, ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuOptionBuilder, APIMessageComponentEmoji, TextInputBuilder, StringSelectMenuInteraction, ButtonInteraction, MessageComponentInteraction, ChannelSelectMenuBuilder, ChannelSelectMenuInteraction, ModalBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
-import confirmationMessage from "../../utils/confirmationMessage.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import client from "../../utils/client.js";
import convertCurlyBracketString from "../../utils/convertCurlyBracketString.js";
-import { callback as statsChannelAddCallback } from "../../reflex/statsChannelUpdate.js";
import singleNotify from "../../utils/singleNotify.js";
+import getEmojiByName from "../../utils/getEmojiByName.js";
+import createPageIndicator from "../../utils/createPageIndicator.js";
+import { modalInteractionCollector } from "../../utils/dualCollector.js";
+
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
.setName("stats")
.setDescription("Controls channels which update when someone joins or leaves the server")
- .addChannelOption((option) => option.setName("channel").setDescription("The channel to modify"))
- .addStringOption((option) =>
- option
- .setName("name")
- .setDescription("The new channel name | Enter any text or use the extra variables like {memberCount}")
- .setAutocomplete(true)
- );
-const callback = async (interaction: CommandInteraction): Promise<unknown> => { // TODO: This command feels unintuitive. Clicking a channel in the select menu deletes it
- // instead, it should give a submenu to edit the channel, enable/disable or delete it
- singleNotify("statsChannelDeleted", interaction.guild!.id, true);
- const m = (await interaction.reply({
- embeds: LoadingEmbed,
- ephemeral: true,
- fetchReply: true
- })) as Message;
- let config = await client.database.guilds.read(interaction.guild!.id);
- if (interaction.options.get("name")?.value as string) {
- let channel;
- if (Object.keys(config.stats).length >= 25) {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("CHANNEL.TEXT.DELETE")
- .setTitle("Stats Channel")
- .setDescription("You can only have 25 stats channels in a server")
- .setStatus("Danger")
- ]
- });
- }
- try {
- channel = interaction.options.get("channel")?.channel as Discord.Channel;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("CHANNEL.TEXT.DELETE")
- .setTitle("Stats Channel")
- .setDescription("The channel you provided is not a valid channel")
- .setStatus("Danger")
- ]
- });
- }
- channel = channel as Discord.TextChannel;
- if (channel.guild.id !== interaction.guild!.id) {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Stats Channel")
- .setDescription("You must choose a channel in this server")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ]
- });
- }
- let newName = await convertCurlyBracketString(
- interaction.options.get("name")?.value as string,
- "",
- "",
- interaction.guild!.name,
- interaction.guild!.members
- );
- if (interaction.options.get("channel")?.channel!.type === Discord.ChannelType.GuildText) {
- newName = newName.toLowerCase().replace(/[\s]/g, "-");
- }
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("CHANNEL.TEXT.EDIT")
- .setTitle("Stats Channel")
- .setDescription(
- `Are you sure you want to set <#${channel.id}> to a stats channel?\n\n*Preview: ${newName.replace(
- /^ +| $/g,
- ""
- )}*`
- )
- .setColor("Warning")
- .setInverted(true)
- .setFailedMessage(`Could not convert <#${channel.id}> to a stats chanel.`, "Danger", "CHANNEL.TEXT.DELETE")
- .send(true);
- if (confirmation.cancelled) return;
- if (confirmation.success) {
- try {
- const name = interaction.options.get("name")?.value as string;
- const channel = interaction.options.get("channel")?.channel as Discord.TextChannel;
- await client.database.guilds.write(interaction.guild!.id, {
- [`stats.${channel.id}`]: { name: name, enabled: true }
- });
- const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
- const data = {
- meta: {
- type: "statsChannelUpdate",
- displayName: "Stats Channel Updated",
- calculateType: "nucleusSettingsUpdated",
- color: NucleusColors.yellow,
- emoji: "CHANNEL.TEXT.EDIT",
- timestamp: new Date().getTime()
- },
- list: {
- memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
- changedBy: entry(interaction.user.id, renderUser(interaction.user)),
- channel: entry(channel.id, renderChannel(channel)),
- name: entry(
- interaction.options.get("name")?.value as string,
- `\`${interaction.options.get("name")?.value as string}\``
+
+const showModal = async (interaction: MessageComponentInteraction, current: { enabled: boolean; name: string; }) => {
+ await interaction.showModal(
+ new ModalBuilder()
+ .setCustomId("modal")
+ .setTitle(`Stats channel name`)
+ .addComponents(
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("ex1")
+ .setLabel("Server Info (1/3)")
+ .setPlaceholder(
+ `{serverName} - This server's name\n\n` +
+ `These placeholders will be replaced with the server's name, etc..`
)
- },
- hidden: {
- guild: interaction.guild!.id
- }
- };
- log(data);
- } catch (e) {
- console.log(e);
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Stats Channel")
- .setDescription("Something went wrong and the stats channel could not be set")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ],
- components: []
- });
- }
- } else {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Stats Channel")
- .setDescription("No changes were made")
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: []
- });
- }
- await statsChannelAddCallback(client, interaction.member as GuildMember);
- }
- let timedOut = false;
- while (!timedOut) {
- config = await client.database.guilds.read(interaction.guild!.id);
- const stats = config.stats;
- const selectMenu = new StringSelectMenuBuilder()
- .setCustomId("remove")
- .setMinValues(1)
- .setMaxValues(Math.max(1, Object.keys(stats).length));
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Stats Channel")
- .setDescription(
- "The following channels update when someone joins or leaves the server. You can select a channel to remove it from the list."
- )
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: [
- new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
- Object.keys(stats).length
- ? [
- selectMenu
- .setPlaceholder("Select a stats channel to remove, stopping it updating")
- .addOptions(
- Object.keys(stats).map((key) => ({
- label: interaction.guild!.channels.cache.get(key)!.name,
- value: key,
- description: `${stats[key]!.name}`
- }))
- )
- ]
- : [
- selectMenu
- .setPlaceholder("The server has no stats channels")
- .setDisabled(true)
- .setOptions([
- {
- label: "*Placeholder*",
- value: "placeholder",
- description: "No stats channels"
- }
- ])
- ]
+ .setMaxLength(1)
+ .setRequired(false)
+ .setStyle(Discord.TextInputStyle.Paragraph)
+ ),
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("ex2")
+ .setLabel("Member Counts (2/3) - {MemberCount:...}")
+ .setPlaceholder(
+ `{:all} - Total member count\n` +
+ `{:humans} - Total non-bot users\n` +
+ `{:bots} - Number of bots\n`
+ )
+ .setMaxLength(1)
+ .setRequired(false)
+ .setStyle(Discord.TextInputStyle.Paragraph)
+ ),
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("ex3")
+ .setLabel("Latest Member (3/3) - {member:...}")
+ .setPlaceholder(
+ `{:name} - The members name\n`
+ )
+ .setMaxLength(1)
+ .setRequired(false)
+ .setStyle(Discord.TextInputStyle.Paragraph)
+ ),
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("text")
+ .setLabel("Channel name input")
+ .setMaxLength(1000)
+ .setRequired(true)
+ .setStyle(Discord.TextInputStyle.Short)
+ .setValue(current.name)
+ )
+ )
+ );
+}
+
+type ObjectSchema = Record<string, {name: string, enabled: boolean}>
+
+
+const addStatsChannel = async (interaction: CommandInteraction, m: Message, currentObject: ObjectSchema): Promise<ObjectSchema> => {
+ let closed = false;
+ let cancelled = false;
+ const originalObject = Object.fromEntries(Object.entries(currentObject).map(([k, v]) => [k, {...v}]));
+ let newChannel: string | undefined;
+ let newChannelName: string = "{memberCount:all}-members";
+ let newChannelEnabled: boolean = true;
+ do {
+ m = await interaction.editReply({
+ embeds: [new EmojiEmbed()
+ .setTitle("Stats Channel")
+ .setDescription(
+ `New stats channel` + (newChannel ? ` in <#${newChannel}>` : "") + "\n\n" +
+ `**Name:** \`${newChannelName}\`\n` +
+ `**Preview:** ${await convertCurlyBracketString(newChannelName, interaction.user!.id, interaction.user.username, interaction.guild!.name, interaction.guild!.members)}\n` +
+ `**Enabled:** ${newChannelEnabled ? "Yes" : "No"}\n\n`
+ )
+ .setEmoji("SETTINGS.STATS.GREEN")
+ .setStatus("Success")
+ ], components: [
+ new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("channel")
+ .setPlaceholder("Select a channel to use")
+ ),
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
+ new ButtonBuilder()
+ .setLabel("Cancel")
+ .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
+ .setStyle(ButtonStyle.Danger)
+ .setCustomId("back"),
+ new ButtonBuilder()
+ .setLabel("Save")
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id"))
+ .setStyle(ButtonStyle.Success)
+ .setCustomId("save"),
+ new ButtonBuilder()
+ .setLabel("Edit name")
+ .setEmoji(getEmojiByName("ICONS.EDIT", "id"))
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId("editName"),
+ new ButtonBuilder()
+ .setLabel(newChannelEnabled ? "Enabled" : "Disabled")
+ .setEmoji(getEmojiByName(newChannelEnabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id"))
+ .setStyle(ButtonStyle.Secondary)
+ .setCustomId("toggleEnabled")
)
]
});
- let i: StringSelectMenuInteraction;
+ let i: ButtonInteraction | ChannelSelectMenuInteraction;
try {
- i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- }) as StringSelectMenuInteraction;
+ i = await m.awaitMessageComponent({ time: 300000, filter: (i) => {
+ return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id;
+ }}) as ButtonInteraction | ChannelSelectMenuInteraction;
} catch (e) {
- timedOut = true;
+ closed = true;
+ cancelled = true;
+ break;
+ }
+ if (i.isButton()) {
+ switch (i.customId) {
+ case "back": {
+ await i.deferUpdate();
+ closed = true;
+ break;
+ }
+ case "save": {
+ await i.deferUpdate();
+ if (newChannel) {
+ currentObject[newChannel] = {
+ name: newChannelName,
+ enabled: newChannelEnabled
+ }
+ }
+ closed = true;
+ break;
+ }
+ case "editName": {
+ await interaction.editReply({
+ embeds: [new EmojiEmbed()
+ .setTitle("Stats Channel")
+ .setDescription("Modal opened. If you can't see it, click back and try again.")
+ .setStatus("Success")
+ .setEmoji("SETTINGS.STATS.GREEN")
+ ],
+ components: [
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
+ new ButtonBuilder()
+ .setLabel("Back")
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId("back")
+ )
+ ]
+ });
+ showModal(i, {name: newChannelName, enabled: newChannelEnabled})
+
+ const out: Discord.ModalSubmitInteraction | ButtonInteraction| null = await modalInteractionCollector(m, interaction.user);
+ if (!out) continue;
+ if (out.isButton()) continue;
+ newChannelName = out.fields.getTextInputValue("text");
+ break;
+ }
+ case "toggleEnabled": {
+ await i.deferUpdate();
+ newChannelEnabled = !newChannelEnabled;
+ break;
+ }
+ }
+ } else {
+ await i.deferUpdate();
+ if (i.customId === "channel") {
+ newChannel = i.values[0];
+ }
+ }
+ } while (!closed)
+ if (cancelled) return originalObject;
+ if (!(newChannel && newChannelName && newChannelEnabled)) return originalObject;
+ return currentObject;
+}
+const callback = async (interaction: CommandInteraction) => {
+ if (!interaction.guild) return;
+ const { renderChannel } = client.logger;
+ const m: Message = 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.stats;
+ let modified = false;
+ do {
+ const embed = new EmojiEmbed()
+ .setTitle("Stats Settings")
+ .setEmoji("SETTINGS.STATS.GREEN")
+ .setStatus("Success");
+ const noStatsChannels = Object.keys(currentObject).length === 0;
+ let current: { enabled: boolean; name: string; };
+
+ const pageSelect = new StringSelectMenuBuilder()
+ .setCustomId("page")
+ .setPlaceholder("Select a stats channel to manage");
+ const actionSelect = new StringSelectMenuBuilder()
+ .setCustomId("action")
+ .setPlaceholder("Perform an action")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Edit")
+ .setDescription("Edit the stats channel")
+ .setValue("edit")
+ .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Delete")
+ .setDescription("Delete the stats channel")
+ .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("Create new")
+ .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
+ .setStyle(ButtonStyle.Secondary)
+ .setDisabled(Object.keys(currentObject).length >= 24),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
+ .setStyle(ButtonStyle.Success)
+ .setDisabled(modified),
+ );
+ if (noStatsChannels) {
+ embed.setDescription("No stats channels 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 stats channels")
+ .setValue("none")
+ );
+ } else {
+ page = Math.min(page, Object.keys(currentObject).length - 1);
+ current = currentObject[Object.keys(config.stats)[page]!]!
+ actionSelect.addOptions(new StringSelectMenuOptionBuilder()
+ .setLabel(current.enabled ? "Disable" : "Enable")
+ .setValue("toggleEnabled")
+ .setDescription(`Currently ${current.enabled ? "Enabled" : "Disabled"}, click to ${current.enabled ? "disable" : "enable"} this channel`)
+ .setEmoji(getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji)
+ );
+ embed.setDescription(`**Currently Editing:** ${renderChannel(Object.keys(currentObject)[page]!)}\n\n` +
+ `${getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS")} Currently ${current.enabled ? "Enabled" : "Disabled"}\n` +
+ `**Name:** \`${current.name}\`\n` +
+ `**Preview:** ${await convertCurlyBracketString(current.name, interaction.user.id, interaction.user.username, interaction.guild.name, interaction.guild.members)}` + '\n\n' +
+ createPageIndicator(Object.keys(config.stats).length, page)
+ );
+ for (const [id, { name, enabled }] of Object.entries(currentObject)) {
+ pageSelect.addOptions(new StringSelectMenuOptionBuilder()
+ .setLabel(`${name} (${renderChannel(id)})`)
+ .setEmoji(getEmojiByName(enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji)
+ .setDescription(`${enabled ? "Enabled" : "Disabled"}`)
+ .setValue(id)
+ );
+ }
+ }
+
+ interaction.editReply({embeds: [embed], components: [
+ new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect),
+ new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect),
+ buttonRow
+ ]});
+
+ let i: StringSelectMenuInteraction | ButtonInteraction;
+ try {
+ i = await m.awaitMessageComponent({ filter: (interaction) => interaction.user.id === interaction.user.id, time: 60000 }) as StringSelectMenuInteraction | ButtonInteraction;
+ } catch (e) {
+ closed = true;
continue;
}
- i.deferUpdate();
- if (i.customId === "remove") {
- const toRemove = i.values;
- await client.database.guilds.write(
- interaction.guild!.id,
- null,
- toRemove.map((k) => `stats.${k}`)
- );
+
+ if(i.isStringSelectMenu()) {
+ switch(i.customId) {
+ case "page": {
+ await i.deferUpdate();
+ page = Object.keys(currentObject).indexOf(i.values[0]!);
+ break;
+ }
+ case "action": {
+ modified = true;
+ switch(i.values[0]!) {
+ case "edit": {
+ showModal(i, current!)
+ await interaction.editReply({
+ embeds: [
+ new EmojiEmbed()
+ .setTitle("Stats Channel")
+ .setDescription("Modal opened. If you can't see it, click back and try again.")
+ .setStatus("Success")
+ .setEmoji("SETTINGS.STATS.GREEN")
+ ],
+ components: [
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
+ new ButtonBuilder()
+ .setLabel("Back")
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId("back")
+ )
+ ]
+ });
+ let out: Discord.ModalSubmitInteraction | null;
+ try {
+ out = await modalInteractionCollector(m, interaction.user) as Discord.ModalSubmitInteraction | null;
+ } catch (e) {
+ continue;
+ }
+ if (!out) continue
+ if (out.isButton()) continue;
+ currentObject[Object.keys(currentObject)[page]!]!.name = out.fields.getTextInputValue("text");
+ break;
+ }
+ case "toggleEnabled": {
+ await i.deferUpdate();
+ currentObject[Object.keys(currentObject)[page]!]!.enabled = !currentObject[Object.keys(currentObject)[page]!]!.enabled;
+ modified = true;
+ break;
+ }
+ case "delete": {
+ await i.deferUpdate();
+ currentObject = Object.fromEntries(Object.entries(currentObject).filter(([k]) => k !== Object.keys(currentObject)[page]!));
+ page = Math.min(page, Object.keys(currentObject).length - 1);
+ modified = true;
+ break;
+ }
+ }
+ break;
+ }
+ }
+ } else {
+ await i.deferUpdate();
+ switch(i.customId) {
+ case "back": {
+ page--;
+ break;
+ }
+ case "next": {
+ page++;
+ break;
+ }
+ case "add": {
+ currentObject = await addStatsChannel(interaction, m, currentObject);
+ page = Object.keys(currentObject).length - 1;
+ break;
+ }
+ case "save": {
+ client.database.guilds.write(interaction.guild.id, {stats: currentObject});
+ singleNotify("statsChannelDeleted", interaction.guild.id, true);
+ modified = false;
+ break;
+ }
+ }
}
- }
- await interaction.editReply({
- embeds: [new Discord.EmbedBuilder(m.embeds[0]!.data).setFooter({ text: "Message timed out" })],
- components: []
- });
+
+ } while (!closed);
+ await interaction.deleteReply()
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageChannels"))
return "You must have the *Manage Channels* permission to use this command";
return true;
};
-const generateStatsChannelAutocomplete = (prompt: string): string[] => {
- return [prompt];
-};
-
-const autocomplete = async (interaction: AutocompleteInteraction): Promise<string[]> => {
- if (!interaction.guild) return [];
- const prompt = interaction.options.getString("tag");
- // generateStatsChannelAutocomplete(int.options.getString("name") ?? "")
- const results = generateStatsChannelAutocomplete(prompt ?? "");
- return results;
-};
-
-
export { command };
export { callback };
-export { check };
-export { autocomplete };
\ No newline at end of file
+export { check };
\ No newline at end of file
diff --git a/src/commands/settings/tickets.ts b/src/commands/settings/tickets.ts
index 892a420..2e046bf 100644
--- a/src/commands/settings/tickets.ts
+++ b/src/commands/settings/tickets.ts
@@ -1,68 +1,38 @@
import { LoadingEmbed } from "../../utils/defaults.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
-import confirmationMessage from "../../utils/confirmationMessage.js";
import Discord, {
CommandInteraction,
- GuildChannel,
Message,
ActionRowBuilder,
ButtonBuilder,
- MessageComponentInteraction,
StringSelectMenuBuilder,
- Role,
- StringSelectMenuInteraction,
ButtonStyle,
TextInputBuilder,
ButtonComponent,
- StringSelectMenuComponent,
ModalSubmitInteraction,
- APIMessageComponentEmoji
+ APIMessageComponentEmoji,
+ RoleSelectMenuBuilder,
+ ChannelSelectMenuBuilder,
+ RoleSelectMenuInteraction,
+ ButtonInteraction,
+ ChannelSelectMenuInteraction,
+ TextInputStyle,
+ ModalBuilder,
+ ChannelType
} from "discord.js";
-import { SlashCommandSubcommandBuilder, StringSelectMenuOptionBuilder } from "@discordjs/builders";
-import { ChannelType } from "discord-api-types/v9";
+import { SlashCommandSubcommandBuilder, StringSelectMenuOptionBuilder } from "discord.js";
import client from "../../utils/client.js";
import { toHexInteger, toHexArray, tickets as ticketTypes } from "../../utils/calculate.js";
import { capitalize } from "../../utils/generateKeyValueList.js";
import { modalInteractionCollector } from "../../utils/dualCollector.js";
import type { GuildConfig } from "../../utils/database.js";
+import { LinkWarningFooter } from "../../utils/defaults.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
.setName("tickets")
- .setDescription("Shows settings for tickets | Use no arguments to manage custom types")
- .addStringOption((option) =>
- option
- .setName("enabled")
- .setDescription("If users should be able to create tickets")
- .setRequired(false)
- .addChoices(
- {name: "Yes", value: "yes"},
- {name: "No",value: "no"}
- )
- )
- .addChannelOption((option) =>
- option
- .setName("category")
- .setDescription("The category where tickets are created")
- .addChannelTypes(ChannelType.GuildCategory)
- .setRequired(false)
- )
- .addNumberOption((option) =>
- option
- .setName("maxticketsperuser")
- .setDescription("The maximum amount of tickets a user can create | Default: 5")
- .setRequired(false)
- .setMinValue(1)
- )
- .addRoleOption((option) =>
- option
- .setName("supportrole")
- .setDescription(
- "This role will have view access to all tickets and will be pinged when a ticket is created"
- )
- .setRequired(false)
- );
+ .setDescription("Shows settings for tickets")
const callback = async (interaction: CommandInteraction): Promise<unknown> => {
if (!interaction.guild) return;
@@ -71,392 +41,130 @@
ephemeral: true,
fetchReply: true
})) as Message;
- const options = {
- enabled: (interaction.options.get("enabled")?.value as string).startsWith("yes") as boolean | null,
- category: interaction.options.get("category")?.channel as Discord.CategoryChannel | null,
- maxtickets: interaction.options.get("maxticketsperuser")?.value as number | null,
- supportping: interaction.options.get("supportrole")?.role as Role | null
- };
- if (options.enabled !== null || options.category || options.maxtickets || options.supportping) {
- if (options.category) {
- let channel: GuildChannel | null;
- try {
- channel = await interaction.guild.channels.fetch(options.category.id) as GuildChannel;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("CHANNEL.TEXT.DELETE")
- .setTitle("Tickets > Category")
- .setDescription("The channel you provided is not a valid category")
- .setStatus("Danger")
- ]
- });
- }
- channel = channel as Discord.CategoryChannel;
- if (channel.guild.id !== interaction.guild.id)
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Tickets > Category")
- .setDescription("You must choose a category in this server")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ]
- });
- }
- if (options.maxtickets) {
- if (options.maxtickets < 1)
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Tickets > Max Tickets")
- .setDescription("You must choose a number greater than 0")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ]
- });
- }
- let role: Role | null;
- if (options.supportping) {
- try {
- role = await interaction.guild.roles.fetch(options.supportping.id);
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("GUILD.ROLE.DELETE")
- .setTitle("Tickets > Support Ping")
- .setDescription("The role you provided is not a valid role")
- .setStatus("Danger")
- ]
- });
- }
- if (!role) return;
- role = role as Discord.Role;
- if (role.guild.id !== interaction.guild.id)
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Tickets > Support Ping")
- .setDescription("You must choose a role in this server")
- .setStatus("Danger")
- .setEmoji("GUILD.ROLE.DELETE")
- ]
- });
- }
-
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("GUILD.TICKET.ARCHIVED")
- .setTitle("Tickets")
- .setDescription(
- (options.category ? `**Category:** ${options.category.name}\n` : "") +
- (options.maxtickets ? `**Max Tickets:** ${options.maxtickets}\n` : "") +
- (options.supportping ? `**Support Ping:** ${options.supportping.name}\n` : "") +
- (options.enabled !== null
- ? `**Enabled:** ${
- options.enabled
- ? `${getEmojiByName("CONTROL.TICK")} Yes`
- : `${getEmojiByName("CONTROL.CROSS")} No`
- }\n`
- : "") +
- "\nAre you sure you want to apply these settings?"
- )
- .setColor("Warning")
- .setFailedMessage("Cancelled", "Warning", "GUILD.TICKET.CLOSE") // TODO: Set Actual Message
- .setInverted(true)
- .send(true);
- if (confirmation.cancelled) return;
- if (confirmation.success) {
- const toUpdate: Record<string, string | boolean | number> = {};
- if (options.enabled !== null) toUpdate["tickets.enabled"] = options.enabled;
- if (options.category) toUpdate["tickets.category"] = options.category.id;
- if (options.maxtickets) toUpdate["tickets.maxTickets"] = options.maxtickets;
- if (options.supportping) toUpdate["tickets.supportRole"] = options.supportping.id;
- try {
- await client.database.guilds.write(interaction.guild.id, toUpdate);
- } catch (e) {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Tickets")
- .setDescription("Something went wrong and the staff notifications channel could not be set")
- .setStatus("Danger")
- .setEmoji("GUILD.TICKET.DELETE")
- ],
- components: []
- });
- }
- } else {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Tickets")
- .setDescription("No changes were made")
- .setStatus("Success")
- .setEmoji("GUILD.TICKET.OPEN")
- ],
- components: []
- });
- }
- }
const data = await client.database.guilds.read(interaction.guild.id);
data.tickets.customTypes = (data.tickets.customTypes ?? []).filter(
(value: string, index: number, array: string[]) => array.indexOf(value) === index
);
- let lastClicked = "";
- const embed: EmojiEmbed = new EmojiEmbed();
- const compiledData = {
- enabled: data.tickets.enabled,
- category: data.tickets.category,
- maxTickets: data.tickets.maxTickets,
- supportRole: data.tickets.supportRole,
- useCustom: data.tickets.useCustom,
- types: data.tickets.types,
- customTypes: data.tickets.customTypes as string[] | null
- };
+ let ticketData = (await client.database.guilds.read(interaction.guild.id)).tickets
+ let changesMade = false;
let timedOut = false;
+ let errorMessage = "";
while (!timedOut) {
- embed
+ const embed: EmojiEmbed = new EmojiEmbed()
.setTitle("Tickets")
.setDescription(
- `${compiledData.enabled ? "" : getEmojiByName("TICKETS.REPORT")} **Enabled:** ${
- compiledData.enabled ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`
+ `${ticketData.enabled ? "" : getEmojiByName("TICKETS.REPORT")} **Enabled:** ${
+ ticketData.enabled ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`
}\n` +
- `${compiledData.category ? "" : getEmojiByName("TICKETS.REPORT")} **Category:** ${
- compiledData.category ? `<#${compiledData.category}>` : "*None set*"
- }\n` +
- `**Max Tickets:** ${compiledData.maxTickets ? compiledData.maxTickets : "*No limit*"}\n` +
- `**Support Ping:** ${compiledData.supportRole ? `<@&${compiledData.supportRole}>` : "*None set*"}\n\n` +
- (compiledData.useCustom && compiledData.customTypes === null ? `${getEmojiByName("TICKETS.REPORT")} ` : "") +
- `${compiledData.useCustom ? "Custom" : "Default"} types in use` +
+ `${ticketData.category ? "" : getEmojiByName("TICKETS.REPORT")}` +
+ ((await interaction.guild.channels.fetch(ticketData.category!))!.type === ChannelType.GuildCategory ?
+ `**Category:** ` : `**Channel:** `) + // TODO: Notify if permissions are wrong
+ `${ticketData.category ? `<#${ticketData.category}>` : "*None set*"}\n` +
+ `**Max Tickets:** ${ticketData.maxTickets ? ticketData.maxTickets : "*No limit*"}\n` +
+ `**Support Ping:** ${ticketData.supportRole ? `<@&${ticketData.supportRole}>` : "*None set*"}\n\n` +
+ (ticketData.useCustom && ticketData.customTypes === null ? `${getEmojiByName("TICKETS.REPORT")} ` : "") +
+ `${ticketData.useCustom ? "Custom" : "Default"} types in use` +
"\n\n" +
`${getEmojiByName("TICKETS.REPORT")} *Indicates a setting stopping tickets from being used*`
)
.setStatus("Success")
.setEmoji("GUILD.TICKET.OPEN");
+ if (errorMessage) embed.setFooter({text: errorMessage, iconURL: LinkWarningFooter.iconURL});
m = (await interaction.editReply({
embeds: [embed],
components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
- .setLabel("Tickets " + (compiledData.enabled ? "enabled" : "disabled"))
- .setEmoji(getEmojiByName("CONTROL." + (compiledData.enabled ? "TICK" : "CROSS"), "id"))
- .setStyle(compiledData.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setLabel("Tickets " + (ticketData.enabled ? "enabled" : "disabled"))
+ .setEmoji(getEmojiByName("CONTROL." + (ticketData.enabled ? "TICK" : "CROSS"), "id"))
+ .setStyle(ticketData.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
.setCustomId("enabled"),
new ButtonBuilder()
- .setLabel(lastClicked === "cat" ? "Click again to confirm" : "Clear category")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Danger)
- .setCustomId("clearCategory")
- .setDisabled(compiledData.category === null),
- new ButtonBuilder()
- .setLabel(lastClicked === "max" ? "Click again to confirm" : "Reset max tickets")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Danger)
- .setCustomId("clearMaxTickets")
- .setDisabled(compiledData.maxTickets === 5),
- new ButtonBuilder()
- .setLabel(lastClicked === "sup" ? "Click again to confirm" : "Clear support ping")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Danger)
- .setCustomId("clearSupportPing")
- .setDisabled(compiledData.supportRole === null)
- ]),
- new ActionRowBuilder<ButtonBuilder>().addComponents([
+ .setLabel("Set max tickets")
+ .setEmoji(getEmojiByName("CONTROL.TICKET", "id"))
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId("setMaxTickets")
+ .setDisabled(!ticketData.enabled),
new ButtonBuilder()
.setLabel("Manage types")
.setEmoji(getEmojiByName("TICKETS.OTHER", "id"))
.setStyle(ButtonStyle.Secondary)
- .setCustomId("manageTypes"),
+ .setCustomId("manageTypes")
+ .setDisabled(!ticketData.enabled),
new ButtonBuilder()
- .setLabel("Add create ticket button")
- .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id"))
- .setStyle(ButtonStyle.Primary)
- .setCustomId("send")
- ])
+ .setLabel("Save")
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id"))
+ .setStyle(ButtonStyle.Success)
+ .setCustomId("save")
+ .setDisabled(!changesMade)
+ ),
+ new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(
+ new RoleSelectMenuBuilder()
+ .setCustomId("supportRole")
+ .setPlaceholder("Select a support role")
+ .setDisabled(!ticketData.enabled)
+ ),
+ new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("category")
+ .setPlaceholder("Select a category or channel")
+ .setDisabled(!ticketData.enabled)
+ )
]
- })) as Message;
- let i: MessageComponentInteraction;
+ }));
+ let i: RoleSelectMenuInteraction | ButtonInteraction | ChannelSelectMenuInteraction;
try {
- i = await m.awaitMessageComponent({
+ i = await m.awaitMessageComponent<2 | 6 | 8>({
time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
+ filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id }
});
} catch (e) {
timedOut = true;
continue;
}
- i.deferUpdate();
- if ((i.component as ButtonComponent).customId === "clearCategory") {
- if (lastClicked === "cat") {
- lastClicked = "";
- await client.database.guilds.write(interaction.guild.id, null, ["tickets.category"]);
- compiledData.category = null;
- } else lastClicked = "cat";
- } else if ((i.component as ButtonComponent).customId === "clearMaxTickets") {
- if (lastClicked === "max") {
- lastClicked = "";
- await client.database.guilds.write(interaction.guild.id, null, ["tickets.maxTickets"]);
- compiledData.maxTickets = 5;
- } else lastClicked = "max";
- } else if ((i.component as ButtonComponent).customId === "clearSupportPing") {
- if (lastClicked === "sup") {
- lastClicked = "";
- await client.database.guilds.write(interaction.guild.id, null, ["tickets.supportRole"]);
- compiledData.supportRole = null;
- } else lastClicked = "sup";
- } else if ((i.component as ButtonComponent).customId === "send") {
- const ticketMessages = [
- {
- label: "Create ticket",
- description: "Click the button below to create a ticket"
- },
- {
- label: "Issues, questions or feedback?",
- description: "Click below to open a ticket and get help from our staff team"
- },
- {
- label: "Contact Us",
- description: "Click the button below to speak to us privately"
+ changesMade = true;
+ if (i.isRoleSelectMenu()) {
+ await i.deferUpdate();
+ ticketData.supportRole = i.values[0] ?? null;
+ } else if (i.isChannelSelectMenu()) {
+ await i.deferUpdate();
+ ticketData.category = i.values[0] ?? null;
+ } else {
+ switch(i.customId) {
+ case "save": {
+ await i.deferUpdate();
+ await client.database.guilds.write(interaction.guild.id, { tickets: ticketData });
+ changesMade = false;
+ break;
}
- ];
- let innerTimedOut = false;
- let templateSelected = false;
- while (!innerTimedOut && !templateSelected) {
- const enabled = compiledData.enabled && compiledData.category !== null;
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Ticket Button")
- .setDescription("Select a message template to send in this channel")
- .setFooter({
- text: enabled
- ? ""
- : "Tickets are not set up correctly so the button may not work for users. Check the main menu to find which options must be set."
- })
- .setStatus(enabled ? "Success" : "Warning")
- .setEmoji("GUILD.ROLES.CREATE")
- ],
- components: [
- new ActionRowBuilder<StringSelectMenuBuilder>().addComponents([
- new StringSelectMenuBuilder()
- .setOptions(
- ticketMessages.map(
- (
- t: {
- label: string;
- description: string;
- value?: string;
- },
- index
- ) => {
- t.value = index.toString();
- return t as {
- value: string;
- label: string;
- description: string;
- };
- }
- )
- )
- .setCustomId("template")
- .setMaxValues(1)
- .setMinValues(1)
- .setPlaceholder("Select a message template")
- ]),
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("back")
- .setLabel("Back")
- .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
- .setStyle(ButtonStyle.Danger),
- new ButtonBuilder().setCustomId("blank").setLabel("Empty").setStyle(ButtonStyle.Secondary),
- new ButtonBuilder()
- .setCustomId("custom")
- .setLabel("Custom")
- .setEmoji(getEmojiByName("TICKETS.OTHER", "id"))
- .setStyle(ButtonStyle.Primary)
- ])
- ]
- });
- let i: MessageComponentInteraction;
- try {
- i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- });
- } catch (e) {
- innerTimedOut = true;
- continue;
+ case "enabled": {
+ await i.deferUpdate();
+ ticketData.enabled = !ticketData.enabled;
+ break;
}
- if ((i.component as StringSelectMenuComponent).customId === "template") {
- i.deferUpdate();
- await interaction.channel!.send({
- embeds: [
- new EmojiEmbed()
- .setTitle(ticketMessages[parseInt((i as StringSelectMenuInteraction).values[0]!)]!.label)
- .setDescription(
- ticketMessages[parseInt((i as StringSelectMenuInteraction).values[0]!)]!.description
- )
- .setStatus("Success")
- .setEmoji("GUILD.TICKET.OPEN")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Create Ticket")
- .setEmoji(getEmojiByName("CONTROL.TICK", "id"))
- .setStyle(ButtonStyle.Success)
- .setCustomId("createticket")
- ])
- ]
- });
- templateSelected = true;
- continue;
- } else if ((i.component as ButtonComponent).customId === "blank") {
- i.deferUpdate();
- await interaction.channel!.send({
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Create Ticket")
- .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id"))
- .setStyle(ButtonStyle.Success)
- .setCustomId("createticket")
- ])
- ]
- });
- templateSelected = true;
- continue;
- } else if ((i.component as ButtonComponent).customId === "custom") {
+ case "setMaxTickets": {
await i.showModal(
- new Discord.ModalBuilder()
- .setCustomId("modal")
- .setTitle("Enter embed details")
+ new ModalBuilder()
+ .setCustomId("maxTickets")
+ .setTitle("Set max tickets")
.addComponents(
- new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new ActionRowBuilder<TextInputBuilder>().setComponents(
new TextInputBuilder()
- .setCustomId("title")
- .setLabel("Title")
- .setMaxLength(256)
- .setRequired(true)
- .setStyle(Discord.TextInputStyle.Short)
- ),
- new ActionRowBuilder<TextInputBuilder>().addComponents(
- new TextInputBuilder()
- .setCustomId("description")
- .setLabel("Description")
- .setMaxLength(4000)
- .setRequired(true)
- .setStyle(Discord.TextInputStyle.Paragraph)
+ .setLabel("Max tickets - Leave blank for no limit")
+ .setCustomId("maxTickets")
+ .setPlaceholder("Enter a number")
+ .setRequired(false)
+ .setValue(ticketData.maxTickets.toString())
+ .setMinLength(1)
+ .setMaxLength(3)
+ .setStyle(TextInputStyle.Short)
)
)
- );
- await interaction.editReply({
+ )
+ await i.editReply({
embeds: [
new EmojiEmbed()
- .setTitle("Ticket Button")
+ .setTitle("Tickets")
.setDescription("Modal opened. If you can't see it, click back and try again.")
.setStatus("Success")
.setEmoji("GUILD.TICKET.OPEN")
@@ -473,54 +181,33 @@
});
let out;
try {
- out = await modalInteractionCollector(
- m,
- (m) => m.channel!.id === interaction.channel!.id,
- (m) => m.customId === "modify"
- );
+ out = await modalInteractionCollector(m, interaction.user);
} catch (e) {
- innerTimedOut = true;
continue;
}
+ if (!out || out.isButton()) continue;
out = out as ModalSubmitInteraction;
- const title = out.fields.getTextInputValue("title");
- const description = out.fields.getTextInputValue("description");
- await interaction.channel!.send({
- embeds: [
- new EmojiEmbed()
- .setTitle(title)
- .setDescription(description)
- .setStatus("Success")
- .setEmoji("GUILD.TICKET.OPEN")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Create Ticket")
- .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id"))
- .setStyle(ButtonStyle.Success)
- .setCustomId("createticket")
- ])
- ]
- });
- templateSelected = true;
+ const toAdd = out.fields.getTextInputValue("maxTickets");
+ if(isNaN(parseInt(toAdd))) {
+ errorMessage = "You entered an invalid number - No changes were made";
+ break;
+ }
+ ticketData.maxTickets = toAdd === "" ? 0 : parseInt(toAdd);
+ break;
+ }
+ case "manageTypes": {
+ await i.deferUpdate();
+ ticketData = await manageTypes(interaction, data.tickets, m);
+ break;
}
}
- } else if ((i.component as ButtonComponent).customId === "enabled") {
- await client.database.guilds.write(interaction.guild.id, {
- "tickets.enabled": !compiledData.enabled
- });
- compiledData.enabled = !compiledData.enabled;
- } else if ((i.component as ButtonComponent).customId === "manageTypes") {
- data.tickets = await manageTypes(interaction, data.tickets, m as Message);
}
}
- await interaction.editReply({
- embeds: [ embed.setFooter({ text: "Message timed out" })],
- components: []
- });
+ await interaction.deleteReply()
};
+
+
async function manageTypes(interaction: CommandInteraction, data: GuildConfig["tickets"], m: Message) {
let timedOut = false;
let backPressed = false;
@@ -545,7 +232,7 @@
.setStatus("Success")
.setEmoji("GUILD.TICKET.OPEN")
],
- components: (customTypes
+ components: (customTypes && customTypes.length > 0
? [
new ActionRowBuilder<StringSelectMenuBuilder | ButtonBuilder>().addComponents([
new Discord.StringSelectMenuBuilder()
@@ -637,29 +324,23 @@
try {
i = await m.awaitMessageComponent({
time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
+ filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id }
});
} catch (e) {
timedOut = true;
continue;
}
- if ((i.component as StringSelectMenuComponent).customId === "types") {
- i.deferUpdate();
- const types = toHexInteger((i as StringSelectMenuInteraction).values, ticketTypes);
- await client.database.guilds.write(interaction.guild!.id, {
- "tickets.types": types
- });
+ if (i.isStringSelectMenu() && i.customId === "types") {
+ await i.deferUpdate();
+ const types = toHexInteger(i.values, ticketTypes);
data.types = types;
- } else if ((i.component as StringSelectMenuComponent).customId === "removeTypes") {
- i.deferUpdate();
- const types = (i as StringSelectMenuInteraction).values;
+ } else if (i.isStringSelectMenu() && i.customId === "removeTypes") {
+ await i.deferUpdate();
+ const types = i.values;
let customTypes = data.customTypes;
if (customTypes) {
customTypes = customTypes.filter((t) => !types.includes(t));
customTypes = customTypes.length > 0 ? customTypes : null;
- await client.database.guilds.write(interaction.guild!.id, {
- "tickets.customTypes": customTypes
- });
data.customTypes = customTypes;
}
} else if ((i.component as ButtonComponent).customId === "addType") {
@@ -680,7 +361,7 @@
)
)
);
- await interaction.editReply({
+ await i.editReply({
embeds: [
new EmojiEmbed()
.setTitle("Tickets > Types")
@@ -700,14 +381,11 @@
});
let out;
try {
- out = await modalInteractionCollector(
- m,
- (m) => m.channel!.id === interaction.channel!.id,
- (m) => m.customId === "addType"
- );
+ out = await modalInteractionCollector(m, interaction.user);
} catch (e) {
continue;
}
+ if (!out || out.isButton()) continue;
out = out as ModalSubmitInteraction;
let toAdd = out.fields.getTextInputValue("type");
if (!toAdd) {
@@ -715,31 +393,31 @@
}
toAdd = toAdd.substring(0, 80);
try {
- await client.database.guilds.append(interaction.guild!.id, "tickets.customTypes", toAdd);
+ if(!data.customTypes) data.customTypes = [];
+ data.customTypes.push(toAdd);
} catch {
continue;
}
- data.customTypes = data.customTypes ?? [];
if (!data.customTypes.includes(toAdd)) {
data.customTypes.push(toAdd);
}
} else if ((i.component as ButtonComponent).customId === "switchToDefault") {
- i.deferUpdate();
+ await i.deferUpdate();
await client.database.guilds.write(interaction.guild!.id, { "tickets.useCustom": false }, []);
data.useCustom = false;
} else if ((i.component as ButtonComponent).customId === "switchToCustom") {
- i.deferUpdate();
+ await i.deferUpdate();
await client.database.guilds.write(interaction.guild!.id, { "tickets.useCustom": true }, []);
data.useCustom = true;
} else {
- i.deferUpdate();
+ await i.deferUpdate();
backPressed = true;
}
}
return data;
}
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageGuild"))
return "You must have the *Manage Server* permission to use this command";
diff --git a/src/commands/settings/tracks.ts b/src/commands/settings/tracks.ts
new file mode 100644
index 0000000..d9d485d
--- /dev/null
+++ b/src/commands/settings/tracks.ts
@@ -0,0 +1,459 @@
+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;
+ 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 };
diff --git a/src/commands/settings/verify.ts b/src/commands/settings/verify.ts
index 0f9f4a0..c440b75 100644
--- a/src/commands/settings/verify.ts
+++ b/src/commands/settings/verify.ts
@@ -1,35 +1,25 @@
import { LoadingEmbed } from "../../utils/defaults.js";
import Discord, {
CommandInteraction,
- Interaction,
Message,
ActionRowBuilder,
ButtonBuilder,
- MessageComponentInteraction,
- ModalSubmitInteraction,
- Role,
ButtonStyle,
- StringSelectMenuBuilder,
- StringSelectMenuComponent,
- TextInputBuilder,
- EmbedBuilder,
- StringSelectMenuInteraction,
- ButtonComponent
+ RoleSelectMenuBuilder,
+ APIMessageComponentEmoji
} from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
-import confirmationMessage from "../../utils/confirmationMessage.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import client from "../../utils/client.js";
-import { modalInteractionCollector } from "../../utils/dualCollector.js";
+import { getCommandMentionByName } from "../../utils/getCommandDataByName.js";
+import lodash from "lodash";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
.setName("verify")
- .setDescription("Manage the role given after typing /verify")
- .addRoleOption((option) =>
- option.setName("role").setDescription("The role to give after verifying").setRequired(false)
- );
+ .setDescription("Manage the role given after a user runs /verify")
+
const callback = async (interaction: CommandInteraction): Promise<unknown> => {
if (!interaction.guild) return;
@@ -38,356 +28,82 @@
ephemeral: true,
fetchReply: true
})) as Message;
- if (interaction.options.get("role")?.role) {
- let role: Role;
- try {
- role = interaction.options.get("role")?.role as Role;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("GUILD.ROLES.DELETE")
- .setTitle("Verify Role")
- .setDescription("The role you provided is not a valid role")
- .setStatus("Danger")
- ]
- });
- }
- role = role as Discord.Role;
- if (role.guild.id !== interaction.guild.id) {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Verify Role")
- .setDescription("You must choose a role in this server")
- .setStatus("Danger")
- .setEmoji("GUILD.ROLES.DELETE")
- ]
- });
- }
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("GUILD.ROLES.EDIT")
- .setTitle("Verify Role")
- .setDescription(`Are you sure you want to set the verify role to <@&${role.id}>?`)
- .setColor("Warning")
- .setFailedMessage("No changes were made", "Warning", "GUILD.ROLES.DELETE")
- .setInverted(true)
- .send(true);
- if (confirmation.cancelled) return;
- if (confirmation.success) {
- try {
- await client.database.guilds.write(interaction.guild.id, {
- "verify.role": role.id,
- "verify.enabled": true
- });
- const { log, NucleusColors, entry, renderUser, renderRole } = client.logger;
- const data = {
- meta: {
- type: "verifyRoleChanged",
- displayName: "Verify Role Changed",
- calculateType: "nucleusSettingsUpdated",
- color: NucleusColors.green,
- emoji: "CONTROL.BLOCKTICK",
- timestamp: new Date().getTime()
- },
- list: {
- memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
- changedBy: entry(interaction.user.id, renderUser(interaction.user)),
- role: entry(role.id, renderRole(role))
- },
- hidden: {
- guild: interaction.guild.id
- }
- };
- log(data);
- } catch (e) {
- console.log(e);
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Verify Role")
- .setDescription("Something went wrong while setting the verify role")
- .setStatus("Danger")
- .setEmoji("GUILD.ROLES.DELETE")
- ],
- components: []
- });
- }
- } else {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Verify Role")
- .setDescription("No changes were made")
- .setStatus("Success")
- .setEmoji("GUILD.ROLES.CREATE")
- ],
- components: []
- });
- }
- }
- let clicks = 0;
- const data = await client.database.guilds.read(interaction.guild.id);
- let role = data.verify.role;
- let timedOut = false;
- while (!timedOut) {
+ let closed = false;
+ let config = await client.database.guilds.read(interaction.guild.id);
+ let data = Object.assign({}, config.verify);
+ do {
+ const selectMenu = new ActionRowBuilder<RoleSelectMenuBuilder>()
+ .addComponents(
+ new RoleSelectMenuBuilder()
+ .setCustomId("role")
+ .setPlaceholder("Select a role")
+ );
+
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("switch")
+ .setLabel(data.enabled ? "Enabled" : "Disabled")
+ .setStyle(data.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(getEmojiByName(data.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setStyle(ButtonStyle.Success)
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
+ .setDisabled(lodash.isEqual(config.verify, data))
+ );
+
+ const embed = new EmojiEmbed()
+ .setTitle("Verify Role")
+ .setDescription(
+ `Select a role to be given to users after they run ${getCommandMentionByName("verify")}` +
+ `\n\nCurrent role: ${config.verify.role ? `<@&${config.verify.role}>` : "None"}`
+ )
+ .setStatus("Success")
+ .setEmoji("CHANNEL.TEXT.CREATE");
+
await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Verify Role")
- .setDescription(
- role ? `Your verify role is currently set to <@&${role}>` : "You have not set a verify role"
- )
- .setStatus("Success")
- .setEmoji("GUILD.ROLES.CREATE")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("clear")
- .setLabel(clicks ? "Click again to confirm" : "Reset role")
- .setEmoji(getEmojiByName(clicks ? "TICKETS.ISSUE" : "CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Danger)
- .setDisabled(!role),
- new ButtonBuilder()
- .setCustomId("send")
- .setLabel("Add verify button")
- .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id"))
- .setStyle(ButtonStyle.Primary)
- ])
- ]
+ embeds: [embed],
+ components: [selectMenu, buttons]
});
- let i: MessageComponentInteraction;
+
+ let i;
try {
i = await m.awaitMessageComponent({
time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
+ filter: (i) => { return i.user.id === interaction.user.id }
});
} catch (e) {
- timedOut = true;
+ closed = true;
continue;
}
- i.deferUpdate();
- if ((i.component as ButtonComponent).customId === "clear") {
- clicks += 1;
- if (clicks === 2) {
- clicks = 0;
- await client.database.guilds.write(interaction.guild.id, null, ["verify.role", "verify.enabled"]);
- role = null;
- }
- } else if ((i.component as ButtonComponent).customId === "send") {
- const verifyMessages = [
- {
- label: "Verify",
- description: "Click the button below to get verified"
- },
- {
- label: "Get verified",
- description: "To get access to the rest of the server, click the button below"
- },
- {
- label: "Ready to verify?",
- description: "Click the button below to verify yourself"
+
+ await i.deferUpdate();
+
+ if(i.isButton()) {
+ switch (i.customId) {
+ case "save": {
+ client.database.guilds.write(interaction.guild.id, {"verify": data} )
+ config = await client.database.guilds.read(interaction.guild.id);
+ data = Object.assign({}, config.verify);
+ break
}
- ];
- let innerTimedOut = false;
- let templateSelected = false;
- while (!innerTimedOut && !templateSelected) {
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Verify Button")
- .setDescription("Select a message template to send in this channel")
- .setFooter({
- text: role ? "" : "You do no have a verify role set so the button will not work."
- })
- .setStatus(role ? "Success" : "Warning")
- .setEmoji("GUILD.ROLES.CREATE")
- ],
- components: [
- new ActionRowBuilder<StringSelectMenuBuilder>().addComponents([
- new StringSelectMenuBuilder()
- .setOptions(
- verifyMessages.map(
- (
- t: {
- label: string;
- description: string;
- value?: string;
- },
- index
- ) => {
- t.value = index.toString();
- return t as {
- value: string;
- label: string;
- description: string;
- };
- }
- )
- )
- .setCustomId("template")
- .setMaxValues(1)
- .setMinValues(1)
- .setPlaceholder("Select a message template")
- ]),
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("back")
- .setLabel("Back")
- .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
- .setStyle(ButtonStyle.Danger),
- new ButtonBuilder().setCustomId("blank").setLabel("Empty").setStyle(ButtonStyle.Secondary),
- new ButtonBuilder()
- .setCustomId("custom")
- .setLabel("Custom")
- .setEmoji(getEmojiByName("TICKETS.OTHER", "id"))
- .setStyle(ButtonStyle.Primary)
- ])
- ]
- });
- let i: MessageComponentInteraction;
- try {
- i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- });
- } catch (e) {
- innerTimedOut = true;
- continue;
- }
- if ((i.component as StringSelectMenuComponent).customId === "template") {
- i.deferUpdate();
- await interaction.channel!.send({
- embeds: [
- new EmojiEmbed()
- .setTitle(verifyMessages[parseInt((i as StringSelectMenuInteraction).values[0]!)]!.label)
- .setDescription(
- verifyMessages[parseInt((i as StringSelectMenuInteraction).values[0]!)]!.description
- )
- .setStatus("Success")
- .setEmoji("CONTROL.BLOCKTICK")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Verify")
- .setEmoji(getEmojiByName("CONTROL.TICK", "id"))
- .setStyle(ButtonStyle.Success)
- .setCustomId("verifybutton")
- ])
- ]
- });
- templateSelected = true;
- continue;
- } else if ((i.component as ButtonComponent).customId === "blank") {
- i.deferUpdate();
- await interaction.channel!.send({
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Verify")
- .setEmoji(getEmojiByName("CONTROL.TICK", "id"))
- .setStyle(ButtonStyle.Success)
- .setCustomId("verifybutton")
- ])
- ]
- });
- templateSelected = true;
- continue;
- } else if ((i.component as ButtonComponent).customId === "custom") {
- await i.showModal(
- new Discord.ModalBuilder()
- .setCustomId("modal")
- .setTitle("Enter embed details")
- .addComponents(
- new ActionRowBuilder<TextInputBuilder>().addComponents(
- new TextInputBuilder()
- .setCustomId("title")
- .setLabel("Title")
- .setMaxLength(256)
- .setRequired(true)
- .setStyle(Discord.TextInputStyle.Short)
- ),
- new ActionRowBuilder<TextInputBuilder>().addComponents(
- new TextInputBuilder()
- .setCustomId("description")
- .setLabel("Description")
- .setMaxLength(4000)
- .setRequired(true)
- .setStyle(Discord.TextInputStyle.Paragraph)
- )
- )
- );
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Verify Button")
- .setDescription("Modal opened. If you can't see it, click back and try again.")
- .setStatus("Success")
- .setEmoji("GUILD.TICKET.OPEN")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Back")
- .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
- .setStyle(ButtonStyle.Primary)
- .setCustomId("back")
- ])
- ]
- });
- let out;
- try {
- out = await modalInteractionCollector(
- m,
- (m: Interaction) =>
- (m as MessageComponentInteraction | ModalSubmitInteraction).channelId ===
- interaction.channelId,
- (m) => m.customId === "modify"
- );
- } catch (e) {
- innerTimedOut = true;
- continue;
- }
- if (out !== null && out instanceof ModalSubmitInteraction) {
- const title = out.fields.getTextInputValue("title");
- const description = out.fields.getTextInputValue("description");
- await interaction.channel!.send({
- embeds: [
- new EmojiEmbed()
- .setTitle(title)
- .setDescription(description)
- .setStatus("Success")
- .setEmoji("CONTROL.BLOCKTICK")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Verify")
- .setEmoji(getEmojiByName("CONTROL.TICK", "id"))
- .setStyle(ButtonStyle.Success)
- .setCustomId("verifybutton")
- ])
- ]
- });
- templateSelected = true;
- }
+ case "switch": {
+ data.enabled = !data.enabled;
+ break
}
}
} else {
- i.deferUpdate();
- break;
+ data.role = i.values[0]!;
}
- }
- await interaction.editReply({
- embeds: [new EmbedBuilder(m.embeds[0]!.data).setFooter({ text: "Message closed" })],
- components: []
- });
+
+ } while (!closed);
+ await interaction.deleteReply()
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageGuild"))
return "You must have the *Manage Server* permission to use this command";
diff --git a/src/commands/settings/welcome.ts b/src/commands/settings/welcome.ts
index e7143fb..7584624 100644
--- a/src/commands/settings/welcome.ts
+++ b/src/commands/settings/welcome.ts
@@ -1,307 +1,263 @@
import { LoadingEmbed } from "../../utils/defaults.js";
import Discord, {
- Channel,
CommandInteraction,
- Message,
+ AutocompleteInteraction,
ActionRowBuilder,
ButtonBuilder,
- MessageComponentInteraction,
- Role,
ButtonStyle,
- AutocompleteInteraction,
- GuildChannel,
- EmbedBuilder
+ APIMessageComponentEmoji,
+ ChannelSelectMenuBuilder,
+ RoleSelectMenuBuilder,
+ RoleSelectMenuInteraction,
+ ChannelSelectMenuInteraction,
+ ButtonInteraction,
+ ModalBuilder,
+ TextInputBuilder,
+ TextInputStyle,
+ ModalSubmitInteraction,
} from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import client from "../../utils/client.js";
-import confirmationMessage from "../../utils/confirmationMessage.js";
-import generateKeyValueList from "../../utils/generateKeyValueList.js";
-import { ChannelType } from "discord-api-types/v9";
import getEmojiByName from "../../utils/getEmojiByName.js";
+import convertCurlyBracketString from "../../utils/convertCurlyBracketString.js";
+import { modalInteractionCollector } from "../../utils/dualCollector.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
.setName("welcome")
.setDescription("Messages and roles sent or given when someone joins the server")
- .addStringOption((option) =>
- option
- .setName("message")
- .setDescription("The message to send when someone joins the server")
- .setAutocomplete(true)
- )
- .addRoleOption((option) =>
- option.setName("role").setDescription("The role given when someone joins the server")
- )
- .addRoleOption((option) =>
- option.setName("ping").setDescription("The role pinged when someone joins the server")
- )
- .addChannelOption((option) =>
- option
- .setName("channel")
- .setDescription("The channel the welcome message should be sent to")
- .addChannelTypes(ChannelType.GuildText)
- );
-const callback = async (interaction: CommandInteraction): Promise<unknown> => {
- const { renderRole, renderChannel, log, NucleusColors, entry, renderUser } = client.logger;
- await interaction.reply({
+const callback = async (interaction: CommandInteraction): Promise<void> => {
+ const { renderChannel } = client.logger;
+ const m = await interaction.reply({
embeds: LoadingEmbed,
fetchReply: true,
ephemeral: true
});
- let m: Message;
- if (
- interaction.options.get("role")?.role ||
- interaction.options.get("channel")?.channel ||
- interaction.options.get("message")?.value as string
- ) {
- let role: Role | null;
- let ping: Role | null;
- let channel: Channel | null;
- const message: string | null = interaction.options.get("message")?.value as string | null;
- try {
- role = interaction.options.get("role")?.role as Role | null;
- ping = interaction.options.get("ping")?.role as Role | null;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("GUILD.ROLES.DELETE")
- .setTitle("Welcome Events")
- .setDescription("The role you provided is not a valid role")
- .setStatus("Danger")
- ]
- });
- }
- try {
- channel = interaction.options.get("channel")?.channel as Channel | null;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("GUILD.ROLES.DELETE")
- .setTitle("Welcome Events")
- .setDescription("The channel you provided is not a valid channel")
- .setStatus("Danger")
- ]
- });
- }
- const options: {
- role?: string;
- ping?: string;
- channel?: string;
- message?: string;
- } = {};
-
- if (role) options.role = renderRole(role);
- if (ping) options.ping = renderRole(ping);
- if (channel) options.channel = renderChannel(channel as GuildChannel);
- if (message) options.message = "\n> " + message;
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("GUILD.ROLES.EDIT")
- .setTitle("Welcome Events")
- .setDescription(generateKeyValueList(options))
- .setColor("Warning")
- .setFailedMessage("Cancelled", "Warning", "GUILD.ROLES.DELETE") //TODO: Actual Message Needed
- .setInverted(true)
- .send(true);
- if (confirmation.cancelled) return;
- if (confirmation.success) {
- try {
- const toChange: {
- "welcome.role"?: string;
- "welcome.ping"?: string;
- "welcome.channel"?: string;
- "welcome.message"?: string;
- } = {};
- if (role) toChange["welcome.role"] = role.id;
- if (ping) toChange["welcome.ping"] = ping.id;
- if (channel) toChange["welcome.channel"] = channel.id;
- if (message) toChange["welcome.message"] = message;
- await client.database.guilds.write(interaction.guild!.id, toChange);
- const list: {
- memberId: ReturnType<typeof entry>;
- changedBy: ReturnType<typeof entry>;
- role?: ReturnType<typeof entry>;
- ping?: ReturnType<typeof entry>;
- channel?: ReturnType<typeof entry>;
- message?: ReturnType<typeof entry>;
- } = {
- memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
- changedBy: entry(interaction.user.id, renderUser(interaction.user))
- };
- if (role) list.role = entry(role.id, renderRole(role));
- if (ping) list.ping = entry(ping.id, renderRole(ping));
- if (channel) list.channel = entry(channel.id, renderChannel(channel as GuildChannel));
- if (message) list.message = entry(message, `\`${message}\``);
- const data = {
- meta: {
- type: "welcomeSettingsUpdated",
- displayName: "Welcome Settings Changed",
- calculateType: "nucleusSettingsUpdated",
- color: NucleusColors.green,
- emoji: "CONTROL.BLOCKTICK",
- timestamp: new Date().getTime()
- },
- list: list,
- hidden: {
- guild: interaction.guild!.id
- }
- };
- log(data);
- } catch (e) {
- console.log(e);
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Welcome Events")
- .setDescription("Something went wrong while updating welcome settings")
- .setStatus("Danger")
- .setEmoji("GUILD.ROLES.DELETE")
- ],
- components: []
- });
- }
- } else {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Welcome Events")
- .setDescription("No changes were made")
- .setStatus("Success")
- .setEmoji("GUILD.ROLES.CREATE")
- ],
- components: []
- });
- }
- }
- let lastClicked = null;
- let timedOut = false;
+ let closed = false;
+ let config = await client.database.guilds.read(interaction.guild!.id);
+ let data = Object.assign({}, config.welcome);
do {
- const config = await client.database.guilds.read(interaction.guild!.id);
- m = (await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Welcome Events")
- .setDescription(
- `**Message:** ${config.welcome.message ? `\n> ${config.welcome.message}` : "*None set*"}\n` +
- `**Role:** ${
- config.welcome.role
- ? renderRole((await interaction.guild!.roles.fetch(config.welcome.role))!)
- : "*None set*"
- }\n` +
- `**Ping:** ${
- config.welcome.ping
- ? renderRole((await interaction.guild!.roles.fetch(config.welcome.ping))!)
- : "*None set*"
- }\n` +
- `**Channel:** ${
- config.welcome.channel
- ? config.welcome.channel == "dm"
- ? "DM"
- : renderChannel((await interaction.guild!.channels.fetch(config.welcome.channel))!)
- : "*None set*"
- }`
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("switch")
+ .setLabel(data.enabled ? "Enabled" : "Disabled")
+ .setStyle(data.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(getEmojiByName(data.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("message")
+ .setLabel((data.message ? "Change" : "Set") + "Message")
+ .setStyle(ButtonStyle.Primary)
+ .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("channelDM")
+ .setLabel("Send in DMs")
+ .setStyle(ButtonStyle.Primary)
+ .setDisabled(data.channel === "dm"),
+ new ButtonBuilder()
+ .setCustomId("role")
+ .setLabel("Clear Role")
+ .setStyle(ButtonStyle.Danger)
+ .setEmoji(getEmojiByName("CONTROL.CROSS", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setStyle(ButtonStyle.Success)
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
+ .setDisabled(
+ data.enabled === config.welcome.enabled &&
+ data.message === config.welcome.message &&
+ data.role === config.welcome.role &&
+ data.ping === config.welcome.ping &&
+ data.channel === config.welcome.channel
)
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel(lastClicked == "clear-message" ? "Click again to confirm" : "Clear Message")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setCustomId("clear-message")
- .setDisabled(!config.welcome.message)
- .setStyle(ButtonStyle.Danger),
- new ButtonBuilder()
- .setLabel(lastClicked == "clear-role" ? "Click again to confirm" : "Clear Role")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setCustomId("clear-role")
- .setDisabled(!config.welcome.role)
- .setStyle(ButtonStyle.Danger),
- new ButtonBuilder()
- .setLabel(lastClicked == "clear-ping" ? "Click again to confirm" : "Clear Ping")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setCustomId("clear-ping")
- .setDisabled(!config.welcome.ping)
- .setStyle(ButtonStyle.Danger),
- new ButtonBuilder()
- .setLabel(lastClicked == "clear-channel" ? "Click again to confirm" : "Clear Channel")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setCustomId("clear-channel")
- .setDisabled(!config.welcome.channel)
- .setStyle(ButtonStyle.Danger),
- new ButtonBuilder()
- .setLabel("Set Channel to DM")
- .setCustomId("set-channel-dm")
- .setDisabled(config.welcome.channel == "dm")
- .setStyle(ButtonStyle.Secondary)
- ])
- ]
- })) as Message;
- let i: MessageComponentInteraction;
+ );
+
+ const channelMenu = new ActionRowBuilder<ChannelSelectMenuBuilder>()
+ .addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("channel")
+ .setPlaceholder("Select a channel to send welcome messages to")
+ );
+ const roleMenu = new ActionRowBuilder<RoleSelectMenuBuilder>()
+ .addComponents(
+ new RoleSelectMenuBuilder()
+ .setCustomId("roleToGive")
+ .setPlaceholder("Select a role to give to the member when they join the server")
+ );
+ const pingMenu = new ActionRowBuilder<RoleSelectMenuBuilder>()
+ .addComponents(
+ new RoleSelectMenuBuilder()
+ .setCustomId("roleToPing")
+ .setPlaceholder("Select a role to ping when a member joins the server")
+ );
+
+ const embed = new EmojiEmbed()
+ .setTitle("Welcome Settings")
+ .setStatus("Success")
+ .setDescription(
+ `${getEmojiByName(data.enabled ? "CONTROL.TICK" : "CONTROL.CROSS")} Welcome messages and roles are ${data.enabled ? "enabled" : "disabled"}\n` +
+ `**Welcome message:** ${data.message ?
+ `\n> ` +
+ await convertCurlyBracketString(
+ data.message,
+ interaction.user.id,
+ interaction.user.username,
+ interaction.guild!.name,
+ interaction.guild!.members
+ )
+ : "*None*"}\n` +
+ `**Send message in:** ` + (data.channel ? (data.channel == "dm" ? "DMs" : renderChannel(data.channel)) : `*None set*`) + `\n` +
+ `**Role to ping:** ` + (data.ping ? `<@&${data.ping}>` : `*None set*`) + `\n` +
+ `**Role given on join:** ` + (data.role ? `<@&${data.role}>` : `*None set*`)
+ )
+
+ await interaction.editReply({
+ embeds: [embed],
+ components: [buttons, channelMenu, roleMenu, pingMenu]
+ });
+
+ let i: RoleSelectMenuInteraction | ChannelSelectMenuInteraction | ButtonInteraction;
try {
i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- });
+ filter: (interaction) => interaction.user.id === interaction.user.id,
+ time: 300000
+ }) as RoleSelectMenuInteraction | ChannelSelectMenuInteraction | ButtonInteraction;
} catch (e) {
- timedOut = true;
+ closed = true;
continue;
}
- i.deferUpdate();
- if (i.customId == "clear-message") {
- if (lastClicked == "clear-message") {
- await client.database.guilds.write(interaction.guild!.id, {
- "welcome.message": null
- });
- lastClicked = null;
- } else {
- lastClicked = "clear-message";
+
+ if(i.isButton()) {
+ switch(i.customId) {
+ case "switch": {
+ await i.deferUpdate();
+ data.enabled = !data.enabled;
+ break;
+ }
+ case "message": {
+ const modal = new ModalBuilder()
+ .setCustomId("modal")
+ .setTitle("Welcome Message")
+ .addComponents(
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("ex1")
+ .setLabel("Server Info (1/3)")
+ .setPlaceholder(
+ `{serverName} - This server's name\n\n` +
+ `These placeholders will be replaced with the server's name, etc..`
+ )
+ .setMaxLength(1)
+ .setRequired(false)
+ .setStyle(TextInputStyle.Paragraph)
+ ),
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("ex2")
+ .setLabel("Member Counts (2/3) - {MemberCount:...}")
+ .setPlaceholder(
+ `{:all} - Total member count\n` +
+ `{:humans} - Total non-bot users\n` +
+ `{:bots} - Number of bots\n`
+ )
+ .setMaxLength(1)
+ .setRequired(false)
+ .setStyle(TextInputStyle.Paragraph)
+ ),
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("ex3")
+ .setLabel("Member who joined (3/3) - {member:...}")
+ .setPlaceholder(
+ `{:name} - The members name\n`
+ )
+ .setMaxLength(1)
+ .setRequired(false)
+ .setStyle(TextInputStyle.Paragraph)
+ ),
+ new ActionRowBuilder<TextInputBuilder>()
+ .addComponents(
+ new TextInputBuilder()
+ .setCustomId("message")
+ .setPlaceholder("Enter a message to send when someone joins the server")
+ .setValue(data.message ?? "")
+ .setLabel("Message")
+ .setStyle(TextInputStyle.Paragraph)
+ )
+ )
+ 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 i.editReply({
+ embeds: [
+ new EmojiEmbed()
+ .setTitle("Welcome Settings")
+ .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) break;
+ data.message = out.fields.getTextInputValue("message");
+ break;
+ }
+ case "save": {
+ await i.deferUpdate();
+ await client.database.guilds.write(interaction.guild!.id, {"welcome": data});
+ config = await client.database.guilds.read(interaction.guild!.id);
+ data = Object.assign({}, config.welcome);
+ break;
+ }
+ case "channelDM": {
+ await i.deferUpdate();
+ data.channel = "dm";
+ break;
+ }
+ case "role": {
+ await i.deferUpdate();
+ data.role = null;
+ break;
+ }
}
- } else if (i.customId == "clear-role") {
- if (lastClicked == "clear-role") {
- await client.database.guilds.write(interaction.guild!.id, {
- "welcome.role": null
- });
- lastClicked = null;
- } else {
- lastClicked = "clear-role";
+ } else if (i.isRoleSelectMenu()) {
+ await i.deferUpdate();
+ switch(i.customId) {
+ case "roleToGive": {
+ data.role = i.values[0]!;
+ break
+ }
+ case "roleToPing": {
+ data.ping = i.values[0]!;
+ break
+ }
}
- } else if (i.customId == "clear-ping") {
- if (lastClicked == "clear-ping") {
- await client.database.guilds.write(interaction.guild!.id, {
- "welcome.ping": null
- });
- lastClicked = null;
- } else {
- lastClicked = "clear-ping";
- }
- } else if (i.customId == "clear-channel") {
- if (lastClicked == "clear-channel") {
- await client.database.guilds.write(interaction.guild!.id, {
- "welcome.channel": null
- });
- lastClicked = null;
- } else {
- lastClicked = "clear-channel";
- }
- } else if (i.customId == "set-channel-dm") {
- await client.database.guilds.write(interaction.guild!.id, {
- "welcome.channel": "dm"
- });
- lastClicked = null;
+ } else {
+ await i.deferUpdate();
+ data.channel = i.values[0]!;
}
- } while (!timedOut);
- await interaction.editReply({
- embeds: [new EmbedBuilder(m.embeds[0]!.data).setFooter({ text: "Message timed out" })],
- components: []
- });
+
+ } while (!closed);
+ await interaction.deleteReply()
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageGuild"))
return "You must have the *Manage Server* permission to use this command";
@@ -309,7 +265,7 @@
};
const autocomplete = async (interaction: AutocompleteInteraction): Promise<string[]> => {
- const validReplacements = ["serverName", "memberCount", "memberCount:bots", "memberCount:humans"]
+ const validReplacements = ["serverName", "memberCount:all", "memberCount:bots", "memberCount:humans"]
if (!interaction.guild) return [];
const prompt = interaction.options.getString("message");
const autocompletions = [];
@@ -324,7 +280,7 @@
if (beforeLastOpenBracket !== null) {
if (afterLastOpenBracket !== null) {
for (const replacement of validReplacements) {
- if (replacement.startsWith(afterLastOpenBracket[0].slice(1))) {
+ if (replacement.startsWith(afterLastOpenBracket[0]!.slice(1))) {
autocompletions.push(`${beforeLastOpenBracket[1]}{${replacement}}`);
}
}
@@ -341,4 +297,4 @@
return autocompletions;
};
-export { command, callback, check, autocomplete };
+export { command, callback, check, autocomplete };
\ No newline at end of file