From d6f33b4742e0b94722a993efc7d18833d9416bb6 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Thu, 7 Nov 2024 22:51:44 +0100 Subject: [PATCH] :sparkles: Add events for account and record update/delete/deactivation (#2661) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: Add events for account and record update/delete/deactivation * :sparkles: Add handle change event * :sparkles: Reduce account events to 2 types and record events to 1 * :sparkles: Store metadata from account, identity and record events * :sparkles: Add created event for record * :sparkles: Add ndd the new events to allowed types in emitEvent * :sparkles: Use string value for record op and add tombstone flag to identity event * :sparkles: Add active flag on account events * :sparkles: Change accountStatus -> status to match with firehose event * :sparkles: Make active flag required * :rotating_light: fix prettier style issue * ✨ Track record/account delete and update data in subject status (#2804) * :sparkles: Store deleted/updated event data in subject_status * :bug: Fix query for recordDeletedAt and recordUpdatedAt * :sparkles: Add tombstoned status * :sparkles: Move from record to hosting term * :white_check_mark: Add tests for hosting params * :sparkles: Update lexicons for hostingStatuses * :white_check_mark: Update snapshots * :white_check_mark: Update snapshots * :white_check_mark: Update snapshots * :sparkles: Adjust hosting statuses * :memo: Add changeset --- .changeset/clever-beers-flash.md | 6 + lexicons/tools/ozone/moderation/defs.json | 128 +++++++++++- .../tools/ozone/moderation/emitEvent.json | 5 +- .../tools/ozone/moderation/queryStatuses.json | 27 +++ packages/api/src/client/lexicons.ts | 186 ++++++++++++++++++ .../types/tools/ozone/moderation/defs.ts | 130 ++++++++++++ .../types/tools/ozone/moderation/emitEvent.ts | 3 + .../tools/ozone/moderation/queryStatuses.ts | 10 + .../ozone/src/api/moderation/queryStatuses.ts | 10 + ...2Z-add-hosting-status-to-subject-status.ts | 57 ++++++ packages/ozone/src/db/migrations/index.ts | 1 + .../ozone/src/db/schema/moderation_event.ts | 3 + .../db/schema/moderation_subject_status.ts | 6 + packages/ozone/src/lexicon/lexicons.ts | 186 ++++++++++++++++++ .../types/tools/ozone/moderation/defs.ts | 130 ++++++++++++ .../types/tools/ozone/moderation/emitEvent.ts | 3 + .../tools/ozone/moderation/queryStatuses.ts | 10 + packages/ozone/src/mod-service/index.ts | 52 +++++ packages/ozone/src/mod-service/status.ts | 116 ++++++++++- packages/ozone/src/mod-service/types.ts | 25 +++ packages/ozone/src/mod-service/views.ts | 47 ++++- .../__snapshots__/get-record.test.ts.snap | 8 + .../__snapshots__/get-records.test.ts.snap | 4 + .../tests/__snapshots__/get-repo.test.ts.snap | 4 + .../__snapshots__/get-repos.test.ts.snap | 4 + .../moderation-events.test.ts.snap | 4 + .../moderation-statuses.test.ts.snap | 24 +++ .../tests/record-and-account-events.test.ts | 185 +++++++++++++++++ packages/pds/src/lexicon/lexicons.ts | 186 ++++++++++++++++++ .../types/tools/ozone/moderation/defs.ts | 130 ++++++++++++ .../types/tools/ozone/moderation/emitEvent.ts | 3 + .../tools/ozone/moderation/queryStatuses.ts | 10 + .../proxied/__snapshots__/admin.test.ts.snap | 12 ++ 33 files changed, 1708 insertions(+), 7 deletions(-) create mode 100644 .changeset/clever-beers-flash.md create mode 100644 packages/ozone/src/db/migrations/20241026T205730722Z-add-hosting-status-to-subject-status.ts create mode 100644 packages/ozone/tests/record-and-account-events.test.ts diff --git a/.changeset/clever-beers-flash.md b/.changeset/clever-beers-flash.md new file mode 100644 index 00000000000..6e482048447 --- /dev/null +++ b/.changeset/clever-beers-flash.md @@ -0,0 +1,6 @@ +--- +"@atproto/ozone": patch +"@atproto/api": patch +--- + +Add mod events and status filter for account and record hosting status diff --git a/lexicons/tools/ozone/moderation/defs.json b/lexicons/tools/ozone/moderation/defs.json index 6e0bc400b33..9210bfc5ec8 100644 --- a/lexicons/tools/ozone/moderation/defs.json +++ b/lexicons/tools/ozone/moderation/defs.json @@ -31,7 +31,10 @@ "#modEventEmail", "#modEventResolveAppeal", "#modEventDivert", - "#modEventTag" + "#modEventTag", + "#accountEvent", + "#identityEvent", + "#recordEvent" ] }, "subject": { @@ -78,7 +81,10 @@ "#modEventEmail", "#modEventResolveAppeal", "#modEventDivert", - "#modEventTag" + "#modEventTag", + "#accountEvent", + "#identityEvent", + "#recordEvent" ] }, "subject": { @@ -110,6 +116,10 @@ "com.atproto.repo.strongRef" ] }, + "hosting": { + "type": "union", + "refs": ["#accountHosting", "#recordHosting"] + }, "subjectBlobCids": { "type": "array", "items": { "type": "string", "format": "cid" } @@ -390,6 +400,62 @@ } } }, + "accountEvent": { + "type": "object", + "description": "Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.", + "required": ["timestamp", "active"], + "properties": { + "comment": { "type": "string" }, + "active": { + "type": "boolean", + "description": "Indicates that the account has a repository which can be fetched from the host that emitted this event." + }, + "status": { + "type": "string", + "knownValues": [ + "unknown", + "deactivated", + "deleted", + "takendown", + "suspended", + "tombstoned" + ] + }, + "timestamp": { + "type": "string", + "format": "datetime" + } + } + }, + "identityEvent": { + "type": "object", + "description": "Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.", + "required": ["timestamp"], + "properties": { + "comment": { "type": "string" }, + "handle": { "type": "string", "format": "handle" }, + "pdsHost": { "type": "string", "format": "uri" }, + "tombstone": { "type": "boolean" }, + "timestamp": { + "type": "string", + "format": "datetime" + } + } + }, + "recordEvent": { + "type": "object", + "description": "Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.", + "required": ["timestamp", "op"], + "properties": { + "comment": { "type": "string" }, + "op": { + "type": "string", + "knownValues": ["create", "update", "delete"] + }, + "cid": { "type": "string", "format": "cid" }, + "timestamp": { "type": "string", "format": "datetime" } + } + }, "repoView": { "type": "object", "required": [ @@ -578,6 +644,64 @@ "height": { "type": "integer" }, "length": { "type": "integer" } } + }, + "accountHosting": { + "type": "object", + "required": ["status"], + "properties": { + "status": { + "type": "string", + "knownValues": [ + "takendown", + "suspended", + "deleted", + "deactivated", + "unknown" + ] + }, + "updatedAt": { + "type": "string", + "format": "datetime" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "deletedAt": { + "type": "string", + "format": "datetime" + }, + "deactivatedAt": { + "type": "string", + "format": "datetime" + }, + "reactivatedAt": { + "type": "string", + "format": "datetime" + } + } + }, + "recordHosting": { + "type": "object", + "required": ["status"], + "properties": { + "status": { + "type": "string", + "knownValues": ["deleted", "unknown"] + }, + "updatedAt": { + "type": "string", + "format": "datetime" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "deletedAt": { + "type": "string", + "format": "datetime" + } + } } } } diff --git a/lexicons/tools/ozone/moderation/emitEvent.json b/lexicons/tools/ozone/moderation/emitEvent.json index 4692dfe29ba..2e85bbfc9b6 100644 --- a/lexicons/tools/ozone/moderation/emitEvent.json +++ b/lexicons/tools/ozone/moderation/emitEvent.json @@ -27,7 +27,10 @@ "tools.ozone.moderation.defs#modEventReverseTakedown", "tools.ozone.moderation.defs#modEventResolveAppeal", "tools.ozone.moderation.defs#modEventEmail", - "tools.ozone.moderation.defs#modEventTag" + "tools.ozone.moderation.defs#modEventTag", + "tools.ozone.moderation.defs#accountEvent", + "tools.ozone.moderation.defs#identityEvent", + "tools.ozone.moderation.defs#recordEvent" ] }, "subject": { diff --git a/lexicons/tools/ozone/moderation/queryStatuses.json b/lexicons/tools/ozone/moderation/queryStatuses.json index 15ce4e180d3..b8bc48df44d 100644 --- a/lexicons/tools/ozone/moderation/queryStatuses.json +++ b/lexicons/tools/ozone/moderation/queryStatuses.json @@ -36,6 +36,33 @@ "format": "datetime", "description": "Search subjects reviewed after a given timestamp" }, + "hostingDeletedAfter": { + "type": "string", + "format": "datetime", + "description": "Search subjects where the associated record/account was deleted after a given timestamp" + }, + "hostingDeletedBefore": { + "type": "string", + "format": "datetime", + "description": "Search subjects where the associated record/account was deleted before a given timestamp" + }, + "hostingUpdatedAfter": { + "type": "string", + "format": "datetime", + "description": "Search subjects where the associated record/account was updated after a given timestamp" + }, + "hostingUpdatedBefore": { + "type": "string", + "format": "datetime", + "description": "Search subjects where the associated record/account was updated before a given timestamp" + }, + "hostingStatuses": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Search subjects by the status of the associated record/account" + }, "reviewedBefore": { "type": "string", "format": "datetime", diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 3d6ae795019..23376822cc4 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -10821,6 +10821,9 @@ export const schemaDict = { 'lex:tools.ozone.moderation.defs#modEventResolveAppeal', 'lex:tools.ozone.moderation.defs#modEventDivert', 'lex:tools.ozone.moderation.defs#modEventTag', + 'lex:tools.ozone.moderation.defs#accountEvent', + 'lex:tools.ozone.moderation.defs#identityEvent', + 'lex:tools.ozone.moderation.defs#recordEvent', ], }, subject: { @@ -10885,6 +10888,9 @@ export const schemaDict = { 'lex:tools.ozone.moderation.defs#modEventResolveAppeal', 'lex:tools.ozone.moderation.defs#modEventDivert', 'lex:tools.ozone.moderation.defs#modEventTag', + 'lex:tools.ozone.moderation.defs#accountEvent', + 'lex:tools.ozone.moderation.defs#identityEvent', + 'lex:tools.ozone.moderation.defs#recordEvent', ], }, subject: { @@ -10927,6 +10933,13 @@ export const schemaDict = { 'lex:com.atproto.repo.strongRef', ], }, + hosting: { + type: 'union', + refs: [ + 'lex:tools.ozone.moderation.defs#accountHosting', + 'lex:tools.ozone.moderation.defs#recordHosting', + ], + }, subjectBlobCids: { type: 'array', items: { @@ -11246,6 +11259,86 @@ export const schemaDict = { }, }, }, + accountEvent: { + type: 'object', + description: + 'Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.', + required: ['timestamp', 'active'], + properties: { + comment: { + type: 'string', + }, + active: { + type: 'boolean', + description: + 'Indicates that the account has a repository which can be fetched from the host that emitted this event.', + }, + status: { + type: 'string', + knownValues: [ + 'unknown', + 'deactivated', + 'deleted', + 'takendown', + 'suspended', + 'tombstoned', + ], + }, + timestamp: { + type: 'string', + format: 'datetime', + }, + }, + }, + identityEvent: { + type: 'object', + description: + 'Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.', + required: ['timestamp'], + properties: { + comment: { + type: 'string', + }, + handle: { + type: 'string', + format: 'handle', + }, + pdsHost: { + type: 'string', + format: 'uri', + }, + tombstone: { + type: 'boolean', + }, + timestamp: { + type: 'string', + format: 'datetime', + }, + }, + }, + recordEvent: { + type: 'object', + description: + 'Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.', + required: ['timestamp', 'op'], + properties: { + comment: { + type: 'string', + }, + op: { + type: 'string', + knownValues: ['create', 'update', 'delete'], + }, + cid: { + type: 'string', + format: 'cid', + }, + timestamp: { + type: 'string', + format: 'datetime', + }, + }, + }, repoView: { type: 'object', required: [ @@ -11571,6 +11664,64 @@ export const schemaDict = { }, }, }, + accountHosting: { + type: 'object', + required: ['status'], + properties: { + status: { + type: 'string', + knownValues: [ + 'takendown', + 'suspended', + 'deleted', + 'deactivated', + 'unknown', + ], + }, + updatedAt: { + type: 'string', + format: 'datetime', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + deletedAt: { + type: 'string', + format: 'datetime', + }, + deactivatedAt: { + type: 'string', + format: 'datetime', + }, + reactivatedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + recordHosting: { + type: 'object', + required: ['status'], + properties: { + status: { + type: 'string', + knownValues: ['deleted', 'unknown'], + }, + updatedAt: { + type: 'string', + format: 'datetime', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + deletedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, }, }, ToolsOzoneModerationEmitEvent: { @@ -11603,6 +11754,9 @@ export const schemaDict = { 'lex:tools.ozone.moderation.defs#modEventResolveAppeal', 'lex:tools.ozone.moderation.defs#modEventEmail', 'lex:tools.ozone.moderation.defs#modEventTag', + 'lex:tools.ozone.moderation.defs#accountEvent', + 'lex:tools.ozone.moderation.defs#identityEvent', + 'lex:tools.ozone.moderation.defs#recordEvent', ], }, subject: { @@ -12005,6 +12159,38 @@ export const schemaDict = { format: 'datetime', description: 'Search subjects reviewed after a given timestamp', }, + hostingDeletedAfter: { + type: 'string', + format: 'datetime', + description: + 'Search subjects where the associated record/account was deleted after a given timestamp', + }, + hostingDeletedBefore: { + type: 'string', + format: 'datetime', + description: + 'Search subjects where the associated record/account was deleted before a given timestamp', + }, + hostingUpdatedAfter: { + type: 'string', + format: 'datetime', + description: + 'Search subjects where the associated record/account was updated after a given timestamp', + }, + hostingUpdatedBefore: { + type: 'string', + format: 'datetime', + description: + 'Search subjects where the associated record/account was updated before a given timestamp', + }, + hostingStatuses: { + type: 'array', + items: { + type: 'string', + }, + description: + 'Search subjects by the status of the associated record/account', + }, reviewedBefore: { type: 'string', format: 'datetime', 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 461e46968d8..f96fccf3a1e 100644 --- a/packages/api/src/client/types/tools/ozone/moderation/defs.ts +++ b/packages/api/src/client/types/tools/ozone/moderation/defs.ts @@ -30,6 +30,9 @@ export interface ModEventView { | ModEventResolveAppeal | ModEventDivert | ModEventTag + | AccountEvent + | IdentityEvent + | RecordEvent | { $type: string; [k: string]: unknown } subject: | ComAtprotoAdminDefs.RepoRef @@ -74,6 +77,9 @@ export interface ModEventViewDetail { | ModEventResolveAppeal | ModEventDivert | ModEventTag + | AccountEvent + | IdentityEvent + | RecordEvent | { $type: string; [k: string]: unknown } subject: | RepoView @@ -105,6 +111,10 @@ export interface SubjectStatusView { | ComAtprotoAdminDefs.RepoRef | ComAtprotoRepoStrongRef.Main | { $type: string; [k: string]: unknown } + hosting?: + | AccountHosting + | RecordHosting + | { $type: string; [k: string]: unknown } subjectBlobCids?: string[] subjectRepoHandle?: string /** Timestamp referencing when the last update was made to the moderation status of the subject */ @@ -472,6 +482,78 @@ export function validateModEventTag(v: unknown): ValidationResult { return lexicons.validate('tools.ozone.moderation.defs#modEventTag', v) } +/** Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */ +export interface AccountEvent { + comment?: string + /** Indicates that the account has a repository which can be fetched from the host that emitted this event. */ + active: boolean + status?: + | 'unknown' + | 'deactivated' + | 'deleted' + | 'takendown' + | 'suspended' + | 'tombstoned' + | (string & {}) + timestamp: string + [k: string]: unknown +} + +export function isAccountEvent(v: unknown): v is AccountEvent { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#accountEvent' + ) +} + +export function validateAccountEvent(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#accountEvent', v) +} + +/** Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */ +export interface IdentityEvent { + comment?: string + handle?: string + pdsHost?: string + tombstone?: boolean + timestamp: string + [k: string]: unknown +} + +export function isIdentityEvent(v: unknown): v is IdentityEvent { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#identityEvent' + ) +} + +export function validateIdentityEvent(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#identityEvent', v) +} + +/** Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */ +export interface RecordEvent { + comment?: string + op: 'create' | 'update' | 'delete' | (string & {}) + cid?: string + timestamp: string + [k: string]: unknown +} + +export function isRecordEvent(v: unknown): v is RecordEvent { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#recordEvent' + ) +} + +export function validateRecordEvent(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#recordEvent', v) +} + export interface RepoView { did: string handle: string @@ -705,3 +787,51 @@ export function isVideoDetails(v: unknown): v is VideoDetails { export function validateVideoDetails(v: unknown): ValidationResult { return lexicons.validate('tools.ozone.moderation.defs#videoDetails', v) } + +export interface AccountHosting { + status: + | 'takendown' + | 'suspended' + | 'deleted' + | 'deactivated' + | 'unknown' + | (string & {}) + updatedAt?: string + createdAt?: string + deletedAt?: string + deactivatedAt?: string + reactivatedAt?: string + [k: string]: unknown +} + +export function isAccountHosting(v: unknown): v is AccountHosting { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#accountHosting' + ) +} + +export function validateAccountHosting(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#accountHosting', v) +} + +export interface RecordHosting { + status: 'deleted' | 'unknown' | (string & {}) + updatedAt?: string + createdAt?: string + deletedAt?: string + [k: string]: unknown +} + +export function isRecordHosting(v: unknown): v is RecordHosting { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#recordHosting' + ) +} + +export function validateRecordHosting(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#recordHosting', v) +} diff --git a/packages/api/src/client/types/tools/ozone/moderation/emitEvent.ts b/packages/api/src/client/types/tools/ozone/moderation/emitEvent.ts index bc3495c5ab7..56cf9279f0b 100644 --- a/packages/api/src/client/types/tools/ozone/moderation/emitEvent.ts +++ b/packages/api/src/client/types/tools/ozone/moderation/emitEvent.ts @@ -28,6 +28,9 @@ export interface InputSchema { | ToolsOzoneModerationDefs.ModEventResolveAppeal | ToolsOzoneModerationDefs.ModEventEmail | ToolsOzoneModerationDefs.ModEventTag + | ToolsOzoneModerationDefs.AccountEvent + | ToolsOzoneModerationDefs.IdentityEvent + | ToolsOzoneModerationDefs.RecordEvent | { $type: string; [k: string]: unknown } subject: | ComAtprotoAdminDefs.RepoRef diff --git a/packages/api/src/client/types/tools/ozone/moderation/queryStatuses.ts b/packages/api/src/client/types/tools/ozone/moderation/queryStatuses.ts index 76cedca54c6..2db3b9c0a60 100644 --- a/packages/api/src/client/types/tools/ozone/moderation/queryStatuses.ts +++ b/packages/api/src/client/types/tools/ozone/moderation/queryStatuses.ts @@ -21,6 +21,16 @@ export interface QueryParams { reportedBefore?: string /** Search subjects reviewed after a given timestamp */ reviewedAfter?: string + /** Search subjects where the associated record/account was deleted after a given timestamp */ + hostingDeletedAfter?: string + /** Search subjects where the associated record/account was deleted before a given timestamp */ + hostingDeletedBefore?: string + /** Search subjects where the associated record/account was updated after a given timestamp */ + hostingUpdatedAfter?: string + /** Search subjects where the associated record/account was updated before a given timestamp */ + hostingUpdatedBefore?: string + /** Search subjects by the status of the associated record/account */ + hostingStatuses?: string[] /** Search subjects reviewed before a given timestamp */ reviewedBefore?: string /** By default, we don't include muted subjects in the results. Set this to true to include them. */ diff --git a/packages/ozone/src/api/moderation/queryStatuses.ts b/packages/ozone/src/api/moderation/queryStatuses.ts index dc1061a8327..00929ff272b 100644 --- a/packages/ozone/src/api/moderation/queryStatuses.ts +++ b/packages/ozone/src/api/moderation/queryStatuses.ts @@ -18,6 +18,11 @@ export default function (server: Server, ctx: AppContext) { reportedBefore, ignoreSubjects, lastReviewedBy, + hostingDeletedBefore, + hostingDeletedAfter, + hostingUpdatedBefore, + hostingUpdatedAfter, + hostingStatuses, sortDirection = 'desc', sortField = 'lastReportedAt', includeMuted = false, @@ -42,6 +47,11 @@ export default function (server: Server, ctx: AppContext) { reportedAfter, reportedBefore, includeMuted, + hostingDeletedBefore, + hostingDeletedAfter, + hostingUpdatedBefore, + hostingUpdatedAfter, + hostingStatuses, onlyMuted, ignoreSubjects, sortDirection, diff --git a/packages/ozone/src/db/migrations/20241026T205730722Z-add-hosting-status-to-subject-status.ts b/packages/ozone/src/db/migrations/20241026T205730722Z-add-hosting-status-to-subject-status.ts new file mode 100644 index 00000000000..927f98ff853 --- /dev/null +++ b/packages/ozone/src/db/migrations/20241026T205730722Z-add-hosting-status-to-subject-status.ts @@ -0,0 +1,57 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable('moderation_subject_status') + .addColumn('hostingStatus', 'varchar', (col) => + col.notNull().defaultTo('unknown'), + ) + .execute() + await db.schema + .alterTable('moderation_subject_status') + .addColumn('hostingDeletedAt', 'varchar') + .execute() + await db.schema + .alterTable('moderation_subject_status') + .addColumn('hostingUpdatedAt', 'varchar') + .execute() + await db.schema + .alterTable('moderation_subject_status') + .addColumn('hostingCreatedAt', 'varchar') + .execute() + await db.schema + .alterTable('moderation_subject_status') + .addColumn('hostingDeactivatedAt', 'varchar') + .execute() + await db.schema + .alterTable('moderation_subject_status') + .addColumn('hostingReactivatedAt', 'varchar') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema + .alterTable('moderation_subject_status') + .dropColumn('hostingStatus') + .execute() + await db.schema + .alterTable('moderation_subject_status') + .dropColumn('hostingDeletedAt') + .execute() + await db.schema + .alterTable('moderation_subject_status') + .dropColumn('hostingUpdatedAt') + .execute() + await db.schema + .alterTable('moderation_subject_status') + .dropColumn('hostingCreatedAt') + .execute() + await db.schema + .alterTable('moderation_subject_status') + .dropColumn('hostingDeactivatedAt') + .execute() + await db.schema + .alterTable('moderation_subject_status') + .dropColumn('hostingReactivatedAt') + .execute() +} diff --git a/packages/ozone/src/db/migrations/index.ts b/packages/ozone/src/db/migrations/index.ts index 8d996398921..be299b76edb 100644 --- a/packages/ozone/src/db/migrations/index.ts +++ b/packages/ozone/src/db/migrations/index.ts @@ -16,3 +16,4 @@ export * as _20240904T205730722Z from './20240904T205730722Z-add-subject-did-ind export * as _20241001T205730722Z from './20241001T205730722Z-subject-status-review-state-index' export * as _20241008T205730722Z from './20241008T205730722Z-sets' export * as _20241018T205730722Z from './20241018T205730722Z-setting' +export * as _20241026T205730722Z from './20241026T205730722Z-add-hosting-status-to-subject-status' diff --git a/packages/ozone/src/db/schema/moderation_event.ts b/packages/ozone/src/db/schema/moderation_event.ts index 6eb6571b041..c6d7dc132a2 100644 --- a/packages/ozone/src/db/schema/moderation_event.ts +++ b/packages/ozone/src/db/schema/moderation_event.ts @@ -19,6 +19,9 @@ export interface ModerationEvent { | 'tools.ozone.moderation.defs#modEventEmail' | 'tools.ozone.moderation.defs#modEventResolveAppeal' | 'tools.ozone.moderation.defs#modEventTag' + | 'tools.ozone.moderation.defs#accountEvent' + | 'tools.ozone.moderation.defs#identityEvent' + | 'tools.ozone.moderation.defs#recordEvent' subjectType: | 'com.atproto.admin.defs#repoRef' | 'com.atproto.repo.strongRef' diff --git a/packages/ozone/src/db/schema/moderation_subject_status.ts b/packages/ozone/src/db/schema/moderation_subject_status.ts index 82438c1dff3..a4d7ad1a72c 100644 --- a/packages/ozone/src/db/schema/moderation_subject_status.ts +++ b/packages/ozone/src/db/schema/moderation_subject_status.ts @@ -25,6 +25,12 @@ export interface ModerationSubjectStatus { lastReviewedAt: string | null lastReportedAt: string | null lastAppealedAt: string | null + hostingUpdatedAt: string | null + hostingDeletedAt: string | null + hostingCreatedAt: string | null + hostingDeactivatedAt: string | null + hostingReactivatedAt: string | null + hostingStatus: string | null muteUntil: string | null muteReportingUntil: string | null suspendUntil: string | null diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index 3d6ae795019..23376822cc4 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -10821,6 +10821,9 @@ export const schemaDict = { 'lex:tools.ozone.moderation.defs#modEventResolveAppeal', 'lex:tools.ozone.moderation.defs#modEventDivert', 'lex:tools.ozone.moderation.defs#modEventTag', + 'lex:tools.ozone.moderation.defs#accountEvent', + 'lex:tools.ozone.moderation.defs#identityEvent', + 'lex:tools.ozone.moderation.defs#recordEvent', ], }, subject: { @@ -10885,6 +10888,9 @@ export const schemaDict = { 'lex:tools.ozone.moderation.defs#modEventResolveAppeal', 'lex:tools.ozone.moderation.defs#modEventDivert', 'lex:tools.ozone.moderation.defs#modEventTag', + 'lex:tools.ozone.moderation.defs#accountEvent', + 'lex:tools.ozone.moderation.defs#identityEvent', + 'lex:tools.ozone.moderation.defs#recordEvent', ], }, subject: { @@ -10927,6 +10933,13 @@ export const schemaDict = { 'lex:com.atproto.repo.strongRef', ], }, + hosting: { + type: 'union', + refs: [ + 'lex:tools.ozone.moderation.defs#accountHosting', + 'lex:tools.ozone.moderation.defs#recordHosting', + ], + }, subjectBlobCids: { type: 'array', items: { @@ -11246,6 +11259,86 @@ export const schemaDict = { }, }, }, + accountEvent: { + type: 'object', + description: + 'Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.', + required: ['timestamp', 'active'], + properties: { + comment: { + type: 'string', + }, + active: { + type: 'boolean', + description: + 'Indicates that the account has a repository which can be fetched from the host that emitted this event.', + }, + status: { + type: 'string', + knownValues: [ + 'unknown', + 'deactivated', + 'deleted', + 'takendown', + 'suspended', + 'tombstoned', + ], + }, + timestamp: { + type: 'string', + format: 'datetime', + }, + }, + }, + identityEvent: { + type: 'object', + description: + 'Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.', + required: ['timestamp'], + properties: { + comment: { + type: 'string', + }, + handle: { + type: 'string', + format: 'handle', + }, + pdsHost: { + type: 'string', + format: 'uri', + }, + tombstone: { + type: 'boolean', + }, + timestamp: { + type: 'string', + format: 'datetime', + }, + }, + }, + recordEvent: { + type: 'object', + description: + 'Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.', + required: ['timestamp', 'op'], + properties: { + comment: { + type: 'string', + }, + op: { + type: 'string', + knownValues: ['create', 'update', 'delete'], + }, + cid: { + type: 'string', + format: 'cid', + }, + timestamp: { + type: 'string', + format: 'datetime', + }, + }, + }, repoView: { type: 'object', required: [ @@ -11571,6 +11664,64 @@ export const schemaDict = { }, }, }, + accountHosting: { + type: 'object', + required: ['status'], + properties: { + status: { + type: 'string', + knownValues: [ + 'takendown', + 'suspended', + 'deleted', + 'deactivated', + 'unknown', + ], + }, + updatedAt: { + type: 'string', + format: 'datetime', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + deletedAt: { + type: 'string', + format: 'datetime', + }, + deactivatedAt: { + type: 'string', + format: 'datetime', + }, + reactivatedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + recordHosting: { + type: 'object', + required: ['status'], + properties: { + status: { + type: 'string', + knownValues: ['deleted', 'unknown'], + }, + updatedAt: { + type: 'string', + format: 'datetime', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + deletedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, }, }, ToolsOzoneModerationEmitEvent: { @@ -11603,6 +11754,9 @@ export const schemaDict = { 'lex:tools.ozone.moderation.defs#modEventResolveAppeal', 'lex:tools.ozone.moderation.defs#modEventEmail', 'lex:tools.ozone.moderation.defs#modEventTag', + 'lex:tools.ozone.moderation.defs#accountEvent', + 'lex:tools.ozone.moderation.defs#identityEvent', + 'lex:tools.ozone.moderation.defs#recordEvent', ], }, subject: { @@ -12005,6 +12159,38 @@ export const schemaDict = { format: 'datetime', description: 'Search subjects reviewed after a given timestamp', }, + hostingDeletedAfter: { + type: 'string', + format: 'datetime', + description: + 'Search subjects where the associated record/account was deleted after a given timestamp', + }, + hostingDeletedBefore: { + type: 'string', + format: 'datetime', + description: + 'Search subjects where the associated record/account was deleted before a given timestamp', + }, + hostingUpdatedAfter: { + type: 'string', + format: 'datetime', + description: + 'Search subjects where the associated record/account was updated after a given timestamp', + }, + hostingUpdatedBefore: { + type: 'string', + format: 'datetime', + description: + 'Search subjects where the associated record/account was updated before a given timestamp', + }, + hostingStatuses: { + type: 'array', + items: { + type: 'string', + }, + description: + 'Search subjects by the status of the associated record/account', + }, reviewedBefore: { type: 'string', format: 'datetime', 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 03be0722f6b..627da8f2cc4 100644 --- a/packages/ozone/src/lexicon/types/tools/ozone/moderation/defs.ts +++ b/packages/ozone/src/lexicon/types/tools/ozone/moderation/defs.ts @@ -30,6 +30,9 @@ export interface ModEventView { | ModEventResolveAppeal | ModEventDivert | ModEventTag + | AccountEvent + | IdentityEvent + | RecordEvent | { $type: string; [k: string]: unknown } subject: | ComAtprotoAdminDefs.RepoRef @@ -74,6 +77,9 @@ export interface ModEventViewDetail { | ModEventResolveAppeal | ModEventDivert | ModEventTag + | AccountEvent + | IdentityEvent + | RecordEvent | { $type: string; [k: string]: unknown } subject: | RepoView @@ -105,6 +111,10 @@ export interface SubjectStatusView { | ComAtprotoAdminDefs.RepoRef | ComAtprotoRepoStrongRef.Main | { $type: string; [k: string]: unknown } + hosting?: + | AccountHosting + | RecordHosting + | { $type: string; [k: string]: unknown } subjectBlobCids?: string[] subjectRepoHandle?: string /** Timestamp referencing when the last update was made to the moderation status of the subject */ @@ -472,6 +482,78 @@ export function validateModEventTag(v: unknown): ValidationResult { return lexicons.validate('tools.ozone.moderation.defs#modEventTag', v) } +/** Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */ +export interface AccountEvent { + comment?: string + /** Indicates that the account has a repository which can be fetched from the host that emitted this event. */ + active: boolean + status?: + | 'unknown' + | 'deactivated' + | 'deleted' + | 'takendown' + | 'suspended' + | 'tombstoned' + | (string & {}) + timestamp: string + [k: string]: unknown +} + +export function isAccountEvent(v: unknown): v is AccountEvent { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#accountEvent' + ) +} + +export function validateAccountEvent(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#accountEvent', v) +} + +/** Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */ +export interface IdentityEvent { + comment?: string + handle?: string + pdsHost?: string + tombstone?: boolean + timestamp: string + [k: string]: unknown +} + +export function isIdentityEvent(v: unknown): v is IdentityEvent { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#identityEvent' + ) +} + +export function validateIdentityEvent(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#identityEvent', v) +} + +/** Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */ +export interface RecordEvent { + comment?: string + op: 'create' | 'update' | 'delete' | (string & {}) + cid?: string + timestamp: string + [k: string]: unknown +} + +export function isRecordEvent(v: unknown): v is RecordEvent { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#recordEvent' + ) +} + +export function validateRecordEvent(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#recordEvent', v) +} + export interface RepoView { did: string handle: string @@ -705,3 +787,51 @@ export function isVideoDetails(v: unknown): v is VideoDetails { export function validateVideoDetails(v: unknown): ValidationResult { return lexicons.validate('tools.ozone.moderation.defs#videoDetails', v) } + +export interface AccountHosting { + status: + | 'takendown' + | 'suspended' + | 'deleted' + | 'deactivated' + | 'unknown' + | (string & {}) + updatedAt?: string + createdAt?: string + deletedAt?: string + deactivatedAt?: string + reactivatedAt?: string + [k: string]: unknown +} + +export function isAccountHosting(v: unknown): v is AccountHosting { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#accountHosting' + ) +} + +export function validateAccountHosting(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#accountHosting', v) +} + +export interface RecordHosting { + status: 'deleted' | 'unknown' | (string & {}) + updatedAt?: string + createdAt?: string + deletedAt?: string + [k: string]: unknown +} + +export function isRecordHosting(v: unknown): v is RecordHosting { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#recordHosting' + ) +} + +export function validateRecordHosting(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#recordHosting', v) +} diff --git a/packages/ozone/src/lexicon/types/tools/ozone/moderation/emitEvent.ts b/packages/ozone/src/lexicon/types/tools/ozone/moderation/emitEvent.ts index fc13876ed57..1399cf589ee 100644 --- a/packages/ozone/src/lexicon/types/tools/ozone/moderation/emitEvent.ts +++ b/packages/ozone/src/lexicon/types/tools/ozone/moderation/emitEvent.ts @@ -29,6 +29,9 @@ export interface InputSchema { | ToolsOzoneModerationDefs.ModEventResolveAppeal | ToolsOzoneModerationDefs.ModEventEmail | ToolsOzoneModerationDefs.ModEventTag + | ToolsOzoneModerationDefs.AccountEvent + | ToolsOzoneModerationDefs.IdentityEvent + | ToolsOzoneModerationDefs.RecordEvent | { $type: string; [k: string]: unknown } subject: | ComAtprotoAdminDefs.RepoRef diff --git a/packages/ozone/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts b/packages/ozone/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts index 68632d8ac8e..f1d5bb61690 100644 --- a/packages/ozone/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts +++ b/packages/ozone/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts @@ -22,6 +22,16 @@ export interface QueryParams { reportedBefore?: string /** Search subjects reviewed after a given timestamp */ reviewedAfter?: string + /** Search subjects where the associated record/account was deleted after a given timestamp */ + hostingDeletedAfter?: string + /** Search subjects where the associated record/account was deleted before a given timestamp */ + hostingDeletedBefore?: string + /** Search subjects where the associated record/account was updated after a given timestamp */ + hostingUpdatedAfter?: string + /** Search subjects where the associated record/account was updated before a given timestamp */ + hostingUpdatedBefore?: string + /** Search subjects by the status of the associated record/account */ + hostingStatuses?: string[] /** Search subjects reviewed before a given timestamp */ reviewedBefore?: string /** By default, we don't include muted subjects in the results. Set this to true to include them. */ diff --git a/packages/ozone/src/mod-service/index.ts b/packages/ozone/src/mod-service/index.ts index 087deef08fb..fc24ece0a00 100644 --- a/packages/ozone/src/mod-service/index.ts +++ b/packages/ozone/src/mod-service/index.ts @@ -18,6 +18,9 @@ import { isModEventTakedown, isModEventEmail, isModEventTag, + isAccountEvent, + isIdentityEvent, + isRecordEvent, REVIEWESCALATED, REVIEWOPEN, } from '../lexicon/types/tools/ozone/moderation/defs' @@ -386,6 +389,25 @@ export class ModerationService { } } + if (isAccountEvent(event)) { + meta.active = event.active + meta.timestamp = event.timestamp + if (event.status) meta.status = event.status + } + + if (isIdentityEvent(event)) { + meta.timestamp = event.timestamp + if (event.handle) meta.handle = event.handle + if (event.pdsHost) meta.pdsHost = event.pdsHost + if (event.tombstone) meta.tombstone = event.tombstone + } + + if (isRecordEvent(event)) { + meta.timestamp = event.timestamp + meta.op = event.op + if (event.cid) meta.cid = event.cid + } + if (isModEventTakedown(event) && event.acknowledgeAccountSubjects) { meta.acknowledgeAccountSubjects = true } @@ -758,6 +780,11 @@ export class ModerationService { reportedAfter, reportedBefore, includeMuted, + hostingDeletedBefore, + hostingDeletedAfter, + hostingUpdatedBefore, + hostingUpdatedAfter, + hostingStatuses, onlyMuted, ignoreSubjects, sortDirection, @@ -780,6 +807,11 @@ export class ModerationService { reportedAfter?: string reportedBefore?: string includeMuted?: boolean + hostingDeletedBefore?: string + hostingDeletedAfter?: string + hostingUpdatedBefore?: string + hostingUpdatedAfter?: string + hostingStatuses?: string[] onlyMuted?: boolean subject?: string ignoreSubjects?: string[] @@ -847,6 +879,26 @@ export class ModerationService { builder = builder.where('lastReviewedAt', '<', reviewedBefore) } + if (hostingUpdatedAfter) { + builder = builder.where('hostingUpdatedAt', '>', hostingUpdatedAfter) + } + + if (hostingUpdatedBefore) { + builder = builder.where('hostingUpdatedAt', '<', hostingUpdatedBefore) + } + + if (hostingDeletedAfter) { + builder = builder.where('hostingDeletedAt', '>', hostingDeletedAfter) + } + + if (hostingDeletedBefore) { + builder = builder.where('hostingDeletedAt', '<', hostingDeletedBefore) + } + + if (hostingStatuses?.length) { + builder = builder.where('hostingStatus', 'in', hostingStatuses) + } + if (reportedAfter) { builder = builder.where('lastReviewedAt', '>', reportedAfter) } diff --git a/packages/ozone/src/mod-service/status.ts b/packages/ozone/src/mod-service/status.ts index 939d8d3bea5..cc3539e7728 100644 --- a/packages/ozone/src/mod-service/status.ts +++ b/packages/ozone/src/mod-service/status.ts @@ -126,6 +126,83 @@ const getSubjectStatusForModerationEvent = ({ } } +const hostingEvents = [ + 'tools.ozone.moderation.defs#accountEvent', + 'tools.ozone.moderation.defs#identityEvent', + 'tools.ozone.moderation.defs#recordEvent', +] + +const getSubjectStatusForRecordEvent = ({ + event, + currentStatus, +}: { + event: ModerationEventRow + currentStatus?: ModerationSubjectStatusRow +}): Partial => { + const timestamp = + typeof event.meta?.timestamp === 'string' + ? event.meta?.timestamp + : event.createdAt + + if (event.action === 'tools.ozone.moderation.defs#recordEvent') { + if (event.meta?.op === 'delete') { + return { + hostingStatus: 'deleted', + hostingDeletedAt: timestamp, + } + } else if (event.meta?.op === 'update') { + return { + hostingStatus: 'active', + hostingUpdatedAt: timestamp, + } + } + return {} + } + + if (event.action === 'tools.ozone.moderation.defs#accountEvent') { + const status: Partial = { + hostingUpdatedAt: timestamp, + } + + if (event.meta?.status) { + status.hostingStatus = `${event.meta?.status}` + } + + if (event.meta?.status === 'deleted') { + status.hostingDeletedAt = timestamp + } else if (event.meta?.status === 'deactivated') { + status.hostingDeactivatedAt = timestamp + } else { + // When deactivated accounts are re-activated, we receive the event with just the active flag set to true + // so we want to make sure that the hostingStatus is not set to an outdated value + if ( + currentStatus?.hostingStatus === 'deactivated' && + event.meta?.active + ) { + status.hostingStatus = 'active' + status.hostingReactivatedAt = timestamp + } + } + + return status + } + + if (event.action === 'tools.ozone.moderation.defs#identityEvent') { + const status: Partial = { + hostingUpdatedAt: timestamp, + } + + if (event.meta?.tombstone) { + status.hostingStatus = 'tombstoned' + status.hostingDeletedAt = timestamp + } + + return status + } + + return {} +} + // Based on a given moderation action event, this function will update the moderation status of the subject // If there's no existing status, it will create one // If the action event does not affect the status, it will do nothing @@ -133,7 +210,7 @@ export const adjustModerationSubjectStatus = async ( db: Database, moderationEvent: ModerationEventRow, blobCids?: string[], -) => { +): Promise => { const { action, subjectDid, @@ -152,6 +229,7 @@ export const adjustModerationSubjectStatus = async ( db.assertTransaction() + const now = new Date().toISOString() const currentStatus = await db.db .selectFrom('moderation_subject_status') .where('did', '=', identifier.did) @@ -161,6 +239,41 @@ export const adjustModerationSubjectStatus = async ( .selectAll() .executeTakeFirst() + if (hostingEvents.includes(action)) { + const newStatus = getSubjectStatusForRecordEvent({ + event: moderationEvent, + currentStatus, + }) + if (!Object.keys(newStatus).length) { + return currentStatus || null + } + + const status = await db.db + .insertInto('moderation_subject_status') + .values({ + ...identifier, + ...newStatus, + // newStatus doesn't contain a reviewState or takendown so in case this is a new entry + // we need to set a default values so that the insert doesn't fail + reviewState: currentStatus ? currentStatus.reviewState : REVIEWNONE, + // @TODO: should we try to update this based on status property of account event? + // For now we're the only one emitting takedowns so i don't think it makes too much of a difference + takendown: currentStatus ? currentStatus.takendown : false, + createdAt: now, + updatedAt: now, + }) + .onConflict((oc) => + oc.constraint('moderation_status_unique_idx').doUpdateSet({ + ...newStatus, + updatedAt: now, + }), + ) + .returningAll() + .executeTakeFirst() + + return status || null + } + // If reporting is muted for this reporter, we don't want to update the subject status if (meta?.isReporterMuted) { return currentStatus || null @@ -178,7 +291,6 @@ export const adjustModerationSubjectStatus = async ( durationInHours: moderationEvent.durationInHours, }) - const now = new Date().toISOString() if ( currentStatus?.reviewState === REVIEWESCALATED && subjectStatus.reviewState !== REVIEWCLOSED diff --git a/packages/ozone/src/mod-service/types.ts b/packages/ozone/src/mod-service/types.ts index 926f39f902a..29a63d3e019 100644 --- a/packages/ozone/src/mod-service/types.ts +++ b/packages/ozone/src/mod-service/types.ts @@ -31,3 +31,28 @@ export type ModEventType = | ToolsOzoneModerationDefs.ModEventMute | ToolsOzoneModerationDefs.ModEventReverseTakedown | ToolsOzoneModerationDefs.ModEventTag + | ToolsOzoneModerationDefs.AccountEvent + | ToolsOzoneModerationDefs.IdentityEvent + | ToolsOzoneModerationDefs.RecordEvent + +type AccountHostingView = { + $type: 'tools.ozone.moderation.defs#accountHosting' + status: 'active' | 'takendown' | 'suspended' | 'deleted' | 'deactivated' + createdAt?: Date + updatedAt?: Date + deletedAt?: Date + deactivatedAt?: Date + reactivatedAt?: Date +} + +type RecordHostingView = { + $type: 'tools.ozone.moderation.defs#recordHosting' + status: 'active' | 'deleted' + createdAt?: Date + updatedAt?: Date + deletedAt?: Date +} + +export type ModerationSubjectHostingView = + | AccountHostingView + | RecordHostingView diff --git a/packages/ozone/src/mod-service/views.ts b/packages/ozone/src/mod-service/views.ts index a17da7b3498..8b3a5968f74 100644 --- a/packages/ozone/src/mod-service/views.ts +++ b/packages/ozone/src/mod-service/views.ts @@ -1,6 +1,10 @@ import { sql } from 'kysely' import { AtUri, INVALID_HANDLE, normalizeDatetimeAlways } from '@atproto/syntax' -import { AtpAgent, AppBskyFeedDefs } from '@atproto/api' +import { + AtpAgent, + AppBskyFeedDefs, + ToolsOzoneModerationDefs, +} from '@atproto/api' import { dedupeStrs } from '@atproto/common' import { BlobRef } from '@atproto/lexicon' import { Keypair } from '@atproto/crypto' @@ -195,6 +199,25 @@ export class ModerationViews { eventView.event.remove = event.removedTags || [] } + if (event.action === 'tools.ozone.moderation.defs#accountEvent') { + eventView.event.active = !!event.meta?.active + eventView.event.timestamp = event.meta?.timestamp + eventView.event.status = event.meta?.status + } + + if (event.action === 'tools.ozone.moderation.defs#identityEvent') { + eventView.event.timestamp = event.meta?.timestamp + eventView.event.handle = event.meta?.handle + eventView.event.pdsHost = event.meta?.pdsHost + eventView.event.tombstone = !!event.meta?.tombstone + } + + if (event.action === 'tools.ozone.moderation.defs#recordEvent') { + eventView.event.op = event.meta?.op + eventView.event.cid = event.meta?.cid + eventView.event.timestamp = event.meta?.timestamp + } + return eventView } @@ -574,7 +597,7 @@ export class ModerationViews { formatSubjectStatus( status: ModerationSubjectStatusRowWithHandle, ): SubjectStatusView { - return { + const statusView: SubjectStatusView = { id: status.id, reviewState: status.reviewState, createdAt: status.createdAt, @@ -594,6 +617,26 @@ export class ModerationViews { tags: status.tags || [], subject: subjectFromStatusRow(status).lex(), } + + if (status.recordPath !== '') { + statusView.hosting = { + $type: 'tools.ozone.moderation.defs#recordHosting', + updatedAt: status.hostingUpdatedAt ?? undefined, + deletedAt: status.hostingDeletedAt ?? undefined, + status: status.hostingStatus ?? 'unknown', + } + } else { + statusView.hosting = { + $type: 'tools.ozone.moderation.defs#accountHosting', + updatedAt: status.hostingUpdatedAt ?? undefined, + deletedAt: status.hostingDeletedAt ?? undefined, + status: status.hostingStatus ?? 'unknown', + deactivatedAt: status.hostingDeactivatedAt ?? undefined, + reactivatedAt: status.hostingReactivatedAt ?? undefined, + } + } + + return statusView } async fetchAuthorFeed( diff --git a/packages/ozone/tests/__snapshots__/get-record.test.ts.snap b/packages/ozone/tests/__snapshots__/get-record.test.ts.snap index c6dac50ee9f..b577606360f 100644 --- a/packages/ozone/tests/__snapshots__/get-record.test.ts.snap +++ b/packages/ozone/tests/__snapshots__/get-record.test.ts.snap @@ -29,6 +29,10 @@ Object { "moderation": Object { "subjectStatus": Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#recordHosting", + "status": "unknown", + }, "id": 1, "lastReportedAt": "1970-01-01T00:00:00.000Z", "lastReviewedAt": "1970-01-01T00:00:00.000Z", @@ -129,6 +133,10 @@ Object { "moderation": Object { "subjectStatus": Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#recordHosting", + "status": "unknown", + }, "id": 1, "lastReportedAt": "1970-01-01T00:00:00.000Z", "lastReviewedAt": "1970-01-01T00:00:00.000Z", diff --git a/packages/ozone/tests/__snapshots__/get-records.test.ts.snap b/packages/ozone/tests/__snapshots__/get-records.test.ts.snap index a65bec04b84..e2ff81c28e0 100644 --- a/packages/ozone/tests/__snapshots__/get-records.test.ts.snap +++ b/packages/ozone/tests/__snapshots__/get-records.test.ts.snap @@ -32,6 +32,10 @@ Object { "moderation": Object { "subjectStatus": Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#recordHosting", + "status": "unknown", + }, "id": 1, "lastReportedAt": "1970-01-01T00:00:00.000Z", "lastReviewedAt": "1970-01-01T00:00:00.000Z", diff --git a/packages/ozone/tests/__snapshots__/get-repo.test.ts.snap b/packages/ozone/tests/__snapshots__/get-repo.test.ts.snap index e2e4dc28a17..9c3aada6ca8 100644 --- a/packages/ozone/tests/__snapshots__/get-repo.test.ts.snap +++ b/packages/ozone/tests/__snapshots__/get-repo.test.ts.snap @@ -23,6 +23,10 @@ Object { "moderation": Object { "subjectStatus": Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#accountHosting", + "status": "unknown", + }, "id": 1, "lastReportedAt": "1970-01-01T00:00:00.000Z", "lastReviewedAt": "1970-01-01T00:00:00.000Z", diff --git a/packages/ozone/tests/__snapshots__/get-repos.test.ts.snap b/packages/ozone/tests/__snapshots__/get-repos.test.ts.snap index 08dff5fdfe3..f1a4cf4b14f 100644 --- a/packages/ozone/tests/__snapshots__/get-repos.test.ts.snap +++ b/packages/ozone/tests/__snapshots__/get-repos.test.ts.snap @@ -26,6 +26,10 @@ Object { "moderation": Object { "subjectStatus": Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#accountHosting", + "status": "unknown", + }, "id": 1, "lastReportedAt": "1970-01-01T00:00:00.000Z", "lastReviewedAt": "1970-01-01T00:00:00.000Z", diff --git a/packages/ozone/tests/__snapshots__/moderation-events.test.ts.snap b/packages/ozone/tests/__snapshots__/moderation-events.test.ts.snap index 59c0e79d046..17dec972658 100644 --- a/packages/ozone/tests/__snapshots__/moderation-events.test.ts.snap +++ b/packages/ozone/tests/__snapshots__/moderation-events.test.ts.snap @@ -19,6 +19,10 @@ Object { "moderation": Object { "subjectStatus": Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#accountHosting", + "status": "unknown", + }, "id": 1, "lastReportedAt": "1970-01-01T00:00:00.000Z", "lastReviewedAt": "1970-01-01T00:00:00.000Z", diff --git a/packages/ozone/tests/__snapshots__/moderation-statuses.test.ts.snap b/packages/ozone/tests/__snapshots__/moderation-statuses.test.ts.snap index 96c288ffa09..330a3f7a45f 100644 --- a/packages/ozone/tests/__snapshots__/moderation-statuses.test.ts.snap +++ b/packages/ozone/tests/__snapshots__/moderation-statuses.test.ts.snap @@ -4,6 +4,10 @@ exports[`moderation-statuses query statuses returns statuses filtered by subject Array [ Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#recordHosting", + "status": "unknown", + }, "id": 7, "lastReportedAt": "1970-01-01T00:00:00.000Z", "reviewState": "tools.ozone.moderation.defs#reviewOpen", @@ -23,6 +27,10 @@ Array [ }, Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#accountHosting", + "status": "unknown", + }, "id": 5, "lastReportedAt": "1970-01-01T00:00:00.000Z", "reviewState": "tools.ozone.moderation.defs#reviewOpen", @@ -46,6 +54,10 @@ exports[`moderation-statuses query statuses returns statuses for subjects that r Array [ Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#recordHosting", + "status": "unknown", + }, "id": 7, "lastReportedAt": "1970-01-01T00:00:00.000Z", "reviewState": "tools.ozone.moderation.defs#reviewOpen", @@ -65,6 +77,10 @@ Array [ }, Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#accountHosting", + "status": "unknown", + }, "id": 5, "lastReportedAt": "1970-01-01T00:00:00.000Z", "reviewState": "tools.ozone.moderation.defs#reviewOpen", @@ -83,6 +99,10 @@ Array [ }, Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#recordHosting", + "status": "unknown", + }, "id": 3, "lastReportedAt": "1970-01-01T00:00:00.000Z", "reviewState": "tools.ozone.moderation.defs#reviewOpen", @@ -101,6 +121,10 @@ Array [ }, Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#accountHosting", + "status": "unknown", + }, "id": 1, "lastReportedAt": "1970-01-01T00:00:00.000Z", "reviewState": "tools.ozone.moderation.defs#reviewOpen", diff --git a/packages/ozone/tests/record-and-account-events.test.ts b/packages/ozone/tests/record-and-account-events.test.ts new file mode 100644 index 00000000000..1c8d8b5d224 --- /dev/null +++ b/packages/ozone/tests/record-and-account-events.test.ts @@ -0,0 +1,185 @@ +import { + TestNetwork, + SeedClient, + basicSeed, + ModeratorClient, +} from '@atproto/dev-env' +import { + ComAtprotoModerationDefs, + ToolsOzoneModerationDefs, +} from '@atproto/api' +import { REVIEWOPEN } from '../src/lexicon/types/tools/ozone/moderation/defs' +import { ToolsOzoneModerationEmitEvent as EmitModerationEvent } from '@atproto/api' +describe('record and account events on moderation subjects', () => { + let network: TestNetwork + let sc: SeedClient + let modClient: ModeratorClient + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'ozone_record_and_account_events', + }) + sc = network.getSeedClient() + modClient = network.ozone.getModClient() + await basicSeed(sc) + await network.processAll() + }) + + afterAll(async () => { + await network.close() + }) + + const getSubjectStatus = async ( + subject: string, + ): Promise => { + const res = await modClient.queryStatuses({ + subject, + }) + return res.subjectStatuses[0] + } + + describe('record events', () => { + const emitRecordEvent = async ( + subject: EmitModerationEvent.InputSchema['subject'], + op: 'create' | 'update' | 'delete', + ) => { + return await modClient.emitEvent( + { + event: { + op, + timestamp: new Date().toISOString(), + $type: 'tools.ozone.moderation.defs#recordEvent', + }, + subject, + createdBy: 'did:example:admin', + }, + 'admin', + ) + } + + it('saves updated and deleted timestamps and record status', async () => { + const bobsPostSubject = { + $type: 'com.atproto.repo.strongRef', + uri: sc.posts[sc.dids.bob][1].ref.uriStr, + cid: sc.posts[sc.dids.bob][1].ref.cidStr, + } + + await sc.createReport({ + reportedBy: sc.dids.carol, + reasonType: ComAtprotoModerationDefs.REASONMISLEADING, + reason: 'misleading', + subject: bobsPostSubject, + }) + + await emitRecordEvent(bobsPostSubject, 'update') + const statusAfterUpdate = await getSubjectStatus(bobsPostSubject.uri) + expect(statusAfterUpdate?.hosting?.updatedAt).toBeTruthy() + + await emitRecordEvent(bobsPostSubject, 'delete') + const statusAfterDelete = await getSubjectStatus(bobsPostSubject.uri) + expect(statusAfterDelete?.hosting?.deletedAt).toBeTruthy() + expect(statusAfterDelete?.hosting?.status).toEqual('deleted') + // Ensure that due to delete or update event, review state does not change + expect(statusAfterDelete?.reviewState).toEqual(REVIEWOPEN) + }) + }) + describe('account/identity events', () => { + const emitAccountEvent = async ( + subject: EmitModerationEvent.InputSchema['subject'], + active: boolean, + status?: 'takendown' | 'deleted' | 'deactivated' | 'suspended', + ) => { + return await modClient.emitEvent( + { + event: { + status, + active, + timestamp: new Date().toISOString(), + $type: 'tools.ozone.moderation.defs#accountEvent', + }, + subject, + createdBy: 'did:example:admin', + }, + 'admin', + ) + } + + it('saves updated and deleted timestamps and account status', async () => { + const carolsAccountSubject = { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.carol, + } + + await sc.createReport({ + reportedBy: sc.dids.carol, + reasonType: ComAtprotoModerationDefs.REASONMISLEADING, + reason: 'misleading', + subject: carolsAccountSubject, + }) + + await emitAccountEvent(carolsAccountSubject, false, 'deactivated') + const statusAfterDeactivation = await getSubjectStatus( + carolsAccountSubject.did, + ) + expect(statusAfterDeactivation?.hosting?.deactivatedAt).toBeTruthy() + expect(statusAfterDeactivation?.hosting?.status).toEqual('deactivated') + expect(statusAfterDeactivation?.reviewState).toEqual(REVIEWOPEN) + + await emitAccountEvent(carolsAccountSubject, true) + const statusAfterReactivation = await getSubjectStatus( + carolsAccountSubject.did, + ) + expect(statusAfterReactivation?.hosting?.updatedAt).toBeTruthy() + expect(statusAfterReactivation?.hosting?.status).toEqual('active') + expect(statusAfterReactivation?.hosting?.deletedAt).toBeFalsy() + }) + + it('gets statuses by hosting properties', async () => { + await Promise.all([ + emitAccountEvent( + { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.carol, + }, + false, + 'deactivated', + ), + emitAccountEvent( + { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + false, + 'deleted', + ), + ]) + const [ + { subjectStatuses: deactivatedOrDeletedStatuses }, + { subjectStatuses: deletedStatusesInPastDay }, + { subjectStatuses: deletedStatusesBeforeYesterday }, + ] = await Promise.all([ + modClient.queryStatuses({ + hostingStatuses: ['deactivated', 'deleted'], + }), + modClient.queryStatuses({ + hostingDeletedAfter: new Date( + Date.now() - 1000 * 60 * 60 * 24, + ).toISOString(), + }), + modClient.queryStatuses({ + hostingDeletedBefore: new Date( + Date.now() - 1000 * 60 * 60 * 24, + ).toISOString(), + }), + ]) + + expect(deactivatedOrDeletedStatuses.length).toEqual(3) + expect(deletedStatusesInPastDay.length).toEqual(2) + expect(deletedStatusesInPastDay[0]?.subject.uri).toEqual( + sc.posts[sc.dids.bob][1].ref.uriStr, + ) + expect(deletedStatusesInPastDay[1]?.subject.did).toEqual(sc.dids.bob) + expect(deletedStatusesBeforeYesterday.length).toEqual(0) + }) + }) +}) diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 3d6ae795019..23376822cc4 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -10821,6 +10821,9 @@ export const schemaDict = { 'lex:tools.ozone.moderation.defs#modEventResolveAppeal', 'lex:tools.ozone.moderation.defs#modEventDivert', 'lex:tools.ozone.moderation.defs#modEventTag', + 'lex:tools.ozone.moderation.defs#accountEvent', + 'lex:tools.ozone.moderation.defs#identityEvent', + 'lex:tools.ozone.moderation.defs#recordEvent', ], }, subject: { @@ -10885,6 +10888,9 @@ export const schemaDict = { 'lex:tools.ozone.moderation.defs#modEventResolveAppeal', 'lex:tools.ozone.moderation.defs#modEventDivert', 'lex:tools.ozone.moderation.defs#modEventTag', + 'lex:tools.ozone.moderation.defs#accountEvent', + 'lex:tools.ozone.moderation.defs#identityEvent', + 'lex:tools.ozone.moderation.defs#recordEvent', ], }, subject: { @@ -10927,6 +10933,13 @@ export const schemaDict = { 'lex:com.atproto.repo.strongRef', ], }, + hosting: { + type: 'union', + refs: [ + 'lex:tools.ozone.moderation.defs#accountHosting', + 'lex:tools.ozone.moderation.defs#recordHosting', + ], + }, subjectBlobCids: { type: 'array', items: { @@ -11246,6 +11259,86 @@ export const schemaDict = { }, }, }, + accountEvent: { + type: 'object', + description: + 'Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.', + required: ['timestamp', 'active'], + properties: { + comment: { + type: 'string', + }, + active: { + type: 'boolean', + description: + 'Indicates that the account has a repository which can be fetched from the host that emitted this event.', + }, + status: { + type: 'string', + knownValues: [ + 'unknown', + 'deactivated', + 'deleted', + 'takendown', + 'suspended', + 'tombstoned', + ], + }, + timestamp: { + type: 'string', + format: 'datetime', + }, + }, + }, + identityEvent: { + type: 'object', + description: + 'Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.', + required: ['timestamp'], + properties: { + comment: { + type: 'string', + }, + handle: { + type: 'string', + format: 'handle', + }, + pdsHost: { + type: 'string', + format: 'uri', + }, + tombstone: { + type: 'boolean', + }, + timestamp: { + type: 'string', + format: 'datetime', + }, + }, + }, + recordEvent: { + type: 'object', + description: + 'Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking.', + required: ['timestamp', 'op'], + properties: { + comment: { + type: 'string', + }, + op: { + type: 'string', + knownValues: ['create', 'update', 'delete'], + }, + cid: { + type: 'string', + format: 'cid', + }, + timestamp: { + type: 'string', + format: 'datetime', + }, + }, + }, repoView: { type: 'object', required: [ @@ -11571,6 +11664,64 @@ export const schemaDict = { }, }, }, + accountHosting: { + type: 'object', + required: ['status'], + properties: { + status: { + type: 'string', + knownValues: [ + 'takendown', + 'suspended', + 'deleted', + 'deactivated', + 'unknown', + ], + }, + updatedAt: { + type: 'string', + format: 'datetime', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + deletedAt: { + type: 'string', + format: 'datetime', + }, + deactivatedAt: { + type: 'string', + format: 'datetime', + }, + reactivatedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, + recordHosting: { + type: 'object', + required: ['status'], + properties: { + status: { + type: 'string', + knownValues: ['deleted', 'unknown'], + }, + updatedAt: { + type: 'string', + format: 'datetime', + }, + createdAt: { + type: 'string', + format: 'datetime', + }, + deletedAt: { + type: 'string', + format: 'datetime', + }, + }, + }, }, }, ToolsOzoneModerationEmitEvent: { @@ -11603,6 +11754,9 @@ export const schemaDict = { 'lex:tools.ozone.moderation.defs#modEventResolveAppeal', 'lex:tools.ozone.moderation.defs#modEventEmail', 'lex:tools.ozone.moderation.defs#modEventTag', + 'lex:tools.ozone.moderation.defs#accountEvent', + 'lex:tools.ozone.moderation.defs#identityEvent', + 'lex:tools.ozone.moderation.defs#recordEvent', ], }, subject: { @@ -12005,6 +12159,38 @@ export const schemaDict = { format: 'datetime', description: 'Search subjects reviewed after a given timestamp', }, + hostingDeletedAfter: { + type: 'string', + format: 'datetime', + description: + 'Search subjects where the associated record/account was deleted after a given timestamp', + }, + hostingDeletedBefore: { + type: 'string', + format: 'datetime', + description: + 'Search subjects where the associated record/account was deleted before a given timestamp', + }, + hostingUpdatedAfter: { + type: 'string', + format: 'datetime', + description: + 'Search subjects where the associated record/account was updated after a given timestamp', + }, + hostingUpdatedBefore: { + type: 'string', + format: 'datetime', + description: + 'Search subjects where the associated record/account was updated before a given timestamp', + }, + hostingStatuses: { + type: 'array', + items: { + type: 'string', + }, + description: + 'Search subjects by the status of the associated record/account', + }, reviewedBefore: { type: 'string', format: 'datetime', 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 03be0722f6b..627da8f2cc4 100644 --- a/packages/pds/src/lexicon/types/tools/ozone/moderation/defs.ts +++ b/packages/pds/src/lexicon/types/tools/ozone/moderation/defs.ts @@ -30,6 +30,9 @@ export interface ModEventView { | ModEventResolveAppeal | ModEventDivert | ModEventTag + | AccountEvent + | IdentityEvent + | RecordEvent | { $type: string; [k: string]: unknown } subject: | ComAtprotoAdminDefs.RepoRef @@ -74,6 +77,9 @@ export interface ModEventViewDetail { | ModEventResolveAppeal | ModEventDivert | ModEventTag + | AccountEvent + | IdentityEvent + | RecordEvent | { $type: string; [k: string]: unknown } subject: | RepoView @@ -105,6 +111,10 @@ export interface SubjectStatusView { | ComAtprotoAdminDefs.RepoRef | ComAtprotoRepoStrongRef.Main | { $type: string; [k: string]: unknown } + hosting?: + | AccountHosting + | RecordHosting + | { $type: string; [k: string]: unknown } subjectBlobCids?: string[] subjectRepoHandle?: string /** Timestamp referencing when the last update was made to the moderation status of the subject */ @@ -472,6 +482,78 @@ export function validateModEventTag(v: unknown): ValidationResult { return lexicons.validate('tools.ozone.moderation.defs#modEventTag', v) } +/** Logs account status related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */ +export interface AccountEvent { + comment?: string + /** Indicates that the account has a repository which can be fetched from the host that emitted this event. */ + active: boolean + status?: + | 'unknown' + | 'deactivated' + | 'deleted' + | 'takendown' + | 'suspended' + | 'tombstoned' + | (string & {}) + timestamp: string + [k: string]: unknown +} + +export function isAccountEvent(v: unknown): v is AccountEvent { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#accountEvent' + ) +} + +export function validateAccountEvent(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#accountEvent', v) +} + +/** Logs identity related events on a repo subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */ +export interface IdentityEvent { + comment?: string + handle?: string + pdsHost?: string + tombstone?: boolean + timestamp: string + [k: string]: unknown +} + +export function isIdentityEvent(v: unknown): v is IdentityEvent { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#identityEvent' + ) +} + +export function validateIdentityEvent(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#identityEvent', v) +} + +/** Logs lifecycle event on a record subject. Normally captured by automod from the firehose and emitted to ozone for historical tracking. */ +export interface RecordEvent { + comment?: string + op: 'create' | 'update' | 'delete' | (string & {}) + cid?: string + timestamp: string + [k: string]: unknown +} + +export function isRecordEvent(v: unknown): v is RecordEvent { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#recordEvent' + ) +} + +export function validateRecordEvent(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#recordEvent', v) +} + export interface RepoView { did: string handle: string @@ -705,3 +787,51 @@ export function isVideoDetails(v: unknown): v is VideoDetails { export function validateVideoDetails(v: unknown): ValidationResult { return lexicons.validate('tools.ozone.moderation.defs#videoDetails', v) } + +export interface AccountHosting { + status: + | 'takendown' + | 'suspended' + | 'deleted' + | 'deactivated' + | 'unknown' + | (string & {}) + updatedAt?: string + createdAt?: string + deletedAt?: string + deactivatedAt?: string + reactivatedAt?: string + [k: string]: unknown +} + +export function isAccountHosting(v: unknown): v is AccountHosting { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#accountHosting' + ) +} + +export function validateAccountHosting(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#accountHosting', v) +} + +export interface RecordHosting { + status: 'deleted' | 'unknown' | (string & {}) + updatedAt?: string + createdAt?: string + deletedAt?: string + [k: string]: unknown +} + +export function isRecordHosting(v: unknown): v is RecordHosting { + return ( + isObj(v) && + hasProp(v, '$type') && + v.$type === 'tools.ozone.moderation.defs#recordHosting' + ) +} + +export function validateRecordHosting(v: unknown): ValidationResult { + return lexicons.validate('tools.ozone.moderation.defs#recordHosting', v) +} diff --git a/packages/pds/src/lexicon/types/tools/ozone/moderation/emitEvent.ts b/packages/pds/src/lexicon/types/tools/ozone/moderation/emitEvent.ts index fc13876ed57..1399cf589ee 100644 --- a/packages/pds/src/lexicon/types/tools/ozone/moderation/emitEvent.ts +++ b/packages/pds/src/lexicon/types/tools/ozone/moderation/emitEvent.ts @@ -29,6 +29,9 @@ export interface InputSchema { | ToolsOzoneModerationDefs.ModEventResolveAppeal | ToolsOzoneModerationDefs.ModEventEmail | ToolsOzoneModerationDefs.ModEventTag + | ToolsOzoneModerationDefs.AccountEvent + | ToolsOzoneModerationDefs.IdentityEvent + | ToolsOzoneModerationDefs.RecordEvent | { $type: string; [k: string]: unknown } subject: | ComAtprotoAdminDefs.RepoRef diff --git a/packages/pds/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts b/packages/pds/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts index 68632d8ac8e..f1d5bb61690 100644 --- a/packages/pds/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts +++ b/packages/pds/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts @@ -22,6 +22,16 @@ export interface QueryParams { reportedBefore?: string /** Search subjects reviewed after a given timestamp */ reviewedAfter?: string + /** Search subjects where the associated record/account was deleted after a given timestamp */ + hostingDeletedAfter?: string + /** Search subjects where the associated record/account was deleted before a given timestamp */ + hostingDeletedBefore?: string + /** Search subjects where the associated record/account was updated after a given timestamp */ + hostingUpdatedAfter?: string + /** Search subjects where the associated record/account was updated before a given timestamp */ + hostingUpdatedBefore?: string + /** Search subjects by the status of the associated record/account */ + hostingStatuses?: string[] /** Search subjects reviewed before a given timestamp */ reviewedBefore?: string /** By default, we don't include muted subjects in the results. Set this to true to include them. */ diff --git a/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap index 20a2bc89c73..55dd7be8a98 100644 --- a/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap @@ -122,6 +122,10 @@ Object { "moderation": Object { "subjectStatus": Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#accountHosting", + "status": "unknown", + }, "id": 1, "lastReportedAt": "1970-01-01T00:00:00.000Z", "lastReviewedAt": "1970-01-01T00:00:00.000Z", @@ -214,6 +218,10 @@ Object { "moderation": Object { "subjectStatus": Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#recordHosting", + "status": "unknown", + }, "id": 4, "lastReviewedAt": "1970-01-01T00:00:00.000Z", "lastReviewedBy": "user(1)", @@ -271,6 +279,10 @@ Object { "moderation": Object { "subjectStatus": Object { "createdAt": "1970-01-01T00:00:00.000Z", + "hosting": Object { + "$type": "tools.ozone.moderation.defs#accountHosting", + "status": "unknown", + }, "id": 1, "lastReportedAt": "1970-01-01T00:00:00.000Z", "lastReviewedAt": "1970-01-01T00:00:00.000Z",