diff --git a/package.json b/package.json index c619c61..79bd570 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@hokify/agenda": "^6.2.12", + "@octokit/graphql": "^5.0.5", "@tensorflow/tfjs": "^3.18.0", "@tensorflow/tfjs-node": "^3.18.0", "@total-typescript/ts-reset": "^0.3.7", @@ -25,6 +26,7 @@ "node-fetch": "^3.3.0", "node-tesseract-ocr": "^2.2.1", "nsfwjs": "^2.4.2", + "octokit": "^2.0.14", "seedrandom": "^3.0.5", "structured-clone": "^0.2.2", "systeminformation": "^5.17.3" @@ -48,7 +50,7 @@ "lint-list": "echo 'Style checking...'; prettier --check .; echo 'Linting...'; eslint src; echo 'To view errors in more detail, please run `yarn lint`'; true", "lint-ci": "echo 'Style checking...' && prettier --check . && echo 'Linting...' && eslint src", "setup": "node Installer.js", - "win-force-build": "clear | rm -r dist | tsc-suppress", + "win-force-build": "clear | rm -r dist | tsc-suppress | yarn copy-files", "audit-fix": "yarn-audit-fix", "versions": "yarn versions && yarn list && node --version" }, diff --git a/src/commands/help.ts b/src/commands/help.ts index 96971e6..041fb47 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -22,7 +22,7 @@ import { capitalize } from "../utils/generateKeyValueList.js"; import { getCommandByName, getCommandMentionByName } from "../utils/getCommandDataByName.js"; import getEmojiByName from "../utils/getEmojiByName.js"; -const command = new SlashCommandBuilder().setName("help").setDescription("Shows help for commands"); +const command = new SlashCommandBuilder().setName("help").setDescription("Shows help for commands").setDMPermission(true); const styles: Record = { help: { emoji: "NUCLEUS.LOGO" }, @@ -127,7 +127,7 @@ const callback = async (interaction: CommandInteraction): Promise => { ); } for (const option of options) { - optionString += `> \`${option.name}\` (${ApplicationCommandOptionType[option.type]}) - ${ + optionString += ` - \`${option.name}\` (${ApplicationCommandOptionType[option.type].replace("Integer", "Number").replace("String", "Text")}) - ${ option.description }\n`; } @@ -136,14 +136,12 @@ const callback = async (interaction: CommandInteraction): Promise => { "commands/" + currentPath.filter((value) => value !== "" && value !== "none").join("/") ]![0]; let allowedToRun = true; - if (APICommand?.check) { + if (interaction.guild && APICommand?.check) { allowedToRun = await APICommand.check(interaction as Interaction, true); } embed.setDescription( - `${getEmojiByName(styles[currentPath[0]]!.emoji)} **${capitalize(currentData.name)}**\n> ${ - currentData.mention - }\n\n` + - `> ${currentData.description}\n\n` + + `${getEmojiByName(styles[currentPath[0]]!.emoji)} **${capitalize(currentData.name)}** | ${currentData.mention}\n\n` + + `${currentData.description}\n\n` + (APICommand ? `${getEmojiByName(allowedToRun ? "CONTROL.TICK" : "CONTROL.CROSS")} You ${ allowedToRun ? "" : "don't " diff --git a/src/commands/mod/nick.ts b/src/commands/mod/nick.ts index a8d4ab8..ed14efb 100644 --- a/src/commands/mod/nick.ts +++ b/src/commands/mod/nick.ts @@ -81,7 +81,7 @@ const callback = async ( "Change nickname", "ICONS.EDIT", "modal", - newNickname ?? "", + {default: newNickname ?? ""}, new ModalBuilder().setTitle("Editing nickname").addComponents( new ActionRowBuilder().addComponents( new TextInputBuilder() @@ -103,7 +103,7 @@ const callback = async ( notify = confirmation.components["notify"]!.active; createAppealTicket = confirmation.components["appeal"]!.active; } - if (confirmation.modals) newNickname = confirmation.modals![0]!.value; + if (confirmation.modals) newNickname = confirmation.modals![0]!.values["default"]; } while (!timedOut && !success); if (timedOut || !success) return; let dmSent = false; @@ -222,7 +222,7 @@ const callback = async ( }); }; -const check = async (interaction: CommandInteraction | ButtonInteraction, partial: boolean, target?: GuildMember) => { +const check = (interaction: CommandInteraction | ButtonInteraction, partial: boolean, target?: GuildMember) => { const member = interaction.member as GuildMember; // Check if the user has manage_nicknames permission if (!member.permissions.has("ManageNicknames")) return "You do not have the *Manage Nicknames* permission"; diff --git a/src/commands/nucleus/stats.ts b/src/commands/nucleus/stats.ts index b2658bc..c2bf9a1 100644 --- a/src/commands/nucleus/stats.ts +++ b/src/commands/nucleus/stats.ts @@ -21,6 +21,54 @@ import config from "../../config/main.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder.setName("stats").setDescription("Gets the bot's stats"); + +const confirm = async (interaction: CommandInteraction) => { + const requiredTexts = [ + "just do it", + "yes, do as i say!", + "clicksminuteper/nucleus", + "i've said it once i'll say it again", + "no, i've changed my mind", + "this incident will be reported", + "coded told me to", + "mini told me to", + "pinea told me to", + "what's a java script", + "it's a feature not a bug", + "that never happened during testing" + ] + const chosen = requiredTexts[Math.floor(Math.random() * (requiredTexts.length - 1))]!; + + const modal = new ModalBuilder() + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setStyle(TextInputStyle.Short) + .setLabel(`Type "${chosen}" below`) + .setCustomId("confirm") + .setPlaceholder("Guild ID") + .setMinLength(chosen.length) + .setMaxLength(chosen.length) + ) + ) + .setTitle("Admin Panel") + .setCustomId("adminPanel"); + await interaction.showModal(modal); + let out: ModalSubmitInteraction; + try { + out = await interaction.awaitModalSubmit({ + filter: (i) => i.customId === "adminPanel" && i.user.id === interaction.user.id, + time: 300000 + }); + } catch { + return; + } + await out.deferUpdate(); + const typed = out.fields.getTextInputValue("confirm"); + return typed.toLowerCase() === chosen.toLowerCase() +} + + const callback = async (interaction: CommandInteraction): Promise => { const description = `**Servers:** ${client.guilds.cache.size}\n` + `**Ping:** \`${client.ws.ping * 2}ms\``; const m = await interaction.reply({ @@ -45,11 +93,7 @@ const callback = async (interaction: CommandInteraction): Promise => { ], components: [ new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId("admin").setLabel("Admin Panel").setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId("mod:nickname:599498449733550102") - .setLabel("Testing") - .setStyle(ButtonStyle.Primary) + new ButtonBuilder().setCustomId("admin").setLabel("Admin Panel").setStyle(ButtonStyle.Primary) ) ] }); @@ -77,28 +121,30 @@ const callback = async (interaction: CommandInteraction): Promise => { return; // console.log(interaction) if (!("awaitMessageComponent" in channel)) return; - try { - i1 = await channel!.awaitMessageComponent({ - filter: (i) => i.customId === "admin" && i.user.id === interaction.user.id && i.message.id === m.id, - time: 300000 - }); - } catch (e) { - console.log(e); - return; - } - await i1.showModal(modal); - let out: ModalSubmitInteraction; - try { - out = await i1.awaitModalSubmit({ - filter: (i) => i.customId === "adminPanel" && i.user.id === interaction.user.id, - time: 300000 - }); - } catch { - return; - } - await out.deferUpdate(); - const GuildID = out.fields.getTextInputValue("guildID"); - if (!client.guilds.cache.has(GuildID)) { + let GuildID = interaction.guildId; + if (!GuildID) { + try { + i1 = await channel!.awaitMessageComponent({ + filter: (i) => i.customId === "admin" && i.user.id === interaction.user.id && i.message.id === m.id, + time: 300000 + }); + } catch (e) { + console.log(e); + return; + } + await i1.showModal(modal); + let out: ModalSubmitInteraction; + try { + out = await i1.awaitModalSubmit({ + filter: (i) => i.customId === "adminPanel" && i.user.id === interaction.user.id, + time: 300000 + }); + } catch { + return; + } + await out.deferUpdate(); + GuildID = out.fields.getTextInputValue("guildID"); + } else if (!client.guilds.cache.has(GuildID)) { await interaction.editReply({ embeds: [new EmojiEmbed().setTitle("Admin").setDescription("Not in server").setStatus("Danger")], components: [] @@ -110,10 +156,10 @@ const callback = async (interaction: CommandInteraction): Promise => { components: [ new ActionRowBuilder().addComponents( new ButtonBuilder().setCustomId("stats").setLabel("Stats").setStyle(ButtonStyle.Primary), - new ButtonBuilder().setCustomId("leave").setLabel("Leave").setStyle(ButtonStyle.Danger), new ButtonBuilder().setCustomId("data").setLabel("Guild data").setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId("cache").setLabel("Reset cache").setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId("leave").setLabel("Leave").setStyle(ButtonStyle.Danger), new ButtonBuilder().setCustomId("purge").setLabel("Delete data").setStyle(ButtonStyle.Danger), - new ButtonBuilder().setCustomId("cache").setLabel("Reset cache").setStyle(ButtonStyle.Success) ) ] }); @@ -126,9 +172,9 @@ const callback = async (interaction: CommandInteraction): Promise => { } catch { return; } - await i.deferUpdate(); const guild = (await client.guilds.fetch(GuildID)) as Guild | null; if (!guild) { + await i.deferUpdate(); await interaction.editReply({ embeds: [new EmojiEmbed().setTitle("Admin").setDescription("Not in server").setStatus("Danger")], components: [] @@ -136,66 +182,91 @@ const callback = async (interaction: CommandInteraction): Promise => { return; } if (i.customId === "stats") { + await i.deferUpdate(); await interaction.editReply({ embeds: [ new EmojiEmbed() - .setTitle("Stats") - .setDescription( - `**Name:** ${guild.name}\n` + - `**ID:** \`${guild.id}\`\n` + - `**Owner:** ${client.users.cache.get(guild.ownerId)!.tag}\n` + - `**Member Count:** ${guild.memberCount}\n` + - `**Created:** \n` + - `**Added Nucleus:** \n` + - `**Nucleus' Perms:** https://discordapi.com/permissions.html#${guild.members.me!.permissions.valueOf()}\n` + .setTitle("Stats") + .setDescription( + `**Name:** ${guild.name}\n` + + `**ID:** \`${guild.id}\`\n` + + `**Owner:** ${client.users.cache.get(guild.ownerId)!.tag}\n` + + `**Member Count:** ${guild.memberCount}\n` + + `**Created:** \n` + + `**Added Nucleus:** \n` + + `**Nucleus' Perms:** https://discordapi.com/permissions.html#${guild.members.me!.permissions.valueOf()}\n` ) .setStatus("Success") .setEmoji("SETTINGS.STATS.GREEN") - ] - }); - } else if (i.customId === "leave") { - await guild.leave(); - await interaction.editReply({ - embeds: [ - new EmojiEmbed() + ] + }); + } else if (i.customId === "leave") { + if (!await confirm(interaction)) { + await interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("No changes were made") + .setStatus("Danger") + ], + components: [] + }); + return; + } + await guild.leave(); + await interaction.editReply({ + embeds: [ + new EmojiEmbed() .setTitle("Left") .setDescription(`Left ${guild.name}`) .setStatus("Success") .setEmoji("SETTINGS.STATS.GREEN") - ], - components: [] - }); - } else if (i.customId === "data") { - // Get all the data and convert to a string - const data = await client.database.guilds.read(guild.id); - const stringified = JSON.stringify(data, null, 2); - const buffer = Buffer.from(stringified); - const attachment = new AttachmentBuilder(buffer).setName("data.json"); - await interaction.editReply({ - embeds: [ - new EmojiEmbed().setTitle("Data").setDescription(`Data for ${guild.name}`).setStatus("Success") - ], - components: [], - files: [attachment] - }); - } else if (i.customId === "purge") { - await client.database.guilds.delete(GuildID); - await client.database.history.delete(GuildID); - await client.database.notes.delete(GuildID); - await client.database.transcripts.deleteAll(GuildID); - await interaction.editReply({ - embeds: [ - new EmojiEmbed() + ], + components: [] + }); + } else if (i.customId === "data") { + await i.deferUpdate(); + // Get all the data and convert to a string + const data = await client.database.guilds.read(guild.id); + const stringified = JSON.stringify(data, null, 2); + const buffer = Buffer.from(stringified); + const attachment = new AttachmentBuilder(buffer).setName("data.json"); + await interaction.editReply({ + embeds: [ + new EmojiEmbed().setTitle("Data").setDescription(`Data for ${guild.name}`).setStatus("Success") + ], + components: [], + files: [attachment] + }); + } else if (i.customId === "purge") { + if (!await confirm(interaction)) { + await interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle("No changes were made") + .setStatus("Danger") + ], + components: [] + }); + return; + } + await client.database.guilds.delete(GuildID); + await client.database.history.delete(GuildID); + await client.database.notes.delete(GuildID); + await client.database.transcripts.deleteAll(GuildID); + await interaction.editReply({ + embeds: [ + new EmojiEmbed() .setTitle("Purge") .setDescription(`Deleted data for ${guild.name}`) .setStatus("Success") .setEmoji("SETTINGS.STATS.GREEN") - ], - components: [] - }); - } else if (i.customId === "cache") { - await client.memory.forceUpdate(guild.id); - await interaction.editReply({ + ], + components: [] + }); + } else if (i.customId === "cache") { + await i.deferUpdate(); + await client.memory.forceUpdate(guild.id); + await interaction.editReply({ embeds: [ new EmojiEmbed() .setTitle("Cache") diff --git a/src/commands/nucleus/suggest.ts b/src/commands/nucleus/suggest.ts index 79a0673..44926c7 100644 --- a/src/commands/nucleus/suggest.ts +++ b/src/commands/nucleus/suggest.ts @@ -1,64 +1,115 @@ import { LoadingEmbed } from "../../utils/defaults.js"; -import { ButtonStyle, CommandInteraction } from "discord.js"; -import Discord from "discord.js"; +import Discord, { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; import type { SlashCommandSubcommandBuilder } from "discord.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import client from "../../utils/client.js"; -import getEmojiByName from "../../utils/getEmojiByName.js"; +import config from "../../config/main.js" +import _ from "lodash"; const command = (builder: SlashCommandSubcommandBuilder) => builder .setName("suggest") .setDescription("Sends a suggestion to the developers") - .addStringOption((option) => - option.setName("suggestion").setDescription("The suggestion to send").setRequired(true) - ); const callback = async (interaction: CommandInteraction): Promise => { await interaction.guild?.members.fetch(interaction.member!.user.id); - const { renderUser } = client.logger; - const suggestion = interaction.options.get("suggestion")?.value as string; await interaction.reply({ embeds: LoadingEmbed, ephemeral: true }); - const confirmation = await new confirmationMessage(interaction) - .setEmoji("ICONS.OPP.ADD") - .setTitle("Suggest") - .setDescription( - `**Suggestion:**\n> ${suggestion}\n` + - "Your username and ID will also be sent with your suggestion.\n\nAre you sure you want to send this suggestion?" - ) - .setColor("Danger") - .setInverted(true) - .setFailedMessage("Your suggestion was deleted", "Success", "ICONS.ADD") - .send(true); - if (confirmation.cancelled || !confirmation.success) return; - await (client.channels.cache.get("955161206459600976") as Discord.TextChannel).send({ + let closed = false; + let suggestionTitle: string | null = null + let suggestionDesc: string | null = null; + do { + const modal = new ModalBuilder() + .setTitle("Suggestion") + .setComponents( + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setLabel("Suggestion Title") + .setRequired(false) + .setStyle(TextInputStyle.Short) + .setCustomId("suggestionTitle") + .setPlaceholder("Summarize your suggestion in 1 sentence...") + .setMaxLength(256) + ), + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setLabel("Suggestion Description") + .setCustomId("suggestionDesc") + .setStyle(TextInputStyle.Paragraph) + .setRequired(true) + .setPlaceholder("Put the full details of your suggestion here...") + .setMinLength(50) + ), + ) + const o: {suggestionDesc?: string, suggestionTitle?: string} = {}; + if(suggestionTitle) { + o.suggestionTitle = suggestionTitle; + modal.components[0]!.components[0]!.setValue(suggestionTitle); + } + if(suggestionDesc) { + o.suggestionDesc = suggestionDesc + modal.components[1]!.components[0]!.setValue(suggestionDesc); + }; + const confirmation = await new confirmationMessage(interaction) + .setEmoji("ICONS.ADD") + .setTitle("Suggest") + .setDescription(suggestionDesc ? (`Are you sure you want to send this suggestion?\n\n**Title ${suggestionTitle ? "" : "(*Placeholder*)"}:**\n> ${suggestionTitle ? suggestionTitle : `${suggestionDesc.substring(0, 70)}`}\n\n**Suggestion:**\n> ${suggestionDesc}`) : "Please enter your suggestion below.") + .addModal("Edit Suggestion", "ICONS.EDIT", "editSuggestion", _.cloneDeep(o), modal) + .setColor("Success") + .setInverted(true) + .setFailedMessage("Your suggestion was deleted", "Success", "ICONS.ADD") + .send(true); + if(confirmation.modals?.[0] && !_.isEqual(confirmation.modals[0].values, o)) { + suggestionTitle = confirmation.modals[0].values["suggestionTitle"] as string | null; + suggestionDesc = confirmation.modals[0].values["suggestionDesc"] as string | null; + continue; + } + if(confirmation.cancelled || confirmation.success === false) { + closed = true; + return; + } + if (confirmation.success) { + closed = true; + }; + } while (!closed); + if(!suggestionDesc) return; + suggestionTitle = suggestionTitle ? suggestionTitle : `${suggestionDesc.substring(0, 70)}`; + const channel = client.channels.cache.get(config.suggestionChannel) as Discord.TextChannel; + const m = await channel.send({ embeds: LoadingEmbed}); + const issue = await client.GitHub.rest.issues.create({ + owner: "ClicksMinutePer", + repo: "Nucleus", + title: suggestionTitle, + body: `Linked Suggestion in Private Developer Channel: [Message](${m.url})\n\n**Suggestion:**\n> ${ + suggestionDesc.replaceAll("@", "@").replaceAll("/issues", "/issues").replaceAll("/pull", "/pull") + }\n\n`, + labels: ["🤖 Auto", "📝 Suggestion"] + }) + await m.edit({ embeds: [ new EmojiEmbed() - .setTitle("Suggestion") - .setDescription( - `**From:** ${renderUser( - interaction.member!.user as Discord.User - )}\n**Suggestion:**\n> ${suggestion}\n\n` + - `**Server:** ${interaction.guild!.name} (${interaction.guild!.id})\n` - ) - .setStatus("Warning") + .setEmoji("ICONS.ADD") + .setTitle(`Suggestion from ${interaction.user.tag} (${interaction.user.id})`) + .setDescription(`**Suggestion:**\n> ${suggestionDesc}\n\n`) + .setStatus("Success") + .setFooter({text: `${issue.data.number}`}) ], components: [ - new Discord.ActionRowBuilder().addComponents( - new Discord.ButtonBuilder() - .setCustomId("suggestionAccept") - .setLabel("Accept") - .setStyle(ButtonStyle.Secondary) - .setEmoji(getEmojiByName("ICONS.ADD", "id")), - new Discord.ButtonBuilder() - .setCustomId("suggestionDeny") - .setLabel("Delete") - .setStyle(ButtonStyle.Secondary) - .setEmoji(getEmojiByName("ICONS.REMOVE", "id")) + new Discord.ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId("accept:Suggestion").setLabel("Accept").setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId("deny:Suggestion").setLabel("Deny").setStyle(ButtonStyle.Danger), + new ButtonBuilder().setCustomId("close:Suggestion").setLabel("Close").setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId("implemented:Suggestion").setLabel("Implemented").setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setLabel(`Open Issue #${issue.data.number}`).setStyle(ButtonStyle.Link).setURL(`https://github.com/ClicksMinutePer/Nucleus/issues/${issue.data.number}`), + ), + new Discord.ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId("lock:Suggestion").setLabel("Lock").setStyle(ButtonStyle.Danger), + new ButtonBuilder().setCustomId("spam:Suggestion").setLabel("Mark as Spam").setStyle(ButtonStyle.Danger), ) ] - }); + }) await interaction.editReply({ embeds: [ new EmojiEmbed() diff --git a/src/commands/privacy.ts b/src/commands/privacy.ts index 3a671ed..7fefa36 100644 --- a/src/commands/privacy.ts +++ b/src/commands/privacy.ts @@ -29,11 +29,11 @@ const callback = async (interaction: CommandInteraction): Promise => { .setDescription( "Nucleus is a bot that naturally needs to store data about servers.\n" + "We are entirely [open source](https://github.com/ClicksMinutePer/Nucleus), so you can check exactly what we store, and how it works.\n\n" + - "Any questions about Nucleus, how it works, and what data is stored can be asked in [our server](https://discord.gg/bPaNnxe)." + "Any questions about Nucleus, how it works, and what data is stored can be asked in [our server](https://discord.gg/bPaNnxe)." + + "\n\n[[Privacy Policy]](https://clicksminuteper.github.io/policies/nucleus) | [[Terms of Service]](https://clicksminuteper.github.io/policies/nucleustos)" ) .setEmoji("NUCLEUS.LOGO") .setStatus("Danger") - .setFooter({ text: "https://clicksminuteper.github.io/policies/nucleus" }) ) .setTitle("Welcome") .setDescription("General privacy information") @@ -43,15 +43,16 @@ const callback = async (interaction: CommandInteraction): Promise => { new EmojiEmbed() .setTitle("Scanners") .setDescription( - "Nucleus uses [unscan](https://rapidapi.com/abcdan/api/unscan/) to scan links, images and files for malware and other threats.\n" + - 'This service\'s [privacy policy](https://unscan.co/policies) is public, and they "do not store or sell your data."' + "Nucleus scans content sent by users for malware and NSFW content\n" + + 'Malware is detected using [ClamAV](https://clamav.net/), and the standard ClamAV database."\n' + + 'NSFW detection is provided by [NsfwJS](https://nsfwjs.com/), with a model provided by [GantMan](https://github.com/GantMan/nsfw_model/releases/tag/1.1.0)\n\n' + + 'All data is processed on our servers and is not processed by a 3rd party.' ) .setEmoji("NUCLEUS.LOGO") .setStatus("Danger") - .setFooter({ text: "https://clicksminuteper.github.io/policies/nucleus" }) ) .setTitle("Scanners") - .setDescription("About Unscan") + .setDescription("About Scanners") .setPageId(1), new Embed() .setEmbed( @@ -62,13 +63,12 @@ const callback = async (interaction: CommandInteraction): Promise => { ) .setEmoji("NUCLEUS.LOGO") .setStatus("Danger") - .setFooter({ text: "https://clicksminuteper.github.io/policies/nucleus" }) ) .setTitle("Link scanning and Transcripts") .setDescription("Information about how links and images are scanned, and transcripts are stored") .setPageId(2) ].concat( - (interaction.member as Discord.GuildMember).permissions.has("Administrator") + (interaction.member as Discord.GuildMember).id === interaction.guild!.ownerId ? [ new Embed() .setEmbed( @@ -77,7 +77,6 @@ const callback = async (interaction: CommandInteraction): Promise => { .setDescription("Below are buttons for controlling this servers privacy settings") .setEmoji("NUCLEUS.LOGO") .setStatus("Danger") - .setFooter({ text: "https://clicksminuteper.github.io/policies/nucleus" }) ) .setTitle("Options") .setDescription("Options") @@ -88,7 +87,6 @@ const callback = async (interaction: CommandInteraction): Promise => { .setLabel("Clear all data") .setCustomId("clear-all-data") .setStyle(ButtonStyle.Danger) - .setDisabled(!(interaction.user.id === interaction.guild!.ownerId)) ]) ]) ] diff --git a/src/commands/settings/automod.ts b/src/commands/settings/automod.ts index b65eb4c..68454e4 100644 --- a/src/commands/settings/automod.ts +++ b/src/commands/settings/automod.ts @@ -28,6 +28,7 @@ import client from "../../utils/client.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; import { modalInteractionCollector } from "../../utils/dualCollector.js"; import listToAndMore from "../../utils/listToAndMore.js"; +import _ from "lodash"; const command = (builder: SlashCommandSubcommandBuilder) => builder.setName("automod").setDescription("Setting for automatic moderation features"); @@ -157,6 +158,7 @@ const toSelectMenu = async ( const imageMenu = async ( interaction: StringSelectMenuInteraction, m: Message, + unsavedChanges: boolean, current: { NSFW: boolean; size: boolean; @@ -186,7 +188,10 @@ const imageMenu = async ( .setTitle("Image Settings") .setDescription( `${emojiFromBoolean(current.NSFW)} **NSFW**\n` + `${emojiFromBoolean(current.size)} **Size**\n` - ); + ) + .setFooter({ + text: unsavedChanges ? "No changes made" : "Changes not saved" + }); await interaction.editReply({ embeds: [embed], components: [options] }); @@ -207,10 +212,12 @@ const imageMenu = async ( } case "nsfw": { current.NSFW = !current.NSFW; + unsavedChanges = true; break; } case "size": { current.size = !current.size; + unsavedChanges = true; break; } } @@ -221,6 +228,7 @@ const imageMenu = async ( const wordMenu = async ( interaction: StringSelectMenuInteraction, m: Message, + unsavedChanges: boolean, current: { enabled: boolean; words: { strict: string[]; loose: string[] }; @@ -296,7 +304,10 @@ const wordMenu = async ( ) ) .setStatus("Success") - .setEmoji("GUILD.SETTINGS.GREEN"); + .setEmoji("GUILD.SETTINGS.GREEN") + .setFooter({ + text: unsavedChanges ? "No changes made" : "Changes not saved" + }); await interaction.editReply({ embeds: [embed], components: [selectMenu, buttons] }); @@ -320,6 +331,7 @@ const wordMenu = async ( } case "enabled": { current.enabled = !current.enabled; + unsavedChanges = true; break; } } @@ -391,6 +403,7 @@ const wordMenu = async ( .split(",") .map((s) => s.trim()) .filter((s) => s.length > 0); + unsavedChanges = true; break; } case "allowedUsers": { @@ -402,6 +415,7 @@ const wordMenu = async ( "member", "Word Filter" ); + unsavedChanges = true; break; } case "allowedRoles": { @@ -413,6 +427,7 @@ const wordMenu = async ( "role", "Word Filter" ); + unsavedChanges = true; break; } case "allowedChannels": { @@ -424,6 +439,7 @@ const wordMenu = async ( "channel", "Word Filter" ); + unsavedChanges = true; break; } } @@ -435,6 +451,7 @@ const wordMenu = async ( const inviteMenu = async ( interaction: StringSelectMenuInteraction, m: Message, + unsavedChanges: boolean, current: { enabled: boolean; allowed: { users: string[]; roles: string[]; channels: string[] }; @@ -503,7 +520,10 @@ const inviteMenu = async ( ) ) .setStatus("Success") - .setEmoji("GUILD.SETTINGS.GREEN"); + .setEmoji("GUILD.SETTINGS.GREEN") + .setFooter({ + text: unsavedChanges ? "No changes made" : "Changes not saved" + }); await interaction.editReply({ embeds: [embed], components: [menu, buttons] }); @@ -526,6 +546,7 @@ const inviteMenu = async ( } case "enabled": { current.enabled = !current.enabled; + unsavedChanges = true; break; } } @@ -540,6 +561,7 @@ const inviteMenu = async ( "member", "Invite Settings" ); + unsavedChanges = true; break; } case "roles": { @@ -550,6 +572,7 @@ const inviteMenu = async ( "role", "Invite Settings" ); + unsavedChanges = true; break; } case "channels": { @@ -560,6 +583,7 @@ const inviteMenu = async ( "channel", "Invite Settings" ); + unsavedChanges = true; break; } } @@ -571,6 +595,7 @@ const inviteMenu = async ( const mentionMenu = async ( interaction: StringSelectMenuInteraction, m: Message, + unsavedChanges: boolean, current: { mass: number; everyone: boolean; @@ -690,7 +715,10 @@ const mentionMenu = async ( }` ) .setStatus("Success") - .setEmoji("GUILD.SETTINGS.GREEN"); + .setEmoji("GUILD.SETTINGS.GREEN") + .setFooter({ + text: unsavedChanges ? "No changes made" : "Changes not saved" + });; await interaction.editReply({ embeds: [embed], components: [menu, allowedMenu, buttons] }); @@ -714,10 +742,12 @@ const mentionMenu = async ( } case "everyone": { current.everyone = !current.everyone; + unsavedChanges = true; break; } case "roles": { current.roles = !current.roles; + unsavedChanges = true; break; } } @@ -767,6 +797,7 @@ const mentionMenu = async ( if (!out) break; if (out.isButton()) break; current.mass = parseInt(out.fields.getTextInputValue("mass")); + unsavedChanges = true; break; } case "roles": { @@ -778,6 +809,7 @@ const mentionMenu = async ( "role", "Mention Settings" ); + unsavedChanges = true; break; } } @@ -794,6 +826,7 @@ const mentionMenu = async ( "member", "Mention Settings" ); + unsavedChanges = true; break; } case "roles": { @@ -804,6 +837,7 @@ const mentionMenu = async ( "role", "Mention Settings" ); + unsavedChanges = true; break; } case "channels": { @@ -814,6 +848,7 @@ const mentionMenu = async ( "channel", "Mention Settings" ); + unsavedChanges = true; break; } } @@ -828,6 +863,7 @@ const mentionMenu = async ( const cleanMenu = async ( interaction: StringSelectMenuInteraction, m: Message, + unsavedChanges: boolean, current?: { channels?: string[]; allowed?: { @@ -890,7 +926,10 @@ const cleanMenu = async ( : "None" }\n\n` ) - .setStatus("Success"); + .setStatus("Success") + .setFooter({ + text: unsavedChanges ? "No changes made" : "Changes not saved" + }); await interaction.editReply({ embeds: [embed], components: [channelMenu, allowedMenu, buttons] }); @@ -958,6 +997,7 @@ const cleanMenu = async ( } } } + unsavedChanges = true; break; } case "allowed": { @@ -970,6 +1010,7 @@ const cleanMenu = async ( "member", "Mention Settings" ); + unsavedChanges = true; break; } case "roles": { @@ -980,6 +1021,7 @@ const cleanMenu = async ( "role", "Mention Settings" ); + unsavedChanges = true; break; } } @@ -1001,15 +1043,16 @@ const cleanMenu = async ( const callback = async (interaction: CommandInteraction): Promise => { 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 config = (await client.database.guilds.read(interaction.guild.id)).filters; let closed = false; - const button = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId("save").setLabel("Save").setStyle(ButtonStyle.Success) - ); + let current = _.cloneDeep(config); do { + const button = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId("save").setLabel("Save").setStyle(ButtonStyle.Success).setDisabled(_.isEqual(config, current)) + ); const selectMenu = new ActionRowBuilder().addComponents( new StringSelectMenuBuilder() .setCustomId("filter") @@ -1055,7 +1098,10 @@ const callback = async (interaction: CommandInteraction): Promise => { `${emojiFromBoolean(config.clean.channels.length > 0)} **Clean**\n` ) .setStatus("Success") - .setEmoji("GUILD.SETTINGS.GREEN"); + .setEmoji("GUILD.SETTINGS.GREEN") + .setFooter({ + text: _.isEqual(config, current) ? "No changes made" : "Changes not saved" + }); await interaction.editReply({ embeds: [embed], components: [selectMenu, button] }); @@ -1069,41 +1115,37 @@ const callback = async (interaction: CommandInteraction): Promise => { closed = true; continue; } - if (i.isButton()) { - await i.deferUpdate(); - await client.database.guilds.write(interaction.guild.id, { filters: config }); + await i.deferUpdate(); + if(i.isButton()) { + await client.database.guilds.write(interaction.guild.id, { filters: current }); await client.memory.forceUpdate(interaction.guild.id); + config = current; + current = _.cloneDeep(config); } else { switch (i.values[0]) { case "invites": { - await i.deferUpdate(); - config.invite = await inviteMenu(i, m, config.invite); + config.invite = await inviteMenu(i, m, _.isEqual(config, current), config.invite); break; } case "mentions": { - await i.deferUpdate(); - config.pings = await mentionMenu(i, m, config.pings); + config.pings = await mentionMenu(i, m, _.isEqual(config, current), config.pings); break; } case "words": { - await i.deferUpdate(); - config.wordFilter = await wordMenu(i, m, config.wordFilter); + config.wordFilter = await wordMenu(i, m, _.isEqual(config, current), config.wordFilter); break; } case "malware": { - await i.deferUpdate(); config.malware = !config.malware; break; } case "images": { - await i.deferUpdate(); - const next = await imageMenu(i, m, config.images); + const next = await imageMenu(i, m, _.isEqual(config, current), config.images); config.images = next; break; } case "clean": { - await i.deferUpdate(); - const next = await cleanMenu(i, m, config.clean); + const next = await cleanMenu(i, m, _.isEqual(config, current), config.clean); config.clean = next; break; } diff --git a/src/config/format.ts b/src/config/format.ts index b63debd..da1815c 100644 --- a/src/config/format.ts +++ b/src/config/format.ts @@ -1,7 +1,7 @@ import fs from "fs"; import * as readLine from "node:readline/promises"; -const defaultDict: Record> = { +const defaultDict: Record> = { developmentToken: "Your development bot token (Used for testing in one server, rather than production)", developmentGuildID: "Your development guild ID", enableDevelopment: true, @@ -26,15 +26,16 @@ const defaultDict: Record `); } @@ -127,22 +131,9 @@ export default async function (walkthrough = false) { } if (walkthrough && !(json["mongoUrl"] ?? false)) json["mongoUrl"] = "mongodb://127.0.0.1:27017"; if (!((json["baseUrl"] as string | undefined) ?? "").endsWith("/")) (json["baseUrl"] as string) += "/"; - let hosts; - try { - hosts = fs.readFileSync("/etc/hosts", "utf8").toString().split("\n"); - } catch (e) { - return console.log( - "\x1b[31m⚠ No /etc/hosts found. Please ensure the file exists and is readable. (Windows is not supported, Mac and Linux users should not experience this error)" - ); - } - let localhost: string | undefined = hosts.find((line) => line.split(" ")[1] === "localhost"); - if (localhost) { - localhost = localhost.split(" ")[0]; - } else { - localhost = "127.0.0.1"; - } - json["mongoUrl"] = (json["mongoUrl"]! as string).replace("localhost", localhost!); - json["baseUrl"] = (json["baseUrl"]! as string).replace("localhost", localhost!); + const localhost = "127.0.0.1" + json["mongoUrl"] = (json["mongoUrl"]! as string).replace("localhost", localhost); + json["baseUrl"] = (json["baseUrl"]! as string).replace("localhost", localhost); json["mongoOptions"] = { username: json["username"] as string, password: json["password"] as string, @@ -150,6 +141,11 @@ export default async function (walkthrough = false) { host: json["host"] as string, authSource: json["authSource"] as string }; + json["clamav"] = { + socket: json["clamAVSocket"] as string | undefined, + host: json["clamAVHost"] as string | undefined, + port: json["clamAVPort"] as number | undefined + } fs.writeFileSync("./src/config/main.ts", "export default " + JSON.stringify(json, null, 4) + ";"); diff --git a/src/config/main.d.ts b/src/config/main.d.ts index 8953c52..6c610e0 100644 --- a/src/config/main.d.ts +++ b/src/config/main.d.ts @@ -18,12 +18,13 @@ declare const config: { authSource: string; }; baseUrl: string; - rapidApiKey: string; clamav: { socket?: string; host?: string; port?: number; }; + githubPAT: string; + suggestionChannel: string; }; export default config; diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 3b0cd62..e27ee69 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -4,8 +4,7 @@ import create from "../actions/tickets/create.js"; import close from "../actions/tickets/delete.js"; import createTranscript from "../premium/createTranscript.js"; -import type { ButtonInteraction, Interaction } from "discord.js"; -import type Discord from "discord.js"; +import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, Interaction, InteractionEditReplyOptions, ModalBuilder, ModalSubmitInteraction, TextInputBuilder, TextInputStyle } from "discord.js"; import type { NucleusClient } from "../utils/client.js"; import EmojiEmbed from "../utils/generateEmojiEmbed.js"; @@ -14,6 +13,7 @@ import { callback as kickCallback, check as kickCheck } from "../commands/mod/ki import { callback as muteCallback, check as muteCheck } from "../commands/mod/mute.js"; import { callback as nicknameCallback, check as nicknameCheck } from "../commands/mod/nick.js"; import { callback as warnCallback, check as warnCheck } from "../commands/mod/warn.js"; +import client from "../utils/client.js"; export const event = "interactionCreate"; @@ -27,6 +27,10 @@ async function errorMessage(interaction: ButtonInteraction, message: string) { async function interactionCreate(interaction: Interaction) { if (interaction.isButton()) { + if (interaction.customId.endsWith(":Suggestion")) { + const value = interaction.customId.startsWith("accept") || interaction.customId.startsWith("implement") ? true : false + return await modifySuggestion(interaction, value); + } switch (interaction.customId) { case "rolemenu": { return await roleMenu(interaction); @@ -43,12 +47,6 @@ async function interactionCreate(interaction: Interaction) { case "createtranscript": { return await createTranscript(interaction); } - case "suggestionAccept": { - return await modifySuggestion(interaction, true); - } - case "suggestionDeny": { - return await modifySuggestion(interaction, false); - } } // Mod actions if (interaction.customId.startsWith("mod:")) { @@ -57,27 +55,27 @@ async function interactionCreate(interaction: Interaction) { const member = await interaction.guild?.members.fetch(memberId!); switch (action) { case "kick": { - const check = await kickCheck(interaction, false, member); + const check = kickCheck(interaction, false, member); if (check !== true) return await errorMessage(interaction, check!); return await kickCallback(interaction, member); } case "ban": { - const check = await banCheck(interaction, false, member); + const check = banCheck(interaction, false, member); if (check !== true) return await errorMessage(interaction, check!); return await banCallback(interaction, member); } case "mute": { - const check = await muteCheck(interaction, false, member); + const check = muteCheck(interaction, false, member); if (check !== true) return await errorMessage(interaction, check!); return await muteCallback(interaction, member); } case "nickname": { - const check = await nicknameCheck(interaction, false, member); + const check = nicknameCheck(interaction, false, member); if (check !== true) return await errorMessage(interaction, check || "Something went wrong"); return await nicknameCallback(interaction, member); } case "warn": { - const check = await warnCheck(interaction, false, member); + const check = warnCheck(interaction, false, member); if (check !== true) return await errorMessage(interaction, check!); return await warnCallback(interaction, member); } @@ -86,24 +84,120 @@ async function interactionCreate(interaction: Interaction) { } } -async function modifySuggestion(interaction: Discord.MessageComponentInteraction, accept: boolean) { - const message = await interaction.message; +const getReason = async (buttonInteraction: ButtonInteraction, prompt: string) => { + const modal = new ModalBuilder() + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setStyle(TextInputStyle.Paragraph) + .setLabel(prompt) + .setCustomId("typed") + ) + ) + .setTitle("Reason") + .setCustomId("modal"); + await buttonInteraction.showModal(modal); + let out: ModalSubmitInteraction; + try { + out = await buttonInteraction.awaitModalSubmit({ + filter: (i) => i.customId === "modal" && i.user.id === buttonInteraction.user.id, + time: 300000 + }); + } catch { + return null; + } + await out.deferUpdate(); + return out.fields.getTextInputValue("typed"); +} + +async function modifySuggestion(interaction: ButtonInteraction, accept: boolean) { + const message = interaction.message; await message.fetch(); if (message.embeds.length === 0) return; - const embed = message.embeds[0]; + const embed = message.embeds[0]!; + const issueNum = embed.footer!.text + if(!issueNum) return; + const issue = { + owner: "ClicksMinutePer", + repo: "Nucleus", + issue_number: parseInt(issueNum) + } + let name = "Unknown"; + const components: InteractionEditReplyOptions["components"] = []; + switch(interaction.customId) { + case "accept:Suggestion": { + name = "Accepted"; + await interaction.deferUpdate(); + await client.GitHub.rest.issues.createComment({...issue, body: "Suggestion accepted by " + interaction.user.tag}); + components.push(new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId("close:Suggestion").setLabel("Close").setStyle(ButtonStyle.Secondary), + new ButtonBuilder().setCustomId("implemented:Suggestion").setLabel("Implemented").setStyle(ButtonStyle.Secondary) + )) + break; + } + case "deny:Suggestion": { + name = "Denied"; + const reason = await getReason(interaction, "Reason for denial"); + await client.GitHub.rest.issues.createComment({...issue, body: "Suggestion denied by " + interaction.user.tag + " for reason:\n>" + reason}); + await client.GitHub.rest.issues.update({...issue, state: "closed", state_reason: "not_planned"}); + // await client.GitHub.rest.issues.lock({...issue, lock_reason: "resolved"}) + components.push(new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId("lock:Suggestion").setLabel("Lock").setStyle(ButtonStyle.Danger) + )) + break; + } + case "close:Suggestion": { + name = "Closed"; + const reason = await getReason(interaction, "Reason for closing"); + await client.GitHub.rest.issues.createComment({...issue, body: "Suggestion closed by " + interaction.user.tag + " for reason:\n>" + reason}); + await client.GitHub.rest.issues.update({...issue, state: "closed"}); + // await client.GitHub.rest.issues.lock({...issue}) + components.push(new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId("lock:Suggestion").setLabel("Lock").setStyle(ButtonStyle.Danger) + )) + break; + } + case "implement:Suggestion": { + name = "Implemented"; + await interaction.deferUpdate(); + await client.GitHub.rest.issues.createComment({...issue, body: "Suggestion implemented"}); + await client.GitHub.rest.issues.update({...issue, state: "closed", state_reason: "completed"}); + await client.GitHub.rest.issues.lock({...issue, lock_reason: "resolved"}) + break; + } + case "lock:Suggestion": { + name = "Locked"; + await interaction.deferUpdate(); + await client.GitHub.rest.issues.lock({...issue}); + break; + } + case "spam:Suggestion": { + name = "Marked as Spam"; + await interaction.deferUpdate(); + await client.GitHub.rest.issues.update({...issue, state: "closed", state_reason: "not_planned"}); + await client.GitHub.rest.issues.lock({...issue, lock_reason: "spam"}) + break; + } + } + const newcolor = accept ? "Success" : "Danger"; - const footer = { - text: `Suggestion ${accept ? "accepted" : "denied"} by ${interaction.user.tag}`, - iconURL: interaction.user.displayAvatarURL() - }; + const newEmoji = accept ? "ICONS.ADD" : "ICONS.OPP.ADD"; const newEmbed = new EmojiEmbed() - .setTitle(embed!.title!) + .setEmoji(newEmoji) + .setTitle(embed!.title!.replace(/.+> /, "")) .setDescription(embed!.description!) - .setFooter(footer) - .setStatus(newcolor); + .setFields({ + name: name + " by", + value: interaction.user.tag, + }) + .setStatus(newcolor) + .setFooter(embed!.footer); - await interaction.update({ embeds: [newEmbed], components: [] }); + await interaction.editReply({ + embeds: [newEmbed], + components: components + }); } export async function callback(_client: NucleusClient, interaction: Interaction) { diff --git a/src/utils/client.ts b/src/utils/client.ts index 43f8c5f..6899b90 100644 --- a/src/utils/client.ts +++ b/src/utils/client.ts @@ -6,6 +6,7 @@ import { Guilds, History, ModNotes, Premium, PerformanceTest, ScanCache, Transcr import EventScheduler from "../utils/eventScheduler.js"; import type { RoleMenuSchema } from "../actions/roleMenu.js"; import config from "../config/main.js"; +import { Octokit } from "octokit"; class NucleusClient extends Client { logger = Logger; @@ -24,6 +25,7 @@ class NucleusClient extends Client { scanCache: ScanCache; transcripts: Transcript; }; + GitHub = new Octokit({ auth: config.githubPAT }); preloadPage: Record = {}; // e.g. { channelID: { command: privacy, page: 3}} commands: Record< string, diff --git a/src/utils/confirmationMessage.ts b/src/utils/confirmationMessage.ts index 59befe6..f1229eb 100644 --- a/src/utils/confirmationMessage.ts +++ b/src/utils/confirmationMessage.ts @@ -42,7 +42,7 @@ class confirmationMessage { emoji: string; customId: string; modal: Discord.ModalBuilder; - value: string | undefined; + values: Record; }[] = []; constructor(interaction: CommandInteraction | ButtonInteraction) { @@ -106,9 +106,9 @@ class confirmationMessage { this.reason = reason; return this; } - addModal(buttonText: string, emoji: string, customId: string, current: string, modal: Discord.ModalBuilder) { + addModal(buttonText: string, emoji: string, customId: string, current: Record, modal: Discord.ModalBuilder) { modal.setCustomId(customId); - this.modals.push({ buttonText, emoji, customId, modal, value: current }); + this.modals.push({ buttonText, emoji, customId, modal, values: current }); return this; } async send(editOnly?: boolean): Promise<{ @@ -121,7 +121,7 @@ class confirmationMessage { emoji: string; customId: string; modal: Discord.ModalBuilder; - value: string | undefined; + values: Record; }[]; }> { let cancelled = false; @@ -131,19 +131,19 @@ class confirmationMessage { while (!cancelled && success === undefined && !returnComponents && !newReason) { const fullComponents = [ - new Discord.ButtonBuilder() + new ButtonBuilder() .setCustomId("yes") .setLabel("Confirm") .setStyle(this.inverted ? ButtonStyle.Success : ButtonStyle.Danger) .setEmoji(getEmojiByName("CONTROL.TICK", "id")), - new Discord.ButtonBuilder() + new ButtonBuilder() .setCustomId("no") .setLabel("Cancel") - .setStyle(ButtonStyle.Secondary) + .setStyle(ButtonStyle.Danger) .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) ]; Object.entries(this.customButtons).forEach(([k, v]) => { - const button = new Discord.ButtonBuilder() + const button = new ButtonBuilder() .setCustomId(k) .setLabel(v.title) .setStyle(v.active ? ButtonStyle.Success : ButtonStyle.Primary) @@ -153,7 +153,7 @@ class confirmationMessage { }); for (const modal of this.modals) { fullComponents.push( - new Discord.ButtonBuilder() + new ButtonBuilder() .setCustomId(modal.customId) .setLabel(modal.buttonText) .setStyle(ButtonStyle.Primary) @@ -163,7 +163,7 @@ class confirmationMessage { } if (this.reason !== null) fullComponents.push( - new Discord.ButtonBuilder() + new ButtonBuilder() .setCustomId("reason") .setLabel("Edit Reason") .setStyle(ButtonStyle.Primary) @@ -174,7 +174,7 @@ class confirmationMessage { for (let i = 0; i < fullComponents.length; i += 5) { components.push( new ActionRowBuilder< - | Discord.ButtonBuilder + | ButtonBuilder | Discord.StringSelectMenuBuilder | Discord.RoleSelectMenuBuilder | Discord.UserSelectMenuBuilder @@ -272,7 +272,7 @@ class confirmationMessage { .setEmoji(this.emoji) ], components: [ - new ActionRowBuilder().addComponents( + new ActionRowBuilder().addComponents( new ButtonBuilder() .setLabel("Back") .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) @@ -322,7 +322,7 @@ class confirmationMessage { .setEmoji(this.emoji) ], components: [ - new ActionRowBuilder().addComponents( + new ActionRowBuilder().addComponents( new ButtonBuilder() .setLabel("Back") .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) @@ -350,7 +350,9 @@ class confirmationMessage { continue; } if (out instanceof ModalSubmitInteraction) { - chosenModal!.value = out.fields.getTextInputValue("default"); + out.fields.fields.forEach((f, k) => { + chosenModal!.values[k] = f.value; + }); } returnComponents = true; continue;