@atproto/ozone 0.1.154 → 0.1.156
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 +21 -0
- package/dist/api/moderation/emitEvent.d.ts.map +1 -1
- package/dist/api/moderation/emitEvent.js +22 -8
- package/dist/api/moderation/emitEvent.js.map +1 -1
- package/dist/daemon/event-pusher.d.ts +1 -1
- package/dist/daemon/event-pusher.d.ts.map +1 -1
- package/dist/daemon/event-pusher.js +17 -5
- package/dist/daemon/event-pusher.js.map +1 -1
- package/dist/daemon/materialized-view-refresher.d.ts.map +1 -1
- package/dist/daemon/materialized-view-refresher.js +0 -1
- package/dist/daemon/materialized-view-refresher.js.map +1 -1
- package/dist/daemon/scheduled-action-processor.d.ts +5 -1
- package/dist/daemon/scheduled-action-processor.d.ts.map +1 -1
- package/dist/daemon/scheduled-action-processor.js +44 -2
- package/dist/daemon/scheduled-action-processor.js.map +1 -1
- package/dist/jetstream/service.d.ts +1 -1
- package/dist/jetstream/service.d.ts.map +1 -1
- package/dist/jetstream/service.js +2 -2
- package/dist/jetstream/service.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +66 -0
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +33 -0
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +4 -0
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/scheduleAction.d.ts +10 -0
- package/dist/lexicon/types/tools/ozone/moderation/scheduleAction.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/scheduleAction.js.map +1 -1
- package/dist/mod-service/index.d.ts +2 -2
- package/dist/mod-service/index.d.ts.map +1 -1
- package/dist/mod-service/index.js +10 -4
- package/dist/mod-service/index.js.map +1 -1
- package/dist/mod-service/views.d.ts.map +1 -1
- package/dist/mod-service/views.js +7 -0
- package/dist/mod-service/views.js.map +1 -1
- package/dist/setting/validators.d.ts.map +1 -1
- package/dist/setting/validators.js +10 -0
- package/dist/setting/validators.js.map +1 -1
- package/package.json +5 -4
- package/src/api/moderation/emitEvent.ts +37 -10
- package/src/daemon/event-pusher.ts +22 -5
- package/src/daemon/materialized-view-refresher.ts +0 -1
- package/src/daemon/scheduled-action-processor.ts +59 -1
- package/src/jetstream/service.ts +1 -1
- package/src/lexicon/lexicons.ts +39 -0
- package/src/lexicon/types/tools/ozone/moderation/defs.ts +4 -0
- package/src/lexicon/types/tools/ozone/moderation/scheduleAction.ts +10 -0
- package/src/mod-service/index.ts +21 -7
- package/src/mod-service/views.ts +10 -0
- package/src/setting/validators.ts +15 -0
- package/tests/query-labels.test.ts +2 -1
- package/tests/scheduled-action-processor.test.ts +30 -6
- package/tests/takedown.test.ts +43 -1
package/src/jetstream/service.ts
CHANGED
package/src/lexicon/lexicons.ts
CHANGED
|
@@ -14889,6 +14889,15 @@ export const schemaDict = {
|
|
|
14889
14889
|
description:
|
|
14890
14890
|
"Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.).",
|
|
14891
14891
|
},
|
|
14892
|
+
targetServices: {
|
|
14893
|
+
type: 'array',
|
|
14894
|
+
items: {
|
|
14895
|
+
type: 'string',
|
|
14896
|
+
knownValues: ['appview', 'pds'],
|
|
14897
|
+
},
|
|
14898
|
+
description:
|
|
14899
|
+
'List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services.',
|
|
14900
|
+
},
|
|
14892
14901
|
strikeCount: {
|
|
14893
14902
|
type: 'integer',
|
|
14894
14903
|
description:
|
|
@@ -15197,6 +15206,11 @@ export const schemaDict = {
|
|
|
15197
15206
|
description:
|
|
15198
15207
|
'When the strike should expire. If not provided, the strike never expires.',
|
|
15199
15208
|
},
|
|
15209
|
+
isDelivered: {
|
|
15210
|
+
type: 'boolean',
|
|
15211
|
+
description:
|
|
15212
|
+
"Indicates whether the email was successfully delivered to the user's inbox.",
|
|
15213
|
+
},
|
|
15200
15214
|
},
|
|
15201
15215
|
},
|
|
15202
15216
|
modEventDivert: {
|
|
@@ -16950,6 +16964,31 @@ export const schemaDict = {
|
|
|
16950
16964
|
description:
|
|
16951
16965
|
'Names/Keywords of the policies that drove the decision.',
|
|
16952
16966
|
},
|
|
16967
|
+
severityLevel: {
|
|
16968
|
+
type: 'string',
|
|
16969
|
+
description:
|
|
16970
|
+
"Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.).",
|
|
16971
|
+
},
|
|
16972
|
+
strikeCount: {
|
|
16973
|
+
type: 'integer',
|
|
16974
|
+
description:
|
|
16975
|
+
'Number of strikes to assign to the user when takedown is applied.',
|
|
16976
|
+
},
|
|
16977
|
+
strikeExpiresAt: {
|
|
16978
|
+
type: 'string',
|
|
16979
|
+
format: 'datetime',
|
|
16980
|
+
description:
|
|
16981
|
+
'When the strike should expire. If not provided, the strike never expires.',
|
|
16982
|
+
},
|
|
16983
|
+
emailContent: {
|
|
16984
|
+
type: 'string',
|
|
16985
|
+
description: 'Email content to be sent to the user upon takedown.',
|
|
16986
|
+
},
|
|
16987
|
+
emailSubject: {
|
|
16988
|
+
type: 'string',
|
|
16989
|
+
description:
|
|
16990
|
+
'Subject of the email to be sent to the user upon takedown.',
|
|
16991
|
+
},
|
|
16953
16992
|
},
|
|
16954
16993
|
},
|
|
16955
16994
|
schedulingConfig: {
|
|
@@ -308,6 +308,8 @@ export interface ModEventTakedown {
|
|
|
308
308
|
policies?: string[]
|
|
309
309
|
/** Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.). */
|
|
310
310
|
severityLevel?: string
|
|
311
|
+
/** List of services where the takedown should be applied. If empty or not provided, takedown is applied on all configured services. */
|
|
312
|
+
targetServices?: ('appview' | 'pds' | (string & {}))[]
|
|
311
313
|
/** Number of strikes to assign to the user for this violation. */
|
|
312
314
|
strikeCount?: number
|
|
313
315
|
/** When the strike should expire. If not provided, the strike never expires. */
|
|
@@ -634,6 +636,8 @@ export interface ModEventEmail {
|
|
|
634
636
|
strikeCount?: number
|
|
635
637
|
/** When the strike should expire. If not provided, the strike never expires. */
|
|
636
638
|
strikeExpiresAt?: string
|
|
639
|
+
/** Indicates whether the email was successfully delivered to the user's inbox. */
|
|
640
|
+
isDelivered?: boolean
|
|
637
641
|
}
|
|
638
642
|
|
|
639
643
|
const hashModEventEmail = 'modEventEmail'
|
|
@@ -56,6 +56,16 @@ export interface Takedown {
|
|
|
56
56
|
acknowledgeAccountSubjects?: boolean
|
|
57
57
|
/** Names/Keywords of the policies that drove the decision. */
|
|
58
58
|
policies?: string[]
|
|
59
|
+
/** Severity level of the violation (e.g., 'sev-0', 'sev-1', 'sev-2', etc.). */
|
|
60
|
+
severityLevel?: string
|
|
61
|
+
/** Number of strikes to assign to the user when takedown is applied. */
|
|
62
|
+
strikeCount?: number
|
|
63
|
+
/** When the strike should expire. If not provided, the strike never expires. */
|
|
64
|
+
strikeExpiresAt?: string
|
|
65
|
+
/** Email content to be sent to the user upon takedown. */
|
|
66
|
+
emailContent?: string
|
|
67
|
+
/** Subject of the email to be sent to the user upon takedown. */
|
|
68
|
+
emailSubject?: string
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
const hashTakedown = 'takedown'
|
package/src/mod-service/index.ts
CHANGED
|
@@ -494,6 +494,7 @@ export class ModerationService {
|
|
|
494
494
|
|
|
495
495
|
if (isModEventEmail(event)) {
|
|
496
496
|
meta.subjectLine = event.subjectLine
|
|
497
|
+
meta.isDelivered = !!event.isDelivered
|
|
497
498
|
if (event.content) {
|
|
498
499
|
meta.content = event.content
|
|
499
500
|
}
|
|
@@ -572,6 +573,10 @@ export class ModerationService {
|
|
|
572
573
|
meta.policies = event.policies.join(',')
|
|
573
574
|
}
|
|
574
575
|
|
|
576
|
+
if (isModEventTakedown(event) && event.targetServices?.length) {
|
|
577
|
+
meta.targetServices = event.targetServices.join(',')
|
|
578
|
+
}
|
|
579
|
+
|
|
575
580
|
// Keep trace of reports that came in while the reporter was in muted stated
|
|
576
581
|
if (isModEventReport(event)) {
|
|
577
582
|
const isReportingMuted = await this.isReportingMutedForSubject(createdBy)
|
|
@@ -773,17 +778,20 @@ export class ModerationService {
|
|
|
773
778
|
async takedownRepo(
|
|
774
779
|
subject: RepoSubject,
|
|
775
780
|
takedownId: number,
|
|
781
|
+
targetServices: Set<string>,
|
|
776
782
|
isSuspend = false,
|
|
777
783
|
) {
|
|
778
784
|
const takedownRef = `BSKY-${
|
|
779
785
|
isSuspend ? 'SUSPEND' : 'TAKEDOWN'
|
|
780
786
|
}-${takedownId}`
|
|
781
787
|
|
|
782
|
-
const values = this.eventPusher
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
788
|
+
const values = this.eventPusher
|
|
789
|
+
.getTakedownServices(targetServices)
|
|
790
|
+
.map((eventType) => ({
|
|
791
|
+
eventType,
|
|
792
|
+
subjectDid: subject.did,
|
|
793
|
+
takedownRef,
|
|
794
|
+
}))
|
|
787
795
|
|
|
788
796
|
const repoEvts = await this.db.db
|
|
789
797
|
.insertInto('repo_push_event')
|
|
@@ -849,7 +857,11 @@ export class ModerationService {
|
|
|
849
857
|
})
|
|
850
858
|
}
|
|
851
859
|
|
|
852
|
-
async takedownRecord(
|
|
860
|
+
async takedownRecord(
|
|
861
|
+
subject: RecordSubject,
|
|
862
|
+
takedownId: number,
|
|
863
|
+
targetServices: Set<string>,
|
|
864
|
+
) {
|
|
853
865
|
this.db.assertTransaction()
|
|
854
866
|
await this.formatAndCreateLabels(subject.uri, subject.cid, {
|
|
855
867
|
create: [TAKEDOWN_LABEL],
|
|
@@ -859,7 +871,9 @@ export class ModerationService {
|
|
|
859
871
|
const blobCids = subject.blobCids
|
|
860
872
|
if (blobCids && blobCids.length > 0) {
|
|
861
873
|
const blobValues: Insertable<BlobPushEvent>[] = []
|
|
862
|
-
for (const eventType of this.eventPusher.
|
|
874
|
+
for (const eventType of this.eventPusher.getTakedownServices(
|
|
875
|
+
targetServices,
|
|
876
|
+
)) {
|
|
863
877
|
for (const cid of blobCids) {
|
|
864
878
|
blobValues.push({
|
|
865
879
|
eventType,
|
package/src/mod-service/views.ts
CHANGED
|
@@ -195,6 +195,15 @@ export class ModerationViews {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
if (isModEventTakedown(event)) {
|
|
199
|
+
if (
|
|
200
|
+
typeof meta.targetServices === 'string' &&
|
|
201
|
+
meta.targetServices.length > 0
|
|
202
|
+
) {
|
|
203
|
+
event.targetServices = meta.targetServices.split(',')
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
198
207
|
if (isModEventLabel(event)) {
|
|
199
208
|
event.createLabelVals = row.createLabelVals?.length
|
|
200
209
|
? row.createLabelVals.split(' ')
|
|
@@ -229,6 +238,7 @@ export class ModerationViews {
|
|
|
229
238
|
if (isModEventEmail(event)) {
|
|
230
239
|
event.content = ifString(meta.content)!
|
|
231
240
|
event.subjectLine = ifString(meta.subjectLine)!
|
|
241
|
+
event.isDelivered = !!meta.isDelivered
|
|
232
242
|
}
|
|
233
243
|
|
|
234
244
|
if (isModEventComment(event) && meta.sticky) {
|
|
@@ -198,6 +198,21 @@ export const settingValidators = new Map<
|
|
|
198
198
|
hasDefault = true
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
|
+
|
|
202
|
+
if (severityVal['targetServices'] !== undefined) {
|
|
203
|
+
if (!Array.isArray(severityVal['targetServices'])) {
|
|
204
|
+
throw new InvalidRequestError(
|
|
205
|
+
`targetServices must be an array for severity level ${severityKey} in policy ${key}`,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
for (const service of severityVal['targetServices']) {
|
|
209
|
+
if (typeof service !== 'string') {
|
|
210
|
+
throw new InvalidRequestError(
|
|
211
|
+
`Each target service must be a string for severity level ${severityKey} in policy ${key}`,
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
201
216
|
}
|
|
202
217
|
}
|
|
203
218
|
}
|
|
@@ -2,7 +2,8 @@ import { AtpAgent } from '@atproto/api'
|
|
|
2
2
|
import { cborEncode } from '@atproto/common'
|
|
3
3
|
import { Secp256k1Keypair, verifySignature } from '@atproto/crypto'
|
|
4
4
|
import { EXAMPLE_LABELER, TestNetwork } from '@atproto/dev-env'
|
|
5
|
-
import { DisconnectError
|
|
5
|
+
import { DisconnectError } from '@atproto/ws-client'
|
|
6
|
+
import { Subscription } from '@atproto/xrpc-server'
|
|
6
7
|
import { ids, lexicons } from '../src/lexicon/lexicons'
|
|
7
8
|
import { Label } from '../src/lexicon/types/com/atproto/label/defs'
|
|
8
9
|
import {
|
|
@@ -19,13 +19,20 @@ describe('scheduled action processor', () => {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const scheduleTestAction = async (
|
|
22
|
+
const scheduleTestAction = async (
|
|
23
|
+
subject: string,
|
|
24
|
+
scheduling: any,
|
|
25
|
+
emailData?: { emailSubject?: string; emailContent?: string },
|
|
26
|
+
) => {
|
|
23
27
|
return await adminAgent.tools.ozone.moderation.scheduleAction(
|
|
24
28
|
{
|
|
25
29
|
action: {
|
|
26
30
|
$type: 'tools.ozone.moderation.scheduleAction#takedown',
|
|
27
31
|
comment: 'Test scheduled takedown',
|
|
28
32
|
policies: ['spam'],
|
|
33
|
+
severityLevel: 'sev-1',
|
|
34
|
+
strikeCount: 1,
|
|
35
|
+
...emailData,
|
|
29
36
|
},
|
|
30
37
|
subjects: [subject],
|
|
31
38
|
createdBy: 'did:plc:moderator',
|
|
@@ -74,7 +81,14 @@ describe('scheduled action processor', () => {
|
|
|
74
81
|
const testSubject = sc.dids.alice
|
|
75
82
|
|
|
76
83
|
const pastTime = new Date(Date.now() - 1000).toISOString()
|
|
77
|
-
await scheduleTestAction(
|
|
84
|
+
await scheduleTestAction(
|
|
85
|
+
testSubject,
|
|
86
|
+
{ executeAt: pastTime },
|
|
87
|
+
{
|
|
88
|
+
emailSubject: 'Test Email Subject',
|
|
89
|
+
emailContent: 'Test Email Content',
|
|
90
|
+
},
|
|
91
|
+
)
|
|
78
92
|
|
|
79
93
|
const pendingActions = await getScheduledActions(
|
|
80
94
|
['pending'],
|
|
@@ -94,12 +108,20 @@ describe('scheduled action processor', () => {
|
|
|
94
108
|
|
|
95
109
|
const modEvents = await getModerationEvents(testSubject, [
|
|
96
110
|
'tools.ozone.moderation.defs#modEventTakedown',
|
|
111
|
+
'tools.ozone.moderation.defs#modEventEmail',
|
|
97
112
|
])
|
|
98
|
-
expect(modEvents.length).toBe(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
expect(modEvents.length).toBe(2)
|
|
114
|
+
const takedownEvent = modEvents.find(
|
|
115
|
+
(e) => e.event.$type === 'tools.ozone.moderation.defs#modEventTakedown',
|
|
116
|
+
)
|
|
117
|
+
const emailEvent = modEvents.find(
|
|
118
|
+
(e) => e.event.$type === 'tools.ozone.moderation.defs#modEventEmail',
|
|
102
119
|
)
|
|
120
|
+
|
|
121
|
+
expect(takedownEvent?.event['comment']).toBeDefined()
|
|
122
|
+
|
|
123
|
+
expect(emailEvent?.event['subjectLine']).toBe('Test Email Subject')
|
|
124
|
+
expect(emailEvent?.event['content']).toBe('Test Email Content')
|
|
103
125
|
})
|
|
104
126
|
|
|
105
127
|
it('skips actions scheduled for future execution', async () => {
|
|
@@ -202,7 +224,9 @@ describe('scheduled action processor', () => {
|
|
|
202
224
|
// Verify the moderation event has all properties
|
|
203
225
|
const modEvents = await getModerationEvents(testSubject, [
|
|
204
226
|
'tools.ozone.moderation.defs#modEventTakedown',
|
|
227
|
+
'tools.ozone.moderation.defs#modEventEmail',
|
|
205
228
|
])
|
|
229
|
+
// No email was sent
|
|
206
230
|
expect(modEvents.length).toBe(1)
|
|
207
231
|
|
|
208
232
|
const takedownEvent = modEvents[0].event as ModEventTakedown
|
package/tests/takedown.test.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AtpAgent,
|
|
4
|
+
ComAtprotoAdminDefs,
|
|
5
|
+
ToolsOzoneModerationDefs,
|
|
6
|
+
} from '@atproto/api'
|
|
3
7
|
import {
|
|
4
8
|
ModeratorClient,
|
|
5
9
|
SeedClient,
|
|
@@ -12,6 +16,8 @@ describe('moderation', () => {
|
|
|
12
16
|
|
|
13
17
|
let sc: SeedClient
|
|
14
18
|
let modClient: ModeratorClient
|
|
19
|
+
let pdsAgent: AtpAgent
|
|
20
|
+
let bskyAgent: AtpAgent
|
|
15
21
|
|
|
16
22
|
const repoSubject = (did: string) => ({
|
|
17
23
|
$type: 'com.atproto.admin.defs#repoRef',
|
|
@@ -24,6 +30,8 @@ describe('moderation', () => {
|
|
|
24
30
|
})
|
|
25
31
|
sc = network.getSeedClient()
|
|
26
32
|
modClient = network.ozone.getModClient()
|
|
33
|
+
pdsAgent = network.pds.getClient()
|
|
34
|
+
bskyAgent = network.bsky.getClient()
|
|
27
35
|
await basicSeed(sc)
|
|
28
36
|
await network.processAll()
|
|
29
37
|
})
|
|
@@ -60,4 +68,38 @@ describe('moderation', () => {
|
|
|
60
68
|
assert(ComAtprotoAdminDefs.isRepoRef(subject))
|
|
61
69
|
expect(subject.did).toEqual(sc.dids.bob)
|
|
62
70
|
})
|
|
71
|
+
|
|
72
|
+
it('applies takedown only to specified service when targetServices is set', async () => {
|
|
73
|
+
await modClient.emitEvent({
|
|
74
|
+
event: {
|
|
75
|
+
$type: 'tools.ozone.moderation.defs#modEventTakedown',
|
|
76
|
+
targetServices: ['appview'],
|
|
77
|
+
},
|
|
78
|
+
subject: repoSubject(sc.dids.carol),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
await network.processAll()
|
|
82
|
+
|
|
83
|
+
const [pdsStatus, appviewStatus, carolsEvents] = await Promise.all([
|
|
84
|
+
pdsAgent.com.atproto.admin.getSubjectStatus(
|
|
85
|
+
{ did: sc.dids.carol },
|
|
86
|
+
{ headers: network.pds.adminAuthHeaders() },
|
|
87
|
+
),
|
|
88
|
+
bskyAgent.com.atproto.admin.getSubjectStatus(
|
|
89
|
+
{ did: sc.dids.carol },
|
|
90
|
+
{ headers: network.bsky.adminAuthHeaders() },
|
|
91
|
+
),
|
|
92
|
+
modClient.queryEvents({
|
|
93
|
+
subject: sc.dids.carol,
|
|
94
|
+
types: ['tools.ozone.moderation.defs#modEventTakedown'],
|
|
95
|
+
}),
|
|
96
|
+
])
|
|
97
|
+
|
|
98
|
+
expect(pdsStatus.data.takedown?.applied).toBe(false)
|
|
99
|
+
expect(appviewStatus.data.takedown?.applied).toBe(true)
|
|
100
|
+
|
|
101
|
+
const event = carolsEvents.events[0].event
|
|
102
|
+
assert(ToolsOzoneModerationDefs.isModEventTakedown(event))
|
|
103
|
+
expect(event.targetServices).toEqual(['appview'])
|
|
104
|
+
})
|
|
63
105
|
})
|