diff --git a/lexicons/tools/ozone/moderation/defs.json b/lexicons/tools/ozone/moderation/defs.json index 23bba198f24..daa0c4e8a68 100644 --- a/lexicons/tools/ozone/moderation/defs.json +++ b/lexicons/tools/ozone/moderation/defs.json @@ -374,6 +374,10 @@ "negateLabelVals": { "type": "array", "items": { "type": "string" } + }, + "durationInHours": { + "type": "integer", + "description": "Indicates how long the label will remain on the subject. Only applies on labels that are being added." } } }, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 36604fa13ae..b8f90be1cce 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -11549,6 +11549,11 @@ export const schemaDict = { type: 'string', }, }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long the label will remain on the subject. Only applies on labels that are being added.', + }, }, }, modEventAcknowledge: { diff --git a/packages/api/src/client/types/tools/ozone/moderation/defs.ts b/packages/api/src/client/types/tools/ozone/moderation/defs.ts index df753c9cfc4..796e32d5749 100644 --- a/packages/api/src/client/types/tools/ozone/moderation/defs.ts +++ b/packages/api/src/client/types/tools/ozone/moderation/defs.ts @@ -347,6 +347,8 @@ export interface ModEventLabel { comment?: string createLabelVals: string[] negateLabelVals: string[] + /** Indicates how long the label will remain on the subject. Only applies on labels that are being added. */ + durationInHours?: number [k: string]: unknown } diff --git a/packages/ozone/src/api/moderation/emitEvent.ts b/packages/ozone/src/api/moderation/emitEvent.ts index 7bf337e005b..47b421ad53b 100644 --- a/packages/ozone/src/api/moderation/emitEvent.ts +++ b/packages/ozone/src/api/moderation/emitEvent.ts @@ -207,6 +207,7 @@ const handleModerationEvent = async ({ ? result.event.negateLabelVals.split(' ') : undefined, }, + result.event.durationInHours ?? undefined, ) } diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index 36604fa13ae..b8f90be1cce 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -11549,6 +11549,11 @@ export const schemaDict = { type: 'string', }, }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long the label will remain on the subject. Only applies on labels that are being added.', + }, }, }, modEventAcknowledge: { diff --git a/packages/ozone/src/lexicon/types/tools/ozone/moderation/defs.ts b/packages/ozone/src/lexicon/types/tools/ozone/moderation/defs.ts index 572d323a298..e31a6d03dba 100644 --- a/packages/ozone/src/lexicon/types/tools/ozone/moderation/defs.ts +++ b/packages/ozone/src/lexicon/types/tools/ozone/moderation/defs.ts @@ -347,6 +347,8 @@ export interface ModEventLabel { comment?: string createLabelVals: string[] negateLabelVals: string[] + /** Indicates how long the label will remain on the subject. Only applies on labels that are being added. */ + durationInHours?: number [k: string]: unknown } diff --git a/packages/ozone/src/mod-service/index.ts b/packages/ozone/src/mod-service/index.ts index ff3891ed225..b298f28a43a 100644 --- a/packages/ozone/src/mod-service/index.ts +++ b/packages/ozone/src/mod-service/index.ts @@ -1159,13 +1159,19 @@ export class ModerationService { uri: string, cid: string | null, labels: { create?: string[]; negate?: string[] }, + durationInHours?: number, ): Promise { + const exp = + durationInHours !== undefined + ? addHoursToDate(durationInHours).toISOString() + : undefined const { create = [], negate = [] } = labels const toCreate = create.map((val) => ({ src: this.cfg.service.did, uri, cid: cid ?? undefined, val, + exp, cts: new Date().toISOString(), })) const toNegate = negate.map((val) => ({ diff --git a/packages/ozone/src/mod-service/views.ts b/packages/ozone/src/mod-service/views.ts index b52f8b47a9c..b4183f0a4d3 100644 --- a/packages/ozone/src/mod-service/views.ts +++ b/packages/ozone/src/mod-service/views.ts @@ -113,6 +113,7 @@ export class ModerationViews { [ 'tools.ozone.moderation.defs#modEventMuteReporter', 'tools.ozone.moderation.defs#modEventTakedown', + 'tools.ozone.moderation.defs#modEventLabel', 'tools.ozone.moderation.defs#modEventMute', ].includes(event.action) ) { diff --git a/packages/ozone/tests/moderation.test.ts b/packages/ozone/tests/moderation.test.ts index b3eae62a7d5..9b9bfb17c3c 100644 --- a/packages/ozone/tests/moderation.test.ts +++ b/packages/ozone/tests/moderation.test.ts @@ -24,6 +24,7 @@ import { EventReverser } from '../src' import { ImageInvalidator } from '../src/image-invalidator' import { TAKEDOWN_LABEL } from '../src/mod-service' import { ids } from '../src/lexicon/lexicons' +import { HOUR } from '@atproto/common' describe('moderation', () => { let network: TestNetwork @@ -514,6 +515,24 @@ describe('moderation', () => { await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual(['kittens']) }) + it('creates expiring label', async () => { + await emitLabelEvent({ + createLabelVals: ['temp'], + negateLabelVals: [], + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + durationInHours: 24, + }) + const repo = await getRepo(sc.dids.bob) + // Losely check that the expiry date is set to above 23 hours from now + expect( + `${repo?.labels?.[0].exp}` > + new Date(Date.now() + 23 * HOUR).toISOString(), + ).toBeTruthy() + }) + it('does not allow triage moderators to label.', async () => { const attemptLabel = modClient.emitEvent( { @@ -708,14 +727,16 @@ describe('moderation', () => { subject: ToolsOzoneModerationEmitEvent.InputSchema['subject'] createLabelVals: ModEventLabel['createLabelVals'] negateLabelVals: ModEventLabel['negateLabelVals'] + durationInHours?: ModEventLabel['durationInHours'] }, ) { - const { createLabelVals, negateLabelVals } = opts + const { createLabelVals, negateLabelVals, durationInHours } = opts const result = await modClient.emitEvent({ event: { $type: 'tools.ozone.moderation.defs#modEventLabel', createLabelVals, negateLabelVals, + durationInHours, }, createdBy: 'did:example:admin', reason: 'Y', @@ -740,7 +761,7 @@ describe('moderation', () => { } async function getRecordLabels(uri: string) { - const result = await agent.api.tools.ozone.moderation.getRecord( + const result = await agent.tools.ozone.moderation.getRecord( { uri }, { headers: await network.ozone.modHeaders( @@ -752,8 +773,8 @@ describe('moderation', () => { return labels.map((l) => l.val) } - async function getRepoLabels(did: string) { - const result = await agent.api.tools.ozone.moderation.getRepo( + async function getRepo(did: string) { + const result = await agent.tools.ozone.moderation.getRepo( { did }, { headers: await network.ozone.modHeaders( @@ -761,7 +782,12 @@ describe('moderation', () => { ), }, ) - const labels = result.data.labels ?? [] + return result.data + } + + async function getRepoLabels(did: string) { + const result = await getRepo(did) + const labels = result.labels ?? [] return labels.map((l) => l.val) } }) diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 36604fa13ae..b8f90be1cce 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -11549,6 +11549,11 @@ export const schemaDict = { type: 'string', }, }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long the label will remain on the subject. Only applies on labels that are being added.', + }, }, }, modEventAcknowledge: { diff --git a/packages/pds/src/lexicon/types/tools/ozone/moderation/defs.ts b/packages/pds/src/lexicon/types/tools/ozone/moderation/defs.ts index 572d323a298..e31a6d03dba 100644 --- a/packages/pds/src/lexicon/types/tools/ozone/moderation/defs.ts +++ b/packages/pds/src/lexicon/types/tools/ozone/moderation/defs.ts @@ -347,6 +347,8 @@ export interface ModEventLabel { comment?: string createLabelVals: string[] negateLabelVals: string[] + /** Indicates how long the label will remain on the subject. Only applies on labels that are being added. */ + durationInHours?: number [k: string]: unknown }