From 045c533fa3f5ff958829ea412cff647de1ebadcd Mon Sep 17 00:00:00 2001 From: JustZvan Date: Sat, 28 Mar 2026 19:42:02 +0100 Subject: chore: remove newlines in dev keys --- keys/private.dev.pem | 29 +---------------------------- keys/public.dev.pem | 10 +--------- 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/keys/private.dev.pem b/keys/private.dev.pem index 3dffe8e..1bbc947 100644 --- a/keys/private.dev.pem +++ b/keys/private.dev.pem @@ -1,28 +1 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCv/l08TEaBNtCw -PLQl841imjOc6XVGogvCit4TOWbI29NMnViBgdqEMOLWQFKZcxqB1DgteyGiijB8 -Xkl9frKiPSBJEWxNJnZVSOHzmTLl6SIGFguUcVGs2s6DqIeZlv5lc2iW85GWTIvp -hR8XTTKNg4bUbKYYl244SoMp0MBRGbcwLdVaJyAI6XjT7XlDPA8AgOrMFnivp9WF -7B1slCU9qGWZwf2sjoWDbBmdQziP2dJi1fj+ZyVWriYUCDdfoVgECqwXlB/Ambul -YDASvVGkBgaL/4WJW1z9bVJZDpsMOcY4yYvagBSA1weOl+4DcvasvD1c4wExxgq2 -yUADhCZHAgMBAAECggEAF1+xAlEfDAo7rSxiwKeYH4BbWnunF7pt1WicFfGJtSN8 -7K/5EToty2Cyv8HLNpYS7ytASsoPrYas6deb6w7oqqNzpkCqIZT6IlmLqM6v89kC -q8xBvXVPY6Wrx9CaMcvb/Z1WRrYSn+OKsXj8qBuYmzLctVm4tYtnGBLNWMBgymRn -GQBOOc1PDCDxXiG4J9UaYEmV1GseRXhuN2JMKkB0sYkbqQg9gfdq63Y7/tnf/Jkk -Vd8RJvsDfO0SO41hg+A7KL9Wqmxwz03Q5Vnjbcm+pnYI6OnH78pKhXxFDNGQVhZQ -CcMOuDXiUDm4T3lrngIPn09LlzmH7WLlIA6+TP/wAQKBgQDxGhA7uPje8yX6nDl3 -eJd6uMGtqdsYy8F/CspbRBaIb0GwrYd+F9x5MbNwXoylYg5VGoAooD4PDDkmtjfw -/fs7+NKO5aqSRU+Ydc4HV67mAqAUscke7SK+6OO+ficB3YNzh7bf2Xo/2+a2tNKH -mdeuoT1ML7edDWBeo7KOIYq18wKBgQC63l5UJ+Z27NKgII20VYwLLjgABBYedg46 -uMOJUYf/GcKNzQ4SGqsUtILLcsab7f86BHhuTr5tHbwUwofj/dUyXjRd2VmxXxg/ -zG4i25wsGFG6WpqcckX2FjhTlnmQfKJgzqECuz2FZ+EBhXwh9XCHEDvMR18uSqrG -VzXNTVH/XQKBgH3dcl4LOXkCjHAhQGrbPJEnhIyJoMR4EmKlGnC8wdql4jA+1v3/ -rOxkAt4FrfzkjMDm3cLXrK4kXm2UMO4RWSe8xQcuZHaJ0nyv+0egAcE326QSEAGi -IEJzx/j5WJnDr00Pq2t+2DAgN3hoO4Poz0zuBdcRDhTiF84wPRWv8v77AoGBAKNv -r2K9TwU+lez069sIYya4MsRYzpuvtzxGssZcJ6zG8/EfoinVZ0IBqs+Tv/9LBcnR -dR9NAaHfussRZNbT/+5AlF5spdTLDiNmggE8v/eVAY4Shl1EWMolnvgEiKgFSeOP -dSU1bFZMh2/UNsBgsR1/5j0BQ07ygTBdwDGiaZAFAoGAZmbteZUjEfqTSuvB9UU9 -KDd4+QV1pulfw6DYPIL8Uhcm1oY/xLMc0XBZSDhsgpaSfy6V1Luz7wut+gJ36Cbc -/liKmXyljvIXuvt9DST122hbI1FSedx8RpVtd6zpaTQ/DQOmmA+6ITKJkjUd14LB -A9QIaN7ekUKDEZlAGzKaR5A= ------END PRIVATE KEY----- +-----BEGIN PRIVATE KEY-----MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCv/l08TEaBNtCwPLQl841imjOc6XVGogvCit4TOWbI29NMnViBgdqEMOLWQFKZcxqB1DgteyGiijB8Xkl9frKiPSBJEWxNJnZVSOHzmTLl6SIGFguUcVGs2s6DqIeZlv5lc2iW85GWTIvphR8XTTKNg4bUbKYYl244SoMp0MBRGbcwLdVaJyAI6XjT7XlDPA8AgOrMFnivp9WF7B1slCU9qGWZwf2sjoWDbBmdQziP2dJi1fj+ZyVWriYUCDdfoVgECqwXlB/AmbulYDASvVGkBgaL/4WJW1z9bVJZDpsMOcY4yYvagBSA1weOl+4DcvasvD1c4wExxgq2yUADhCZHAgMBAAECggEAF1+xAlEfDAo7rSxiwKeYH4BbWnunF7pt1WicFfGJtSN87K/5EToty2Cyv8HLNpYS7ytASsoPrYas6deb6w7oqqNzpkCqIZT6IlmLqM6v89kCq8xBvXVPY6Wrx9CaMcvb/Z1WRrYSn+OKsXj8qBuYmzLctVm4tYtnGBLNWMBgymRnGQBOOc1PDCDxXiG4J9UaYEmV1GseRXhuN2JMKkB0sYkbqQg9gfdq63Y7/tnf/JkkVd8RJvsDfO0SO41hg+A7KL9Wqmxwz03Q5Vnjbcm+pnYI6OnH78pKhXxFDNGQVhZQCcMOuDXiUDm4T3lrngIPn09LlzmH7WLlIA6+TP/wAQKBgQDxGhA7uPje8yX6nDl3eJd6uMGtqdsYy8F/CspbRBaIb0GwrYd+F9x5MbNwXoylYg5VGoAooD4PDDkmtjfw/fs7+NKO5aqSRU+Ydc4HV67mAqAUscke7SK+6OO+ficB3YNzh7bf2Xo/2+a2tNKHmdeuoT1ML7edDWBeo7KOIYq18wKBgQC63l5UJ+Z27NKgII20VYwLLjgABBYedg46uMOJUYf/GcKNzQ4SGqsUtILLcsab7f86BHhuTr5tHbwUwofj/dUyXjRd2VmxXxg/zG4i25wsGFG6WpqcckX2FjhTlnmQfKJgzqECuz2FZ+EBhXwh9XCHEDvMR18uSqrGVzXNTVH/XQKBgH3dcl4LOXkCjHAhQGrbPJEnhIyJoMR4EmKlGnC8wdql4jA+1v3/rOxkAt4FrfzkjMDm3cLXrK4kXm2UMO4RWSe8xQcuZHaJ0nyv+0egAcE326QSEAGiIEJzx/j5WJnDr00Pq2t+2DAgN3hoO4Poz0zuBdcRDhTiF84wPRWv8v77AoGBAKNvr2K9TwU+lez069sIYya4MsRYzpuvtzxGssZcJ6zG8/EfoinVZ0IBqs+Tv/9LBcnRdR9NAaHfussRZNbT/+5AlF5spdTLDiNmggE8v/eVAY4Shl1EWMolnvgEiKgFSeOPdSU1bFZMh2/UNsBgsR1/5j0BQ07ygTBdwDGiaZAFAoGAZmbteZUjEfqTSuvB9UU9KDd4+QV1pulfw6DYPIL8Uhcm1oY/xLMc0XBZSDhsgpaSfy6V1Luz7wut+gJ36Cbc/liKmXyljvIXuvt9DST122hbI1FSedx8RpVtd6zpaTQ/DQOmmA+6ITKJkjUd14LBA9QIaN7ekUKDEZlAGzKaR5A=-----END PRIVATE KEY----- diff --git a/keys/public.dev.pem b/keys/public.dev.pem index 6d198a1..1899e3e 100644 --- a/keys/public.dev.pem +++ b/keys/public.dev.pem @@ -1,9 +1 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/5dPExGgTbQsDy0JfON -YpoznOl1RqILworeEzlmyNvTTJ1YgYHahDDi1kBSmXMagdQ4LXshooowfF5JfX6y -oj0gSRFsTSZ2VUjh85ky5ekiBhYLlHFRrNrOg6iHmZb+ZXNolvORlkyL6YUfF00y -jYOG1GymGJduOEqDKdDAURm3MC3VWicgCOl40+15QzwPAIDqzBZ4r6fVhewdbJQl -PahlmcH9rI6Fg2wZnUM4j9nSYtX4/mclVq4mFAg3X6FYBAqsF5QfwJm7pWAwEr1R -pAYGi/+FiVtc/W1SWQ6bDDnGOMmL2oAUgNcHjpfuA3L2rLw9XOMBMcYKtslAA4Qm -RwIDAQAB ------END PUBLIC KEY----- +-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/5dPExGgTbQsDy0JfONYpoznOl1RqILworeEzlmyNvTTJ1YgYHahDDi1kBSmXMagdQ4LXshooowfF5JfX6yoj0gSRFsTSZ2VUjh85ky5ekiBhYLlHFRrNrOg6iHmZb+ZXNolvORlkyL6YUfF00yjYOG1GymGJduOEqDKdDAURm3MC3VWicgCOl40+15QzwPAIDqzBZ4r6fVhewdbJQlPahlmcH9rI6Fg2wZnUM4j9nSYtX4/mclVq4mFAg3X6FYBAqsF5QfwJm7pWAwEr1RpAYGi/+FiVtc/W1SWQ6bDDnGOMmL2oAUgNcHjpfuA3L2rLw9XOMBMcYKtslAA4QmRwIDAQAB-----END PUBLIC KEY----- -- cgit v1.2.3 From 4f34bdf4de0d847591faa3e6981eafdf2b80a0b7 Mon Sep 17 00:00:00 2001 From: JustZvan Date: Mon, 30 Mar 2026 13:10:26 +0200 Subject: feat: add gallery scanning --- src/db/schema.ts | 16 +++++++++-- src/index.ts | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/routes/kid.ts | 1 + vitest.setup.ts | 5 +++- 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/db/schema.ts b/src/db/schema.ts index ed2e36b..fe58708 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,7 +1,18 @@ -import { integer, pgTable, varchar, boolean, text, pgEnum } from "drizzle-orm/pg-core"; +import { + integer, + pgTable, + varchar, + boolean, + text, + pgEnum, +} from "drizzle-orm/pg-core"; import { defineRelations, sql } from "drizzle-orm"; -export const galleryScanningMode = pgEnum("galleryScanningMode", ["delete", "notify", "none"]); +export const galleryScanningMode = pgEnum("galleryScanningMode", [ + "delete", + "notify", + "none", +]); /** Parent user accounts with email auth and push notification tokens */ export const users = pgTable("users", { @@ -41,6 +52,7 @@ export const deviceConfig = pgTable("deviceConfig", { notifyNewContactAdded: boolean("notify_new_contact_added") .notNull() .default(true), + galleryScanningMode: galleryScanningMode().default("notify").notNull(), }); /** Stores flagged messages and content alerts for parent review */ diff --git a/src/index.ts b/src/index.ts index 22c9024..2d4c114 100644 --- a/src/index.ts +++ b/src/index.ts @@ -552,6 +552,91 @@ app.ws("/kid/connect", (ws, req) => { return; } + if (data.type === "nsfw_image_detected") { + const deviceId = (ws as unknown as KidWebSocket).deviceId; + if (!deviceId) { + ws.send( + JSON.stringify({ success: false, reason: "Not authenticated" }), + ); + return; + } + + try { + const device = await db + .select() + .from(linkedDevices) + .where(eq(linkedDevices.id, deviceId)) + .limit(1); + + if (device.length === 0) { + logger.error({ deviceId }, "Device not found for nsfw image event"); + ws.send(JSON.stringify({ success: true })); + return; + } + + const parentId = device[0]!.parentId; + const deviceName = device[0]!.nickname; + + const parent = await db + .select({ pushTokens: users.pushTokens }) + .from(users) + .where(eq(users.id, parentId)) + .limit(1); + + if ( + parent.length > 0 && + parent[0]!.pushTokens && + parent[0]!.pushTokens.length > 0 + ) { + await pushNotificationQueue.add("nsfw-image-alert", { + pushTokens: parent[0]!.pushTokens, + notification: { + title: `⚠️ NSFW Image Detected`, + body: `An NSFW image was found on ${deviceName}`, + data: { + type: "nsfw_image_detected", + screen: "DeviceDetail", + deviceId: deviceId.toString(), + deviceName: deviceName, + }, + channelId: "alerts", + }, + }); + + await db.insert(alerts).values({ + deviceId: deviceId, + parentId: parentId, + category: "nsfw_image", + title: `NSFW Image Detected`, + message: `An NSFW image was found on ${deviceName}`, + summary: + "An image containing nudity or sexually explicit content was detected.", + confidence: 100, + packageName: (data.packageName as string) || "unknown", + timestamp: Math.floor(Date.now() / 1000), + read: false, + }); + + logger.info({ parentId, deviceId }, "NSFW image notification sent"); + } + + ws.send(JSON.stringify({ success: true })); + } catch (e) { + logger.error( + { error: e, deviceId }, + "Failed to process nsfw image event", + ); + ws.send( + JSON.stringify({ + success: false, + reason: "Failed to process nsfw image event", + }), + ); + } + + return; + } + logger.debug( { data, deviceId: (ws as unknown as KidWebSocket).deviceId }, "Unknown message type received", diff --git a/src/routes/kid.ts b/src/routes/kid.ts index a326d5b..182535c 100644 --- a/src/routes/kid.ts +++ b/src/routes/kid.ts @@ -93,6 +93,7 @@ router.get("/kid/getconfig", authDevice, async (req, res) => { disableBuddy: cfg.disableBuddy, blockAdultSites: cfg.blockAdultSites, familyLinkAntiCircumvention: cfg.familyLinkAntiCircumvention, + galleryScanningMode: cfg.galleryScanningMode, }, }); } catch (e) { diff --git a/vitest.setup.ts b/vitest.setup.ts index e24e32e..60f7ae3 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -37,6 +37,8 @@ vi.mock("./src/db/db", async () => { "devEnabled" BOOLEAN DEFAULT false ); + CREATE TYPE "galleryScanningMode" AS ENUM ('delete', 'notify', 'none'); + CREATE TABLE IF NOT EXISTS "deviceConfig" ( "id" SERIAL PRIMARY KEY, "device_id" INTEGER NOT NULL UNIQUE, @@ -46,7 +48,8 @@ vi.mock("./src/db/db", async () => { "new_contact_alerts" BOOLEAN NOT NULL DEFAULT true, "block_strangers" BOOLEAN NOT NULL DEFAULT false, "notify_dangerous_messages" BOOLEAN NOT NULL DEFAULT true, - "notify_new_contact_added" BOOLEAN NOT NULL DEFAULT true + "notify_new_contact_added" BOOLEAN NOT NULL DEFAULT true, + "galleryScanningMode" "galleryScanningMode" NOT NULL DEFAULT 'notify' ); CREATE TABLE IF NOT EXISTS "alerts" ( -- cgit v1.2.3