Development (#11)
We need this NOW.
---------
Co-authored-by: PineaFan <ash@pinea.dev>
Co-authored-by: pineafan <pineapplefanyt@gmail.com>
Co-authored-by: PineappleFan <PineaFan@users.noreply.github.com>
Co-authored-by: Skyler <skyler3665@gmail.com>
diff --git a/src/reflex/guide.ts b/src/reflex/guide.ts
index 6829ef2..a3027e4 100644
--- a/src/reflex/guide.ts
+++ b/src/reflex/guide.ts
@@ -1,9 +1,9 @@
+import { getCommandMentionByName } from './../utils/getCommandDataByName.js';
import { LoadingEmbed } from "../utils/defaults.js";
import Discord, {
ActionRowBuilder,
ButtonBuilder,
MessageComponentInteraction,
- StringSelectMenuInteraction,
Guild,
CommandInteraction,
GuildTextBasedChannel,
@@ -19,32 +19,43 @@
export default async (guild: Guild, interaction?: CommandInteraction) => {
let c: GuildTextBasedChannel | null = guild.publicUpdatesChannel ? guild.publicUpdatesChannel : guild.systemChannel;
c = c
- ? c
- : (guild.channels.cache.find(
- (ch) =>
- [
- ChannelType.GuildText,
- ChannelType.GuildAnnouncement,
- ChannelType.PublicThread,
- ChannelType.PrivateThread,
- ChannelType.AnnouncementThread
- ].includes(ch.type) &&
- ch.permissionsFor(guild.roles.everyone).has("SendMessages") &&
- ch.permissionsFor(guild.members.me!).has("EmbedLinks")
- ) as GuildTextBasedChannel | undefined) ?? null;
+ ? c
+ : (guild.channels.cache.find(
+ (ch) =>
+ [
+ ChannelType.GuildText,
+ ChannelType.GuildAnnouncement,
+ ChannelType.PublicThread,
+ ChannelType.PrivateThread,
+ ChannelType.AnnouncementThread
+ ].includes(ch.type) &&
+ ch.permissionsFor(guild.roles.everyone).has("SendMessages") &&
+ ch.permissionsFor(guild.members.me!).has("EmbedLinks")
+ ) as GuildTextBasedChannel | undefined) ?? null;
if (interaction) c = interaction.channel as GuildTextBasedChannel;
if (!c) {
return;
}
+ let m: Message;
+ if (interaction) {
+ m = (await interaction.reply({
+ embeds: LoadingEmbed,
+ fetchReply: true,
+ ephemeral: true
+ })) as Message;
+ } else {
+ m = await c.send({ embeds: LoadingEmbed });
+ }
+ let page = 0;
const pages = [
new Embed()
.setEmbed(
new EmojiEmbed()
.setTitle("Welcome to Nucleus")
.setDescription(
- "Thanks for adding Nucleus to your server\n\n" +
- "On the next few pages you can find instructions on getting started, and commands you may want to set up\n\n" +
- "If you need support, have questions or want features, you can let us know in [Clicks](https://discord.gg/bPaNnxe)"
+ "Thanks for adding Nucleus to your server!\n\n" +
+ "The next few pages will show what features Nucleus has to offer, and how to enable them.\n\n" +
+ "If you need support, have questions or want features, you can let us know in [Clicks](https://discord.gg/bPaNnxe)!"
)
.setEmoji("NUCLEUS.LOGO")
.setStatus("Danger")
@@ -55,15 +66,17 @@
new Embed()
.setEmbed(
new EmojiEmbed()
- .setTitle("Logging")
+ .setTitle("Logs")
.setDescription(
"Nucleus can log server events and keep you informed with what content is being posted to your server.\n" +
"We have 2 different types of logs, which each can be configured to send to a channel of your choice:\n" +
- "**General Logs:** These are events like kicks and channel changes etc.\n" +
- "**Warning Logs:** Warnings like NSFW avatars and spam etc. that may require action by a server staff member. " +
- "These go to to a separate staff notifications channel.\n\n" +
- "A general log channel can be set with `/settings log`\n" +
- "A warning log channel can be set with `/settings warnings channel`"
+ "**General:** These are events like kicks and channel changes etc.\n" +
+ `> These are standard logs and can be set with ${getCommandMentionByName("settings/logs/general")}\n` +
+ "**Warnings:** Warnings like NSFW avatars and spam etc. that may require action by a server staff member.\n" +
+ `> These may require special action by a moderator. You can set the channel with ${getCommandMentionByName("settings/logs/warnings")}\n` +
+ "**Attachments:** All images sent in the server - Used to keep a record of deleted images\n" +
+ `> Sent to a separate log channel to avoid spam. This can be set with ${getCommandMentionByName("settings/logs/attachments")}\n` +
+ `> ${getEmojiByName("NUCLEUS.PREMIUM")} Please note this feature is only available with ${getCommandMentionByName("nucleus/premium")}`
)
.setEmoji("ICONS.LOGGING")
.setStatus("Danger")
@@ -77,27 +90,15 @@
.setTitle("Moderation")
.setDescription(
"Nucleus has a number of commands that can be used to moderate your server.\n" +
- "These commands are all found under `/mod`, and they include:\n" +
- `**${getEmojiByName(
- "PUNISH.WARN.YELLOW"
- )} Warn:** The user is warned (via DM) that they violated server rules.\n` +
- `**${getEmojiByName(
- "PUNISH.CLEARHISTORY"
- )} Clear:** Some messages from a user are deleted in a channel.\n` +
- `**${getEmojiByName(
- "PUNISH.MUTE.YELLOW"
- )} Mute:** The user is unable to send messages or join voice chats.\n` +
- `**${getEmojiByName(
- "PUNISH.MUTE.GREEN"
- )} Unmute:** The user is able to send messages in the server.\n` +
- `**${getEmojiByName("PUNISH.KICK.RED")} Kick:** The user is removed from the server.\n` +
- `**${getEmojiByName(
- "PUNISH.SOFTBAN"
- )} Softban:** Kicks the user, deleting their messages from every channel.\n` +
- `**${getEmojiByName(
- "PUNISH.BAN.RED"
- )} Ban:** The user is removed from the server, and they are unable to rejoin.\n` +
- `**${getEmojiByName("PUNISH.BAN.GREEN")} Unban:** The user is able to rejoin the server.`
+ `These commands are all found under ${getCommandMentionByName(("mod"))}, and they include:\n` +
+ `${getEmojiByName("PUNISH.WARN.YELLOW")} ${getCommandMentionByName("mod/warn")}: The user is warned (via DM) that they violated server rules. More options given if DMs are disabled.\n` +
+ `${getEmojiByName("PUNISH.CLEARHISTORY")} ${getCommandMentionByName("mod/purge")}: Deletes messages in a channel, giving options to only delete messages by a certain user.\n` +
+ `${getEmojiByName("PUNISH.MUTE.YELLOW")} ${getCommandMentionByName("mod/mute")}: Stops users sending messages or joining voice chats.\n` +
+ `${getEmojiByName("PUNISH.MUTE.GREEN")} ${getCommandMentionByName("mod/unmute")}: Allows user to send messages and join voice chats.\n` +
+ `${getEmojiByName("PUNISH.KICK.RED")} ${getCommandMentionByName("mod/kick")}: Removes a member from the server. They will be able to rejoin.\n` +
+ `${getEmojiByName("PUNISH.SOFTBAN")} ${getCommandMentionByName("mod/softban")}: Kicks the user, deleting their messages from every channel in a given time frame.\n` +
+ `${getEmojiByName("PUNISH.BAN.RED")} ${getCommandMentionByName("mod/ban")}: Removes the user from the server, deleting messages from every channel and stops them from rejoining.\n` +
+ `${getEmojiByName("PUNISH.BAN.GREEN")} ${getCommandMentionByName("mod/unban")}: Allows a member to rejoin the server after being banned.`
)
.setEmoji("PUNISH.BAN.RED")
.setStatus("Danger")
@@ -111,9 +112,9 @@
.setTitle("Verify")
.setDescription(
"Nucleus has a verification system that allows users to prove they aren't bots.\n" +
- "This is done by running `/verify` which sends a message only the user can see, giving them a link to a CAPTCHA to verify.\n" +
- "After the user complete's the CAPTCHA, they are given a role and can use the permissions accordingly.\n" +
- "You can set the role given with `/settings verify`"
+ `This is done by running ${getCommandMentionByName("verify")} which sends a message only the user can see, giving them a link to a website to verify.\n` +
+ "After the user complete's the check, they are given a role, which can be set to unlock specific channels.\n" +
+ `You can set the role given with ${getCommandMentionByName("settings/verify")}`
)
.setEmoji("CONTROL.REDTICK")
.setStatus("Danger")
@@ -127,8 +128,8 @@
.setTitle("Content Scanning")
.setDescription(
"Nucleus has a content scanning system that automatically scans links and images sent by users.\n" +
- "Nucleus can detect, delete, and punish users for sending NSFW content, or links to scam or adult sites.\n" +
- "You can set the threshold for this in `/settings automation`" // TODO
+ "The staff team can be notified when an NSFW image is detected, or malicious links are sent.\n" +
+ `You can check and manage what to moderate in ${getCommandMentionByName("settings/automod")}`
)
.setEmoji("MOD.IMAGES.TOOSMALL")
.setStatus("Danger")
@@ -141,10 +142,12 @@
new EmojiEmbed()
.setTitle("Tickets")
.setDescription(
- "Nucleus has a ticket system that allows users to create tickets and have a support team respond to them.\n" +
- "Tickets can be created with `/ticket create` and a channel is created, pinging the user and support role.\n" +
- "When the ticket is resolved, anyone can run `/ticket close` (or click the button) to close it.\n" +
- "Running `/ticket close` again will delete the ticket."
+ "Nucleus has a ticket system which allows users to create tickets and talk to the server staff or support team.\n" +
+ `Tickets can be created by users with ${getCommandMentionByName("ticket/create")}, or by clicking a button created by moderators.\n` +
+ `After being created, a new channel or thread is created, and the user and support team are pinged. \n` +
+ `The category or channel to create threads in can be set with ${getCommandMentionByName("settings/tickets")}\n` +
+ `When the ticket is resolved, anyone can run ${getCommandMentionByName("ticket/close")} (or click the button) to close it.\n` +
+ `Running ${getCommandMentionByName("ticket/close")} again will delete the ticket.`
)
.setEmoji("GUILD.TICKET.CLOSE")
.setStatus("Danger")
@@ -157,11 +160,10 @@
new EmojiEmbed()
.setTitle("Tags")
.setDescription(
- "Add a tag system to your server with the `/tag` and `/tags` commands.\n" +
- "To create a tag, type `/tags create <tag name> <tag content>`.\n" +
- "Tag names and content can be edited with `/tags edit`.\n" +
- "To delete a tag, type `/tags delete <tag name>`.\n" +
- "To view all tags, type `/tags list`.\n"
+ "Nucleus allows you to create tags, which allow a message to be sent when a specific tag is typed.\n" +
+ `Tags can be created with ${getCommandMentionByName("tags/create")}, and can be edited with ${getCommandMentionByName("tags/edit")}\n` +
+ `Tags can be deleted with ${getCommandMentionByName("tags/delete")}, and can be listed with ${getCommandMentionByName("tags/list")}\n` +
+ `To use a tag, you can type ${getCommandMentionByName("tag")}, followed by the tag to send`
)
.setEmoji("PUNISH.NICKNAME.RED")
.setStatus("Danger")
@@ -174,29 +176,18 @@
new EmojiEmbed()
.setTitle("Premium")
.setDescription(
- "In the near future, we will be releasing extra premium only features.\n" +
- "These features will include:\n\n" +
- "**Attachment logs**\n> When a message with attachments is edited or deleted, the logs will also include the images sent.\n" +
- "\nPremium is not yet available. Check `/nucleus premium` for updates on features and pricing"
+ "Nucleus Premium allows you to use extra features in your server, which are useful but not essential.\n" +
+ "**No currently free commands will become premium features.**\n" +
+ "Premium features include creating ticket transcripts and attachment logs.\n\n" +
+ "Premium can be purchased in [our server](https://discord.gg/bPaNnxe) in the subscriptions page" // TODO: add a table graphic
)
- .setEmoji("NUCLEUS.COMMANDS.LOCK")
+ .setEmoji("NUCLEUS.PREMIUM")
.setStatus("Danger")
)
.setTitle("Premium")
.setDescription("Premium features")
.setPageId(7)
];
- let m: Message;
- if (interaction) {
- m = (await interaction.reply({
- embeds: LoadingEmbed,
- fetchReply: true,
- ephemeral: true
- })) as Message;
- } else {
- m = await c.send({ embeds: LoadingEmbed });
- }
- let page = 0;
const publicFilter = async (component: MessageComponentInteraction) => {
return (component.member as Discord.GuildMember).permissions.has("ManageGuild");
@@ -274,7 +265,7 @@
timedOut = true;
continue;
}
- i.deferUpdate();
+ await i.deferUpdate();
if (!("customId" in i.component)) {
continue;
} else if (i.component.customId === "left") {
@@ -285,8 +276,8 @@
selectPaneOpen = false;
} else if (i.component.customId === "select") {
selectPaneOpen = !selectPaneOpen;
- } else if (i.component.customId === "page") {
- page = parseInt((i as StringSelectMenuInteraction).values[0]!);
+ } else if (i.isStringSelectMenu() && i.component.customId === "page") {
+ page = parseInt(i.values[0]!);
selectPaneOpen = false;
} else {
cancelled = true;
diff --git a/src/reflex/scanners.ts b/src/reflex/scanners.ts
index 9761e4b..cf713e6 100644
--- a/src/reflex/scanners.ts
+++ b/src/reflex/scanners.ts
@@ -1,23 +1,27 @@
import fetch from "node-fetch";
-import FormData from "form-data";
-import { writeFileSync, createReadStream } from "fs";
+import fs, { writeFileSync, createReadStream } from "fs";
import generateFileName from "../utils/temp/generateFileName.js";
import Tesseract from "node-tesseract-ocr";
import type Discord from "discord.js";
import client from "../utils/client.js";
+import { createHash } from "crypto";
interface NSFWSchema {
nsfw: boolean;
+ errored?: boolean;
}
interface MalwareSchema {
safe: boolean;
+ errored?: boolean;
}
export async function testNSFW(link: string): Promise<NSFWSchema> {
- const p = await saveAttachment(link);
- const data = new FormData();
- console.log(link);
- data.append("file", createReadStream(p));
+ const [p, hash] = await saveAttachment(link);
+ const alreadyHaveCheck = await client.database.scanCache.read(hash)
+ if(alreadyHaveCheck) return { nsfw: alreadyHaveCheck.data };
+ const data = new URLSearchParams();
+ const r = createReadStream(p)
+ data.append("file", r.read(fs.statSync(p).size));
const result = await fetch("https://unscan.p.rapidapi.com/", {
method: "POST",
headers: {
@@ -26,20 +30,24 @@
},
body: data
})
- .then((response) => response.json() as Promise<NSFWSchema>)
+ .then((response) => response.status === 200 ? response.json() as Promise<NSFWSchema> : { nsfw: false, errored: true })
.catch((err) => {
console.error(err);
- return { nsfw: false };
+ return { nsfw: false, errored: true };
});
- console.log(result);
+ if(!result.errored) {
+ client.database.scanCache.write(hash, result.nsfw);
+ }
return { nsfw: result.nsfw };
}
export async function testMalware(link: string): Promise<MalwareSchema> {
- const p = await saveAttachment(link);
- const data = new FormData();
- data.append("file", createReadStream(p));
- console.log(link);
+ const [p, hash] = await saveAttachment(link);
+ const alreadyHaveCheck = await client.database.scanCache.read(hash)
+ if(alreadyHaveCheck) return { safe: alreadyHaveCheck.data };
+ const data = new URLSearchParams();
+ const f = createReadStream(p);
+ data.append("file", f.read(fs.statSync(p).size));
const result = await fetch("https://unscan.p.rapidapi.com/malware", {
method: "POST",
headers: {
@@ -48,18 +56,21 @@
},
body: data
})
- .then((response) => response.json() as Promise<MalwareSchema>)
+ .then((response) => response.status === 200 ? response.json() as Promise<MalwareSchema> : { safe: true, errored: true })
.catch((err) => {
console.error(err);
- return { safe: true };
+ return { safe: true, errored: true };
});
- console.log(result);
+ if (!result.errored) {
+ client.database.scanCache.write(hash, result.safe);
+ }
return { safe: result.safe };
}
export async function testLink(link: string): Promise<{ safe: boolean; tags: string[] }> {
- console.log(link);
- const scanned: { safe?: boolean; tags?: string[] } = await fetch("https://unscan.p.rapidapi.com/malware", {
+ const alreadyHaveCheck = await client.database.scanCache.read(link)
+ if(alreadyHaveCheck) return { safe: alreadyHaveCheck.data, tags: [] };
+ const scanned: { safe?: boolean; tags?: string[] } = await fetch("https://unscan.p.rapidapi.com/link", {
method: "POST",
headers: {
"X-RapidAPI-Key": client.config.rapidApiKey,
@@ -72,18 +83,19 @@
console.error(err);
return { safe: true, tags: [] };
});
- console.log(scanned);
+ client.database.scanCache.write(link, scanned.safe ?? true, []);
return {
safe: scanned.safe ?? true,
tags: scanned.tags ?? []
};
}
-export async function saveAttachment(link: string): Promise<string> {
- const image = (await (await fetch(link)).buffer()).toString("base64");
+export async function saveAttachment(link: string): Promise<[string, string]> {
+ const image = await (await fetch(link)).arrayBuffer()
const fileName = generateFileName(link.split("/").pop()!.split(".").pop()!);
- writeFileSync(fileName, image, "base64");
- return fileName;
+ const enc = new TextDecoder("utf-8");
+ writeFileSync(fileName, new DataView(image), "base64");
+ return [fileName, createHash('sha512').update(enc.decode(image), 'base64').digest('base64')];
}
const linkTypes = {
@@ -139,8 +151,7 @@
export async function NSFWCheck(element: string): Promise<boolean> {
try {
- const test = await testNSFW(element);
- return test.nsfw;
+ return (await testNSFW(element)).nsfw;
} catch {
return false;
}
diff --git a/src/reflex/statsChannelUpdate.ts b/src/reflex/statsChannelUpdate.ts
index db705d9..daa82fd 100644
--- a/src/reflex/statsChannelUpdate.ts
+++ b/src/reflex/statsChannelUpdate.ts
@@ -1,4 +1,4 @@
-import { getCommandMentionByName } from '../utils/getCommandMentionByName.js';
+import { getCommandMentionByName } from '../utils/getCommandDataByName.js';
import type { Guild, User } from "discord.js";
import type { NucleusClient } from "../utils/client.js";
import type { GuildMember } from "discord.js";
@@ -32,7 +32,7 @@
return singleNotify(
"statsChannelDeleted",
guild!.id,
- `One or more of your stats channels have been deleted. You can use ${await getCommandMentionByName("settings/stats")}.\n` +
+ `One or more of your stats channels have been deleted. You can use ${getCommandMentionByName("settings/stats")}.\n` +
`The channels name was: ${deleted!.name}`,
"Critical"
);
diff --git a/src/reflex/verify.ts b/src/reflex/verify.ts
index 573da5e..290e372 100644
--- a/src/reflex/verify.ts
+++ b/src/reflex/verify.ts
@@ -13,6 +13,8 @@
import { TestString, NSFWCheck } from "./scanners.js";
import createPageIndicator from "../utils/createPageIndicator.js";
import client from "../utils/client.js";
+import singleNotify from "../utils/singleNotify.js";
+import { getCommandMentionByName } from "../utils/getCommandDataByName.js";
export interface VerifySchema {
uID: string;
@@ -182,14 +184,14 @@
let itt = 0;
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
do {
- itt += 1;
+ itt ++;
code = "";
for (let i = 0; i < length; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
if (itt > 1000) {
itt = 0;
- length += 1;
+ length ++;
}
} while (code in verify);
const role: Role | null = await interaction.guild!.roles.fetch(config.verify.role);
@@ -206,7 +208,8 @@
.setEmoji("CONTROL.BLOCKCROSS")
]
});
- return; // TODO: SEN
+ singleNotify("verifyRoleDeleted", interaction.guild!.id, `The role given when a member is verified has been deleted. Use ${getCommandMentionByName("settings/verify")} to set a new one`, "Critical")
+ return;
}
verify[code] = {
uID: interaction.member!.user.id,
diff --git a/src/reflex/welcome.ts b/src/reflex/welcome.ts
index 87bb81a..c2eede3 100644
--- a/src/reflex/welcome.ts
+++ b/src/reflex/welcome.ts
@@ -1,4 +1,4 @@
-import { getCommandMentionByName } from './../utils/getCommandMentionByName.js';
+import { getCommandMentionByName } from './../utils/getCommandDataByName.js';
import type { NucleusClient } from "../utils/client.js";
import convertCurlyBracketString from "../utils/convertCurlyBracketString.js";
import client from "../utils/client.js";
@@ -27,19 +27,19 @@
});
} else {
const channel: GuildChannel | null = await member.guild.channels.fetch(config.welcome.channel) as GuildChannel | null;
- if (!channel) return await singleNotify("welcomeChannelDeleted", member.guild.id, `The welcome channel has been deleted or is no longer accessible. Use ${await getCommandMentionByName("settings/welcome")} to set a new one`, "Warning")
+ if (!channel) return await singleNotify("welcomeChannelDeleted", member.guild.id, `The welcome channel has been deleted or is no longer accessible. Use ${getCommandMentionByName("settings/welcome")} to set a new one`, "Warning")
if (!(channel instanceof BaseGuildTextChannel)) return;
if (channel.guild.id !== member.guild.id) return;
try {
await channel.send({
embeds: [new EmojiEmbed().setDescription(string).setStatus("Success")],
- content: (config.welcome.ping ? `<@${config.welcome.ping}>` : "") + `<@${member.id}>`
+ content: (config.welcome.ping ? `<@&${config.welcome.ping}>` : "") + `<@${member.id}>`
});
} catch (err) {
singleNotify(
"welcomeChannelDeleted",
member.guild.id,
- `The welcome channel has been deleted or is no longer accessible. Use ${await getCommandMentionByName("settings/welcome")} to set a new one`,
+ `The welcome channel has been deleted or is no longer accessible. Use ${getCommandMentionByName("settings/welcome")} to set a new one`,
"Warning"
)
}