diff --git a/src/commands/server/buttons.ts b/src/commands/server/buttons.ts new file mode 100644 index 0000000..4a299b2 --- /dev/null +++ b/src/commands/server/buttons.ts @@ -0,0 +1,252 @@ +import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, ChannelType, CommandInteraction, MessageCreateOptions, ModalBuilder, SlashCommandSubcommandBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; +import type Discord from "discord.js"; +import { LoadingEmbed } from "../../utils/defaults.js"; +import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; +import lodash from "lodash"; +import getEmojiByName from "../../utils/getEmojiByName.js"; +import { modalInteractionCollector } from "../../utils/dualCollector.js"; + +export const command = new SlashCommandSubcommandBuilder() + .setName("buttons") + .setDescription("Create clickable buttons for verifying, role menus etc."); + +interface Data { + buttons: string[], + title: string | null, + description: string | null, + color: number, + channel: string | null +} + +const colors: Record = { + RED: 0xF27878, + ORANGE: 0xE5AB71, + YELLOW: 0xF2D478, + GREEN: 0x65CC76, + BLUE: 0x72AEF5, + PURPLE: 0xA358B2, + PINK: 0xD46899, + GRAY: 0x999999, +} + +const buttonNames: Record = { + verifybutton: "Verify", + rolemenu: "Role Menu", + createticket: "Ticket" +} + +export const callback = async (interaction: CommandInteraction): Promise => { + + const m = await interaction.reply({ + embeds: LoadingEmbed, + fetchReply: true, + ephemeral: true + }); + + let closed = false; + let data: Data = { + buttons: [], + title: null, + description: null, + color: colors["RED"]!, + channel: interaction.channelId + } + do { + + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("edit") + .setLabel("Edit Embed") + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId("send") + .setLabel("Send") + .setStyle(ButtonStyle.Primary) + .setDisabled(!data.channel) + ); + + const colorSelect = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("color") + .setPlaceholder("Select a color") + .setMinValues(1) + .addOptions( + Object.keys(colors).map((color: string) => { + return new StringSelectMenuOptionBuilder() + .setLabel(lodash.capitalize(color)) + .setValue(color) + .setEmoji(getEmojiByName("COLORS." + color, "id") as APIMessageComponentEmoji) + .setDefault(data.color === colors[color]) + } + ) + ) + ); + + const buttonSelect = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("button") + .setPlaceholder("Select buttons to add") + .setMinValues(1) + .setMaxValues(3) + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Verify") + .setValue("verifybutton") + .setDescription("Click to get verified in the server") + .setDefault(data.buttons.includes("verifybutton")), + new StringSelectMenuOptionBuilder() + .setLabel("Role Menu") + .setValue("rolemenu") + .setDescription("Click to customize your roles") + .setDefault(data.buttons.includes("rolemenu")), + new StringSelectMenuOptionBuilder() + .setLabel("Ticket") + .setValue("createticket") + .setDescription("Click to create a support ticket") + .setDefault(data.buttons.includes("createticket")) + ) + ) + + const channelMenu = new ActionRowBuilder() + .addComponents( + new ChannelSelectMenuBuilder() + .setCustomId("channel") + .setPlaceholder("Select a channel") + .setChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement, ChannelType.PublicThread, ChannelType.AnnouncementThread) + ) + let channelName = interaction.guild!.channels.cache.get(data.channel!)?.name; + if (data.channel === interaction.channelId) channelName = "this channel"; + const embed = new EmojiEmbed() + .setTitle(data.title ?? "No title set") + .setDescription(data.description ?? "*No description set*") + .setColor(data.color) + .setFooter({text: `Click the button below to edit the embed | The embed will be sent in ${channelName}`}); + + + await interaction.editReply({ + embeds: [embed], + components: [colorSelect, buttonSelect, channelMenu, buttons] + }); + + let i: Discord.ButtonInteraction | Discord.ChannelSelectMenuInteraction | Discord.StringSelectMenuInteraction; + try { + i = await interaction.channel!.awaitMessageComponent({ + filter: (i) => i.user.id === interaction.user.id, + time: 300000 + }) as Discord.ButtonInteraction | Discord.ChannelSelectMenuInteraction | Discord.StringSelectMenuInteraction; + } catch (e) { + closed = true; + break; + } + if(i.isButton()) { + switch(i.customId) { + case "edit": { + await i.showModal( + new ModalBuilder() + .setCustomId("modal") + .setTitle(`Options for ${i.customId}`) + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("title") + .setLabel("Title") + .setMaxLength(256) + .setRequired(false) + .setStyle(TextInputStyle.Short) + .setValue(data.title ?? "") + ), + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("description") + .setLabel("The text to display below the title") + .setMaxLength(4000) + .setRequired(false) + .setStyle(TextInputStyle.Paragraph) + .setValue(data.description ?? "") + ) + ) + ); + await interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("Button Editor") + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus("Success") + .setEmoji("GUILD.TICKET.OPEN") + ], + components: [ + new ActionRowBuilder().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) { + closed = true; + continue; + } + if (!out || out.isButton()) continue + data.title = out.fields.getTextInputValue("title"); + data.description = out.fields.getTextInputValue("description"); + break; + } + case "send": { + await i.deferUpdate(); + let channel = interaction.guild!.channels.cache.get(data.channel!) as Discord.TextChannel; + let components = new ActionRowBuilder(); + for(let button of data.buttons) { + components.addComponents( + new ButtonBuilder() + .setCustomId(button) + .setLabel(buttonNames[button]!) + .setStyle(ButtonStyle.Primary) + ); + } + let messageData: MessageCreateOptions = {components: [components]} + if (data.title || data.description) { + let e = new EmojiEmbed() + if(data.title) e.setTitle(data.title); + if(data.description) e.setDescription(data.description); + if(data.color) e.setColor(data.color); + messageData.embeds = [e]; + } + await channel.send(messageData); + break; + } + } + } else if(i.isStringSelectMenu()) { + await i.deferUpdate(); + switch(i.customId) { + case "color": { + data.color = colors[i.values[0]!]!; + break; + } + case "button": { + data.buttons = i.values; + break; + } + } + } else { + await i.deferUpdate(); + data.channel = i.values[0]!; + } + + } while (!closed); + await interaction.deleteReply(); +} + +export 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; +}; diff --git a/src/config/emojis.json b/src/config/emojis.json index e4afdfb..35743e1 100644 --- a/src/config/emojis.json +++ b/src/config/emojis.json @@ -349,7 +349,7 @@ "TOP": { "ACTIVE": "963122664648630293", "INACTIVE": "963122659862917140", - "GREY": { + "GRAY": { "ACTIVE": "963123505052934144", "INACTIVE": "963123495221469194" } @@ -357,7 +357,7 @@ "MIDDLE": { "ACTIVE": "963122679332880384", "INACTIVE": "963122673246937199", - "GREY": { + "GRAY": { "ACTIVE": "963123517702955018", "INACTIVE": "963123511927390329" } @@ -365,7 +365,7 @@ "BOTTOM": { "ACTIVE": "963122691752218624", "INACTIVE": "963122685691453552", - "GREY": { + "GRAY": { "ACTIVE": "963123529988059187", "INACTIVE": "963123523742748742" } @@ -374,10 +374,20 @@ "SINGLE": { "ACTIVE": "963361162215424060", "INACTIVE": "963361431758176316", - "GREY": { + "GRAY": { "ACTIVE": "963361204695334943", "INACTIVE": "963361200828198952" } } + }, + "COLORS": { + "RED": "875822912802803754", + "ORANGE": "875822913104785418", + "YELLOW": "875822913079611402", + "GREEN": "875822913213841418", + "BLUE": "875822912777637889", + "PURPLE": "875822913213841419", + "PINK": "875822913088020541", + "GRAY": "875822913117368340" } } diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index a22045b..80c2c1b 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -31,14 +31,14 @@ async function modifySuggestion(interaction: Discord.MessageComponentInteraction await message.fetch(); if (message.embeds.length === 0) return; const embed = message.embeds[0]; - const newColour = accept ? "Success" : "Danger"; + const newcolor = accept ? "Success" : "Danger"; const footer = {text: `Suggestion ${accept ? "accepted" : "denied"} by ${interaction.user.tag}`, iconURL: interaction.user.displayAvatarURL()}; const newEmbed = new EmojiEmbed() .setTitle(embed!.title!) .setDescription(embed!.description!) .setFooter(footer) - .setStatus(newColour); + .setStatus(newcolor); await interaction.update({embeds: [newEmbed], components: []}); } diff --git a/src/utils/commandRegistration/register.ts b/src/utils/commandRegistration/register.ts index 6805019..0ff04b3 100644 --- a/src/utils/commandRegistration/register.ts +++ b/src/utils/commandRegistration/register.ts @@ -6,7 +6,7 @@ import fs from "fs"; import EmojiEmbed from '../generateEmojiEmbed.js'; import getEmojiByName from '../getEmojiByName.js'; -const colours = { +const colors = { red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", @@ -26,11 +26,11 @@ async function registerCommands() { for (const file of files) { const last = i === files.length - 1 ? "└" : "├"; if (file.isDirectory()) { - console.log(`${last}─ ${colours.yellow}Loading subcommands of ${file.name}${colours.none}`) + console.log(`${last}─ ${colors.yellow}Loading subcommands of ${file.name}${colors.none}`) const fetched = (await import(`../../../${config.commandsFolder}/${file.name}/_meta.js`)); commands.push(fetched.command); } else if (file.name.endsWith(".js")) { - console.log(`${last}─ ${colours.yellow}Loading command ${file.name}${colours.none}`) + console.log(`${last}─ ${colors.yellow}Loading command ${file.name}${colors.none}`) const fetched = (await import(`../../../${config.commandsFolder}/${file.name}`)); fetched.command.setDMPermission(fetched.allowedInDMs ?? false) fetched.command.setNameLocalizations(fetched.nameLocalizations ?? {}) @@ -43,9 +43,9 @@ async function registerCommands() { ]; } i++; - console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.green}Loaded ${file.name} [${i} / ${files.length}]${colours.none}`) + console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.green}Loaded ${file.name} [${i} / ${files.length}]${colors.none}`) } - console.log(`${colours.yellow}Loaded ${commands.length} commands, processing...`) + console.log(`${colors.yellow}Loaded ${commands.length} commands, processing...`) const processed = [] for (const subcommand of commands) { @@ -56,7 +56,7 @@ async function registerCommands() { } } - console.log(`${colours.green}Processed ${processed.length} commands${colours.none}`) + console.log(`${colors.green}Processed ${processed.length} commands${colors.none}`) return processed; }; @@ -73,15 +73,15 @@ async function registerEvents() { const last = i === files.length - 1 ? "└" : "├"; i++; try { - console.log(`${last}─ ${colours.yellow}Loading event ${file.name}${colours.none}`) + console.log(`${last}─ ${colors.yellow}Loading event ${file.name}${colors.none}`) const event = (await import(`../../../${config.eventsFolder}/${file.name}`)); client.on(event.event, event.callback.bind(null, client)); - console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.green}Loaded ${file.name} [${i} / ${files.length}]${colours.none}`) + console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.green}Loaded ${file.name} [${i} / ${files.length}]${colors.none}`) } catch (e) { errors++; - console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.red}Failed to load ${file.name} [${i} / ${files.length}]${colours.none}`) + console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.red}Failed to load ${file.name} [${i} / ${files.length}]${colors.none}`) } } console.log(`Loaded ${files.length - errors} events (${errors} failed)`) @@ -104,7 +104,7 @@ async function registerContextMenus() { const last = i === totalFiles - 1 ? "└" : "├"; i++; try { - console.log(`${last}─ ${colours.yellow}Loading message context menu ${file.name}${colours.none}`) + console.log(`${last}─ ${colors.yellow}Loading message context menu ${file.name}${colors.none}`) const context = (await import(`../../../${config.messageContextFolder}/${file.name}`)); context.command.setType(ApplicationCommandType.Message); context.command.setDMPermission(context.allowedInDMs ?? false) @@ -113,27 +113,27 @@ async function registerContextMenus() { client.commands["contextCommands/message/" + context.command.name] = context; - console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.green}Loaded ${file.name} [${i} / ${totalFiles}]${colours.none}`) + console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.green}Loaded ${file.name} [${i} / ${totalFiles}]${colors.none}`) } catch (e) { errors++; - console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.red}Failed to load ${file.name} [${i} / ${totalFiles}] | ${e}${colours.none}`) + console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.red}Failed to load ${file.name} [${i} / ${totalFiles}] | ${e}${colors.none}`) } } for (const file of userFiles) { const last = i === totalFiles - 1 ? "└" : "├"; i++; try { - console.log(`${last}─ ${colours.yellow}Loading user context menu ${file.name}${colours.none}`) + console.log(`${last}─ ${colors.yellow}Loading user context menu ${file.name}${colors.none}`) const context = (await import(`../../../${config.userContextFolder}/${file.name}`)); context.command.setType(ApplicationCommandType.User); commands.push(context.command); client.commands["contextCommands/user/" + context.command.name] = context; - console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.green}Loaded ${file.name} [${i} / ${totalFiles}]${colours.none}`) + console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.green}Loaded ${file.name} [${i} / ${totalFiles}]${colors.none}`) } catch (e) { errors++; - console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.red}Failed to load ${file.name} [${i} / ${totalFiles}]${colours.none}`) + console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.red}Failed to load ${file.name} [${i} / ${totalFiles}]${colors.none}`) } } @@ -210,18 +210,18 @@ export default async function register() { if (process.argv.includes("--update-commands")) { if (config.enableDevelopment) { const guild = await client.guilds.fetch(config.developmentGuildID); - console.log(`${colours.purple}Registering commands in ${guild!.name}${colours.none}`) + console.log(`${colors.purple}Registering commands in ${guild!.name}${colors.none}`) await guild.commands.set(commandList); } else { - console.log(`${colours.blue}Registering commands in production mode${colours.none}`) + console.log(`${colors.blue}Registering commands in production mode${colors.none}`) await client.application?.commands.set(commandList); } } await registerCommandHandler(); await registerEvents(); - console.log(`${colours.green}Registered commands, events and context menus${colours.none}`) + console.log(`${colors.green}Registered commands, events and context menus${colors.none}`) console.log( - (config.enableDevelopment ? `${colours.purple}Bot started in Development mode` : - `${colours.blue}Bot started in Production mode`) + colours.none) + (config.enableDevelopment ? `${colors.purple}Bot started in Development mode` : + `${colors.blue}Bot started in Production mode`) + colors.none) // console.log(client.commands) }; diff --git a/src/utils/commandRegistration/slashCommandBuilder.ts b/src/utils/commandRegistration/slashCommandBuilder.ts index ef45875..9a72605 100644 --- a/src/utils/commandRegistration/slashCommandBuilder.ts +++ b/src/utils/commandRegistration/slashCommandBuilder.ts @@ -6,7 +6,7 @@ import client from "../client.js"; import Discord from "discord.js"; -const colours = { +const colors = { red: "\x1b[31m", green: "\x1b[32m", none: "\x1b[0m" @@ -23,7 +23,7 @@ export async function group( // If the name of the command does not match the path (e.g. attachment.ts has /attachments), use commandString console.log(`│ ├─ Loading group ${name}`) const fetched = await getSubcommandsInFolder(config.commandsFolder + "/" + path, "│ ") - console.log(`│ │ └─ ${fetched.errors ? colours.red : colours.green}Loaded ${fetched.subcommands.length} subcommands for ${name} (${fetched.errors} failed)${colours.none}`) + console.log(`│ │ └─ ${fetched.errors ? colors.red : colors.green}Loaded ${fetched.subcommands.length} subcommands for ${name} (${fetched.errors} failed)${colors.none}`) return (subcommandGroup: SlashCommandSubcommandGroupBuilder) => { subcommandGroup .setName(name) @@ -54,7 +54,7 @@ export async function command( // If the name of the command does not match the path (e.g. attachment.ts has /attachments), use commandString commandString = "commands/" + (commandString ?? path); const fetched = await getSubcommandsInFolder(config.commandsFolder + "/" + path); - console.log(`│ ├─ ${fetched.errors ? colours.red : colours.green}Loaded ${fetched.subcommands.length} subcommands and ${fetched.subcommandGroups.length} subcommand groups for ${name} (${fetched.errors} failed)${colours.none}`) + console.log(`│ ├─ ${fetched.errors ? colors.red : colors.green}Loaded ${fetched.subcommands.length} subcommands and ${fetched.subcommandGroups.length} subcommand groups for ${name} (${fetched.errors} failed)${colors.none}`) // console.log({name: name, description: description}) client.commands[commandString!] = [undefined, { name: name, description: description }] return (command: SlashCommandBuilder) => { diff --git a/src/utils/createPageIndicator.ts b/src/utils/createPageIndicator.ts index 29ea83b..6bc86a4 100644 --- a/src/utils/createPageIndicator.ts +++ b/src/utils/createPageIndicator.ts @@ -2,7 +2,7 @@ import getEmojiByName from "./getEmojiByName.js"; function pageIndicator(amount: number, selected: number, showDetails?: boolean, disabled?: boolean | string) { let out = ""; - disabled = disabled ? "GREY." : "" + disabled = disabled ? "GRAY." : "" if (amount === 1) { out += getEmojiByName("TRACKS.SINGLE." + (disabled) + (selected === 0 ? "ACTIVE" : "INACTIVE")); } else { @@ -23,7 +23,7 @@ function pageIndicator(amount: number, selected: number, showDetails?: boolean, export const verticalTrackIndicator = (position: number, active: string | boolean, size: number, disabled: string | boolean) => { active = active ? "ACTIVE" : "INACTIVE"; - disabled = disabled ? "GREY." : ""; + disabled = disabled ? "GRAY." : ""; if (position === 0 && size === 1) return "TRACKS.SINGLE." + disabled + active; if (position === size - 1) return "TRACKS.VERTICAL.BOTTOM." + disabled + active; if (position === 0) return "TRACKS.VERTICAL.TOP." + disabled + active;