@atproto/bsky 0.0.11 → 0.0.12

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 (155) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/api/com/atproto/admin/util.d.ts +5 -0
  3. package/dist/context.d.ts +6 -1
  4. package/dist/db/index.js +51 -2
  5. package/dist/db/index.js.map +3 -3
  6. package/dist/db/migrations/20230929T192920807Z-record-cursor-indexes.d.ts +3 -0
  7. package/dist/db/migrations/index.d.ts +1 -0
  8. package/dist/did-cache.d.ts +2 -2
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +1381 -530
  11. package/dist/index.js.map +3 -3
  12. package/dist/lexicon/index.d.ts +8 -0
  13. package/dist/lexicon/lexicons.d.ts +231 -3
  14. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +1 -0
  15. package/dist/lexicon/types/com/atproto/admin/defs.d.ts +28 -0
  16. package/dist/lexicon/types/com/atproto/admin/getAccountInfo.d.ts +29 -0
  17. package/dist/lexicon/types/com/atproto/admin/getSubjectStatus.d.ts +39 -0
  18. package/dist/lexicon/types/com/atproto/admin/searchRepos.d.ts +0 -1
  19. package/dist/lexicon/types/com/atproto/admin/updateSubjectStatus.d.ts +46 -0
  20. package/dist/lexicon/types/com/atproto/server/createAccount.d.ts +2 -0
  21. package/dist/lexicon/types/com/atproto/server/createSession.d.ts +1 -0
  22. package/dist/lexicon/types/com/atproto/server/refreshSession.d.ts +1 -0
  23. package/dist/lexicon/types/com/atproto/server/reserveSigningKey.d.ts +30 -0
  24. package/dist/lexicon/types/com/atproto/sync/listRepos.d.ts +1 -0
  25. package/dist/services/actor/types.d.ts +1 -0
  26. package/dist/services/graph/index.d.ts +2 -0
  27. package/dist/services/moderation/index.d.ts +13 -3
  28. package/package.json +13 -14
  29. package/src/api/app/bsky/feed/getAuthorFeed.ts +2 -2
  30. package/src/api/app/bsky/feed/getPostThread.ts +2 -2
  31. package/src/api/app/bsky/notification/listNotifications.ts +33 -22
  32. package/src/api/com/atproto/admin/getModerationAction.ts +28 -2
  33. package/src/api/com/atproto/admin/getModerationReport.ts +27 -2
  34. package/src/api/com/atproto/admin/getRecord.ts +14 -2
  35. package/src/api/com/atproto/admin/getRepo.ts +13 -2
  36. package/src/api/com/atproto/admin/reverseModerationAction.ts +31 -5
  37. package/src/api/com/atproto/admin/searchRepos.ts +2 -5
  38. package/src/api/com/atproto/admin/takeModerationAction.ts +41 -7
  39. package/src/api/com/atproto/admin/util.ts +50 -0
  40. package/src/api/well-known.ts +8 -0
  41. package/src/auth.ts +12 -5
  42. package/src/auto-moderator/index.ts +1 -0
  43. package/src/context.ts +25 -1
  44. package/src/db/migrations/20230929T192920807Z-record-cursor-indexes.ts +40 -0
  45. package/src/db/migrations/index.ts +1 -0
  46. package/src/did-cache.ts +29 -14
  47. package/src/feed-gen/with-friends.ts +2 -2
  48. package/src/index.ts +4 -1
  49. package/src/indexer/subscription.ts +1 -21
  50. package/src/lexicon/index.ts +48 -0
  51. package/src/lexicon/lexicons.ts +246 -4
  52. package/src/lexicon/types/app/bsky/actor/defs.ts +1 -0
  53. package/src/lexicon/types/com/atproto/admin/defs.ts +61 -0
  54. package/src/lexicon/types/com/atproto/admin/getAccountInfo.ts +41 -0
  55. package/src/lexicon/types/com/atproto/admin/getSubjectStatus.ts +54 -0
  56. package/src/lexicon/types/com/atproto/admin/searchRepos.ts +0 -1
  57. package/src/lexicon/types/com/atproto/admin/updateSubjectStatus.ts +61 -0
  58. package/src/lexicon/types/com/atproto/server/createAccount.ts +2 -0
  59. package/src/lexicon/types/com/atproto/server/createSession.ts +1 -0
  60. package/src/lexicon/types/com/atproto/server/refreshSession.ts +1 -0
  61. package/src/lexicon/types/com/atproto/server/reserveSigningKey.ts +44 -0
  62. package/src/lexicon/types/com/atproto/sync/listRepos.ts +1 -0
  63. package/src/logger.ts +8 -0
  64. package/src/services/actor/index.ts +7 -1
  65. package/src/services/actor/types.ts +1 -0
  66. package/src/services/actor/views.ts +26 -8
  67. package/src/services/graph/index.ts +26 -7
  68. package/src/services/indexing/index.ts +15 -17
  69. package/src/services/moderation/index.ts +94 -14
  70. package/src/services/moderation/views.ts +1 -0
  71. package/tests/__snapshots__/feed-generation.test.ts.snap +12 -12
  72. package/tests/__snapshots__/indexing.test.ts.snap +4 -4
  73. package/tests/admin/__snapshots__/get-moderation-action.test.ts.snap +172 -0
  74. package/tests/admin/__snapshots__/get-moderation-actions.test.ts.snap +178 -0
  75. package/tests/admin/__snapshots__/get-moderation-report.test.ts.snap +177 -0
  76. package/tests/admin/__snapshots__/get-moderation-reports.test.ts.snap +307 -0
  77. package/tests/admin/__snapshots__/get-record.test.ts.snap +275 -0
  78. package/tests/admin/__snapshots__/get-repo.test.ts.snap +103 -0
  79. package/tests/admin/get-moderation-action.test.ts +100 -0
  80. package/tests/admin/get-moderation-actions.test.ts +164 -0
  81. package/tests/admin/get-moderation-report.test.ts +100 -0
  82. package/tests/admin/get-moderation-reports.test.ts +332 -0
  83. package/tests/admin/get-record.test.ts +115 -0
  84. package/tests/admin/get-repo.test.ts +101 -0
  85. package/tests/{moderation.test.ts → admin/moderation.test.ts} +107 -9
  86. package/tests/admin/repo-search.test.ts +124 -0
  87. package/tests/algos/hot-classic.test.ts +3 -5
  88. package/tests/algos/whats-hot.test.ts +3 -5
  89. package/tests/algos/with-friends.test.ts +2 -4
  90. package/tests/auth.test.ts +64 -0
  91. package/tests/auto-moderator/fuzzy-matcher.test.ts +2 -3
  92. package/tests/auto-moderator/labeler.test.ts +5 -7
  93. package/tests/auto-moderator/takedowns.test.ts +11 -12
  94. package/tests/blob-resolver.test.ts +1 -3
  95. package/tests/did-cache.test.ts +2 -5
  96. package/tests/feed-generation.test.ts +8 -6
  97. package/tests/handle-invalidation.test.ts +2 -3
  98. package/tests/image/server.test.ts +1 -4
  99. package/tests/image/sharp.test.ts +1 -1
  100. package/tests/indexing.test.ts +4 -4
  101. package/tests/notification-server.test.ts +2 -3
  102. package/tests/pipeline/backpressure.test.ts +2 -3
  103. package/tests/pipeline/reingest.test.ts +7 -4
  104. package/tests/pipeline/repartition.test.ts +2 -3
  105. package/tests/reprocessing.test.ts +2 -6
  106. package/tests/seeds/basic.ts +4 -4
  107. package/tests/seeds/follows.ts +1 -1
  108. package/tests/seeds/likes.ts +1 -1
  109. package/tests/seeds/reposts.ts +1 -1
  110. package/tests/seeds/users-bulk.ts +1 -1
  111. package/tests/seeds/users.ts +1 -1
  112. package/tests/server.test.ts +1 -3
  113. package/tests/subscription/repo.test.ts +2 -4
  114. package/tests/views/__snapshots__/author-feed.test.ts.snap +24 -24
  115. package/tests/views/__snapshots__/block-lists.test.ts.snap +42 -7
  116. package/tests/views/__snapshots__/blocks.test.ts.snap +2 -2
  117. package/tests/views/__snapshots__/list-feed.test.ts.snap +6 -6
  118. package/tests/views/__snapshots__/mute-lists.test.ts.snap +15 -4
  119. package/tests/views/__snapshots__/mutes.test.ts.snap +2 -2
  120. package/tests/views/__snapshots__/notifications.test.ts.snap +2 -2
  121. package/tests/views/__snapshots__/posts.test.ts.snap +8 -8
  122. package/tests/views/__snapshots__/thread.test.ts.snap +10 -10
  123. package/tests/views/__snapshots__/timeline.test.ts.snap +58 -58
  124. package/tests/views/actor-likes.test.ts +2 -3
  125. package/tests/views/actor-search.test.ts +5 -5
  126. package/tests/views/admin/repo-search.test.ts +2 -4
  127. package/tests/views/author-feed.test.ts +2 -4
  128. package/tests/views/block-lists.test.ts +34 -7
  129. package/tests/views/blocks.test.ts +6 -3
  130. package/tests/views/follows.test.ts +2 -4
  131. package/tests/views/likes.test.ts +2 -5
  132. package/tests/views/list-feed.test.ts +2 -4
  133. package/tests/views/mute-lists.test.ts +23 -5
  134. package/tests/views/mutes.test.ts +2 -5
  135. package/tests/views/notifications.test.ts +2 -4
  136. package/tests/views/posts.test.ts +2 -5
  137. package/tests/views/profile.test.ts +4 -5
  138. package/tests/views/reposts.test.ts +2 -4
  139. package/tests/views/suggested-follows.test.ts +2 -3
  140. package/tests/views/suggestions.test.ts +2 -4
  141. package/tests/views/thread.test.ts +2 -4
  142. package/tests/views/threadgating.test.ts +2 -3
  143. package/tests/views/timeline.test.ts +2 -4
  144. package/dist/env.d.ts +0 -1
  145. package/example.dev.env +0 -5
  146. package/src/env.ts +0 -9
  147. package/tests/seeds/client.ts +0 -466
  148. /package/tests/{__snapshots__ → admin/__snapshots__}/moderation.test.ts.snap +0 -0
  149. /package/tests/{image/fixtures → sample-img}/at.png +0 -0
  150. /package/tests/{image/fixtures → sample-img}/hd-key.jpg +0 -0
  151. /package/tests/{image/fixtures → sample-img}/key-alt.jpg +0 -0
  152. /package/tests/{image/fixtures → sample-img}/key-landscape-large.jpg +0 -0
  153. /package/tests/{image/fixtures → sample-img}/key-landscape-small.jpg +0 -0
  154. /package/tests/{image/fixtures → sample-img}/key-portrait-large.jpg +0 -0
  155. /package/tests/{image/fixtures → sample-img}/key-portrait-small.jpg +0 -0
@@ -0,0 +1,100 @@
1
+ import { TestNetwork, SeedClient } from '@atproto/dev-env'
2
+ import AtpAgent from '@atproto/api'
3
+ import {
4
+ FLAG,
5
+ TAKEDOWN,
6
+ } from '@atproto/api/src/client/types/com/atproto/admin/defs'
7
+ import {
8
+ REASONOTHER,
9
+ REASONSPAM,
10
+ } from '../../src/lexicon/types/com/atproto/moderation/defs'
11
+ import { forSnapshot } from '../_util'
12
+ import basicSeed from '../seeds/basic'
13
+
14
+ describe('admin get moderation action view', () => {
15
+ let network: TestNetwork
16
+ let agent: AtpAgent
17
+ let sc: SeedClient
18
+
19
+ beforeAll(async () => {
20
+ network = await TestNetwork.create({
21
+ dbPostgresSchema: 'views_admin_get_moderation_action',
22
+ })
23
+ agent = network.pds.getClient()
24
+ sc = network.getSeedClient()
25
+ await basicSeed(sc)
26
+ })
27
+
28
+ afterAll(async () => {
29
+ await network.close()
30
+ })
31
+
32
+ beforeAll(async () => {
33
+ const reportRepo = await sc.createReport({
34
+ reportedBy: sc.dids.bob,
35
+ reasonType: REASONSPAM,
36
+ subject: {
37
+ $type: 'com.atproto.admin.defs#repoRef',
38
+ did: sc.dids.alice,
39
+ },
40
+ })
41
+ const reportRecord = await sc.createReport({
42
+ reportedBy: sc.dids.carol,
43
+ reasonType: REASONOTHER,
44
+ reason: 'defamation',
45
+ subject: {
46
+ $type: 'com.atproto.repo.strongRef',
47
+ uri: sc.posts[sc.dids.alice][0].ref.uriStr,
48
+ cid: sc.posts[sc.dids.alice][0].ref.cidStr,
49
+ },
50
+ })
51
+ const flagRepo = await sc.takeModerationAction({
52
+ action: FLAG,
53
+ subject: {
54
+ $type: 'com.atproto.admin.defs#repoRef',
55
+ did: sc.dids.alice,
56
+ },
57
+ })
58
+ const takedownRecord = await sc.takeModerationAction({
59
+ action: TAKEDOWN,
60
+ subject: {
61
+ $type: 'com.atproto.repo.strongRef',
62
+ uri: sc.posts[sc.dids.alice][0].ref.uriStr,
63
+ cid: sc.posts[sc.dids.alice][0].ref.cidStr,
64
+ },
65
+ })
66
+ await sc.resolveReports({
67
+ actionId: flagRepo.id,
68
+ reportIds: [reportRepo.id, reportRecord.id],
69
+ })
70
+ await sc.resolveReports({
71
+ actionId: takedownRecord.id,
72
+ reportIds: [reportRecord.id],
73
+ })
74
+ await sc.reverseModerationAction({ id: flagRepo.id })
75
+ })
76
+
77
+ it('gets moderation action for a repo.', async () => {
78
+ const result = await agent.api.com.atproto.admin.getModerationAction(
79
+ { id: 1 },
80
+ { headers: { authorization: network.pds.adminAuth() } },
81
+ )
82
+ expect(forSnapshot(result.data)).toMatchSnapshot()
83
+ })
84
+
85
+ it('gets moderation action for a record.', async () => {
86
+ const result = await agent.api.com.atproto.admin.getModerationAction(
87
+ { id: 2 },
88
+ { headers: { authorization: network.pds.adminAuth() } },
89
+ )
90
+ expect(forSnapshot(result.data)).toMatchSnapshot()
91
+ })
92
+
93
+ it('fails when moderation action does not exist.', async () => {
94
+ const promise = agent.api.com.atproto.admin.getModerationAction(
95
+ { id: 100 },
96
+ { headers: { authorization: network.pds.adminAuth() } },
97
+ )
98
+ await expect(promise).rejects.toThrow('Action not found')
99
+ })
100
+ })
@@ -0,0 +1,164 @@
1
+ import { SeedClient, TestNetwork } from '@atproto/dev-env'
2
+ import AtpAgent from '@atproto/api'
3
+ import {
4
+ ACKNOWLEDGE,
5
+ FLAG,
6
+ TAKEDOWN,
7
+ } from '@atproto/api/src/client/types/com/atproto/admin/defs'
8
+ import {
9
+ REASONOTHER,
10
+ REASONSPAM,
11
+ } from '../../src/lexicon/types/com/atproto/moderation/defs'
12
+ import { forSnapshot, paginateAll } from '../_util'
13
+ import basicSeed from '../seeds/basic'
14
+
15
+ describe('admin get moderation actions view', () => {
16
+ let network: TestNetwork
17
+ let agent: AtpAgent
18
+ let sc: SeedClient
19
+
20
+ beforeAll(async () => {
21
+ network = await TestNetwork.create({
22
+ dbPostgresSchema: 'views_admin_get_moderation_actions',
23
+ })
24
+ agent = network.pds.getClient()
25
+ sc = network.getSeedClient()
26
+ await basicSeed(sc)
27
+ })
28
+
29
+ afterAll(async () => {
30
+ await network.close()
31
+ })
32
+
33
+ beforeAll(async () => {
34
+ const oneIn = (n) => (_, i) => i % n === 0
35
+ const getAction = (i) => [FLAG, ACKNOWLEDGE, TAKEDOWN][i % 3]
36
+ const posts = Object.values(sc.posts)
37
+ .flatMap((x) => x)
38
+ .filter(oneIn(2))
39
+ const dids = Object.values(sc.dids).filter(oneIn(2))
40
+ // Take actions on records
41
+ const recordActions: Awaited<ReturnType<typeof sc.takeModerationAction>>[] =
42
+ []
43
+ for (let i = 0; i < posts.length; ++i) {
44
+ const post = posts[i]
45
+ recordActions.push(
46
+ await sc.takeModerationAction({
47
+ action: getAction(i),
48
+ subject: {
49
+ $type: 'com.atproto.repo.strongRef',
50
+ uri: post.ref.uriStr,
51
+ cid: post.ref.cidStr,
52
+ },
53
+ }),
54
+ )
55
+ }
56
+ // Reverse an action
57
+ await sc.reverseModerationAction({
58
+ id: recordActions[0].id,
59
+ })
60
+ // Take actions on repos
61
+ const repoActions: Awaited<ReturnType<typeof sc.takeModerationAction>>[] =
62
+ []
63
+ for (let i = 0; i < dids.length; ++i) {
64
+ const did = dids[i]
65
+ repoActions.push(
66
+ await sc.takeModerationAction({
67
+ action: getAction(i),
68
+ subject: {
69
+ $type: 'com.atproto.admin.defs#repoRef',
70
+ did,
71
+ },
72
+ }),
73
+ )
74
+ }
75
+ // Back some of the actions with a report, possibly resolved
76
+ const someRecordActions = recordActions.filter(oneIn(2))
77
+ for (let i = 0; i < someRecordActions.length; ++i) {
78
+ const action = someRecordActions[i]
79
+ const ab = oneIn(2)(action, i)
80
+ const report = await sc.createReport({
81
+ reportedBy: ab ? sc.dids.carol : sc.dids.alice,
82
+ reasonType: ab ? REASONSPAM : REASONOTHER,
83
+ subject: {
84
+ $type: 'com.atproto.repo.strongRef',
85
+ uri: action.subject.uri,
86
+ cid: action.subject.cid,
87
+ },
88
+ })
89
+ if (ab) {
90
+ await sc.resolveReports({
91
+ actionId: action.id,
92
+ reportIds: [report.id],
93
+ })
94
+ }
95
+ }
96
+ const someRepoActions = repoActions.filter(oneIn(2))
97
+ for (let i = 0; i < someRepoActions.length; ++i) {
98
+ const action = someRepoActions[i]
99
+ const ab = oneIn(2)(action, i)
100
+ const report = await sc.createReport({
101
+ reportedBy: ab ? sc.dids.carol : sc.dids.alice,
102
+ reasonType: ab ? REASONSPAM : REASONOTHER,
103
+ subject: {
104
+ $type: 'com.atproto.admin.defs#repoRef',
105
+ did: action.subject.did,
106
+ },
107
+ })
108
+ if (ab) {
109
+ await sc.resolveReports({
110
+ actionId: action.id,
111
+ reportIds: [report.id],
112
+ })
113
+ }
114
+ }
115
+ })
116
+
117
+ it('gets all moderation actions.', async () => {
118
+ const result = await agent.api.com.atproto.admin.getModerationActions(
119
+ {},
120
+ { headers: network.pds.adminAuthHeaders() },
121
+ )
122
+ expect(forSnapshot(result.data.actions)).toMatchSnapshot()
123
+ })
124
+
125
+ it('gets all moderation actions for a repo.', async () => {
126
+ const result = await agent.api.com.atproto.admin.getModerationActions(
127
+ { subject: Object.values(sc.dids)[0] },
128
+ { headers: network.pds.adminAuthHeaders() },
129
+ )
130
+ expect(forSnapshot(result.data.actions)).toMatchSnapshot()
131
+ })
132
+
133
+ it('gets all moderation actions for a record.', async () => {
134
+ const result = await agent.api.com.atproto.admin.getModerationActions(
135
+ { subject: Object.values(sc.posts)[0][0].ref.uriStr },
136
+ { headers: network.pds.adminAuthHeaders() },
137
+ )
138
+ expect(forSnapshot(result.data.actions)).toMatchSnapshot()
139
+ })
140
+
141
+ it('paginates.', async () => {
142
+ const results = (results) => results.flatMap((res) => res.actions)
143
+ const paginator = async (cursor?: string) => {
144
+ const res = await agent.api.com.atproto.admin.getModerationActions(
145
+ { cursor, limit: 3 },
146
+ { headers: network.pds.adminAuthHeaders() },
147
+ )
148
+ return res.data
149
+ }
150
+
151
+ const paginatedAll = await paginateAll(paginator)
152
+ paginatedAll.forEach((res) =>
153
+ expect(res.actions.length).toBeLessThanOrEqual(3),
154
+ )
155
+
156
+ const full = await agent.api.com.atproto.admin.getModerationActions(
157
+ {},
158
+ { headers: network.pds.adminAuthHeaders() },
159
+ )
160
+
161
+ expect(full.data.actions.length).toEqual(6)
162
+ expect(results(paginatedAll)).toEqual(results([full.data]))
163
+ })
164
+ })
@@ -0,0 +1,100 @@
1
+ import { SeedClient, TestNetwork } from '@atproto/dev-env'
2
+ import AtpAgent from '@atproto/api'
3
+ import {
4
+ FLAG,
5
+ TAKEDOWN,
6
+ } from '@atproto/api/src/client/types/com/atproto/admin/defs'
7
+ import {
8
+ REASONOTHER,
9
+ REASONSPAM,
10
+ } from '../../src/lexicon/types/com/atproto/moderation/defs'
11
+ import { forSnapshot } from '../_util'
12
+ import basicSeed from '../seeds/basic'
13
+
14
+ describe('admin get moderation action view', () => {
15
+ let network: TestNetwork
16
+ let agent: AtpAgent
17
+ let sc: SeedClient
18
+
19
+ beforeAll(async () => {
20
+ network = await TestNetwork.create({
21
+ dbPostgresSchema: 'views_admin_get_moderation_report',
22
+ })
23
+ agent = network.pds.getClient()
24
+ sc = network.getSeedClient()
25
+ await basicSeed(sc)
26
+ })
27
+
28
+ afterAll(async () => {
29
+ await network.close()
30
+ })
31
+
32
+ beforeAll(async () => {
33
+ const reportRepo = await sc.createReport({
34
+ reportedBy: sc.dids.bob,
35
+ reasonType: REASONSPAM,
36
+ subject: {
37
+ $type: 'com.atproto.admin.defs#repoRef',
38
+ did: sc.dids.alice,
39
+ },
40
+ })
41
+ const reportRecord = await sc.createReport({
42
+ reportedBy: sc.dids.carol,
43
+ reasonType: REASONOTHER,
44
+ reason: 'defamation',
45
+ subject: {
46
+ $type: 'com.atproto.repo.strongRef',
47
+ uri: sc.posts[sc.dids.alice][0].ref.uriStr,
48
+ cid: sc.posts[sc.dids.alice][0].ref.cidStr,
49
+ },
50
+ })
51
+ const flagRepo = await sc.takeModerationAction({
52
+ action: FLAG,
53
+ subject: {
54
+ $type: 'com.atproto.admin.defs#repoRef',
55
+ did: sc.dids.alice,
56
+ },
57
+ })
58
+ const takedownRecord = await sc.takeModerationAction({
59
+ action: TAKEDOWN,
60
+ subject: {
61
+ $type: 'com.atproto.repo.strongRef',
62
+ uri: sc.posts[sc.dids.alice][0].ref.uriStr,
63
+ cid: sc.posts[sc.dids.alice][0].ref.cidStr,
64
+ },
65
+ })
66
+ await sc.resolveReports({
67
+ actionId: flagRepo.id,
68
+ reportIds: [reportRepo.id, reportRecord.id],
69
+ })
70
+ await sc.resolveReports({
71
+ actionId: takedownRecord.id,
72
+ reportIds: [reportRecord.id],
73
+ })
74
+ await sc.reverseModerationAction({ id: flagRepo.id })
75
+ })
76
+
77
+ it('gets moderation report for a repo.', async () => {
78
+ const result = await agent.api.com.atproto.admin.getModerationReport(
79
+ { id: 1 },
80
+ { headers: network.pds.adminAuthHeaders() },
81
+ )
82
+ expect(forSnapshot(result.data)).toMatchSnapshot()
83
+ })
84
+
85
+ it('gets moderation report for a record.', async () => {
86
+ const result = await agent.api.com.atproto.admin.getModerationReport(
87
+ { id: 2 },
88
+ { headers: network.pds.adminAuthHeaders() },
89
+ )
90
+ expect(forSnapshot(result.data)).toMatchSnapshot()
91
+ })
92
+
93
+ it('fails when moderation report does not exist.', async () => {
94
+ const promise = agent.api.com.atproto.admin.getModerationReport(
95
+ { id: 100 },
96
+ { headers: network.pds.adminAuthHeaders() },
97
+ )
98
+ await expect(promise).rejects.toThrow('Report not found')
99
+ })
100
+ })
@@ -0,0 +1,332 @@
1
+ import { SeedClient, TestNetwork } from '@atproto/dev-env'
2
+ import AtpAgent from '@atproto/api'
3
+ import {
4
+ ACKNOWLEDGE,
5
+ FLAG,
6
+ TAKEDOWN,
7
+ } from '@atproto/api/src/client/types/com/atproto/admin/defs'
8
+ import {
9
+ REASONOTHER,
10
+ REASONSPAM,
11
+ } from '../../src/lexicon/types/com/atproto/moderation/defs'
12
+ import { forSnapshot, paginateAll } from '../_util'
13
+ import basicSeed from '../seeds/basic'
14
+
15
+ describe('admin get moderation reports view', () => {
16
+ let network: TestNetwork
17
+ let agent: AtpAgent
18
+ let sc: SeedClient
19
+
20
+ beforeAll(async () => {
21
+ network = await TestNetwork.create({
22
+ dbPostgresSchema: 'views_admin_get_moderation_reports',
23
+ })
24
+ agent = network.pds.getClient()
25
+ sc = network.getSeedClient()
26
+ await basicSeed(sc)
27
+ })
28
+
29
+ afterAll(async () => {
30
+ await network.close()
31
+ })
32
+
33
+ beforeAll(async () => {
34
+ const oneIn = (n) => (_, i) => i % n === 0
35
+ const getAction = (i) => [FLAG, ACKNOWLEDGE, TAKEDOWN][i % 3]
36
+ const getReasonType = (i) => [REASONOTHER, REASONSPAM][i % 2]
37
+ const getReportedByDid = (i) => [sc.dids.alice, sc.dids.carol][i % 2]
38
+ const posts = Object.values(sc.posts)
39
+ .flatMap((x) => x)
40
+ .filter(oneIn(2))
41
+ const dids = Object.values(sc.dids).filter(oneIn(2))
42
+ const recordReports: Awaited<ReturnType<typeof sc.createReport>>[] = []
43
+ for (let i = 0; i < posts.length; ++i) {
44
+ const post = posts[i]
45
+ recordReports.push(
46
+ await sc.createReport({
47
+ reasonType: getReasonType(i),
48
+ reportedBy: getReportedByDid(i),
49
+ subject: {
50
+ $type: 'com.atproto.repo.strongRef',
51
+ uri: post.ref.uriStr,
52
+ cid: post.ref.cidStr,
53
+ },
54
+ }),
55
+ )
56
+ }
57
+ const repoReports: Awaited<ReturnType<typeof sc.createReport>>[] = []
58
+ for (let i = 0; i < dids.length; ++i) {
59
+ const did = dids[i]
60
+ repoReports.push(
61
+ await sc.createReport({
62
+ reasonType: getReasonType(i),
63
+ reportedBy: getReportedByDid(i),
64
+ subject: {
65
+ $type: 'com.atproto.admin.defs#repoRef',
66
+ did,
67
+ },
68
+ }),
69
+ )
70
+ }
71
+ for (let i = 0; i < recordReports.length; ++i) {
72
+ const report = recordReports[i]
73
+ const ab = oneIn(2)(report, i)
74
+ const action = await sc.takeModerationAction({
75
+ action: getAction(i),
76
+ subject: {
77
+ $type: 'com.atproto.repo.strongRef',
78
+ uri: report.subject.uri,
79
+ cid: report.subject.cid,
80
+ },
81
+ createdBy: `did:example:admin${i}`,
82
+ })
83
+ if (ab) {
84
+ await sc.resolveReports({
85
+ actionId: action.id,
86
+ reportIds: [report.id],
87
+ })
88
+ } else {
89
+ await sc.reverseModerationAction({
90
+ id: action.id,
91
+ })
92
+ }
93
+ }
94
+ for (let i = 0; i < repoReports.length; ++i) {
95
+ const report = repoReports[i]
96
+ const ab = oneIn(2)(report, i)
97
+ const action = await sc.takeModerationAction({
98
+ action: getAction(i),
99
+ subject: {
100
+ $type: 'com.atproto.admin.defs#repoRef',
101
+ did: report.subject.did,
102
+ },
103
+ })
104
+ if (ab) {
105
+ await sc.resolveReports({
106
+ actionId: action.id,
107
+ reportIds: [report.id],
108
+ })
109
+ } else {
110
+ await sc.reverseModerationAction({
111
+ id: action.id,
112
+ })
113
+ }
114
+ }
115
+ })
116
+
117
+ it('ignores subjects when specified.', async () => {
118
+ // Get all reports and then make another request with a filter to ignore some subject dids
119
+ // and assert that the reports for those subject dids are ignored in the result set
120
+ const getDids = (reportsResponse) =>
121
+ reportsResponse.data.reports
122
+ .map((report) => report.subject.did)
123
+ // Not all reports contain a did so we're discarding the undefined values in the mapped array
124
+ .filter(Boolean)
125
+
126
+ const allReports = await agent.api.com.atproto.admin.getModerationReports(
127
+ {},
128
+ { headers: network.pds.adminAuthHeaders() },
129
+ )
130
+
131
+ const ignoreSubjects = getDids(allReports).slice(0, 2)
132
+
133
+ const filteredReportsByDid =
134
+ await agent.api.com.atproto.admin.getModerationReports(
135
+ { ignoreSubjects },
136
+ { headers: network.pds.adminAuthHeaders() },
137
+ )
138
+
139
+ // Validate that when ignored by DID, all reports for that DID is ignored
140
+ getDids(filteredReportsByDid).forEach((resultDid) =>
141
+ expect(ignoreSubjects).not.toContain(resultDid),
142
+ )
143
+
144
+ const ignoredAtUriSubjects: string[] = [
145
+ `${
146
+ allReports.data.reports.find(({ subject }) => !!subject.uri)?.subject
147
+ ?.uri
148
+ }`,
149
+ ]
150
+ const filteredReportsByAtUri =
151
+ await agent.api.com.atproto.admin.getModerationReports(
152
+ {
153
+ ignoreSubjects: ignoredAtUriSubjects,
154
+ },
155
+ { headers: network.pds.adminAuthHeaders() },
156
+ )
157
+
158
+ // Validate that when ignored by at uri, only the reports for that at uri is ignored
159
+ expect(filteredReportsByAtUri.data.reports.length).toEqual(
160
+ allReports.data.reports.length - 1,
161
+ )
162
+ expect(
163
+ filteredReportsByAtUri.data.reports
164
+ .map(({ subject }) => subject.uri)
165
+ .filter(Boolean),
166
+ ).not.toContain(ignoredAtUriSubjects[0])
167
+ })
168
+
169
+ it('gets all moderation reports.', async () => {
170
+ const result = await agent.api.com.atproto.admin.getModerationReports(
171
+ {},
172
+ { headers: network.pds.adminAuthHeaders() },
173
+ )
174
+ expect(forSnapshot(result.data.reports)).toMatchSnapshot()
175
+ })
176
+
177
+ it('gets all moderation reports for a repo.', async () => {
178
+ const result = await agent.api.com.atproto.admin.getModerationReports(
179
+ { subject: Object.values(sc.dids)[0] },
180
+ { headers: network.pds.adminAuthHeaders() },
181
+ )
182
+ expect(forSnapshot(result.data.reports)).toMatchSnapshot()
183
+ })
184
+
185
+ it('gets all moderation reports for a record.', async () => {
186
+ const result = await agent.api.com.atproto.admin.getModerationReports(
187
+ { subject: Object.values(sc.posts)[0][0].ref.uriStr },
188
+ { headers: network.pds.adminAuthHeaders() },
189
+ )
190
+ expect(forSnapshot(result.data.reports)).toMatchSnapshot()
191
+ })
192
+
193
+ it('gets all resolved/unresolved moderation reports.', async () => {
194
+ const resolved = await agent.api.com.atproto.admin.getModerationReports(
195
+ { resolved: true },
196
+ { headers: network.pds.adminAuthHeaders() },
197
+ )
198
+ expect(forSnapshot(resolved.data.reports)).toMatchSnapshot()
199
+ const unresolved = await agent.api.com.atproto.admin.getModerationReports(
200
+ { resolved: false },
201
+ { headers: network.pds.adminAuthHeaders() },
202
+ )
203
+ expect(forSnapshot(unresolved.data.reports)).toMatchSnapshot()
204
+ })
205
+
206
+ it('allows reverting the order of reports.', async () => {
207
+ const [
208
+ {
209
+ data: { reports: reverseList },
210
+ },
211
+ {
212
+ data: { reports: defaultList },
213
+ },
214
+ ] = await Promise.all([
215
+ agent.api.com.atproto.admin.getModerationReports(
216
+ { reverse: true },
217
+ { headers: network.pds.adminAuthHeaders() },
218
+ ),
219
+ agent.api.com.atproto.admin.getModerationReports(
220
+ {},
221
+ { headers: network.pds.adminAuthHeaders() },
222
+ ),
223
+ ])
224
+
225
+ expect(defaultList[0].id).toEqual(reverseList[reverseList.length - 1].id)
226
+ expect(defaultList[defaultList.length - 1].id).toEqual(reverseList[0].id)
227
+ })
228
+
229
+ it('gets all moderation reports by active resolution action type.', async () => {
230
+ const reportsWithTakedown =
231
+ await agent.api.com.atproto.admin.getModerationReports(
232
+ { actionType: TAKEDOWN },
233
+ { headers: network.pds.adminAuthHeaders() },
234
+ )
235
+ expect(forSnapshot(reportsWithTakedown.data.reports)).toMatchSnapshot()
236
+ })
237
+
238
+ it('gets all moderation reports actioned by a certain moderator.', async () => {
239
+ const adminDidOne = 'did:example:admin0'
240
+ const adminDidTwo = 'did:example:admin2'
241
+ const [actionedByAdminOne, actionedByAdminTwo] = await Promise.all([
242
+ agent.api.com.atproto.admin.getModerationReports(
243
+ { actionedBy: adminDidOne },
244
+ { headers: network.pds.adminAuthHeaders() },
245
+ ),
246
+ agent.api.com.atproto.admin.getModerationReports(
247
+ { actionedBy: adminDidTwo },
248
+ { headers: network.pds.adminAuthHeaders() },
249
+ ),
250
+ ])
251
+ const [fullReportOne, fullReportTwo] = await Promise.all([
252
+ agent.api.com.atproto.admin.getModerationReport(
253
+ { id: actionedByAdminOne.data.reports[0].id },
254
+ { headers: network.pds.adminAuthHeaders() },
255
+ ),
256
+ agent.api.com.atproto.admin.getModerationReport(
257
+ { id: actionedByAdminTwo.data.reports[0].id },
258
+ { headers: network.pds.adminAuthHeaders() },
259
+ ),
260
+ ])
261
+
262
+ expect(forSnapshot(actionedByAdminOne.data.reports)).toMatchSnapshot()
263
+ expect(fullReportOne.data.resolvedByActions[0].createdBy).toEqual(
264
+ adminDidOne,
265
+ )
266
+ expect(forSnapshot(actionedByAdminTwo.data.reports)).toMatchSnapshot()
267
+ expect(fullReportTwo.data.resolvedByActions[0].createdBy).toEqual(
268
+ adminDidTwo,
269
+ )
270
+ })
271
+
272
+ it('paginates.', async () => {
273
+ const results = (results) => results.flatMap((res) => res.reports)
274
+ const paginator = async (cursor?: string) => {
275
+ const res = await agent.api.com.atproto.admin.getModerationReports(
276
+ { cursor, limit: 3 },
277
+ { headers: network.pds.adminAuthHeaders() },
278
+ )
279
+ return res.data
280
+ }
281
+
282
+ const paginatedAll = await paginateAll(paginator)
283
+ paginatedAll.forEach((res) =>
284
+ expect(res.reports.length).toBeLessThanOrEqual(3),
285
+ )
286
+
287
+ const full = await agent.api.com.atproto.admin.getModerationReports(
288
+ {},
289
+ { headers: network.pds.adminAuthHeaders() },
290
+ )
291
+
292
+ expect(full.data.reports.length).toEqual(6)
293
+ expect(results(paginatedAll)).toEqual(results([full.data]))
294
+ })
295
+
296
+ it('paginates reverted list of reports.', async () => {
297
+ const paginator =
298
+ (reverse = false) =>
299
+ async (cursor?: string) => {
300
+ const res = await agent.api.com.atproto.admin.getModerationReports(
301
+ { cursor, limit: 3, reverse },
302
+ { headers: network.pds.adminAuthHeaders() },
303
+ )
304
+ return res.data
305
+ }
306
+
307
+ const [reverseResponse, defaultResponse] = await Promise.all([
308
+ paginateAll(paginator(true)),
309
+ paginateAll(paginator()),
310
+ ])
311
+
312
+ const reverseList = reverseResponse.flatMap((res) => res.reports)
313
+ const defaultList = defaultResponse.flatMap((res) => res.reports)
314
+
315
+ expect(defaultList[0].id).toEqual(reverseList[reverseList.length - 1].id)
316
+ expect(defaultList[defaultList.length - 1].id).toEqual(reverseList[0].id)
317
+ })
318
+
319
+ it('filters reports by reporter DID.', async () => {
320
+ const result = await agent.api.com.atproto.admin.getModerationReports(
321
+ { reporters: [sc.dids.alice] },
322
+ { headers: network.pds.adminAuthHeaders() },
323
+ )
324
+
325
+ const reporterDidsFromReports = [
326
+ ...new Set(result.data.reports.map(({ reportedBy }) => reportedBy)),
327
+ ]
328
+
329
+ expect(reporterDidsFromReports.length).toEqual(1)
330
+ expect(reporterDidsFromReports[0]).toEqual(sc.dids.alice)
331
+ })
332
+ })