@atproto/bsky 0.0.15 → 0.0.17

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 (236) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/api/com/atproto/moderation/util.d.ts +4 -3
  3. package/dist/cache/read-through.d.ts +30 -0
  4. package/dist/config.d.ts +18 -0
  5. package/dist/context.d.ts +21 -6
  6. package/dist/daemon/config.d.ts +15 -0
  7. package/dist/daemon/context.d.ts +15 -0
  8. package/dist/daemon/index.d.ts +23 -0
  9. package/dist/daemon/logger.d.ts +3 -0
  10. package/dist/daemon/notifications.d.ts +18 -0
  11. package/dist/daemon/services.d.ts +11 -0
  12. package/dist/db/database-schema.d.ts +1 -2
  13. package/dist/db/index.js +41 -1
  14. package/dist/db/index.js.map +3 -3
  15. package/dist/db/migrations/20231003T202833377Z-create-moderation-subject-status.d.ts +3 -0
  16. package/dist/db/migrations/20231205T000257238Z-remove-did-cache.d.ts +3 -0
  17. package/dist/db/migrations/index.d.ts +2 -0
  18. package/dist/db/pagination.d.ts +2 -1
  19. package/dist/db/{periodic-moderation-action-reversal.d.ts → periodic-moderation-event-reversal.d.ts} +3 -5
  20. package/dist/db/tables/moderation.d.ts +24 -34
  21. package/dist/did-cache.d.ts +10 -7
  22. package/dist/feed-gen/types.d.ts +1 -1
  23. package/dist/index.d.ts +6 -1
  24. package/dist/index.js +4370 -2758
  25. package/dist/index.js.map +3 -3
  26. package/dist/indexer/context.d.ts +2 -0
  27. package/dist/indexer/index.d.ts +1 -0
  28. package/dist/lexicon/index.d.ts +23 -18
  29. package/dist/lexicon/lexicons.d.ts +561 -412
  30. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +1 -7
  31. package/dist/lexicon/types/app/bsky/graph/defs.d.ts +1 -0
  32. package/dist/lexicon/types/com/atproto/admin/defs.d.ts +114 -48
  33. package/dist/lexicon/types/com/atproto/admin/{resolveModerationReports.d.ts → deleteAccount.d.ts} +2 -13
  34. package/dist/lexicon/types/com/atproto/admin/{takeModerationAction.d.ts → emitModerationEvent.d.ts} +5 -6
  35. package/dist/lexicon/types/com/atproto/admin/{getModerationAction.d.ts → getModerationEvent.d.ts} +1 -1
  36. package/dist/lexicon/types/com/atproto/admin/{getModerationActions.d.ts → queryModerationEvents.d.ts} +5 -1
  37. package/dist/lexicon/types/com/atproto/admin/{getModerationReports.d.ts → queryModerationStatuses.d.ts} +12 -6
  38. package/dist/lexicon/types/com/atproto/admin/sendEmail.d.ts +1 -0
  39. package/dist/lexicon/types/com/atproto/{admin/getModerationReport.d.ts → temp/importRepo.d.ts} +10 -7
  40. package/dist/lexicon/types/com/atproto/temp/pushBlob.d.ts +25 -0
  41. package/dist/lexicon/types/com/atproto/{admin/reverseModerationAction.d.ts → temp/transferAccount.d.ts} +11 -5
  42. package/dist/logger.d.ts +1 -0
  43. package/dist/migrate-moderation-data.d.ts +1 -0
  44. package/dist/redis.d.ts +10 -1
  45. package/dist/services/actor/index.d.ts +18 -4
  46. package/dist/services/actor/views.d.ts +6 -8
  47. package/dist/services/feed/index.d.ts +7 -4
  48. package/dist/services/feed/util.d.ts +9 -1
  49. package/dist/services/feed/views.d.ts +11 -21
  50. package/dist/services/graph/index.d.ts +5 -29
  51. package/dist/services/graph/types.d.ts +1 -0
  52. package/dist/services/index.d.ts +3 -7
  53. package/dist/services/label/index.d.ts +10 -4
  54. package/dist/services/moderation/index.d.ts +134 -72
  55. package/dist/services/moderation/pagination.d.ts +36 -0
  56. package/dist/services/moderation/status.d.ts +13 -0
  57. package/dist/services/moderation/types.d.ts +35 -0
  58. package/dist/services/moderation/views.d.ts +18 -14
  59. package/dist/services/types.d.ts +3 -0
  60. package/dist/services/util/notification.d.ts +5 -0
  61. package/dist/services/util/post.d.ts +6 -6
  62. package/dist/util/debug.d.ts +1 -1
  63. package/dist/util/retry.d.ts +1 -6
  64. package/package.json +11 -11
  65. package/src/api/app/bsky/feed/getActorFeeds.ts +2 -1
  66. package/src/api/app/bsky/feed/getActorLikes.ts +1 -3
  67. package/src/api/app/bsky/feed/getAuthorFeed.ts +1 -3
  68. package/src/api/app/bsky/feed/getFeed.ts +9 -9
  69. package/src/api/app/bsky/feed/getFeedGenerator.ts +3 -0
  70. package/src/api/app/bsky/feed/getFeedGenerators.ts +2 -1
  71. package/src/api/app/bsky/feed/getListFeed.ts +1 -3
  72. package/src/api/app/bsky/feed/getPostThread.ts +15 -54
  73. package/src/api/app/bsky/feed/getPosts.ts +21 -18
  74. package/src/api/app/bsky/feed/getSuggestedFeeds.ts +2 -1
  75. package/src/api/app/bsky/feed/getTimeline.ts +1 -3
  76. package/src/api/app/bsky/feed/searchPosts.ts +20 -17
  77. package/src/api/app/bsky/graph/getList.ts +6 -3
  78. package/src/api/app/bsky/graph/getListBlocks.ts +3 -2
  79. package/src/api/app/bsky/graph/getListMutes.ts +2 -1
  80. package/src/api/app/bsky/graph/getLists.ts +2 -1
  81. package/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +3 -1
  82. package/src/api/blob-resolver.ts +6 -11
  83. package/src/api/com/atproto/admin/emitModerationEvent.ts +220 -0
  84. package/src/api/com/atproto/admin/{getModerationActions.ts → getModerationEvent.ts} +5 -11
  85. package/src/api/com/atproto/admin/getRecord.ts +1 -0
  86. package/src/api/com/atproto/admin/{getModerationReports.ts → queryModerationEvents.ts} +13 -16
  87. package/src/api/com/atproto/admin/queryModerationStatuses.ts +55 -0
  88. package/src/api/com/atproto/moderation/createReport.ts +9 -7
  89. package/src/api/com/atproto/moderation/util.ts +38 -20
  90. package/src/api/index.ts +8 -14
  91. package/src/auth.ts +29 -21
  92. package/src/auto-moderator/index.ts +26 -19
  93. package/src/cache/read-through.ts +151 -0
  94. package/src/config.ts +90 -1
  95. package/src/context.ts +11 -7
  96. package/src/daemon/config.ts +60 -0
  97. package/src/daemon/context.ts +27 -0
  98. package/src/daemon/index.ts +78 -0
  99. package/src/daemon/logger.ts +6 -0
  100. package/src/daemon/notifications.ts +54 -0
  101. package/src/daemon/services.ts +22 -0
  102. package/src/db/database-schema.ts +0 -2
  103. package/src/db/migrations/20231003T202833377Z-create-moderation-subject-status.ts +123 -0
  104. package/src/db/migrations/20231205T000257238Z-remove-did-cache.ts +14 -0
  105. package/src/db/migrations/index.ts +2 -0
  106. package/src/db/pagination.ts +26 -3
  107. package/src/db/{periodic-moderation-action-reversal.ts → periodic-moderation-event-reversal.ts} +50 -46
  108. package/src/db/tables/moderation.ts +35 -52
  109. package/src/did-cache.ts +33 -56
  110. package/src/feed-gen/bsky-team.ts +1 -1
  111. package/src/feed-gen/hot-classic.ts +1 -1
  112. package/src/feed-gen/index.ts +0 -4
  113. package/src/feed-gen/mutuals.ts +6 -2
  114. package/src/feed-gen/types.ts +1 -1
  115. package/src/index.ts +57 -17
  116. package/src/indexer/context.ts +5 -0
  117. package/src/indexer/index.ts +10 -7
  118. package/src/lexicon/index.ts +80 -67
  119. package/src/lexicon/lexicons.ts +698 -507
  120. package/src/lexicon/types/app/bsky/feed/defs.ts +1 -18
  121. package/src/lexicon/types/app/bsky/graph/defs.ts +1 -0
  122. package/src/lexicon/types/com/atproto/admin/defs.ts +276 -84
  123. package/src/lexicon/types/com/atproto/admin/{resolveModerationReports.ts → deleteAccount.ts} +2 -13
  124. package/src/lexicon/types/com/atproto/admin/{takeModerationAction.ts → emitModerationEvent.ts} +13 -11
  125. package/src/lexicon/types/com/atproto/admin/{getModerationReport.ts → getModerationEvent.ts} +1 -1
  126. package/src/lexicon/types/com/atproto/admin/{getModerationActions.ts → queryModerationEvents.ts} +8 -1
  127. package/src/lexicon/types/com/atproto/admin/{getModerationReports.ts → queryModerationStatuses.ts} +21 -14
  128. package/src/lexicon/types/com/atproto/admin/sendEmail.ts +1 -0
  129. package/src/lexicon/types/com/atproto/{admin/getModerationAction.ts → temp/importRepo.ts} +11 -7
  130. package/src/lexicon/types/com/atproto/temp/pushBlob.ts +39 -0
  131. package/src/lexicon/types/com/atproto/{admin/reverseModerationAction.ts → temp/transferAccount.ts} +18 -5
  132. package/src/logger.ts +2 -0
  133. package/src/migrate-moderation-data.ts +414 -0
  134. package/src/redis.ts +43 -3
  135. package/src/services/actor/index.ts +55 -7
  136. package/src/services/actor/views.ts +18 -21
  137. package/src/services/feed/index.ts +52 -19
  138. package/src/services/feed/util.ts +47 -19
  139. package/src/services/feed/views.ts +87 -13
  140. package/src/services/graph/index.ts +21 -3
  141. package/src/services/graph/types.ts +1 -0
  142. package/src/services/index.ts +14 -14
  143. package/src/services/indexing/index.ts +7 -10
  144. package/src/services/indexing/plugins/block.ts +2 -3
  145. package/src/services/indexing/plugins/feed-generator.ts +2 -3
  146. package/src/services/indexing/plugins/follow.ts +2 -3
  147. package/src/services/indexing/plugins/like.ts +2 -3
  148. package/src/services/indexing/plugins/list-block.ts +2 -3
  149. package/src/services/indexing/plugins/list-item.ts +2 -3
  150. package/src/services/indexing/plugins/list.ts +2 -3
  151. package/src/services/indexing/plugins/post.ts +16 -4
  152. package/src/services/indexing/plugins/repost.ts +2 -3
  153. package/src/services/indexing/plugins/thread-gate.ts +2 -3
  154. package/src/services/label/index.ts +68 -25
  155. package/src/services/moderation/index.ts +380 -395
  156. package/src/services/moderation/pagination.ts +96 -0
  157. package/src/services/moderation/status.ts +241 -0
  158. package/src/services/moderation/types.ts +49 -0
  159. package/src/services/moderation/views.ts +278 -329
  160. package/src/services/types.ts +4 -0
  161. package/src/services/util/notification.ts +70 -0
  162. package/src/util/debug.ts +2 -2
  163. package/src/util/retry.ts +1 -44
  164. package/tests/__snapshots__/feed-generation.test.ts.snap +322 -6
  165. package/tests/__snapshots__/indexing.test.ts.snap +0 -6
  166. package/tests/admin/__snapshots__/get-record.test.ts.snap +30 -132
  167. package/tests/admin/__snapshots__/get-repo.test.ts.snap +14 -60
  168. package/tests/admin/__snapshots__/moderation-events.test.ts.snap +146 -0
  169. package/tests/admin/__snapshots__/moderation-statuses.test.ts.snap +64 -0
  170. package/tests/admin/__snapshots__/moderation.test.ts.snap +0 -125
  171. package/tests/admin/get-record.test.ts +5 -9
  172. package/tests/admin/get-repo.test.ts +10 -12
  173. package/tests/admin/moderation-events.test.ts +221 -0
  174. package/tests/admin/moderation-statuses.test.ts +145 -0
  175. package/tests/admin/moderation.test.ts +512 -860
  176. package/tests/admin/repo-search.test.ts +3 -3
  177. package/tests/algos/hot-classic.test.ts +1 -2
  178. package/tests/auth.test.ts +1 -1
  179. package/tests/auto-moderator/fuzzy-matcher.test.ts +2 -1
  180. package/tests/auto-moderator/labeler.test.ts +19 -20
  181. package/tests/auto-moderator/takedowns.test.ts +61 -28
  182. package/tests/blob-resolver.test.ts +4 -2
  183. package/tests/daemon.test.ts +191 -0
  184. package/tests/did-cache.test.ts +20 -5
  185. package/tests/feed-generation.test.ts +57 -9
  186. package/tests/handle-invalidation.test.ts +1 -5
  187. package/tests/indexing.test.ts +20 -13
  188. package/tests/redis-cache.test.ts +231 -0
  189. package/tests/seeds/basic.ts +3 -0
  190. package/tests/subscription/repo.test.ts +4 -7
  191. package/tests/views/__snapshots__/block-lists.test.ts.snap +3 -9
  192. package/tests/views/__snapshots__/blocks.test.ts.snap +0 -9
  193. package/tests/views/__snapshots__/mute-lists.test.ts.snap +5 -5
  194. package/tests/views/__snapshots__/mutes.test.ts.snap +0 -3
  195. package/tests/views/__snapshots__/thread.test.ts.snap +0 -30
  196. package/tests/views/actor-search.test.ts +2 -3
  197. package/tests/views/author-feed.test.ts +42 -36
  198. package/tests/views/follows.test.ts +40 -35
  199. package/tests/views/list-feed.test.ts +17 -9
  200. package/tests/views/notifications.test.ts +13 -9
  201. package/tests/views/profile.test.ts +20 -19
  202. package/tests/views/thread.test.ts +117 -94
  203. package/tests/views/threadgating.test.ts +89 -19
  204. package/tests/views/timeline.test.ts +21 -13
  205. package/dist/api/com/atproto/admin/resolveModerationReports.d.ts +0 -3
  206. package/dist/api/com/atproto/admin/reverseModerationAction.d.ts +0 -3
  207. package/dist/api/com/atproto/admin/takeModerationAction.d.ts +0 -3
  208. package/dist/db/tables/did-cache.d.ts +0 -10
  209. package/dist/feed-gen/best-of-follows.d.ts +0 -29
  210. package/dist/feed-gen/whats-hot.d.ts +0 -29
  211. package/dist/feed-gen/with-friends.d.ts +0 -3
  212. package/dist/label-cache.d.ts +0 -19
  213. package/src/api/com/atproto/admin/getModerationAction.ts +0 -44
  214. package/src/api/com/atproto/admin/getModerationReport.ts +0 -43
  215. package/src/api/com/atproto/admin/resolveModerationReports.ts +0 -24
  216. package/src/api/com/atproto/admin/reverseModerationAction.ts +0 -115
  217. package/src/api/com/atproto/admin/takeModerationAction.ts +0 -156
  218. package/src/db/tables/did-cache.ts +0 -13
  219. package/src/feed-gen/best-of-follows.ts +0 -74
  220. package/src/feed-gen/whats-hot.ts +0 -101
  221. package/src/feed-gen/with-friends.ts +0 -39
  222. package/src/label-cache.ts +0 -90
  223. package/tests/admin/__snapshots__/get-moderation-action.test.ts.snap +0 -172
  224. package/tests/admin/__snapshots__/get-moderation-actions.test.ts.snap +0 -178
  225. package/tests/admin/__snapshots__/get-moderation-report.test.ts.snap +0 -177
  226. package/tests/admin/__snapshots__/get-moderation-reports.test.ts.snap +0 -307
  227. package/tests/admin/get-moderation-action.test.ts +0 -100
  228. package/tests/admin/get-moderation-actions.test.ts +0 -164
  229. package/tests/admin/get-moderation-report.test.ts +0 -100
  230. package/tests/admin/get-moderation-reports.test.ts +0 -332
  231. package/tests/algos/whats-hot.test.ts +0 -118
  232. package/tests/algos/with-friends.test.ts +0 -145
  233. /package/dist/api/com/atproto/admin/{getModerationAction.d.ts → emitModerationEvent.d.ts} +0 -0
  234. /package/dist/api/com/atproto/admin/{getModerationActions.d.ts → getModerationEvent.d.ts} +0 -0
  235. /package/dist/api/com/atproto/admin/{getModerationReport.d.ts → queryModerationEvents.d.ts} +0 -0
  236. /package/dist/api/com/atproto/admin/{getModerationReports.d.ts → queryModerationStatuses.d.ts} +0 -0
@@ -1,20 +1,34 @@
1
1
  import { TestNetwork, ImageRef, RecordRef, SeedClient } from '@atproto/dev-env'
2
- import { TID, cidForCbor } from '@atproto/common'
3
- import AtpAgent, { ComAtprotoAdminTakeModerationAction } from '@atproto/api'
2
+ import AtpAgent, {
3
+ ComAtprotoAdminEmitModerationEvent,
4
+ ComAtprotoAdminQueryModerationStatuses,
5
+ ComAtprotoModerationCreateReport,
6
+ } from '@atproto/api'
4
7
  import { AtUri } from '@atproto/syntax'
5
8
  import { forSnapshot } from '../_util'
6
9
  import basicSeed from '../seeds/basic'
7
10
  import {
8
- ACKNOWLEDGE,
9
- ESCALATE,
10
- FLAG,
11
- TAKEDOWN,
12
- } from '../../src/lexicon/types/com/atproto/admin/defs'
13
- import {
11
+ REASONMISLEADING,
14
12
  REASONOTHER,
15
13
  REASONSPAM,
16
14
  } from '../../src/lexicon/types/com/atproto/moderation/defs'
17
- import { PeriodicModerationActionReversal } from '../../src'
15
+ import {
16
+ ModEventLabel,
17
+ ModEventTakedown,
18
+ REVIEWCLOSED,
19
+ REVIEWESCALATED,
20
+ } from '../../src/lexicon/types/com/atproto/admin/defs'
21
+ import { PeriodicModerationEventReversal } from '../../src'
22
+
23
+ type BaseCreateReportParams =
24
+ | { account: string }
25
+ | { content: { uri: string; cid: string } }
26
+ type CreateReportParams = BaseCreateReportParams & {
27
+ author: string
28
+ } & Omit<ComAtprotoModerationCreateReport.InputSchema, 'subject'>
29
+
30
+ type TakedownParams = BaseCreateReportParams &
31
+ Omit<ComAtprotoAdminEmitModerationEvent.InputSchema, 'subject'>
18
32
 
19
33
  describe('moderation', () => {
20
34
  let network: TestNetwork
@@ -22,6 +36,99 @@ describe('moderation', () => {
22
36
  let pdsAgent: AtpAgent
23
37
  let sc: SeedClient
24
38
 
39
+ const createReport = async (params: CreateReportParams) => {
40
+ const { author, ...rest } = params
41
+ return agent.api.com.atproto.moderation.createReport(
42
+ {
43
+ // Set default type to spam
44
+ reasonType: REASONSPAM,
45
+ ...rest,
46
+ subject:
47
+ 'account' in params
48
+ ? {
49
+ $type: 'com.atproto.admin.defs#repoRef',
50
+ did: params.account,
51
+ }
52
+ : {
53
+ $type: 'com.atproto.repo.strongRef',
54
+ uri: params.content.uri,
55
+ cid: params.content.cid,
56
+ },
57
+ },
58
+ {
59
+ headers: await network.serviceHeaders(author),
60
+ encoding: 'application/json',
61
+ },
62
+ )
63
+ }
64
+
65
+ const performTakedown = async ({
66
+ durationInHours,
67
+ ...rest
68
+ }: TakedownParams & Pick<ModEventTakedown, 'durationInHours'>) =>
69
+ agent.api.com.atproto.admin.emitModerationEvent(
70
+ {
71
+ event: {
72
+ $type: 'com.atproto.admin.defs#modEventTakedown',
73
+ durationInHours,
74
+ },
75
+ subject:
76
+ 'account' in rest
77
+ ? {
78
+ $type: 'com.atproto.admin.defs#repoRef',
79
+ did: rest.account,
80
+ }
81
+ : {
82
+ $type: 'com.atproto.repo.strongRef',
83
+ uri: rest.content.uri,
84
+ cid: rest.content.cid,
85
+ },
86
+ createdBy: 'did:example:admin',
87
+ ...rest,
88
+ },
89
+ {
90
+ encoding: 'application/json',
91
+ headers: network.bsky.adminAuthHeaders(),
92
+ },
93
+ )
94
+
95
+ const performReverseTakedown = async (params: TakedownParams) =>
96
+ agent.api.com.atproto.admin.emitModerationEvent(
97
+ {
98
+ event: {
99
+ $type: 'com.atproto.admin.defs#modEventReverseTakedown',
100
+ },
101
+ subject:
102
+ 'account' in params
103
+ ? {
104
+ $type: 'com.atproto.admin.defs#repoRef',
105
+ did: params.account,
106
+ }
107
+ : {
108
+ $type: 'com.atproto.repo.strongRef',
109
+ uri: params.content.uri,
110
+ cid: params.content.cid,
111
+ },
112
+ createdBy: 'did:example:admin',
113
+ ...params,
114
+ },
115
+ {
116
+ encoding: 'application/json',
117
+ headers: network.bsky.adminAuthHeaders(),
118
+ },
119
+ )
120
+
121
+ const getStatuses = async (
122
+ params: ComAtprotoAdminQueryModerationStatuses.QueryParams,
123
+ ) => {
124
+ const { data } = await agent.api.com.atproto.admin.queryModerationStatuses(
125
+ params,
126
+ { headers: network.bsky.adminAuthHeaders() },
127
+ )
128
+
129
+ return data
130
+ }
131
+
25
132
  beforeAll(async () => {
26
133
  network = await TestNetwork.create({
27
134
  dbPostgresSchema: 'bsky_moderation',
@@ -39,89 +146,51 @@ describe('moderation', () => {
39
146
 
40
147
  describe('reporting', () => {
41
148
  it('creates reports of a repo.', async () => {
42
- const { data: reportA } =
43
- await agent.api.com.atproto.moderation.createReport(
44
- {
45
- reasonType: REASONSPAM,
46
- subject: {
47
- $type: 'com.atproto.admin.defs#repoRef',
48
- did: sc.dids.bob,
49
- },
50
- },
51
- {
52
- headers: await network.serviceHeaders(sc.dids.alice),
53
- encoding: 'application/json',
54
- },
55
- )
56
- const { data: reportB } =
57
- await agent.api.com.atproto.moderation.createReport(
58
- {
59
- reasonType: REASONOTHER,
60
- reason: 'impersonation',
61
- subject: {
62
- $type: 'com.atproto.admin.defs#repoRef',
63
- did: sc.dids.bob,
64
- },
65
- },
66
- {
67
- headers: await network.serviceHeaders(sc.dids.carol),
68
- encoding: 'application/json',
69
- },
70
- )
149
+ const { data: reportA } = await createReport({
150
+ reasonType: REASONSPAM,
151
+ account: sc.dids.bob,
152
+ author: sc.dids.alice,
153
+ })
154
+ const { data: reportB } = await createReport({
155
+ reasonType: REASONOTHER,
156
+ reason: 'impersonation',
157
+ account: sc.dids.bob,
158
+ author: sc.dids.carol,
159
+ })
71
160
  expect(forSnapshot([reportA, reportB])).toMatchSnapshot()
72
161
  })
73
162
 
74
163
  it("allows reporting a repo that doesn't exist.", async () => {
75
- const promise = agent.api.com.atproto.moderation.createReport(
76
- {
77
- reasonType: REASONSPAM,
78
- subject: {
79
- $type: 'com.atproto.admin.defs#repoRef',
80
- did: 'did:plc:unknown',
81
- },
82
- },
83
- {
84
- headers: await network.serviceHeaders(sc.dids.alice),
85
- encoding: 'application/json',
86
- },
87
- )
164
+ const promise = createReport({
165
+ reasonType: REASONSPAM,
166
+ account: 'did:plc:unknown',
167
+ author: sc.dids.alice,
168
+ })
88
169
  await expect(promise).resolves.toBeDefined()
89
170
  })
90
171
 
91
172
  it('creates reports of a record.', async () => {
92
173
  const postA = sc.posts[sc.dids.bob][0].ref
93
174
  const postB = sc.posts[sc.dids.bob][1].ref
94
- const { data: reportA } =
95
- await agent.api.com.atproto.moderation.createReport(
96
- {
97
- reasonType: REASONSPAM,
98
- subject: {
99
- $type: 'com.atproto.repo.strongRef',
100
- uri: postA.uriStr,
101
- cid: postA.cidStr,
102
- },
103
- },
104
- {
105
- headers: await network.serviceHeaders(sc.dids.alice),
106
- encoding: 'application/json',
107
- },
108
- )
109
- const { data: reportB } =
110
- await agent.api.com.atproto.moderation.createReport(
111
- {
112
- reasonType: REASONOTHER,
113
- reason: 'defamation',
114
- subject: {
115
- $type: 'com.atproto.repo.strongRef',
116
- uri: postB.uriStr,
117
- cid: postB.cidStr,
118
- },
119
- },
120
- {
121
- headers: await network.serviceHeaders(sc.dids.carol),
122
- encoding: 'application/json',
123
- },
124
- )
175
+ const { data: reportA } = await createReport({
176
+ author: sc.dids.alice,
177
+ reasonType: REASONSPAM,
178
+ content: {
179
+ $type: 'com.atproto.repo.strongRef',
180
+ uri: postA.uriStr,
181
+ cid: postA.cidStr,
182
+ },
183
+ })
184
+ const { data: reportB } = await createReport({
185
+ reasonType: REASONOTHER,
186
+ reason: 'defamation',
187
+ content: {
188
+ $type: 'com.atproto.repo.strongRef',
189
+ uri: postB.uriStr,
190
+ cid: postB.cidStr,
191
+ },
192
+ author: sc.dids.carol,
193
+ })
125
194
  expect(forSnapshot([reportA, reportB])).toMatchSnapshot()
126
195
  })
127
196
 
@@ -131,682 +200,238 @@ describe('moderation', () => {
131
200
  const postUriBad = new AtUri(postA.uriStr)
132
201
  postUriBad.rkey = 'badrkey'
133
202
 
134
- const promiseA = agent.api.com.atproto.moderation.createReport(
135
- {
136
- reasonType: REASONSPAM,
137
- subject: {
138
- $type: 'com.atproto.repo.strongRef',
139
- uri: postUriBad.toString(),
140
- cid: postA.cidStr,
141
- },
142
- },
143
- {
144
- headers: await network.serviceHeaders(sc.dids.alice),
145
- encoding: 'application/json',
203
+ const promiseA = createReport({
204
+ reasonType: REASONSPAM,
205
+ content: {
206
+ $type: 'com.atproto.repo.strongRef',
207
+ uri: postUriBad.toString(),
208
+ cid: postA.cidStr,
146
209
  },
147
- )
210
+ author: sc.dids.alice,
211
+ })
148
212
  await expect(promiseA).resolves.toBeDefined()
149
213
 
150
- const promiseB = agent.api.com.atproto.moderation.createReport(
151
- {
152
- reasonType: REASONOTHER,
153
- reason: 'defamation',
154
- subject: {
155
- $type: 'com.atproto.repo.strongRef',
156
- uri: postB.uri.toString(),
157
- cid: postA.cidStr, // bad cid
158
- },
159
- },
160
- {
161
- headers: await network.serviceHeaders(sc.dids.carol),
162
- encoding: 'application/json',
214
+ const promiseB = createReport({
215
+ reasonType: REASONOTHER,
216
+ reason: 'defamation',
217
+ content: {
218
+ $type: 'com.atproto.repo.strongRef',
219
+ uri: postB.uri.toString(),
220
+ cid: postA.cidStr, // bad cid
163
221
  },
164
- )
222
+ author: sc.dids.carol,
223
+ })
165
224
  await expect(promiseB).resolves.toBeDefined()
166
225
  })
167
226
  })
168
227
 
169
228
  describe('actioning', () => {
170
229
  it('resolves reports on repos and records.', async () => {
171
- const { data: reportA } =
172
- await agent.api.com.atproto.moderation.createReport(
173
- {
174
- reasonType: REASONSPAM,
175
- subject: {
176
- $type: 'com.atproto.admin.defs#repoRef',
177
- did: sc.dids.bob,
178
- },
179
- },
180
- {
181
- headers: await network.serviceHeaders(sc.dids.alice),
182
- encoding: 'application/json',
183
- },
184
- )
185
230
  const post = sc.posts[sc.dids.bob][1].ref
186
- const { data: reportB } =
187
- await agent.api.com.atproto.moderation.createReport(
188
- {
189
- reasonType: REASONOTHER,
190
- reason: 'defamation',
191
- subject: {
192
- $type: 'com.atproto.repo.strongRef',
193
- uri: post.uri.toString(),
194
- cid: post.cid.toString(),
195
- },
196
- },
197
- {
198
- headers: await network.serviceHeaders(sc.dids.carol),
199
- encoding: 'application/json',
200
- },
201
- )
202
- const { data: action } =
203
- await agent.api.com.atproto.admin.takeModerationAction(
204
- {
205
- action: TAKEDOWN,
206
- subject: {
207
- $type: 'com.atproto.admin.defs#repoRef',
208
- did: sc.dids.bob,
209
- },
210
- createdBy: 'did:example:admin',
211
- reason: 'Y',
212
- },
213
- {
214
- encoding: 'application/json',
215
- headers: network.bsky.adminAuthHeaders(),
216
- },
217
- )
218
- const { data: actionResolvedReports } =
219
- await agent.api.com.atproto.admin.resolveModerationReports(
220
- {
221
- actionId: action.id,
222
- reportIds: [reportB.id, reportA.id],
223
- createdBy: 'did:example:admin',
224
- },
225
- {
226
- encoding: 'application/json',
227
- headers: network.bsky.adminAuthHeaders(),
228
- },
229
- )
230
-
231
- expect(forSnapshot(actionResolvedReports)).toMatchSnapshot()
232
-
233
- // Cleanup
234
- await agent.api.com.atproto.admin.reverseModerationAction(
235
- {
236
- id: action.id,
237
- createdBy: 'did:example:admin',
238
- reason: 'Y',
239
- },
240
- {
241
- encoding: 'application/json',
242
- headers: network.bsky.adminAuthHeaders(),
243
- },
244
- )
245
- })
246
231
 
247
- it('resolves reports on missing repos and records.', async () => {
248
- const unknownDid = 'did:plc:unknown'
249
- const unknownPostUri = `at://did:plc:unknown/app.bsky.feed.post/${TID.nextStr()}`
250
- const unknownPostCid = (await cidForCbor({})).toString()
251
- // Report user and post unknown to bsky
252
- const { data: reportA } =
253
- await agent.api.com.atproto.moderation.createReport(
254
- {
255
- reasonType: REASONSPAM,
256
- subject: {
257
- $type: 'com.atproto.admin.defs#repoRef',
258
- did: unknownDid,
259
- },
260
- },
261
- {
262
- headers: await network.serviceHeaders(sc.dids.alice),
263
- encoding: 'application/json',
264
- },
265
- )
266
- const { data: reportB } =
267
- await agent.api.com.atproto.moderation.createReport(
268
- {
269
- reasonType: REASONOTHER,
270
- reason: 'defamation',
271
- subject: {
272
- $type: 'com.atproto.repo.strongRef',
273
- uri: unknownPostUri,
274
- cid: unknownPostCid,
275
- },
276
- },
277
- {
278
- headers: await network.serviceHeaders(sc.dids.carol),
279
- encoding: 'application/json',
280
- },
281
- )
282
- // Take action on deleted content
283
- const { data: action } =
284
- await agent.api.com.atproto.admin.takeModerationAction(
285
- {
286
- action: FLAG,
287
- subject: {
288
- $type: 'com.atproto.repo.strongRef',
289
- uri: unknownPostUri,
290
- cid: unknownPostCid,
291
- },
292
- createdBy: 'did:example:admin',
293
- reason: 'Y',
294
- },
295
- {
296
- encoding: 'application/json',
297
- headers: network.bsky.adminAuthHeaders(),
298
- },
299
- )
300
- await agent.api.com.atproto.admin.resolveModerationReports(
301
- {
302
- actionId: action.id,
303
- reportIds: [reportB.id, reportA.id],
304
- createdBy: 'did:example:admin',
305
- },
306
- {
307
- encoding: 'application/json',
308
- headers: network.bsky.adminAuthHeaders(),
309
- },
310
- )
311
- // Check report and action details
312
- const { data: recordActionDetail } =
313
- await agent.api.com.atproto.admin.getModerationAction(
314
- { id: action.id },
315
- { headers: network.bsky.adminAuthHeaders() },
316
- )
317
- const { data: reportADetail } =
318
- await agent.api.com.atproto.admin.getModerationReport(
319
- { id: reportA.id },
320
- { headers: network.bsky.adminAuthHeaders() },
321
- )
322
- const { data: reportBDetail } =
323
- await agent.api.com.atproto.admin.getModerationReport(
324
- { id: reportB.id },
325
- { headers: network.bsky.adminAuthHeaders() },
326
- )
327
- expect(
328
- forSnapshot({
329
- recordActionDetail,
330
- reportADetail,
331
- reportBDetail,
332
- }),
333
- ).toMatchSnapshot()
334
- // Cleanup
335
- await agent.api.com.atproto.admin.reverseModerationAction(
336
- {
337
- id: action.id,
338
- createdBy: 'did:example:admin',
339
- reason: 'Y',
340
- },
341
- {
342
- encoding: 'application/json',
343
- headers: network.bsky.adminAuthHeaders(),
344
- },
345
- )
346
- })
347
-
348
- it('does not resolve report for mismatching repo.', async () => {
349
- const { data: report } =
350
- await agent.api.com.atproto.moderation.createReport(
351
- {
352
- reasonType: REASONSPAM,
353
- subject: {
354
- $type: 'com.atproto.admin.defs#repoRef',
355
- did: sc.dids.bob,
356
- },
357
- },
358
- {
359
- headers: await network.serviceHeaders(sc.dids.alice),
360
- encoding: 'application/json',
361
- },
362
- )
363
- const { data: action } =
364
- await agent.api.com.atproto.admin.takeModerationAction(
365
- {
366
- action: TAKEDOWN,
367
- subject: {
368
- $type: 'com.atproto.admin.defs#repoRef',
369
- did: sc.dids.carol,
370
- },
371
- createdBy: 'did:example:admin',
372
- reason: 'Y',
373
- },
374
- {
375
- encoding: 'application/json',
376
- headers: network.bsky.adminAuthHeaders(),
377
- },
378
- )
379
-
380
- const promise = agent.api.com.atproto.admin.resolveModerationReports(
381
- {
382
- actionId: action.id,
383
- reportIds: [report.id],
384
- createdBy: 'did:example:admin',
385
- },
386
- {
387
- encoding: 'application/json',
388
- headers: network.bsky.adminAuthHeaders(),
389
- },
390
- )
391
-
392
- await expect(promise).rejects.toThrow(
393
- `Report ${report.id} cannot be resolved by action`,
394
- )
395
-
396
- // Cleanup
397
- await agent.api.com.atproto.admin.reverseModerationAction(
398
- {
399
- id: action.id,
400
- createdBy: 'did:example:admin',
401
- reason: 'Y',
402
- },
403
- {
404
- encoding: 'application/json',
405
- headers: network.bsky.adminAuthHeaders(),
406
- },
407
- )
408
- })
409
-
410
- it('does not resolve report for mismatching record.', async () => {
411
- const postRef1 = sc.posts[sc.dids.alice][0].ref
412
- const postRef2 = sc.posts[sc.dids.bob][0].ref
413
- const { data: report } =
414
- await agent.api.com.atproto.moderation.createReport(
415
- {
416
- reasonType: REASONSPAM,
417
- subject: {
418
- $type: 'com.atproto.repo.strongRef',
419
- uri: postRef1.uriStr,
420
- cid: postRef1.cidStr,
421
- },
422
- },
423
- {
424
- headers: await network.serviceHeaders(sc.dids.alice),
425
- encoding: 'application/json',
426
- },
427
- )
428
- const { data: action } =
429
- await agent.api.com.atproto.admin.takeModerationAction(
430
- {
431
- action: TAKEDOWN,
432
- subject: {
433
- $type: 'com.atproto.repo.strongRef',
434
- uri: postRef2.uriStr,
435
- cid: postRef2.cidStr,
436
- },
437
- createdBy: 'did:example:admin',
438
- reason: 'Y',
439
- },
440
- {
441
- encoding: 'application/json',
442
- headers: network.bsky.adminAuthHeaders(),
443
- },
444
- )
445
-
446
- const promise = agent.api.com.atproto.admin.resolveModerationReports(
447
- {
448
- actionId: action.id,
449
- reportIds: [report.id],
450
- createdBy: 'did:example:admin',
451
- },
452
- {
453
- encoding: 'application/json',
454
- headers: network.bsky.adminAuthHeaders(),
455
- },
456
- )
457
-
458
- await expect(promise).rejects.toThrow(
459
- `Report ${report.id} cannot be resolved by action`,
460
- )
461
-
462
- // Cleanup
463
- await agent.api.com.atproto.admin.reverseModerationAction(
464
- {
465
- id: action.id,
466
- createdBy: 'did:example:admin',
467
- reason: 'Y',
468
- },
469
- {
470
- encoding: 'application/json',
471
- headers: network.bsky.adminAuthHeaders(),
472
- },
473
- )
474
- })
475
-
476
- it('supports escalating and acknowledging for triage.', async () => {
477
- const postRef1 = sc.posts[sc.dids.alice][0].ref
478
- const postRef2 = sc.posts[sc.dids.bob][0].ref
479
- const { data: action1 } =
480
- await agent.api.com.atproto.admin.takeModerationAction(
481
- {
482
- action: ESCALATE,
483
- subject: {
484
- $type: 'com.atproto.repo.strongRef',
485
- uri: postRef1.uri.toString(),
486
- cid: postRef1.cid.toString(),
487
- },
488
- createdBy: 'did:example:admin',
489
- reason: 'Y',
490
- },
491
- {
492
- encoding: 'application/json',
493
- headers: network.bsky.adminAuthHeaders('triage'),
494
- },
495
- )
496
- expect(action1).toEqual(
497
- expect.objectContaining({
498
- action: ESCALATE,
499
- subject: {
500
- $type: 'com.atproto.repo.strongRef',
501
- uri: postRef1.uriStr,
502
- cid: postRef1.cidStr,
503
- },
232
+ await Promise.all([
233
+ createReport({
234
+ reasonType: REASONSPAM,
235
+ account: sc.dids.bob,
236
+ author: sc.dids.alice,
504
237
  }),
505
- )
506
- const { data: action2 } =
507
- await agent.api.com.atproto.admin.takeModerationAction(
508
- {
509
- action: ACKNOWLEDGE,
510
- subject: {
511
- $type: 'com.atproto.repo.strongRef',
512
- uri: postRef2.uri.toString(),
513
- cid: postRef2.cid.toString(),
514
- },
515
- createdBy: 'did:example:admin',
516
- reason: 'Y',
517
- },
518
- {
519
- encoding: 'application/json',
520
- headers: network.bsky.adminAuthHeaders('triage'),
521
- },
522
- )
523
- expect(action2).toEqual(
524
- expect.objectContaining({
525
- action: ACKNOWLEDGE,
526
- subject: {
527
- $type: 'com.atproto.repo.strongRef',
528
- uri: postRef2.uriStr,
529
- cid: postRef2.cidStr,
238
+ createReport({
239
+ reasonType: REASONOTHER,
240
+ reason: 'defamation',
241
+ content: {
242
+ uri: post.uri.toString(),
243
+ cid: post.cid.toString(),
530
244
  },
245
+ author: sc.dids.carol,
531
246
  }),
532
- )
533
- // Cleanup
534
- await agent.api.com.atproto.admin.reverseModerationAction(
535
- {
536
- id: action1.id,
537
- createdBy: 'did:example:admin',
538
- reason: 'Y',
539
- },
540
- {
541
- encoding: 'application/json',
542
- headers: network.bsky.adminAuthHeaders('triage'),
543
- },
544
- )
545
- await agent.api.com.atproto.admin.reverseModerationAction(
546
- {
547
- id: action2.id,
548
- createdBy: 'did:example:admin',
549
- reason: 'Y',
550
- },
551
- {
552
- encoding: 'application/json',
553
- headers: network.bsky.adminAuthHeaders('triage'),
554
- },
555
- )
556
- })
247
+ ])
557
248
 
558
- it('only allows record to have one current action.', async () => {
559
- const postRef = sc.posts[sc.dids.alice][0].ref
560
- const { data: acknowledge } =
561
- await agent.api.com.atproto.admin.takeModerationAction(
562
- {
563
- action: ACKNOWLEDGE,
564
- subject: {
565
- $type: 'com.atproto.repo.strongRef',
566
- uri: postRef.uriStr,
567
- cid: postRef.cidStr,
568
- },
569
- createdBy: 'did:example:admin',
570
- reason: 'Y',
571
- },
572
- {
573
- encoding: 'application/json',
574
- headers: network.bsky.adminAuthHeaders(),
575
- },
576
- )
577
- const flagPromise = agent.api.com.atproto.admin.takeModerationAction(
578
- {
579
- action: FLAG,
580
- subject: {
581
- $type: 'com.atproto.repo.strongRef',
582
- uri: postRef.uriStr,
583
- cid: postRef.cidStr,
584
- },
585
- createdBy: 'did:example:admin',
586
- reason: 'Y',
587
- },
588
- {
589
- encoding: 'application/json',
590
- headers: network.bsky.adminAuthHeaders(),
591
- },
592
- )
593
- await expect(flagPromise).rejects.toThrow(
594
- 'Subject already has an active action:',
595
- )
249
+ await performTakedown({
250
+ account: sc.dids.bob,
251
+ })
596
252
 
597
- // Reverse current then retry
598
- await agent.api.com.atproto.admin.reverseModerationAction(
599
- {
600
- id: acknowledge.id,
601
- createdBy: 'did:example:admin',
602
- reason: 'Y',
603
- },
604
- {
605
- encoding: 'application/json',
606
- headers: network.bsky.adminAuthHeaders(),
607
- },
608
- )
609
- const { data: flag } =
610
- await agent.api.com.atproto.admin.takeModerationAction(
611
- {
612
- action: FLAG,
613
- subject: {
614
- $type: 'com.atproto.repo.strongRef',
615
- uri: postRef.uriStr,
616
- cid: postRef.cidStr,
617
- },
618
- createdBy: 'did:example:admin',
619
- reason: 'Y',
620
- },
621
- {
622
- encoding: 'application/json',
623
- headers: network.bsky.adminAuthHeaders(),
624
- },
625
- )
253
+ const moderationStatusOnBobsAccount = await getStatuses({
254
+ subject: sc.dids.bob,
255
+ })
626
256
 
627
- // Cleanup
628
- await agent.api.com.atproto.admin.reverseModerationAction(
629
- {
630
- id: flag.id,
631
- createdBy: 'did:example:admin',
632
- reason: 'Y',
633
- },
634
- {
635
- encoding: 'application/json',
636
- headers: network.bsky.adminAuthHeaders(),
257
+ // Validate that subject status is set to review closed and takendown flag is on
258
+ expect(moderationStatusOnBobsAccount.subjectStatuses[0]).toMatchObject({
259
+ reviewState: REVIEWCLOSED,
260
+ takendown: true,
261
+ subject: {
262
+ $type: 'com.atproto.admin.defs#repoRef',
263
+ did: sc.dids.bob,
637
264
  },
638
- )
265
+ })
266
+
267
+ // Cleanup
268
+ await performReverseTakedown({
269
+ account: sc.dids.bob,
270
+ })
639
271
  })
640
272
 
641
- it('only allows repo to have one current action.', async () => {
642
- const { data: acknowledge } =
643
- await agent.api.com.atproto.admin.takeModerationAction(
644
- {
645
- action: ACKNOWLEDGE,
646
- subject: {
647
- $type: 'com.atproto.admin.defs#repoRef',
648
- did: sc.dids.alice,
649
- },
650
- createdBy: 'did:example:admin',
651
- reason: 'Y',
652
- },
653
- {
654
- encoding: 'application/json',
655
- headers: network.bsky.adminAuthHeaders(),
656
- },
657
- )
658
- const flagPromise = agent.api.com.atproto.admin.takeModerationAction(
273
+ it('supports escalating a subject', async () => {
274
+ const alicesPostRef = sc.posts[sc.dids.alice][0].ref
275
+ const alicesPostSubject = {
276
+ $type: 'com.atproto.repo.strongRef',
277
+ uri: alicesPostRef.uri.toString(),
278
+ cid: alicesPostRef.cid.toString(),
279
+ }
280
+ await agent.api.com.atproto.admin.emitModerationEvent(
659
281
  {
660
- action: FLAG,
661
- subject: {
662
- $type: 'com.atproto.admin.defs#repoRef',
663
- did: sc.dids.alice,
282
+ event: {
283
+ $type: 'com.atproto.admin.defs#modEventEscalate',
284
+ comment: 'Y',
664
285
  },
286
+ subject: alicesPostSubject,
665
287
  createdBy: 'did:example:admin',
666
- reason: 'Y',
667
288
  },
668
289
  {
669
290
  encoding: 'application/json',
670
- headers: network.bsky.adminAuthHeaders(),
291
+ headers: network.bsky.adminAuthHeaders('triage'),
671
292
  },
672
293
  )
673
- await expect(flagPromise).rejects.toThrow(
674
- 'Subject already has an active action:',
675
- )
676
294
 
677
- // Reverse current then retry
678
- await agent.api.com.atproto.admin.reverseModerationAction(
679
- {
680
- id: acknowledge.id,
295
+ const alicesPostStatus = await getStatuses({
296
+ subject: alicesPostRef.uri.toString(),
297
+ })
298
+
299
+ expect(alicesPostStatus.subjectStatuses[0]).toMatchObject({
300
+ reviewState: REVIEWESCALATED,
301
+ takendown: false,
302
+ subject: alicesPostSubject,
303
+ })
304
+ })
305
+
306
+ it('adds persistent comment on subject through comment event', async () => {
307
+ const alicesPostRef = sc.posts[sc.dids.alice][0].ref
308
+ const alicesPostSubject = {
309
+ $type: 'com.atproto.repo.strongRef',
310
+ uri: alicesPostRef.uri.toString(),
311
+ cid: alicesPostRef.cid.toString(),
312
+ }
313
+ await agent.api.com.atproto.admin.emitModerationEvent(
314
+ {
315
+ event: {
316
+ $type: 'com.atproto.admin.defs#modEventComment',
317
+ sticky: true,
318
+ comment: 'This is a persistent note',
319
+ },
320
+ subject: alicesPostSubject,
681
321
  createdBy: 'did:example:admin',
682
- reason: 'Y',
683
322
  },
684
323
  {
685
324
  encoding: 'application/json',
686
- headers: network.bsky.adminAuthHeaders(),
325
+ headers: network.bsky.adminAuthHeaders('triage'),
687
326
  },
688
327
  )
689
- const { data: flag } =
690
- await agent.api.com.atproto.admin.takeModerationAction(
691
- {
692
- action: FLAG,
693
- subject: {
694
- $type: 'com.atproto.admin.defs#repoRef',
695
- did: sc.dids.alice,
696
- },
697
- createdBy: 'did:example:admin',
698
- reason: 'Y',
699
- },
700
- {
701
- encoding: 'application/json',
702
- headers: network.bsky.adminAuthHeaders(),
703
- },
704
- )
705
328
 
706
- // Cleanup
707
- await agent.api.com.atproto.admin.reverseModerationAction(
708
- {
709
- id: flag.id,
710
- createdBy: 'did:example:admin',
711
- reason: 'Y',
712
- },
713
- {
714
- encoding: 'application/json',
715
- headers: network.bsky.adminAuthHeaders(),
716
- },
329
+ const alicesPostStatus = await getStatuses({
330
+ subject: alicesPostRef.uri.toString(),
331
+ })
332
+
333
+ expect(alicesPostStatus.subjectStatuses[0].comment).toEqual(
334
+ 'This is a persistent note',
717
335
  )
718
336
  })
719
337
 
720
- it('only allows blob to have one current action.', async () => {
721
- const img = sc.posts[sc.dids.carol][0].images[0]
722
- const postA = await sc.post(sc.dids.carol, 'image A', undefined, [img])
723
- const postB = await sc.post(sc.dids.carol, 'image B', undefined, [img])
724
- const { data: acknowledge } =
725
- await agent.api.com.atproto.admin.takeModerationAction(
726
- {
727
- action: ACKNOWLEDGE,
728
- subject: {
729
- $type: 'com.atproto.repo.strongRef',
730
- uri: postA.ref.uriStr,
731
- cid: postA.ref.cidStr,
732
- },
733
- subjectBlobCids: [img.image.ref.toString()],
734
- createdBy: 'did:example:admin',
735
- reason: 'Y',
736
- },
737
- {
738
- encoding: 'application/json',
739
- headers: network.bsky.adminAuthHeaders(),
740
- },
741
- )
742
- const flagPromise = agent.api.com.atproto.admin.takeModerationAction(
743
- {
744
- action: FLAG,
338
+ it('reverses status when revert event is triggered.', async () => {
339
+ const alicesPostRef = sc.posts[sc.dids.alice][0].ref
340
+ const emitModEvent = async (
341
+ event: ComAtprotoAdminEmitModerationEvent.InputSchema['event'],
342
+ overwrites: Partial<ComAtprotoAdminEmitModerationEvent.InputSchema> = {},
343
+ ) => {
344
+ const baseAction = {
745
345
  subject: {
746
346
  $type: 'com.atproto.repo.strongRef',
747
- uri: postB.ref.uriStr,
748
- cid: postB.ref.cidStr,
347
+ uri: alicesPostRef.uriStr,
348
+ cid: alicesPostRef.cidStr,
749
349
  },
750
- subjectBlobCids: [img.image.ref.toString()],
751
- createdBy: 'did:example:admin',
752
- reason: 'Y',
753
- },
754
- {
755
- encoding: 'application/json',
756
- headers: network.bsky.adminAuthHeaders(),
757
- },
758
- )
759
- await expect(flagPromise).rejects.toThrow(
760
- 'Blob already has an active action:',
761
- )
762
- // Reverse current then retry
763
- await agent.api.com.atproto.admin.reverseModerationAction(
764
- {
765
- id: acknowledge.id,
766
350
  createdBy: 'did:example:admin',
767
- reason: 'Y',
768
- },
769
- {
770
- encoding: 'application/json',
771
- headers: network.bsky.adminAuthHeaders(),
772
- },
773
- )
774
- const { data: flag } =
775
- await agent.api.com.atproto.admin.takeModerationAction(
351
+ }
352
+ return agent.api.com.atproto.admin.emitModerationEvent(
776
353
  {
777
- action: FLAG,
778
- subject: {
779
- $type: 'com.atproto.repo.strongRef',
780
- uri: postB.ref.uriStr,
781
- cid: postB.ref.cidStr,
782
- },
783
- subjectBlobCids: [img.image.ref.toString()],
784
- createdBy: 'did:example:admin',
785
- reason: 'Y',
354
+ event,
355
+ ...baseAction,
356
+ ...overwrites,
786
357
  },
787
358
  {
788
359
  encoding: 'application/json',
789
360
  headers: network.bsky.adminAuthHeaders(),
790
361
  },
791
362
  )
363
+ }
364
+ // Validate that subject status is marked as escalated
365
+ await emitModEvent({
366
+ $type: 'com.atproto.admin.defs#modEventReport',
367
+ reportType: REASONSPAM,
368
+ })
369
+ await emitModEvent({
370
+ $type: 'com.atproto.admin.defs#modEventReport',
371
+ reportType: REASONMISLEADING,
372
+ })
373
+ await emitModEvent({
374
+ $type: 'com.atproto.admin.defs#modEventEscalate',
375
+ })
376
+ const alicesPostStatusAfterEscalation = await getStatuses({
377
+ subject: alicesPostRef.uriStr,
378
+ })
379
+ expect(
380
+ alicesPostStatusAfterEscalation.subjectStatuses[0].reviewState,
381
+ ).toEqual(REVIEWESCALATED)
792
382
 
793
- // Cleanup
794
- await agent.api.com.atproto.admin.reverseModerationAction(
795
- {
796
- id: flag.id,
797
- createdBy: 'did:example:admin',
798
- reason: 'Y',
799
- },
800
- {
801
- encoding: 'application/json',
802
- headers: network.bsky.adminAuthHeaders(),
803
- },
804
- )
383
+ // Validate that subject status is marked as takendown
384
+
385
+ await emitModEvent({
386
+ $type: 'com.atproto.admin.defs#modEventLabel',
387
+ createLabelVals: ['nsfw'],
388
+ negateLabelVals: [],
389
+ })
390
+ await emitModEvent({
391
+ $type: 'com.atproto.admin.defs#modEventTakedown',
392
+ })
393
+
394
+ const alicesPostStatusAfterTakedown = await getStatuses({
395
+ subject: alicesPostRef.uriStr,
396
+ })
397
+ expect(alicesPostStatusAfterTakedown.subjectStatuses[0]).toMatchObject({
398
+ reviewState: REVIEWCLOSED,
399
+ takendown: true,
400
+ })
401
+
402
+ await emitModEvent({
403
+ $type: 'com.atproto.admin.defs#modEventReverseTakedown',
404
+ })
405
+ const alicesPostStatusAfterRevert = await getStatuses({
406
+ subject: alicesPostRef.uriStr,
407
+ })
408
+ // Validate that after reverting, the status of the subject is reverted to the last status changing event
409
+ expect(alicesPostStatusAfterRevert.subjectStatuses[0]).toMatchObject({
410
+ reviewState: REVIEWCLOSED,
411
+ takendown: false,
412
+ })
413
+ // Validate that after reverting, the last review date of the subject
414
+ // DOES NOT update to the the last status changing event
415
+ expect(
416
+ new Date(
417
+ alicesPostStatusAfterEscalation.subjectStatuses[0]
418
+ .lastReviewedAt as string,
419
+ ) <
420
+ new Date(
421
+ alicesPostStatusAfterRevert.subjectStatuses[0]
422
+ .lastReviewedAt as string,
423
+ ),
424
+ ).toBeTruthy()
805
425
  })
806
426
 
807
- it('negates an existing label and reverses.', async () => {
427
+ it('negates an existing label.', async () => {
808
428
  const { ctx } = network.bsky
809
429
  const post = sc.posts[sc.dids.bob][0].ref
430
+ const bobsPostSubject = {
431
+ $type: 'com.atproto.repo.strongRef',
432
+ uri: post.uriStr,
433
+ cid: post.cidStr,
434
+ }
810
435
  const labelingService = ctx.services.label(ctx.db.getPrimary())
811
436
  await labelingService.formatAndCreate(
812
437
  ctx.cfg.labelerDid,
@@ -814,16 +439,18 @@ describe('moderation', () => {
814
439
  post.cidStr,
815
440
  { create: ['kittens'] },
816
441
  )
817
- const action = await actionWithLabels({
442
+ await emitLabelEvent({
818
443
  negateLabelVals: ['kittens'],
819
- subject: {
820
- $type: 'com.atproto.repo.strongRef',
821
- uri: post.uriStr,
822
- cid: post.cidStr,
823
- },
444
+ createLabelVals: [],
445
+ subject: bobsPostSubject,
824
446
  })
825
447
  await expect(getRecordLabels(post.uriStr)).resolves.toEqual([])
826
- await reverse(action.id)
448
+
449
+ await emitLabelEvent({
450
+ createLabelVals: ['kittens'],
451
+ negateLabelVals: [],
452
+ subject: bobsPostSubject,
453
+ })
827
454
  await expect(getRecordLabels(post.uriStr)).resolves.toEqual(['kittens'])
828
455
  // Cleanup
829
456
  await labelingService.formatAndCreate(
@@ -838,8 +465,9 @@ describe('moderation', () => {
838
465
  const { ctx } = network.bsky
839
466
  const post = sc.posts[sc.dids.bob][0].ref
840
467
  const labelingService = ctx.services.label(ctx.db.getPrimary())
841
- const action = await actionWithLabels({
468
+ await emitLabelEvent({
842
469
  negateLabelVals: ['bears'],
470
+ createLabelVals: [],
843
471
  subject: {
844
472
  $type: 'com.atproto.repo.strongRef',
845
473
  uri: post.uriStr,
@@ -847,7 +475,15 @@ describe('moderation', () => {
847
475
  },
848
476
  })
849
477
  await expect(getRecordLabels(post.uriStr)).resolves.toEqual([])
850
- await reverse(action.id)
478
+ await emitLabelEvent({
479
+ createLabelVals: ['bears'],
480
+ negateLabelVals: [],
481
+ subject: {
482
+ $type: 'com.atproto.repo.strongRef',
483
+ uri: post.uriStr,
484
+ cid: post.cidStr,
485
+ },
486
+ })
851
487
  await expect(getRecordLabels(post.uriStr)).resolves.toEqual(['bears'])
852
488
  // Cleanup
853
489
  await labelingService.formatAndCreate(
@@ -860,7 +496,7 @@ describe('moderation', () => {
860
496
 
861
497
  it('creates non-existing labels and reverses.', async () => {
862
498
  const post = sc.posts[sc.dids.bob][0].ref
863
- const action = await actionWithLabels({
499
+ await emitLabelEvent({
864
500
  createLabelVals: ['puppies', 'doggies'],
865
501
  negateLabelVals: [],
866
502
  subject: {
@@ -873,35 +509,20 @@ describe('moderation', () => {
873
509
  'puppies',
874
510
  'doggies',
875
511
  ])
876
- await reverse(action.id)
877
- await expect(getRecordLabels(post.uriStr)).resolves.toEqual([])
878
- })
879
-
880
- it('no-ops when creating an existing label and reverses.', async () => {
881
- const { ctx } = network.bsky
882
- const post = sc.posts[sc.dids.bob][0].ref
883
- const labelingService = ctx.services.label(ctx.db.getPrimary())
884
- await labelingService.formatAndCreate(
885
- ctx.cfg.labelerDid,
886
- post.uriStr,
887
- post.cidStr,
888
- { create: ['birds'] },
889
- )
890
- const action = await actionWithLabels({
891
- createLabelVals: ['birds'],
512
+ await emitLabelEvent({
513
+ negateLabelVals: ['puppies', 'doggies'],
514
+ createLabelVals: [],
892
515
  subject: {
893
516
  $type: 'com.atproto.repo.strongRef',
894
517
  uri: post.uriStr,
895
518
  cid: post.cidStr,
896
519
  },
897
520
  })
898
- await expect(getRecordLabels(post.uriStr)).resolves.toEqual(['birds'])
899
- await reverse(action.id)
900
521
  await expect(getRecordLabels(post.uriStr)).resolves.toEqual([])
901
522
  })
902
523
 
903
524
  it('creates labels on a repo and reverses.', async () => {
904
- const action = await actionWithLabels({
525
+ await emitLabelEvent({
905
526
  createLabelVals: ['puppies', 'doggies'],
906
527
  negateLabelVals: [],
907
528
  subject: {
@@ -913,7 +534,14 @@ describe('moderation', () => {
913
534
  'puppies',
914
535
  'doggies',
915
536
  ])
916
- await reverse(action.id)
537
+ await emitLabelEvent({
538
+ negateLabelVals: ['puppies', 'doggies'],
539
+ createLabelVals: [],
540
+ subject: {
541
+ $type: 'com.atproto.admin.defs#repoRef',
542
+ did: sc.dids.bob,
543
+ },
544
+ })
917
545
  await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual([])
918
546
  })
919
547
 
@@ -926,7 +554,7 @@ describe('moderation', () => {
926
554
  null,
927
555
  { create: ['kittens'] },
928
556
  )
929
- const action = await actionWithLabels({
557
+ await emitLabelEvent({
930
558
  createLabelVals: ['puppies'],
931
559
  negateLabelVals: ['kittens'],
932
560
  subject: {
@@ -935,22 +563,32 @@ describe('moderation', () => {
935
563
  },
936
564
  })
937
565
  await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual(['puppies'])
938
- await reverse(action.id)
566
+
567
+ await emitLabelEvent({
568
+ negateLabelVals: ['puppies'],
569
+ createLabelVals: ['kittens'],
570
+ subject: {
571
+ $type: 'com.atproto.admin.defs#repoRef',
572
+ did: sc.dids.bob,
573
+ },
574
+ })
939
575
  await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual(['kittens'])
940
576
  })
941
577
 
942
578
  it('does not allow triage moderators to label.', async () => {
943
- const attemptLabel = agent.api.com.atproto.admin.takeModerationAction(
579
+ const attemptLabel = agent.api.com.atproto.admin.emitModerationEvent(
944
580
  {
945
- action: ACKNOWLEDGE,
581
+ event: {
582
+ $type: 'com.atproto.admin.defs#modEventLabel',
583
+ negateLabelVals: ['a'],
584
+ createLabelVals: ['b', 'c'],
585
+ },
946
586
  createdBy: 'did:example:moderator',
947
587
  reason: 'Y',
948
588
  subject: {
949
589
  $type: 'com.atproto.admin.defs#repoRef',
950
590
  did: sc.dids.bob,
951
591
  },
952
- negateLabelVals: ['a'],
953
- createLabelVals: ['b', 'c'],
954
592
  },
955
593
  {
956
594
  encoding: 'application/json',
@@ -962,23 +600,30 @@ describe('moderation', () => {
962
600
  )
963
601
  })
964
602
 
603
+ it('does not allow take down event on takendown post or reverse takedown on available post.', async () => {
604
+ await performTakedown({
605
+ account: sc.dids.bob,
606
+ })
607
+ await expect(
608
+ performTakedown({
609
+ account: sc.dids.bob,
610
+ }),
611
+ ).rejects.toThrow('Subject is already taken down')
612
+
613
+ // Cleanup
614
+ await performReverseTakedown({
615
+ account: sc.dids.bob,
616
+ })
617
+ await expect(
618
+ performReverseTakedown({
619
+ account: sc.dids.bob,
620
+ }),
621
+ ).rejects.toThrow('Subject is not taken down')
622
+ })
965
623
  it('fans out repo takedowns to pds', async () => {
966
- const { data: action } =
967
- await agent.api.com.atproto.admin.takeModerationAction(
968
- {
969
- action: TAKEDOWN,
970
- createdBy: 'did:example:moderator',
971
- reason: 'Y',
972
- subject: {
973
- $type: 'com.atproto.admin.defs#repoRef',
974
- did: sc.dids.bob,
975
- },
976
- },
977
- {
978
- encoding: 'application/json',
979
- headers: network.bsky.adminAuthHeaders(),
980
- },
981
- )
624
+ await performTakedown({
625
+ account: sc.dids.bob,
626
+ })
982
627
 
983
628
  const res1 = await pdsAgent.api.com.atproto.admin.getSubjectStatus(
984
629
  {
@@ -989,7 +634,7 @@ describe('moderation', () => {
989
634
  expect(res1.data.takedown?.applied).toBe(true)
990
635
 
991
636
  // cleanup
992
- await reverse(action.id)
637
+ await performReverseTakedown({ account: sc.dids.bob })
993
638
 
994
639
  const res2 = await pdsAgent.api.com.atproto.admin.getSubjectStatus(
995
640
  {
@@ -1004,24 +649,9 @@ describe('moderation', () => {
1004
649
  const post = sc.posts[sc.dids.bob][0]
1005
650
  const uri = post.ref.uriStr
1006
651
  const cid = post.ref.cidStr
1007
- const { data: action } =
1008
- await agent.api.com.atproto.admin.takeModerationAction(
1009
- {
1010
- action: TAKEDOWN,
1011
- createdBy: 'did:example:moderator',
1012
- reason: 'Y',
1013
- subject: {
1014
- $type: 'com.atproto.repo.strongRef',
1015
- uri,
1016
- cid,
1017
- },
1018
- },
1019
- {
1020
- encoding: 'application/json',
1021
- headers: network.bsky.adminAuthHeaders(),
1022
- },
1023
- )
1024
-
652
+ await performTakedown({
653
+ content: { uri, cid },
654
+ })
1025
655
  const res1 = await pdsAgent.api.com.atproto.admin.getSubjectStatus(
1026
656
  { uri },
1027
657
  { headers: network.pds.adminAuthHeaders() },
@@ -1029,7 +659,7 @@ describe('moderation', () => {
1029
659
  expect(res1.data.takedown?.applied).toBe(true)
1030
660
 
1031
661
  // cleanup
1032
- await reverse(action.id)
662
+ await performReverseTakedown({ content: { uri, cid } })
1033
663
 
1034
664
  const res2 = await pdsAgent.api.com.atproto.admin.getSubjectStatus(
1035
665
  { uri },
@@ -1039,33 +669,39 @@ describe('moderation', () => {
1039
669
  })
1040
670
 
1041
671
  it('allows full moderators to takedown.', async () => {
1042
- const { data: action } =
1043
- await agent.api.com.atproto.admin.takeModerationAction(
1044
- {
1045
- action: TAKEDOWN,
1046
- createdBy: 'did:example:moderator',
1047
- reason: 'Y',
1048
- subject: {
1049
- $type: 'com.atproto.admin.defs#repoRef',
1050
- did: sc.dids.bob,
1051
- },
672
+ await agent.api.com.atproto.admin.emitModerationEvent(
673
+ {
674
+ event: {
675
+ $type: 'com.atproto.admin.defs#modEventTakedown',
1052
676
  },
1053
- {
1054
- encoding: 'application/json',
1055
- headers: network.bsky.adminAuthHeaders('moderator'),
677
+ createdBy: 'did:example:moderator',
678
+ subject: {
679
+ $type: 'com.atproto.admin.defs#repoRef',
680
+ did: sc.dids.bob,
1056
681
  },
1057
- )
682
+ },
683
+ {
684
+ encoding: 'application/json',
685
+ headers: network.bsky.adminAuthHeaders('moderator'),
686
+ },
687
+ )
1058
688
  // cleanup
1059
- await reverse(action.id)
689
+ await reverse({
690
+ subject: {
691
+ $type: 'com.atproto.admin.defs#repoRef',
692
+ did: sc.dids.bob,
693
+ },
694
+ })
1060
695
  })
1061
696
 
1062
697
  it('does not allow non-full moderators to takedown.', async () => {
1063
698
  const attemptTakedownTriage =
1064
- agent.api.com.atproto.admin.takeModerationAction(
699
+ agent.api.com.atproto.admin.emitModerationEvent(
1065
700
  {
1066
- action: TAKEDOWN,
701
+ event: {
702
+ $type: 'com.atproto.admin.defs#modEventTakedown',
703
+ },
1067
704
  createdBy: 'did:example:moderator',
1068
- reason: 'Y',
1069
705
  subject: {
1070
706
  $type: 'com.atproto.admin.defs#repoRef',
1071
707
  did: sc.dids.bob,
@@ -1081,61 +717,76 @@ describe('moderation', () => {
1081
717
  )
1082
718
  })
1083
719
  it('automatically reverses actions marked with duration', async () => {
1084
- const { data: action } =
1085
- await agent.api.com.atproto.admin.takeModerationAction(
1086
- {
1087
- action: TAKEDOWN,
1088
- createdBy: 'did:example:moderator',
1089
- reason: 'Y',
1090
- subject: {
1091
- $type: 'com.atproto.admin.defs#repoRef',
1092
- did: sc.dids.bob,
1093
- },
1094
- createLabelVals: ['takendown'],
1095
- // Use negative value to set the expiry time in the past so that the action is automatically reversed
1096
- // right away without having to wait n number of hours for a successful assertion
1097
- durationInHours: -1,
1098
- },
1099
- {
1100
- encoding: 'application/json',
1101
- headers: network.bsky.adminAuthHeaders('moderator'),
1102
- },
720
+ await createReport({
721
+ reasonType: REASONSPAM,
722
+ account: sc.dids.bob,
723
+ author: sc.dids.alice,
724
+ })
725
+ const { data: action } = await performTakedown({
726
+ account: sc.dids.bob,
727
+ // Use negative value to set the expiry time in the past so that the action is automatically reversed
728
+ // right away without having to wait n number of hours for a successful assertion
729
+ durationInHours: -1,
730
+ })
731
+
732
+ const { data: statusesAfterTakedown } =
733
+ await agent.api.com.atproto.admin.queryModerationStatuses(
734
+ { subject: sc.dids.bob },
735
+ { headers: network.bsky.adminAuthHeaders('moderator') },
1103
736
  )
1104
737
 
1105
- const labelsAfterTakedown = await getRepoLabels(sc.dids.bob)
1106
- expect(labelsAfterTakedown).toContain('takendown')
738
+ expect(statusesAfterTakedown.subjectStatuses[0]).toMatchObject({
739
+ takendown: true,
740
+ })
741
+
1107
742
  // In the actual app, this will be instantiated and run on server startup
1108
- const periodicReversal = new PeriodicModerationActionReversal(
743
+ const periodicReversal = new PeriodicModerationEventReversal(
1109
744
  network.bsky.ctx,
1110
745
  )
1111
746
  await periodicReversal.findAndRevertDueActions()
1112
747
 
1113
- const { data: reversedAction } =
1114
- await agent.api.com.atproto.admin.getModerationAction(
1115
- { id: action.id },
748
+ const [{ data: eventList }, { data: statuses }] = await Promise.all([
749
+ agent.api.com.atproto.admin.queryModerationEvents(
750
+ { subject: sc.dids.bob },
1116
751
  { headers: network.bsky.adminAuthHeaders('moderator') },
1117
- )
752
+ ),
753
+ agent.api.com.atproto.admin.queryModerationStatuses(
754
+ { subject: sc.dids.bob },
755
+ { headers: network.bsky.adminAuthHeaders('moderator') },
756
+ ),
757
+ ])
1118
758
 
759
+ expect(statuses.subjectStatuses[0]).toMatchObject({
760
+ takendown: false,
761
+ reviewState: REVIEWCLOSED,
762
+ })
1119
763
  // Verify that the automatic reversal is attributed to the original moderator of the temporary action
1120
764
  // and that the reason is set to indicate that the action was automatically reversed.
1121
- expect(reversedAction.reversal).toMatchObject({
765
+ expect(eventList.events[0]).toMatchObject({
1122
766
  createdBy: action.createdBy,
1123
- reason: '[SCHEDULED_REVERSAL] Reverting action as originally scheduled',
767
+ event: {
768
+ $type: 'com.atproto.admin.defs#modEventReverseTakedown',
769
+ comment:
770
+ '[SCHEDULED_REVERSAL] Reverting action as originally scheduled',
771
+ },
1124
772
  })
1125
-
1126
- // Verify that labels are also reversed when takedown action is reversed
1127
- const labelsAfterReversal = await getRepoLabels(sc.dids.bob)
1128
- expect(labelsAfterReversal).not.toContain('takendown')
1129
773
  })
1130
774
 
1131
- async function actionWithLabels(
1132
- opts: Partial<ComAtprotoAdminTakeModerationAction.InputSchema> & {
1133
- subject: ComAtprotoAdminTakeModerationAction.InputSchema['subject']
775
+ async function emitLabelEvent(
776
+ opts: Partial<ComAtprotoAdminEmitModerationEvent.InputSchema> & {
777
+ subject: ComAtprotoAdminEmitModerationEvent.InputSchema['subject']
778
+ createLabelVals: ModEventLabel['createLabelVals']
779
+ negateLabelVals: ModEventLabel['negateLabelVals']
1134
780
  },
1135
781
  ) {
1136
- const result = await agent.api.com.atproto.admin.takeModerationAction(
782
+ const { createLabelVals, negateLabelVals } = opts
783
+ const result = await agent.api.com.atproto.admin.emitModerationEvent(
1137
784
  {
1138
- action: FLAG,
785
+ event: {
786
+ $type: 'com.atproto.admin.defs#modEventLabel',
787
+ createLabelVals,
788
+ negateLabelVals,
789
+ },
1139
790
  createdBy: 'did:example:admin',
1140
791
  reason: 'Y',
1141
792
  ...opts,
@@ -1148,12 +799,19 @@ describe('moderation', () => {
1148
799
  return result.data
1149
800
  }
1150
801
 
1151
- async function reverse(actionId: number) {
1152
- await agent.api.com.atproto.admin.reverseModerationAction(
802
+ async function reverse(
803
+ opts: Partial<ComAtprotoAdminEmitModerationEvent.InputSchema> & {
804
+ subject: ComAtprotoAdminEmitModerationEvent.InputSchema['subject']
805
+ },
806
+ ) {
807
+ await agent.api.com.atproto.admin.emitModerationEvent(
1153
808
  {
1154
- id: actionId,
809
+ event: {
810
+ $type: 'com.atproto.admin.defs#modEventReverseTakedown',
811
+ },
1155
812
  createdBy: 'did:example:admin',
1156
813
  reason: 'Y',
814
+ ...opts,
1157
815
  },
1158
816
  {
1159
817
  encoding: 'application/json',
@@ -1185,7 +843,6 @@ describe('moderation', () => {
1185
843
  let post: { ref: RecordRef; images: ImageRef[] }
1186
844
  let blob: ImageRef
1187
845
  let imageUri: string
1188
- let actionId: number
1189
846
  beforeAll(async () => {
1190
847
  const { ctx } = network.bsky
1191
848
  post = sc.posts[sc.dids.carol][0]
@@ -1201,24 +858,23 @@ describe('moderation', () => {
1201
858
  await fetch(imageUri)
1202
859
  const cached = await fetch(imageUri)
1203
860
  expect(cached.headers.get('x-cache')).toEqual('hit')
1204
- const takeAction = await agent.api.com.atproto.admin.takeModerationAction(
1205
- {
1206
- action: TAKEDOWN,
1207
- subject: {
1208
- $type: 'com.atproto.repo.strongRef',
1209
- uri: post.ref.uriStr,
1210
- cid: post.ref.cidStr,
1211
- },
1212
- subjectBlobCids: [blob.image.ref.toString()],
1213
- createdBy: 'did:example:admin',
1214
- reason: 'Y',
1215
- },
1216
- {
1217
- encoding: 'application/json',
1218
- headers: network.bsky.adminAuthHeaders(),
861
+ await performTakedown({
862
+ content: {
863
+ uri: post.ref.uriStr,
864
+ cid: post.ref.cidStr,
1219
865
  },
1220
- )
1221
- actionId = takeAction.data.id
866
+ subjectBlobCids: [blob.image.ref.toString()],
867
+ })
868
+ })
869
+
870
+ it('sets blobCids in moderation status', async () => {
871
+ const { subjectStatuses } = await getStatuses({
872
+ subject: post.ref.uriStr,
873
+ })
874
+
875
+ expect(subjectStatuses[0].subjectBlobCids).toEqual([
876
+ blob.image.ref.toString(),
877
+ ])
1222
878
  })
1223
879
 
1224
880
  it('prevents resolution of blob', async () => {
@@ -1249,17 +905,13 @@ describe('moderation', () => {
1249
905
  })
1250
906
 
1251
907
  it('restores blob when action is reversed.', async () => {
1252
- await agent.api.com.atproto.admin.reverseModerationAction(
1253
- {
1254
- id: actionId,
1255
- createdBy: 'did:example:admin',
1256
- reason: 'Y',
1257
- },
1258
- {
1259
- encoding: 'application/json',
1260
- headers: network.bsky.adminAuthHeaders(),
908
+ await performReverseTakedown({
909
+ content: {
910
+ uri: post.ref.uriStr,
911
+ cid: post.ref.cidStr,
1261
912
  },
1262
- )
913
+ subjectBlobCids: [blob.image.ref.toString()],
914
+ })
1263
915
 
1264
916
  // Can resolve blob
1265
917
  const blobPath = `/blob/${sc.dids.carol}/${blob.image.ref.toString()}`