Reports by active action type ()

* Lexicon updates for getting mod reports by active action type

* Update pds and bsky to support getting mod reports by active action type
This commit is contained in:
devin ivy 2023-05-12 17:45:15 -04:00 committed by GitHub
parent 5804716504
commit 1cbffd63ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 361 additions and 6 deletions
lexicons/com/atproto/admin
packages
api/src/client
lexicons.ts
types/com/atproto/admin
bsky/src
pds
src
api/com/atproto/admin
lexicon
lexicons.ts
types/com/atproto/admin
services/moderation
tests/views/admin

@ -10,6 +10,15 @@
"properties": {
"subject": {"type": "string"},
"resolved": {"type": "boolean"},
"actionType": {
"type": "string",
"knownValues": [
"com.atproto.admin.defs#takedown",
"com.atproto.admin.defs#flag",
"com.atproto.admin.defs#acknowledge",
"com.atproto.admin.defs#escalate"
]
},
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50},
"cursor": {"type": "string"}
}

@ -835,6 +835,15 @@ export const schemaDict = {
resolved: {
type: 'boolean',
},
actionType: {
type: 'string',
knownValues: [
'com.atproto.admin.defs#takedown',
'com.atproto.admin.defs#flag',
'com.atproto.admin.defs#acknowledge',
'com.atproto.admin.defs#escalate',
],
},
limit: {
type: 'integer',
minimum: 1,

@ -11,6 +11,12 @@ import * as ComAtprotoAdminDefs from './defs'
export interface QueryParams {
subject?: string
resolved?: boolean
actionType?:
| 'com.atproto.admin.defs#takedown'
| 'com.atproto.admin.defs#flag'
| 'com.atproto.admin.defs#acknowledge'
| 'com.atproto.admin.defs#escalate'
| (string & {})
limit?: number
cursor?: string
}

@ -7,11 +7,12 @@ export default function (server: Server, ctx: AppContext) {
auth: adminVerifier(ctx.cfg.adminPassword),
handler: async ({ params }) => {
const { db, services } = ctx
const { subject, resolved, limit = 50, cursor } = params
const { subject, resolved, actionType, limit = 50, cursor } = params
const moderationService = services.moderation(db)
const results = await moderationService.getReports({
subject,
resolved,
actionType,
limit,
cursor,
})

@ -9,7 +9,9 @@ import {
StreamAuthVerifier,
} from '@atproto/xrpc-server'
import { schemas } from './lexicons'
import * as ComAtprotoAdminDisableAccountInvites from './types/com/atproto/admin/disableAccountInvites'
import * as ComAtprotoAdminDisableInviteCodes from './types/com/atproto/admin/disableInviteCodes'
import * as ComAtprotoAdminEnableAccountInvites from './types/com/atproto/admin/enableAccountInvites'
import * as ComAtprotoAdminGetInviteCodes from './types/com/atproto/admin/getInviteCodes'
import * as ComAtprotoAdminGetModerationAction from './types/com/atproto/admin/getModerationAction'
import * as ComAtprotoAdminGetModerationActions from './types/com/atproto/admin/getModerationActions'
@ -35,6 +37,7 @@ import * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRe
import * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord'
import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords'
import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord'
import * as ComAtprotoRepoRebaseRepo from './types/com/atproto/repo/rebaseRepo'
import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob'
import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount'
import * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword'
@ -158,6 +161,16 @@ export class AdminNS {
this._server = server
}
disableAccountInvites<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
ComAtprotoAdminDisableAccountInvites.Handler<ExtractAuth<AV>>
>,
) {
const nsid = 'com.atproto.admin.disableAccountInvites' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
disableInviteCodes<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
@ -168,6 +181,16 @@ export class AdminNS {
return this._server.xrpc.method(nsid, cfg)
}
enableAccountInvites<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
ComAtprotoAdminEnableAccountInvites.Handler<ExtractAuth<AV>>
>,
) {
const nsid = 'com.atproto.admin.enableAccountInvites' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
getInviteCodes<AV extends AuthVerifier>(
cfg: ConfigOf<AV, ComAtprotoAdminGetInviteCodes.Handler<ExtractAuth<AV>>>,
) {
@ -405,6 +428,13 @@ export class RepoNS {
return this._server.xrpc.method(nsid, cfg)
}
rebaseRepo<AV extends AuthVerifier>(
cfg: ConfigOf<AV, ComAtprotoRepoRebaseRepo.Handler<ExtractAuth<AV>>>,
) {
const nsid = 'com.atproto.repo.rebaseRepo' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
uploadBlob<AV extends AuthVerifier>(
cfg: ConfigOf<AV, ComAtprotoRepoUploadBlob.Handler<ExtractAuth<AV>>>,
) {

@ -333,6 +333,9 @@ export const schemaDict = {
type: 'ref',
ref: 'lex:com.atproto.server.defs#inviteCode',
},
invitesDisabled: {
type: 'boolean',
},
},
},
repoViewDetail: {
@ -388,6 +391,9 @@ export const schemaDict = {
ref: 'lex:com.atproto.server.defs#inviteCode',
},
},
invitesDisabled: {
type: 'boolean',
},
},
},
repoRef: {
@ -589,6 +595,30 @@ export const schemaDict = {
},
},
},
ComAtprotoAdminDisableAccountInvites: {
lexicon: 1,
id: 'com.atproto.admin.disableAccountInvites',
defs: {
main: {
type: 'procedure',
description:
'Disable an account from receiving new invite codes, but does not invalidate existing codes',
input: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['account'],
properties: {
account: {
type: 'string',
format: 'did',
},
},
},
},
},
},
},
ComAtprotoAdminDisableInviteCodes: {
lexicon: 1,
id: 'com.atproto.admin.disableInviteCodes',
@ -620,6 +650,29 @@ export const schemaDict = {
},
},
},
ComAtprotoAdminEnableAccountInvites: {
lexicon: 1,
id: 'com.atproto.admin.enableAccountInvites',
defs: {
main: {
type: 'procedure',
description: 'Re-enable an accounts ability to receive invite codes',
input: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['account'],
properties: {
account: {
type: 'string',
format: 'did',
},
},
},
},
},
},
},
ComAtprotoAdminGetInviteCodes: {
lexicon: 1,
id: 'com.atproto.admin.getInviteCodes',
@ -782,6 +835,15 @@ export const schemaDict = {
resolved: {
type: 'boolean',
},
actionType: {
type: 'string',
knownValues: [
'com.atproto.admin.defs#takedown',
'com.atproto.admin.defs#flag',
'com.atproto.admin.defs#acknowledge',
'com.atproto.admin.defs#escalate',
],
},
limit: {
type: 'integer',
minimum: 1,
@ -1959,6 +2021,41 @@ export const schemaDict = {
},
},
},
ComAtprotoRepoRebaseRepo: {
lexicon: 1,
id: 'com.atproto.repo.rebaseRepo',
defs: {
main: {
type: 'procedure',
description: 'Simple rebase of repo that deletes history',
input: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['repo'],
properties: {
repo: {
type: 'string',
format: 'at-identifier',
description: 'The handle or DID of the repo.',
},
swapCommit: {
type: 'string',
format: 'cid',
description:
'Compare and swap with the previous commit by cid.',
},
},
},
},
errors: [
{
name: 'InvalidSwap',
},
],
},
},
},
ComAtprotoRepoStrongRef: {
lexicon: 1,
id: 'com.atproto.repo.strongRef',
@ -5207,7 +5304,10 @@ export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[]
export const lexicons: Lexicons = new Lexicons(schemas)
export const ids = {
ComAtprotoAdminDefs: 'com.atproto.admin.defs',
ComAtprotoAdminDisableAccountInvites:
'com.atproto.admin.disableAccountInvites',
ComAtprotoAdminDisableInviteCodes: 'com.atproto.admin.disableInviteCodes',
ComAtprotoAdminEnableAccountInvites: 'com.atproto.admin.enableAccountInvites',
ComAtprotoAdminGetInviteCodes: 'com.atproto.admin.getInviteCodes',
ComAtprotoAdminGetModerationAction: 'com.atproto.admin.getModerationAction',
ComAtprotoAdminGetModerationActions: 'com.atproto.admin.getModerationActions',
@ -5237,6 +5337,7 @@ export const ids = {
ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord',
ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords',
ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord',
ComAtprotoRepoRebaseRepo: 'com.atproto.repo.rebaseRepo',
ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',
ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob',
ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount',

@ -177,6 +177,7 @@ export interface RepoView {
indexedAt: string
moderation: Moderation
invitedBy?: ComAtprotoServerDefs.InviteCode
invitesDisabled?: boolean
[k: string]: unknown
}
@ -202,6 +203,7 @@ export interface RepoViewDetail {
labels?: ComAtprotoLabelDefs.Label[]
invitedBy?: ComAtprotoServerDefs.InviteCode
invites?: ComAtprotoServerDefs.InviteCode[]
invitesDisabled?: boolean
[k: string]: unknown
}

@ -0,0 +1,35 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { ValidationResult, BlobRef } from '@atproto/lexicon'
import { lexicons } from '../../../../lexicons'
import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
import { HandlerAuth } from '@atproto/xrpc-server'
export interface QueryParams {}
export interface InputSchema {
account: string
[k: string]: unknown
}
export interface HandlerInput {
encoding: 'application/json'
body: InputSchema
}
export interface HandlerError {
status: number
message?: string
}
export type HandlerOutput = HandlerError | void
export type Handler<HA extends HandlerAuth = never> = (ctx: {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
}) => Promise<HandlerOutput> | HandlerOutput

@ -0,0 +1,35 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { ValidationResult, BlobRef } from '@atproto/lexicon'
import { lexicons } from '../../../../lexicons'
import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
import { HandlerAuth } from '@atproto/xrpc-server'
export interface QueryParams {}
export interface InputSchema {
account: string
[k: string]: unknown
}
export interface HandlerInput {
encoding: 'application/json'
body: InputSchema
}
export interface HandlerError {
status: number
message?: string
}
export type HandlerOutput = HandlerError | void
export type Handler<HA extends HandlerAuth = never> = (ctx: {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
}) => Promise<HandlerOutput> | HandlerOutput

@ -12,6 +12,12 @@ import * as ComAtprotoAdminDefs from './defs'
export interface QueryParams {
subject?: string
resolved?: boolean
actionType?:
| 'com.atproto.admin.defs#takedown'
| 'com.atproto.admin.defs#flag'
| 'com.atproto.admin.defs#acknowledge'
| 'com.atproto.admin.defs#escalate'
| (string & {})
limit: number
cursor?: string
}

@ -0,0 +1,39 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { ValidationResult, BlobRef } from '@atproto/lexicon'
import { lexicons } from '../../../../lexicons'
import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
import { HandlerAuth } from '@atproto/xrpc-server'
export interface QueryParams {}
export interface InputSchema {
/** The handle or DID of the repo. */
repo: string
/** Compare and swap with the previous commit by cid. */
swapCommit?: string
[k: string]: unknown
}
export interface HandlerInput {
encoding: 'application/json'
body: InputSchema
}
export interface HandlerError {
status: number
message?: string
error?: 'InvalidSwap'
}
export type HandlerOutput = HandlerError | void
export type Handler<HA extends HandlerAuth = never> = (ctx: {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
}) => Promise<HandlerOutput> | HandlerOutput

@ -1,4 +1,4 @@
import { Selectable } from 'kysely'
import { Selectable, sql } from 'kysely'
import { CID } from 'multiformats/cid'
import { AtUri } from '@atproto/uri'
import { InvalidRequestError } from '@atproto/xrpc-server'
@ -78,10 +78,11 @@ export class ModerationService {
async getReports(opts: {
subject?: string
resolved?: boolean
actionType?: string
limit: number
cursor?: string
}): Promise<ModerationReportRow[]> {
const { subject, resolved, limit, cursor } = opts
const { subject, resolved, actionType, limit, cursor } = opts
const { ref } = this.db.db.dynamic
let builder = this.db.db.selectFrom('moderation_report')
if (subject) {
@ -104,6 +105,24 @@ export class ModerationService {
? builder.whereExists(resolutionsQuery)
: builder.whereNotExists(resolutionsQuery)
}
if (actionType !== undefined) {
const resolutionActionsQuery = this.db.db
.selectFrom('moderation_report_resolution')
.innerJoin(
'moderation_action',
'moderation_action.id',
'moderation_report_resolution.actionId',
)
.whereRef(
'moderation_report_resolution.reportId',
'=',
ref('moderation_report.id'),
)
.where('moderation_action.action', '=', sql`${actionType}`)
.where('moderation_action.reversedAt', 'is', null)
.selectAll()
builder = builder.whereExists(resolutionActionsQuery)
}
if (cursor) {
const cursorNumeric = parseInt(cursor, 10)
if (isNaN(cursorNumeric)) {

@ -6,11 +6,12 @@ export default function (server: Server, ctx: AppContext) {
auth: ctx.moderatorVerifier,
handler: async ({ params }) => {
const { db, services } = ctx
const { subject, resolved, limit = 50, cursor } = params
const { subject, resolved, actionType, limit = 50, cursor } = params
const moderationService = services.moderation(db)
const results = await moderationService.getReports({
subject,
resolved,
actionType,
limit,
cursor,
})

@ -835,6 +835,15 @@ export const schemaDict = {
resolved: {
type: 'boolean',
},
actionType: {
type: 'string',
knownValues: [
'com.atproto.admin.defs#takedown',
'com.atproto.admin.defs#flag',
'com.atproto.admin.defs#acknowledge',
'com.atproto.admin.defs#escalate',
],
},
limit: {
type: 'integer',
minimum: 1,

@ -12,6 +12,12 @@ import * as ComAtprotoAdminDefs from './defs'
export interface QueryParams {
subject?: string
resolved?: boolean
actionType?:
| 'com.atproto.admin.defs#takedown'
| 'com.atproto.admin.defs#flag'
| 'com.atproto.admin.defs#acknowledge'
| 'com.atproto.admin.defs#escalate'
| (string & {})
limit: number
cursor?: string
}

@ -1,4 +1,4 @@
import { Selectable } from 'kysely'
import { Selectable, sql } from 'kysely'
import { CID } from 'multiformats/cid'
import { BlobStore } from '@atproto/repo'
import { AtUri } from '@atproto/uri'
@ -96,10 +96,11 @@ export class ModerationService {
async getReports(opts: {
subject?: string
resolved?: boolean
actionType?: string
limit: number
cursor?: string
}): Promise<ModerationReportRow[]> {
const { subject, resolved, limit, cursor } = opts
const { subject, resolved, actionType, limit, cursor } = opts
const { ref } = this.db.db.dynamic
let builder = this.db.db.selectFrom('moderation_report')
if (subject) {
@ -122,6 +123,24 @@ export class ModerationService {
? builder.whereExists(resolutionsQuery)
: builder.whereNotExists(resolutionsQuery)
}
if (actionType !== undefined) {
const resolutionActionsQuery = this.db.db
.selectFrom('moderation_report_resolution')
.innerJoin(
'moderation_action',
'moderation_action.id',
'moderation_report_resolution.actionId',
)
.whereRef(
'moderation_report_resolution.reportId',
'=',
ref('moderation_report.id'),
)
.where('moderation_action.action', '=', sql`${actionType}`)
.where('moderation_action.reversedAt', 'is', null)
.selectAll()
builder = builder.whereExists(resolutionActionsQuery)
}
if (cursor) {
const cursorNumeric = parseInt(cursor, 10)
if (isNaN(cursorNumeric)) {

@ -1,5 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`pds admin get moderation reports view gets all moderation reports by active resolution action type. 1`] = `
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)",
},
},
]
`;
exports[`pds admin get moderation reports view gets all moderation reports for a record. 1`] = `
Array [
Object {

@ -157,6 +157,15 @@ describe('pds admin get moderation reports view', () => {
expect(forSnapshot(unresolved.data.reports)).toMatchSnapshot()
})
it('gets all moderation reports by active resolution action type.', async () => {
const reportsWithTakedown =
await agent.api.com.atproto.admin.getModerationReports(
{ actionType: TAKEDOWN },
{ headers: { authorization: adminAuth() } },
)
expect(forSnapshot(reportsWithTakedown.data.reports)).toMatchSnapshot()
})
it('paginates.', async () => {
const results = (results) => results.flatMap((res) => res.reports)
const paginator = async (cursor?: string) => {