@atproto/ozone 0.1.150 → 0.1.152
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/api/moderation/queryEvents.d.ts.map +1 -1
- package/dist/api/moderation/queryEvents.js +2 -1
- package/dist/api/moderation/queryEvents.js.map +1 -1
- package/dist/context.d.ts +3 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +7 -1
- package/dist/context.js.map +1 -1
- package/dist/daemon/context.d.ts +3 -0
- package/dist/daemon/context.d.ts.map +1 -1
- package/dist/daemon/context.js +11 -1
- package/dist/daemon/context.js.map +1 -1
- package/dist/daemon/index.d.ts +1 -0
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +3 -1
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/strike-expiry-processor.d.ts +18 -0
- package/dist/daemon/strike-expiry-processor.d.ts.map +1 -0
- package/dist/daemon/strike-expiry-processor.js +111 -0
- package/dist/daemon/strike-expiry-processor.js.map +1 -0
- package/dist/db/migrations/20251008T120000000Z-add-strike-system.d.ts +4 -0
- package/dist/db/migrations/20251008T120000000Z-add-strike-system.d.ts.map +1 -0
- package/dist/db/migrations/20251008T120000000Z-add-strike-system.js +75 -0
- package/dist/db/migrations/20251008T120000000Z-add-strike-system.js.map +1 -0
- package/dist/db/migrations/index.d.ts +1 -0
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +2 -1
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/schema/account_strike.d.ts +12 -0
- package/dist/db/schema/account_strike.d.ts.map +1 -0
- package/dist/db/schema/account_strike.js +5 -0
- package/dist/db/schema/account_strike.js.map +1 -0
- package/dist/db/schema/index.d.ts +3 -1
- package/dist/db/schema/index.d.ts.map +1 -1
- package/dist/db/schema/index.js.map +1 -1
- package/dist/db/schema/job_cursor.d.ts +11 -0
- package/dist/db/schema/job_cursor.d.ts.map +1 -0
- package/dist/db/schema/job_cursor.js +5 -0
- package/dist/db/schema/job_cursor.js.map +1 -0
- package/dist/db/schema/moderation_event.d.ts +3 -0
- package/dist/db/schema/moderation_event.d.ts.map +1 -1
- package/dist/db/schema/moderation_event.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +208 -0
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +104 -0
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts +12 -0
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/defs.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/defs.d.ts +4 -0
- package/dist/lexicon/types/app/bsky/feed/defs.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/defs.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +35 -0
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.js +9 -0
- package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts +2 -0
- package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryEvents.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts +2 -0
- package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.js.map +1 -1
- package/dist/mod-service/index.d.ts +9 -3
- package/dist/mod-service/index.d.ts.map +1 -1
- package/dist/mod-service/index.js +59 -6
- package/dist/mod-service/index.js.map +1 -1
- package/dist/mod-service/status.d.ts +44 -2
- package/dist/mod-service/status.d.ts.map +1 -1
- package/dist/mod-service/status.js +7 -0
- package/dist/mod-service/status.js.map +1 -1
- package/dist/mod-service/strike.d.ts +19 -0
- package/dist/mod-service/strike.d.ts.map +1 -0
- package/dist/mod-service/strike.js +86 -0
- package/dist/mod-service/strike.js.map +1 -0
- package/dist/mod-service/types.d.ts +4 -0
- package/dist/mod-service/types.d.ts.map +1 -1
- package/dist/mod-service/types.js.map +1 -1
- package/dist/mod-service/views.d.ts.map +1 -1
- package/dist/mod-service/views.js +20 -4
- package/dist/mod-service/views.js.map +1 -1
- package/dist/setting/constants.d.ts +1 -0
- package/dist/setting/constants.d.ts.map +1 -1
- package/dist/setting/constants.js +2 -1
- package/dist/setting/constants.js.map +1 -1
- package/dist/setting/validators.d.ts.map +1 -1
- package/dist/setting/validators.js +179 -0
- package/dist/setting/validators.js.map +1 -1
- package/package.json +3 -3
- package/src/api/moderation/queryEvents.ts +2 -0
- package/src/context.ts +20 -11
- package/src/daemon/context.ts +15 -1
- package/src/daemon/index.ts +1 -0
- package/src/daemon/strike-expiry-processor.ts +111 -0
- package/src/db/migrations/20251008T120000000Z-add-strike-system.ts +87 -0
- package/src/db/migrations/index.ts +1 -0
- package/src/db/schema/account_strike.ts +13 -0
- package/src/db/schema/index.ts +4 -0
- package/src/db/schema/job_cursor.ts +13 -0
- package/src/db/schema/moderation_event.ts +3 -0
- package/src/lexicon/lexicons.ts +119 -0
- package/src/lexicon/types/app/bsky/actor/defs.ts +6 -0
- package/src/lexicon/types/app/bsky/feed/defs.ts +2 -0
- package/src/lexicon/types/tools/ozone/moderation/defs.ts +44 -0
- package/src/lexicon/types/tools/ozone/moderation/queryEvents.ts +2 -0
- package/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts +2 -0
- package/src/mod-service/index.ts +70 -3
- package/src/mod-service/status.ts +9 -0
- package/src/mod-service/strike.ts +96 -0
- package/src/mod-service/types.ts +6 -0
- package/src/mod-service/views.ts +25 -4
- package/src/setting/constants.ts +1 -0
- package/src/setting/validators.ts +231 -1
- package/tests/__snapshots__/account-strikes.test.ts.snap +159 -0
- package/tests/account-strikes.test.ts +184 -0
- package/tests/query-labels.test.ts +1 -0
- package/tests/strike-expiry-processor.test.ts +299 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.tests.tsbuildinfo +1 -1
|
@@ -157,6 +157,7 @@ export interface SubjectStatusView {
|
|
|
157
157
|
tags?: string[]
|
|
158
158
|
accountStats?: AccountStats
|
|
159
159
|
recordsStats?: RecordsStats
|
|
160
|
+
accountStrike?: AccountStrike
|
|
160
161
|
/** Current age assurance state of the subject. */
|
|
161
162
|
ageAssuranceState?:
|
|
162
163
|
| 'pending'
|
|
@@ -256,6 +257,29 @@ export function validateRecordsStats<V>(v: V) {
|
|
|
256
257
|
return validate<RecordsStats & V>(v, id, hashRecordsStats)
|
|
257
258
|
}
|
|
258
259
|
|
|
260
|
+
/** Strike information for an account */
|
|
261
|
+
export interface AccountStrike {
|
|
262
|
+
$type?: 'tools.ozone.moderation.defs#accountStrike'
|
|
263
|
+
/** Current number of active strikes (excluding expired strikes) */
|
|
264
|
+
activeStrikeCount?: number
|
|
265
|
+
/** Total number of strikes ever received (including expired strikes) */
|
|
266
|
+
totalStrikeCount?: number
|
|
267
|
+
/** Timestamp of the first strike received */
|
|
268
|
+
firstStrikeAt?: string
|
|
269
|
+
/** Timestamp of the most recent strike received */
|
|
270
|
+
lastStrikeAt?: string
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const hashAccountStrike = 'accountStrike'
|
|
274
|
+
|
|
275
|
+
export function isAccountStrike<V>(v: V) {
|
|
276
|
+
return is$typed(v, id, hashAccountStrike)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function validateAccountStrike<V>(v: V) {
|
|
280
|
+
return validate<AccountStrike & V>(v, id, hashAccountStrike)
|
|
281
|
+
}
|
|
282
|
+
|
|
259
283
|
export type SubjectReviewState =
|
|
260
284
|
| 'lex:tools.ozone.moderation.defs#reviewOpen'
|
|
261
285
|
| 'lex:tools.ozone.moderation.defs#reviewEscalated'
|
|
@@ -282,6 +306,12 @@ export interface ModEventTakedown {
|
|
|
282
306
|
acknowledgeAccountSubjects?: boolean
|
|
283
307
|
/** Names/Keywords of the policies that drove the decision. */
|
|
284
308
|
policies?: string[]
|
|
309
|
+
/** Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.). */
|
|
310
|
+
severityLevel?: string
|
|
311
|
+
/** Number of strikes to assign to the user for this violation. */
|
|
312
|
+
strikeCount?: number
|
|
313
|
+
/** When the strike should expire. If not provided, the strike never expires. */
|
|
314
|
+
strikeExpiresAt?: string
|
|
285
315
|
}
|
|
286
316
|
|
|
287
317
|
const hashModEventTakedown = 'modEventTakedown'
|
|
@@ -299,6 +329,12 @@ export interface ModEventReverseTakedown {
|
|
|
299
329
|
$type?: 'tools.ozone.moderation.defs#modEventReverseTakedown'
|
|
300
330
|
/** Describe reasoning behind the reversal. */
|
|
301
331
|
comment?: string
|
|
332
|
+
/** Names/Keywords of the policy infraction for which takedown is being reversed. */
|
|
333
|
+
policies?: string[]
|
|
334
|
+
/** Severity level of the violation. Usually set from the last policy infraction's severity. */
|
|
335
|
+
severityLevel?: string
|
|
336
|
+
/** Number of strikes to subtract from the user's strike count. Usually set from the last policy infraction's severity. */
|
|
337
|
+
strikeCount?: number
|
|
302
338
|
}
|
|
303
339
|
|
|
304
340
|
const hashModEventReverseTakedown = 'modEventReverseTakedown'
|
|
@@ -590,6 +626,14 @@ export interface ModEventEmail {
|
|
|
590
626
|
content?: string
|
|
591
627
|
/** Additional comment about the outgoing comm. */
|
|
592
628
|
comment?: string
|
|
629
|
+
/** Names/Keywords of the policies that necessitated the email. */
|
|
630
|
+
policies?: string[]
|
|
631
|
+
/** Severity level of the violation. Normally 'sev-1' that adds strike on repeat offense */
|
|
632
|
+
severityLevel?: string
|
|
633
|
+
/** Number of strikes to assign to the user for this violation. Normally 0 as an indicator of a warning and only added as a strike on a repeat offense. */
|
|
634
|
+
strikeCount?: number
|
|
635
|
+
/** When the strike should expire. If not provided, the strike never expires. */
|
|
636
|
+
strikeExpiresAt?: string
|
|
593
637
|
}
|
|
594
638
|
|
|
595
639
|
const hashModEventEmail = 'modEventEmail'
|
|
@@ -82,6 +82,8 @@ export type QueryParams = {
|
|
|
82
82
|
minTakendownRecordsCount?: number
|
|
83
83
|
/** If specified, only subjects that have priority score value above the given value will be returned. */
|
|
84
84
|
minPriorityScore?: number
|
|
85
|
+
/** If specified, only subjects that belong to an account that has at least this many active strikes will be returned. */
|
|
86
|
+
minStrikeCount?: number
|
|
85
87
|
/** If specified, only subjects with the given age assurance state will be returned. */
|
|
86
88
|
ageAssuranceState?:
|
|
87
89
|
| 'pending'
|
package/src/mod-service/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
isModEventMute,
|
|
37
37
|
isModEventPriorityScore,
|
|
38
38
|
isModEventReport,
|
|
39
|
+
isModEventReverseTakedown,
|
|
39
40
|
isModEventTag,
|
|
40
41
|
isModEventTakedown,
|
|
41
42
|
isRecordEvent,
|
|
@@ -49,6 +50,7 @@ import {
|
|
|
49
50
|
getStatusIdentifierFromSubject,
|
|
50
51
|
moderationSubjectStatusQueryBuilder,
|
|
51
52
|
} from './status'
|
|
53
|
+
import { StrikeService, StrikeServiceCreator } from './strike'
|
|
52
54
|
import {
|
|
53
55
|
ModSubject,
|
|
54
56
|
RecordSubject,
|
|
@@ -89,6 +91,7 @@ export class ModerationService {
|
|
|
89
91
|
aud: string,
|
|
90
92
|
method: string,
|
|
91
93
|
) => Promise<AuthHeaders>,
|
|
94
|
+
public strikeService: StrikeService,
|
|
92
95
|
public imgInvalidator?: ImageInvalidator,
|
|
93
96
|
) {}
|
|
94
97
|
|
|
@@ -101,10 +104,12 @@ export class ModerationService {
|
|
|
101
104
|
eventPusher: EventPusher,
|
|
102
105
|
appviewAgent: AtpAgent,
|
|
103
106
|
createAuthHeaders: (aud: string, method: string) => Promise<AuthHeaders>,
|
|
107
|
+
strikeServiceCreator: StrikeServiceCreator,
|
|
104
108
|
imgInvalidator?: ImageInvalidator,
|
|
105
109
|
) {
|
|
106
|
-
return (db: Database) =>
|
|
107
|
-
|
|
110
|
+
return (db: Database) => {
|
|
111
|
+
const strikeService = strikeServiceCreator(db)
|
|
112
|
+
return new ModerationService(
|
|
108
113
|
db,
|
|
109
114
|
signingKey,
|
|
110
115
|
signingKeyId,
|
|
@@ -114,8 +119,10 @@ export class ModerationService {
|
|
|
114
119
|
eventPusher,
|
|
115
120
|
appviewAgent,
|
|
116
121
|
createAuthHeaders,
|
|
122
|
+
strikeService,
|
|
117
123
|
imgInvalidator,
|
|
118
124
|
)
|
|
125
|
+
}
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
views = new ModerationViews(
|
|
@@ -190,6 +197,7 @@ export class ModerationService {
|
|
|
190
197
|
modTool?: string[]
|
|
191
198
|
ageAssuranceState?: string
|
|
192
199
|
batchId?: string
|
|
200
|
+
withStrike?: boolean
|
|
193
201
|
}): Promise<{ cursor?: string; events: ModerationEventRow[] }> {
|
|
194
202
|
const {
|
|
195
203
|
subject,
|
|
@@ -214,6 +222,7 @@ export class ModerationService {
|
|
|
214
222
|
modTool,
|
|
215
223
|
ageAssuranceState,
|
|
216
224
|
batchId,
|
|
225
|
+
withStrike,
|
|
217
226
|
} = opts
|
|
218
227
|
const { ref } = this.db.db.dynamic
|
|
219
228
|
let builder = this.db.db.selectFrom('moderation_event').selectAll()
|
|
@@ -333,6 +342,10 @@ export class ModerationService {
|
|
|
333
342
|
.where(sql`meta->>'status'`, '=', ageAssuranceState)
|
|
334
343
|
}
|
|
335
344
|
|
|
345
|
+
if (withStrike !== undefined) {
|
|
346
|
+
builder = builder.where('strikeCount', 'is not', null)
|
|
347
|
+
}
|
|
348
|
+
|
|
336
349
|
const keyset = new TimeIdKeyset(
|
|
337
350
|
ref(`moderation_event.createdAt`),
|
|
338
351
|
ref('moderation_event.id'),
|
|
@@ -484,6 +497,9 @@ export class ModerationService {
|
|
|
484
497
|
if (event.content) {
|
|
485
498
|
meta.content = event.content
|
|
486
499
|
}
|
|
500
|
+
if (event.policies?.length) {
|
|
501
|
+
meta.policies = event.policies.join(',')
|
|
502
|
+
}
|
|
487
503
|
}
|
|
488
504
|
|
|
489
505
|
if (isAccountEvent(event)) {
|
|
@@ -566,6 +582,32 @@ export class ModerationService {
|
|
|
566
582
|
|
|
567
583
|
const subjectInfo = subject.info()
|
|
568
584
|
|
|
585
|
+
// Store severityLevel, strikeCount, and strikeExpiresAt if provided
|
|
586
|
+
// These values should be calculated by the client based on configuration
|
|
587
|
+
// processNewEvent will update the account_strike table with the new strike count
|
|
588
|
+
let severityLevel: string | null = null
|
|
589
|
+
let strikeCount: number | null = null
|
|
590
|
+
let strikeExpiresAt: string | null = null
|
|
591
|
+
|
|
592
|
+
if (
|
|
593
|
+
isModEventTakedown(event) ||
|
|
594
|
+
isModEventEmail(event) ||
|
|
595
|
+
isModEventReverseTakedown(event)
|
|
596
|
+
) {
|
|
597
|
+
// Store severityLevel if provided (for display/tracking)
|
|
598
|
+
if (event.severityLevel) {
|
|
599
|
+
severityLevel = event.severityLevel
|
|
600
|
+
}
|
|
601
|
+
// Store explicit strikeCount if provided
|
|
602
|
+
if (event.strikeCount !== undefined) {
|
|
603
|
+
strikeCount = event.strikeCount
|
|
604
|
+
}
|
|
605
|
+
// Store strikeExpiresAt if provided by client
|
|
606
|
+
if ('strikeExpiresAt' in event && event.strikeExpiresAt) {
|
|
607
|
+
strikeExpiresAt = event.strikeExpiresAt
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
569
611
|
const modEvent = await this.db.db
|
|
570
612
|
.insertInto('moderation_event')
|
|
571
613
|
.values({
|
|
@@ -599,6 +641,9 @@ export class ModerationService {
|
|
|
599
641
|
subjectMessageId: subjectInfo.subjectMessageId,
|
|
600
642
|
modTool: modTool ? jsonb(modTool) : null,
|
|
601
643
|
externalId: externalId ?? null,
|
|
644
|
+
severityLevel,
|
|
645
|
+
strikeCount,
|
|
646
|
+
strikeExpiresAt,
|
|
602
647
|
})
|
|
603
648
|
.returningAll()
|
|
604
649
|
.executeTakeFirstOrThrow()
|
|
@@ -609,6 +654,19 @@ export class ModerationService {
|
|
|
609
654
|
subject.blobCids,
|
|
610
655
|
)
|
|
611
656
|
|
|
657
|
+
// Updates are only needed if strikeCount is numeric (in some cases even 0)
|
|
658
|
+
if (modEvent.strikeCount !== null) {
|
|
659
|
+
try {
|
|
660
|
+
await this.strikeService.updateSubjectStrikeCount(modEvent.subjectDid)
|
|
661
|
+
} catch (error) {
|
|
662
|
+
// Log error but don't fail the entire operation to ensure that events are logged even if updating strike count fails
|
|
663
|
+
log.error(
|
|
664
|
+
{ err: error, modEventId: modEvent.id },
|
|
665
|
+
'Error processing strikes for moderation event',
|
|
666
|
+
)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
612
670
|
return { event: modEvent, subjectStatus }
|
|
613
671
|
}
|
|
614
672
|
|
|
@@ -959,6 +1017,7 @@ export class ModerationService {
|
|
|
959
1017
|
minReportedRecordsCount,
|
|
960
1018
|
minTakendownRecordsCount,
|
|
961
1019
|
minPriorityScore,
|
|
1020
|
+
minStrikeCount,
|
|
962
1021
|
ageAssuranceState,
|
|
963
1022
|
}: QueryStatusParams): Promise<{
|
|
964
1023
|
statuses: ModerationSubjectStatusRowWithHandle[]
|
|
@@ -1224,6 +1283,14 @@ export class ModerationService {
|
|
|
1224
1283
|
)
|
|
1225
1284
|
}
|
|
1226
1285
|
|
|
1286
|
+
if (minStrikeCount != null && minStrikeCount >= 0) {
|
|
1287
|
+
builder = builder.where(
|
|
1288
|
+
'account_strike.activeStrikeCount',
|
|
1289
|
+
'>=',
|
|
1290
|
+
minStrikeCount,
|
|
1291
|
+
)
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1227
1294
|
if (ageAssuranceState) {
|
|
1228
1295
|
builder = builder.where(
|
|
1229
1296
|
'moderation_subject_status.ageAssuranceState',
|
|
@@ -1329,7 +1396,6 @@ export class ModerationService {
|
|
|
1329
1396
|
)
|
|
1330
1397
|
const dbVals = signedLabels.map((l) => formatLabelRow(l, this.signingKeyId))
|
|
1331
1398
|
const { ref } = this.db.db.dynamic
|
|
1332
|
-
await sql`notify ${ref(LabelChannel)}`.execute(this.db.db)
|
|
1333
1399
|
const excluded = (col: string) => ref(`excluded.${col}`)
|
|
1334
1400
|
const res = await this.db.db
|
|
1335
1401
|
.insertInto('label')
|
|
@@ -1346,6 +1412,7 @@ export class ModerationService {
|
|
|
1346
1412
|
)
|
|
1347
1413
|
.returningAll()
|
|
1348
1414
|
.execute()
|
|
1415
|
+
await sql`notify ${ref(LabelChannel)}`.execute(this.db.db)
|
|
1349
1416
|
return res.map((row) => formatLabel(row))
|
|
1350
1417
|
}
|
|
1351
1418
|
|
|
@@ -258,6 +258,15 @@ export const moderationSubjectStatusQueryBuilder = (db: DatabaseSchema) => {
|
|
|
258
258
|
'account_record_status_stats.processedCount',
|
|
259
259
|
'account_record_status_stats.takendownCount',
|
|
260
260
|
])
|
|
261
|
+
.leftJoin('account_strike', (join) =>
|
|
262
|
+
join.onRef('moderation_subject_status.did', '=', 'account_strike.did'),
|
|
263
|
+
)
|
|
264
|
+
.select([
|
|
265
|
+
'account_strike.activeStrikeCount as strikeCount',
|
|
266
|
+
'account_strike.totalStrikeCount',
|
|
267
|
+
'account_strike.firstStrikeAt',
|
|
268
|
+
'account_strike.lastStrikeAt',
|
|
269
|
+
])
|
|
261
270
|
}
|
|
262
271
|
|
|
263
272
|
// Based on a given moderation action event, this function will update the moderation status of the subject
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Database } from '../db'
|
|
2
|
+
|
|
3
|
+
export type StrikeServiceCreator = (db: Database) => StrikeService
|
|
4
|
+
|
|
5
|
+
export class StrikeService {
|
|
6
|
+
constructor(private db: Database) {}
|
|
7
|
+
|
|
8
|
+
static creator() {
|
|
9
|
+
return (db: Database) => {
|
|
10
|
+
return new StrikeService(db)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Update the strike count in account_strike table
|
|
16
|
+
*/
|
|
17
|
+
async updateSubjectStrikeCount(subjectDid: string): Promise<void> {
|
|
18
|
+
const now = new Date().toISOString()
|
|
19
|
+
|
|
20
|
+
// This should not incur too many rows since we tend to do permanent takedown on relatively low strike count
|
|
21
|
+
// and we have a very specific index to support this query
|
|
22
|
+
const events = await this.db.db
|
|
23
|
+
.selectFrom('moderation_event')
|
|
24
|
+
.where('subjectDid', '=', subjectDid)
|
|
25
|
+
.where('strikeCount', '<>', 0)
|
|
26
|
+
.select(['strikeCount', 'strikeExpiresAt', 'createdAt'])
|
|
27
|
+
.orderBy('createdAt', 'asc')
|
|
28
|
+
.execute()
|
|
29
|
+
|
|
30
|
+
if (!events.length) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let activeStrikeCount = 0
|
|
35
|
+
let totalStrikeCount = 0
|
|
36
|
+
|
|
37
|
+
const firstStrikeAt = events[0].createdAt
|
|
38
|
+
const lastStrikeAt = events[events.length - 1].createdAt
|
|
39
|
+
|
|
40
|
+
for (const event of events) {
|
|
41
|
+
const strikeCount = event.strikeCount || 0
|
|
42
|
+
totalStrikeCount += strikeCount
|
|
43
|
+
|
|
44
|
+
// Count as active if not expired
|
|
45
|
+
const isActive =
|
|
46
|
+
event.strikeExpiresAt === null || event.strikeExpiresAt > now
|
|
47
|
+
if (isActive) {
|
|
48
|
+
activeStrikeCount += strikeCount
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await this.db.db
|
|
53
|
+
.insertInto('account_strike')
|
|
54
|
+
.values({
|
|
55
|
+
did: subjectDid,
|
|
56
|
+
activeStrikeCount,
|
|
57
|
+
totalStrikeCount,
|
|
58
|
+
firstStrikeAt,
|
|
59
|
+
lastStrikeAt,
|
|
60
|
+
})
|
|
61
|
+
.onConflict((oc) =>
|
|
62
|
+
oc.column('did').doUpdateSet({
|
|
63
|
+
activeStrikeCount,
|
|
64
|
+
totalStrikeCount,
|
|
65
|
+
firstStrikeAt,
|
|
66
|
+
lastStrikeAt,
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
.execute()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get distinct subjects with expired strikes since a given timestamp
|
|
74
|
+
* Used by the strike expiry processor to find accounts that need strike count updates
|
|
75
|
+
*/
|
|
76
|
+
async getExpiredStrikeSubjects(
|
|
77
|
+
afterTimestamp?: string,
|
|
78
|
+
): Promise<Array<{ subjectDid: string }>> {
|
|
79
|
+
const now = new Date().toISOString()
|
|
80
|
+
|
|
81
|
+
let query = this.db.db
|
|
82
|
+
.selectFrom('moderation_event')
|
|
83
|
+
.where('strikeExpiresAt', 'is not', null)
|
|
84
|
+
.where('strikeExpiresAt', '<=', now)
|
|
85
|
+
.where('strikeCount', '<>', 0)
|
|
86
|
+
.select('subjectDid')
|
|
87
|
+
.distinct()
|
|
88
|
+
|
|
89
|
+
// Only process strikes that expired since the last run
|
|
90
|
+
if (afterTimestamp) {
|
|
91
|
+
query = query.where('strikeExpiresAt', '>=', afterTimestamp)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return await query.execute()
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/mod-service/types.ts
CHANGED
|
@@ -37,6 +37,12 @@ export type ModerationSubjectStatusRowWithStats = ModerationSubjectStatusRow & {
|
|
|
37
37
|
pendingCount: number | null
|
|
38
38
|
processedCount: number | null
|
|
39
39
|
takendownCount: number | null
|
|
40
|
+
|
|
41
|
+
// account_strike
|
|
42
|
+
strikeCount: number | null
|
|
43
|
+
totalStrikeCount: number | null
|
|
44
|
+
firstStrikeAt: string | null
|
|
45
|
+
lastStrikeAt: string | null
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
export type ModerationSubjectStatusRowWithHandle =
|
package/src/mod-service/views.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
isModEventMuteReporter,
|
|
42
42
|
isModEventPriorityScore,
|
|
43
43
|
isModEventReport,
|
|
44
|
+
isModEventReverseTakedown,
|
|
44
45
|
isModEventTag,
|
|
45
46
|
isModEventTakedown,
|
|
46
47
|
isRecordEvent,
|
|
@@ -178,11 +179,20 @@ export class ModerationViews {
|
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
if (
|
|
181
|
-
isModEventTakedown(event)
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
isModEventTakedown(event) ||
|
|
183
|
+
isModEventEmail(event) ||
|
|
184
|
+
isModEventReverseTakedown(event)
|
|
184
185
|
) {
|
|
185
|
-
|
|
186
|
+
if (typeof meta.policies === 'string' && meta.policies.length > 0) {
|
|
187
|
+
event.policies = meta.policies.split(',')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
event.strikeCount = ifNumber(row.strikeCount)
|
|
191
|
+
event.severityLevel = ifString(row.severityLevel)
|
|
192
|
+
|
|
193
|
+
if (isModEventTakedown(event) || isModEventEmail(event)) {
|
|
194
|
+
event.strikeExpiresAt = ifString(row.strikeExpiresAt)
|
|
195
|
+
}
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
if (isModEventLabel(event)) {
|
|
@@ -745,6 +755,17 @@ export class ModerationViews {
|
|
|
745
755
|
processedCount: status.processedCount ?? undefined,
|
|
746
756
|
takendownCount: status.takendownCount ?? undefined,
|
|
747
757
|
},
|
|
758
|
+
|
|
759
|
+
accountStrike:
|
|
760
|
+
status.strikeCount !== null || status.totalStrikeCount !== null
|
|
761
|
+
? {
|
|
762
|
+
$type: 'tools.ozone.moderation.defs#accountStrike',
|
|
763
|
+
activeStrikeCount: status.strikeCount ?? undefined,
|
|
764
|
+
totalStrikeCount: status.totalStrikeCount ?? undefined,
|
|
765
|
+
firstStrikeAt: status.firstStrikeAt ?? undefined,
|
|
766
|
+
lastStrikeAt: status.lastStrikeAt ?? undefined,
|
|
767
|
+
}
|
|
768
|
+
: undefined,
|
|
748
769
|
}
|
|
749
770
|
|
|
750
771
|
if (status.recordPath !== '') {
|
package/src/setting/constants.ts
CHANGED