updated /nucleus suggest to be nice, fixed help, added needs saving to footer for automod settings.

pull/59/head
TheCodedProf 3 years ago
parent 252e7bd67a
commit 35e7371361

@ -1,6 +1,7 @@
{ {
"dependencies": { "dependencies": {
"@hokify/agenda": "^6.2.12", "@hokify/agenda": "^6.2.12",
"@octokit/graphql": "^5.0.5",
"@tensorflow/tfjs": "^3.18.0", "@tensorflow/tfjs": "^3.18.0",
"@tensorflow/tfjs-node": "^3.18.0", "@tensorflow/tfjs-node": "^3.18.0",
"@total-typescript/ts-reset": "^0.3.7", "@total-typescript/ts-reset": "^0.3.7",
@ -25,6 +26,7 @@
"node-fetch": "^3.3.0", "node-fetch": "^3.3.0",
"node-tesseract-ocr": "^2.2.1", "node-tesseract-ocr": "^2.2.1",
"nsfwjs": "^2.4.2", "nsfwjs": "^2.4.2",
"octokit": "^2.0.14",
"seedrandom": "^3.0.5", "seedrandom": "^3.0.5",
"structured-clone": "^0.2.2", "structured-clone": "^0.2.2",
"systeminformation": "^5.17.3" "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-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", "lint-ci": "echo 'Style checking...' && prettier --check . && echo 'Linting...' && eslint src",
"setup": "node Installer.js", "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", "audit-fix": "yarn-audit-fix",
"versions": "yarn versions && yarn list && node --version" "versions": "yarn versions && yarn list && node --version"
}, },

@ -22,7 +22,7 @@ import { capitalize } from "../utils/generateKeyValueList.js";
import { getCommandByName, getCommandMentionByName } from "../utils/getCommandDataByName.js"; import { getCommandByName, getCommandMentionByName } from "../utils/getCommandDataByName.js";
import getEmojiByName from "../utils/getEmojiByName.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<string, { emoji: string }> = { const styles: Record<string, { emoji: string }> = {
help: { emoji: "NUCLEUS.LOGO" }, help: { emoji: "NUCLEUS.LOGO" },
@ -127,7 +127,7 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
); );
} }
for (const option of options) { 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 option.description
}\n`; }\n`;
} }
@ -136,14 +136,12 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
"commands/" + currentPath.filter((value) => value !== "" && value !== "none").join("/") "commands/" + currentPath.filter((value) => value !== "" && value !== "none").join("/")
]![0]; ]![0];
let allowedToRun = true; let allowedToRun = true;
if (APICommand?.check) { if (interaction.guild && APICommand?.check) {
allowedToRun = await APICommand.check(interaction as Interaction, true); allowedToRun = await APICommand.check(interaction as Interaction, true);
} }
embed.setDescription( embed.setDescription(
`${getEmojiByName(styles[currentPath[0]]!.emoji)} **${capitalize(currentData.name)}**\n> ${ `${getEmojiByName(styles[currentPath[0]]!.emoji)} **${capitalize(currentData.name)}** | ${currentData.mention}\n\n` +
currentData.mention `${currentData.description}\n\n` +
}\n\n` +
`> ${currentData.description}\n\n` +
(APICommand (APICommand
? `${getEmojiByName(allowedToRun ? "CONTROL.TICK" : "CONTROL.CROSS")} You ${ ? `${getEmojiByName(allowedToRun ? "CONTROL.TICK" : "CONTROL.CROSS")} You ${
allowedToRun ? "" : "don't " allowedToRun ? "" : "don't "

@ -81,7 +81,7 @@ const callback = async (
"Change nickname", "Change nickname",
"ICONS.EDIT", "ICONS.EDIT",
"modal", "modal",
newNickname ?? "", {default: newNickname ?? ""},
new ModalBuilder().setTitle("Editing nickname").addComponents( new ModalBuilder().setTitle("Editing nickname").addComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents( new ActionRowBuilder<TextInputBuilder>().addComponents(
new TextInputBuilder() new TextInputBuilder()
@ -103,7 +103,7 @@ const callback = async (
notify = confirmation.components["notify"]!.active; notify = confirmation.components["notify"]!.active;
createAppealTicket = confirmation.components["appeal"]!.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); } while (!timedOut && !success);
if (timedOut || !success) return; if (timedOut || !success) return;
let dmSent = false; 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; const member = interaction.member as GuildMember;
// Check if the user has manage_nicknames permission // Check if the user has manage_nicknames permission
if (!member.permissions.has("ManageNicknames")) return "You do not have the *Manage Nicknames* permission"; if (!member.permissions.has("ManageNicknames")) return "You do not have the *Manage Nicknames* permission";

@ -21,6 +21,54 @@ import config from "../../config/main.js";
const command = (builder: SlashCommandSubcommandBuilder) => const command = (builder: SlashCommandSubcommandBuilder) =>
builder.setName("stats").setDescription("Gets the bot's stats"); 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<TextInputBuilder>().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<void> => { const callback = async (interaction: CommandInteraction): Promise<void> => {
const description = `**Servers:** ${client.guilds.cache.size}\n` + `**Ping:** \`${client.ws.ping * 2}ms\``; const description = `**Servers:** ${client.guilds.cache.size}\n` + `**Ping:** \`${client.ws.ping * 2}ms\``;
const m = await interaction.reply({ const m = await interaction.reply({
@ -45,11 +93,7 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
], ],
components: [ components: [
new ActionRowBuilder<ButtonBuilder>().addComponents( new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId("admin").setLabel("Admin Panel").setStyle(ButtonStyle.Primary), new ButtonBuilder().setCustomId("admin").setLabel("Admin Panel").setStyle(ButtonStyle.Primary)
new ButtonBuilder()
.setCustomId("mod:nickname:599498449733550102")
.setLabel("Testing")
.setStyle(ButtonStyle.Primary)
) )
] ]
}); });
@ -77,28 +121,30 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
return; return;
// console.log(interaction) // console.log(interaction)
if (!("awaitMessageComponent" in channel)) return; if (!("awaitMessageComponent" in channel)) return;
try { let GuildID = interaction.guildId;
i1 = await channel!.awaitMessageComponent<ComponentType.Button>({ if (!GuildID) {
filter: (i) => i.customId === "admin" && i.user.id === interaction.user.id && i.message.id === m.id, try {
time: 300000 i1 = await channel!.awaitMessageComponent<ComponentType.Button>({
}); filter: (i) => i.customId === "admin" && i.user.id === interaction.user.id && i.message.id === m.id,
} catch (e) { time: 300000
console.log(e); });
return; } catch (e) {
} console.log(e);
await i1.showModal(modal); return;
let out: ModalSubmitInteraction; }
try { await i1.showModal(modal);
out = await i1.awaitModalSubmit({ let out: ModalSubmitInteraction;
filter: (i) => i.customId === "adminPanel" && i.user.id === interaction.user.id, try {
time: 300000 out = await i1.awaitModalSubmit({
}); filter: (i) => i.customId === "adminPanel" && i.user.id === interaction.user.id,
} catch { time: 300000
return; });
} } catch {
await out.deferUpdate(); return;
const GuildID = out.fields.getTextInputValue("guildID"); }
if (!client.guilds.cache.has(GuildID)) { await out.deferUpdate();
GuildID = out.fields.getTextInputValue("guildID");
} else if (!client.guilds.cache.has(GuildID)) {
await interaction.editReply({ await interaction.editReply({
embeds: [new EmojiEmbed().setTitle("Admin").setDescription("Not in server").setStatus("Danger")], embeds: [new EmojiEmbed().setTitle("Admin").setDescription("Not in server").setStatus("Danger")],
components: [] components: []
@ -110,10 +156,10 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
components: [ components: [
new ActionRowBuilder<ButtonBuilder>().addComponents( new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId("stats").setLabel("Stats").setStyle(ButtonStyle.Primary), 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("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("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<void> => {
} catch { } catch {
return; return;
} }
await i.deferUpdate();
const guild = (await client.guilds.fetch(GuildID)) as Guild | null; const guild = (await client.guilds.fetch(GuildID)) as Guild | null;
if (!guild) { if (!guild) {
await i.deferUpdate();
await interaction.editReply({ await interaction.editReply({
embeds: [new EmojiEmbed().setTitle("Admin").setDescription("Not in server").setStatus("Danger")], embeds: [new EmojiEmbed().setTitle("Admin").setDescription("Not in server").setStatus("Danger")],
components: [] components: []
@ -136,66 +182,91 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
return; return;
} }
if (i.customId === "stats") { if (i.customId === "stats") {
await i.deferUpdate();
await interaction.editReply({ await interaction.editReply({
embeds: [ embeds: [
new EmojiEmbed() new EmojiEmbed()
.setTitle("Stats") .setTitle("Stats")
.setDescription( .setDescription(
`**Name:** ${guild.name}\n` + `**Name:** ${guild.name}\n` +
`**ID:** \`${guild.id}\`\n` + `**ID:** \`${guild.id}\`\n` +
`**Owner:** ${client.users.cache.get(guild.ownerId)!.tag}\n` + `**Owner:** ${client.users.cache.get(guild.ownerId)!.tag}\n` +
`**Member Count:** ${guild.memberCount}\n` + `**Member Count:** ${guild.memberCount}\n` +
`**Created:** <t:${guild.createdTimestamp}:F>\n` + `**Created:** <t:${guild.createdTimestamp}:F>\n` +
`**Added Nucleus:** <t:${guild.members.me!.joinedTimestamp}:R>\n` + `**Added Nucleus:** <t:${guild.members.me!.joinedTimestamp}:R>\n` +
`**Nucleus' Perms:** https://discordapi.com/permissions.html#${guild.members.me!.permissions.valueOf()}\n` `**Nucleus' Perms:** https://discordapi.com/permissions.html#${guild.members.me!.permissions.valueOf()}\n`
) )
.setStatus("Success") .setStatus("Success")
.setEmoji("SETTINGS.STATS.GREEN") .setEmoji("SETTINGS.STATS.GREEN")
] ]
}); });
} else if (i.customId === "leave") { } else if (i.customId === "leave") {
await guild.leave(); if (!await confirm(interaction)) {
await interaction.editReply({ await interaction.editReply({
embeds: [ embeds: [
new EmojiEmbed() new EmojiEmbed()
.setTitle("No changes were made")
.setStatus("Danger")
],
components: []
});
return;
}
await guild.leave();
await interaction.editReply({
embeds: [
new EmojiEmbed()
.setTitle("Left") .setTitle("Left")
.setDescription(`Left ${guild.name}`) .setDescription(`Left ${guild.name}`)
.setStatus("Success") .setStatus("Success")
.setEmoji("SETTINGS.STATS.GREEN") .setEmoji("SETTINGS.STATS.GREEN")
], ],
components: [] components: []
}); });
} else if (i.customId === "data") { } else if (i.customId === "data") {
// Get all the data and convert to a string await i.deferUpdate();
const data = await client.database.guilds.read(guild.id); // Get all the data and convert to a string
const stringified = JSON.stringify(data, null, 2); const data = await client.database.guilds.read(guild.id);
const buffer = Buffer.from(stringified); const stringified = JSON.stringify(data, null, 2);
const attachment = new AttachmentBuilder(buffer).setName("data.json"); const buffer = Buffer.from(stringified);
await interaction.editReply({ const attachment = new AttachmentBuilder(buffer).setName("data.json");
embeds: [ await interaction.editReply({
new EmojiEmbed().setTitle("Data").setDescription(`Data for ${guild.name}`).setStatus("Success") embeds: [
], new EmojiEmbed().setTitle("Data").setDescription(`Data for ${guild.name}`).setStatus("Success")
components: [], ],
files: [attachment] components: [],
}); files: [attachment]
} else if (i.customId === "purge") { });
await client.database.guilds.delete(GuildID); } else if (i.customId === "purge") {
await client.database.history.delete(GuildID); if (!await confirm(interaction)) {
await client.database.notes.delete(GuildID); await interaction.editReply({
await client.database.transcripts.deleteAll(GuildID); embeds: [
await interaction.editReply({ new EmojiEmbed()
embeds: [ .setTitle("No changes were made")
new EmojiEmbed() .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") .setTitle("Purge")
.setDescription(`Deleted data for ${guild.name}`) .setDescription(`Deleted data for ${guild.name}`)
.setStatus("Success") .setStatus("Success")
.setEmoji("SETTINGS.STATS.GREEN") .setEmoji("SETTINGS.STATS.GREEN")
], ],
components: [] components: []
}); });
} else if (i.customId === "cache") { } else if (i.customId === "cache") {
await client.memory.forceUpdate(guild.id); await i.deferUpdate();
await interaction.editReply({ await client.memory.forceUpdate(guild.id);
await interaction.editReply({
embeds: [ embeds: [
new EmojiEmbed() new EmojiEmbed()
.setTitle("Cache") .setTitle("Cache")

@ -1,64 +1,115 @@
import { LoadingEmbed } from "../../utils/defaults.js"; import { LoadingEmbed } from "../../utils/defaults.js";
import { ButtonStyle, CommandInteraction } from "discord.js"; import Discord, { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
import Discord from "discord.js";
import type { SlashCommandSubcommandBuilder } from "discord.js"; import type { SlashCommandSubcommandBuilder } from "discord.js";
import confirmationMessage from "../../utils/confirmationMessage.js"; import confirmationMessage from "../../utils/confirmationMessage.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import client from "../../utils/client.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) => const command = (builder: SlashCommandSubcommandBuilder) =>
builder builder
.setName("suggest") .setName("suggest")
.setDescription("Sends a suggestion to the developers") .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<void> => { const callback = async (interaction: CommandInteraction): Promise<void> => {
await interaction.guild?.members.fetch(interaction.member!.user.id); 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 }); await interaction.reply({ embeds: LoadingEmbed, ephemeral: true });
const confirmation = await new confirmationMessage(interaction) let closed = false;
.setEmoji("ICONS.OPP.ADD") let suggestionTitle: string | null = null
.setTitle("Suggest") let suggestionDesc: string | null = null;
.setDescription( do {
`**Suggestion:**\n> ${suggestion}\n` + const modal = new ModalBuilder()
"Your username and ID will also be sent with your suggestion.\n\nAre you sure you want to send this suggestion?" .setTitle("Suggestion")
) .setComponents(
.setColor("Danger") new ActionRowBuilder<TextInputBuilder>()
.setInverted(true) .addComponents(
.setFailedMessage("Your suggestion was deleted", "Success", "ICONS.ADD") new TextInputBuilder()
.send(true); .setLabel("Suggestion Title")
if (confirmation.cancelled || !confirmation.success) return; .setRequired(false)
await (client.channels.cache.get("955161206459600976") as Discord.TextChannel).send({ .setStyle(TextInputStyle.Short)
.setCustomId("suggestionTitle")
.setPlaceholder("Summarize your suggestion in 1 sentence...")
.setMaxLength(256)
),
new ActionRowBuilder<TextInputBuilder>()
.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: [ embeds: [
new EmojiEmbed() new EmojiEmbed()
.setTitle("Suggestion") .setEmoji("ICONS.ADD")
.setDescription( .setTitle(`Suggestion from ${interaction.user.tag} (${interaction.user.id})`)
`**From:** ${renderUser( .setDescription(`**Suggestion:**\n> ${suggestionDesc}\n\n`)
interaction.member!.user as Discord.User .setStatus("Success")
)}\n**Suggestion:**\n> ${suggestion}\n\n` + .setFooter({text: `${issue.data.number}`})
`**Server:** ${interaction.guild!.name} (${interaction.guild!.id})\n`
)
.setStatus("Warning")
], ],
components: [ components: [
new Discord.ActionRowBuilder<Discord.ButtonBuilder>().addComponents( new Discord.ActionRowBuilder<ButtonBuilder>().addComponents(
new Discord.ButtonBuilder() new ButtonBuilder().setCustomId("accept:Suggestion").setLabel("Accept").setStyle(ButtonStyle.Success),
.setCustomId("suggestionAccept") new ButtonBuilder().setCustomId("deny:Suggestion").setLabel("Deny").setStyle(ButtonStyle.Danger),
.setLabel("Accept") new ButtonBuilder().setCustomId("close:Suggestion").setLabel("Close").setStyle(ButtonStyle.Secondary),
.setStyle(ButtonStyle.Secondary) new ButtonBuilder().setCustomId("implemented:Suggestion").setLabel("Implemented").setStyle(ButtonStyle.Secondary),
.setEmoji(getEmojiByName("ICONS.ADD", "id")), new ButtonBuilder().setLabel(`Open Issue #${issue.data.number}`).setStyle(ButtonStyle.Link).setURL(`https://github.com/ClicksMinutePer/Nucleus/issues/${issue.data.number}`),
new Discord.ButtonBuilder() ),
.setCustomId("suggestionDeny") new Discord.ActionRowBuilder<ButtonBuilder>().addComponents(
.setLabel("Delete") new ButtonBuilder().setCustomId("lock:Suggestion").setLabel("Lock").setStyle(ButtonStyle.Danger),
.setStyle(ButtonStyle.Secondary) new ButtonBuilder().setCustomId("spam:Suggestion").setLabel("Mark as Spam").setStyle(ButtonStyle.Danger),
.setEmoji(getEmojiByName("ICONS.REMOVE", "id"))
) )
] ]
}); })
await interaction.editReply({ await interaction.editReply({
embeds: [ embeds: [
new EmojiEmbed() new EmojiEmbed()

@ -29,11 +29,11 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
.setDescription( .setDescription(
"Nucleus is a bot that naturally needs to store data about servers.\n" + "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" + "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") .setEmoji("NUCLEUS.LOGO")
.setStatus("Danger") .setStatus("Danger")
.setFooter({ text: "https://clicksminuteper.github.io/policies/nucleus" })
) )
.setTitle("Welcome") .setTitle("Welcome")
.setDescription("General privacy information") .setDescription("General privacy information")
@ -43,15 +43,16 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
new EmojiEmbed() new EmojiEmbed()
.setTitle("Scanners") .setTitle("Scanners")
.setDescription( .setDescription(
"Nucleus uses [unscan](https://rapidapi.com/abcdan/api/unscan/) to scan links, images and files for malware and other threats.\n" + "Nucleus scans content sent by users for malware and NSFW content\n" +
'This service\'s [privacy policy](https://unscan.co/policies) is public, and they "do not store or sell your data."' '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") .setEmoji("NUCLEUS.LOGO")
.setStatus("Danger") .setStatus("Danger")
.setFooter({ text: "https://clicksminuteper.github.io/policies/nucleus" })
) )
.setTitle("Scanners") .setTitle("Scanners")
.setDescription("About Unscan") .setDescription("About Scanners")
.setPageId(1), .setPageId(1),
new Embed() new Embed()
.setEmbed( .setEmbed(
@ -62,13 +63,12 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
) )
.setEmoji("NUCLEUS.LOGO") .setEmoji("NUCLEUS.LOGO")
.setStatus("Danger") .setStatus("Danger")
.setFooter({ text: "https://clicksminuteper.github.io/policies/nucleus" })
) )
.setTitle("Link scanning and Transcripts") .setTitle("Link scanning and Transcripts")
.setDescription("Information about how links and images are scanned, and transcripts are stored") .setDescription("Information about how links and images are scanned, and transcripts are stored")
.setPageId(2) .setPageId(2)
].concat( ].concat(
(interaction.member as Discord.GuildMember).permissions.has("Administrator") (interaction.member as Discord.GuildMember).id === interaction.guild!.ownerId
? [ ? [
new Embed() new Embed()
.setEmbed( .setEmbed(
@ -77,7 +77,6 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
.setDescription("Below are buttons for controlling this servers privacy settings") .setDescription("Below are buttons for controlling this servers privacy settings")
.setEmoji("NUCLEUS.LOGO") .setEmoji("NUCLEUS.LOGO")
.setStatus("Danger") .setStatus("Danger")
.setFooter({ text: "https://clicksminuteper.github.io/policies/nucleus" })
) )
.setTitle("Options") .setTitle("Options")
.setDescription("Options") .setDescription("Options")
@ -88,7 +87,6 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
.setLabel("Clear all data") .setLabel("Clear all data")
.setCustomId("clear-all-data") .setCustomId("clear-all-data")
.setStyle(ButtonStyle.Danger) .setStyle(ButtonStyle.Danger)
.setDisabled(!(interaction.user.id === interaction.guild!.ownerId))
]) ])
]) ])
] ]

@ -28,6 +28,7 @@ import client from "../../utils/client.js";
import getEmojiByName from "../../utils/getEmojiByName.js"; import getEmojiByName from "../../utils/getEmojiByName.js";
import { modalInteractionCollector } from "../../utils/dualCollector.js"; import { modalInteractionCollector } from "../../utils/dualCollector.js";
import listToAndMore from "../../utils/listToAndMore.js"; import listToAndMore from "../../utils/listToAndMore.js";
import _ from "lodash";
const command = (builder: SlashCommandSubcommandBuilder) => const command = (builder: SlashCommandSubcommandBuilder) =>
builder.setName("automod").setDescription("Setting for automatic moderation features"); builder.setName("automod").setDescription("Setting for automatic moderation features");
@ -157,6 +158,7 @@ const toSelectMenu = async (
const imageMenu = async ( const imageMenu = async (
interaction: StringSelectMenuInteraction, interaction: StringSelectMenuInteraction,
m: Message, m: Message,
unsavedChanges: boolean,
current: { current: {
NSFW: boolean; NSFW: boolean;
size: boolean; size: boolean;
@ -186,7 +188,10 @@ const imageMenu = async (
.setTitle("Image Settings") .setTitle("Image Settings")
.setDescription( .setDescription(
`${emojiFromBoolean(current.NSFW)} **NSFW**\n` + `${emojiFromBoolean(current.size)} **Size**\n` `${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] }); await interaction.editReply({ embeds: [embed], components: [options] });
@ -207,10 +212,12 @@ const imageMenu = async (
} }
case "nsfw": { case "nsfw": {
current.NSFW = !current.NSFW; current.NSFW = !current.NSFW;
unsavedChanges = true;
break; break;
} }
case "size": { case "size": {
current.size = !current.size; current.size = !current.size;
unsavedChanges = true;
break; break;
} }
} }
@ -221,6 +228,7 @@ const imageMenu = async (
const wordMenu = async ( const wordMenu = async (
interaction: StringSelectMenuInteraction, interaction: StringSelectMenuInteraction,
m: Message, m: Message,
unsavedChanges: boolean,
current: { current: {
enabled: boolean; enabled: boolean;
words: { strict: string[]; loose: string[] }; words: { strict: string[]; loose: string[] };
@ -296,7 +304,10 @@ const wordMenu = async (
) )
) )
.setStatus("Success") .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] }); await interaction.editReply({ embeds: [embed], components: [selectMenu, buttons] });
@ -320,6 +331,7 @@ const wordMenu = async (
} }
case "enabled": { case "enabled": {
current.enabled = !current.enabled; current.enabled = !current.enabled;
unsavedChanges = true;
break; break;
} }
} }
@ -391,6 +403,7 @@ const wordMenu = async (
.split(",") .split(",")
.map((s) => s.trim()) .map((s) => s.trim())
.filter((s) => s.length > 0); .filter((s) => s.length > 0);
unsavedChanges = true;
break; break;
} }
case "allowedUsers": { case "allowedUsers": {
@ -402,6 +415,7 @@ const wordMenu = async (
"member", "member",
"Word Filter" "Word Filter"
); );
unsavedChanges = true;
break; break;
} }
case "allowedRoles": { case "allowedRoles": {
@ -413,6 +427,7 @@ const wordMenu = async (
"role", "role",
"Word Filter" "Word Filter"
); );
unsavedChanges = true;
break; break;
} }
case "allowedChannels": { case "allowedChannels": {
@ -424,6 +439,7 @@ const wordMenu = async (
"channel", "channel",
"Word Filter" "Word Filter"
); );
unsavedChanges = true;
break; break;
} }
} }
@ -435,6 +451,7 @@ const wordMenu = async (
const inviteMenu = async ( const inviteMenu = async (
interaction: StringSelectMenuInteraction, interaction: StringSelectMenuInteraction,
m: Message, m: Message,
unsavedChanges: boolean,
current: { current: {
enabled: boolean; enabled: boolean;
allowed: { users: string[]; roles: string[]; channels: string[] }; allowed: { users: string[]; roles: string[]; channels: string[] };
@ -503,7 +520,10 @@ const inviteMenu = async (
) )
) )
.setStatus("Success") .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] }); await interaction.editReply({ embeds: [embed], components: [menu, buttons] });
@ -526,6 +546,7 @@ const inviteMenu = async (
} }
case "enabled": { case "enabled": {
current.enabled = !current.enabled; current.enabled = !current.enabled;
unsavedChanges = true;
break; break;
} }
} }
@ -540,6 +561,7 @@ const inviteMenu = async (
"member", "member",
"Invite Settings" "Invite Settings"
); );
unsavedChanges = true;
break; break;
} }
case "roles": { case "roles": {
@ -550,6 +572,7 @@ const inviteMenu = async (
"role", "role",
"Invite Settings" "Invite Settings"
); );
unsavedChanges = true;
break; break;
} }
case "channels": { case "channels": {
@ -560,6 +583,7 @@ const inviteMenu = async (
"channel", "channel",
"Invite Settings" "Invite Settings"
); );
unsavedChanges = true;
break; break;
} }
} }
@ -571,6 +595,7 @@ const inviteMenu = async (
const mentionMenu = async ( const mentionMenu = async (
interaction: StringSelectMenuInteraction, interaction: StringSelectMenuInteraction,
m: Message, m: Message,
unsavedChanges: boolean,
current: { current: {
mass: number; mass: number;
everyone: boolean; everyone: boolean;
@ -690,7 +715,10 @@ const mentionMenu = async (
}` }`
) )
.setStatus("Success") .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] }); await interaction.editReply({ embeds: [embed], components: [menu, allowedMenu, buttons] });
@ -714,10 +742,12 @@ const mentionMenu = async (
} }
case "everyone": { case "everyone": {
current.everyone = !current.everyone; current.everyone = !current.everyone;
unsavedChanges = true;
break; break;
} }
case "roles": { case "roles": {
current.roles = !current.roles; current.roles = !current.roles;
unsavedChanges = true;
break; break;
} }
} }
@ -767,6 +797,7 @@ const mentionMenu = async (
if (!out) break; if (!out) break;
if (out.isButton()) break; if (out.isButton()) break;
current.mass = parseInt(out.fields.getTextInputValue("mass")); current.mass = parseInt(out.fields.getTextInputValue("mass"));
unsavedChanges = true;
break; break;
} }
case "roles": { case "roles": {
@ -778,6 +809,7 @@ const mentionMenu = async (
"role", "role",
"Mention Settings" "Mention Settings"
); );
unsavedChanges = true;
break; break;
} }
} }
@ -794,6 +826,7 @@ const mentionMenu = async (
"member", "member",
"Mention Settings" "Mention Settings"
); );
unsavedChanges = true;
break; break;
} }
case "roles": { case "roles": {
@ -804,6 +837,7 @@ const mentionMenu = async (
"role", "role",
"Mention Settings" "Mention Settings"
); );
unsavedChanges = true;
break; break;
} }
case "channels": { case "channels": {
@ -814,6 +848,7 @@ const mentionMenu = async (
"channel", "channel",
"Mention Settings" "Mention Settings"
); );
unsavedChanges = true;
break; break;
} }
} }
@ -828,6 +863,7 @@ const mentionMenu = async (
const cleanMenu = async ( const cleanMenu = async (
interaction: StringSelectMenuInteraction, interaction: StringSelectMenuInteraction,
m: Message, m: Message,
unsavedChanges: boolean,
current?: { current?: {
channels?: string[]; channels?: string[];
allowed?: { allowed?: {
@ -890,7 +926,10 @@ const cleanMenu = async (
: "None" : "None"
}\n\n` }\n\n`
) )
.setStatus("Success"); .setStatus("Success")
.setFooter({
text: unsavedChanges ? "No changes made" : "Changes not saved"
});
await interaction.editReply({ embeds: [embed], components: [channelMenu, allowedMenu, buttons] }); await interaction.editReply({ embeds: [embed], components: [channelMenu, allowedMenu, buttons] });
@ -958,6 +997,7 @@ const cleanMenu = async (
} }
} }
} }
unsavedChanges = true;
break; break;
} }
case "allowed": { case "allowed": {
@ -970,6 +1010,7 @@ const cleanMenu = async (
"member", "member",
"Mention Settings" "Mention Settings"
); );
unsavedChanges = true;
break; break;
} }
case "roles": { case "roles": {
@ -980,6 +1021,7 @@ const cleanMenu = async (
"role", "role",
"Mention Settings" "Mention Settings"
); );
unsavedChanges = true;
break; break;
} }
} }
@ -1001,15 +1043,16 @@ const cleanMenu = async (
const callback = async (interaction: CommandInteraction): Promise<void> => { const callback = async (interaction: CommandInteraction): Promise<void> => {
if (!interaction.guild) return; if (!interaction.guild) return;
const m = await interaction.reply({ embeds: LoadingEmbed, fetchReply: true, ephemeral: true }); 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; let closed = false;
const button = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId("save").setLabel("Save").setStyle(ButtonStyle.Success)
);
let current = _.cloneDeep(config);
do { do {
const button = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId("save").setLabel("Save").setStyle(ButtonStyle.Success).setDisabled(_.isEqual(config, current))
);
const selectMenu = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents( const selectMenu = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
new StringSelectMenuBuilder() new StringSelectMenuBuilder()
.setCustomId("filter") .setCustomId("filter")
@ -1055,7 +1098,10 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
`${emojiFromBoolean(config.clean.channels.length > 0)} **Clean**\n` `${emojiFromBoolean(config.clean.channels.length > 0)} **Clean**\n`
) )
.setStatus("Success") .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] }); await interaction.editReply({ embeds: [embed], components: [selectMenu, button] });
@ -1069,41 +1115,37 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
closed = true; closed = true;
continue; continue;
} }
if (i.isButton()) { await i.deferUpdate();
await i.deferUpdate(); if(i.isButton()) {
await client.database.guilds.write(interaction.guild.id, { filters: config }); await client.database.guilds.write(interaction.guild.id, { filters: current });
await client.memory.forceUpdate(interaction.guild.id); await client.memory.forceUpdate(interaction.guild.id);
config = current;
current = _.cloneDeep(config);
} else { } else {
switch (i.values[0]) { switch (i.values[0]) {
case "invites": { case "invites": {
await i.deferUpdate(); config.invite = await inviteMenu(i, m, _.isEqual(config, current), config.invite);
config.invite = await inviteMenu(i, m, config.invite);
break; break;
} }
case "mentions": { case "mentions": {
await i.deferUpdate(); config.pings = await mentionMenu(i, m, _.isEqual(config, current), config.pings);
config.pings = await mentionMenu(i, m, config.pings);
break; break;
} }
case "words": { case "words": {
await i.deferUpdate(); config.wordFilter = await wordMenu(i, m, _.isEqual(config, current), config.wordFilter);
config.wordFilter = await wordMenu(i, m, config.wordFilter);
break; break;
} }
case "malware": { case "malware": {
await i.deferUpdate();
config.malware = !config.malware; config.malware = !config.malware;
break; break;
} }
case "images": { case "images": {
await i.deferUpdate(); const next = await imageMenu(i, m, _.isEqual(config, current), config.images);
const next = await imageMenu(i, m, config.images);
config.images = next; config.images = next;
break; break;
} }
case "clean": { case "clean": {
await i.deferUpdate(); const next = await cleanMenu(i, m, _.isEqual(config, current), config.clean);
const next = await cleanMenu(i, m, config.clean);
config.clean = next; config.clean = next;
break; break;
} }

@ -1,7 +1,7 @@
import fs from "fs"; import fs from "fs";
import * as readLine from "node:readline/promises"; import * as readLine from "node:readline/promises";
const defaultDict: Record<string, string | string[] | boolean | Record<string, string | number>> = { const defaultDict: Record<string, string | string[] | boolean | Record<string, string | number | undefined>> = {
developmentToken: "Your development bot token (Used for testing in one server, rather than production)", developmentToken: "Your development bot token (Used for testing in one server, rather than production)",
developmentGuildID: "Your development guild ID", developmentGuildID: "Your development guild ID",
enableDevelopment: true, enableDevelopment: true,
@ -26,15 +26,16 @@ const defaultDict: Record<string, string | string[] | boolean | Record<string, s
authSource: "" authSource: ""
}, },
baseUrl: "Your website where buttons such as Verify and Role menu will link to, e.g. https://example.com/", baseUrl: "Your website where buttons such as Verify and Role menu will link to, e.g. https://example.com/",
pastebinApiKey: "An API key for pastebin (optional)", clamAVSocket: "Your ClamAV socket file (optional)",
pastebinUsername: "Your pastebin username (optional)", clamAVHost: "Your ClamAV host (optional)",
pastebinPassword: "Your pastebin password (optional)", clamAVPort: "Your ClamAV port (optional)",
rapidApiKey: "Your RapidAPI key (optional), used for Unscan",
clamav: { clamav: {
socket: "Your ClamAV socket file (optional)", socket: "",
host: "Your ClamAV host (optional)", host: "",
port: "Your ClamAV port (optional)" port: 0
} },
githubPAT: "Your GitHub Personal Access Token (optional)",
suggestionChannel: "Your suggestion channel ID (optional)"
}; };
const readline = readLine.createInterface({ const readline = readLine.createInterface({
@ -116,6 +117,9 @@ export default async function (walkthrough = false) {
case "mongoOptions": { case "mongoOptions": {
break; break;
} }
case "clamav": {
break;
}
default: { default: {
json[key] = await getInput(`\x1b[36m${key} \x1b[0m(\x1b[35m${defaultDict[key]}\x1b[0m) > `); json[key] = await getInput(`\x1b[36m${key} \x1b[0m(\x1b[35m${defaultDict[key]}\x1b[0m) > `);
} }
@ -127,22 +131,9 @@ export default async function (walkthrough = false) {
} }
if (walkthrough && !(json["mongoUrl"] ?? false)) json["mongoUrl"] = "mongodb://127.0.0.1:27017"; if (walkthrough && !(json["mongoUrl"] ?? false)) json["mongoUrl"] = "mongodb://127.0.0.1:27017";
if (!((json["baseUrl"] as string | undefined) ?? "").endsWith("/")) (json["baseUrl"] as string) += "/"; if (!((json["baseUrl"] as string | undefined) ?? "").endsWith("/")) (json["baseUrl"] as string) += "/";
let hosts; const localhost = "127.0.0.1"
try { json["mongoUrl"] = (json["mongoUrl"]! as string).replace("localhost", localhost);
hosts = fs.readFileSync("/etc/hosts", "utf8").toString().split("\n"); json["baseUrl"] = (json["baseUrl"]! as string).replace("localhost", localhost);
} 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!);
json["mongoOptions"] = { json["mongoOptions"] = {
username: json["username"] as string, username: json["username"] as string,
password: json["password"] as string, password: json["password"] as string,
@ -150,6 +141,11 @@ export default async function (walkthrough = false) {
host: json["host"] as string, host: json["host"] as string,
authSource: json["authSource"] 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) + ";"); fs.writeFileSync("./src/config/main.ts", "export default " + JSON.stringify(json, null, 4) + ";");

@ -18,12 +18,13 @@ declare const config: {
authSource: string; authSource: string;
}; };
baseUrl: string; baseUrl: string;
rapidApiKey: string;
clamav: { clamav: {
socket?: string; socket?: string;
host?: string; host?: string;
port?: number; port?: number;
}; };
githubPAT: string;
suggestionChannel: string;
}; };
export default config; export default config;

@ -4,8 +4,7 @@ import create from "../actions/tickets/create.js";
import close from "../actions/tickets/delete.js"; import close from "../actions/tickets/delete.js";
import createTranscript from "../premium/createTranscript.js"; import createTranscript from "../premium/createTranscript.js";
import type { ButtonInteraction, Interaction } from "discord.js"; import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, Interaction, InteractionEditReplyOptions, ModalBuilder, ModalSubmitInteraction, TextInputBuilder, TextInputStyle } from "discord.js";
import type Discord from "discord.js";
import type { NucleusClient } from "../utils/client.js"; import type { NucleusClient } from "../utils/client.js";
import EmojiEmbed from "../utils/generateEmojiEmbed.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 muteCallback, check as muteCheck } from "../commands/mod/mute.js";
import { callback as nicknameCallback, check as nicknameCheck } from "../commands/mod/nick.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 { callback as warnCallback, check as warnCheck } from "../commands/mod/warn.js";
import client from "../utils/client.js";
export const event = "interactionCreate"; export const event = "interactionCreate";
@ -27,6 +27,10 @@ async function errorMessage(interaction: ButtonInteraction, message: string) {
async function interactionCreate(interaction: Interaction) { async function interactionCreate(interaction: Interaction) {
if (interaction.isButton()) { 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) { switch (interaction.customId) {
case "rolemenu": { case "rolemenu": {
return await roleMenu(interaction); return await roleMenu(interaction);
@ -43,12 +47,6 @@ async function interactionCreate(interaction: Interaction) {
case "createtranscript": { case "createtranscript": {
return await createTranscript(interaction); return await createTranscript(interaction);
} }
case "suggestionAccept": {
return await modifySuggestion(interaction, true);
}
case "suggestionDeny": {
return await modifySuggestion(interaction, false);
}
} }
// Mod actions // Mod actions
if (interaction.customId.startsWith("mod:")) { if (interaction.customId.startsWith("mod:")) {
@ -57,27 +55,27 @@ async function interactionCreate(interaction: Interaction) {
const member = await interaction.guild?.members.fetch(memberId!); const member = await interaction.guild?.members.fetch(memberId!);
switch (action) { switch (action) {
case "kick": { case "kick": {
const check = await kickCheck(interaction, false, member); const check = kickCheck(interaction, false, member);
if (check !== true) return await errorMessage(interaction, check!); if (check !== true) return await errorMessage(interaction, check!);
return await kickCallback(interaction, member); return await kickCallback(interaction, member);
} }
case "ban": { case "ban": {
const check = await banCheck(interaction, false, member); const check = banCheck(interaction, false, member);
if (check !== true) return await errorMessage(interaction, check!); if (check !== true) return await errorMessage(interaction, check!);
return await banCallback(interaction, member); return await banCallback(interaction, member);
} }
case "mute": { case "mute": {
const check = await muteCheck(interaction, false, member); const check = muteCheck(interaction, false, member);
if (check !== true) return await errorMessage(interaction, check!); if (check !== true) return await errorMessage(interaction, check!);
return await muteCallback(interaction, member); return await muteCallback(interaction, member);
} }
case "nickname": { 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"); if (check !== true) return await errorMessage(interaction, check || "Something went wrong");
return await nicknameCallback(interaction, member); return await nicknameCallback(interaction, member);
} }
case "warn": { case "warn": {
const check = await warnCheck(interaction, false, member); const check = warnCheck(interaction, false, member);
if (check !== true) return await errorMessage(interaction, check!); if (check !== true) return await errorMessage(interaction, check!);
return await warnCallback(interaction, member); return await warnCallback(interaction, member);
} }
@ -86,24 +84,120 @@ async function interactionCreate(interaction: Interaction) {
} }
} }
async function modifySuggestion(interaction: Discord.MessageComponentInteraction, accept: boolean) { const getReason = async (buttonInteraction: ButtonInteraction, prompt: string) => {
const message = await interaction.message; const modal = new ModalBuilder()
.addComponents(
new ActionRowBuilder<TextInputBuilder>().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(); await message.fetch();
if (message.embeds.length === 0) return; 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<ButtonBuilder>().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<ButtonBuilder>().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<ButtonBuilder>().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 newcolor = accept ? "Success" : "Danger";
const footer = { const newEmoji = accept ? "ICONS.ADD" : "ICONS.OPP.ADD";
text: `Suggestion ${accept ? "accepted" : "denied"} by ${interaction.user.tag}`,
iconURL: interaction.user.displayAvatarURL()
};
const newEmbed = new EmojiEmbed() const newEmbed = new EmojiEmbed()
.setTitle(embed!.title!) .setEmoji(newEmoji)
.setTitle(embed!.title!.replace(/.+> /, ""))
.setDescription(embed!.description!) .setDescription(embed!.description!)
.setFooter(footer) .setFields({
.setStatus(newcolor); 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) { export async function callback(_client: NucleusClient, interaction: Interaction) {

@ -6,6 +6,7 @@ import { Guilds, History, ModNotes, Premium, PerformanceTest, ScanCache, Transcr
import EventScheduler from "../utils/eventScheduler.js"; import EventScheduler from "../utils/eventScheduler.js";
import type { RoleMenuSchema } from "../actions/roleMenu.js"; import type { RoleMenuSchema } from "../actions/roleMenu.js";
import config from "../config/main.js"; import config from "../config/main.js";
import { Octokit } from "octokit";
class NucleusClient extends Client { class NucleusClient extends Client {
logger = Logger; logger = Logger;
@ -24,6 +25,7 @@ class NucleusClient extends Client {
scanCache: ScanCache; scanCache: ScanCache;
transcripts: Transcript; transcripts: Transcript;
}; };
GitHub = new Octokit({ auth: config.githubPAT });
preloadPage: Record<string, { command: string; argument: string }> = {}; // e.g. { channelID: { command: privacy, page: 3}} preloadPage: Record<string, { command: string; argument: string }> = {}; // e.g. { channelID: { command: privacy, page: 3}}
commands: Record< commands: Record<
string, string,

@ -42,7 +42,7 @@ class confirmationMessage {
emoji: string; emoji: string;
customId: string; customId: string;
modal: Discord.ModalBuilder; modal: Discord.ModalBuilder;
value: string | undefined; values: Record<string, string>;
}[] = []; }[] = [];
constructor(interaction: CommandInteraction | ButtonInteraction) { constructor(interaction: CommandInteraction | ButtonInteraction) {
@ -106,9 +106,9 @@ class confirmationMessage {
this.reason = reason; this.reason = reason;
return this; return this;
} }
addModal(buttonText: string, emoji: string, customId: string, current: string, modal: Discord.ModalBuilder) { addModal(buttonText: string, emoji: string, customId: string, current: Record<string, string>, modal: Discord.ModalBuilder) {
modal.setCustomId(customId); modal.setCustomId(customId);
this.modals.push({ buttonText, emoji, customId, modal, value: current }); this.modals.push({ buttonText, emoji, customId, modal, values: current });
return this; return this;
} }
async send(editOnly?: boolean): Promise<{ async send(editOnly?: boolean): Promise<{
@ -121,7 +121,7 @@ class confirmationMessage {
emoji: string; emoji: string;
customId: string; customId: string;
modal: Discord.ModalBuilder; modal: Discord.ModalBuilder;
value: string | undefined; values: Record<string, string>;
}[]; }[];
}> { }> {
let cancelled = false; let cancelled = false;
@ -131,19 +131,19 @@ class confirmationMessage {
while (!cancelled && success === undefined && !returnComponents && !newReason) { while (!cancelled && success === undefined && !returnComponents && !newReason) {
const fullComponents = [ const fullComponents = [
new Discord.ButtonBuilder() new ButtonBuilder()
.setCustomId("yes") .setCustomId("yes")
.setLabel("Confirm") .setLabel("Confirm")
.setStyle(this.inverted ? ButtonStyle.Success : ButtonStyle.Danger) .setStyle(this.inverted ? ButtonStyle.Success : ButtonStyle.Danger)
.setEmoji(getEmojiByName("CONTROL.TICK", "id")), .setEmoji(getEmojiByName("CONTROL.TICK", "id")),
new Discord.ButtonBuilder() new ButtonBuilder()
.setCustomId("no") .setCustomId("no")
.setLabel("Cancel") .setLabel("Cancel")
.setStyle(ButtonStyle.Secondary) .setStyle(ButtonStyle.Danger)
.setEmoji(getEmojiByName("CONTROL.CROSS", "id")) .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
]; ];
Object.entries(this.customButtons).forEach(([k, v]) => { Object.entries(this.customButtons).forEach(([k, v]) => {
const button = new Discord.ButtonBuilder() const button = new ButtonBuilder()
.setCustomId(k) .setCustomId(k)
.setLabel(v.title) .setLabel(v.title)
.setStyle(v.active ? ButtonStyle.Success : ButtonStyle.Primary) .setStyle(v.active ? ButtonStyle.Success : ButtonStyle.Primary)
@ -153,7 +153,7 @@ class confirmationMessage {
}); });
for (const modal of this.modals) { for (const modal of this.modals) {
fullComponents.push( fullComponents.push(
new Discord.ButtonBuilder() new ButtonBuilder()
.setCustomId(modal.customId) .setCustomId(modal.customId)
.setLabel(modal.buttonText) .setLabel(modal.buttonText)
.setStyle(ButtonStyle.Primary) .setStyle(ButtonStyle.Primary)
@ -163,7 +163,7 @@ class confirmationMessage {
} }
if (this.reason !== null) if (this.reason !== null)
fullComponents.push( fullComponents.push(
new Discord.ButtonBuilder() new ButtonBuilder()
.setCustomId("reason") .setCustomId("reason")
.setLabel("Edit Reason") .setLabel("Edit Reason")
.setStyle(ButtonStyle.Primary) .setStyle(ButtonStyle.Primary)
@ -174,7 +174,7 @@ class confirmationMessage {
for (let i = 0; i < fullComponents.length; i += 5) { for (let i = 0; i < fullComponents.length; i += 5) {
components.push( components.push(
new ActionRowBuilder< new ActionRowBuilder<
| Discord.ButtonBuilder | ButtonBuilder
| Discord.StringSelectMenuBuilder | Discord.StringSelectMenuBuilder
| Discord.RoleSelectMenuBuilder | Discord.RoleSelectMenuBuilder
| Discord.UserSelectMenuBuilder | Discord.UserSelectMenuBuilder
@ -272,7 +272,7 @@ class confirmationMessage {
.setEmoji(this.emoji) .setEmoji(this.emoji)
], ],
components: [ components: [
new ActionRowBuilder<Discord.ButtonBuilder>().addComponents( new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder() new ButtonBuilder()
.setLabel("Back") .setLabel("Back")
.setEmoji(getEmojiByName("CONTROL.LEFT", "id")) .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
@ -322,7 +322,7 @@ class confirmationMessage {
.setEmoji(this.emoji) .setEmoji(this.emoji)
], ],
components: [ components: [
new ActionRowBuilder<Discord.ButtonBuilder>().addComponents( new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder() new ButtonBuilder()
.setLabel("Back") .setLabel("Back")
.setEmoji(getEmojiByName("CONTROL.LEFT", "id")) .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
@ -350,7 +350,9 @@ class confirmationMessage {
continue; continue;
} }
if (out instanceof ModalSubmitInteraction) { if (out instanceof ModalSubmitInteraction) {
chosenModal!.value = out.fields.getTextInputValue("default"); out.fields.fields.forEach((f, k) => {
chosenModal!.values[k] = f.value;
});
} }
returnComponents = true; returnComponents = true;
continue; continue;

Loading…
Cancel
Save