diff options
| author | Zvan Milisavljević <112730082+JustZvan@users.noreply.github.com> | 2026-03-30 13:13:19 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-30 13:13:19 +0200 |
| commit | 3a4611cf51a1f5a5e6f3dd8b12f0fb31eda1a950 (patch) | |
| tree | f2dcb99cec464f0576a2f9815ce81afd866f485c /src | |
| parent | 7beacb50b1f3febf23abbcc41b8d5602cf12a28b (diff) | |
| parent | 4f34bdf4de0d847591faa3e6981eafdf2b80a0b7 (diff) | |
feat: add gallery scanning (#1)
feat: add gallery scanning
Diffstat (limited to 'src')
| -rw-r--r-- | src/db/schema.ts | 16 | ||||
| -rw-r--r-- | src/index.ts | 85 | ||||
| -rw-r--r-- | src/routes/kid.ts | 1 |
3 files changed, 100 insertions, 2 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) { |