@atproto/ozone 0.0.16 → 0.0.17-next.1
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/dist/api/util.d.ts +10 -0
- package/dist/auth-verifier.d.ts +8 -12
- package/dist/communication-service/template.d.ts +2 -2
- package/dist/config/config.d.ts +6 -0
- package/dist/config/env.d.ts +3 -2
- package/dist/config/secrets.d.ts +0 -2
- package/dist/context.d.ts +6 -0
- package/dist/daemon/blob-diverter.d.ts +26 -0
- package/dist/daemon/event-pusher.d.ts +6 -0
- package/dist/daemon/index.d.ts +1 -0
- package/dist/db/index.js +21 -1
- package/dist/db/index.js.map +3 -3
- package/dist/db/migrations/20240228T003647759Z-add-label-sigs.d.ts +3 -0
- package/dist/db/migrations/index.d.ts +1 -0
- package/dist/db/schema/index.d.ts +2 -1
- package/dist/db/schema/label.d.ts +4 -0
- package/dist/db/schema/moderation_event.d.ts +1 -1
- package/dist/db/schema/moderation_subject_status.d.ts +2 -2
- package/dist/db/schema/signing_key.d.ts +9 -0
- package/dist/index.js +10400 -10313
- package/dist/index.js.map +3 -3
- package/dist/lexicon/index.d.ts +55 -27
- package/dist/lexicon/lexicons.d.ts +5046 -4757
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts +23 -1
- package/dist/lexicon/types/app/bsky/embed/record.d.ts +2 -1
- package/dist/lexicon/types/app/bsky/feed/defs.d.ts +1 -0
- package/dist/lexicon/types/app/bsky/graph/defs.d.ts +3 -0
- package/dist/lexicon/types/app/bsky/labeler/defs.d.ts +41 -0
- package/dist/lexicon/types/app/bsky/labeler/getServices.d.ts +36 -0
- package/dist/lexicon/types/app/bsky/labeler/service.d.ts +14 -0
- package/dist/lexicon/types/com/atproto/admin/defs.d.ts +0 -304
- package/dist/lexicon/types/com/atproto/label/defs.d.ts +23 -0
- package/dist/lexicon/types/{com/atproto/admin/createCommunicationTemplate.d.ts → tools/ozone/communication/createTemplate.d.ts} +2 -2
- package/dist/lexicon/types/tools/ozone/communication/defs.d.ts +14 -0
- package/dist/lexicon/types/{com/atproto/admin/listCommunicationTemplates.d.ts → tools/ozone/communication/listTemplates.d.ts} +2 -2
- package/dist/lexicon/types/{com/atproto/admin/updateCommunicationTemplate.d.ts → tools/ozone/communication/updateTemplate.d.ts} +2 -2
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +269 -0
- package/dist/lexicon/types/{com/atproto/admin/emitModerationEvent.d.ts → tools/ozone/moderation/emitEvent.d.ts} +5 -4
- package/dist/lexicon/types/{com/atproto/admin/getModerationEvent.d.ts → tools/ozone/moderation/getEvent.d.ts} +2 -2
- package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRecord.d.ts +2 -2
- package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRepo.d.ts +2 -2
- package/dist/lexicon/types/{com/atproto/admin/queryModerationEvents.d.ts → tools/ozone/moderation/queryEvents.d.ts} +2 -2
- package/dist/lexicon/types/{com/atproto/admin/queryModerationStatuses.d.ts → tools/ozone/moderation/queryStatuses.d.ts} +2 -2
- package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/searchRepos.d.ts +2 -2
- package/dist/mod-service/index.d.ts +16 -15
- package/dist/mod-service/subject.d.ts +1 -1
- package/dist/mod-service/types.d.ts +2 -2
- package/dist/mod-service/util.d.ts +6 -0
- package/dist/mod-service/views.d.ts +9 -3
- package/dist/sequencer/sequencer.d.ts +6 -4
- package/dist/util.d.ts +2 -0
- package/package.json +9 -8
- package/src/api/{admin/createCommunicationTemplate.ts → communication/createTemplate.ts} +2 -2
- package/src/api/{admin/deleteCommunicationTemplate.ts → communication/deleteTemplate.ts} +2 -2
- package/src/api/{admin/listCommunicationTemplates.ts → communication/listTemplates.ts} +2 -2
- package/src/api/{admin/updateCommunicationTemplate.ts → communication/updateTemplate.ts} +2 -2
- package/src/api/index.ts +21 -21
- package/src/api/{temp → label}/fetchLabels.ts +5 -3
- package/src/api/label/queryLabels.ts +4 -2
- package/src/api/moderation/emitEvent.ts +218 -0
- package/src/api/{admin/getModerationEvent.ts → moderation/getEvent.ts} +2 -2
- package/src/api/{admin → moderation}/getRecord.ts +3 -3
- package/src/api/{admin → moderation}/getRepo.ts +3 -3
- package/src/api/{admin/queryModerationEvents.ts → moderation/queryEvents.ts} +3 -3
- package/src/api/{admin/queryModerationStatuses.ts → moderation/queryStatuses.ts} +3 -3
- package/src/api/{admin → moderation}/searchRepos.ts +2 -2
- package/src/api/proxied.ts +8 -8
- package/src/api/{moderation → report}/createReport.ts +2 -3
- package/src/api/util.ts +119 -0
- package/src/auth-verifier.ts +20 -30
- package/src/communication-service/template.ts +2 -2
- package/src/config/config.ts +24 -7
- package/src/config/env.ts +6 -4
- package/src/config/secrets.ts +0 -6
- package/src/context.ts +36 -12
- package/src/daemon/blob-diverter.ts +150 -0
- package/src/daemon/context.ts +11 -7
- package/src/daemon/event-pusher.ts +58 -15
- package/src/daemon/index.ts +1 -0
- package/src/db/migrations/20240228T003647759Z-add-label-sigs.ts +25 -0
- package/src/db/migrations/index.ts +1 -0
- package/src/db/schema/index.ts +2 -0
- package/src/db/schema/label.ts +3 -0
- package/src/db/schema/moderation_event.ts +11 -11
- package/src/db/schema/moderation_subject_status.ts +7 -2
- package/src/db/schema/signing_key.ts +10 -0
- package/src/lexicon/index.ts +200 -137
- package/src/lexicon/lexicons.ts +6310 -6012
- package/src/lexicon/types/app/bsky/actor/defs.ts +57 -1
- package/src/lexicon/types/app/bsky/embed/record.ts +2 -0
- package/src/lexicon/types/app/bsky/feed/defs.ts +1 -0
- package/src/lexicon/types/app/bsky/graph/defs.ts +3 -0
- package/src/lexicon/types/app/bsky/labeler/defs.ts +93 -0
- package/src/lexicon/types/app/bsky/labeler/getServices.ts +51 -0
- package/src/lexicon/types/app/bsky/labeler/service.ts +31 -0
- package/src/lexicon/types/com/atproto/admin/defs.ts +0 -694
- package/src/lexicon/types/com/atproto/label/defs.ts +78 -0
- package/src/lexicon/types/{com/atproto/admin/createCommunicationTemplate.ts → tools/ozone/communication/createTemplate.ts} +2 -2
- package/src/lexicon/types/tools/ozone/communication/defs.ts +35 -0
- package/src/lexicon/types/{com/atproto/admin/listCommunicationTemplates.ts → tools/ozone/communication/listTemplates.ts} +2 -2
- package/src/lexicon/types/{com/atproto/admin/updateCommunicationTemplate.ts → tools/ozone/communication/updateTemplate.ts} +2 -2
- package/src/lexicon/types/tools/ozone/moderation/defs.ts +641 -0
- package/src/lexicon/types/{com/atproto/admin/emitModerationEvent.ts → tools/ozone/moderation/emitEvent.ts} +15 -14
- package/src/lexicon/types/{com/atproto/admin/getModerationEvent.ts → tools/ozone/moderation/getEvent.ts} +2 -2
- package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRecord.ts +2 -2
- package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRepo.ts +2 -2
- package/src/lexicon/types/{com/atproto/admin/queryModerationEvents.ts → tools/ozone/moderation/queryEvents.ts} +3 -3
- package/src/lexicon/types/{com/atproto/admin/queryModerationStatuses.ts → tools/ozone/moderation/queryStatuses.ts} +2 -2
- package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/searchRepos.ts +2 -2
- package/src/mod-service/index.ts +46 -50
- package/src/mod-service/lang.ts +1 -1
- package/src/mod-service/status.ts +60 -41
- package/src/mod-service/subject.ts +1 -1
- package/src/mod-service/types.ts +10 -10
- package/src/mod-service/util.ts +49 -5
- package/src/mod-service/views.ts +45 -18
- package/src/sequencer/sequencer.ts +12 -11
- package/src/util.ts +21 -0
- package/tests/__snapshots__/blob-divert.test.ts.snap +22 -0
- package/tests/__snapshots__/get-record.test.ts.snap +14 -6
- package/tests/__snapshots__/get-repo.test.ts.snap +7 -3
- package/tests/__snapshots__/moderation-events.test.ts.snap +8 -8
- package/tests/__snapshots__/moderation-statuses.test.ts.snap +6 -6
- package/tests/_util.ts +5 -0
- package/tests/blob-divert.test.ts +87 -0
- package/tests/communication-templates.test.ts +33 -37
- package/tests/db.test.ts +6 -6
- package/tests/get-record.test.ts +22 -12
- package/tests/get-repo.test.ts +33 -21
- package/tests/moderation-appeals.test.ts +39 -67
- package/tests/moderation-events.test.ts +99 -142
- package/tests/moderation-status-tags.test.ts +20 -37
- package/tests/moderation-statuses.test.ts +132 -65
- package/tests/moderation.test.ts +147 -301
- package/tests/query-labels.test.ts +86 -10
- package/tests/repo-search.test.ts +18 -11
- package/tests/sequencer.test.ts +6 -3
- package/dist/api/admin/util.d.ts +0 -5
- package/dist/api/moderation/util.d.ts +0 -4
- package/src/api/admin/emitModerationEvent.ts +0 -170
- package/src/api/admin/util.ts +0 -54
- package/src/api/moderation/util.ts +0 -67
- /package/dist/api/{admin/createCommunicationTemplate.d.ts → communication/createTemplate.d.ts} +0 -0
- /package/dist/api/{admin/deleteCommunicationTemplate.d.ts → communication/deleteTemplate.d.ts} +0 -0
- /package/dist/api/{admin/emitModerationEvent.d.ts → communication/listTemplates.d.ts} +0 -0
- /package/dist/api/{admin/getModerationEvent.d.ts → communication/updateTemplate.d.ts} +0 -0
- /package/dist/api/{temp → label}/fetchLabels.d.ts +0 -0
- /package/dist/api/{admin/getRecord.d.ts → moderation/emitEvent.d.ts} +0 -0
- /package/dist/api/{admin/getRepo.d.ts → moderation/getEvent.d.ts} +0 -0
- /package/dist/api/{admin/listCommunicationTemplates.d.ts → moderation/getRecord.d.ts} +0 -0
- /package/dist/api/{admin/queryModerationEvents.d.ts → moderation/getRepo.d.ts} +0 -0
- /package/dist/api/{admin/queryModerationStatuses.d.ts → moderation/queryEvents.d.ts} +0 -0
- /package/dist/api/{admin/searchRepos.d.ts → moderation/queryStatuses.d.ts} +0 -0
- /package/dist/api/{admin/updateCommunicationTemplate.d.ts → moderation/searchRepos.d.ts} +0 -0
- /package/dist/api/{moderation → report}/createReport.d.ts +0 -0
- /package/dist/lexicon/types/{com/atproto/admin/deleteCommunicationTemplate.d.ts → tools/ozone/communication/deleteTemplate.d.ts} +0 -0
- /package/src/lexicon/types/{com/atproto/admin/deleteCommunicationTemplate.ts → tools/ozone/communication/deleteTemplate.ts} +0 -0
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import AtpAgent from '@atproto/api'
|
|
2
|
-
import { TestNetwork } from '@atproto/dev-env'
|
|
2
|
+
import { EXAMPLE_LABELER, TestNetwork } from '@atproto/dev-env'
|
|
3
3
|
import { DisconnectError, Subscription } from '@atproto/xrpc-server'
|
|
4
4
|
import { ids, lexicons } from '../src/lexicon/lexicons'
|
|
5
5
|
import { Label } from '../src/lexicon/types/com/atproto/label/defs'
|
|
6
|
+
import { Secp256k1Keypair, verifySignature } from '@atproto/crypto'
|
|
7
|
+
import { cborEncode } from '@atproto/common'
|
|
8
|
+
import { ModerationService } from '../src/mod-service'
|
|
6
9
|
import {
|
|
7
10
|
OutputSchema as LabelMessage,
|
|
8
11
|
isLabels,
|
|
9
12
|
} from '../src/lexicon/types/com/atproto/label/subscribeLabels'
|
|
13
|
+
import { getSigningKeyId } from '../src/util'
|
|
10
14
|
|
|
11
15
|
describe('ozone query labels', () => {
|
|
12
16
|
let network: TestNetwork
|
|
@@ -21,44 +25,44 @@ describe('ozone query labels', () => {
|
|
|
21
25
|
|
|
22
26
|
agent = network.ozone.getClient()
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
const toCreate = [
|
|
25
29
|
{
|
|
26
|
-
src:
|
|
30
|
+
src: EXAMPLE_LABELER,
|
|
27
31
|
uri: 'did:example:blah',
|
|
28
32
|
val: 'spam',
|
|
29
33
|
neg: false,
|
|
30
34
|
cts: new Date().toISOString(),
|
|
31
35
|
},
|
|
32
36
|
{
|
|
33
|
-
src:
|
|
37
|
+
src: EXAMPLE_LABELER,
|
|
34
38
|
uri: 'did:example:blah',
|
|
35
39
|
val: 'impersonation',
|
|
36
40
|
neg: false,
|
|
37
41
|
cts: new Date().toISOString(),
|
|
38
42
|
},
|
|
39
43
|
{
|
|
40
|
-
src:
|
|
44
|
+
src: EXAMPLE_LABELER,
|
|
41
45
|
uri: 'at://did:example:blah/app.bsky.feed.post/1234abcde',
|
|
42
46
|
val: 'spam',
|
|
43
47
|
neg: false,
|
|
44
48
|
cts: new Date().toISOString(),
|
|
45
49
|
},
|
|
46
50
|
{
|
|
47
|
-
src:
|
|
51
|
+
src: EXAMPLE_LABELER,
|
|
48
52
|
uri: 'at://did:example:blah/app.bsky.feed.post/1234abcfg',
|
|
49
53
|
val: 'spam',
|
|
50
54
|
neg: false,
|
|
51
55
|
cts: new Date().toISOString(),
|
|
52
56
|
},
|
|
53
57
|
{
|
|
54
|
-
src:
|
|
58
|
+
src: EXAMPLE_LABELER,
|
|
55
59
|
uri: 'at://did:example:blah/app.bsky.actor.profile/self',
|
|
56
60
|
val: 'spam',
|
|
57
61
|
neg: false,
|
|
58
62
|
cts: new Date().toISOString(),
|
|
59
63
|
},
|
|
60
64
|
{
|
|
61
|
-
src:
|
|
65
|
+
src: EXAMPLE_LABELER,
|
|
62
66
|
uri: 'did:example:thing',
|
|
63
67
|
val: 'spam',
|
|
64
68
|
neg: false,
|
|
@@ -67,7 +71,7 @@ describe('ozone query labels', () => {
|
|
|
67
71
|
]
|
|
68
72
|
|
|
69
73
|
const modService = network.ozone.ctx.modService(network.ozone.ctx.db)
|
|
70
|
-
await modService.createLabels(
|
|
74
|
+
labels = await modService.createLabels(toCreate)
|
|
71
75
|
})
|
|
72
76
|
|
|
73
77
|
afterAll(async () => {
|
|
@@ -128,6 +132,72 @@ describe('ozone query labels', () => {
|
|
|
128
132
|
)
|
|
129
133
|
})
|
|
130
134
|
|
|
135
|
+
it('returns validly signed labels', async () => {
|
|
136
|
+
const res = await agent.api.com.atproto.label.queryLabels({
|
|
137
|
+
uriPatterns: ['*'],
|
|
138
|
+
})
|
|
139
|
+
const signingKey = network.ozone.ctx.signingKey.did()
|
|
140
|
+
for (const label of res.data.labels) {
|
|
141
|
+
const { sig, ...rest } = label
|
|
142
|
+
if (!sig) {
|
|
143
|
+
throw new Error('Missing signature')
|
|
144
|
+
}
|
|
145
|
+
const encodedLabel = cborEncode(rest)
|
|
146
|
+
const isValid = await verifySignature(signingKey, encodedLabel, sig)
|
|
147
|
+
expect(isValid).toBe(true)
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('resigns labels if the signingKey changes', async () => {
|
|
152
|
+
// mock changing the signing key for the service
|
|
153
|
+
const ctx = network.ozone.ctx
|
|
154
|
+
const origModServiceFn = ctx.modService
|
|
155
|
+
|
|
156
|
+
const modSrvc = ctx.modService(ctx.db)
|
|
157
|
+
const newSigningKey = await Secp256k1Keypair.create()
|
|
158
|
+
const newSigningKeyId = await getSigningKeyId(ctx.db, newSigningKey.did())
|
|
159
|
+
ctx.devOverride({
|
|
160
|
+
modService: ModerationService.creator(
|
|
161
|
+
newSigningKey,
|
|
162
|
+
newSigningKeyId,
|
|
163
|
+
ctx.cfg,
|
|
164
|
+
modSrvc.backgroundQueue,
|
|
165
|
+
ctx.idResolver,
|
|
166
|
+
modSrvc.eventPusher,
|
|
167
|
+
modSrvc.appviewAgent,
|
|
168
|
+
ctx.serviceAuthHeaders,
|
|
169
|
+
),
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const res = await agent.api.com.atproto.label.queryLabels({
|
|
173
|
+
uriPatterns: ['*'],
|
|
174
|
+
})
|
|
175
|
+
for (const label of res.data.labels) {
|
|
176
|
+
const { sig, ...rest } = label
|
|
177
|
+
if (!sig) {
|
|
178
|
+
throw new Error('Missing signature')
|
|
179
|
+
}
|
|
180
|
+
const encodedLabel = cborEncode(rest)
|
|
181
|
+
const isValid = await verifySignature(
|
|
182
|
+
newSigningKey.did(),
|
|
183
|
+
encodedLabel,
|
|
184
|
+
sig,
|
|
185
|
+
)
|
|
186
|
+
expect(isValid).toBe(true)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await network.ozone.processAll()
|
|
190
|
+
|
|
191
|
+
const fromDb = await ctx.db.db.selectFrom('label').selectAll().execute()
|
|
192
|
+
expect(fromDb.every((row) => row.signingKeyId === newSigningKeyId)).toBe(
|
|
193
|
+
true,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
ctx.devOverride({
|
|
197
|
+
modService: origModServiceFn,
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
131
201
|
describe('subscribeLabels', () => {
|
|
132
202
|
it('streams all labels from initial cursor.', async () => {
|
|
133
203
|
const ac = new AbortController()
|
|
@@ -154,7 +224,13 @@ describe('ozone query labels', () => {
|
|
|
154
224
|
for await (const message of sub) {
|
|
155
225
|
resetDoneTimer()
|
|
156
226
|
if (isLabels(message)) {
|
|
157
|
-
|
|
227
|
+
for (const label of message.labels) {
|
|
228
|
+
// sigs are currently parsed as a Buffer which is a Uint8Array under the hood, but fails our equality test so we cast to Uint8Array
|
|
229
|
+
streamedLabels.push({
|
|
230
|
+
...label,
|
|
231
|
+
sig: label.sig ? new Uint8Array(label.sig) : undefined,
|
|
232
|
+
})
|
|
233
|
+
}
|
|
158
234
|
}
|
|
159
235
|
}
|
|
160
236
|
expect(streamedLabels).toEqual(labels)
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ModeratorClient,
|
|
3
|
+
SeedClient,
|
|
4
|
+
TestNetwork,
|
|
5
|
+
usersBulkSeed,
|
|
6
|
+
} from '@atproto/dev-env'
|
|
2
7
|
import AtpAgent from '@atproto/api'
|
|
3
8
|
import { paginateAll } from './_util'
|
|
4
9
|
|
|
@@ -6,16 +11,18 @@ describe('admin repo search view', () => {
|
|
|
6
11
|
let network: TestNetwork
|
|
7
12
|
let agent: AtpAgent
|
|
8
13
|
let sc: SeedClient
|
|
14
|
+
let modClient: ModeratorClient
|
|
9
15
|
let headers: { [s: string]: string }
|
|
10
16
|
|
|
11
17
|
beforeAll(async () => {
|
|
12
18
|
network = await TestNetwork.create({
|
|
13
19
|
dbPostgresSchema: 'ozone_admin_repo_search',
|
|
14
20
|
})
|
|
15
|
-
agent = network.
|
|
21
|
+
agent = network.ozone.getClient()
|
|
16
22
|
sc = network.getSeedClient()
|
|
23
|
+
modClient = network.ozone.getModClient()
|
|
17
24
|
await usersBulkSeed(sc)
|
|
18
|
-
headers = network.
|
|
25
|
+
headers = await network.ozone.modHeaders()
|
|
19
26
|
await network.processAll()
|
|
20
27
|
})
|
|
21
28
|
|
|
@@ -24,8 +31,8 @@ describe('admin repo search view', () => {
|
|
|
24
31
|
})
|
|
25
32
|
|
|
26
33
|
beforeAll(async () => {
|
|
27
|
-
await
|
|
28
|
-
event: { $type: '
|
|
34
|
+
await modClient.emitEvent({
|
|
35
|
+
event: { $type: 'tools.ozone.moderation.defs#modEventTakedown' },
|
|
29
36
|
subject: {
|
|
30
37
|
$type: 'com.atproto.admin.defs#repoRef',
|
|
31
38
|
did: sc.dids['cara-wiegand69.test'],
|
|
@@ -34,7 +41,7 @@ describe('admin repo search view', () => {
|
|
|
34
41
|
})
|
|
35
42
|
|
|
36
43
|
it('gives relevant results', async () => {
|
|
37
|
-
const result = await agent.api.
|
|
44
|
+
const result = await agent.api.tools.ozone.moderation.searchRepos(
|
|
38
45
|
{ term: 'car' },
|
|
39
46
|
{ headers },
|
|
40
47
|
)
|
|
@@ -64,7 +71,7 @@ describe('admin repo search view', () => {
|
|
|
64
71
|
|
|
65
72
|
it('finds repo by did', async () => {
|
|
66
73
|
const term = sc.dids['cara-wiegand69.test']
|
|
67
|
-
const res = await agent.api.
|
|
74
|
+
const res = await agent.api.tools.ozone.moderation.searchRepos(
|
|
68
75
|
{ term },
|
|
69
76
|
{ headers },
|
|
70
77
|
)
|
|
@@ -76,7 +83,7 @@ describe('admin repo search view', () => {
|
|
|
76
83
|
it('paginates with term', async () => {
|
|
77
84
|
const results = (results) => results.flatMap((res) => res.users)
|
|
78
85
|
const paginator = async (cursor?: string) => {
|
|
79
|
-
const res = await agent.api.
|
|
86
|
+
const res = await agent.api.tools.ozone.moderation.searchRepos(
|
|
80
87
|
{ term: 'p', cursor, limit: 3 },
|
|
81
88
|
{ headers },
|
|
82
89
|
)
|
|
@@ -88,7 +95,7 @@ describe('admin repo search view', () => {
|
|
|
88
95
|
expect(res.repos.length).toBeLessThanOrEqual(3),
|
|
89
96
|
)
|
|
90
97
|
|
|
91
|
-
const full = await agent.api.
|
|
98
|
+
const full = await agent.api.tools.ozone.moderation.searchRepos(
|
|
92
99
|
{ term: 'p' },
|
|
93
100
|
{ headers },
|
|
94
101
|
)
|
|
@@ -100,7 +107,7 @@ describe('admin repo search view', () => {
|
|
|
100
107
|
it('paginates without term', async () => {
|
|
101
108
|
const results = (results) => results.flatMap((res) => res.repos)
|
|
102
109
|
const paginator = async (cursor?: string) => {
|
|
103
|
-
const res = await agent.api.
|
|
110
|
+
const res = await agent.api.tools.ozone.moderation.searchRepos(
|
|
104
111
|
{ cursor, limit: 3 },
|
|
105
112
|
{ headers },
|
|
106
113
|
)
|
|
@@ -112,7 +119,7 @@ describe('admin repo search view', () => {
|
|
|
112
119
|
expect(res.repos.length).toBeLessThanOrEqual(3),
|
|
113
120
|
)
|
|
114
121
|
|
|
115
|
-
const full = await agent.api.
|
|
122
|
+
const full = await agent.api.tools.ozone.moderation.searchRepos(
|
|
116
123
|
{ limit: 15 },
|
|
117
124
|
{ headers },
|
|
118
125
|
)
|
package/tests/sequencer.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TestNetwork } from '@atproto/dev-env'
|
|
1
|
+
import { EXAMPLE_LABELER, TestNetwork } from '@atproto/dev-env'
|
|
2
2
|
import { readFromGenerator, wait } from '@atproto/common'
|
|
3
3
|
import { LabelsEvt, Sequencer } from '../src/sequencer'
|
|
4
4
|
import Outbox from '../src/sequencer/outbox'
|
|
@@ -33,11 +33,14 @@ describe('sequencer', () => {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const evtToDbRow = (e: LabelsEvt) => {
|
|
36
|
-
const label = e.labels[0]
|
|
36
|
+
const { ver: _, ...label } = e.labels[0]
|
|
37
37
|
return {
|
|
38
38
|
id: e.seq,
|
|
39
39
|
...label,
|
|
40
40
|
cid: label.cid ? label.cid : '',
|
|
41
|
+
exp: null,
|
|
42
|
+
sig: label.sig ? Buffer.from(label.sig) : null,
|
|
43
|
+
signingKeyId: network.ozone.ctx.signingKeyId,
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -54,7 +57,7 @@ describe('sequencer', () => {
|
|
|
54
57
|
for (let i = 0; i < count; i++) {
|
|
55
58
|
const did = `did:example:${randomStr(10, 'base32')}`
|
|
56
59
|
const label = {
|
|
57
|
-
src:
|
|
60
|
+
src: EXAMPLE_LABELER,
|
|
58
61
|
uri: did,
|
|
59
62
|
val: 'spam',
|
|
60
63
|
neg: false,
|
package/dist/api/admin/util.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import AppContext from '../../context';
|
|
2
|
-
import { RepoView, RepoViewDetail, AccountView } from '../../lexicon/types/com/atproto/admin/defs';
|
|
3
|
-
export declare const getPdsAccountInfo: (ctx: AppContext, did: string) => Promise<AccountView | null>;
|
|
4
|
-
export declare const addAccountInfoToRepoViewDetail: (repoView: RepoViewDetail, accountInfo: AccountView | null, includeEmail?: boolean) => RepoViewDetail;
|
|
5
|
-
export declare const addAccountInfoToRepoView: (repoView: RepoView, accountInfo: AccountView | null, includeEmail?: boolean) => RepoView;
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { InputSchema as ReportInput } from '../../lexicon/types/com/atproto/moderation/createReport';
|
|
2
|
-
export declare const getReasonType: (reasonType: ReportInput['reasonType']) => string | boolean;
|
|
3
|
-
export declare const getEventType: (type: string) => "com.atproto.admin.defs#modEventTakedown" | "com.atproto.admin.defs#modEventAcknowledge" | "com.atproto.admin.defs#modEventEscalate" | "com.atproto.admin.defs#modEventComment" | "com.atproto.admin.defs#modEventLabel" | "com.atproto.admin.defs#modEventReport" | "com.atproto.admin.defs#modEventMute" | "com.atproto.admin.defs#modEventReverseTakedown" | "com.atproto.admin.defs#modEventEmail" | "com.atproto.admin.defs#modEventResolveAppeal" | "com.atproto.admin.defs#modEventTag";
|
|
4
|
-
export declare const getReviewState: (reviewState?: string) => "com.atproto.admin.defs#reviewOpen" | "com.atproto.admin.defs#reviewEscalated" | "com.atproto.admin.defs#reviewClosed" | undefined;
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'
|
|
2
|
-
import { Server } from '../../lexicon'
|
|
3
|
-
import AppContext from '../../context'
|
|
4
|
-
import {
|
|
5
|
-
isModEventEmail,
|
|
6
|
-
isModEventLabel,
|
|
7
|
-
isModEventReverseTakedown,
|
|
8
|
-
isModEventTakedown,
|
|
9
|
-
} from '../../lexicon/types/com/atproto/admin/defs'
|
|
10
|
-
import { subjectFromInput } from '../../mod-service/subject'
|
|
11
|
-
import { ModerationLangService } from '../../mod-service/lang'
|
|
12
|
-
import { retryHttp } from '../../util'
|
|
13
|
-
|
|
14
|
-
export default function (server: Server, ctx: AppContext) {
|
|
15
|
-
server.com.atproto.admin.emitModerationEvent({
|
|
16
|
-
auth: ctx.authVerifier.modOrRole,
|
|
17
|
-
handler: async ({ input, auth }) => {
|
|
18
|
-
const access = auth.credentials
|
|
19
|
-
const db = ctx.db
|
|
20
|
-
const moderationService = ctx.modService(db)
|
|
21
|
-
const { createdBy, event } = input.body
|
|
22
|
-
const isTakedownEvent = isModEventTakedown(event)
|
|
23
|
-
const isReverseTakedownEvent = isModEventReverseTakedown(event)
|
|
24
|
-
const isLabelEvent = isModEventLabel(event)
|
|
25
|
-
const subject = subjectFromInput(
|
|
26
|
-
input.body.subject,
|
|
27
|
-
input.body.subjectBlobCids,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
// apply access rules
|
|
31
|
-
|
|
32
|
-
// if less than moderator access then can only take ack and escalation actions
|
|
33
|
-
if (isTakedownEvent || isReverseTakedownEvent) {
|
|
34
|
-
if (!access.isModerator) {
|
|
35
|
-
throw new AuthRequiredError(
|
|
36
|
-
'Must be a full moderator to take this type of action',
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Non admins should not be able to take down feed generators
|
|
41
|
-
if (
|
|
42
|
-
!access.isAdmin &&
|
|
43
|
-
subject.recordPath?.includes('app.bsky.feed.generator/')
|
|
44
|
-
) {
|
|
45
|
-
throw new AuthRequiredError(
|
|
46
|
-
'Must be a full admin to take this type of action on feed generators',
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
// if less than moderator access then can not apply labels
|
|
51
|
-
if (!access.isModerator && isLabelEvent) {
|
|
52
|
-
throw new AuthRequiredError('Must be a full moderator to label content')
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (isLabelEvent) {
|
|
56
|
-
validateLabels([
|
|
57
|
-
...(event.createLabelVals ?? []),
|
|
58
|
-
...(event.negateLabelVals ?? []),
|
|
59
|
-
])
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (isTakedownEvent || isReverseTakedownEvent) {
|
|
63
|
-
const status = await moderationService.getStatus(subject)
|
|
64
|
-
|
|
65
|
-
if (status?.takendown && isTakedownEvent) {
|
|
66
|
-
throw new InvalidRequestError(`Subject is already taken down`)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (!status?.takendown && isReverseTakedownEvent) {
|
|
70
|
-
throw new InvalidRequestError(`Subject is not taken down`)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (status?.takendown && isReverseTakedownEvent && subject.isRecord()) {
|
|
74
|
-
// due to the way blob status is modeled, we should reverse takedown on all
|
|
75
|
-
// blobs for the record being restored, which aren't taken down on another record.
|
|
76
|
-
subject.blobCids = status.blobCids ?? []
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (isModEventEmail(event) && event.content) {
|
|
81
|
-
// sending email prior to logging the event to avoid a long transaction below
|
|
82
|
-
if (!subject.isRepo()) {
|
|
83
|
-
throw new InvalidRequestError(
|
|
84
|
-
'Email can only be sent to a repo subject',
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
const { content, subjectLine } = event
|
|
88
|
-
await retryHttp(() =>
|
|
89
|
-
ctx.modService(db).sendEmail({
|
|
90
|
-
subject: subjectLine,
|
|
91
|
-
content,
|
|
92
|
-
recipientDid: subject.did,
|
|
93
|
-
}),
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const moderationEvent = await db.transaction(async (dbTxn) => {
|
|
98
|
-
const moderationTxn = ctx.modService(dbTxn)
|
|
99
|
-
|
|
100
|
-
const result = await moderationTxn.logEvent({
|
|
101
|
-
event,
|
|
102
|
-
subject,
|
|
103
|
-
createdBy,
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
const moderationLangService = new ModerationLangService(moderationTxn)
|
|
107
|
-
await moderationLangService.tagSubjectWithLang({
|
|
108
|
-
subject,
|
|
109
|
-
createdBy: ctx.cfg.service.did,
|
|
110
|
-
subjectStatus: result.subjectStatus,
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
if (subject.isRepo()) {
|
|
114
|
-
if (isTakedownEvent) {
|
|
115
|
-
const isSuspend = !!result.event.durationInHours
|
|
116
|
-
await moderationTxn.takedownRepo(
|
|
117
|
-
subject,
|
|
118
|
-
result.event.id,
|
|
119
|
-
isSuspend,
|
|
120
|
-
)
|
|
121
|
-
} else if (isReverseTakedownEvent) {
|
|
122
|
-
await moderationTxn.reverseTakedownRepo(subject)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (subject.isRecord()) {
|
|
127
|
-
if (isTakedownEvent) {
|
|
128
|
-
await moderationTxn.takedownRecord(subject, result.event.id)
|
|
129
|
-
} else if (isReverseTakedownEvent) {
|
|
130
|
-
await moderationTxn.reverseTakedownRecord(subject)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (isLabelEvent) {
|
|
135
|
-
await moderationTxn.formatAndCreateLabels(
|
|
136
|
-
result.event.subjectUri ?? result.event.subjectDid,
|
|
137
|
-
result.event.subjectCid,
|
|
138
|
-
{
|
|
139
|
-
create: result.event.createLabelVals?.length
|
|
140
|
-
? result.event.createLabelVals.split(' ')
|
|
141
|
-
: undefined,
|
|
142
|
-
negate: result.event.negateLabelVals?.length
|
|
143
|
-
? result.event.negateLabelVals.split(' ')
|
|
144
|
-
: undefined,
|
|
145
|
-
},
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return result.event
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
encoding: 'application/json',
|
|
154
|
-
body: moderationService.views.formatEvent(moderationEvent),
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
})
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const validateLabels = (labels: string[]) => {
|
|
161
|
-
for (const label of labels) {
|
|
162
|
-
for (const char of badChars) {
|
|
163
|
-
if (label.includes(char)) {
|
|
164
|
-
throw new InvalidRequestError(`Invalid label: ${label}`)
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const badChars = [' ', ',', ';', `'`, `"`]
|
package/src/api/admin/util.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import AppContext from '../../context'
|
|
2
|
-
import {
|
|
3
|
-
RepoView,
|
|
4
|
-
RepoViewDetail,
|
|
5
|
-
AccountView,
|
|
6
|
-
} from '../../lexicon/types/com/atproto/admin/defs'
|
|
7
|
-
|
|
8
|
-
export const getPdsAccountInfo = async (
|
|
9
|
-
ctx: AppContext,
|
|
10
|
-
did: string,
|
|
11
|
-
): Promise<AccountView | null> => {
|
|
12
|
-
const agent = ctx.pdsAgent
|
|
13
|
-
if (!agent) return null
|
|
14
|
-
const auth = await ctx.pdsAuth()
|
|
15
|
-
if (!auth) return null
|
|
16
|
-
try {
|
|
17
|
-
const res = await agent.api.com.atproto.admin.getAccountInfo({ did }, auth)
|
|
18
|
-
return res.data
|
|
19
|
-
} catch (err) {
|
|
20
|
-
return null
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const addAccountInfoToRepoViewDetail = (
|
|
25
|
-
repoView: RepoViewDetail,
|
|
26
|
-
accountInfo: AccountView | null,
|
|
27
|
-
includeEmail = false,
|
|
28
|
-
): RepoViewDetail => {
|
|
29
|
-
if (!accountInfo) return repoView
|
|
30
|
-
return {
|
|
31
|
-
...repoView,
|
|
32
|
-
email: includeEmail ? accountInfo.email : undefined,
|
|
33
|
-
invitedBy: accountInfo.invitedBy,
|
|
34
|
-
invitesDisabled: accountInfo.invitesDisabled,
|
|
35
|
-
inviteNote: accountInfo.inviteNote,
|
|
36
|
-
invites: accountInfo.invites,
|
|
37
|
-
emailConfirmedAt: accountInfo.emailConfirmedAt,
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const addAccountInfoToRepoView = (
|
|
42
|
-
repoView: RepoView,
|
|
43
|
-
accountInfo: AccountView | null,
|
|
44
|
-
includeEmail = false,
|
|
45
|
-
): RepoView => {
|
|
46
|
-
if (!accountInfo) return repoView
|
|
47
|
-
return {
|
|
48
|
-
...repoView,
|
|
49
|
-
email: includeEmail ? accountInfo.email : undefined,
|
|
50
|
-
invitedBy: accountInfo.invitedBy,
|
|
51
|
-
invitesDisabled: accountInfo.invitesDisabled,
|
|
52
|
-
inviteNote: accountInfo.inviteNote,
|
|
53
|
-
}
|
|
54
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
2
|
-
import { InputSchema as ReportInput } from '../../lexicon/types/com/atproto/moderation/createReport'
|
|
3
|
-
import {
|
|
4
|
-
REASONOTHER,
|
|
5
|
-
REASONSPAM,
|
|
6
|
-
REASONMISLEADING,
|
|
7
|
-
REASONRUDE,
|
|
8
|
-
REASONSEXUAL,
|
|
9
|
-
REASONVIOLATION,
|
|
10
|
-
REASONAPPEAL,
|
|
11
|
-
} from '../../lexicon/types/com/atproto/moderation/defs'
|
|
12
|
-
import {
|
|
13
|
-
REVIEWCLOSED,
|
|
14
|
-
REVIEWESCALATED,
|
|
15
|
-
REVIEWOPEN,
|
|
16
|
-
} from '../../lexicon/types/com/atproto/admin/defs'
|
|
17
|
-
import { ModerationEvent } from '../../db/schema/moderation_event'
|
|
18
|
-
import { ModerationSubjectStatusRow } from '../../mod-service/types'
|
|
19
|
-
|
|
20
|
-
export const getReasonType = (reasonType: ReportInput['reasonType']) => {
|
|
21
|
-
if (reasonTypes.has(reasonType)) {
|
|
22
|
-
return reasonType as NonNullable<ModerationEvent['meta']>['reportType']
|
|
23
|
-
}
|
|
24
|
-
throw new InvalidRequestError('Invalid reason type')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const getEventType = (type: string) => {
|
|
28
|
-
if (eventTypes.has(type)) {
|
|
29
|
-
return type as ModerationEvent['action']
|
|
30
|
-
}
|
|
31
|
-
throw new InvalidRequestError('Invalid event type')
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const getReviewState = (reviewState?: string) => {
|
|
35
|
-
if (!reviewState) return undefined
|
|
36
|
-
if (reviewStates.has(reviewState)) {
|
|
37
|
-
return reviewState as ModerationSubjectStatusRow['reviewState']
|
|
38
|
-
}
|
|
39
|
-
throw new InvalidRequestError('Invalid review state')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const reviewStates = new Set([REVIEWCLOSED, REVIEWESCALATED, REVIEWOPEN])
|
|
43
|
-
|
|
44
|
-
const reasonTypes = new Set([
|
|
45
|
-
REASONOTHER,
|
|
46
|
-
REASONSPAM,
|
|
47
|
-
REASONMISLEADING,
|
|
48
|
-
REASONRUDE,
|
|
49
|
-
REASONSEXUAL,
|
|
50
|
-
REASONVIOLATION,
|
|
51
|
-
REASONAPPEAL,
|
|
52
|
-
])
|
|
53
|
-
|
|
54
|
-
const eventTypes = new Set([
|
|
55
|
-
'com.atproto.admin.defs#modEventTakedown',
|
|
56
|
-
'com.atproto.admin.defs#modEventAcknowledge',
|
|
57
|
-
'com.atproto.admin.defs#modEventEscalate',
|
|
58
|
-
'com.atproto.admin.defs#modEventComment',
|
|
59
|
-
'com.atproto.admin.defs#modEventLabel',
|
|
60
|
-
'com.atproto.admin.defs#modEventReport',
|
|
61
|
-
'com.atproto.admin.defs#modEventMute',
|
|
62
|
-
'com.atproto.admin.defs#modEventUnmute',
|
|
63
|
-
'com.atproto.admin.defs#modEventReverseTakedown',
|
|
64
|
-
'com.atproto.admin.defs#modEventEmail',
|
|
65
|
-
'com.atproto.admin.defs#modEventResolveAppeal',
|
|
66
|
-
'com.atproto.admin.defs#modEventTag',
|
|
67
|
-
])
|
/package/dist/api/{admin/createCommunicationTemplate.d.ts → communication/createTemplate.d.ts}
RENAMED
|
File without changes
|
/package/dist/api/{admin/deleteCommunicationTemplate.d.ts → communication/deleteTemplate.d.ts}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|