@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.
Files changed (118) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/api/moderation/queryEvents.d.ts.map +1 -1
  3. package/dist/api/moderation/queryEvents.js +2 -1
  4. package/dist/api/moderation/queryEvents.js.map +1 -1
  5. package/dist/context.d.ts +3 -0
  6. package/dist/context.d.ts.map +1 -1
  7. package/dist/context.js +7 -1
  8. package/dist/context.js.map +1 -1
  9. package/dist/daemon/context.d.ts +3 -0
  10. package/dist/daemon/context.d.ts.map +1 -1
  11. package/dist/daemon/context.js +11 -1
  12. package/dist/daemon/context.js.map +1 -1
  13. package/dist/daemon/index.d.ts +1 -0
  14. package/dist/daemon/index.d.ts.map +1 -1
  15. package/dist/daemon/index.js +3 -1
  16. package/dist/daemon/index.js.map +1 -1
  17. package/dist/daemon/strike-expiry-processor.d.ts +18 -0
  18. package/dist/daemon/strike-expiry-processor.d.ts.map +1 -0
  19. package/dist/daemon/strike-expiry-processor.js +111 -0
  20. package/dist/daemon/strike-expiry-processor.js.map +1 -0
  21. package/dist/db/migrations/20251008T120000000Z-add-strike-system.d.ts +4 -0
  22. package/dist/db/migrations/20251008T120000000Z-add-strike-system.d.ts.map +1 -0
  23. package/dist/db/migrations/20251008T120000000Z-add-strike-system.js +75 -0
  24. package/dist/db/migrations/20251008T120000000Z-add-strike-system.js.map +1 -0
  25. package/dist/db/migrations/index.d.ts +1 -0
  26. package/dist/db/migrations/index.d.ts.map +1 -1
  27. package/dist/db/migrations/index.js +2 -1
  28. package/dist/db/migrations/index.js.map +1 -1
  29. package/dist/db/schema/account_strike.d.ts +12 -0
  30. package/dist/db/schema/account_strike.d.ts.map +1 -0
  31. package/dist/db/schema/account_strike.js +5 -0
  32. package/dist/db/schema/account_strike.js.map +1 -0
  33. package/dist/db/schema/index.d.ts +3 -1
  34. package/dist/db/schema/index.d.ts.map +1 -1
  35. package/dist/db/schema/index.js.map +1 -1
  36. package/dist/db/schema/job_cursor.d.ts +11 -0
  37. package/dist/db/schema/job_cursor.d.ts.map +1 -0
  38. package/dist/db/schema/job_cursor.js +5 -0
  39. package/dist/db/schema/job_cursor.js.map +1 -0
  40. package/dist/db/schema/moderation_event.d.ts +3 -0
  41. package/dist/db/schema/moderation_event.d.ts.map +1 -1
  42. package/dist/db/schema/moderation_event.js.map +1 -1
  43. package/dist/lexicon/lexicons.d.ts +208 -0
  44. package/dist/lexicon/lexicons.d.ts.map +1 -1
  45. package/dist/lexicon/lexicons.js +104 -0
  46. package/dist/lexicon/lexicons.js.map +1 -1
  47. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +12 -0
  48. package/dist/lexicon/types/app/bsky/actor/defs.d.ts.map +1 -1
  49. package/dist/lexicon/types/app/bsky/actor/defs.js.map +1 -1
  50. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +4 -0
  51. package/dist/lexicon/types/app/bsky/feed/defs.d.ts.map +1 -1
  52. package/dist/lexicon/types/app/bsky/feed/defs.js.map +1 -1
  53. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +35 -0
  54. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
  55. package/dist/lexicon/types/tools/ozone/moderation/defs.js +9 -0
  56. package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
  57. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts +2 -0
  58. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts.map +1 -1
  59. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.js.map +1 -1
  60. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts +2 -0
  61. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts.map +1 -1
  62. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.js.map +1 -1
  63. package/dist/mod-service/index.d.ts +9 -3
  64. package/dist/mod-service/index.d.ts.map +1 -1
  65. package/dist/mod-service/index.js +59 -6
  66. package/dist/mod-service/index.js.map +1 -1
  67. package/dist/mod-service/status.d.ts +44 -2
  68. package/dist/mod-service/status.d.ts.map +1 -1
  69. package/dist/mod-service/status.js +7 -0
  70. package/dist/mod-service/status.js.map +1 -1
  71. package/dist/mod-service/strike.d.ts +19 -0
  72. package/dist/mod-service/strike.d.ts.map +1 -0
  73. package/dist/mod-service/strike.js +86 -0
  74. package/dist/mod-service/strike.js.map +1 -0
  75. package/dist/mod-service/types.d.ts +4 -0
  76. package/dist/mod-service/types.d.ts.map +1 -1
  77. package/dist/mod-service/types.js.map +1 -1
  78. package/dist/mod-service/views.d.ts.map +1 -1
  79. package/dist/mod-service/views.js +20 -4
  80. package/dist/mod-service/views.js.map +1 -1
  81. package/dist/setting/constants.d.ts +1 -0
  82. package/dist/setting/constants.d.ts.map +1 -1
  83. package/dist/setting/constants.js +2 -1
  84. package/dist/setting/constants.js.map +1 -1
  85. package/dist/setting/validators.d.ts.map +1 -1
  86. package/dist/setting/validators.js +179 -0
  87. package/dist/setting/validators.js.map +1 -1
  88. package/package.json +3 -3
  89. package/src/api/moderation/queryEvents.ts +2 -0
  90. package/src/context.ts +20 -11
  91. package/src/daemon/context.ts +15 -1
  92. package/src/daemon/index.ts +1 -0
  93. package/src/daemon/strike-expiry-processor.ts +111 -0
  94. package/src/db/migrations/20251008T120000000Z-add-strike-system.ts +87 -0
  95. package/src/db/migrations/index.ts +1 -0
  96. package/src/db/schema/account_strike.ts +13 -0
  97. package/src/db/schema/index.ts +4 -0
  98. package/src/db/schema/job_cursor.ts +13 -0
  99. package/src/db/schema/moderation_event.ts +3 -0
  100. package/src/lexicon/lexicons.ts +119 -0
  101. package/src/lexicon/types/app/bsky/actor/defs.ts +6 -0
  102. package/src/lexicon/types/app/bsky/feed/defs.ts +2 -0
  103. package/src/lexicon/types/tools/ozone/moderation/defs.ts +44 -0
  104. package/src/lexicon/types/tools/ozone/moderation/queryEvents.ts +2 -0
  105. package/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts +2 -0
  106. package/src/mod-service/index.ts +70 -3
  107. package/src/mod-service/status.ts +9 -0
  108. package/src/mod-service/strike.ts +96 -0
  109. package/src/mod-service/types.ts +6 -0
  110. package/src/mod-service/views.ts +25 -4
  111. package/src/setting/constants.ts +1 -0
  112. package/src/setting/validators.ts +231 -1
  113. package/tests/__snapshots__/account-strikes.test.ts.snap +159 -0
  114. package/tests/account-strikes.test.ts +184 -0
  115. package/tests/query-labels.test.ts +1 -0
  116. package/tests/strike-expiry-processor.test.ts +299 -0
  117. package/tsconfig.build.tsbuildinfo +1 -1
  118. 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 { PolicyListSettingKey, ProtectedTagSettingKey } from './constants'
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
+ })
@@ -163,6 +163,7 @@ describe('ozone query labels', () => {
163
163
  modSrvc.eventPusher,
164
164
  modSrvc.appviewAgent,
165
165
  ctx.serviceAuthHeaders,
166
+ ctx.strikeService,
166
167
  ),
167
168
  })
168
169