@atproto/bsky 0.0.15 → 0.0.16

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 (170) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/api/com/atproto/moderation/util.d.ts +4 -3
  3. package/dist/context.d.ts +15 -0
  4. package/dist/db/index.js +26 -1
  5. package/dist/db/index.js.map +3 -3
  6. package/dist/db/migrations/20231003T202833377Z-create-moderation-subject-status.d.ts +3 -0
  7. package/dist/db/migrations/index.d.ts +1 -0
  8. package/dist/db/pagination.d.ts +2 -1
  9. package/dist/db/{periodic-moderation-action-reversal.d.ts → periodic-moderation-event-reversal.d.ts} +3 -5
  10. package/dist/db/tables/moderation.d.ts +24 -34
  11. package/dist/feed-gen/types.d.ts +1 -1
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.js +2750 -2121
  14. package/dist/index.js.map +3 -3
  15. package/dist/lexicon/index.d.ts +11 -18
  16. package/dist/lexicon/lexicons.d.ts +414 -399
  17. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +1 -7
  18. package/dist/lexicon/types/app/bsky/graph/defs.d.ts +1 -0
  19. package/dist/lexicon/types/com/atproto/admin/defs.d.ts +114 -48
  20. package/dist/lexicon/types/com/atproto/admin/{takeModerationAction.d.ts → emitModerationEvent.d.ts} +5 -6
  21. package/dist/lexicon/types/com/atproto/admin/{getModerationAction.d.ts → getModerationEvent.d.ts} +1 -1
  22. package/dist/lexicon/types/com/atproto/admin/{getModerationActions.d.ts → queryModerationEvents.d.ts} +5 -1
  23. package/dist/lexicon/types/com/atproto/admin/{getModerationReports.d.ts → queryModerationStatuses.d.ts} +12 -6
  24. package/dist/lexicon/types/com/atproto/admin/sendEmail.d.ts +1 -0
  25. package/dist/migrate-moderation-data.d.ts +1 -0
  26. package/dist/services/actor/views.d.ts +2 -5
  27. package/dist/services/feed/index.d.ts +1 -0
  28. package/dist/services/feed/util.d.ts +9 -1
  29. package/dist/services/feed/views.d.ts +6 -17
  30. package/dist/services/graph/index.d.ts +5 -29
  31. package/dist/services/graph/types.d.ts +1 -0
  32. package/dist/services/moderation/index.d.ts +135 -72
  33. package/dist/services/moderation/pagination.d.ts +36 -0
  34. package/dist/services/moderation/status.d.ts +13 -0
  35. package/dist/services/moderation/types.d.ts +35 -0
  36. package/dist/services/moderation/views.d.ts +18 -14
  37. package/dist/util/debug.d.ts +1 -1
  38. package/package.json +11 -11
  39. package/src/api/app/bsky/feed/getActorFeeds.ts +2 -1
  40. package/src/api/app/bsky/feed/getActorLikes.ts +1 -3
  41. package/src/api/app/bsky/feed/getAuthorFeed.ts +1 -3
  42. package/src/api/app/bsky/feed/getFeed.ts +9 -9
  43. package/src/api/app/bsky/feed/getFeedGenerator.ts +3 -0
  44. package/src/api/app/bsky/feed/getFeedGenerators.ts +2 -1
  45. package/src/api/app/bsky/feed/getListFeed.ts +1 -3
  46. package/src/api/app/bsky/feed/getPostThread.ts +15 -54
  47. package/src/api/app/bsky/feed/getPosts.ts +21 -18
  48. package/src/api/app/bsky/feed/getSuggestedFeeds.ts +2 -1
  49. package/src/api/app/bsky/feed/getTimeline.ts +1 -3
  50. package/src/api/app/bsky/feed/searchPosts.ts +20 -17
  51. package/src/api/app/bsky/graph/getList.ts +6 -3
  52. package/src/api/app/bsky/graph/getListBlocks.ts +3 -2
  53. package/src/api/app/bsky/graph/getListMutes.ts +2 -1
  54. package/src/api/app/bsky/graph/getLists.ts +2 -1
  55. package/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +3 -1
  56. package/src/api/blob-resolver.ts +6 -11
  57. package/src/api/com/atproto/admin/emitModerationEvent.ts +220 -0
  58. package/src/api/com/atproto/admin/{getModerationActions.ts → getModerationEvent.ts} +5 -11
  59. package/src/api/com/atproto/admin/getRecord.ts +1 -0
  60. package/src/api/com/atproto/admin/{getModerationReports.ts → queryModerationEvents.ts} +13 -16
  61. package/src/api/com/atproto/admin/queryModerationStatuses.ts +55 -0
  62. package/src/api/com/atproto/moderation/createReport.ts +9 -7
  63. package/src/api/com/atproto/moderation/util.ts +38 -20
  64. package/src/api/index.ts +8 -14
  65. package/src/auth.ts +29 -21
  66. package/src/auto-moderator/index.ts +26 -19
  67. package/src/context.ts +4 -0
  68. package/src/db/migrations/20231003T202833377Z-create-moderation-subject-status.ts +123 -0
  69. package/src/db/migrations/index.ts +1 -0
  70. package/src/db/pagination.ts +26 -3
  71. package/src/db/{periodic-moderation-action-reversal.ts → periodic-moderation-event-reversal.ts} +50 -46
  72. package/src/db/tables/moderation.ts +35 -52
  73. package/src/feed-gen/best-of-follows.ts +6 -3
  74. package/src/feed-gen/bsky-team.ts +1 -1
  75. package/src/feed-gen/hot-classic.ts +1 -1
  76. package/src/feed-gen/mutuals.ts +6 -2
  77. package/src/feed-gen/types.ts +1 -1
  78. package/src/feed-gen/whats-hot.ts +1 -1
  79. package/src/feed-gen/with-friends.ts +7 -3
  80. package/src/index.ts +2 -1
  81. package/src/lexicon/index.ts +30 -67
  82. package/src/lexicon/lexicons.ts +526 -491
  83. package/src/lexicon/types/app/bsky/feed/defs.ts +1 -18
  84. package/src/lexicon/types/app/bsky/graph/defs.ts +1 -0
  85. package/src/lexicon/types/com/atproto/admin/defs.ts +276 -84
  86. package/src/lexicon/types/com/atproto/admin/{takeModerationAction.ts → emitModerationEvent.ts} +13 -11
  87. package/src/lexicon/types/com/atproto/admin/{getModerationReport.ts → getModerationEvent.ts} +1 -1
  88. package/src/lexicon/types/com/atproto/admin/{getModerationActions.ts → queryModerationEvents.ts} +8 -1
  89. package/src/lexicon/types/com/atproto/admin/{getModerationReports.ts → queryModerationStatuses.ts} +21 -14
  90. package/src/lexicon/types/com/atproto/admin/sendEmail.ts +1 -0
  91. package/src/migrate-moderation-data.ts +414 -0
  92. package/src/services/actor/views.ts +5 -14
  93. package/src/services/feed/index.ts +26 -7
  94. package/src/services/feed/util.ts +47 -19
  95. package/src/services/feed/views.ts +68 -4
  96. package/src/services/graph/index.ts +21 -3
  97. package/src/services/graph/types.ts +1 -0
  98. package/src/services/indexing/plugins/block.ts +2 -3
  99. package/src/services/indexing/plugins/feed-generator.ts +2 -3
  100. package/src/services/indexing/plugins/follow.ts +2 -3
  101. package/src/services/indexing/plugins/like.ts +2 -3
  102. package/src/services/indexing/plugins/list-block.ts +2 -3
  103. package/src/services/indexing/plugins/list-item.ts +2 -3
  104. package/src/services/indexing/plugins/list.ts +2 -3
  105. package/src/services/indexing/plugins/post.ts +3 -4
  106. package/src/services/indexing/plugins/repost.ts +2 -3
  107. package/src/services/indexing/plugins/thread-gate.ts +2 -3
  108. package/src/services/label/index.ts +2 -3
  109. package/src/services/moderation/index.ts +380 -395
  110. package/src/services/moderation/pagination.ts +96 -0
  111. package/src/services/moderation/status.ts +244 -0
  112. package/src/services/moderation/types.ts +49 -0
  113. package/src/services/moderation/views.ts +278 -329
  114. package/src/util/debug.ts +2 -2
  115. package/tests/__snapshots__/feed-generation.test.ts.snap +322 -6
  116. package/tests/__snapshots__/indexing.test.ts.snap +0 -6
  117. package/tests/admin/__snapshots__/get-record.test.ts.snap +30 -132
  118. package/tests/admin/__snapshots__/get-repo.test.ts.snap +14 -60
  119. package/tests/admin/__snapshots__/moderation-events.test.ts.snap +146 -0
  120. package/tests/admin/__snapshots__/moderation-statuses.test.ts.snap +64 -0
  121. package/tests/admin/__snapshots__/moderation.test.ts.snap +0 -125
  122. package/tests/admin/get-record.test.ts +5 -9
  123. package/tests/admin/get-repo.test.ts +5 -9
  124. package/tests/admin/moderation-events.test.ts +221 -0
  125. package/tests/admin/moderation-statuses.test.ts +145 -0
  126. package/tests/admin/moderation.test.ts +512 -860
  127. package/tests/admin/repo-search.test.ts +2 -3
  128. package/tests/auto-moderator/fuzzy-matcher.test.ts +2 -1
  129. package/tests/auto-moderator/takedowns.test.ts +45 -18
  130. package/tests/feed-generation.test.ts +57 -9
  131. package/tests/views/__snapshots__/block-lists.test.ts.snap +3 -9
  132. package/tests/views/__snapshots__/blocks.test.ts.snap +0 -9
  133. package/tests/views/__snapshots__/mute-lists.test.ts.snap +5 -5
  134. package/tests/views/__snapshots__/mutes.test.ts.snap +0 -3
  135. package/tests/views/__snapshots__/thread.test.ts.snap +0 -30
  136. package/tests/views/actor-search.test.ts +2 -3
  137. package/tests/views/author-feed.test.ts +42 -36
  138. package/tests/views/follows.test.ts +40 -35
  139. package/tests/views/list-feed.test.ts +17 -9
  140. package/tests/views/notifications.test.ts +13 -9
  141. package/tests/views/profile.test.ts +20 -18
  142. package/tests/views/thread.test.ts +54 -26
  143. package/tests/views/threadgating.test.ts +51 -19
  144. package/tests/views/timeline.test.ts +21 -13
  145. package/dist/api/com/atproto/admin/resolveModerationReports.d.ts +0 -3
  146. package/dist/api/com/atproto/admin/reverseModerationAction.d.ts +0 -3
  147. package/dist/api/com/atproto/admin/takeModerationAction.d.ts +0 -3
  148. package/dist/lexicon/types/com/atproto/admin/getModerationReport.d.ts +0 -29
  149. package/dist/lexicon/types/com/atproto/admin/resolveModerationReports.d.ts +0 -36
  150. package/dist/lexicon/types/com/atproto/admin/reverseModerationAction.d.ts +0 -36
  151. package/src/api/com/atproto/admin/getModerationAction.ts +0 -44
  152. package/src/api/com/atproto/admin/getModerationReport.ts +0 -43
  153. package/src/api/com/atproto/admin/resolveModerationReports.ts +0 -24
  154. package/src/api/com/atproto/admin/reverseModerationAction.ts +0 -115
  155. package/src/api/com/atproto/admin/takeModerationAction.ts +0 -156
  156. package/src/lexicon/types/com/atproto/admin/getModerationAction.ts +0 -41
  157. package/src/lexicon/types/com/atproto/admin/resolveModerationReports.ts +0 -49
  158. package/src/lexicon/types/com/atproto/admin/reverseModerationAction.ts +0 -49
  159. package/tests/admin/__snapshots__/get-moderation-action.test.ts.snap +0 -172
  160. package/tests/admin/__snapshots__/get-moderation-actions.test.ts.snap +0 -178
  161. package/tests/admin/__snapshots__/get-moderation-report.test.ts.snap +0 -177
  162. package/tests/admin/__snapshots__/get-moderation-reports.test.ts.snap +0 -307
  163. package/tests/admin/get-moderation-action.test.ts +0 -100
  164. package/tests/admin/get-moderation-actions.test.ts +0 -164
  165. package/tests/admin/get-moderation-report.test.ts +0 -100
  166. package/tests/admin/get-moderation-reports.test.ts +0 -332
  167. /package/dist/api/com/atproto/admin/{getModerationAction.d.ts → emitModerationEvent.d.ts} +0 -0
  168. /package/dist/api/com/atproto/admin/{getModerationActions.d.ts → getModerationEvent.d.ts} +0 -0
  169. /package/dist/api/com/atproto/admin/{getModerationReport.d.ts → queryModerationEvents.d.ts} +0 -0
  170. /package/dist/api/com/atproto/admin/{getModerationReports.d.ts → queryModerationStatuses.d.ts} +0 -0
@@ -29,6 +29,19 @@ describe('views with thread gating', () => {
29
29
  await network.close()
30
30
  })
31
31
 
32
+ // check that replyDisabled state is applied correctly in a simple method like getPosts
33
+ const checkReplyDisabled = async (
34
+ uri: string,
35
+ user: string,
36
+ blocked: boolean | undefined,
37
+ ) => {
38
+ const res = await agent.api.app.bsky.feed.getPosts(
39
+ { uris: [uri] },
40
+ { headers: await network.serviceHeaders(user) },
41
+ )
42
+ expect(res.data.posts[0].viewer?.replyDisabled).toBe(blocked)
43
+ }
44
+
32
45
  it('applies gate for empty rules.', async () => {
33
46
  const post = await sc.post(sc.dids.carol, 'empty rules')
34
47
  await pdsAgent.api.app.bsky.feed.threadgate.create(
@@ -46,8 +59,9 @@ describe('views with thread gating', () => {
46
59
  )
47
60
  assert(isThreadViewPost(thread))
48
61
  expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot()
49
- expect(thread.viewer).toEqual({ canReply: false })
62
+ expect(thread.post.viewer).toEqual({ replyDisabled: true })
50
63
  expect(thread.replies?.length).toEqual(0)
64
+ await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, true)
51
65
  })
52
66
 
53
67
  it('applies gate for mention rule.', async () => {
@@ -98,7 +112,8 @@ describe('views with thread gating', () => {
98
112
  { headers: await network.serviceHeaders(sc.dids.alice) },
99
113
  )
100
114
  assert(isThreadViewPost(aliceThread))
101
- expect(aliceThread.viewer).toEqual({ canReply: false })
115
+ expect(aliceThread.post.viewer).toEqual({ replyDisabled: true })
116
+ await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, true)
102
117
  const {
103
118
  data: { thread: danThread },
104
119
  } = await agent.api.app.bsky.feed.getPostThread(
@@ -107,7 +122,8 @@ describe('views with thread gating', () => {
107
122
  )
108
123
  assert(isThreadViewPost(danThread))
109
124
  expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot()
110
- expect(danThread.viewer).toEqual({ canReply: true })
125
+ expect(danThread.post.viewer).toEqual({ replyDisabled: false })
126
+ await checkReplyDisabled(post.ref.uriStr, sc.dids.dan, false)
111
127
  const [reply, ...otherReplies] = danThread.replies ?? []
112
128
  assert(isThreadViewPost(reply))
113
129
  expect(otherReplies.length).toEqual(0)
@@ -146,7 +162,8 @@ describe('views with thread gating', () => {
146
162
  { headers: await network.serviceHeaders(sc.dids.dan) },
147
163
  )
148
164
  assert(isThreadViewPost(danThread))
149
- expect(danThread.viewer).toEqual({ canReply: false })
165
+ expect(danThread.post.viewer).toEqual({ replyDisabled: true })
166
+ await checkReplyDisabled(post.ref.uriStr, sc.dids.dan, true)
150
167
  const {
151
168
  data: { thread: aliceThread },
152
169
  } = await agent.api.app.bsky.feed.getPostThread(
@@ -155,7 +172,8 @@ describe('views with thread gating', () => {
155
172
  )
156
173
  assert(isThreadViewPost(aliceThread))
157
174
  expect(forSnapshot(aliceThread.post.threadgate)).toMatchSnapshot()
158
- expect(aliceThread.viewer).toEqual({ canReply: true })
175
+ expect(aliceThread.post.viewer).toEqual({ replyDisabled: false })
176
+ await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, false)
159
177
  const [reply, ...otherReplies] = aliceThread.replies ?? []
160
178
  assert(isThreadViewPost(reply))
161
179
  expect(otherReplies.length).toEqual(0)
@@ -235,7 +253,8 @@ describe('views with thread gating', () => {
235
253
  { headers: await network.serviceHeaders(sc.dids.bob) },
236
254
  )
237
255
  assert(isThreadViewPost(bobThread))
238
- expect(bobThread.viewer).toEqual({ canReply: false })
256
+ expect(bobThread.post.viewer).toEqual({ replyDisabled: true })
257
+ await checkReplyDisabled(post.ref.uriStr, sc.dids.bob, true)
239
258
  const {
240
259
  data: { thread: aliceThread },
241
260
  } = await agent.api.app.bsky.feed.getPostThread(
@@ -243,7 +262,8 @@ describe('views with thread gating', () => {
243
262
  { headers: await network.serviceHeaders(sc.dids.alice) },
244
263
  )
245
264
  assert(isThreadViewPost(aliceThread))
246
- expect(aliceThread.viewer).toEqual({ canReply: true })
265
+ expect(aliceThread.post.viewer).toEqual({ replyDisabled: false })
266
+ await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, false)
247
267
  const {
248
268
  data: { thread: danThread },
249
269
  } = await agent.api.app.bsky.feed.getPostThread(
@@ -252,7 +272,8 @@ describe('views with thread gating', () => {
252
272
  )
253
273
  assert(isThreadViewPost(danThread))
254
274
  expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot()
255
- expect(danThread.viewer).toEqual({ canReply: true })
275
+ expect(danThread.post.viewer).toEqual({ replyDisabled: false })
276
+ await checkReplyDisabled(post.ref.uriStr, sc.dids.dan, false)
256
277
  const [reply1, reply2, ...otherReplies] = aliceThread.replies ?? []
257
278
  assert(isThreadViewPost(reply1))
258
279
  assert(isThreadViewPost(reply2))
@@ -292,8 +313,9 @@ describe('views with thread gating', () => {
292
313
  )
293
314
  assert(isThreadViewPost(thread))
294
315
  expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot()
295
- expect(thread.viewer).toEqual({ canReply: false })
316
+ expect(thread.post.viewer).toEqual({ replyDisabled: true })
296
317
  expect(thread.replies?.length).toEqual(0)
318
+ await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, true)
297
319
  })
298
320
 
299
321
  it('applies gate for multiple rules.', async () => {
@@ -339,7 +361,8 @@ describe('views with thread gating', () => {
339
361
  { headers: await network.serviceHeaders(sc.dids.bob) },
340
362
  )
341
363
  assert(isThreadViewPost(bobThread))
342
- expect(bobThread.viewer).toEqual({ canReply: false })
364
+ expect(bobThread.post.viewer).toEqual({ replyDisabled: true })
365
+ await checkReplyDisabled(post.ref.uriStr, sc.dids.bob, true)
343
366
  const {
344
367
  data: { thread: aliceThread },
345
368
  } = await agent.api.app.bsky.feed.getPostThread(
@@ -347,7 +370,8 @@ describe('views with thread gating', () => {
347
370
  { headers: await network.serviceHeaders(sc.dids.alice) },
348
371
  )
349
372
  assert(isThreadViewPost(aliceThread))
350
- expect(aliceThread.viewer).toEqual({ canReply: true })
373
+ expect(aliceThread.post.viewer).toEqual({ replyDisabled: false })
374
+ await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, false)
351
375
  const {
352
376
  data: { thread: danThread },
353
377
  } = await agent.api.app.bsky.feed.getPostThread(
@@ -356,7 +380,8 @@ describe('views with thread gating', () => {
356
380
  )
357
381
  assert(isThreadViewPost(danThread))
358
382
  expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot()
359
- expect(danThread.viewer).toEqual({ canReply: true })
383
+ expect(danThread.post.viewer).toEqual({ replyDisabled: false })
384
+ await checkReplyDisabled(post.ref.uriStr, sc.dids.dan, false)
360
385
  const [reply1, reply2, ...otherReplies] = aliceThread.replies ?? []
361
386
  assert(isThreadViewPost(reply1))
362
387
  assert(isThreadViewPost(reply2))
@@ -387,7 +412,8 @@ describe('views with thread gating', () => {
387
412
  )
388
413
  assert(isThreadViewPost(thread))
389
414
  expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot()
390
- expect(thread.viewer).toEqual({ canReply: true })
415
+ expect(thread.post.viewer).toEqual({ replyDisabled: false })
416
+ await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, false)
391
417
  const [reply, ...otherReplies] = thread.replies ?? []
392
418
  assert(isThreadViewPost(reply))
393
419
  expect(otherReplies.length).toEqual(0)
@@ -438,7 +464,8 @@ describe('views with thread gating', () => {
438
464
  { headers: await network.serviceHeaders(sc.dids.dan) },
439
465
  )
440
466
  assert(isThreadViewPost(danThread))
441
- expect(danThread.viewer).toEqual({ canReply: false })
467
+ expect(danThread.post.viewer).toEqual({ replyDisabled: true })
468
+ await checkReplyDisabled(orphanedReply.ref.uriStr, sc.dids.dan, true)
442
469
  const {
443
470
  data: { thread: aliceThread },
444
471
  } = await agent.api.app.bsky.feed.getPostThread(
@@ -451,7 +478,8 @@ describe('views with thread gating', () => {
451
478
  aliceThread.parent.uri === post.ref.uriStr,
452
479
  )
453
480
  expect(aliceThread.post.threadgate).toMatchSnapshot()
454
- expect(aliceThread.viewer).toEqual({ canReply: true })
481
+ expect(aliceThread.post.viewer).toEqual({ replyDisabled: false })
482
+ await checkReplyDisabled(orphanedReply.ref.uriStr, sc.dids.alice, false)
455
483
  const [reply, ...otherReplies] = aliceThread.replies ?? []
456
484
  assert(isThreadViewPost(reply))
457
485
  expect(otherReplies.length).toEqual(0)
@@ -480,7 +508,8 @@ describe('views with thread gating', () => {
480
508
  )
481
509
  assert(isThreadViewPost(thread))
482
510
  expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot()
483
- expect(thread.viewer).toEqual({ canReply: true })
511
+ expect(thread.post.viewer).toEqual({ replyDisabled: false })
512
+ await checkReplyDisabled(post.ref.uriStr, sc.dids.carol, false)
484
513
  const [reply, ...otherReplies] = thread.replies ?? []
485
514
  assert(isThreadViewPost(reply))
486
515
  expect(otherReplies.length).toEqual(0)
@@ -516,10 +545,11 @@ describe('views with thread gating', () => {
516
545
  { headers: await network.serviceHeaders(sc.dids.alice) },
517
546
  )
518
547
  assert(isThreadViewPost(thread))
519
- expect(thread.viewer).toEqual({ canReply: false }) // nobody can reply to this, not even alice.
548
+ expect(thread.post.viewer).toEqual({ replyDisabled: true }) // nobody can reply to this, not even alice.
520
549
  expect(thread.replies).toBeUndefined()
521
550
  expect(thread.parent).toBeUndefined()
522
551
  expect(thread.post.threadgate).toBeUndefined()
552
+ await checkReplyDisabled(badReply.ref.uriStr, sc.dids.alice, true)
523
553
  // check feed view
524
554
  const {
525
555
  data: { feed },
@@ -552,8 +582,9 @@ describe('views with thread gating', () => {
552
582
  )
553
583
  assert(isThreadViewPost(threadA))
554
584
  expect(threadA.post.threadgate).toBeUndefined()
555
- expect(threadA.viewer).toEqual({ canReply: true })
585
+ expect(threadA.post.viewer).toEqual({})
556
586
  expect(threadA.replies?.length).toEqual(1)
587
+ await checkReplyDisabled(postA.ref.uriStr, sc.dids.alice, undefined)
557
588
  const {
558
589
  data: { thread: threadB },
559
590
  } = await agent.api.app.bsky.feed.getPostThread(
@@ -562,7 +593,8 @@ describe('views with thread gating', () => {
562
593
  )
563
594
  assert(isThreadViewPost(threadB))
564
595
  expect(threadB.post.threadgate).toBeUndefined()
565
- expect(threadB.viewer).toEqual({ canReply: true })
596
+ expect(threadB.post.viewer).toEqual({})
597
+ await checkReplyDisabled(postB.ref.uriStr, sc.dids.alice, undefined)
566
598
  expect(threadB.replies?.length).toEqual(1)
567
599
  })
568
600
  })
@@ -1,7 +1,6 @@
1
1
  import assert from 'assert'
2
2
  import AtpAgent from '@atproto/api'
3
3
  import { TestNetwork, SeedClient } from '@atproto/dev-env'
4
- import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs'
5
4
  import { forSnapshot, getOriginator, paginateAll } from '../_util'
6
5
  import basicSeed from '../seeds/basic'
7
6
  import { FeedAlgorithm } from '../../src/api/app/bsky/util/feed'
@@ -182,11 +181,11 @@ describe('timeline views', () => {
182
181
  })
183
182
 
184
183
  it('blocks posts, reposts, replies by actor takedown', async () => {
185
- const actionResults = await Promise.all(
184
+ await Promise.all(
186
185
  [bob, carol].map((did) =>
187
- agent.api.com.atproto.admin.takeModerationAction(
186
+ agent.api.com.atproto.admin.emitModerationEvent(
188
187
  {
189
- action: TAKEDOWN,
188
+ event: { $type: 'com.atproto.admin.defs#modEventTakedown' },
190
189
  subject: {
191
190
  $type: 'com.atproto.admin.defs#repoRef',
192
191
  did,
@@ -211,10 +210,14 @@ describe('timeline views', () => {
211
210
 
212
211
  // Cleanup
213
212
  await Promise.all(
214
- actionResults.map((result) =>
215
- agent.api.com.atproto.admin.reverseModerationAction(
213
+ [bob, carol].map((did) =>
214
+ agent.api.com.atproto.admin.emitModerationEvent(
216
215
  {
217
- id: result.data.id,
216
+ event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' },
217
+ subject: {
218
+ $type: 'com.atproto.admin.defs#repoRef',
219
+ did,
220
+ },
218
221
  createdBy: 'did:example:admin',
219
222
  reason: 'Y',
220
223
  },
@@ -230,11 +233,11 @@ describe('timeline views', () => {
230
233
  it('blocks posts, reposts, replies by record takedown.', async () => {
231
234
  const postRef1 = sc.posts[dan][1].ref // Repost
232
235
  const postRef2 = sc.replies[bob][0].ref // Post and reply parent
233
- const actionResults = await Promise.all(
236
+ await Promise.all(
234
237
  [postRef1, postRef2].map((postRef) =>
235
- agent.api.com.atproto.admin.takeModerationAction(
238
+ agent.api.com.atproto.admin.emitModerationEvent(
236
239
  {
237
- action: TAKEDOWN,
240
+ event: { $type: 'com.atproto.admin.defs#modEventTakedown' },
238
241
  subject: {
239
242
  $type: 'com.atproto.repo.strongRef',
240
243
  uri: postRef.uriStr,
@@ -260,10 +263,15 @@ describe('timeline views', () => {
260
263
 
261
264
  // Cleanup
262
265
  await Promise.all(
263
- actionResults.map((result) =>
264
- agent.api.com.atproto.admin.reverseModerationAction(
266
+ [postRef1, postRef2].map((postRef) =>
267
+ agent.api.com.atproto.admin.emitModerationEvent(
265
268
  {
266
- id: result.data.id,
269
+ event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' },
270
+ subject: {
271
+ $type: 'com.atproto.repo.strongRef',
272
+ uri: postRef.uriStr,
273
+ cid: postRef.cidStr,
274
+ },
267
275
  createdBy: 'did:example:admin',
268
276
  reason: 'Y',
269
277
  },
@@ -1,3 +0,0 @@
1
- import { Server } from '../../../../lexicon';
2
- import AppContext from '../../../../context';
3
- export default function (server: Server, ctx: AppContext): void;
@@ -1,3 +0,0 @@
1
- import { Server } from '../../../../lexicon';
2
- import AppContext from '../../../../context';
3
- export default function (server: Server, ctx: AppContext): void;
@@ -1,3 +0,0 @@
1
- import { Server } from '../../../../lexicon';
2
- import AppContext from '../../../../context';
3
- export default function (server: Server, ctx: AppContext): void;
@@ -1,29 +0,0 @@
1
- import express from 'express';
2
- import { HandlerAuth } from '@atproto/xrpc-server';
3
- import * as ComAtprotoAdminDefs from './defs';
4
- export interface QueryParams {
5
- id: number;
6
- }
7
- export declare type InputSchema = undefined;
8
- export declare type OutputSchema = ComAtprotoAdminDefs.ReportViewDetail;
9
- export declare type HandlerInput = undefined;
10
- export interface HandlerSuccess {
11
- encoding: 'application/json';
12
- body: OutputSchema;
13
- headers?: {
14
- [key: string]: string;
15
- };
16
- }
17
- export interface HandlerError {
18
- status: number;
19
- message?: string;
20
- }
21
- export declare type HandlerOutput = HandlerError | HandlerSuccess;
22
- export declare type HandlerReqCtx<HA extends HandlerAuth = never> = {
23
- auth: HA;
24
- params: QueryParams;
25
- input: HandlerInput;
26
- req: express.Request;
27
- res: express.Response;
28
- };
29
- export declare type Handler<HA extends HandlerAuth = never> = (ctx: HandlerReqCtx<HA>) => Promise<HandlerOutput> | HandlerOutput;
@@ -1,36 +0,0 @@
1
- import express from 'express';
2
- import { HandlerAuth } from '@atproto/xrpc-server';
3
- import * as ComAtprotoAdminDefs from './defs';
4
- export interface QueryParams {
5
- }
6
- export interface InputSchema {
7
- actionId: number;
8
- reportIds: number[];
9
- createdBy: string;
10
- [k: string]: unknown;
11
- }
12
- export declare type OutputSchema = ComAtprotoAdminDefs.ActionView;
13
- export interface HandlerInput {
14
- encoding: 'application/json';
15
- body: InputSchema;
16
- }
17
- export interface HandlerSuccess {
18
- encoding: 'application/json';
19
- body: OutputSchema;
20
- headers?: {
21
- [key: string]: string;
22
- };
23
- }
24
- export interface HandlerError {
25
- status: number;
26
- message?: string;
27
- }
28
- export declare type HandlerOutput = HandlerError | HandlerSuccess;
29
- export declare type HandlerReqCtx<HA extends HandlerAuth = never> = {
30
- auth: HA;
31
- params: QueryParams;
32
- input: HandlerInput;
33
- req: express.Request;
34
- res: express.Response;
35
- };
36
- export declare type Handler<HA extends HandlerAuth = never> = (ctx: HandlerReqCtx<HA>) => Promise<HandlerOutput> | HandlerOutput;
@@ -1,36 +0,0 @@
1
- import express from 'express';
2
- import { HandlerAuth } from '@atproto/xrpc-server';
3
- import * as ComAtprotoAdminDefs from './defs';
4
- export interface QueryParams {
5
- }
6
- export interface InputSchema {
7
- id: number;
8
- reason: string;
9
- createdBy: string;
10
- [k: string]: unknown;
11
- }
12
- export declare type OutputSchema = ComAtprotoAdminDefs.ActionView;
13
- export interface HandlerInput {
14
- encoding: 'application/json';
15
- body: InputSchema;
16
- }
17
- export interface HandlerSuccess {
18
- encoding: 'application/json';
19
- body: OutputSchema;
20
- headers?: {
21
- [key: string]: string;
22
- };
23
- }
24
- export interface HandlerError {
25
- status: number;
26
- message?: string;
27
- }
28
- export declare type HandlerOutput = HandlerError | HandlerSuccess;
29
- export declare type HandlerReqCtx<HA extends HandlerAuth = never> = {
30
- auth: HA;
31
- params: QueryParams;
32
- input: HandlerInput;
33
- req: express.Request;
34
- res: express.Response;
35
- };
36
- export declare type Handler<HA extends HandlerAuth = never> = (ctx: HandlerReqCtx<HA>) => Promise<HandlerOutput> | HandlerOutput;
@@ -1,44 +0,0 @@
1
- import { Server } from '../../../../lexicon'
2
- import AppContext from '../../../../context'
3
- import { addAccountInfoToRepoView, getPdsAccountInfo } from './util'
4
- import {
5
- isRecordView,
6
- isRepoView,
7
- } from '../../../../lexicon/types/com/atproto/admin/defs'
8
-
9
- export default function (server: Server, ctx: AppContext) {
10
- server.com.atproto.admin.getModerationAction({
11
- auth: ctx.roleVerifier,
12
- handler: async ({ params, auth }) => {
13
- const { id } = params
14
- const db = ctx.db.getPrimary()
15
- const moderationService = ctx.services.moderation(db)
16
- const result = await moderationService.getActionOrThrow(id)
17
-
18
- const [action, accountInfo] = await Promise.all([
19
- moderationService.views.actionDetail(result),
20
- getPdsAccountInfo(ctx, result.subjectDid),
21
- ])
22
-
23
- // add in pds account info if available
24
- if (isRepoView(action.subject)) {
25
- action.subject = addAccountInfoToRepoView(
26
- action.subject,
27
- accountInfo,
28
- auth.credentials.moderator,
29
- )
30
- } else if (isRecordView(action.subject)) {
31
- action.subject.repo = addAccountInfoToRepoView(
32
- action.subject.repo,
33
- accountInfo,
34
- auth.credentials.moderator,
35
- )
36
- }
37
-
38
- return {
39
- encoding: 'application/json',
40
- body: action,
41
- }
42
- },
43
- })
44
- }
@@ -1,43 +0,0 @@
1
- import { Server } from '../../../../lexicon'
2
- import AppContext from '../../../../context'
3
- import {
4
- isRecordView,
5
- isRepoView,
6
- } from '../../../../lexicon/types/com/atproto/admin/defs'
7
- import { addAccountInfoToRepoView, getPdsAccountInfo } from './util'
8
-
9
- export default function (server: Server, ctx: AppContext) {
10
- server.com.atproto.admin.getModerationReport({
11
- auth: ctx.roleVerifier,
12
- handler: async ({ params, auth }) => {
13
- const { id } = params
14
- const db = ctx.db.getPrimary()
15
- const moderationService = ctx.services.moderation(db)
16
- const result = await moderationService.getReportOrThrow(id)
17
- const [report, accountInfo] = await Promise.all([
18
- moderationService.views.reportDetail(result),
19
- getPdsAccountInfo(ctx, result.subjectDid),
20
- ])
21
-
22
- // add in pds account info if available
23
- if (isRepoView(report.subject)) {
24
- report.subject = addAccountInfoToRepoView(
25
- report.subject,
26
- accountInfo,
27
- auth.credentials.moderator,
28
- )
29
- } else if (isRecordView(report.subject)) {
30
- report.subject.repo = addAccountInfoToRepoView(
31
- report.subject.repo,
32
- accountInfo,
33
- auth.credentials.moderator,
34
- )
35
- }
36
-
37
- return {
38
- encoding: 'application/json',
39
- body: report,
40
- }
41
- },
42
- })
43
- }
@@ -1,24 +0,0 @@
1
- import { Server } from '../../../../lexicon'
2
- import AppContext from '../../../../context'
3
-
4
- export default function (server: Server, ctx: AppContext) {
5
- server.com.atproto.admin.resolveModerationReports({
6
- auth: ctx.roleVerifier,
7
- handler: async ({ input }) => {
8
- const db = ctx.db.getPrimary()
9
- const moderationService = ctx.services.moderation(db)
10
- const { actionId, reportIds, createdBy } = input.body
11
-
12
- const moderationAction = await db.transaction(async (dbTxn) => {
13
- const moderationTxn = ctx.services.moderation(dbTxn)
14
- await moderationTxn.resolveReports({ reportIds, actionId, createdBy })
15
- return await moderationTxn.getActionOrThrow(actionId)
16
- })
17
-
18
- return {
19
- encoding: 'application/json',
20
- body: await moderationService.views.action(moderationAction),
21
- }
22
- },
23
- })
24
- }
@@ -1,115 +0,0 @@
1
- import {
2
- AuthRequiredError,
3
- InvalidRequestError,
4
- UpstreamFailureError,
5
- } from '@atproto/xrpc-server'
6
- import {
7
- ACKNOWLEDGE,
8
- ESCALATE,
9
- TAKEDOWN,
10
- } from '../../../../lexicon/types/com/atproto/admin/defs'
11
- import { Server } from '../../../../lexicon'
12
- import AppContext from '../../../../context'
13
- import { retryHttp } from '../../../../util/retry'
14
-
15
- export default function (server: Server, ctx: AppContext) {
16
- server.com.atproto.admin.reverseModerationAction({
17
- auth: ctx.roleVerifier,
18
- handler: async ({ input, auth }) => {
19
- const access = auth.credentials
20
- const db = ctx.db.getPrimary()
21
- const moderationService = ctx.services.moderation(db)
22
- const { id, createdBy, reason } = input.body
23
-
24
- const { result, restored } = await db.transaction(async (dbTxn) => {
25
- const moderationTxn = ctx.services.moderation(dbTxn)
26
- const labelTxn = ctx.services.label(dbTxn)
27
- const now = new Date()
28
-
29
- const existing = await moderationTxn.getAction(id)
30
- if (!existing) {
31
- throw new InvalidRequestError('Moderation action does not exist')
32
- }
33
- if (existing.reversedAt !== null) {
34
- throw new InvalidRequestError(
35
- 'Moderation action has already been reversed',
36
- )
37
- }
38
-
39
- // apply access rules
40
-
41
- // if less than moderator access then can only reverse ack and escalation actions
42
- if (
43
- !access.moderator &&
44
- ![ACKNOWLEDGE, ESCALATE].includes(existing.action)
45
- ) {
46
- throw new AuthRequiredError(
47
- 'Must be a full moderator to reverse this type of action',
48
- )
49
- }
50
- // if less than moderator access then cannot reverse takedown on an account
51
- if (
52
- !access.moderator &&
53
- existing.action === TAKEDOWN &&
54
- existing.subjectType === 'com.atproto.admin.defs#repoRef'
55
- ) {
56
- throw new AuthRequiredError(
57
- 'Must be a full moderator to reverse an account takedown',
58
- )
59
- }
60
-
61
- const { result, restored } = await moderationTxn.revertAction({
62
- id,
63
- createdAt: now,
64
- createdBy,
65
- reason,
66
- })
67
-
68
- // invert creates & negates
69
- const { createLabelVals, negateLabelVals } = result
70
- const negate =
71
- createLabelVals && createLabelVals.length > 0
72
- ? createLabelVals.split(' ')
73
- : undefined
74
- const create =
75
- negateLabelVals && negateLabelVals.length > 0
76
- ? negateLabelVals.split(' ')
77
- : undefined
78
- await labelTxn.formatAndCreate(
79
- ctx.cfg.labelerDid,
80
- result.subjectUri ?? result.subjectDid,
81
- result.subjectCid,
82
- { create, negate },
83
- )
84
-
85
- return { result, restored }
86
- })
87
-
88
- if (restored && ctx.moderationPushAgent) {
89
- const agent = ctx.moderationPushAgent
90
- const { subjects } = restored
91
- const results = await Promise.allSettled(
92
- subjects.map((subject) =>
93
- retryHttp(() =>
94
- agent.api.com.atproto.admin.updateSubjectStatus({
95
- subject,
96
- takedown: {
97
- applied: false,
98
- },
99
- }),
100
- ),
101
- ),
102
- )
103
- const hadFailure = results.some((r) => r.status === 'rejected')
104
- if (hadFailure) {
105
- throw new UpstreamFailureError('failed to revert action on PDS')
106
- }
107
- }
108
-
109
- return {
110
- encoding: 'application/json',
111
- body: await moderationService.views.action(result),
112
- }
113
- },
114
- })
115
- }