worked on NSFW Image testing
diff --git a/package.json b/package.json
index 96c93ce..6a9a795 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"@tensorflow/tfjs-node": "^3.18.0",
"@total-typescript/ts-reset": "^0.3.7",
"@tsconfig/node18-strictest-esm": "^1.0.0",
+ "@types/gm": "^1.25.0",
"@types/node": "^18.14.6",
"@ungap/structured-clone": "^1.0.1",
"agenda": "^4.3.0",
@@ -15,6 +16,8 @@
"eslint": "^8.21.0",
"express": "^4.18.1",
"fuse.js": "^6.6.2",
+ "gifencoder": "^2.0.1",
+ "gm": "^1.25.0",
"humanize-duration": "^3.27.1",
"immutable": "^4.1.0",
"lodash": "^4.17.21",
@@ -66,6 +69,7 @@
"type": "module",
"devDependencies": {
"@types/clamscan": "^2.0.4",
+ "@types/gifencoder": "^2.0.1",
"@types/lodash": "^4.14.191",
"@typescript-eslint/eslint-plugin": "^5.32.0",
"@typescript-eslint/parser": "^5.32.0",
diff --git a/src/api/index.ts b/src/api/index.ts
index 41f281d..79b115e 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -1,3 +1,4 @@
+#!/bin
import type { Guild, GuildMember } from "discord.js";
import type { NucleusClient } from "../utils/client.js";
//@ts-expect-error
diff --git a/src/commands/settings/logs/warnings.ts b/src/commands/settings/logs/warnings.ts
index 38b645a..a988fae 100644
--- a/src/commands/settings/logs/warnings.ts
+++ b/src/commands/settings/logs/warnings.ts
@@ -5,12 +5,14 @@
ButtonBuilder,
ButtonStyle,
ChannelSelectMenuBuilder,
- ChannelType
+ ChannelType,
+ ComponentType
} from "discord.js";
import EmojiEmbed from "../../../utils/generateEmojiEmbed.js";
import getEmojiByName from "../../../utils/getEmojiByName.js";
import type { SlashCommandSubcommandBuilder } from "discord.js";
import client from "../../../utils/client.js";
+import _ from "lodash";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder.setName("warnings").setDescription("Settings for the staff notifications channel");
@@ -24,7 +26,7 @@
});
let data = await client.database.guilds.read(interaction.guild.id);
- let channel = data.logging.staff.channel;
+ let channel = _.clone(data.logging.staff.channel);
let closed = false;
do {
const channelMenu = new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(
@@ -45,7 +47,7 @@
.setLabel("Save")
.setStyle(ButtonStyle.Success)
.setEmoji(getEmojiByName("ICONS.SAVE", "id") as Discord.APIMessageComponentEmoji)
- .setDisabled(channel === data.logging.staff.channel)
+ .setDisabled(_.isEqual(channel, data.logging.staff.channel))
);
const embed = new EmojiEmbed()
@@ -62,12 +64,12 @@
components: [channelMenu, buttons]
});
- let i: Discord.ButtonInteraction | Discord.SelectMenuInteraction;
+ let i: Discord.ButtonInteraction | Discord.ChannelSelectMenuInteraction;
try {
- i = (await interaction.channel!.awaitMessageComponent({
+ i = (await interaction.channel!.awaitMessageComponent<ComponentType.Button | ComponentType.ChannelSelect>({
filter: (i: Discord.Interaction) => i.user.id === interaction.user.id,
time: 300000
- })) as Discord.ButtonInteraction | Discord.SelectMenuInteraction;
+ }))
} catch (e) {
closed = true;
continue;
@@ -81,7 +83,7 @@
}
case "save": {
await client.database.guilds.write(interaction.guild!.id, {
- "logging.warnings.channel": channel
+ "logging.staff.channel": channel
});
data = await client.database.guilds.read(interaction.guild!.id);
await client.memory.forceUpdate(interaction.guild!.id);
diff --git a/src/events/guildMemberUpdate.ts b/src/events/guildMemberUpdate.ts
index 537a483..fe11ee1 100644
--- a/src/events/guildMemberUpdate.ts
+++ b/src/events/guildMemberUpdate.ts
@@ -93,7 +93,7 @@
if (!auditLog) return;
if (auditLog.executor!.id === client.user!.id) return;
if (before.nickname !== after.nickname) {
- await doMemberChecks(after, after.guild);
+ await doMemberChecks(after);
await client.database.history.create(
"nickname",
after.guild.id,
@@ -125,6 +125,7 @@
};
await log(data);
}
+ if (before.displayAvatarURL !== after.displayAvatarURL) await doMemberChecks(after);
if (
(before.communicationDisabledUntilTimestamp ?? 0) < Date.now() &&
new Date(after.communicationDisabledUntil ?? 0).getTime() > Date.now()
diff --git a/src/events/memberJoin.ts b/src/events/memberJoin.ts
index b01eb60..55f9ba8 100644
--- a/src/events/memberJoin.ts
+++ b/src/events/memberJoin.ts
@@ -9,7 +9,7 @@
export async function callback(client: NucleusClient, member: GuildMember) {
await welcome(member);
await statsChannelAdd(member.user, member.guild);
- await doMemberChecks(member, member.guild);
+ await doMemberChecks(member);
const { log, isLogging, NucleusColors, entry, renderUser, renderDelta } = client.logger;
if (!(await isLogging(member.guild.id, "guildMemberUpdate"))) return;
await client.database.history.create("join", member.guild.id, member.user, null, null);
diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts
index 5369bf4..38c7674 100644
--- a/src/events/messageCreate.ts
+++ b/src/events/messageCreate.ts
@@ -105,42 +105,42 @@
for (const element of fileNames.files) {
const url = element.url ? element.url : element.local;
if (
- /\.(j(pe?g|fif)|a?png|gifv?|w(eb[mp]|av)|mp([34]|eg-\d)|ogg|avi|h\.26(4|5)|cda)$/.test(
+ /\.(jpg|jpeg|png|apng|gif|gifv|webm|webp|mp4|wav|mp3|ogg|jfif|mpeg-\d|avi|h\.264|h\.265)$/.test(
url.toLowerCase()
)
) {
- // jpg|jpeg|png|apng|gif|gifv|webm|webp|mp4|wav|mp3|ogg|jfif|MPEG-#|avi|h.264|h.265
+ // j(pe?g|fif)|a?png|gifv?|w(eb[mp]|av)|mp([34]|eg-\d)|ogg|avi|h\.26(4|5)
+ // ^no
if (
config.filters.images.NSFW &&
- !(message.channel instanceof ThreadChannel ? message.channel.parent?.nsfw : message.channel.nsfw)
+ !(message.channel instanceof ThreadChannel ? message.channel.parent?.nsfw : message.channel.nsfw) &&
+ (await NSFWCheck(element))
) {
- if (await NSFWCheck(url)) {
- messageException(message.guild.id, message.channel.id, message.id);
- await message.delete();
- const data = {
- meta: {
- type: "messageDelete",
- displayName: "Message Deleted",
- calculateType: "autoModeratorDeleted",
- color: NucleusColors.red,
- emoji: "MESSAGE.DELETE",
- timestamp: Date.now()
- },
- separate: {
- start:
- filter +
- " Image detected as NSFW\n\n" +
- (content
- ? `**Message:**\n\`\`\`${content}\`\`\``
- : "**Message:** *Message had no content*")
- },
- list: list,
- hidden: {
- guild: message.channel.guild.id
- }
- };
- return log(data);
- }
+ messageException(message.guild.id, message.channel.id, message.id);
+ await message.delete();
+ const data = {
+ meta: {
+ type: "messageDelete",
+ displayName: "Message Deleted",
+ calculateType: "autoModeratorDeleted",
+ color: NucleusColors.red,
+ emoji: "MESSAGE.DELETE",
+ timestamp: Date.now()
+ },
+ separate: {
+ start:
+ filter +
+ " Image detected as NSFW\n\n" +
+ (content
+ ? `**Message:**\n\`\`\`${content}\`\`\``
+ : "**Message:** *Message had no content*")
+ },
+ list: list,
+ hidden: {
+ guild: message.channel.guild.id
+ }
+ };
+ return log(data);
}
if (config.filters.wordFilter.enabled) {
const text = await TestImage(url);
@@ -209,34 +209,30 @@
}
}
}
- if (config.filters.malware) {
- if (!(await MalwareCheck(url))) {
- messageException(message.guild.id, message.channel.id, message.id);
- await message.delete();
- const data = {
- meta: {
- type: "messageDelete",
- displayName: "Message Deleted",
- calculateType: "autoModeratorDeleted",
- color: NucleusColors.red,
- emoji: "MESSAGE.DELETE",
- timestamp: Date.now()
- },
- separate: {
- start:
- filter +
- " File detected as malware\n\n" +
- (content
- ? `**Message:**\n\`\`\`${content}\`\`\``
- : "**Message:** *Message had no content*")
- },
- list: list,
- hidden: {
- guild: message.channel.guild.id
- }
- };
- return log(data);
- }
+ if (config.filters.malware && (await MalwareCheck(url))) {
+ messageException(message.guild.id, message.channel.id, message.id);
+ await message.delete();
+ const data = {
+ meta: {
+ type: "messageDelete",
+ displayName: "Message Deleted",
+ calculateType: "autoModeratorDeleted",
+ color: NucleusColors.red,
+ emoji: "MESSAGE.DELETE",
+ timestamp: Date.now()
+ },
+ separate: {
+ start:
+ filter +
+ " File detected as malware\n\n" +
+ (content ? `**Message:**\n\`\`\`${content}\`\`\`` : "**Message:** *Message had no content*")
+ },
+ list: list,
+ hidden: {
+ guild: message.channel.guild.id
+ }
+ };
+ return log(data);
}
}
}
diff --git a/src/reflex/scanners.ts b/src/reflex/scanners.ts
index cce8b84..acd3b41 100644
--- a/src/reflex/scanners.ts
+++ b/src/reflex/scanners.ts
@@ -12,13 +12,16 @@
import getEmojiByName from "../utils/getEmojiByName.js";
import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
import config from "../config/main.js";
+import GIFEncoder from "gifencoder";
+import gm_var from 'gm';
+const gm = gm_var.subClass({ imageMagick: '7+' });
interface NSFWSchema {
nsfw: boolean;
errored?: boolean;
}
interface MalwareSchema {
- safe: boolean;
+ malware: boolean;
errored?: boolean;
}
@@ -29,15 +32,28 @@
}
});
-export async function testNSFW(link: string): Promise<NSFWSchema> {
- const [fileStream, hash] = await streamAttachment(link);
+export async function testNSFW(attachment: {
+ url: string;
+ local: string;
+ height: number | null;
+ width: number | null;
+}): Promise<NSFWSchema> {
+ const [fileStream, hash] = await streamAttachment(attachment.url);
const alreadyHaveCheck = await client.database.scanCache.read(hash);
- if (alreadyHaveCheck?.nsfw !== undefined) return { nsfw: alreadyHaveCheck.nsfw };
+ if (alreadyHaveCheck && ("nsfw" in alreadyHaveCheck!)) {
+ return { nsfw: alreadyHaveCheck.nsfw }
+ };
- const image = tf.tensor3d(new Uint8Array(fileStream));
+ const image = gm(fileStream).command('convert').in('-')
- const predictions = (await nsfw_model.classify(image, 1))[0]!;
- image.dispose();
+ const encoder = new GIFEncoder(attachment.width ?? 1024, attachment.height ?? 1024);
+
+
+ // const array = new Uint8Array(fileStream);
+ const img = tf.node.decodeImage(array) as tf.Tensor3D;
+
+ const predictions = (await nsfw_model.classify(img, 1))[0]!;
+ console.log(2, predictions);
const nsfw = predictions.className === "Hentai" || predictions.className === "Porn";
await client.database.scanCache.write(hash, "nsfw", nsfw);
@@ -48,51 +64,42 @@
export async function testMalware(link: string): Promise<MalwareSchema> {
const [fileName, hash] = await saveAttachment(link);
const alreadyHaveCheck = await client.database.scanCache.read(hash);
- if (alreadyHaveCheck?.malware !== undefined) return { safe: alreadyHaveCheck.malware };
+ if (alreadyHaveCheck?.malware !== undefined) return { malware: alreadyHaveCheck.malware };
let malware;
try {
malware = (await clamscanner.scanFile(fileName)).isInfected;
} catch (e) {
- return { safe: true };
+ return { malware: true };
}
await client.database.scanCache.write(hash, "malware", malware);
- return { safe: !malware };
+ return { malware };
}
export async function testLink(link: string): Promise<{ safe: boolean; tags: string[] }> {
const alreadyHaveCheck = await client.database.scanCache.read(link);
- if (alreadyHaveCheck?.bad_link !== undefined) return { safe: alreadyHaveCheck.bad_link, tags: alreadyHaveCheck.tags ?? [] };
- const scanned: { safe?: boolean; tags?: string[] } = await fetch("https://unscan.p.rapidapi.com/link", {
- method: "POST",
- headers: {
- "X-RapidAPI-Key": client.config.rapidApiKey,
- "X-RapidAPI-Host": "unscan.p.rapidapi.com"
- },
- body: `{"link":"${link}"}`
- })
- .then((response) => response.json() as Promise<MalwareSchema>)
- .catch((err) => {
- console.error(err);
- return { safe: true, tags: [] };
- });
- await client.database.scanCache.write(link, "bad_link", scanned.safe ?? true, scanned.tags ?? []);
- return {
- safe: scanned.safe ?? true,
- tags: scanned.tags ?? []
- };
+ if (alreadyHaveCheck?.bad_link !== undefined)
+ return { safe: alreadyHaveCheck.bad_link, tags: alreadyHaveCheck.tags ?? [] };
+ return { safe: true, tags: [] };
+ // const scanned: { safe?: boolean; tags?: string[] } = {}
+ // await client.database.scanCache.write(link, "bad_link", scanned.safe ?? true, scanned.tags ?? []);
+ // return {
+ // safe: scanned.safe ?? true,
+ // tags: scanned.tags ?? []
+ // };
}
-export async function streamAttachment(link: string): Promise<[ArrayBuffer, string]> {
+export async function streamAttachment(link: string): Promise<[Buffer, string]> {
const image = await (await fetch(link)).arrayBuffer();
const enc = new TextDecoder("utf-8");
- return [image, createHash("sha512").update(enc.decode(image), "base64").digest("base64")];
+ const buf = Buffer.from(image);
+ return [buf, createHash("sha512").update(enc.decode(image), "base64").digest("base64")];
}
export async function saveAttachment(link: string): Promise<[string, string]> {
const image = await (await fetch(link)).arrayBuffer();
const fileName = await generateFileName(link.split("/").pop()!.split(".").pop()!);
const enc = new TextDecoder("utf-8");
- writeFileSync(fileName, new DataView(image), "base64");
+ writeFileSync(fileName, new DataView(image));
return [fileName, createHash("sha512").update(enc.decode(image), "base64").digest("base64")];
}
@@ -147,10 +154,16 @@
return detectionsTypes as string[];
}
-export async function NSFWCheck(element: string): Promise<boolean> {
+export async function NSFWCheck(element: {
+ url: string;
+ local: string;
+ height: number | null;
+ width: number | null;
+}): Promise<boolean> {
try {
return (await testNSFW(element)).nsfw;
- } catch {
+ } catch (e) {
+ console.log(e)
return false;
}
}
@@ -163,7 +176,7 @@
export async function MalwareCheck(element: string): Promise<boolean> {
try {
- return (await testMalware(element)).safe;
+ return (await testMalware(element)).malware;
} catch {
return true;
}
@@ -200,15 +213,20 @@
return text;
}
-export async function doMemberChecks(member: Discord.GuildMember, guild: Discord.Guild): Promise<void> {
+export async function doMemberChecks(member: Discord.GuildMember): Promise<void> {
if (member.user.bot) return;
+ console.log("Checking member " + member.user.tag)
+ const guild = member.guild;
const guildData = await client.database.guilds.read(guild.id);
if (!guildData.logging.staff.channel) return;
const [loose, strict] = [guildData.filters.wordFilter.words.loose, guildData.filters.wordFilter.words.strict];
+ console.log(1, loose, strict)
// Does the username contain filtered words
const usernameCheck = TestString(member.user.username, loose, strict, guildData.filters.wordFilter.enabled);
+ console.log(2, usernameCheck)
// Does the nickname contain filtered words
const nicknameCheck = TestString(member.nickname ?? "", loose, strict, guildData.filters.wordFilter.enabled);
+ console.log(3, nicknameCheck)
// Does the profile picture contain filtered words
const avatarTextCheck = TestString(
(await TestImage(member.user.displayAvatarURL({ forceStatic: true }))) ?? "",
@@ -216,15 +234,19 @@
strict,
guildData.filters.wordFilter.enabled
);
+ console.log(4, avatarTextCheck)
// Is the profile picture NSFW
+ const avatar = member.displayAvatarURL({ extension: "png", size: 1024, forceStatic: true });
const avatarCheck =
- guildData.filters.images.NSFW && (await NSFWCheck(member.user.displayAvatarURL({ forceStatic: true })));
+ guildData.filters.images.NSFW && (await NSFWCheck({url: avatar, local: "", height: 1024, width: 1024}));
+ console.log(5, avatarCheck)
// Does the username contain an invite
const inviteCheck = guildData.filters.invite.enabled && /discord\.gg\/[a-zA-Z0-9]+/gi.test(member.user.username);
+ console.log(6, inviteCheck)
// Does the nickname contain an invite
const nicknameInviteCheck =
guildData.filters.invite.enabled && /discord\.gg\/[a-zA-Z0-9]+/gi.test(member.nickname ?? "");
-
+ console.log(7, nicknameInviteCheck)
if (
usernameCheck !== null ||
nicknameCheck !== null ||
diff --git a/src/utils/confirmationMessage.ts b/src/utils/confirmationMessage.ts
index a417f6f..59befe6 100644
--- a/src/utils/confirmationMessage.ts
+++ b/src/utils/confirmationMessage.ts
@@ -291,10 +291,13 @@
cancelled = true;
continue;
}
- if (out === null || out.isButton()) {
+ if (out === null) {
cancelled = true;
continue;
}
+ if (out.isButton()) {
+ continue;
+ }
if (out instanceof ModalSubmitInteraction) {
newReason = out.fields.getTextInputValue("reason");
continue;
@@ -339,7 +342,11 @@
cancelled = true;
continue;
}
- if (out === null || out.isButton()) {
+ if (out === null) {
+ cancelled = true;
+ continue;
+ }
+ if (out.isButton()) {
continue;
}
if (out instanceof ModalSubmitInteraction) {