@atproto/ozone 0.0.6 → 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 +14 -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 +440 -294
  10. package/dist/index.js.map +3 -3
  11. package/dist/lexicon/index.d.ts +2 -2
  12. package/dist/lexicon/lexicons.d.ts +67 -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/lexicon/types/{app/bsky/unspecced/getTimelineSkeleton.d.ts → com/atproto/temp/checkSignupQueue.d.ts} +3 -6
  16. package/dist/mod-service/index.d.ts +13 -4
  17. package/dist/mod-service/subject.d.ts +3 -0
  18. package/dist/mod-service/types.d.ts +2 -0
  19. package/package.json +6 -6
  20. package/src/api/admin/emitModerationEvent.ts +9 -6
  21. package/src/api/admin/queryModerationEvents.ts +14 -0
  22. package/src/api/moderation/util.ts +1 -0
  23. package/src/api/temp/fetchLabels.ts +36 -21
  24. package/src/context.ts +1 -0
  25. package/src/daemon/context.ts +1 -0
  26. package/src/db/migrations/20240201T051104136Z-mod-event-blobs.ts +15 -0
  27. package/src/db/migrations/index.ts +1 -0
  28. package/src/db/schema/moderation_event.ts +1 -0
  29. package/src/db/types.ts +6 -1
  30. package/src/lexicon/index.ts +12 -12
  31. package/src/lexicon/lexicons.ts +72 -50
  32. package/src/lexicon/types/com/atproto/admin/defs.ts +2 -0
  33. package/src/lexicon/types/com/atproto/admin/queryModerationEvents.ts +13 -0
  34. package/src/lexicon/types/{app/bsky/unspecced/getTimelineSkeleton.ts → com/atproto/temp/checkSignupQueue.ts} +4 -8
  35. package/src/mod-service/index.ts +142 -64
  36. package/src/mod-service/status.ts +3 -2
  37. package/src/mod-service/subject.ts +9 -2
  38. package/src/mod-service/types.ts +4 -0
  39. package/src/mod-service/views.ts +1 -1
  40. package/tests/__snapshots__/get-record.test.ts.snap +16 -0
  41. package/tests/__snapshots__/get-repo.test.ts.snap +9 -1
  42. package/tests/moderation-appeals.test.ts +1 -1
  43. package/tests/moderation-events.test.ts +161 -8
  44. package/tests/moderation-statuses.test.ts +55 -0
  45. package/tests/moderation.test.ts +133 -34
@@ -91,6 +91,7 @@ export const schemaDict = {
91
91
  'lex:com.atproto.admin.defs#modEventEscalate',
92
92
  'lex:com.atproto.admin.defs#modEventMute',
93
93
  'lex:com.atproto.admin.defs#modEventEmail',
94
+ 'lex:com.atproto.admin.defs#modEventResolveAppeal',
94
95
  ],
95
96
  },
96
97
  subject: {
@@ -147,6 +148,7 @@ export const schemaDict = {
147
148
  'lex:com.atproto.admin.defs#modEventAcknowledge',
148
149
  'lex:com.atproto.admin.defs#modEventEscalate',
149
150
  'lex:com.atproto.admin.defs#modEventMute',
151
+ 'lex:com.atproto.admin.defs#modEventEmail',
150
152
  'lex:com.atproto.admin.defs#modEventResolveAppeal',
151
153
  ],
152
154
  },
@@ -1450,6 +1452,16 @@ export const schemaDict = {
1450
1452
  description:
1451
1453
  'Sort direction for the events. Defaults to descending order of created at timestamp.',
1452
1454
  },
1455
+ createdAfter: {
1456
+ type: 'string',
1457
+ format: 'datetime',
1458
+ description: 'Retrieve events created after a given timestamp',
1459
+ },
1460
+ createdBefore: {
1461
+ type: 'string',
1462
+ format: 'datetime',
1463
+ description: 'Retrieve events created before a given timestamp',
1464
+ },
1453
1465
  subject: {
1454
1466
  type: 'string',
1455
1467
  format: 'uri',
@@ -1466,6 +1478,37 @@ export const schemaDict = {
1466
1478
  maximum: 100,
1467
1479
  default: 50,
1468
1480
  },
1481
+ hasComment: {
1482
+ type: 'boolean',
1483
+ description: 'If true, only events with comments are returned',
1484
+ },
1485
+ comment: {
1486
+ type: 'string',
1487
+ description:
1488
+ 'If specified, only events with comments containing the keyword are returned',
1489
+ },
1490
+ addedLabels: {
1491
+ type: 'array',
1492
+ items: {
1493
+ type: 'string',
1494
+ },
1495
+ description:
1496
+ 'If specified, only events where all of these labels were added are returned',
1497
+ },
1498
+ removedLabels: {
1499
+ type: 'array',
1500
+ items: {
1501
+ type: 'string',
1502
+ },
1503
+ description:
1504
+ 'If specified, only events where all of these labels were removed are returned',
1505
+ },
1506
+ reportTypes: {
1507
+ type: 'array',
1508
+ items: {
1509
+ type: 'string',
1510
+ },
1511
+ },
1469
1512
  cursor: {
1470
1513
  type: 'string',
1471
1514
  },
@@ -4246,6 +4289,34 @@ export const schemaDict = {
4246
4289
  },
4247
4290
  },
4248
4291
  },
4292
+ ComAtprotoTempCheckSignupQueue: {
4293
+ lexicon: 1,
4294
+ id: 'com.atproto.temp.checkSignupQueue',
4295
+ defs: {
4296
+ main: {
4297
+ type: 'query',
4298
+ description: 'Check accounts location in signup queue.',
4299
+ output: {
4300
+ encoding: 'application/json',
4301
+ schema: {
4302
+ type: 'object',
4303
+ required: ['activated'],
4304
+ properties: {
4305
+ activated: {
4306
+ type: 'boolean',
4307
+ },
4308
+ placeInQueue: {
4309
+ type: 'integer',
4310
+ },
4311
+ estimatedTimeMs: {
4312
+ type: 'integer',
4313
+ },
4314
+ },
4315
+ },
4316
+ },
4317
+ },
4318
+ },
4319
+ },
4249
4320
  ComAtprotoTempFetchLabels: {
4250
4321
  lexicon: 1,
4251
4322
  id: 'com.atproto.temp.fetchLabels',
@@ -8072,55 +8143,6 @@ export const schemaDict = {
8072
8143
  },
8073
8144
  },
8074
8145
  },
8075
- AppBskyUnspeccedGetTimelineSkeleton: {
8076
- lexicon: 1,
8077
- id: 'app.bsky.unspecced.getTimelineSkeleton',
8078
- defs: {
8079
- main: {
8080
- type: 'query',
8081
- description:
8082
- 'DEPRECATED: a skeleton of a timeline. Unspecced and will be unavailable soon.',
8083
- parameters: {
8084
- type: 'params',
8085
- properties: {
8086
- limit: {
8087
- type: 'integer',
8088
- minimum: 1,
8089
- maximum: 100,
8090
- default: 50,
8091
- },
8092
- cursor: {
8093
- type: 'string',
8094
- },
8095
- },
8096
- },
8097
- output: {
8098
- encoding: 'application/json',
8099
- schema: {
8100
- type: 'object',
8101
- required: ['feed'],
8102
- properties: {
8103
- cursor: {
8104
- type: 'string',
8105
- },
8106
- feed: {
8107
- type: 'array',
8108
- items: {
8109
- type: 'ref',
8110
- ref: 'lex:app.bsky.feed.defs#skeletonFeedPost',
8111
- },
8112
- },
8113
- },
8114
- },
8115
- },
8116
- errors: [
8117
- {
8118
- name: 'UnknownFeed',
8119
- },
8120
- ],
8121
- },
8122
- },
8123
- },
8124
8146
  AppBskyUnspeccedSearchActorsSkeleton: {
8125
8147
  lexicon: 1,
8126
8148
  id: 'app.bsky.unspecced.searchActorsSkeleton',
@@ -8336,6 +8358,7 @@ export const ids = {
8336
8358
  ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate',
8337
8359
  ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl',
8338
8360
  ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos',
8361
+ ComAtprotoTempCheckSignupQueue: 'com.atproto.temp.checkSignupQueue',
8339
8362
  ComAtprotoTempFetchLabels: 'com.atproto.temp.fetchLabels',
8340
8363
  ComAtprotoTempImportRepo: 'com.atproto.temp.importRepo',
8341
8364
  ComAtprotoTempPushBlob: 'com.atproto.temp.pushBlob',
@@ -8409,7 +8432,6 @@ export const ids = {
8409
8432
  'app.bsky.unspecced.getPopularFeedGenerators',
8410
8433
  AppBskyUnspeccedGetTaggedSuggestions:
8411
8434
  'app.bsky.unspecced.getTaggedSuggestions',
8412
- AppBskyUnspeccedGetTimelineSkeleton: 'app.bsky.unspecced.getTimelineSkeleton',
8413
8435
  AppBskyUnspeccedSearchActorsSkeleton:
8414
8436
  'app.bsky.unspecced.searchActorsSkeleton',
8415
8437
  AppBskyUnspeccedSearchPostsSkeleton: 'app.bsky.unspecced.searchPostsSkeleton',
@@ -40,6 +40,7 @@ export interface ModEventView {
40
40
  | ModEventEscalate
41
41
  | ModEventMute
42
42
  | ModEventEmail
43
+ | ModEventResolveAppeal
43
44
  | { $type: string; [k: string]: unknown }
44
45
  subject:
45
46
  | RepoRef
@@ -76,6 +77,7 @@ export interface ModEventViewDetail {
76
77
  | ModEventAcknowledge
77
78
  | ModEventEscalate
78
79
  | ModEventMute
80
+ | ModEventEmail
79
81
  | ModEventResolveAppeal
80
82
  | { $type: string; [k: string]: unknown }
81
83
  subject:
@@ -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
 
@@ -7,18 +7,15 @@ import { lexicons } from '../../../../lexicons'
7
7
  import { isObj, hasProp } from '../../../../util'
8
8
  import { CID } from 'multiformats/cid'
9
9
  import { HandlerAuth } from '@atproto/xrpc-server'
10
- import * as AppBskyFeedDefs from '../feed/defs'
11
10
 
12
- export interface QueryParams {
13
- limit: number
14
- cursor?: string
15
- }
11
+ export interface QueryParams {}
16
12
 
17
13
  export type InputSchema = undefined
18
14
 
19
15
  export interface OutputSchema {
20
- cursor?: string
21
- feed: AppBskyFeedDefs.SkeletonFeedPost[]
16
+ activated: boolean
17
+ placeInQueue?: number
18
+ estimatedTimeMs?: number
22
19
  [k: string]: unknown
23
20
  }
24
21
 
@@ -33,7 +30,6 @@ export interface HandlerSuccess {
33
30
  export interface HandlerError {
34
31
  status: number
35
32
  message?: string
36
- error?: 'UnknownFeed'
37
33
  }
38
34
 
39
35
  export type HandlerOutput = HandlerError | HandlerSuccess
@@ -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,