Skip to content

Commit

Permalink
✨ Allow muting reporter
Browse files Browse the repository at this point in the history
  • Loading branch information
foysalit committed Apr 8, 2024
1 parent 7570d31 commit f603e05
Show file tree
Hide file tree
Showing 21 changed files with 251 additions and 26 deletions.
12 changes: 12 additions & 0 deletions lexicons/tools/ozone/moderation/defs.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@
"type": "string",
"format": "datetime"
},
"muteReportingUntil": {
"type": "string",
"format": "datetime"
},
"lastReviewedBy": {
"type": "string",
"format": "did"
Expand Down Expand Up @@ -284,6 +288,10 @@
"required": ["durationInHours"],
"properties": {
"comment": { "type": "string" },
"reportingOnly": {
"type": "boolean",
"description": "Mute incoming reports from this subject. Only enabled when the subject is a DID"
},
"durationInHours": {
"type": "integer",
"description": "Indicates how long the subject should remain muted."
Expand All @@ -294,6 +302,10 @@
"type": "object",
"description": "Unmute action on a subject",
"properties": {
"reportingOnly": {
"type": "boolean",
"description": "Unmute incoming reports from this subject. Only enabled when the subject is a DID"
},
"comment": {
"type": "string",
"description": "Describe reasoning behind the reversal."
Expand Down
14 changes: 14 additions & 0 deletions packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8180,6 +8180,10 @@ export const schemaDict = {
type: 'string',
format: 'datetime',
},
muteReportingUntil: {
type: 'string',
format: 'datetime',
},
lastReviewedBy: {
type: 'string',
format: 'did',
Expand Down Expand Up @@ -8355,6 +8359,11 @@ export const schemaDict = {
comment: {
type: 'string',
},
reportingOnly: {
type: 'boolean',
description:
'Mute incoming reports from this subject. Only enabled when the subject is a DID',
},
durationInHours: {
type: 'integer',
description: 'Indicates how long the subject should remain muted.',
Expand All @@ -8365,6 +8374,11 @@ export const schemaDict = {
type: 'object',
description: 'Unmute action on a subject',
properties: {
reportingOnly: {
type: 'boolean',
description:
'Unmute incoming reports from this subject. Only enabled when the subject is a DID',
},
comment: {
type: 'string',
description: 'Describe reasoning behind the reversal.',
Expand Down
5 changes: 5 additions & 0 deletions packages/api/src/client/types/tools/ozone/moderation/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export interface SubjectStatusView {
/** Sticky comment on the subject. */
comment?: string
muteUntil?: string
muteReportingUntil?: string
lastReviewedBy?: string
lastReviewedAt?: string
lastReportedAt?: string
Expand Down Expand Up @@ -310,6 +311,8 @@ export function validateModEventEscalate(v: unknown): ValidationResult {
/** Mute incoming reports on a subject */
export interface ModEventMute {
comment?: string
/** Mute incoming reports from this subject. Only enabled when the subject is a DID */
reportingOnly?: boolean
/** Indicates how long the subject should remain muted. */
durationInHours: number
[k: string]: unknown
Expand All @@ -329,6 +332,8 @@ export function validateModEventMute(v: unknown): ValidationResult {

/** Unmute action on a subject */
export interface ModEventUnmute {
/** Unmute incoming reports from this subject. Only enabled when the subject is a DID */
reportingOnly?: boolean
/** Describe reasoning behind the reversal. */
comment?: string
[k: string]: unknown
Expand Down
12 changes: 12 additions & 0 deletions packages/ozone/src/api/moderation/emitEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {
isModEventDivert,
isModEventEmail,
isModEventLabel,
isModEventMute,
isModEventReverseTakedown,
isModEventTakedown,
isModEventUnmute,
} from '../../lexicon/types/tools/ozone/moderation/defs'
import { HandlerInput } from '../../lexicon/types/tools/ozone/moderation/emitEvent'
import { subjectFromInput } from '../../mod-service/subject'
Expand Down Expand Up @@ -113,6 +115,16 @@ const handleModerationEvent = async ({
await ctx.blobDiverter.uploadBlobOnService(subject.info())
}

if (
(isModEventMute(event) || isModEventUnmute(event)) &&
!subject.isRepo() &&
event.reportingOnly
) {
throw new InvalidRequestError(
'Subject must be a repo when muting reporting only',
)
}

const moderationEvent = await db.transaction(async (dbTxn) => {
const moderationTxn = ctx.modService(dbTxn)

Expand Down
5 changes: 1 addition & 4 deletions packages/ozone/src/auth-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,7 @@ export class AuthVerifier {
triage: string[]
private adminPassword: string

constructor(
public idResolver: IdResolver,
opts: AuthVerifierOpts,
) {
constructor(public idResolver: IdResolver, opts: AuthVerifierOpts) {
this.serviceDid = opts.serviceDid
this.admins = opts.admins
this.moderators = opts.moderators
Expand Down
5 changes: 1 addition & 4 deletions packages/ozone/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ export type AppContextOptions = {
}

export class AppContext {
constructor(
private opts: AppContextOptions,
private secrets: OzoneSecrets,
) {}
constructor(private opts: AppContextOptions, private secrets: OzoneSecrets) {}

static async fromConfig(
cfg: OzoneConfig,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Kysely } from 'kysely'

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

export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema
.alterTable('moderation_subject_status')
.dropColumn('muteReportingUntil')
.execute()
}
1 change: 1 addition & 0 deletions packages/ozone/src/db/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * as _20240116T085607200Z from './20240116T085607200Z-communication-templ
export * as _20240201T051104136Z from './20240201T051104136Z-mod-event-blobs'
export * as _20240208T213404429Z from './20240208T213404429Z-add-tags-column-to-moderation-subject'
export * as _20240228T003647759Z from './20240228T003647759Z-add-label-sigs'
export * as _20240408T192432676Z from './20240408T192432676Z-mute-reporting'
5 changes: 1 addition & 4 deletions packages/ozone/src/db/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ export type LabeledResult = {
* ↳ SQL Condition
*/
export abstract class GenericKeyset<R, LR extends LabeledResult> {
constructor(
public primary: DbRef,
public secondary: DbRef,
) {}
constructor(public primary: DbRef, public secondary: DbRef) {}
abstract labelResult(result: R): LR
abstract labeledResultToCursor(labeled: LR): Cursor
abstract cursorToLabeledResult(cursor: Cursor): LR
Expand Down
1 change: 1 addition & 0 deletions packages/ozone/src/db/schema/moderation_subject_status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface ModerationSubjectStatus {
lastReportedAt: string | null
lastAppealedAt: string | null
muteUntil: string | null
muteReportingUntil: string | null
suspendUntil: string | null
takendown: boolean
appealed: boolean | null
Expand Down
14 changes: 14 additions & 0 deletions packages/ozone/src/lexicon/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8180,6 +8180,10 @@ export const schemaDict = {
type: 'string',
format: 'datetime',
},
muteReportingUntil: {
type: 'string',
format: 'datetime',
},
lastReviewedBy: {
type: 'string',
format: 'did',
Expand Down Expand Up @@ -8355,6 +8359,11 @@ export const schemaDict = {
comment: {
type: 'string',
},
reportingOnly: {
type: 'boolean',
description:
'Mute incoming reports from this subject. Only enabled when the subject is a DID',
},
durationInHours: {
type: 'integer',
description: 'Indicates how long the subject should remain muted.',
Expand All @@ -8365,6 +8374,11 @@ export const schemaDict = {
type: 'object',
description: 'Unmute action on a subject',
properties: {
reportingOnly: {
type: 'boolean',
description:
'Unmute incoming reports from this subject. Only enabled when the subject is a DID',
},
comment: {
type: 'string',
description: 'Describe reasoning behind the reversal.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export interface SubjectStatusView {
/** Sticky comment on the subject. */
comment?: string
muteUntil?: string
muteReportingUntil?: string
lastReviewedBy?: string
lastReviewedAt?: string
lastReportedAt?: string
Expand Down Expand Up @@ -310,6 +311,8 @@ export function validateModEventEscalate(v: unknown): ValidationResult {
/** Mute incoming reports on a subject */
export interface ModEventMute {
comment?: string
/** Mute incoming reports from this subject. Only enabled when the subject is a DID */
reportingOnly?: boolean
/** Indicates how long the subject should remain muted. */
durationInHours: number
[k: string]: unknown
Expand All @@ -329,6 +332,8 @@ export function validateModEventMute(v: unknown): ValidationResult {

/** Unmute action on a subject */
export interface ModEventUnmute {
/** Unmute incoming reports from this subject. Only enabled when the subject is a DID */
reportingOnly?: boolean
/** Describe reasoning behind the reversal. */
comment?: string
[k: string]: unknown
Expand Down
34 changes: 34 additions & 0 deletions packages/ozone/src/mod-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
isModEventTakedown,
isModEventEmail,
isModEventTag,
isModEventUnmute,
} from '../lexicon/types/tools/ozone/moderation/defs'
import { RepoRef, RepoBlobRef } from '../lexicon/types/com/atproto/admin/defs'
import {
Expand Down Expand Up @@ -303,6 +304,13 @@ export class ModerationService {
meta.sticky = event.sticky
}

if (
(isModEventMute(event) || isModEventUnmute(event)) &&
event.reportingOnly
) {
meta.reportingOnly = event.reportingOnly
}

if (isModEventEmail(event)) {
meta.subjectLine = event.subjectLine
if (event.content) {
Expand Down Expand Up @@ -341,6 +349,18 @@ export class ModerationService {
.returningAll()
.executeTakeFirstOrThrow()

// If reporting is muted for this reporter, we don't want to create a subject status
if (isModEventReport(event)) {
const isReportingMuted = await this.isReportingMutedForSubject(createdBy)
if (isReportingMuted) {
const subjectStatus = await this.getStatus(subject)
return {
event: modEvent,
subjectStatus,
}
}
}

const subjectStatus = await adjustModerationSubjectStatus(
this.db,
modEvent,
Expand Down Expand Up @@ -816,6 +836,20 @@ export class ModerationService {
return result ?? null
}

// This is used to check if the reporter of an incoming report is muted from reporting
// so we want to make sure this look up is as fast as possible
async isReportingMutedForSubject(did: string) {
const result = await this.db.db
.selectFrom('moderation_subject_status')
.where('did', '=', did)
.where('recordPath', '=', '')
.where('muteReportingUntil', '>', new Date().toISOString())
.select(sql`true`.as('status'))
.executeTakeFirst()

return !!result
}

async formatAndCreateLabels(
uri: string,
cid: string | null,
Expand Down
9 changes: 7 additions & 2 deletions packages/ozone/src/mod-service/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ const getSubjectStatusForModerationEvent = ({
action,
createdBy,
createdAt,
muteReportingOnly,
durationInHours,
}: {
currentStatus?: ModerationSubjectStatusRow
action: string
createdBy: string
createdAt: string
muteReportingOnly?: boolean
durationInHours: number | null
}): Partial<ModerationSubjectStatusRow> => {
const defaultReviewState = currentStatus
Expand Down Expand Up @@ -60,7 +62,9 @@ const getSubjectStatusForModerationEvent = ({
case 'tools.ozone.moderation.defs#modEventUnmute':
return {
lastReviewedBy: createdBy,
muteUntil: null,
...(muteReportingOnly
? { muteReportingUntil: null }
: { muteUntil: null }),
// It's not likely to receive an unmute event that does not already have a status row
// but if it does happen, default to unnecessary
reviewState: defaultReviewState,
Expand All @@ -81,7 +85,7 @@ const getSubjectStatusForModerationEvent = ({
lastReviewedBy: createdBy,
lastReviewedAt: createdAt,
// By default, mute for 24hrs
muteUntil: new Date(
[muteReportingOnly ? 'muteReportingUntil' : 'muteUntil']: new Date(
Date.now() + (durationInHours || 24) * HOUR,
).toISOString(),
// It's not likely to receive a mute event on a subject that does not already have a status row
Expand Down Expand Up @@ -149,6 +153,7 @@ export const adjustModerationSubjectStatus = async (
action,
createdBy,
createdAt,
muteReportingOnly: !!meta?.reportingOnly,
durationInHours: moderationEvent.durationInHours,
})

Expand Down
1 change: 1 addition & 0 deletions packages/ozone/src/mod-service/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ export class ModerationViews {
lastReportedAt: status.lastReportedAt ?? undefined,
lastAppealedAt: status.lastAppealedAt ?? undefined,
muteUntil: status.muteUntil ?? undefined,
muteReportingUntil: status.muteReportingUntil ?? undefined,
suspendUntil: status.suspendUntil ?? undefined,
takendown: status.takendown ?? undefined,
appealed: status.appealed ?? undefined,
Expand Down
5 changes: 1 addition & 4 deletions packages/ozone/src/sequencer/outbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ export class Outbox {
cutoverBuffer: LabelsEvt[]
outBuffer: AsyncBuffer<LabelsEvt>

constructor(
public sequencer: Sequencer,
opts: Partial<OutboxOpts> = {},
) {
constructor(public sequencer: Sequencer, opts: Partial<OutboxOpts> = {}) {
const { maxBufferSize = 500 } = opts
this.cutoverBuffer = []
this.outBuffer = new AsyncBuffer<LabelsEvt>(maxBufferSize)
Expand Down
5 changes: 1 addition & 4 deletions packages/ozone/src/sequencer/sequencer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ export class Sequencer extends (EventEmitter as new () => SequencerEmitter) {
queued = false
conn: PoolClient | undefined

constructor(
public modSrvc: ModerationService,
public lastSeen = 0,
) {
constructor(public modSrvc: ModerationService, public lastSeen = 0) {
super()
this.db = modSrvc.db
// note: this does not err when surpassed, just prints a warning to stderr
Expand Down

0 comments on commit f603e05

Please sign in to comment.