| import { LoadingEmbed } from "../../utils/defaults.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 type { SlashCommandSubcommandBuilder } from "discord.js"; |
| import client from "../../utils/client.js"; |
| import convertCurlyBracketString from "../../utils/convertCurlyBracketString.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") |
| |
| |
| 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..` |
| ) |
| .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: ButtonInteraction | ChannelSelectMenuInteraction; |
| try { |
| 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) { |
| 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 = await modalInteractionCollector( |
| m, |
| (m) => m.channel!.id === interaction.channel!.id && m.user!.id === interaction.user!.id, |
| (i) => i.channel!.id === interaction.channel!.id && i.user!.id === interaction.user!.id && i.message!.id === m.id |
| ) as Discord.ModalSubmitInteraction | null; |
| if (!out) continue; |
| if (!out.fields) 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; |
| } |
| |
| 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, |
| (m) => m.channel!.id === interaction.channel!.id, |
| (_) => true |
| ) as Discord.ModalSubmitInteraction | null; |
| } catch (e) { |
| continue; |
| } |
| if (!out) continue |
| if (!out.fields) 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(); |
| delete currentObject[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; |
| } |
| } |
| |
| } while (!closed); |
| }; |
| |
| 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; |
| }; |
| |
| |
| export { command }; |
| export { callback }; |
| export { check }; |