Skip to content

Commit

Permalink
introduce mod subject wrappers
Browse files Browse the repository at this point in the history
  • Loading branch information
dholms committed Dec 21, 2023
1 parent c8312f5 commit 8016357
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 379 deletions.
194 changes: 47 additions & 147 deletions packages/ozone/src/api/com/atproto/admin/emitModerationEvent.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import { CID } from 'multiformats/cid'
import { AtUri } from '@atproto/syntax'
import {
AuthRequiredError,
InvalidRequestError,
UpstreamFailureError,
} from '@atproto/xrpc-server'
import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
import { getSubject } from '../moderation/util'
import {
isModEventLabel,
isModEventReverseTakedown,
isModEventTakedown,
} from '../../../../lexicon/types/com/atproto/admin/defs'
import { TakedownSubjects } from '../../../../services/moderation'
import { retryHttp } from '../../../../util/retry'
import { subjectFromInput } from '../../../../services/moderation/subject'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.admin.emitModerationEvent({
Expand All @@ -23,15 +15,19 @@ export default function (server: Server, ctx: AppContext) {
const access = auth.credentials
const db = ctx.db
const moderationService = ctx.services.moderation(db)
const { subject, createdBy, subjectBlobCids, event } = input.body
const { createdBy, event } = input.body
const isTakedownEvent = isModEventTakedown(event)
const isReverseTakedownEvent = isModEventReverseTakedown(event)
const isLabelEvent = isModEventLabel(event)
const subject = subjectFromInput(
input.body.subject,
input.body.subjectBlobCids,
)

// apply access rules

// if less than moderator access then can not takedown an account
if (!access.moderator && isTakedownEvent && 'did' in subject) {
if (!access.moderator && isTakedownEvent && subject.isRepo()) {
throw new AuthRequiredError(
'Must be a full moderator to perform an account takedown',
)
Expand All @@ -54,11 +50,9 @@ export default function (server: Server, ctx: AppContext) {
])
}

const subjectInfo = getSubject(subject)

if (isTakedownEvent || isReverseTakedownEvent) {
const isSubjectTakendown = await moderationService.isSubjectTakendown(
subjectInfo,
subject,
)

if (isSubjectTakendown && isTakedownEvent) {
Expand All @@ -70,145 +64,51 @@ export default function (server: Server, ctx: AppContext) {
}
}

const { result: moderationEvent, takenDown } = await db.transaction(
async (dbTxn) => {
const moderationTxn = ctx.services.moderation(dbTxn)

const result = await moderationTxn.logEvent({
event,
subject: subjectInfo,
subjectBlobCids:
subjectBlobCids?.map((cid) => CID.parse(cid)) ?? [],
createdBy,
})

let takenDown: TakedownSubjects | undefined

if (
result.subjectType === 'com.atproto.admin.defs#repoRef' &&
result.subjectDid
) {
// No credentials to revoke on appview
if (isTakedownEvent) {
takenDown = await moderationTxn.takedownRepo({
takedownId: result.id,
did: result.subjectDid,
})
}

if (isReverseTakedownEvent) {
await moderationTxn.reverseTakedownRepo({
did: result.subjectDid,
})
takenDown = {
subjects: [
{
$type: 'com.atproto.admin.defs#repoRef',
did: result.subjectDid,
},
],
did: result.subjectDid,
}
}
const moderationEvent = await db.transaction(async (dbTxn) => {
const moderationTxn = ctx.services.moderation(dbTxn)

const result = await moderationTxn.logEvent({
event,
subject,
createdBy,
})

if (subject.isRepo()) {
if (isTakedownEvent) {
await moderationTxn.takedownRepo(subject, result.id)
} else if (isReverseTakedownEvent) {
await moderationTxn.reverseTakedownRepo(subject)
}
}

if (
result.subjectType === 'com.atproto.repo.strongRef' &&
result.subjectUri
) {
const subjectUri = new AtUri(result.subjectUri)
const blobCids = subjectBlobCids?.map((cid) => CID.parse(cid)) ?? []
if (isTakedownEvent) {
await moderationTxn.takedownRecord({
takedownId: result.id,
uri: subjectUri,
// TODO: I think this will always be available for strongRefs?
cid: CID.parse(result.subjectCid as string),
})
if (blobCids && blobCids.length > 0) {
await moderationTxn.takedownBlobs({
takedownId: result.id,
did: subjectUri.hostname,
blobCids,
})
}
}

if (isReverseTakedownEvent) {
await moderationTxn.reverseTakedownRecord({
uri: new AtUri(result.subjectUri),
})
await moderationTxn.reverseTakedownBlobs({
did: subjectUri.hostname,
blobCids,
})
// takenDown = {
// did: result.subjectDid,
// subjects: [
// {
// $type: 'com.atproto.repo.strongRef',
// uri: result.subjectUri,
// cid: result.subjectCid ?? '',
// },
// ...blobCids.map((cid) => ({
// $type: 'com.atproto.admin.defs#repoBlobRef',
// did: result.subjectDid,
// cid: cid.toString(),
// recordUri: result.subjectUri,
// })),
// ],
// }
}
if (subject.isRecord()) {
if (isTakedownEvent) {
await moderationTxn.takedownRecord(subject, result.id)
}

if (isLabelEvent) {
await moderationTxn.formatAndCreateLabels(
ctx.cfg.labelerDid,
result.subjectUri ?? result.subjectDid,
result.subjectCid,
{
create: result.createLabelVals?.length
? result.createLabelVals.split(' ')
: undefined,
negate: result.negateLabelVals?.length
? result.negateLabelVals.split(' ')
: undefined,
},
)
if (isReverseTakedownEvent) {
await moderationTxn.reverseTakedownRecord(subject)
}
}

return { result, takenDown }
},
)
if (isLabelEvent) {
await moderationTxn.formatAndCreateLabels(
ctx.cfg.labelerDid,
result.subjectUri ?? result.subjectDid,
result.subjectCid,
{
create: result.createLabelVals?.length
? result.createLabelVals.split(' ')
: undefined,
negate: result.negateLabelVals?.length
? result.negateLabelVals.split(' ')
: undefined,
},
)
}

// @TODO move to commit hook on takedown method
// if (takenDown && ctx.moderationPushAgent) {
// const { did, subjects } = takenDown
// if (did && subjects.length > 0) {
// const agent = ctx.moderationPushAgent
// const results = await Promise.allSettled(
// subjects.map((subject) =>
// retryHttp(() =>
// agent.api.com.atproto.admin.updateSubjectStatus({
// subject,
// takedown: isTakedownEvent
// ? {
// applied: true,
// ref: moderationEvent.id.toString(),
// }
// : {
// applied: false,
// },
// }),
// ),
// ),
// )
// const hadFailure = results.some((r) => r.status === 'rejected')
// if (hadFailure) {
// throw new UpstreamFailureError('failed to apply action on PDS')
// }
// }
// }
return result
})

return {
encoding: 'application/json',
Expand Down
8 changes: 5 additions & 3 deletions packages/ozone/src/api/com/atproto/moderation/createReport.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { AuthRequiredError } from '@atproto/xrpc-server'

Check warning on line 1 in packages/ozone/src/api/com/atproto/moderation/createReport.ts

View workflow job for this annotation

GitHub Actions / Verify

'AuthRequiredError' is defined but never used. Allowed unused vars must match /^_/u
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
import { getReasonType, getSubject } from './util'
import { getReasonType } from './util'
import { softDeleted } from '../../../../db/util'

Check warning on line 5 in packages/ozone/src/api/com/atproto/moderation/createReport.ts

View workflow job for this annotation

GitHub Actions / Verify

'softDeleted' is defined but never used. Allowed unused vars must match /^_/u
import { subjectFromInput } from '../../../../services/moderation/subject'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.moderation.createReport({
// @TODO anonymous reports w/ optional auth are a temporary measure
auth: ctx.authOptionalVerifier,
handler: async ({ input, auth }) => {
const { reasonType, reason, subject } = input.body
const requester = auth.credentials.did
const { reasonType, reason } = input.body
const subject = subjectFromInput(input.body.subject)

const db = ctx.db

Expand All @@ -28,7 +30,7 @@ export default function (server: Server, ctx: AppContext) {
return moderationTxn.report({
reasonType: getReasonType(reasonType),
reason,
subject: getSubject(subject),
subject,
reportedBy: requester || ctx.cfg.serverDid,
})
})
Expand Down
26 changes: 0 additions & 26 deletions packages/ozone/src/api/com/atproto/moderation/util.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { CID } from 'multiformats/cid'
import { InvalidRequestError } from '@atproto/xrpc-server'
import { AtUri } from '@atproto/syntax'
import { InputSchema as ReportInput } from '../../../../lexicon/types/com/atproto/moderation/createReport'
import { InputSchema as ActionInput } from '../../../../lexicon/types/com/atproto/admin/emitModerationEvent'
import {
REASONOTHER,
REASONSPAM,
Expand All @@ -19,29 +16,6 @@ import {
import { ModerationEvent } from '../../../../db/schema/moderation_event'
import { ModerationSubjectStatusRow } from '../../../../services/moderation/types'

type SubjectInput = ReportInput['subject'] | ActionInput['subject']

export const getSubject = (subject: SubjectInput) => {
if (
subject.$type === 'com.atproto.admin.defs#repoRef' &&
typeof subject.did === 'string'
) {
return { did: subject.did }
}
if (
subject.$type === 'com.atproto.repo.strongRef' &&
typeof subject.uri === 'string' &&
typeof subject.cid === 'string'
) {
const uri = new AtUri(subject.uri)
return {
uri,
cid: CID.parse(subject.cid),
}
}
throw new InvalidRequestError('Invalid subject')
}

export const getReasonType = (reasonType: ReportInput['reasonType']) => {
if (reasonTypes.has(reasonType)) {
return reasonType as NonNullable<ModerationEvent['meta']>['reportType']
Expand Down
19 changes: 4 additions & 15 deletions packages/ozone/src/db/periodic-moderation-event-reversal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ import { wait } from '@atproto/common'
import { Leader } from './leader'
import { dbLogger } from '../logger'
import AppContext from '../context'
import { AtUri } from '@atproto/api'
import { ModerationSubjectStatusRow } from '../services/moderation/types'
import { CID } from 'multiformats/cid'
import AtpAgent from '@atproto/api'
import { retryHttp } from '../util/retry'
import { ReversalSubject } from '../services/moderation'

export const MODERATION_ACTION_REVERSAL_ID = 1011

Expand All @@ -19,27 +17,18 @@ export class PeriodicModerationEventReversal {
this.pushAgent = appContext.moderationPushAgent
}

async revertState(eventRow: ModerationSubjectStatusRow) {
async revertState(subject: ReversalSubject) {
await this.appContext.db.transaction(async (dbTxn) => {
const moderationTxn = this.appContext.services.moderation(dbTxn)
const originalEvent =
await moderationTxn.getLastReversibleEventForSubject(eventRow)
await moderationTxn.getLastReversibleEventForSubject(subject)
if (originalEvent) {
const { restored } = await moderationTxn.revertState({
action: originalEvent.action,
createdBy: originalEvent.createdBy,
comment:
'[SCHEDULED_REVERSAL] Reverting action as originally scheduled',
subject:
eventRow.recordPath && eventRow.recordCid
? {
uri: AtUri.make(
eventRow.did,
...eventRow.recordPath.split('/'),
),
cid: CID.parse(eventRow.recordCid),
}
: { did: eventRow.did },
subject: subject.subject,
createdAt: new Date(),
})

Expand Down
Loading

0 comments on commit 8016357

Please sign in to comment.