@atproto/ozone 0.1.150 → 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 +9 -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/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 +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 +103 -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 +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
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import AtpAgent from '@atproto/api'
|
|
2
|
+
import { SECOND } from '@atproto/common'
|
|
3
|
+
import {
|
|
4
|
+
ModeratorClient,
|
|
5
|
+
SeedClient,
|
|
6
|
+
TestNetwork,
|
|
7
|
+
basicSeed,
|
|
8
|
+
} from '@atproto/dev-env'
|
|
9
|
+
import { ids } from '../src/lexicon/lexicons'
|
|
10
|
+
import { SeverityLevelSettingKey } from '../src/setting/constants'
|
|
11
|
+
|
|
12
|
+
const strikeConfig = {
|
|
13
|
+
'sev-1': {
|
|
14
|
+
strikeCount: 1,
|
|
15
|
+
expiresInDays: 0, // Set to 0 so we can use future timestamps
|
|
16
|
+
},
|
|
17
|
+
'sev-2': {
|
|
18
|
+
strikeCount: 2,
|
|
19
|
+
expiresInDays: 0,
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('strike expiry processor', () => {
|
|
24
|
+
let network: TestNetwork
|
|
25
|
+
let agent: AtpAgent
|
|
26
|
+
let sc: SeedClient
|
|
27
|
+
let modClient: ModeratorClient
|
|
28
|
+
|
|
29
|
+
const repoSubject = (did: string) => ({
|
|
30
|
+
$type: 'com.atproto.admin.defs#repoRef',
|
|
31
|
+
did,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const configureSeverityLevels = async () => {
|
|
35
|
+
await agent.tools.ozone.setting.upsertOption(
|
|
36
|
+
{
|
|
37
|
+
scope: 'instance',
|
|
38
|
+
key: SeverityLevelSettingKey,
|
|
39
|
+
value: strikeConfig,
|
|
40
|
+
description: 'Severity level configuration for strike system',
|
|
41
|
+
managerRole: 'tools.ozone.team.defs#roleAdmin',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
encoding: 'application/json',
|
|
45
|
+
headers: await network.ozone.modHeaders(
|
|
46
|
+
ids.ToolsOzoneSettingUpsertOption,
|
|
47
|
+
'admin',
|
|
48
|
+
),
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const getAccountStatus = async (did: string) => {
|
|
54
|
+
const { subjectStatuses } = await modClient.queryStatuses({ subject: did })
|
|
55
|
+
return subjectStatuses[0]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
beforeAll(async () => {
|
|
59
|
+
network = await TestNetwork.create({
|
|
60
|
+
dbPostgresSchema: 'ozone_strike_expiry_processor',
|
|
61
|
+
})
|
|
62
|
+
agent = network.ozone.getClient()
|
|
63
|
+
sc = network.getSeedClient()
|
|
64
|
+
modClient = network.ozone.getModClient()
|
|
65
|
+
await basicSeed(sc)
|
|
66
|
+
await network.processAll()
|
|
67
|
+
await configureSeverityLevels()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
afterAll(async () => {
|
|
71
|
+
await network.close()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('processes expired strikes and updates active strike count', async () => {
|
|
75
|
+
const bobDid = sc.dids.bob
|
|
76
|
+
const bobPost1 = {
|
|
77
|
+
$type: 'com.atproto.repo.strongRef',
|
|
78
|
+
uri: sc.posts[bobDid][0].ref.uriStr,
|
|
79
|
+
cid: sc.posts[bobDid][0].ref.cidStr,
|
|
80
|
+
}
|
|
81
|
+
const bobPost2 = {
|
|
82
|
+
$type: 'com.atproto.repo.strongRef',
|
|
83
|
+
uri: sc.posts[bobDid][1].ref.uriStr,
|
|
84
|
+
cid: sc.posts[bobDid][1].ref.cidStr,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// first strike on a post that expires in 2 seconds
|
|
88
|
+
const expiresAt1 = new Date(Date.now() + 2 * SECOND).toISOString()
|
|
89
|
+
await modClient.emitEvent({
|
|
90
|
+
event: {
|
|
91
|
+
$type: 'tools.ozone.moderation.defs#modEventTakedown',
|
|
92
|
+
severityLevel: 'sev-2',
|
|
93
|
+
strikeCount: 2,
|
|
94
|
+
strikeExpiresAt: expiresAt1,
|
|
95
|
+
comment: 'First violation - expires soon',
|
|
96
|
+
},
|
|
97
|
+
subject: bobPost1,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// second strike on another post that expires in 3 seconds
|
|
101
|
+
const expiresAt2 = new Date(Date.now() + 3 * SECOND).toISOString()
|
|
102
|
+
await modClient.emitEvent({
|
|
103
|
+
event: {
|
|
104
|
+
$type: 'tools.ozone.moderation.defs#modEventTakedown',
|
|
105
|
+
strikeCount: 1,
|
|
106
|
+
severityLevel: 'sev-1',
|
|
107
|
+
strikeExpiresAt: expiresAt2,
|
|
108
|
+
comment: 'Second violation - expires later',
|
|
109
|
+
},
|
|
110
|
+
subject: bobPost2,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// account-level event to ensure account status is created
|
|
114
|
+
await modClient.emitEvent({
|
|
115
|
+
event: {
|
|
116
|
+
$type: 'tools.ozone.moderation.defs#modEventComment',
|
|
117
|
+
comment: 'Account under review',
|
|
118
|
+
},
|
|
119
|
+
subject: repoSubject(bobDid),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Verify initial state - both strikes are active
|
|
123
|
+
let status = await getAccountStatus(bobDid)
|
|
124
|
+
expect(status.accountStrike).toBeDefined()
|
|
125
|
+
expect(status.accountStrike!.activeStrikeCount).toBe(3) // 2 + 1
|
|
126
|
+
expect(status.accountStrike!.totalStrikeCount).toBe(3)
|
|
127
|
+
|
|
128
|
+
// Wait for first strike to expire
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve, 2.1 * SECOND))
|
|
130
|
+
|
|
131
|
+
// Run the processor
|
|
132
|
+
await network.ozone.daemon.ctx.strikeExpiryProcessor.processExpiredStrikes()
|
|
133
|
+
|
|
134
|
+
// Verify first strike expired - only second strike remains active
|
|
135
|
+
status = await getAccountStatus(bobDid)
|
|
136
|
+
expect(status.accountStrike).toBeDefined()
|
|
137
|
+
expect(status.accountStrike!.activeStrikeCount).toBe(1) // Only second strike
|
|
138
|
+
expect(status.accountStrike!.totalStrikeCount).toBe(3) // Total unchanged
|
|
139
|
+
|
|
140
|
+
// Wait for second strike to expire
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, 1 * SECOND))
|
|
142
|
+
|
|
143
|
+
// Run the processor again
|
|
144
|
+
await network.ozone.daemon.ctx.strikeExpiryProcessor.processExpiredStrikes()
|
|
145
|
+
|
|
146
|
+
// Verify all strikes expired
|
|
147
|
+
status = await getAccountStatus(bobDid)
|
|
148
|
+
expect(status.accountStrike).toBeDefined()
|
|
149
|
+
expect(status.accountStrike!.activeStrikeCount).toBe(0)
|
|
150
|
+
expect(status.accountStrike!.totalStrikeCount).toBe(3) // Total unchanged
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('handles accounts with no expired strikes', async () => {
|
|
154
|
+
const aliceDid = sc.dids.alice
|
|
155
|
+
|
|
156
|
+
// strike that expires far in the future
|
|
157
|
+
const expiresAt = new Date(Date.now() + 1000 * SECOND).toISOString()
|
|
158
|
+
await modClient.emitEvent({
|
|
159
|
+
event: {
|
|
160
|
+
$type: 'tools.ozone.moderation.defs#modEventTakedown',
|
|
161
|
+
severityLevel: 'sev-2',
|
|
162
|
+
strikeCount: 2,
|
|
163
|
+
strikeExpiresAt: expiresAt,
|
|
164
|
+
comment: 'Future expiry',
|
|
165
|
+
},
|
|
166
|
+
subject: repoSubject(aliceDid),
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Get initial state
|
|
170
|
+
let status = await getAccountStatus(aliceDid)
|
|
171
|
+
expect(status.accountStrike).toBeDefined()
|
|
172
|
+
const initialActiveCount = status.accountStrike!.activeStrikeCount!
|
|
173
|
+
expect(initialActiveCount).toBe(2)
|
|
174
|
+
|
|
175
|
+
await network.ozone.daemon.ctx.strikeExpiryProcessor.processExpiredStrikes()
|
|
176
|
+
|
|
177
|
+
// Verify nothing changed
|
|
178
|
+
status = await getAccountStatus(aliceDid)
|
|
179
|
+
expect(status.accountStrike).toBeDefined()
|
|
180
|
+
expect(status.accountStrike!.activeStrikeCount).toBe(initialActiveCount)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('handles strikes with no expiry date (permanent strikes)', async () => {
|
|
184
|
+
const carolDid = sc.dids.carol
|
|
185
|
+
|
|
186
|
+
// permanent strike (no expiry)
|
|
187
|
+
await modClient.emitEvent({
|
|
188
|
+
event: {
|
|
189
|
+
$type: 'tools.ozone.moderation.defs#modEventTakedown',
|
|
190
|
+
severityLevel: 'sev-2',
|
|
191
|
+
strikeCount: 2,
|
|
192
|
+
comment: 'Permanent strike - no expiry',
|
|
193
|
+
},
|
|
194
|
+
subject: repoSubject(carolDid),
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// Get initial state
|
|
198
|
+
let status = await getAccountStatus(carolDid)
|
|
199
|
+
expect(status.accountStrike).toBeDefined()
|
|
200
|
+
expect(status.accountStrike!.activeStrikeCount).toBe(2)
|
|
201
|
+
|
|
202
|
+
await network.ozone.daemon.ctx.strikeExpiryProcessor.processExpiredStrikes()
|
|
203
|
+
|
|
204
|
+
// Verify permanent strikes remain active
|
|
205
|
+
status = await getAccountStatus(carolDid)
|
|
206
|
+
expect(status.accountStrike).toBeDefined()
|
|
207
|
+
expect(status.accountStrike!.activeStrikeCount).toBe(2)
|
|
208
|
+
expect(status.accountStrike!.totalStrikeCount).toBe(2)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('processes multiple accounts with expired strikes in batch', async () => {
|
|
212
|
+
const danDid = 'did:plc:dan'
|
|
213
|
+
const eveDid = 'did:plc:eve'
|
|
214
|
+
|
|
215
|
+
const expiresAt = new Date(Date.now() + 1 * SECOND).toISOString()
|
|
216
|
+
|
|
217
|
+
// strikes to multiple accounts
|
|
218
|
+
await modClient.emitEvent({
|
|
219
|
+
event: {
|
|
220
|
+
$type: 'tools.ozone.moderation.defs#modEventTakedown',
|
|
221
|
+
severityLevel: 'sev-1',
|
|
222
|
+
strikeCount: 1,
|
|
223
|
+
strikeExpiresAt: expiresAt,
|
|
224
|
+
comment: 'Dan violation',
|
|
225
|
+
},
|
|
226
|
+
subject: repoSubject(danDid),
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
await modClient.emitEvent({
|
|
230
|
+
event: {
|
|
231
|
+
$type: 'tools.ozone.moderation.defs#modEventTakedown',
|
|
232
|
+
severityLevel: 'sev-2',
|
|
233
|
+
strikeCount: 2,
|
|
234
|
+
strikeExpiresAt: expiresAt,
|
|
235
|
+
comment: 'Eve violation',
|
|
236
|
+
},
|
|
237
|
+
subject: repoSubject(eveDid),
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// Verify initial states
|
|
241
|
+
let danStatus = await getAccountStatus(danDid)
|
|
242
|
+
let eveStatus = await getAccountStatus(eveDid)
|
|
243
|
+
expect(danStatus.accountStrike?.activeStrikeCount).toBe(1)
|
|
244
|
+
expect(eveStatus.accountStrike?.activeStrikeCount).toBe(2)
|
|
245
|
+
|
|
246
|
+
// Wait for strikes to expire
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, 1.1 * SECOND))
|
|
248
|
+
|
|
249
|
+
await network.ozone.daemon.ctx.strikeExpiryProcessor.processExpiredStrikes()
|
|
250
|
+
|
|
251
|
+
// Verify both accounts' strikes expired
|
|
252
|
+
danStatus = await getAccountStatus(danDid)
|
|
253
|
+
eveStatus = await getAccountStatus(eveDid)
|
|
254
|
+
expect(danStatus.accountStrike?.activeStrikeCount).toBe(0)
|
|
255
|
+
expect(eveStatus.accountStrike?.activeStrikeCount).toBe(0)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('updates cursor to track last processed timestamp', async () => {
|
|
259
|
+
const frankDid = 'did:plc:frank'
|
|
260
|
+
const expiresAt = new Date(Date.now() + 1 * SECOND).toISOString()
|
|
261
|
+
|
|
262
|
+
await modClient.emitEvent({
|
|
263
|
+
event: {
|
|
264
|
+
$type: 'tools.ozone.moderation.defs#modEventTakedown',
|
|
265
|
+
severityLevel: 'sev-1',
|
|
266
|
+
strikeCount: 1,
|
|
267
|
+
strikeExpiresAt: expiresAt,
|
|
268
|
+
comment: 'Frank violation',
|
|
269
|
+
},
|
|
270
|
+
subject: repoSubject(frankDid),
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// Wait for strike to expire
|
|
274
|
+
await new Promise((resolve) => setTimeout(resolve, 1.1 * SECOND))
|
|
275
|
+
|
|
276
|
+
// Get cursor before processing
|
|
277
|
+
const cursorBefore =
|
|
278
|
+
await network.ozone.daemon.ctx.strikeExpiryProcessor.getCursor()
|
|
279
|
+
|
|
280
|
+
await network.ozone.daemon.ctx.strikeExpiryProcessor.processExpiredStrikes()
|
|
281
|
+
|
|
282
|
+
// Get cursor after processing
|
|
283
|
+
const cursorAfter =
|
|
284
|
+
await network.ozone.daemon.ctx.strikeExpiryProcessor.getCursor()
|
|
285
|
+
|
|
286
|
+
expect(cursorAfter).not.toBe(cursorBefore)
|
|
287
|
+
expect(cursorAfter).toBeTruthy()
|
|
288
|
+
|
|
289
|
+
// Verify strike was processed
|
|
290
|
+
const status = await getAccountStatus(frankDid)
|
|
291
|
+
expect(status.accountStrike?.activeStrikeCount).toBe(0)
|
|
292
|
+
|
|
293
|
+
// running processor again should not reprocess the same strike
|
|
294
|
+
await network.ozone.daemon.ctx.strikeExpiryProcessor.processExpiredStrikes()
|
|
295
|
+
const cursorAfterSecond =
|
|
296
|
+
await network.ozone.daemon.ctx.strikeExpiryProcessor.getCursor()
|
|
297
|
+
expect(cursorAfterSecond).not.toBe(cursorAfter)
|
|
298
|
+
})
|
|
299
|
+
})
|