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": {
"@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"
},

@ -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<string, { emoji: string }> = {
help: { emoji: "NUCLEUS.LOGO" },
@ -127,7 +127,7 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
);
}
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<void> => {
"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 "

@ -81,7 +81,7 @@ const callback = async (
"Change nickname",
"ICONS.EDIT",
"modal",
newNickname ?? "",
{default: newNickname ?? ""},
new ModalBuilder().setTitle("Editing nickname").addComponents(
new ActionRowBuilder<TextInputBuilder>().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";

@ -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<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 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<void> => {
],
components: [
new ActionRowBuilder<ButtonBuilder>().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,6 +121,8 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
return;
// console.log(interaction)
if (!("awaitMessageComponent" in channel)) return;
let GuildID = interaction.guildId;
if (!GuildID) {
try {
i1 = await channel!.awaitMessageComponent<ComponentType.Button>({
filter: (i) => i.customId === "admin" && i.user.id === interaction.user.id && i.message.id === m.id,
@ -97,8 +143,8 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
return;
}
await out.deferUpdate();
const GuildID = out.fields.getTextInputValue("guildID");
if (!client.guilds.cache.has(GuildID)) {
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<void> => {
components: [
new ActionRowBuilder<ButtonBuilder>().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<void> => {
} 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,6 +182,7 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
return;
}
if (i.customId === "stats") {
await i.deferUpdate();
await interaction.editReply({
embeds: [
new EmojiEmbed()
@ -154,6 +201,17 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
]
});
} 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: [
@ -166,6 +224,7 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
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);
@ -179,6 +238,17 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
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);
@ -194,6 +264,7 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
components: []
});
} else if (i.customId === "cache") {
await i.deferUpdate();
await client.memory.forceUpdate(guild.id);
await interaction.editReply({
embeds: [

@ -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<void> => {
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 });
let closed = false;
let suggestionTitle: string | null = null
let suggestionDesc: string | null = null;
do {
const modal = new ModalBuilder()
.setTitle("Suggestion")
.setComponents(
new ActionRowBuilder<TextInputBuilder>()
.addComponents(
new TextInputBuilder()
.setLabel("Suggestion Title")
.setRequired(false)
.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.OPP.ADD")
.setEmoji("ICONS.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")
.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.cancelled || !confirmation.success) return;
await (client.channels.cache.get("955161206459600976") as Discord.TextChannel).send({
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<Discord.ButtonBuilder>().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<ButtonBuilder>().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<ButtonBuilder>().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()

@ -29,11 +29,11 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
.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<void> => {
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<void> => {
)
.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<void> => {
.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<void> => {
.setLabel("Clear all data")
.setCustomId("clear-all-data")
.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 { 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<void> => {
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<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId("save").setLabel("Save").setStyle(ButtonStyle.Success)
);
let current = _.cloneDeep(config);
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(
new StringSelectMenuBuilder()
.setCustomId("filter")
@ -1055,7 +1098,10 @@ const callback = async (interaction: CommandInteraction): Promise<void> => {
`${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<void> => {
closed = true;
continue;
}
if (i.isButton()) {
await i.deferUpdate();
await client.database.guilds.write(interaction.guild.id, { filters: config });
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;
}

@ -1,7 +1,7 @@
import fs from "fs";
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)",
developmentGuildID: "Your development guild ID",
enableDevelopment: true,
@ -26,15 +26,16 @@ const defaultDict: Record<string, string | string[] | boolean | Record<string, s
authSource: ""
},
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)",
pastebinUsername: "Your pastebin username (optional)",
pastebinPassword: "Your pastebin password (optional)",
rapidApiKey: "Your RapidAPI key (optional), used for Unscan",
clamAVSocket: "Your ClamAV socket file (optional)",
clamAVHost: "Your ClamAV host (optional)",
clamAVPort: "Your ClamAV port (optional)",
clamav: {
socket: "Your ClamAV socket file (optional)",
host: "Your ClamAV host (optional)",
port: "Your ClamAV port (optional)"
}
socket: "",
host: "",
port: 0
},
githubPAT: "Your GitHub Personal Access Token (optional)",
suggestionChannel: "Your suggestion channel ID (optional)"
};
const readline = readLine.createInterface({
@ -116,6 +117,9 @@ export default async function (walkthrough = false) {
case "mongoOptions": {
break;
}
case "clamav": {
break;
}
default: {
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 (!((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) + ";");

@ -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;

@ -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<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();
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 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) {

@ -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<string, { command: string; argument: string }> = {}; // e.g. { channelID: { command: privacy, page: 3}}
commands: Record<
string,

@ -42,7 +42,7 @@ class confirmationMessage {
emoji: string;
customId: string;
modal: Discord.ModalBuilder;
value: string | undefined;
values: Record<string, string>;
}[] = [];
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<string, string>, 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<string, string>;
}[];
}> {
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<Discord.ButtonBuilder>().addComponents(
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setLabel("Back")
.setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
@ -322,7 +322,7 @@ class confirmationMessage {
.setEmoji(this.emoji)
],
components: [
new ActionRowBuilder<Discord.ButtonBuilder>().addComponents(
new ActionRowBuilder<ButtonBuilder>().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;

Loading…
Cancel
Save