From 0d8852256b03e8de8b9170f7fa07143b1364ef17 Mon Sep 17 00:00:00 2001 From: Skyler Grey Date: Wed, 8 Mar 2023 21:46:37 +0000 Subject: [PATCH] worked on NSFW Image testing --- package.json | 4 + src/api/index.ts | 1 + src/commands/settings/logs/warnings.ts | 16 ++-- src/events/guildMemberUpdate.ts | 3 +- src/events/memberJoin.ts | 2 +- src/events/messageCreate.ts | 112 ++++++++++++------------- src/reflex/scanners.ts | 98 +++++++++++++--------- src/utils/confirmationMessage.ts | 11 ++- 8 files changed, 140 insertions(+), 107 deletions(-) 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 @@ import Discord, { 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 @@ const callback = async (interaction: CommandInteraction): Promise => { }); 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().addComponents( @@ -45,7 +47,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .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 @@ const callback = async (interaction: CommandInteraction): Promise => { 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({ 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 @@ const callback = async (interaction: CommandInteraction): Promise => { } 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 @@ export async function callback(client: NucleusClient, before: GuildMember, after 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 @@ export async function callback(client: NucleusClient, before: GuildMember, after }; 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 const event = "guildMemberAdd"; 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 @@ export async function callback(_client: NucleusClient, message: Message) { 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 @@ export async function callback(_client: NucleusClient, message: Message) { } } } - 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 EmojiEmbed from "../utils/generateEmojiEmbed.js"; 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 @@ const clamscanner = await new ClamScan().init({ } }); -export async function testNSFW(link: string): Promise { - const [fileStream, hash] = await streamAttachment(link); +export async function testNSFW(attachment: { + url: string; + local: string; + height: number | null; + width: number | null; +}): Promise { + 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 = gm(fileStream).command('convert').in('-') + + const encoder = new GIFEncoder(attachment.width ?? 1024, attachment.height ?? 1024); - const image = tf.tensor3d(new Uint8Array(fileStream)); - const predictions = (await nsfw_model.classify(image, 1))[0]!; - image.dispose(); + // 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 testNSFW(link: string): Promise { export async function testMalware(link: string): Promise { 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) - .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 @@ export async function LinkCheck(message: Discord.Message): Promise { return detectionsTypes as string[]; } -export async function NSFWCheck(element: string): Promise { +export async function NSFWCheck(element: { + url: string; + local: string; + height: number | null; + width: number | null; +}): Promise { try { return (await testNSFW(element)).nsfw; - } catch { + } catch (e) { + console.log(e) return false; } } @@ -163,7 +176,7 @@ export async function SizeCheck(element: { height: number | null; width: number export async function MalwareCheck(element: string): Promise { try { - return (await testMalware(element)).safe; + return (await testMalware(element)).malware; } catch { return true; } @@ -200,15 +213,20 @@ export async function TestImage(url: string): Promise { return text; } -export async function doMemberChecks(member: Discord.GuildMember, guild: Discord.Guild): Promise { +export async function doMemberChecks(member: Discord.GuildMember): Promise { 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 @@ export async function doMemberChecks(member: Discord.GuildMember, guild: Discord 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 @@ class confirmationMessage { 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 @@ class confirmationMessage { cancelled = true; continue; } - if (out === null || out.isButton()) { + if (out === null) { + cancelled = true; + continue; + } + if (out.isButton()) { continue; } if (out instanceof ModalSubmitInteraction) {