@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.
- package/CHANGELOG.md +13 -0
- package/dist/api/com/atproto/admin/util.d.ts +5 -0
- package/dist/context.d.ts +6 -1
- package/dist/db/index.js +51 -2
- package/dist/db/index.js.map +3 -3
- package/dist/db/migrations/20230929T192920807Z-record-cursor-indexes.d.ts +3 -0
- package/dist/db/migrations/index.d.ts +1 -0
- package/dist/did-cache.d.ts +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1381 -530
- package/dist/index.js.map +3 -3
- package/dist/lexicon/index.d.ts +8 -0
- package/dist/lexicon/lexicons.d.ts +231 -3
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts +1 -0
- package/dist/lexicon/types/com/atproto/admin/defs.d.ts +28 -0
- package/dist/lexicon/types/com/atproto/admin/getAccountInfo.d.ts +29 -0
- package/dist/lexicon/types/com/atproto/admin/getSubjectStatus.d.ts +39 -0
- package/dist/lexicon/types/com/atproto/admin/searchRepos.d.ts +0 -1
- package/dist/lexicon/types/com/atproto/admin/updateSubjectStatus.d.ts +46 -0
- package/dist/lexicon/types/com/atproto/server/createAccount.d.ts +2 -0
- package/dist/lexicon/types/com/atproto/server/createSession.d.ts +1 -0
- package/dist/lexicon/types/com/atproto/server/refreshSession.d.ts +1 -0
- package/dist/lexicon/types/com/atproto/server/reserveSigningKey.d.ts +30 -0
- package/dist/lexicon/types/com/atproto/sync/listRepos.d.ts +1 -0
- package/dist/services/actor/types.d.ts +1 -0
- package/dist/services/graph/index.d.ts +2 -0
- package/dist/services/moderation/index.d.ts +13 -3
- package/package.json +13 -14
- package/src/api/app/bsky/feed/getAuthorFeed.ts +2 -2
- package/src/api/app/bsky/feed/getPostThread.ts +2 -2
- package/src/api/app/bsky/notification/listNotifications.ts +33 -22
- package/src/api/com/atproto/admin/getModerationAction.ts +28 -2
- package/src/api/com/atproto/admin/getModerationReport.ts +27 -2
- package/src/api/com/atproto/admin/getRecord.ts +14 -2
- package/src/api/com/atproto/admin/getRepo.ts +13 -2
- package/src/api/com/atproto/admin/reverseModerationAction.ts +31 -5
- package/src/api/com/atproto/admin/searchRepos.ts +2 -5
- package/src/api/com/atproto/admin/takeModerationAction.ts +41 -7
- package/src/api/com/atproto/admin/util.ts +50 -0
- package/src/api/well-known.ts +8 -0
- package/src/auth.ts +12 -5
- package/src/auto-moderator/index.ts +1 -0
- package/src/context.ts +25 -1
- package/src/db/migrations/20230929T192920807Z-record-cursor-indexes.ts +40 -0
- package/src/db/migrations/index.ts +1 -0
- package/src/did-cache.ts +29 -14
- package/src/feed-gen/with-friends.ts +2 -2
- package/src/index.ts +4 -1
- package/src/indexer/subscription.ts +1 -21
- package/src/lexicon/index.ts +48 -0
- package/src/lexicon/lexicons.ts +246 -4
- package/src/lexicon/types/app/bsky/actor/defs.ts +1 -0
- package/src/lexicon/types/com/atproto/admin/defs.ts +61 -0
- package/src/lexicon/types/com/atproto/admin/getAccountInfo.ts +41 -0
- package/src/lexicon/types/com/atproto/admin/getSubjectStatus.ts +54 -0
- package/src/lexicon/types/com/atproto/admin/searchRepos.ts +0 -1
- package/src/lexicon/types/com/atproto/admin/updateSubjectStatus.ts +61 -0
- package/src/lexicon/types/com/atproto/server/createAccount.ts +2 -0
- package/src/lexicon/types/com/atproto/server/createSession.ts +1 -0
- package/src/lexicon/types/com/atproto/server/refreshSession.ts +1 -0
- package/src/lexicon/types/com/atproto/server/reserveSigningKey.ts +44 -0
- package/src/lexicon/types/com/atproto/sync/listRepos.ts +1 -0
- package/src/logger.ts +8 -0
- package/src/services/actor/index.ts +7 -1
- package/src/services/actor/types.ts +1 -0
- package/src/services/actor/views.ts +26 -8
- package/src/services/graph/index.ts +26 -7
- package/src/services/indexing/index.ts +15 -17
- package/src/services/moderation/index.ts +94 -14
- package/src/services/moderation/views.ts +1 -0
- package/tests/__snapshots__/feed-generation.test.ts.snap +12 -12
- package/tests/__snapshots__/indexing.test.ts.snap +4 -4
- package/tests/admin/__snapshots__/get-moderation-action.test.ts.snap +172 -0
- package/tests/admin/__snapshots__/get-moderation-actions.test.ts.snap +178 -0
- package/tests/admin/__snapshots__/get-moderation-report.test.ts.snap +177 -0
- package/tests/admin/__snapshots__/get-moderation-reports.test.ts.snap +307 -0
- package/tests/admin/__snapshots__/get-record.test.ts.snap +275 -0
- package/tests/admin/__snapshots__/get-repo.test.ts.snap +103 -0
- package/tests/admin/get-moderation-action.test.ts +100 -0
- package/tests/admin/get-moderation-actions.test.ts +164 -0
- package/tests/admin/get-moderation-report.test.ts +100 -0
- package/tests/admin/get-moderation-reports.test.ts +332 -0
- package/tests/admin/get-record.test.ts +115 -0
- package/tests/admin/get-repo.test.ts +101 -0
- package/tests/{moderation.test.ts → admin/moderation.test.ts} +107 -9
- package/tests/admin/repo-search.test.ts +124 -0
- package/tests/algos/hot-classic.test.ts +3 -5
- package/tests/algos/whats-hot.test.ts +3 -5
- package/tests/algos/with-friends.test.ts +2 -4
- package/tests/auth.test.ts +64 -0
- package/tests/auto-moderator/fuzzy-matcher.test.ts +2 -3
- package/tests/auto-moderator/labeler.test.ts +5 -7
- package/tests/auto-moderator/takedowns.test.ts +11 -12
- package/tests/blob-resolver.test.ts +1 -3
- package/tests/did-cache.test.ts +2 -5
- package/tests/feed-generation.test.ts +8 -6
- package/tests/handle-invalidation.test.ts +2 -3
- package/tests/image/server.test.ts +1 -4
- package/tests/image/sharp.test.ts +1 -1
- package/tests/indexing.test.ts +4 -4
- package/tests/notification-server.test.ts +2 -3
- package/tests/pipeline/backpressure.test.ts +2 -3
- package/tests/pipeline/reingest.test.ts +7 -4
- package/tests/pipeline/repartition.test.ts +2 -3
- package/tests/reprocessing.test.ts +2 -6
- package/tests/seeds/basic.ts +4 -4
- package/tests/seeds/follows.ts +1 -1
- package/tests/seeds/likes.ts +1 -1
- package/tests/seeds/reposts.ts +1 -1
- package/tests/seeds/users-bulk.ts +1 -1
- package/tests/seeds/users.ts +1 -1
- package/tests/server.test.ts +1 -3
- package/tests/subscription/repo.test.ts +2 -4
- package/tests/views/__snapshots__/author-feed.test.ts.snap +24 -24
- package/tests/views/__snapshots__/block-lists.test.ts.snap +42 -7
- package/tests/views/__snapshots__/blocks.test.ts.snap +2 -2
- package/tests/views/__snapshots__/list-feed.test.ts.snap +6 -6
- package/tests/views/__snapshots__/mute-lists.test.ts.snap +15 -4
- package/tests/views/__snapshots__/mutes.test.ts.snap +2 -2
- package/tests/views/__snapshots__/notifications.test.ts.snap +2 -2
- package/tests/views/__snapshots__/posts.test.ts.snap +8 -8
- package/tests/views/__snapshots__/thread.test.ts.snap +10 -10
- package/tests/views/__snapshots__/timeline.test.ts.snap +58 -58
- package/tests/views/actor-likes.test.ts +2 -3
- package/tests/views/actor-search.test.ts +5 -5
- package/tests/views/admin/repo-search.test.ts +2 -4
- package/tests/views/author-feed.test.ts +2 -4
- package/tests/views/block-lists.test.ts +34 -7
- package/tests/views/blocks.test.ts +6 -3
- package/tests/views/follows.test.ts +2 -4
- package/tests/views/likes.test.ts +2 -5
- package/tests/views/list-feed.test.ts +2 -4
- package/tests/views/mute-lists.test.ts +23 -5
- package/tests/views/mutes.test.ts +2 -5
- package/tests/views/notifications.test.ts +2 -4
- package/tests/views/posts.test.ts +2 -5
- package/tests/views/profile.test.ts +4 -5
- package/tests/views/reposts.test.ts +2 -4
- package/tests/views/suggested-follows.test.ts +2 -3
- package/tests/views/suggestions.test.ts +2 -4
- package/tests/views/thread.test.ts +2 -4
- package/tests/views/threadgating.test.ts +2 -3
- package/tests/views/timeline.test.ts +2 -4
- package/dist/env.d.ts +0 -1
- package/example.dev.env +0 -5
- package/src/env.ts +0 -9
- package/tests/seeds/client.ts +0 -466
- /package/tests/{__snapshots__ → admin/__snapshots__}/moderation.test.ts.snap +0 -0
- /package/tests/{image/fixtures → sample-img}/at.png +0 -0
- /package/tests/{image/fixtures → sample-img}/hd-key.jpg +0 -0
- /package/tests/{image/fixtures → sample-img}/key-alt.jpg +0 -0
- /package/tests/{image/fixtures → sample-img}/key-landscape-large.jpg +0 -0
- /package/tests/{image/fixtures → sample-img}/key-landscape-small.jpg +0 -0
- /package/tests/{image/fixtures → sample-img}/key-portrait-large.jpg +0 -0
- /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
|
+
})
|