From 549c52aa41ee8b8941405c176f2db281e6b33d2b Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Mon, 10 Aug 2026 15:17:11 -0400 Subject: [PATCH] feat: adult content pref --- api/so/sprk/actor/getPreferences.ts | 7 ++ api/so/sprk/actor/putPreferences.ts | 8 ++ data-plane/db/models.ts | 4 + lexicons/so/sprk/actor/defs.json | 8 ++ tests/preferences_test.ts | 117 ++++++++++++++++++++++++++++ tests/util.ts | 2 + 6 files changed, 146 insertions(+) create mode 100644 tests/preferences_test.ts diff --git a/api/so/sprk/actor/getPreferences.ts b/api/so/sprk/actor/getPreferences.ts index 4228191..da880be 100644 --- a/api/so/sprk/actor/getPreferences.ts +++ b/api/so/sprk/actor/getPreferences.ts @@ -31,6 +31,13 @@ export default function (server: Server, ctx: AppContext) { }; } + if (userPref.adultContentPref) { + preferences.push({ + $type: "so.sprk.actor.defs#adultContentPref", + enabled: userPref.adultContentPref.enabled, + }); + } + if (userPref.contentLabelPrefs?.length) { for (const pref of userPref.contentLabelPrefs) { preferences.push({ diff --git a/api/so/sprk/actor/putPreferences.ts b/api/so/sprk/actor/putPreferences.ts index 6e59ffb..b1dfb90 100644 --- a/api/so/sprk/actor/putPreferences.ts +++ b/api/so/sprk/actor/putPreferences.ts @@ -4,6 +4,7 @@ import { PreferenceDocument } from "../../../../data-plane/db/models.ts"; import { AppContext } from "../../../../context.ts"; import * as so from "../../../../lex/so.ts"; import { + AdultContentPref, ContentLabelPref, FeedViewPref, HiddenPostsPref, @@ -39,6 +40,13 @@ export default function (server: Server, ctx: AppContext) { for (const pref of body.preferences) { switch (pref.$type) { + case "so.sprk.actor.defs#adultContentPref": { + const p = pref as AdultContentPref; + updateData.adultContentPref = { + enabled: p.enabled, + }; + break; + } case "so.sprk.actor.defs#contentLabelPref": { const p = pref as ContentLabelPref; contentLabelPrefs.push({ diff --git a/data-plane/db/models.ts b/data-plane/db/models.ts index d8580b8..7322d75 100644 --- a/data-plane/db/models.ts +++ b/data-plane/db/models.ts @@ -577,6 +577,9 @@ export const actorSchema = new Schema({ export interface PreferenceDocument { userDid: string; + adultContentPref?: { + enabled: boolean; + }; contentLabelPrefs?: Array<{ labelerDid?: string; label: string; @@ -633,6 +636,7 @@ export interface PreferenceDocument { } export const preferenceSchema = new Schema({ userDid: { type: String, required: true, unique: true, index: true }, + adultContentPref: { type: Object, required: false }, contentLabelPrefs: { type: [Object], required: false }, savedFeeds: { type: [Object], required: false }, personalDetailsPref: { type: Object, required: false }, diff --git a/lexicons/so/sprk/actor/defs.json b/lexicons/so/sprk/actor/defs.json index d85f1aa..60fa417 100644 --- a/lexicons/so/sprk/actor/defs.json +++ b/lexicons/so/sprk/actor/defs.json @@ -165,6 +165,7 @@ "items": { "type": "union", "refs": [ + "#adultContentPref", "#contentLabelPref", "#savedFeedsPref", "#personalDetailsPref", @@ -177,6 +178,13 @@ ] } }, + "adultContentPref": { + "type": "object", + "required": ["enabled"], + "properties": { + "enabled": { "type": "boolean", "default": false } + } + }, "contentLabelPref": { "type": "object", "required": ["label", "visibility"], diff --git a/tests/preferences_test.ts b/tests/preferences_test.ts new file mode 100644 index 0000000..b1af3dd --- /dev/null +++ b/tests/preferences_test.ts @@ -0,0 +1,117 @@ +import { assertEquals } from "@std/assert"; +import type { Server } from "@atp/xrpc-server"; + +import type { AppContext } from "../context.ts"; +import type { PreferenceDocument } from "../data-plane/db/models.ts"; +import getPreferences from "../api/so/sprk/actor/getPreferences.ts"; +import putPreferences from "../api/so/sprk/actor/putPreferences.ts"; + +interface Auth { + credentials: { + iss: string; + }; +} + +interface PutPreferencesArgs { + auth: Auth; + input: { + body: { + preferences: Array<{ + $type: string; + enabled: boolean; + }>; + }; + }; +} + +interface GetPreferencesArgs { + auth: Auth; +} + +type PutPreferencesHandler = (args: PutPreferencesArgs) => Promise; +type GetPreferencesHandler = (args: GetPreferencesArgs) => Promise<{ + encoding: string; + body: { + preferences: unknown[]; + }; +}>; + +function handlerServer( + setHandler: (handler: Handler) => void, +): Server { + return { + add: ( + _method: unknown, + implementation: { handler: Handler }, + ) => setHandler(implementation.handler), + } as unknown as Server; +} + +Deno.test("Spark adult content preference round-trips when disabled", async () => { + const userDid = "did:plc:testuser"; + let storedPreferences: Partial = {}; + + const ctx = { + authVerifier: { + standard: {}, + }, + dataplane: { + preferences: { + putPreferences: ( + did: string, + preferences: Partial, + ) => { + assertEquals(did, userDid); + storedPreferences = preferences; + return Promise.resolve(); + }, + getPreferences: (did: string) => { + assertEquals(did, userDid); + return Promise.resolve({ + userDid, + createdAt: "2026-08-09T00:00:00.000Z", + updatedAt: "2026-08-09T00:00:00.000Z", + ...storedPreferences, + }); + }, + }, + }, + } as unknown as AppContext; + + let putHandler: PutPreferencesHandler = () => + Promise.reject(new Error("putPreferences handler was not registered")); + putPreferences( + handlerServer((handler: PutPreferencesHandler) => putHandler = handler), + ctx, + ); + + await putHandler({ + auth: { credentials: { iss: userDid } }, + input: { + body: { + preferences: [{ + $type: "so.sprk.actor.defs#adultContentPref", + enabled: false, + }], + }, + }, + }); + + assertEquals(storedPreferences.adultContentPref, { enabled: false }); + + let getHandler: GetPreferencesHandler = () => + Promise.reject(new Error("getPreferences handler was not registered")); + getPreferences( + handlerServer((handler: GetPreferencesHandler) => getHandler = handler), + ctx, + ); + + const response = await getHandler({ + auth: { credentials: { iss: userDid } }, + }); + + assertEquals(response.body.preferences, [{ + $type: "so.sprk.actor.defs#adultContentPref", + enabled: false, + }]); +}); diff --git a/tests/util.ts b/tests/util.ts index 757fe3c..ff93088 100644 --- a/tests/util.ts +++ b/tests/util.ts @@ -945,6 +945,7 @@ async function seedTestData( if (options.preferences) { await models.Preference.create({ userDid: TEST_USERS[0].did, + adultContentPref: { enabled: true }, contentLabelPrefs: [], savedFeeds: [], personalDetailsPref: {}, @@ -961,6 +962,7 @@ async function seedTestData( await models.Preference.create({ userDid: TEST_USERS[1].did, + adultContentPref: { enabled: false }, contentLabelPrefs: [], savedFeeds: [], personalDetailsPref: {}, -- 2.51.2