@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
|
@@ -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
|
],
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`account-strikes tracks strikes and exposes them through queryStatuses and queryEvents 1`] = `
|
|
4
|
+
Object {
|
|
5
|
+
"accountStats": Object {
|
|
6
|
+
"$type": "tools.ozone.moderation.defs#accountStats",
|
|
7
|
+
},
|
|
8
|
+
"accountStrike": Object {
|
|
9
|
+
"$type": "tools.ozone.moderation.defs#accountStrike",
|
|
10
|
+
"activeStrikeCount": 5,
|
|
11
|
+
"firstStrikeAt": "1970-01-01T00:00:00.000Z",
|
|
12
|
+
"lastStrikeAt": "1970-01-01T00:00:00.000Z",
|
|
13
|
+
"totalStrikeCount": 5,
|
|
14
|
+
},
|
|
15
|
+
"ageAssuranceState": "unknown",
|
|
16
|
+
"createdAt": "1970-01-01T00:00:00.000Z",
|
|
17
|
+
"hosting": Object {
|
|
18
|
+
"$type": "tools.ozone.moderation.defs#accountHosting",
|
|
19
|
+
"status": "unknown",
|
|
20
|
+
},
|
|
21
|
+
"id": 9,
|
|
22
|
+
"lastReviewedAt": "1970-01-01T00:00:00.000Z",
|
|
23
|
+
"lastReviewedBy": "user(0)",
|
|
24
|
+
"priorityScore": 0,
|
|
25
|
+
"recordsStats": Object {
|
|
26
|
+
"$type": "tools.ozone.moderation.defs#recordsStats",
|
|
27
|
+
},
|
|
28
|
+
"reviewState": "tools.ozone.moderation.defs#reviewClosed",
|
|
29
|
+
"subject": Object {
|
|
30
|
+
"$type": "com.atproto.admin.defs#repoRef",
|
|
31
|
+
"did": "user(1)",
|
|
32
|
+
},
|
|
33
|
+
"subjectBlobCids": Array [],
|
|
34
|
+
"subjectRepoHandle": "alice.test",
|
|
35
|
+
"tags": Array [
|
|
36
|
+
"lang:und",
|
|
37
|
+
],
|
|
38
|
+
"takendown": true,
|
|
39
|
+
"updatedAt": "1970-01-01T00:00:00.000Z",
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
exports[`account-strikes tracks strikes and exposes them through queryStatuses and queryEvents 2`] = `
|
|
44
|
+
Array [
|
|
45
|
+
Object {
|
|
46
|
+
"createdAt": "1970-01-01T00:00:00.000Z",
|
|
47
|
+
"createdBy": "user(0)",
|
|
48
|
+
"event": Object {
|
|
49
|
+
"$type": "tools.ozone.moderation.defs#modEventReverseTakedown",
|
|
50
|
+
"comment": "Appeal granted - reversing first takedown",
|
|
51
|
+
"severityLevel": "sev-2",
|
|
52
|
+
"strikeCount": -2,
|
|
53
|
+
},
|
|
54
|
+
"id": 11,
|
|
55
|
+
"subject": Object {
|
|
56
|
+
"$type": "com.atproto.repo.strongRef",
|
|
57
|
+
"cid": "cids(0)",
|
|
58
|
+
"uri": "record(0)",
|
|
59
|
+
},
|
|
60
|
+
"subjectBlobCids": Array [],
|
|
61
|
+
"subjectHandle": "alice.test",
|
|
62
|
+
},
|
|
63
|
+
Object {
|
|
64
|
+
"createdAt": "1970-01-01T00:00:00.000Z",
|
|
65
|
+
"createdBy": "user(0)",
|
|
66
|
+
"event": Object {
|
|
67
|
+
"$type": "tools.ozone.moderation.defs#modEventTakedown",
|
|
68
|
+
"comment": "Warning only",
|
|
69
|
+
"severityLevel": "sev-0",
|
|
70
|
+
"strikeCount": 0,
|
|
71
|
+
},
|
|
72
|
+
"id": 9,
|
|
73
|
+
"subject": Object {
|
|
74
|
+
"$type": "com.atproto.admin.defs#repoRef",
|
|
75
|
+
"did": "user(1)",
|
|
76
|
+
},
|
|
77
|
+
"subjectBlobCids": Array [],
|
|
78
|
+
"subjectHandle": "alice.test",
|
|
79
|
+
},
|
|
80
|
+
Object {
|
|
81
|
+
"createdAt": "1970-01-01T00:00:00.000Z",
|
|
82
|
+
"createdBy": "user(0)",
|
|
83
|
+
"event": Object {
|
|
84
|
+
"$type": "tools.ozone.moderation.defs#modEventTakedown",
|
|
85
|
+
"comment": "Second sev-1 violation",
|
|
86
|
+
"policies": Array [
|
|
87
|
+
"spam-policy",
|
|
88
|
+
],
|
|
89
|
+
"severityLevel": "sev-1",
|
|
90
|
+
"strikeCount": 1,
|
|
91
|
+
},
|
|
92
|
+
"id": 7,
|
|
93
|
+
"subject": Object {
|
|
94
|
+
"$type": "com.atproto.repo.strongRef",
|
|
95
|
+
"cid": "cids(1)",
|
|
96
|
+
"uri": "record(1)",
|
|
97
|
+
},
|
|
98
|
+
"subjectBlobCids": Array [],
|
|
99
|
+
"subjectHandle": "alice.test",
|
|
100
|
+
},
|
|
101
|
+
Object {
|
|
102
|
+
"createdAt": "1970-01-01T00:00:00.000Z",
|
|
103
|
+
"createdBy": "user(0)",
|
|
104
|
+
"event": Object {
|
|
105
|
+
"$type": "tools.ozone.moderation.defs#modEventTakedown",
|
|
106
|
+
"comment": "First sev-1 violation",
|
|
107
|
+
"policies": Array [
|
|
108
|
+
"spam-policy",
|
|
109
|
+
],
|
|
110
|
+
"severityLevel": "sev-1",
|
|
111
|
+
"strikeCount": 0,
|
|
112
|
+
},
|
|
113
|
+
"id": 5,
|
|
114
|
+
"subject": Object {
|
|
115
|
+
"$type": "com.atproto.repo.strongRef",
|
|
116
|
+
"cid": "cids(2)",
|
|
117
|
+
"uri": "record(2)",
|
|
118
|
+
},
|
|
119
|
+
"subjectBlobCids": Array [],
|
|
120
|
+
"subjectHandle": "alice.test",
|
|
121
|
+
},
|
|
122
|
+
Object {
|
|
123
|
+
"createdAt": "1970-01-01T00:00:00.000Z",
|
|
124
|
+
"createdBy": "user(0)",
|
|
125
|
+
"event": Object {
|
|
126
|
+
"$type": "tools.ozone.moderation.defs#modEventTakedown",
|
|
127
|
+
"comment": "Second violation",
|
|
128
|
+
"severityLevel": "sev-2",
|
|
129
|
+
"strikeCount": 2,
|
|
130
|
+
},
|
|
131
|
+
"id": 3,
|
|
132
|
+
"subject": Object {
|
|
133
|
+
"$type": "com.atproto.repo.strongRef",
|
|
134
|
+
"cid": "cids(3)",
|
|
135
|
+
"uri": "record(3)",
|
|
136
|
+
},
|
|
137
|
+
"subjectBlobCids": Array [],
|
|
138
|
+
"subjectHandle": "alice.test",
|
|
139
|
+
},
|
|
140
|
+
Object {
|
|
141
|
+
"createdAt": "1970-01-01T00:00:00.000Z",
|
|
142
|
+
"createdBy": "user(0)",
|
|
143
|
+
"event": Object {
|
|
144
|
+
"$type": "tools.ozone.moderation.defs#modEventTakedown",
|
|
145
|
+
"comment": "First violation",
|
|
146
|
+
"severityLevel": "sev-2",
|
|
147
|
+
"strikeCount": 2,
|
|
148
|
+
},
|
|
149
|
+
"id": 1,
|
|
150
|
+
"subject": Object {
|
|
151
|
+
"$type": "com.atproto.repo.strongRef",
|
|
152
|
+
"cid": "cids(0)",
|
|
153
|
+
"uri": "record(0)",
|
|
154
|
+
},
|
|
155
|
+
"subjectBlobCids": Array [],
|
|
156
|
+
"subjectHandle": "alice.test",
|
|
157
|
+
},
|
|
158
|
+
]
|
|
159
|
+
`;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import AtpAgent from '@atproto/api'
|
|
2
|
+
import {
|
|
3
|
+
ModeratorClient,
|
|
4
|
+
SeedClient,
|
|
5
|
+
TestNetwork,
|
|
6
|
+
basicSeed,
|
|
7
|
+
} from '@atproto/dev-env'
|
|
8
|
+
import { ids } from '../src/lexicon/lexicons'
|
|
9
|
+
import { SeverityLevelSettingKey } from '../src/setting/constants'
|
|
10
|
+
import { forSnapshot } from './_util'
|
|
11
|
+
|
|
12
|
+
const strikeConfig = {
|
|
13
|
+
'sev-0': { strikeCount: 0 },
|
|
14
|
+
'sev-1': {
|
|
15
|
+
strikeCount: 1,
|
|
16
|
+
strikeOnOccurrence: 2,
|
|
17
|
+
expiresInDays: 365,
|
|
18
|
+
},
|
|
19
|
+
'sev-2': { strikeCount: 2, expiresInDays: 365 },
|
|
20
|
+
'sev-4': { strikeCount: 4, expiresInDays: 0 },
|
|
21
|
+
'sev-5': { needsTakedown: true },
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('account-strikes', () => {
|
|
25
|
+
let network: TestNetwork
|
|
26
|
+
let agent: AtpAgent
|
|
27
|
+
let sc: SeedClient
|
|
28
|
+
let modClient: ModeratorClient
|
|
29
|
+
|
|
30
|
+
const repoSubject = (did: string) => ({
|
|
31
|
+
$type: 'com.atproto.admin.defs#repoRef',
|
|
32
|
+
did,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const configureSeverityLevels = async () => {
|
|
36
|
+
// Configure severity level settings
|
|
37
|
+
await agent.tools.ozone.setting.upsertOption(
|
|
38
|
+
{
|
|
39
|
+
scope: 'instance',
|
|
40
|
+
key: SeverityLevelSettingKey,
|
|
41
|
+
value: strikeConfig,
|
|
42
|
+
description: 'Severity level configuration for strike system',
|
|
43
|
+
managerRole: 'tools.ozone.team.defs#roleAdmin',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
encoding: 'application/json',
|
|
47
|
+
headers: await network.ozone.modHeaders(
|
|
48
|
+
ids.ToolsOzoneSettingUpsertOption,
|
|
49
|
+
'admin',
|
|
50
|
+
),
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
beforeAll(async () => {
|
|
56
|
+
network = await TestNetwork.create({
|
|
57
|
+
dbPostgresSchema: 'ozone_account_strikes',
|
|
58
|
+
})
|
|
59
|
+
agent = network.ozone.getClient()
|
|
60
|
+
sc = network.getSeedClient()
|
|
61
|
+
modClient = network.ozone.getModClient()
|
|
62
|
+
await basicSeed(sc)
|
|
63
|
+
await network.processAll()
|
|
64
|
+
await configureSeverityLevels()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
afterAll(async () => {
|
|
68
|
+
await network.close()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('tracks strikes and exposes them through queryStatuses and queryEvents', async () => {
|
|
72
|
+
const aliceSubject = repoSubject(sc.dids.alice)
|
|
73
|
+
const alicePost = {
|
|
74
|
+
$type: 'com.atproto.repo.strongRef',
|
|
75
|
+
uri: sc.posts[sc.dids.alice][0].ref.uriStr,
|
|
76
|
+
cid: sc.posts[sc.dids.alice][0].ref.cidStr,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await modClient.emitEvent({
|
|
80
|
+
event: {
|
|
81
|
+
$type: 'tools.ozone.moderation.defs#modEventTakedown',
|
|
82
|
+
severityLevel: 'sev-2',
|
|
83
|
+
strikeCount: strikeConfig['sev-2'].strikeCount,
|
|
84
|
+
comment: 'First violation',
|
|
85
|
+
},
|
|
86
|
+
subject: alicePost,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const alicePost2 = {
|
|
90
|
+
$type: 'com.atproto.repo.strongRef',
|
|
91
|
+
uri: sc.posts[sc.dids.alice][1].ref.uriStr,
|
|
92
|
+
cid: sc.posts[sc.dids.alice][1].ref.cidStr,
|
|
93
|
+
}
|
|
94
|
+
await modClient.emitEvent({
|
|
95
|
+
event: {
|
|
96
|
+
$type: 'tools.ozone.moderation.defs#modEventTakedown',
|
|
97
|
+
severityLevel: 'sev-2',
|
|
98
|
+
strikeCount: strikeConfig['sev-2'].strikeCount,
|
|
99
|
+
comment: 'Second violation',
|
|
100
|
+
},
|
|
101
|
+
subject: alicePost2,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const alicePost3 = {
|
|
105
|
+
$type: 'com.atproto.repo.strongRef',
|
|
106
|
+
uri: sc.posts[sc.dids.alice][2].ref.uriStr,
|
|
107
|
+
cid: sc.posts[sc.dids.alice][2].ref.cidStr,
|
|
108
|
+
}
|
|
109
|
+
await modClient.emitEvent({
|
|
110
|
+
event: {
|
|
111
|
+
$type: 'tools.ozone.moderation.defs#modEventTakedown',
|
|
112
|
+
severityLevel: 'sev-1',
|
|
113
|
+
strikeCount: 0, // First occurrence - warning
|
|
114
|
+
policies: ['spam-policy'],
|
|
115
|
+
comment: 'First sev-1 violation',
|
|
116
|
+
},
|
|
117
|
+
subject: alicePost3,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Issue second sev-1 takedown with same policy on another post (second occurrence, should be 1 strike, total 5)
|
|
121
|
+
const aliceReply = {
|
|
122
|
+
$type: 'com.atproto.repo.strongRef',
|
|
123
|
+
uri: sc.replies[sc.dids.alice][0].ref.uriStr,
|
|
124
|
+
cid: sc.replies[sc.dids.alice][0].ref.cidStr,
|
|
125
|
+
}
|
|
126
|
+
await modClient.emitEvent({
|
|
127
|
+
event: {
|
|
128
|
+
$type: 'tools.ozone.moderation.defs#modEventTakedown',
|
|
129
|
+
severityLevel: 'sev-1',
|
|
130
|
+
strikeCount: strikeConfig['sev-1'].strikeCount, // Second occurrence - actual strike
|
|
131
|
+
policies: ['spam-policy'],
|
|
132
|
+
comment: 'Second sev-1 violation',
|
|
133
|
+
},
|
|
134
|
+
subject: aliceReply,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
await modClient.emitEvent({
|
|
138
|
+
event: {
|
|
139
|
+
$type: 'tools.ozone.moderation.defs#modEventTakedown',
|
|
140
|
+
severityLevel: 'sev-0',
|
|
141
|
+
strikeCount: strikeConfig['sev-0'].strikeCount,
|
|
142
|
+
comment: 'Warning only',
|
|
143
|
+
},
|
|
144
|
+
subject: aliceSubject,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
let statusResult = await modClient.queryStatuses({
|
|
148
|
+
subject: sc.dids.alice,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
expect(statusResult.subjectStatuses.length).toBeGreaterThan(0)
|
|
152
|
+
expect(forSnapshot(statusResult.subjectStatuses[0])).toMatchSnapshot()
|
|
153
|
+
const strikeCountBefore =
|
|
154
|
+
statusResult.subjectStatuses[0].accountStrike?.activeStrikeCount
|
|
155
|
+
|
|
156
|
+
// Reverse one of the takedowns with negative strikeCount
|
|
157
|
+
await modClient.emitEvent({
|
|
158
|
+
event: {
|
|
159
|
+
$type: 'tools.ozone.moderation.defs#modEventReverseTakedown',
|
|
160
|
+
severityLevel: 'sev-2',
|
|
161
|
+
strikeCount: -2, // Negative to reverse strikes
|
|
162
|
+
comment: 'Appeal granted - reversing first takedown',
|
|
163
|
+
},
|
|
164
|
+
subject: alicePost,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Verify strikes were reduced (if strikeCount tracking is implemented)
|
|
168
|
+
statusResult = await modClient.queryStatuses({
|
|
169
|
+
subject: sc.dids.alice,
|
|
170
|
+
})
|
|
171
|
+
const strikeCountAfter =
|
|
172
|
+
statusResult.subjectStatuses[0].accountStrike?.activeStrikeCount
|
|
173
|
+
expect(strikeCountAfter).toBe(3)
|
|
174
|
+
expect(strikeCountAfter).toBeLessThan(strikeCountBefore!)
|
|
175
|
+
|
|
176
|
+
const eventsWithStrikes = await modClient.queryEvents({
|
|
177
|
+
subject: sc.dids.alice,
|
|
178
|
+
includeAllUserRecords: true,
|
|
179
|
+
withStrike: true,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
expect(forSnapshot(eventsWithStrikes.events)).toMatchSnapshot()
|
|
183
|
+
})
|
|
184
|
+
})
|