summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--keys/private.dev.pem29
-rw-r--r--keys/public.dev.pem10
-rw-r--r--src/db/schema.ts16
-rw-r--r--src/index.ts85
-rw-r--r--src/routes/kid.ts1
-rw-r--r--vitest.setup.ts5
6 files changed, 106 insertions, 40 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-----
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" (