diff --git a/packages/bsky/src/data-plane/server/db/migrations/20250207T174822012Z-add-label-exp.ts b/packages/bsky/src/data-plane/server/db/migrations/20250207T174822012Z-add-label-exp.ts new file mode 100644 index 00000000000..4fc3f9db02a --- /dev/null +++ b/packages/bsky/src/data-plane/server/db/migrations/20250207T174822012Z-add-label-exp.ts @@ -0,0 +1,9 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema.alterTable('label').addColumn('exp', 'varchar').execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.alterTable('label').dropColumn('exp').execute() +} diff --git a/packages/bsky/src/data-plane/server/db/migrations/index.ts b/packages/bsky/src/data-plane/server/db/migrations/index.ts index 90356eca865..890658bd43f 100644 --- a/packages/bsky/src/data-plane/server/db/migrations/index.ts +++ b/packages/bsky/src/data-plane/server/db/migrations/index.ts @@ -46,3 +46,4 @@ export * as _20240829T211238293Z from './20240829T211238293Z-simplify-actor-sync export * as _20240831T134810923Z from './20240831T134810923Z-pinned-posts' export * as _20241114T153108102Z from './20241114T153108102Z-add-starter-packs-name' export * as _20250116T222618297Z from './20250116T222618297Z-post-embed-video' +export * as _20250207T174822012Z from './20250207T174822012Z-add-label-exp' diff --git a/packages/bsky/src/data-plane/server/db/tables/label.ts b/packages/bsky/src/data-plane/server/db/tables/label.ts index 0c8a398a7db..a2e783f3a65 100644 --- a/packages/bsky/src/data-plane/server/db/tables/label.ts +++ b/packages/bsky/src/data-plane/server/db/tables/label.ts @@ -7,6 +7,7 @@ export interface Label { val: string neg: boolean cts: string + exp: string | null } export type PartialDB = { [tableName]: Label } diff --git a/packages/bsky/src/data-plane/server/routes/labels.ts b/packages/bsky/src/data-plane/server/routes/labels.ts index 045279f8238..9c1e5438731 100644 --- a/packages/bsky/src/data-plane/server/routes/labels.ts +++ b/packages/bsky/src/data-plane/server/routes/labels.ts @@ -1,5 +1,5 @@ import { ServiceImpl } from '@connectrpc/connect' -import { Selectable } from 'kysely' +import { Selectable, sql } from 'kysely' import * as ui8 from 'uint8arrays' import { noUndefinedVals } from '@atproto/common' import { Service } from '../../../proto/bsky_connect' @@ -14,10 +14,14 @@ export default (db: Database): Partial> => ({ if (subjects.length === 0 || issuers.length === 0) { return { labels: [] } } + const res: LabelRow[] = await db.db .selectFrom('label') .where('uri', 'in', subjects) .where('src', 'in', issuers) + .where((qb) => + qb.where('exp', 'is', null).orWhere(sql`exp::timestamp > now()`), + ) .selectAll() .execute() @@ -34,6 +38,7 @@ export default (db: Database): Partial> => ({ return labelsForSub.map((l) => { const formatted = noUndefinedVals({ ...l, + exp: l.exp === null ? undefined : l.exp, cid: l.cid === '' ? undefined : l.cid, neg: l.neg === true ? true : undefined, }) diff --git a/packages/bsky/tests/label-hydration.test.ts b/packages/bsky/tests/label-hydration.test.ts index c9711b24d40..88beb902684 100644 --- a/packages/bsky/tests/label-hydration.test.ts +++ b/packages/bsky/tests/label-hydration.test.ts @@ -1,4 +1,6 @@ +import assert from 'node:assert' import { AtpAgent } from '@atproto/api' +import { MINUTE } from '@atproto/common' import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env' describe('label hydration', () => { @@ -10,6 +12,7 @@ describe('label hydration', () => { let bob: string let carol: string let labelerDid: string + let labeler2Did: string beforeAll(async () => { network = await TestNetwork.create({ @@ -22,6 +25,7 @@ describe('label hydration', () => { bob = sc.dids.bob carol = sc.dids.carol labelerDid = network.bsky.ctx.cfg.labelsFromIssuerDids[0] + labeler2Did = network.bsky.ctx.cfg.labelsFromIssuerDids[1] await createLabel({ src: alice, uri: carol, cid: '', val: 'spam' }) await createLabel({ src: bob, uri: carol, cid: '', val: 'impersonation' }) await createLabel({ @@ -30,6 +34,20 @@ describe('label hydration', () => { cid: '', val: 'misleading', }) + await createLabel({ + src: labeler2Did, + uri: carol, + cid: '', + val: 'expired', + exp: new Date(Date.now() - MINUTE).toISOString(), + }) + await createLabel({ + src: labeler2Did, + uri: carol, + cid: '', + val: 'not-expired', + exp: new Date(Date.now() + MINUTE).toISOString(), + }) await network.processAll() }) @@ -39,17 +57,33 @@ describe('label hydration', () => { it('hydrates labels based on a supplied labeler header', async () => { AtpAgent.configure({ appLabelers: [alice] }) - pdsAgent.configureLabelers([]) + pdsAgent.configureLabelers([labeler2Did]) const res = await pdsAgent.api.app.bsky.actor.getProfile( { actor: carol }, { headers: sc.getHeaders(bob), }, ) - expect(res.data.labels?.length).toBe(1) - expect(res.data.labels?.[0].src).toBe(alice) - expect(res.data.labels?.[0].val).toBe('spam') - expect(res.headers['atproto-content-labelers']).toEqual(`${alice};redact`) + expect(res.data.labels?.length).toBe(2) + assert(res.data.labels) + + const sortedLabels = res.data.labels.sort((a, b) => + a.src.localeCompare(b.src), + ) + const sortedExpected = [ + { src: labeler2Did, val: 'not-expired' }, + { src: alice, val: 'spam' }, + ].sort((a, b) => a.src.localeCompare(b.src)) + + expect(sortedLabels[0].src).toBe(sortedExpected[0].src) + expect(sortedLabels[0].val).toBe(sortedExpected[0].val) + + expect(sortedLabels[1].src).toBe(sortedExpected[1].src) + expect(sortedLabels[1].val).toBe(sortedExpected[1].val) + + expect(res.headers['atproto-content-labelers']).toEqual( + `${alice};redact,${labeler2Did}`, + ) }) it('hydrates labels based on multiple a supplied labelers', async () => { @@ -88,9 +122,21 @@ describe('label hydration', () => { { headers: sc.getHeaders(bob) }, ) const data = await res.json() - expect(data.labels?.length).toBe(1) - expect(data.labels?.[0].src).toBe(labelerDid) - expect(data.labels?.[0].val).toBe('misleading') + + expect(data.labels?.length).toBe(2) + assert(data.labels) + + const sortedLabels = data.labels.sort((a, b) => a.src.localeCompare(b.src)) + const sortedExpected = [ + { src: labeler2Did, val: 'not-expired' }, + { src: labelerDid, val: 'misleading' }, + ].sort((a, b) => a.src.localeCompare(b.src)) + + expect(sortedLabels[0].src).toBe(sortedExpected[0].src) + expect(sortedLabels[0].val).toBe(sortedExpected[0].val) + + expect(sortedLabels[1].src).toBe(sortedExpected[1].src) + expect(sortedLabels[1].val).toBe(sortedExpected[1].val) expect(res.headers.get('atproto-content-labelers')).toEqual( network.bsky.ctx.cfg.labelsFromIssuerDids @@ -181,6 +227,7 @@ describe('label hydration', () => { uri: string cid: string val: string + exp?: string }) => { await network.bsky.db.db .insertInto('label') @@ -189,6 +236,7 @@ describe('label hydration', () => { cid: opts.cid, val: opts.val, cts: new Date().toISOString(), + exp: opts.exp ?? null, neg: false, src: opts.src ?? labelerDid, }) diff --git a/packages/bsky/tests/query-labels.test.ts b/packages/bsky/tests/query-labels.test.ts index 401bba89ecd..059575322a5 100644 --- a/packages/bsky/tests/query-labels.test.ts +++ b/packages/bsky/tests/query-labels.test.ts @@ -76,6 +76,7 @@ describe('label hydration', () => { cid: opts.cid, val: opts.val, cts: new Date().toISOString(), + exp: null, neg: false, src: opts.src ?? labelerDid, }) diff --git a/packages/bsky/tests/views/labels-needs-review.test.ts b/packages/bsky/tests/views/labels-needs-review.test.ts index dbd37bdb1c3..02e23f18cd8 100644 --- a/packages/bsky/tests/views/labels-needs-review.test.ts +++ b/packages/bsky/tests/views/labels-needs-review.test.ts @@ -50,6 +50,7 @@ describe('bsky needs-review labels', () => { uri: sc.dids.geoff, cid: '', val: 'needs-review', + exp: null, neg: false, cts: new Date().toISOString(), }) diff --git a/packages/bsky/tests/views/labels-takedown.test.ts b/packages/bsky/tests/views/labels-takedown.test.ts index 98b59810268..10390002416 100644 --- a/packages/bsky/tests/views/labels-takedown.test.ts +++ b/packages/bsky/tests/views/labels-takedown.test.ts @@ -121,6 +121,7 @@ describe('bsky takedown labels', () => { uri, cid: '', val: '!takedown', + exp: null, neg: false, cts, })) diff --git a/packages/bsky/tests/views/timeline.test.ts b/packages/bsky/tests/views/timeline.test.ts index 46727b0740f..831fba5eabf 100644 --- a/packages/bsky/tests/views/timeline.test.ts +++ b/packages/bsky/tests/views/timeline.test.ts @@ -315,6 +315,7 @@ const createLabel = async ( cid: opts.cid, val: opts.val, cts: new Date().toISOString(), + exp: null, neg: false, src: EXAMPLE_LABELER, })