diff --git a/lexicons/com/atproto/admin/getModerationReports.json b/lexicons/com/atproto/admin/getModerationReports.json index a7f7bf83c4a..ad930389147 100644 --- a/lexicons/com/atproto/admin/getModerationReports.json +++ b/lexicons/com/atproto/admin/getModerationReports.json @@ -10,6 +10,11 @@ "properties": { "subject": { "type": "string" }, "ignoreSubjects": { "type": "array", "items": { "type": "string" } }, + "actionedBy": { + "type": "string", + "format": "did", + "description": "Get all reports that were actioned by a specific moderator" + }, "reporters": { "type": "array", "items": { "type": "string" }, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 68186fdb49e..3039bef31f8 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -865,6 +865,12 @@ export const schemaDict = { type: 'string', }, }, + actionedBy: { + type: 'string', + format: 'did', + description: + 'Get all reports that were actioned by a specific moderator', + }, reporters: { type: 'array', items: { @@ -6327,7 +6333,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'A skeleton of a timeline', + description: 'A skeleton of a timeline - UNSPECCED & WILL GO AWAY SOON', parameters: { type: 'params', properties: { diff --git a/packages/api/src/client/types/com/atproto/admin/getModerationReports.ts b/packages/api/src/client/types/com/atproto/admin/getModerationReports.ts index 6897a374e38..cc6c6f00f3c 100644 --- a/packages/api/src/client/types/com/atproto/admin/getModerationReports.ts +++ b/packages/api/src/client/types/com/atproto/admin/getModerationReports.ts @@ -11,6 +11,8 @@ import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { subject?: string ignoreSubjects?: string[] + /** Get all reports that were actioned by a specific moderator */ + actionedBy?: string /** Filter reports made by one or more DIDs */ reporters?: string[] resolved?: boolean diff --git a/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts b/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts index 5eb74b7e022..85c53f78758 100644 --- a/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts +++ b/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts @@ -15,6 +15,7 @@ export default function (server: Server, ctx: AppContext) { ignoreSubjects, reverse = false, reporters = [], + actionedBy, } = params const moderationService = services.moderation(db) const results = await moderationService.getReports({ @@ -26,6 +27,7 @@ export default function (server: Server, ctx: AppContext) { ignoreSubjects, reverse, reporters, + actionedBy, }) return { encoding: 'application/json', diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 68186fdb49e..3039bef31f8 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -865,6 +865,12 @@ export const schemaDict = { type: 'string', }, }, + actionedBy: { + type: 'string', + format: 'did', + description: + 'Get all reports that were actioned by a specific moderator', + }, reporters: { type: 'array', items: { @@ -6327,7 +6333,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'A skeleton of a timeline', + description: 'A skeleton of a timeline - UNSPECCED & WILL GO AWAY SOON', parameters: { type: 'params', properties: { diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReports.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReports.ts index 93ec8bc879d..f3372b96cc8 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReports.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReports.ts @@ -12,6 +12,8 @@ import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { subject?: string ignoreSubjects?: string[] + /** Get all reports that were actioned by a specific moderator */ + actionedBy?: string /** Filter reports made by one or more DIDs */ reporters?: string[] resolved?: boolean diff --git a/packages/bsky/src/services/moderation/index.ts b/packages/bsky/src/services/moderation/index.ts index 99715866d2a..4958b9ec8c5 100644 --- a/packages/bsky/src/services/moderation/index.ts +++ b/packages/bsky/src/services/moderation/index.ts @@ -84,6 +84,7 @@ export class ModerationService { ignoreSubjects?: string[] reverse?: boolean reporters?: string[] + actionedBy?: string }): Promise { const { subject, @@ -94,6 +95,7 @@ export class ModerationService { ignoreSubjects, reverse = false, reporters, + actionedBy, } = opts const { ref } = this.db.db.dynamic let builder = this.db.db.selectFrom('moderation_report') @@ -148,8 +150,8 @@ export class ModerationService { ? builder.whereExists(resolutionsQuery) : builder.whereNotExists(resolutionsQuery) } - if (actionType !== undefined) { - const resolutionActionsQuery = this.db.db + if (actionType !== undefined || actionedBy !== undefined) { + let resolutionActionsQuery = this.db.db .selectFrom('moderation_report_resolution') .innerJoin( 'moderation_action', @@ -161,10 +163,22 @@ export class ModerationService { '=', ref('moderation_report.id'), ) - .where('moderation_action.action', '=', sql`${actionType}`) - .where('moderation_action.reversedAt', 'is', null) - .selectAll() - builder = builder.whereExists(resolutionActionsQuery) + + if (actionType) { + resolutionActionsQuery = resolutionActionsQuery + .where('moderation_action.action', '=', sql`${actionType}`) + .where('moderation_action.reversedAt', 'is', null) + } + + if (actionedBy) { + resolutionActionsQuery = resolutionActionsQuery.where( + 'moderation_action.createdBy', + '=', + actionedBy, + ) + } + + builder = builder.whereExists(resolutionActionsQuery.selectAll()) } if (cursor) { const cursorNumeric = parseInt(cursor, 10) diff --git a/packages/pds/src/api/com/atproto/admin/getModerationReports.ts b/packages/pds/src/api/com/atproto/admin/getModerationReports.ts index 171777088c6..ce7cc936e83 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationReports.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationReports.ts @@ -15,6 +15,7 @@ export default function (server: Server, ctx: AppContext) { ignoreSubjects = [], reverse = false, reporters = [], + actionedBy, } = params const moderationService = services.moderation(db) const results = await moderationService.getReports({ @@ -26,6 +27,7 @@ export default function (server: Server, ctx: AppContext) { ignoreSubjects, reverse, reporters, + actionedBy, }) return { encoding: 'application/json', diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 68186fdb49e..3039bef31f8 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -865,6 +865,12 @@ export const schemaDict = { type: 'string', }, }, + actionedBy: { + type: 'string', + format: 'did', + description: + 'Get all reports that were actioned by a specific moderator', + }, reporters: { type: 'array', items: { @@ -6327,7 +6333,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'A skeleton of a timeline', + description: 'A skeleton of a timeline - UNSPECCED & WILL GO AWAY SOON', parameters: { type: 'params', properties: { diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReports.ts b/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReports.ts index 93ec8bc879d..f3372b96cc8 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReports.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReports.ts @@ -12,6 +12,8 @@ import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { subject?: string ignoreSubjects?: string[] + /** Get all reports that were actioned by a specific moderator */ + actionedBy?: string /** Filter reports made by one or more DIDs */ reporters?: string[] resolved?: boolean diff --git a/packages/pds/src/services/moderation/index.ts b/packages/pds/src/services/moderation/index.ts index 18e3ceb5608..97fa178d878 100644 --- a/packages/pds/src/services/moderation/index.ts +++ b/packages/pds/src/services/moderation/index.ts @@ -102,6 +102,7 @@ export class ModerationService { ignoreSubjects?: string[] reverse?: boolean reporters?: string[] + actionedBy?: string }): Promise { const { subject, @@ -112,6 +113,7 @@ export class ModerationService { ignoreSubjects, reverse = false, reporters, + actionedBy, } = opts const { ref } = this.db.db.dynamic let builder = this.db.db.selectFrom('moderation_report') @@ -166,8 +168,8 @@ export class ModerationService { ? builder.whereExists(resolutionsQuery) : builder.whereNotExists(resolutionsQuery) } - if (actionType !== undefined) { - const resolutionActionsQuery = this.db.db + if (actionType !== undefined || actionedBy !== undefined) { + let resolutionActionsQuery = this.db.db .selectFrom('moderation_report_resolution') .innerJoin( 'moderation_action', @@ -179,10 +181,22 @@ export class ModerationService { '=', ref('moderation_report.id'), ) - .where('moderation_action.action', '=', sql`${actionType}`) - .where('moderation_action.reversedAt', 'is', null) - .selectAll() - builder = builder.whereExists(resolutionActionsQuery) + + if (actionType) { + resolutionActionsQuery = resolutionActionsQuery + .where('moderation_action.action', '=', sql`${actionType}`) + .where('moderation_action.reversedAt', 'is', null) + } + + if (actionedBy) { + resolutionActionsQuery = resolutionActionsQuery.where( + 'moderation_action.createdBy', + '=', + actionedBy, + ) + } + + builder = builder.whereExists(resolutionActionsQuery.selectAll()) } if (cursor) { diff --git a/packages/pds/tests/views/admin/__snapshots__/get-moderation-reports.test.ts.snap b/packages/pds/tests/views/admin/__snapshots__/get-moderation-reports.test.ts.snap index c9e984bfb1a..9cfb5ae3c34 100644 --- a/packages/pds/tests/views/admin/__snapshots__/get-moderation-reports.test.ts.snap +++ b/packages/pds/tests/views/admin/__snapshots__/get-moderation-reports.test.ts.snap @@ -1,5 +1,45 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`pds admin get moderation reports view gets all moderation reports actioned by a certain moderator. 1`] = ` +Array [ + Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "id": 1, + "reasonType": "com.atproto.moderation.defs#reasonOther", + "reportedBy": "user(0)", + "resolvedByActionIds": Array [ + 2, + ], + "subject": Object { + "$type": "com.atproto.repo.strongRef", + "cid": "cids(0)", + "uri": "record(0)", + }, + "subjectRepoHandle": "alice.test", + }, +] +`; + +exports[`pds admin get moderation reports view gets all moderation reports actioned by a certain moderator. 2`] = ` +Array [ + Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "id": 3, + "reasonType": "com.atproto.moderation.defs#reasonOther", + "reportedBy": "user(0)", + "resolvedByActionIds": Array [ + 4, + ], + "subject": Object { + "$type": "com.atproto.repo.strongRef", + "cid": "cids(0)", + "uri": "record(0)", + }, + "subjectRepoHandle": "bob.test", + }, +] +`; + exports[`pds admin get moderation reports view gets all moderation reports by active resolution action type. 1`] = ` Array [ Object { diff --git a/packages/pds/tests/views/admin/get-moderation-reports.test.ts b/packages/pds/tests/views/admin/get-moderation-reports.test.ts index 0ef0f92685c..14be4ce821a 100644 --- a/packages/pds/tests/views/admin/get-moderation-reports.test.ts +++ b/packages/pds/tests/views/admin/get-moderation-reports.test.ts @@ -85,6 +85,7 @@ describe('pds admin get moderation reports view', () => { uri: report.subject.uri, cid: report.subject.cid, }, + createdBy: `did:example:admin${i}`, }) if (ab) { await sc.resolveReports({ @@ -241,6 +242,40 @@ describe('pds admin get moderation reports view', () => { expect(forSnapshot(reportsWithTakedown.data.reports)).toMatchSnapshot() }) + it('gets all moderation reports actioned by a certain moderator.', async () => { + const adminDidOne = 'did:example:admin0' + const adminDidTwo = 'did:example:admin2' + const [actionedByAdminOne, actionedByAdminTwo] = await Promise.all([ + agent.api.com.atproto.admin.getModerationReports( + { actionedBy: adminDidOne }, + { headers: { authorization: adminAuth() } }, + ), + agent.api.com.atproto.admin.getModerationReports( + { actionedBy: adminDidTwo }, + { headers: { authorization: adminAuth() } }, + ), + ]) + const [fullReportOne, fullReportTwo] = await Promise.all([ + agent.api.com.atproto.admin.getModerationReport( + { id: actionedByAdminOne.data.reports[0].id }, + { headers: { authorization: adminAuth() } }, + ), + agent.api.com.atproto.admin.getModerationReport( + { id: actionedByAdminTwo.data.reports[0].id }, + { headers: { authorization: adminAuth() } }, + ), + ]) + + expect(forSnapshot(actionedByAdminOne.data.reports)).toMatchSnapshot() + expect(fullReportOne.data.resolvedByActions[0].createdBy).toEqual( + adminDidOne, + ) + expect(forSnapshot(actionedByAdminTwo.data.reports)).toMatchSnapshot() + expect(fullReportTwo.data.resolvedByActions[0].createdBy).toEqual( + adminDidTwo, + ) + }) + it('paginates.', async () => { const results = (results) => results.flatMap((res) => res.reports) const paginator = async (cursor?: string) => {