@atproto/ozone 0.0.7 → 0.0.8

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 (45) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE.txt +1 -1
  3. package/dist/db/index.js +15 -1
  4. package/dist/db/index.js.map +3 -3
  5. package/dist/db/migrations/20240201T051104136Z-mod-event-blobs.d.ts +3 -0
  6. package/dist/db/migrations/index.d.ts +1 -0
  7. package/dist/db/schema/moderation_event.d.ts +1 -0
  8. package/dist/db/types.d.ts +1 -0
  9. package/dist/index.js +276 -202
  10. package/dist/index.js.map +3 -3
  11. package/dist/lexicon/index.d.ts +0 -2
  12. package/dist/lexicon/lexicons.d.ts +38 -47
  13. package/dist/lexicon/types/com/atproto/admin/defs.d.ts +2 -2
  14. package/dist/lexicon/types/com/atproto/admin/queryModerationEvents.d.ts +7 -0
  15. package/dist/mod-service/index.d.ts +13 -4
  16. package/dist/mod-service/subject.d.ts +3 -0
  17. package/dist/mod-service/types.d.ts +2 -0
  18. package/package.json +5 -5
  19. package/src/api/admin/emitModerationEvent.ts +9 -6
  20. package/src/api/admin/queryModerationEvents.ts +14 -0
  21. package/src/api/moderation/util.ts +1 -0
  22. package/src/api/temp/fetchLabels.ts +36 -21
  23. package/src/context.ts +1 -0
  24. package/src/daemon/context.ts +1 -0
  25. package/src/db/migrations/20240201T051104136Z-mod-event-blobs.ts +15 -0
  26. package/src/db/migrations/index.ts +1 -0
  27. package/src/db/schema/moderation_event.ts +1 -0
  28. package/src/db/types.ts +6 -1
  29. package/src/lexicon/index.ts +0 -12
  30. package/src/lexicon/lexicons.ts +43 -50
  31. package/src/lexicon/types/com/atproto/admin/defs.ts +2 -0
  32. package/src/lexicon/types/com/atproto/admin/queryModerationEvents.ts +13 -0
  33. package/src/mod-service/index.ts +142 -64
  34. package/src/mod-service/status.ts +3 -2
  35. package/src/mod-service/subject.ts +9 -2
  36. package/src/mod-service/types.ts +4 -0
  37. package/src/mod-service/views.ts +1 -1
  38. package/tests/__snapshots__/get-record.test.ts.snap +16 -0
  39. package/tests/__snapshots__/get-repo.test.ts.snap +9 -1
  40. package/tests/moderation-appeals.test.ts +1 -1
  41. package/tests/moderation-events.test.ts +161 -8
  42. package/tests/moderation-statuses.test.ts +55 -0
  43. package/tests/moderation.test.ts +133 -34
  44. package/dist/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.d.ts +0 -35
  45. package/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts +0 -49
@@ -15,10 +15,23 @@ export interface QueryParams {
15
15
  createdBy?: string
16
16
  /** Sort direction for the events. Defaults to descending order of created at timestamp. */
17
17
  sortDirection: 'asc' | 'desc'
18
+ /** Retrieve events created after a given timestamp */
19
+ createdAfter?: string
20
+ /** Retrieve events created before a given timestamp */
21
+ createdBefore?: string
18
22
  subject?: string
19
23
  /** If true, events on all record types (posts, lists, profile etc.) owned by the did are returned */
20
24
  includeAllUserRecords: boolean
21
25
  limit: number
26
+ /** If true, only events with comments are returned */
27
+ hasComment?: boolean
28
+ /** If specified, only events with comments containing the keyword are returned */
29
+ comment?: string
30
+ /** If specified, only events where all of these labels were added are returned */
31
+ addedLabels?: string[]
32
+ /** If specified, only events where all of these labels were removed are returned */
33
+ removedLabels?: string[]
34
+ reportTypes?: string[]
22
35
  cursor?: string
23
36
  }
24
37
 
@@ -24,6 +24,8 @@ import {
24
24
  ModerationEventRow,
25
25
  ModerationSubjectStatusRow,
26
26
  ReversibleModerationEvent,
27
+ UNSPECCED_TAKEDOWN_BLOBS_LABEL,
28
+ UNSPECCED_TAKEDOWN_LABEL,
27
29
  } from './types'
28
30
  import { ModerationEvent } from '../db/schema/moderation_event'
29
31
  import { StatusKeyset, TimeIdKeyset, paginate } from '../db/pagination'
@@ -39,6 +41,7 @@ import {
39
41
  import { BlobPushEvent } from '../db/schema/blob_push_event'
40
42
  import { BackgroundQueue } from '../background'
41
43
  import { EventPusher } from '../daemon'
44
+ import { jsonb } from '../db/types'
42
45
 
43
46
  export type ModerationServiceCreator = (db: Database) => ModerationService
44
47
 
@@ -49,6 +52,7 @@ export class ModerationService {
49
52
  public eventPusher: EventPusher,
50
53
  public appviewAgent: AtpAgent,
51
54
  private appviewAuth: AppviewAuth,
55
+ public serverDid: string,
52
56
  ) {}
53
57
 
54
58
  static creator(
@@ -56,6 +60,7 @@ export class ModerationService {
56
60
  eventPusher: EventPusher,
57
61
  appviewAgent: AtpAgent,
58
62
  appviewAuth: AppviewAuth,
63
+ serverDid: string,
59
64
  ) {
60
65
  return (db: Database) =>
61
66
  new ModerationService(
@@ -64,6 +69,7 @@ export class ModerationService {
64
69
  eventPusher,
65
70
  appviewAgent,
66
71
  appviewAuth,
72
+ serverDid,
67
73
  )
68
74
  }
69
75
 
@@ -91,6 +97,13 @@ export class ModerationService {
91
97
  includeAllUserRecords: boolean
92
98
  types: ModerationEvent['action'][]
93
99
  sortDirection?: 'asc' | 'desc'
100
+ hasComment?: boolean
101
+ comment?: string
102
+ createdAfter?: string
103
+ createdBefore?: string
104
+ addedLabels: string[]
105
+ removedLabels: string[]
106
+ reportTypes?: string[]
94
107
  }): Promise<{ cursor?: string; events: ModerationEventRow[] }> {
95
108
  const {
96
109
  subject,
@@ -100,6 +113,13 @@ export class ModerationService {
100
113
  includeAllUserRecords,
101
114
  sortDirection = 'desc',
102
115
  types,
116
+ hasComment,
117
+ comment,
118
+ createdAfter,
119
+ createdBefore,
120
+ addedLabels,
121
+ removedLabels,
122
+ reportTypes,
103
123
  } = opts
104
124
  let builder = this.db.db.selectFrom('moderation_event').selectAll()
105
125
  if (subject) {
@@ -134,6 +154,33 @@ export class ModerationService {
134
154
  if (createdBy) {
135
155
  builder = builder.where('createdBy', '=', createdBy)
136
156
  }
157
+ if (createdAfter) {
158
+ builder = builder.where('createdAt', '>=', createdAfter)
159
+ }
160
+ if (createdBefore) {
161
+ builder = builder.where('createdAt', '<=', createdBefore)
162
+ }
163
+ if (comment) {
164
+ builder = builder.where('comment', 'ilike', `%${comment}%`)
165
+ }
166
+ if (hasComment) {
167
+ builder = builder.where('comment', 'is not', null)
168
+ }
169
+
170
+ // If multiple labels are passed, then only retrieve events where all those labels exist
171
+ if (addedLabels.length) {
172
+ addedLabels.forEach((label) => {
173
+ builder = builder.where('createLabelVals', 'ilike', `%${label}%`)
174
+ })
175
+ }
176
+ if (removedLabels.length) {
177
+ removedLabels.forEach((label) => {
178
+ builder = builder.where('negateLabelVals', 'ilike', `%${label}%`)
179
+ })
180
+ }
181
+ if (reportTypes?.length) {
182
+ builder = builder.where(sql`meta->>'reportType'`, 'in', reportTypes)
183
+ }
137
184
 
138
185
  const { ref } = this.db.db.dynamic
139
186
  const keyset = new TimeIdKeyset(
@@ -218,6 +265,8 @@ export class ModerationService {
218
265
  meta.subjectLine = event.subjectLine
219
266
  }
220
267
 
268
+ const subjectInfo = subject.info()
269
+
221
270
  const modEvent = await this.db.db
222
271
  .insertInto('moderation_event')
223
272
  .values({
@@ -236,7 +285,11 @@ export class ModerationService {
236
285
  event.durationInHours
237
286
  ? addHoursToDate(event.durationInHours, createdAt).toISOString()
238
287
  : undefined,
239
- ...subject.info(),
288
+ subjectType: subjectInfo.subjectType,
289
+ subjectDid: subjectInfo.subjectDid,
290
+ subjectUri: subjectInfo.subjectUri,
291
+ subjectCid: subjectInfo.subjectCid,
292
+ subjectBlobCids: jsonb(subjectInfo.subjectBlobCids),
240
293
  })
241
294
  .returningAll()
242
295
  .executeTakeFirstOrThrow()
@@ -359,19 +412,25 @@ export class ModerationService {
359
412
  subjectDid: subject.did,
360
413
  takedownRef,
361
414
  }))
362
- const repoEvts = await this.db.db
363
- .insertInto('repo_push_event')
364
- .values(values)
365
- .onConflict((oc) =>
366
- oc.columns(['subjectDid', 'eventType']).doUpdateSet({
367
- takedownRef,
368
- confirmedAt: null,
369
- attempts: 0,
370
- lastAttempted: null,
371
- }),
372
- )
373
- .returning('id')
374
- .execute()
415
+
416
+ const [repoEvts] = await Promise.all([
417
+ this.db.db
418
+ .insertInto('repo_push_event')
419
+ .values(values)
420
+ .onConflict((oc) =>
421
+ oc.columns(['subjectDid', 'eventType']).doUpdateSet({
422
+ takedownRef,
423
+ confirmedAt: null,
424
+ attempts: 0,
425
+ lastAttempted: null,
426
+ }),
427
+ )
428
+ .returning('id')
429
+ .execute(),
430
+ this.formatAndCreateLabels(subject.did, null, {
431
+ create: [UNSPECCED_TAKEDOWN_LABEL],
432
+ }),
433
+ ])
375
434
 
376
435
  this.db.onCommit(() => {
377
436
  this.backgroundQueue.add(async () => {
@@ -383,18 +442,23 @@ export class ModerationService {
383
442
  }
384
443
 
385
444
  async reverseTakedownRepo(subject: RepoSubject) {
386
- const repoEvts = await this.db.db
387
- .updateTable('repo_push_event')
388
- .where('eventType', 'in', TAKEDOWNS)
389
- .where('subjectDid', '=', subject.did)
390
- .set({
391
- takedownRef: null,
392
- confirmedAt: null,
393
- attempts: 0,
394
- lastAttempted: null,
395
- })
396
- .returning('id')
397
- .execute()
445
+ const [repoEvts] = await Promise.all([
446
+ this.db.db
447
+ .updateTable('repo_push_event')
448
+ .where('eventType', 'in', TAKEDOWNS)
449
+ .where('subjectDid', '=', subject.did)
450
+ .set({
451
+ takedownRef: null,
452
+ confirmedAt: null,
453
+ attempts: 0,
454
+ lastAttempted: null,
455
+ })
456
+ .returning('id')
457
+ .execute(),
458
+ this.formatAndCreateLabels(subject.did, null, {
459
+ negate: [UNSPECCED_TAKEDOWN_LABEL],
460
+ }),
461
+ ])
398
462
 
399
463
  this.db.onCommit(() => {
400
464
  this.backgroundQueue.add(async () => {
@@ -415,19 +479,27 @@ export class ModerationService {
415
479
  subjectCid: subject.cid,
416
480
  takedownRef,
417
481
  }))
418
- const recordEvts = await this.db.db
419
- .insertInto('record_push_event')
420
- .values(values)
421
- .onConflict((oc) =>
422
- oc.columns(['subjectUri', 'eventType']).doUpdateSet({
423
- takedownRef,
424
- confirmedAt: null,
425
- attempts: 0,
426
- lastAttempted: null,
427
- }),
428
- )
429
- .returning('id')
430
- .execute()
482
+ const blobCids = subject.blobCids
483
+ const labels: string[] = [UNSPECCED_TAKEDOWN_LABEL]
484
+ if (blobCids && blobCids.length > 0) {
485
+ labels.push(UNSPECCED_TAKEDOWN_BLOBS_LABEL)
486
+ }
487
+ const [recordEvts] = await Promise.all([
488
+ this.db.db
489
+ .insertInto('record_push_event')
490
+ .values(values)
491
+ .onConflict((oc) =>
492
+ oc.columns(['subjectUri', 'eventType']).doUpdateSet({
493
+ takedownRef,
494
+ confirmedAt: null,
495
+ attempts: 0,
496
+ lastAttempted: null,
497
+ }),
498
+ )
499
+ .returning('id')
500
+ .execute(),
501
+ this.formatAndCreateLabels(subject.uri, subject.cid, { create: labels }),
502
+ ])
431
503
 
432
504
  this.db.onCommit(() => {
433
505
  this.backgroundQueue.add(async () => {
@@ -437,7 +509,6 @@ export class ModerationService {
437
509
  })
438
510
  })
439
511
 
440
- const blobCids = subject.blobCids
441
512
  if (blobCids && blobCids.length > 0) {
442
513
  const blobValues: Insertable<BlobPushEvent>[] = []
443
514
  for (const eventType of TAKEDOWNS) {
@@ -478,19 +549,27 @@ export class ModerationService {
478
549
 
479
550
  async reverseTakedownRecord(subject: RecordSubject) {
480
551
  this.db.assertTransaction()
481
- const recordEvts = await this.db.db
482
- .updateTable('record_push_event')
483
- .where('eventType', 'in', TAKEDOWNS)
484
- .where('subjectDid', '=', subject.did)
485
- .where('subjectUri', '=', subject.uri)
486
- .set({
487
- takedownRef: null,
488
- confirmedAt: null,
489
- attempts: 0,
490
- lastAttempted: null,
491
- })
492
- .returning('id')
493
- .execute()
552
+ const labels: string[] = [UNSPECCED_TAKEDOWN_LABEL]
553
+ const blobCids = subject.blobCids
554
+ if (blobCids && blobCids.length > 0) {
555
+ labels.push(UNSPECCED_TAKEDOWN_BLOBS_LABEL)
556
+ }
557
+ const [recordEvts] = await Promise.all([
558
+ this.db.db
559
+ .updateTable('record_push_event')
560
+ .where('eventType', 'in', TAKEDOWNS)
561
+ .where('subjectDid', '=', subject.did)
562
+ .where('subjectUri', '=', subject.uri)
563
+ .set({
564
+ takedownRef: null,
565
+ confirmedAt: null,
566
+ attempts: 0,
567
+ lastAttempted: null,
568
+ })
569
+ .returning('id')
570
+ .execute(),
571
+ this.formatAndCreateLabels(subject.uri, subject.cid, { negate: labels }),
572
+ ])
494
573
  this.db.onCommit(() => {
495
574
  this.backgroundQueue.add(async () => {
496
575
  await Promise.all(
@@ -499,7 +578,6 @@ export class ModerationService {
499
578
  })
500
579
  })
501
580
 
502
- const blobCids = subject.blobCids
503
581
  if (blobCids && blobCids.length > 0) {
504
582
  const blobEvts = await this.db.db
505
583
  .updateTable('blob_push_event')
@@ -683,26 +761,26 @@ export class ModerationService {
683
761
  }
684
762
  }
685
763
 
686
- async isSubjectTakendown(subject: ModSubject): Promise<boolean> {
687
- const builder = this.db.db
764
+ async getStatus(
765
+ subject: ModSubject,
766
+ ): Promise<ModerationSubjectStatusRow | null> {
767
+ const result = await this.db.db
688
768
  .selectFrom('moderation_subject_status')
689
769
  .where('did', '=', subject.did)
690
- .where('recordPath', '=', subject.recordPath || '')
691
-
692
- const result = await builder.select('takendown').executeTakeFirst()
693
-
694
- return !!result?.takendown
770
+ .where('recordPath', '=', subject.recordPath ?? '')
771
+ .selectAll()
772
+ .executeTakeFirst()
773
+ return result ?? null
695
774
  }
696
775
 
697
776
  async formatAndCreateLabels(
698
- src: string,
699
777
  uri: string,
700
778
  cid: string | null,
701
779
  labels: { create?: string[]; negate?: string[] },
702
780
  ): Promise<Label[]> {
703
781
  const { create = [], negate = [] } = labels
704
782
  const toCreate = create.map((val) => ({
705
- src,
783
+ src: this.serverDid,
706
784
  uri,
707
785
  cid: cid ?? undefined,
708
786
  val,
@@ -710,7 +788,7 @@ export class ModerationService {
710
788
  cts: new Date().toISOString(),
711
789
  }))
712
790
  const toNegate = negate.map((val) => ({
713
- src,
791
+ src: this.serverDid,
714
792
  uri,
715
793
  cid: cid ?? undefined,
716
794
  val,
@@ -12,6 +12,7 @@ import { ModerationEventRow, ModerationSubjectStatusRow } from './types'
12
12
  import { HOUR } from '@atproto/common'
13
13
  import { sql } from 'kysely'
14
14
  import { REASONAPPEAL } from '../lexicon/types/com/atproto/moderation/defs'
15
+ import { jsonb } from '../db/types'
15
16
 
16
17
  const getSubjectStatusForModerationEvent = ({
17
18
  action,
@@ -191,9 +192,9 @@ export const adjustModerationSubjectStatus = async (
191
192
  }
192
193
 
193
194
  if (blobCids?.length) {
194
- const newBlobCids = sql<string[]>`${JSON.stringify(
195
+ const newBlobCids = jsonb(
195
196
  blobCids,
196
- )}` as unknown as ModerationSubjectStatusRow['blobCids']
197
+ ) as unknown as ModerationSubjectStatusRow['blobCids']
197
198
  newStatus.blobCids = newBlobCids
198
199
  subjectStatus.blobCids = newBlobCids
199
200
  }
@@ -37,7 +37,11 @@ export const subjectFromEventRow = (row: ModerationEventRow): ModSubject => {
37
37
  row.subjectUri &&
38
38
  row.subjectCid
39
39
  ) {
40
- return new RecordSubject(row.subjectUri, row.subjectCid)
40
+ return new RecordSubject(
41
+ row.subjectUri,
42
+ row.subjectCid,
43
+ row.subjectBlobCids ?? [],
44
+ )
41
45
  } else {
42
46
  return new RepoSubject(row.subjectDid)
43
47
  }
@@ -50,7 +54,7 @@ export const subjectFromStatusRow = (
50
54
  // Not too intuitive but the recordpath is basically <collection>/<rkey>
51
55
  // which is what the last 2 params of .make() arguments are
52
56
  const uri = AtUri.make(row.did, ...row.recordPath.split('/')).toString()
53
- return new RecordSubject(uri.toString(), row.recordCid)
57
+ return new RecordSubject(uri.toString(), row.recordCid, row.blobCids ?? [])
54
58
  } else {
55
59
  return new RepoSubject(row.did)
56
60
  }
@@ -61,6 +65,7 @@ type SubjectInfo = {
61
65
  subjectDid: string
62
66
  subjectUri: string | null
63
67
  subjectCid: string | null
68
+ subjectBlobCids: string[] | null
64
69
  }
65
70
 
66
71
  export interface ModSubject {
@@ -89,6 +94,7 @@ export class RepoSubject implements ModSubject {
89
94
  subjectDid: this.did,
90
95
  subjectUri: null,
91
96
  subjectCid: null,
97
+ subjectBlobCids: null,
92
98
  }
93
99
  }
94
100
  lex(): RepoRef {
@@ -124,6 +130,7 @@ export class RecordSubject implements ModSubject {
124
130
  subjectDid: this.did,
125
131
  subjectUri: this.uri,
126
132
  subjectCid: this.cid,
133
+ subjectBlobCids: this.blobCids ?? [],
127
134
  }
128
135
  }
129
136
  lex(): StrongRef {
@@ -30,3 +30,7 @@ export type ModEventType =
30
30
  | ComAtprotoAdminDefs.ModEventReport
31
31
  | ComAtprotoAdminDefs.ModEventMute
32
32
  | ComAtprotoAdminDefs.ModEventReverseTakedown
33
+
34
+ export const UNSPECCED_TAKEDOWN_LABEL = '!unspecced-takedown'
35
+
36
+ export const UNSPECCED_TAKEDOWN_BLOBS_LABEL = '!unspecced-takedown-blobs'
@@ -88,7 +88,7 @@ export class ModerationViews {
88
88
  comment: event.comment ?? undefined,
89
89
  },
90
90
  subject: subjectFromEventRow(event).lex(),
91
- subjectBlobCids: [],
91
+ subjectBlobCids: event.subjectBlobCids ?? [],
92
92
  createdBy: event.createdBy,
93
93
  createdAt: event.createdAt,
94
94
  subjectHandle: event.subjectHandle ?? undefined,
@@ -7,6 +7,14 @@ Object {
7
7
  "cid": "cids(0)",
8
8
  "indexedAt": "1970-01-01T00:00:00.000Z",
9
9
  "labels": Array [
10
+ Object {
11
+ "cid": "cids(0)",
12
+ "cts": "1970-01-01T00:00:00.000Z",
13
+ "neg": false,
14
+ "src": "user(1)",
15
+ "uri": "record(0)",
16
+ "val": "!unspecced-takedown",
17
+ },
10
18
  Object {
11
19
  "cid": "cids(0)",
12
20
  "cts": "1970-01-01T00:00:00.000Z",
@@ -93,6 +101,14 @@ Object {
93
101
  "cid": "cids(0)",
94
102
  "indexedAt": "1970-01-01T00:00:00.000Z",
95
103
  "labels": Array [
104
+ Object {
105
+ "cid": "cids(0)",
106
+ "cts": "1970-01-01T00:00:00.000Z",
107
+ "neg": false,
108
+ "src": "user(1)",
109
+ "uri": "record(0)",
110
+ "val": "!unspecced-takedown",
111
+ },
96
112
  Object {
97
113
  "cid": "cids(0)",
98
114
  "cts": "1970-01-01T00:00:00.000Z",
@@ -8,7 +8,15 @@ Object {
8
8
  "indexedAt": "1970-01-01T00:00:00.000Z",
9
9
  "invites": Array [],
10
10
  "invitesDisabled": false,
11
- "labels": Array [],
11
+ "labels": Array [
12
+ Object {
13
+ "cts": "1970-01-01T00:00:00.000Z",
14
+ "neg": false,
15
+ "src": "user(1)",
16
+ "uri": "user(0)",
17
+ "val": "!unspecced-takedown",
18
+ },
19
+ ],
12
20
  "moderation": Object {
13
21
  "subjectStatus": Object {
14
22
  "createdAt": "1970-01-01T00:00:00.000Z",
@@ -39,7 +39,7 @@ describe('moderation-appeals', () => {
39
39
 
40
40
  beforeAll(async () => {
41
41
  network = await TestNetwork.create({
42
- dbPostgresSchema: 'ozone_moderation_statuses',
42
+ dbPostgresSchema: 'ozone_moderation_appeals',
43
43
  })
44
44
  agent = network.ozone.getClient()
45
45
  pdsAgent = network.pds.getClient()