Skip to content

Commit

Permalink
✨ Allow appealing a moderator decision through special report type (#…
Browse files Browse the repository at this point in the history
…1969)

* ✨ Allow appealing a moderator decision through special report type

* ✨ Allow querying subjects by appealed status

* ✨ Move to appealed boolean state column

* ✨ Remove leftover

* ✨ Move appealed status to new boolean column

* ✨ Throw when non-author attempts to appeal a subject

* 🚨 Appease the linter gods

* build

---------

Co-authored-by: Devin Ivy <devinivy@gmail.com>
  • Loading branch information
foysalit and devinivy authored Jan 3, 2024
1 parent ad0d976 commit 5e7b013
Show file tree
Hide file tree
Showing 32 changed files with 574 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-and-push-bsky-aws.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on:
push:
branches:
- main
- bsky-node-clustering
- appeal-report
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
Expand Down
22 changes: 21 additions & 1 deletion lexicons/com/atproto/admin/defs.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@
"#modEventLabel",
"#modEventAcknowledge",
"#modEventEscalate",
"#modEventMute"
"#modEventMute",
"#modEventResolveAppeal"
]
},
"subject": {
Expand Down Expand Up @@ -167,9 +168,18 @@
"type": "string",
"format": "datetime"
},
"lastAppealedAt": {
"type": "string",
"format": "datetime",
"description": "Timestamp referencing when the author of the subject appealed a moderation action"
},
"takendown": {
"type": "boolean"
},
"appealed": {
"type": "boolean",
"description": "True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators."
},
"suspendUntil": {
"type": "string",
"format": "datetime"
Expand Down Expand Up @@ -469,6 +479,16 @@
}
}
},
"modEventResolveAppeal": {
"type": "object",
"description": "Resolve appeal on a subject",
"properties": {
"comment": {
"type": "string",
"description": "Describe resolution."
}
}
},
"modEventComment": {
"type": "object",
"description": "Add a comment to a subject",
Expand Down
4 changes: 4 additions & 0 deletions lexicons/com/atproto/admin/queryModerationStatuses.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
"type": "boolean",
"description": "Get subjects that were taken down"
},
"appealed": {
"type": "boolean",
"description": "Get subjects in unresolved appealed status"
},
"limit": {
"type": "integer",
"minimum": 1,
Expand Down
7 changes: 6 additions & 1 deletion lexicons/com/atproto/moderation/defs.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"com.atproto.moderation.defs#reasonMisleading",
"com.atproto.moderation.defs#reasonSexual",
"com.atproto.moderation.defs#reasonRude",
"com.atproto.moderation.defs#reasonOther"
"com.atproto.moderation.defs#reasonOther",
"com.atproto.moderation.defs#reasonAppeal"
]
},
"reasonSpam": {
Expand All @@ -36,6 +37,10 @@
"reasonOther": {
"type": "token",
"description": "Other: reports not falling under another report category"
},
"reasonAppeal": {
"type": "token",
"description": "Appeal: appeal a previously taken moderation action"
}
}
}
1 change: 1 addition & 0 deletions packages/api/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ export const COM_ATPROTO_MODERATION = {
DefsReasonSexual: 'com.atproto.moderation.defs#reasonSexual',
DefsReasonRude: 'com.atproto.moderation.defs#reasonRude',
DefsReasonOther: 'com.atproto.moderation.defs#reasonOther',
DefsReasonAppeal: 'com.atproto.moderation.defs#reasonAppeal',
}
export const APP_BSKY_GRAPH = {
DefsModlist: 'app.bsky.graph.defs#modlist',
Expand Down
31 changes: 31 additions & 0 deletions packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export const schemaDict = {
'lex:com.atproto.admin.defs#modEventAcknowledge',
'lex:com.atproto.admin.defs#modEventEscalate',
'lex:com.atproto.admin.defs#modEventMute',
'lex:com.atproto.admin.defs#modEventResolveAppeal',
],
},
subject: {
Expand Down Expand Up @@ -237,9 +238,20 @@ export const schemaDict = {
type: 'string',
format: 'datetime',
},
lastAppealedAt: {
type: 'string',
format: 'datetime',
description:
'Timestamp referencing when the author of the subject appealed a moderation action',
},
takendown: {
type: 'boolean',
},
appealed: {
type: 'boolean',
description:
'True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators.',
},
suspendUntil: {
type: 'string',
format: 'datetime',
Expand Down Expand Up @@ -717,6 +729,16 @@ export const schemaDict = {
},
},
},
modEventResolveAppeal: {
type: 'object',
description: 'Resolve appeal on a subject',
properties: {
comment: {
type: 'string',
description: 'Describe resolution.',
},
},
},
modEventComment: {
type: 'object',
description: 'Add a comment to a subject',
Expand Down Expand Up @@ -1361,6 +1383,10 @@ export const schemaDict = {
type: 'boolean',
description: 'Get subjects that were taken down',
},
appealed: {
type: 'boolean',
description: 'Get subjects in unresolved appealed status',
},
limit: {
type: 'integer',
minimum: 1,
Expand Down Expand Up @@ -1946,6 +1972,7 @@ export const schemaDict = {
'com.atproto.moderation.defs#reasonSexual',
'com.atproto.moderation.defs#reasonRude',
'com.atproto.moderation.defs#reasonOther',
'com.atproto.moderation.defs#reasonAppeal',
],
},
reasonSpam: {
Expand Down Expand Up @@ -1973,6 +2000,10 @@ export const schemaDict = {
type: 'token',
description: 'Other: reports not falling under another report category',
},
reasonAppeal: {
type: 'token',
description: 'Appeal: appeal a previously taken moderation action',
},
},
},
ComAtprotoRepoApplyWrites: {
Expand Down
26 changes: 26 additions & 0 deletions packages/api/src/client/types/com/atproto/admin/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface ModEventViewDetail {
| ModEventAcknowledge
| ModEventEscalate
| ModEventMute
| ModEventResolveAppeal
| { $type: string; [k: string]: unknown }
subject:
| RepoView
Expand Down Expand Up @@ -147,7 +148,11 @@ export interface SubjectStatusView {
lastReviewedBy?: string
lastReviewedAt?: string
lastReportedAt?: string
/** Timestamp referencing when the author of the subject appealed a moderation action */
lastAppealedAt?: string
takendown?: boolean
/** True indicates that the a previously taken moderator action was appealed against, by the author of the content. False indicates last appeal was resolved by moderators. */
appealed?: boolean
suspendUntil?: string
[k: string]: unknown
}
Expand Down Expand Up @@ -538,6 +543,27 @@ export function validateModEventReverseTakedown(v: unknown): ValidationResult {
return lexicons.validate('com.atproto.admin.defs#modEventReverseTakedown', v)
}

/** Resolve appeal on a subject */
export interface ModEventResolveAppeal {
/** Describe resolution. */
comment?: string
[k: string]: unknown
}

export function isModEventResolveAppeal(
v: unknown,
): v is ModEventResolveAppeal {
return (
isObj(v) &&
hasProp(v, '$type') &&
v.$type === 'com.atproto.admin.defs#modEventResolveAppeal'
)
}

export function validateModEventResolveAppeal(v: unknown): ValidationResult {
return lexicons.validate('com.atproto.admin.defs#modEventResolveAppeal', v)
}

/** Add a comment to a subject */
export interface ModEventComment {
comment: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface QueryParams {
sortDirection?: 'asc' | 'desc'
/** Get subjects that were taken down */
takendown?: boolean
/** Get subjects in unresolved appealed status */
appealed?: boolean
limit?: number
cursor?: string
}
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/client/types/com/atproto/moderation/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type ReasonType =
| 'com.atproto.moderation.defs#reasonSexual'
| 'com.atproto.moderation.defs#reasonRude'
| 'com.atproto.moderation.defs#reasonOther'
| 'com.atproto.moderation.defs#reasonAppeal'
| (string & {})

/** Spam: frequent unwanted promotion, replies, mentions */
Expand All @@ -27,3 +28,5 @@ export const REASONSEXUAL = 'com.atproto.moderation.defs#reasonSexual'
export const REASONRUDE = 'com.atproto.moderation.defs#reasonRude'
/** Other: reports not falling under another report category */
export const REASONOTHER = 'com.atproto.moderation.defs#reasonOther'
/** Appeal: appeal a previously taken moderation action */
export const REASONAPPEAL = 'com.atproto.moderation.defs#reasonAppeal'
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default function (server: Server, ctx: AppContext) {
const {
subject,
takendown,
appealed,
reviewState,
reviewedAfter,
reviewedBefore,
Expand All @@ -28,6 +29,7 @@ export default function (server: Server, ctx: AppContext) {
reviewState: getReviewState(reviewState),
subject,
takendown,
appealed,
reviewedAfter,
reviewedBefore,
reportedAfter,
Expand Down
17 changes: 14 additions & 3 deletions packages/bsky/src/api/com/atproto/moderation/createReport.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { AuthRequiredError } from '@atproto/xrpc-server'
import { AuthRequiredError, ForbiddenError } from '@atproto/xrpc-server'
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
import { getReasonType, getSubject } from './util'
import { softDeleted } from '../../../../db/util'
import { REASONAPPEAL } from '../../../../lexicon/types/com/atproto/moderation/defs'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.moderation.createReport({
Expand All @@ -22,12 +23,22 @@ export default function (server: Server, ctx: AppContext) {
}
}

const reportReasonType = getReasonType(reasonType)
const reportSubject = getSubject(subject)
const subjectDid =
'did' in reportSubject ? reportSubject.did : reportSubject.uri.host

// If the report is an appeal, the requester must be the author of the subject
if (reasonType === REASONAPPEAL && requester !== subjectDid) {
throw new ForbiddenError('You cannot appeal this report')
}

const report = await db.transaction(async (dbTxn) => {
const moderationTxn = ctx.services.moderation(dbTxn)
return moderationTxn.report({
reasonType: getReasonType(reasonType),
reasonType: reportReasonType,
reason,
subject: getSubject(subject),
subject: reportSubject,
reportedBy: requester || ctx.cfg.serverDid,
})
})
Expand Down
2 changes: 2 additions & 0 deletions packages/bsky/src/api/com/atproto/moderation/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
REASONRUDE,
REASONSEXUAL,
REASONVIOLATION,
REASONAPPEAL,
} from '../../../../lexicon/types/com/atproto/moderation/defs'
import {
REVIEWCLOSED,
Expand Down Expand Up @@ -73,6 +74,7 @@ const reasonTypes = new Set([
REASONRUDE,
REASONSEXUAL,
REASONVIOLATION,
REASONAPPEAL,
])

const eventTypes = new Set([
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Kysely } from 'kysely'

export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.alterTable('moderation_subject_status')
.addColumn('lastAppealedAt', 'varchar')
.execute()
await db.schema
.alterTable('moderation_subject_status')
.addColumn('appealed', 'boolean')
.execute()
}

export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema
.alterTable('moderation_subject_status')
.dropColumn('lastAppealedAt')
.execute()
await db.schema
.alterTable('moderation_subject_status')
.dropColumn('appealed')
.execute()
}
1 change: 1 addition & 0 deletions packages/bsky/src/db/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ export * as _20230920T213858047Z from './20230920T213858047Z-add-tags-to-post'
export * as _20230929T192920807Z from './20230929T192920807Z-record-cursor-indexes'
export * as _20231003T202833377Z from './20231003T202833377Z-create-moderation-subject-status'
export * as _20231205T000257238Z from './20231205T000257238Z-remove-did-cache'
export * as _20231213T181744386Z from './20231213T181744386Z-moderation-subject-appeal'
3 changes: 3 additions & 0 deletions packages/bsky/src/db/tables/moderation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface ModerationEvent {
| 'com.atproto.admin.defs#modEventMute'
| 'com.atproto.admin.defs#modEventReverseTakedown'
| 'com.atproto.admin.defs#modEventEmail'
| 'com.atproto.admin.defs#modEventResolveAppeal'
subjectType: 'com.atproto.admin.defs#repoRef' | 'com.atproto.repo.strongRef'
subjectDid: string
subjectUri: string | null
Expand Down Expand Up @@ -47,9 +48,11 @@ export interface ModerationSubjectStatus {
lastReviewedBy: string | null
lastReviewedAt: string | null
lastReportedAt: string | null
lastAppealedAt: string | null
muteUntil: string | null
suspendUntil: string | null
takendown: boolean
appealed: boolean | null
comment: string | null
}

Expand Down
1 change: 1 addition & 0 deletions packages/bsky/src/lexicon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export const COM_ATPROTO_MODERATION = {
DefsReasonSexual: 'com.atproto.moderation.defs#reasonSexual',
DefsReasonRude: 'com.atproto.moderation.defs#reasonRude',
DefsReasonOther: 'com.atproto.moderation.defs#reasonOther',
DefsReasonAppeal: 'com.atproto.moderation.defs#reasonAppeal',
}
export const APP_BSKY_GRAPH = {
DefsModlist: 'app.bsky.graph.defs#modlist',
Expand Down
Loading

0 comments on commit 5e7b013

Please sign in to comment.