From 36b585f245612d870648e7d90e1d351817281492 Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Sun, 31 May 2026 16:56:42 -0400 Subject: [PATCH] fix: label hydration and views --- api/so/sprk/sound/getActorAudios.ts | 2 +- api/so/sprk/sound/getAudioPosts.ts | 2 +- api/so/sprk/sound/getAudios.ts | 2 +- api/so/sprk/sound/getTrendingAudios.ts | 2 +- hydration/index.ts | 5 +- tests/labels_test.ts | 182 +++++++++++++++++++++++++ tests/sounds_test.ts | 67 ++++++--- views/index.ts | 88 +++++++++++- views/types.ts | 3 + 9 files changed, 327 insertions(+), 26 deletions(-) create mode 100644 tests/labels_test.ts diff --git a/api/so/sprk/sound/getActorAudios.ts b/api/so/sprk/sound/getActorAudios.ts index 1e9cb32..aff2477 100644 --- a/api/so/sprk/sound/getActorAudios.ts +++ b/api/so/sprk/sound/getActorAudios.ts @@ -33,7 +33,7 @@ export default function (server: Server, ctx: AppContext) { return { encoding: "application/json", body: results, - headers: resHeaders({}), + headers: resHeaders({ labelers: hydrateCtx.labelers }), }; }, }); diff --git a/api/so/sprk/sound/getAudioPosts.ts b/api/so/sprk/sound/getAudioPosts.ts index 681fae9..217d2ac 100644 --- a/api/so/sprk/sound/getAudioPosts.ts +++ b/api/so/sprk/sound/getAudioPosts.ts @@ -37,7 +37,7 @@ export default function (server: Server, ctx: AppContext) { return { encoding: "application/json", body: results, - headers: resHeaders({}), + headers: resHeaders({ labelers: hydrateCtx.labelers }), }; }, }); diff --git a/api/so/sprk/sound/getAudios.ts b/api/so/sprk/sound/getAudios.ts index 1332c55..e0490a6 100644 --- a/api/so/sprk/sound/getAudios.ts +++ b/api/so/sprk/sound/getAudios.ts @@ -33,7 +33,7 @@ export default function (server: Server, ctx: AppContext) { return { encoding: "application/json", body: results, - headers: resHeaders({}), + headers: resHeaders({ labelers: hydrateCtx.labelers }), }; }, }); diff --git a/api/so/sprk/sound/getTrendingAudios.ts b/api/so/sprk/sound/getTrendingAudios.ts index 785a846..421953c 100644 --- a/api/so/sprk/sound/getTrendingAudios.ts +++ b/api/so/sprk/sound/getTrendingAudios.ts @@ -33,7 +33,7 @@ export default function (server: Server, ctx: AppContext) { return { encoding: "application/json", body: results, - headers: resHeaders({}), + headers: resHeaders({ labelers: hydrateCtx.labelers }), }; }, }); diff --git a/hydration/index.ts b/hydration/index.ts index 19f8d36..0180162 100644 --- a/hydration/index.ts +++ b/hydration/index.ts @@ -852,12 +852,13 @@ export class Hydrator { uris: string[], ctx: HydrateCtx, ): Promise { - const [sounds, soundAggs, profileState] = await Promise.all([ + const [sounds, soundAggs, labels, profileState] = await Promise.all([ this.feed.getSounds(uris, ctx.includeTakedowns), this.feed.getSoundAggregates(uris.map((uri) => ({ uri }))), + this.label.getLabelsForSubjects(uris, ctx.labelers), this.hydrateProfilesBasic(uris.map(didFromUri), ctx), ]); - return mergeStates(profileState, { sounds, soundAggs, ctx }); + return mergeStates(profileState, { sounds, soundAggs, labels, ctx }); } // provides partial hydration state within getFollows / getFollowers, mainly for applying rules diff --git a/tests/labels_test.ts b/tests/labels_test.ts new file mode 100644 index 0000000..7c91d2f --- /dev/null +++ b/tests/labels_test.ts @@ -0,0 +1,182 @@ +import { assertEquals } from "@std/assert"; +import type { + AtUriString, + BlobRef, + CidString, + DatetimeString, + DidString, + UriString, +} from "@atp/lex"; +import { parseCid } from "@atp/lex/data"; + +import { HydrationState } from "../hydration/index.ts"; +import { Label, Labels } from "../hydration/label.ts"; +import { Actor } from "../hydration/actor.ts"; +import { FeedGen, Post, Reply, Sound } from "../hydration/feed.ts"; +import { HydrationMap, RecordInfo } from "../hydration/util.ts"; +import * as com from "../lex/com.ts"; +import * as so from "../lex/so.ts"; +import { Views } from "../views/index.ts"; + +interface SelfLabels extends com.atproto.label.defs.SelfLabels { + $type: "com.atproto.label.defs#selfLabels"; +} + +const NOW = new Date("2026-01-01T00:00:00.000Z"); +const NOW_ISO = NOW.toISOString() as DatetimeString; +const LABELER_DID = "did:plc:labeler" as DidString; +const AUTHOR_DID = "did:plc:author" as DidString; +const CID = + "bafyreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku" as CidString; + +const blob = { + $type: "blob", + ref: parseCid(CID), + mimeType: "image/jpeg", + size: 1, +} as unknown as BlobRef; + +const audioBlob = { + ...blob, + mimeType: "audio/mpeg", +} as unknown as BlobRef; + +const selfLabels = (val: string): SelfLabels => ({ + $type: "com.atproto.label.defs#selfLabels", + values: [{ val }], +}); + +const recordInfo = ( + record: T, +): RecordInfo => ({ + record, + cid: CID, + sortedAt: NOW, + indexedAt: NOW, + takedownRef: undefined, +}); + +const addLabel = (labels: Labels, subject: string, val: string) => { + const label: Label = { + src: LABELER_DID, + uri: subject as UriString, + cid: CID, + val, + cts: NOW_ISO, + }; + const existing = labels.get(subject); + const entry = existing ?? { + isImpersonation: false, + isTakendown: false, + needsReview: false, + labels: new HydrationMap