| 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: 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; |
| } |
| |
| 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": { |
| await client.database.guilds.write(interaction.guild.id, { stats: currentObject }); |
| singleNotify("statsChannelDeleted", interaction.guild.id, true); |
| modified = false; |
| await client.memory.forceUpdate(interaction.guild.id); |
| break; |
| } |
| } |
| } |
| } while (!closed); |
| await interaction.deleteReply(); |
| }; |
| |
| 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 }; |