@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/api/moderation/emitEvent.d.ts.map +1 -1
  3. package/dist/api/moderation/emitEvent.js +22 -8
  4. package/dist/api/moderation/emitEvent.js.map +1 -1
  5. package/dist/daemon/event-pusher.d.ts +1 -1
  6. package/dist/daemon/event-pusher.d.ts.map +1 -1
  7. package/dist/daemon/event-pusher.js +17 -5
  8. package/dist/daemon/event-pusher.js.map +1 -1
  9. package/dist/daemon/materialized-view-refresher.d.ts.map +1 -1
  10. package/dist/daemon/materialized-view-refresher.js +0 -1
  11. package/dist/daemon/materialized-view-refresher.js.map +1 -1
  12. package/dist/daemon/scheduled-action-processor.d.ts +5 -1
  13. package/dist/daemon/scheduled-action-processor.d.ts.map +1 -1
  14. package/dist/daemon/scheduled-action-processor.js +44 -2
  15. package/dist/daemon/scheduled-action-processor.js.map +1 -1
  16. package/dist/jetstream/service.d.ts +1 -1
  17. package/dist/jetstream/service.d.ts.map +1 -1
  18. package/dist/jetstream/service.js +2 -2
  19. package/dist/jetstream/service.js.map +1 -1
  20. package/dist/lexicon/lexicons.d.ts +66 -0
  21. package/dist/lexicon/lexicons.d.ts.map +1 -1
  22. package/dist/lexicon/lexicons.js +33 -0
  23. package/dist/lexicon/lexicons.js.map +1 -1
  24. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +4 -0
  25. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
  26. package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
  27. package/dist/lexicon/types/tools/ozone/moderation/scheduleAction.d.ts +10 -0
  28. package/dist/lexicon/types/tools/ozone/moderation/scheduleAction.d.ts.map +1 -1
  29. package/dist/lexicon/types/tools/ozone/moderation/scheduleAction.js.map +1 -1
  30. package/dist/mod-service/index.d.ts +2 -2
  31. package/dist/mod-service/index.d.ts.map +1 -1
  32. package/dist/mod-service/index.js +10 -4
  33. package/dist/mod-service/index.js.map +1 -1
  34. package/dist/mod-service/views.d.ts.map +1 -1
  35. package/dist/mod-service/views.js +7 -0
  36. package/dist/mod-service/views.js.map +1 -1
  37. package/dist/setting/validators.d.ts.map +1 -1
  38. package/dist/setting/validators.js +10 -0
  39. package/dist/setting/validators.js.map +1 -1
  40. package/package.json +5 -4
  41. package/src/api/moderation/emitEvent.ts +37 -10
  42. package/src/daemon/event-pusher.ts +22 -5
  43. package/src/daemon/materialized-view-refresher.ts +0 -1
  44. package/src/daemon/scheduled-action-processor.ts +59 -1
  45. package/src/jetstream/service.ts +1 -1
  46. package/src/lexicon/lexicons.ts +39 -0
  47. package/src/lexicon/types/tools/ozone/moderation/defs.ts +4 -0
  48. package/src/lexicon/types/tools/ozone/moderation/scheduleAction.ts +10 -0
  49. package/src/mod-service/index.ts +21 -7
  50. package/src/mod-service/views.ts +10 -0
  51. package/src/setting/validators.ts +15 -0
  52. package/tests/query-labels.test.ts +2 -1
  53. package/tests/scheduled-action-processor.test.ts +30 -6
  54. package/tests/takedown.test.ts +43 -1
@@ -1,4 +1,4 @@
1
- import { WebSocketKeepAlive } from '@atproto/xrpc-server'
1
+ import { WebSocketKeepAlive } from '@atproto/ws-client'
2
2
 
3
3
  type JetstreamRecord = Record<string, unknown>
4
4
  type OnCreateCallback<T extends JetstreamRecord> = (
@@ -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'
@@ -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.takedowns.map((eventType) => ({
783
- eventType,
784
- subjectDid: subject.did,
785
- takedownRef,
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(subject: RecordSubject, takedownId: number) {
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.takedowns) {
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,
@@ -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, Subscription } from '@atproto/xrpc-server'
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 (subject: string, scheduling: any) => {
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(testSubject, { executeAt: pastTime })
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(1)
99
-
100
- expect(modEvents[0].event['comment']).toContain(
101
- '[SCHEDULED_ACTION] Test scheduled takedown',
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
@@ -1,5 +1,9 @@
1
1
  import assert from 'node:assert'
2
- import { ComAtprotoAdminDefs, ToolsOzoneModerationDefs } from '@atproto/api'
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
  })