@atproto/ozone 0.1.149 → 0.1.151
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 +176 -0
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +88 -0
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/profile.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/actor/profile.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/profile.js +9 -7
- package/dist/lexicon/types/app/bsky/actor/profile.js.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/status.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/actor/status.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/status.js +9 -7
- package/dist/lexicon/types/app/bsky/actor/status.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/generator.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/feed/generator.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/generator.js +9 -7
- package/dist/lexicon/types/app/bsky/feed/generator.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/like.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/feed/like.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/like.js +9 -7
- package/dist/lexicon/types/app/bsky/feed/like.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/post.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/feed/post.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/post.js +9 -7
- package/dist/lexicon/types/app/bsky/feed/post.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/postgate.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/feed/postgate.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/postgate.js +9 -7
- package/dist/lexicon/types/app/bsky/feed/postgate.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/repost.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/feed/repost.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/repost.js +9 -7
- package/dist/lexicon/types/app/bsky/feed/repost.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/threadgate.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/feed/threadgate.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/threadgate.js +9 -7
- package/dist/lexicon/types/app/bsky/feed/threadgate.js.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/block.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/graph/block.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/block.js +9 -7
- package/dist/lexicon/types/app/bsky/graph/block.js.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/follow.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/graph/follow.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/follow.js +9 -7
- package/dist/lexicon/types/app/bsky/graph/follow.js.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/list.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/graph/list.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/list.js +9 -7
- package/dist/lexicon/types/app/bsky/graph/list.js.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/listblock.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/graph/listblock.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/listblock.js +9 -7
- package/dist/lexicon/types/app/bsky/graph/listblock.js.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/listitem.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/graph/listitem.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/listitem.js +9 -7
- package/dist/lexicon/types/app/bsky/graph/listitem.js.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/starterpack.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/graph/starterpack.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/starterpack.js +9 -7
- package/dist/lexicon/types/app/bsky/graph/starterpack.js.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/verification.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/graph/verification.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/verification.js +9 -7
- package/dist/lexicon/types/app/bsky/graph/verification.js.map +1 -1
- package/dist/lexicon/types/app/bsky/labeler/service.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/labeler/service.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/labeler/service.js +9 -7
- package/dist/lexicon/types/app/bsky/labeler/service.js.map +1 -1
- package/dist/lexicon/types/app/bsky/notification/declaration.d.ts +4 -3
- package/dist/lexicon/types/app/bsky/notification/declaration.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/notification/declaration.js +9 -7
- package/dist/lexicon/types/app/bsky/notification/declaration.js.map +1 -1
- package/dist/lexicon/types/chat/bsky/actor/declaration.d.ts +4 -3
- package/dist/lexicon/types/chat/bsky/actor/declaration.d.ts.map +1 -1
- package/dist/lexicon/types/chat/bsky/actor/declaration.js +9 -7
- package/dist/lexicon/types/chat/bsky/actor/declaration.js.map +1 -1
- package/dist/lexicon/types/com/atproto/lexicon/schema.d.ts +4 -3
- package/dist/lexicon/types/com/atproto/lexicon/schema.d.ts.map +1 -1
- package/dist/lexicon/types/com/atproto/lexicon/schema.js +9 -7
- package/dist/lexicon/types/com/atproto/lexicon/schema.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 +58 -5
- 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 +4 -4
- 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 +103 -0
- package/src/lexicon/types/app/bsky/actor/profile.ts +12 -6
- package/src/lexicon/types/app/bsky/actor/status.ts +12 -6
- package/src/lexicon/types/app/bsky/feed/generator.ts +12 -6
- package/src/lexicon/types/app/bsky/feed/like.ts +12 -6
- package/src/lexicon/types/app/bsky/feed/post.ts +12 -6
- package/src/lexicon/types/app/bsky/feed/postgate.ts +12 -6
- package/src/lexicon/types/app/bsky/feed/repost.ts +12 -6
- package/src/lexicon/types/app/bsky/feed/threadgate.ts +12 -6
- package/src/lexicon/types/app/bsky/graph/block.ts +12 -6
- package/src/lexicon/types/app/bsky/graph/follow.ts +12 -6
- package/src/lexicon/types/app/bsky/graph/list.ts +12 -6
- package/src/lexicon/types/app/bsky/graph/listblock.ts +12 -6
- package/src/lexicon/types/app/bsky/graph/listitem.ts +12 -6
- package/src/lexicon/types/app/bsky/graph/starterpack.ts +12 -6
- package/src/lexicon/types/app/bsky/graph/verification.ts +12 -6
- package/src/lexicon/types/app/bsky/labeler/service.ts +12 -6
- package/src/lexicon/types/app/bsky/notification/declaration.ts +12 -6
- package/src/lexicon/types/chat/bsky/actor/declaration.ts +12 -6
- package/src/lexicon/types/com/atproto/lexicon/schema.ts +12 -6
- 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 +69 -2
- 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
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',
|
|
@@ -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
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Selectable } from 'kysely'
|
|
2
2
|
import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
3
3
|
import { Setting } from '../db/schema/setting'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
PolicyListSettingKey,
|
|
6
|
+
ProtectedTagSettingKey,
|
|
7
|
+
SeverityLevelSettingKey,
|
|
8
|
+
} from './constants'
|
|
5
9
|
|
|
6
10
|
export const settingValidators = new Map<
|
|
7
11
|
string,
|
|
@@ -9,6 +13,21 @@ export const settingValidators = new Map<
|
|
|
9
13
|
>([
|
|
10
14
|
[
|
|
11
15
|
ProtectedTagSettingKey,
|
|
16
|
+
/*
|
|
17
|
+
* Example configuration:
|
|
18
|
+
* {
|
|
19
|
+
* "sensitive-tag": {
|
|
20
|
+
* "roles": ["tools.ozone.team.defs#roleAdmin", "tools.ozone.team.defs#roleModerator"],
|
|
21
|
+
* "moderators": ["did:plc:example1", "did:plc:example2"]
|
|
22
|
+
* },
|
|
23
|
+
* "high-risk-tag": {
|
|
24
|
+
* "roles": ["tools.ozone.team.defs#roleAdmin"]
|
|
25
|
+
* },
|
|
26
|
+
* "admin-only-tag": {
|
|
27
|
+
* "moderators": ["did:plc:admin1"]
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
*/
|
|
12
31
|
async (setting: Partial<Selectable<Setting>>) => {
|
|
13
32
|
if (setting.managerRole !== 'tools.ozone.team.defs#roleAdmin') {
|
|
14
33
|
throw new InvalidRequestError(
|
|
@@ -60,6 +79,61 @@ export const settingValidators = new Map<
|
|
|
60
79
|
],
|
|
61
80
|
[
|
|
62
81
|
PolicyListSettingKey,
|
|
82
|
+
/*
|
|
83
|
+
* Example configuration:
|
|
84
|
+
* {
|
|
85
|
+
* "harassment": {
|
|
86
|
+
* "name": "Anti-Harassment",
|
|
87
|
+
* "description": "Content that harasses, intimidates, or bullies users",
|
|
88
|
+
* "severityLevels": {
|
|
89
|
+
* "sev-1": {
|
|
90
|
+
* "description": "Minor harassment",
|
|
91
|
+
* "isDefault": true
|
|
92
|
+
* },
|
|
93
|
+
* "sev-2": {
|
|
94
|
+
* "description": "Moderate harassment",
|
|
95
|
+
* "isDefault": false
|
|
96
|
+
* },
|
|
97
|
+
* "sev-4": {
|
|
98
|
+
* "description": "Severe harassment",
|
|
99
|
+
* "isDefault": false
|
|
100
|
+
* }
|
|
101
|
+
* }
|
|
102
|
+
* },
|
|
103
|
+
* "death-threats": {
|
|
104
|
+
* "name": "Death Threats",
|
|
105
|
+
* "description": "Threats of violence or death against individuals",
|
|
106
|
+
* "severityLevels": {
|
|
107
|
+
* "death-threat": {
|
|
108
|
+
* "description": "Death threat violation",
|
|
109
|
+
* "isDefault": true
|
|
110
|
+
* }
|
|
111
|
+
* }
|
|
112
|
+
* },
|
|
113
|
+
* "spam": {
|
|
114
|
+
* "name": "Spam",
|
|
115
|
+
* "description": "Unsolicited or repetitive content",
|
|
116
|
+
* "severityLevels": {
|
|
117
|
+
* "sev-0": {
|
|
118
|
+
* "description": "Minor spam",
|
|
119
|
+
* "isDefault": false
|
|
120
|
+
* },
|
|
121
|
+
* "sev-1": {
|
|
122
|
+
* "description": "Moderate spam",
|
|
123
|
+
* "isDefault": true
|
|
124
|
+
* },
|
|
125
|
+
* "sev-2": {
|
|
126
|
+
* "description": "Severe spam",
|
|
127
|
+
* "isDefault": false
|
|
128
|
+
* }
|
|
129
|
+
* }
|
|
130
|
+
* },
|
|
131
|
+
* "minimal-policy": {
|
|
132
|
+
* "name": "Basic Policy",
|
|
133
|
+
* "description": "Simple policy without severity levels"
|
|
134
|
+
* }
|
|
135
|
+
* }
|
|
136
|
+
*/
|
|
63
137
|
async (setting: Partial<Selectable<Setting>>) => {
|
|
64
138
|
if (setting.managerRole !== 'tools.ozone.team.defs#roleAdmin') {
|
|
65
139
|
throw new InvalidRequestError(
|
|
@@ -82,6 +156,162 @@ export const settingValidators = new Map<
|
|
|
82
156
|
`Must define a name and description for policy ${key}`,
|
|
83
157
|
)
|
|
84
158
|
}
|
|
159
|
+
|
|
160
|
+
if (val['severityLevels'] !== undefined) {
|
|
161
|
+
if (typeof val['severityLevels'] !== 'object') {
|
|
162
|
+
throw new InvalidRequestError(
|
|
163
|
+
`Severity levels must be an object for policy ${key}`,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let hasDefault = false
|
|
168
|
+
for (const [severityKey, severityVal] of Object.entries(
|
|
169
|
+
val['severityLevels'],
|
|
170
|
+
)) {
|
|
171
|
+
if (!severityVal || typeof severityVal !== 'object') {
|
|
172
|
+
throw new InvalidRequestError(
|
|
173
|
+
`Invalid configuration for severity level ${severityKey} in policy ${key}`,
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (
|
|
178
|
+
severityVal['description'] !== undefined &&
|
|
179
|
+
typeof severityVal['description'] !== 'string'
|
|
180
|
+
) {
|
|
181
|
+
throw new InvalidRequestError(
|
|
182
|
+
`Description must be a string for severity level ${severityKey} in policy ${key}`,
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (severityVal['isDefault'] !== undefined) {
|
|
187
|
+
if (typeof severityVal['isDefault'] !== 'boolean') {
|
|
188
|
+
throw new InvalidRequestError(
|
|
189
|
+
`isDefault must be a boolean for severity level ${severityKey} in policy ${key}`,
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
if (severityVal['isDefault']) {
|
|
193
|
+
if (hasDefault) {
|
|
194
|
+
throw new InvalidRequestError(
|
|
195
|
+
`Only one severity level can be the default for policy ${key}`,
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
hasDefault = true
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
[
|
|
207
|
+
SeverityLevelSettingKey,
|
|
208
|
+
/*
|
|
209
|
+
* Example configuration:
|
|
210
|
+
* {
|
|
211
|
+
* "sev-0": {
|
|
212
|
+
* "strikeCount": 0
|
|
213
|
+
* },
|
|
214
|
+
* "sev-1": {
|
|
215
|
+
* "strikeCount": 1,
|
|
216
|
+
* "strikeOnOccurrence": 2
|
|
217
|
+
* },
|
|
218
|
+
* "sev-2": {
|
|
219
|
+
* "strikeCount": 2
|
|
220
|
+
* },
|
|
221
|
+
* "sev-4": {
|
|
222
|
+
* "strikeCount": 4,
|
|
223
|
+
* "expiresInDays": 365
|
|
224
|
+
* },
|
|
225
|
+
* "sev-5": {
|
|
226
|
+
* "needsTakedown": true
|
|
227
|
+
* },
|
|
228
|
+
* "death-threat": {
|
|
229
|
+
* "strikeCount": 4,
|
|
230
|
+
* "firstOccurrenceStrikeCount": 4,
|
|
231
|
+
* },
|
|
232
|
+
* "custom-severity": {
|
|
233
|
+
* "strikeCount": 3,
|
|
234
|
+
* "strikeOnOccurrence": 1,
|
|
235
|
+
* },
|
|
236
|
+
* "escalating-severity": {
|
|
237
|
+
* "firstOccurrenceStrikeCount": 2,
|
|
238
|
+
* "repeatOccurrenceStrikeCount": 5
|
|
239
|
+
* }
|
|
240
|
+
* }
|
|
241
|
+
*/
|
|
242
|
+
async (setting: Partial<Selectable<Setting>>) => {
|
|
243
|
+
if (setting.managerRole !== 'tools.ozone.team.defs#roleAdmin') {
|
|
244
|
+
throw new InvalidRequestError(
|
|
245
|
+
'Only admins should be able to manage severity levels',
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (typeof setting.value !== 'object') {
|
|
250
|
+
throw new InvalidRequestError('Invalid value')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
for (const [key, val] of Object.entries(setting.value)) {
|
|
254
|
+
if (!val || typeof val !== 'object') {
|
|
255
|
+
throw new InvalidRequestError(
|
|
256
|
+
`Invalid configuration for severity level ${key}`,
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (val['strikeCount'] !== undefined) {
|
|
261
|
+
if (
|
|
262
|
+
typeof val['strikeCount'] !== 'number' ||
|
|
263
|
+
!Number.isInteger(val['strikeCount']) ||
|
|
264
|
+
val['strikeCount'] < 0
|
|
265
|
+
) {
|
|
266
|
+
throw new InvalidRequestError(
|
|
267
|
+
`Strike count must be a non-negative integer for severity level ${key}`,
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (val['strikeOnOccurrence'] !== undefined) {
|
|
273
|
+
if (
|
|
274
|
+
typeof val['strikeOnOccurrence'] !== 'number' ||
|
|
275
|
+
!Number.isInteger(val['strikeOnOccurrence']) ||
|
|
276
|
+
val['strikeOnOccurrence'] < 1
|
|
277
|
+
) {
|
|
278
|
+
throw new InvalidRequestError(
|
|
279
|
+
`Strike on occurrence must be a positive integer for severity level ${key}`,
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (val['needsTakedown'] !== undefined) {
|
|
285
|
+
if (typeof val['needsTakedown'] !== 'boolean') {
|
|
286
|
+
throw new InvalidRequestError(
|
|
287
|
+
`Needs takedown must be a boolean for severity level ${key}`,
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (val['expiresInDays'] !== undefined) {
|
|
293
|
+
if (
|
|
294
|
+
typeof val['expiresInDays'] !== 'number' ||
|
|
295
|
+
!Number.isInteger(val['expiresInDays']) ||
|
|
296
|
+
val['expiresInDays'] < 0
|
|
297
|
+
) {
|
|
298
|
+
throw new InvalidRequestError(
|
|
299
|
+
`Expires in days must be a non-negative integer for severity level ${key}`,
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (val['firstOccurrenceStrikeCount'] !== undefined) {
|
|
305
|
+
if (
|
|
306
|
+
typeof val['firstOccurrenceStrikeCount'] !== 'number' ||
|
|
307
|
+
!Number.isInteger(val['firstOccurrenceStrikeCount']) ||
|
|
308
|
+
val['firstOccurrenceStrikeCount'] < 0
|
|
309
|
+
) {
|
|
310
|
+
throw new InvalidRequestError(
|
|
311
|
+
`First occurrence strike count must be a non-negative integer for severity level ${key}`,
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
85
315
|
}
|
|
86
316
|
},
|
|
87
317
|
],
|