@atproto/ozone 0.0.13 → 0.0.15
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 +14 -0
- package/dist/config/config.d.ts +4 -0
- package/dist/config/env.d.ts +1 -0
- package/dist/context.d.ts +2 -0
- package/dist/image-invalidator.d.ts +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25 -6
- package/dist/index.js.map +2 -2
- package/dist/mod-service/index.d.ts +5 -2
- package/package.json +5 -5
- package/src/config/config.ts +10 -0
- package/src/config/env.ts +3 -1
- package/src/context.ts +4 -0
- package/src/image-invalidator.ts +7 -0
- package/src/index.ts +1 -0
- package/src/mod-service/index.ts +37 -5
- package/tests/moderation.test.ts +27 -0
|
@@ -11,6 +11,7 @@ import { Label } from '../lexicon/types/com/atproto/label/defs';
|
|
|
11
11
|
import { ModSubject, RecordSubject, RepoSubject } from './subject';
|
|
12
12
|
import { BackgroundQueue } from '../background';
|
|
13
13
|
import { EventPusher } from '../daemon';
|
|
14
|
+
import { ImageInvalidator } from '../image-invalidator';
|
|
14
15
|
export type ModerationServiceCreator = (db: Database) => ModerationService;
|
|
15
16
|
export declare class ModerationService {
|
|
16
17
|
db: Database;
|
|
@@ -19,8 +20,10 @@ export declare class ModerationService {
|
|
|
19
20
|
appviewAgent: AtpAgent;
|
|
20
21
|
private appviewAuth;
|
|
21
22
|
serverDid: string;
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
imgInvalidator?: ImageInvalidator | undefined;
|
|
24
|
+
cdnPaths?: string[] | undefined;
|
|
25
|
+
constructor(db: Database, backgroundQueue: BackgroundQueue, eventPusher: EventPusher, appviewAgent: AtpAgent, appviewAuth: AppviewAuth, serverDid: string, imgInvalidator?: ImageInvalidator | undefined, cdnPaths?: string[] | undefined);
|
|
26
|
+
static creator(backgroundQueue: BackgroundQueue, eventPusher: EventPusher, appviewAgent: AtpAgent, appviewAuth: AppviewAuth, serverDid: string, imgInvalidator?: ImageInvalidator, cdnPaths?: string[]): (db: Database) => ModerationService;
|
|
24
27
|
views: ModerationViews;
|
|
25
28
|
getEvent(id: number): Promise<ModerationEventRow | undefined>;
|
|
26
29
|
getEventOrThrow(id: number): Promise<ModerationEventRow>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/ozone",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Backend service for moderating the Bluesky network.",
|
|
6
6
|
"keywords": [
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"pino-http": "^8.2.1",
|
|
31
31
|
"typed-emitter": "^2.1.0",
|
|
32
32
|
"uint8arrays": "3.0.0",
|
|
33
|
-
"@atproto/api": "^0.10.
|
|
33
|
+
"@atproto/api": "^0.10.4",
|
|
34
34
|
"@atproto/common": "^0.3.3",
|
|
35
35
|
"@atproto/crypto": "^0.3.0",
|
|
36
36
|
"@atproto/identity": "^0.3.2",
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
"@types/pg": "^8.6.6",
|
|
47
47
|
"@types/qs": "^6.9.7",
|
|
48
48
|
"axios": "^0.27.2",
|
|
49
|
-
"@atproto/api": "^0.10.
|
|
50
|
-
"@atproto/dev-env": "^0.2.
|
|
49
|
+
"@atproto/api": "^0.10.4",
|
|
50
|
+
"@atproto/dev-env": "^0.2.36",
|
|
51
51
|
"@atproto/lex-cli": "^0.3.1",
|
|
52
|
-
"@atproto/pds": "^0.4.
|
|
52
|
+
"@atproto/pds": "^0.4.4",
|
|
53
53
|
"@atproto/xrpc": "^0.4.2"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
package/src/config/config.ts
CHANGED
|
@@ -38,6 +38,10 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => {
|
|
|
38
38
|
did: env.pdsDid,
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
const cdnCfg: OzoneConfig['cdn'] = {
|
|
42
|
+
paths: env.cdnPaths,
|
|
43
|
+
}
|
|
44
|
+
|
|
41
45
|
assert(env.didPlcUrl)
|
|
42
46
|
const identityCfg: OzoneConfig['identity'] = {
|
|
43
47
|
plcUrl: env.didPlcUrl,
|
|
@@ -48,6 +52,7 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => {
|
|
|
48
52
|
db: dbCfg,
|
|
49
53
|
appview: appviewCfg,
|
|
50
54
|
pds: pdsCfg,
|
|
55
|
+
cdn: cdnCfg,
|
|
51
56
|
identity: identityCfg,
|
|
52
57
|
}
|
|
53
58
|
}
|
|
@@ -57,6 +62,7 @@ export type OzoneConfig = {
|
|
|
57
62
|
db: DatabaseConfig
|
|
58
63
|
appview: AppviewConfig
|
|
59
64
|
pds: PdsConfig | null
|
|
65
|
+
cdn: CdnConfig
|
|
60
66
|
identity: IdentityConfig
|
|
61
67
|
}
|
|
62
68
|
|
|
@@ -88,3 +94,7 @@ export type PdsConfig = {
|
|
|
88
94
|
export type IdentityConfig = {
|
|
89
95
|
plcUrl: string
|
|
90
96
|
}
|
|
97
|
+
|
|
98
|
+
export type CdnConfig = {
|
|
99
|
+
paths?: string[]
|
|
100
|
+
}
|
package/src/config/env.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { envInt, envStr } from '@atproto/common'
|
|
1
|
+
import { envInt, envList, envStr } from '@atproto/common'
|
|
2
2
|
|
|
3
3
|
export const readEnv = (): OzoneEnvironment => {
|
|
4
4
|
return {
|
|
@@ -17,6 +17,7 @@ export const readEnv = (): OzoneEnvironment => {
|
|
|
17
17
|
dbPoolMaxUses: envInt('OZONE_DB_POOL_MAX_USES'),
|
|
18
18
|
dbPoolIdleTimeoutMs: envInt('OZONE_DB_POOL_IDLE_TIMEOUT_MS'),
|
|
19
19
|
didPlcUrl: envStr('OZONE_DID_PLC_URL'),
|
|
20
|
+
cdnPaths: envList('OZONE_CDN_PATHS'),
|
|
20
21
|
adminPassword: envStr('OZONE_ADMIN_PASSWORD'),
|
|
21
22
|
moderatorPassword: envStr('OZONE_MODERATOR_PASSWORD'),
|
|
22
23
|
triagePassword: envStr('OZONE_TRIAGE_PASSWORD'),
|
|
@@ -40,6 +41,7 @@ export type OzoneEnvironment = {
|
|
|
40
41
|
dbPoolMaxUses?: number
|
|
41
42
|
dbPoolIdleTimeoutMs?: number
|
|
42
43
|
didPlcUrl?: string
|
|
44
|
+
cdnPaths?: string[]
|
|
43
45
|
adminPassword?: string
|
|
44
46
|
moderatorPassword?: string
|
|
45
47
|
triagePassword?: string
|
package/src/context.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
CommunicationTemplateService,
|
|
16
16
|
CommunicationTemplateServiceCreator,
|
|
17
17
|
} from './communication-service/template'
|
|
18
|
+
import { ImageInvalidator } from './image-invalidator'
|
|
18
19
|
|
|
19
20
|
export type AppContextOptions = {
|
|
20
21
|
db: Database
|
|
@@ -25,6 +26,7 @@ export type AppContextOptions = {
|
|
|
25
26
|
pdsAgent: AtpAgent | undefined
|
|
26
27
|
signingKey: Keypair
|
|
27
28
|
idResolver: IdResolver
|
|
29
|
+
imgInvalidator?: ImageInvalidator
|
|
28
30
|
backgroundQueue: BackgroundQueue
|
|
29
31
|
sequencer: Sequencer
|
|
30
32
|
}
|
|
@@ -71,6 +73,8 @@ export class AppContext {
|
|
|
71
73
|
appviewAgent,
|
|
72
74
|
appviewAuth,
|
|
73
75
|
cfg.service.did,
|
|
76
|
+
overrides?.imgInvalidator,
|
|
77
|
+
cfg.cdn.paths,
|
|
74
78
|
)
|
|
75
79
|
|
|
76
80
|
const communicationTemplateService = CommunicationTemplateService.creator()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Invalidation is a general interface for propagating an image blob
|
|
2
|
+
// takedown through any caches where a representation of it may be stored.
|
|
3
|
+
// @NOTE this does not remove the blob from storage: just invalidates it from caches.
|
|
4
|
+
// @NOTE keep in sync with same interface in aws/src/cloudfront.ts
|
|
5
|
+
export interface ImageInvalidator {
|
|
6
|
+
invalidate(subject: string, paths: string[]): Promise<void>
|
|
7
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { createServer } from './lexicon'
|
|
|
13
13
|
import AppContext, { AppContextOptions } from './context'
|
|
14
14
|
|
|
15
15
|
export * from './config'
|
|
16
|
+
export { type ImageInvalidator } from './image-invalidator'
|
|
16
17
|
export { Database } from './db'
|
|
17
18
|
export { OzoneDaemon, EventPusher, EventReverser } from './daemon'
|
|
18
19
|
export { AppContext } from './context'
|
package/src/mod-service/index.ts
CHANGED
|
@@ -39,11 +39,13 @@ import {
|
|
|
39
39
|
RepoSubject,
|
|
40
40
|
subjectFromStatusRow,
|
|
41
41
|
} from './subject'
|
|
42
|
+
import { jsonb } from '../db/types'
|
|
43
|
+
import { LabelChannel } from '../db/schema/label'
|
|
42
44
|
import { BlobPushEvent } from '../db/schema/blob_push_event'
|
|
43
45
|
import { BackgroundQueue } from '../background'
|
|
44
46
|
import { EventPusher } from '../daemon'
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
+
import { ImageInvalidator } from '../image-invalidator'
|
|
48
|
+
import { httpLogger as log } from '../logger'
|
|
47
49
|
|
|
48
50
|
export type ModerationServiceCreator = (db: Database) => ModerationService
|
|
49
51
|
|
|
@@ -55,6 +57,8 @@ export class ModerationService {
|
|
|
55
57
|
public appviewAgent: AtpAgent,
|
|
56
58
|
private appviewAuth: AppviewAuth,
|
|
57
59
|
public serverDid: string,
|
|
60
|
+
public imgInvalidator?: ImageInvalidator,
|
|
61
|
+
public cdnPaths?: string[],
|
|
58
62
|
) {}
|
|
59
63
|
|
|
60
64
|
static creator(
|
|
@@ -63,6 +67,8 @@ export class ModerationService {
|
|
|
63
67
|
appviewAgent: AtpAgent,
|
|
64
68
|
appviewAuth: AppviewAuth,
|
|
65
69
|
serverDid: string,
|
|
70
|
+
imgInvalidator?: ImageInvalidator,
|
|
71
|
+
cdnPaths?: string[],
|
|
66
72
|
) {
|
|
67
73
|
return (db: Database) =>
|
|
68
74
|
new ModerationService(
|
|
@@ -72,6 +78,8 @@ export class ModerationService {
|
|
|
72
78
|
appviewAgent,
|
|
73
79
|
appviewAuth,
|
|
74
80
|
serverDid,
|
|
81
|
+
imgInvalidator,
|
|
82
|
+
cdnPaths,
|
|
75
83
|
)
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -556,14 +564,38 @@ export class ModerationService {
|
|
|
556
564
|
lastAttempted: null,
|
|
557
565
|
}),
|
|
558
566
|
)
|
|
559
|
-
.returning('id')
|
|
567
|
+
.returning(['id', 'subjectDid', 'subjectBlobCid', 'eventType'])
|
|
560
568
|
.execute()
|
|
561
569
|
|
|
562
570
|
this.db.onCommit(() => {
|
|
563
571
|
this.backgroundQueue.add(async () => {
|
|
564
|
-
await Promise.
|
|
565
|
-
blobEvts.map((evt) =>
|
|
572
|
+
await Promise.allSettled(
|
|
573
|
+
blobEvts.map((evt) =>
|
|
574
|
+
this.eventPusher
|
|
575
|
+
.attemptBlobEvent(evt.id)
|
|
576
|
+
.catch((err) =>
|
|
577
|
+
log.error({ err, ...evt }, 'failed to push blob event'),
|
|
578
|
+
),
|
|
579
|
+
),
|
|
566
580
|
)
|
|
581
|
+
|
|
582
|
+
if (this.imgInvalidator) {
|
|
583
|
+
await Promise.allSettled(
|
|
584
|
+
(subject.blobCids ?? []).map((cid) => {
|
|
585
|
+
const paths = (this.cdnPaths ?? []).map((path) =>
|
|
586
|
+
path.replace('%s', subject.did).replace('%s', cid),
|
|
587
|
+
)
|
|
588
|
+
return this.imgInvalidator
|
|
589
|
+
?.invalidate(cid, paths)
|
|
590
|
+
.catch((err) =>
|
|
591
|
+
log.error(
|
|
592
|
+
{ err, paths, cid },
|
|
593
|
+
'failed to invalidate blob on cdn',
|
|
594
|
+
),
|
|
595
|
+
)
|
|
596
|
+
}),
|
|
597
|
+
)
|
|
598
|
+
}
|
|
567
599
|
})
|
|
568
600
|
})
|
|
569
601
|
}
|
package/tests/moderation.test.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from '../src/lexicon/types/com/atproto/admin/defs'
|
|
26
26
|
import { EventReverser } from '../src'
|
|
27
27
|
import { TestOzone } from '@atproto/dev-env/src/ozone'
|
|
28
|
+
import { ImageInvalidator } from '../src/image-invalidator'
|
|
28
29
|
import {
|
|
29
30
|
UNSPECCED_TAKEDOWN_BLOBS_LABEL,
|
|
30
31
|
UNSPECCED_TAKEDOWN_LABEL,
|
|
@@ -43,6 +44,7 @@ type TakedownParams = BaseCreateReportParams &
|
|
|
43
44
|
describe('moderation', () => {
|
|
44
45
|
let network: TestNetwork
|
|
45
46
|
let ozone: TestOzone
|
|
47
|
+
let mockInvalidator: MockInvalidator
|
|
46
48
|
let agent: AtpAgent
|
|
47
49
|
let bskyAgent: AtpAgent
|
|
48
50
|
let pdsAgent: AtpAgent
|
|
@@ -155,8 +157,13 @@ describe('moderation', () => {
|
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
beforeAll(async () => {
|
|
160
|
+
mockInvalidator = new MockInvalidator()
|
|
158
161
|
network = await TestNetwork.create({
|
|
159
162
|
dbPostgresSchema: 'ozone_moderation',
|
|
163
|
+
ozone: {
|
|
164
|
+
imgInvalidator: mockInvalidator,
|
|
165
|
+
cdnPaths: ['/path1/%s/%s', '/path2/%s/%s'],
|
|
166
|
+
},
|
|
160
167
|
})
|
|
161
168
|
ozone = network.ozone
|
|
162
169
|
agent = network.ozone.getClient()
|
|
@@ -981,6 +988,18 @@ describe('moderation', () => {
|
|
|
981
988
|
expect(await fetchImage.json()).toEqual({ message: 'Image not found' })
|
|
982
989
|
})
|
|
983
990
|
|
|
991
|
+
it('invalidates the image in the cdn', async () => {
|
|
992
|
+
const blobCid = blob.image.ref.toString()
|
|
993
|
+
expect(mockInvalidator.invalidated.length).toBe(1)
|
|
994
|
+
expect(mockInvalidator.invalidated.at(0)?.subject).toBe(blobCid)
|
|
995
|
+
expect(mockInvalidator.invalidated.at(0)?.paths.at(0)).toEqual(
|
|
996
|
+
`/path1/${sc.dids.carol}/${blobCid}`,
|
|
997
|
+
)
|
|
998
|
+
expect(mockInvalidator.invalidated.at(0)?.paths.at(1)).toEqual(
|
|
999
|
+
`/path2/${sc.dids.carol}/${blobCid}`,
|
|
1000
|
+
)
|
|
1001
|
+
})
|
|
1002
|
+
|
|
984
1003
|
it('fans takedown out to pds', async () => {
|
|
985
1004
|
const res = await pdsAgent.api.com.atproto.admin.getSubjectStatus(
|
|
986
1005
|
{
|
|
@@ -1059,3 +1078,11 @@ describe('moderation', () => {
|
|
|
1059
1078
|
})
|
|
1060
1079
|
})
|
|
1061
1080
|
})
|
|
1081
|
+
|
|
1082
|
+
class MockInvalidator implements ImageInvalidator {
|
|
1083
|
+
invalidated: { subject: string; paths: string[] }[] = []
|
|
1084
|
+
|
|
1085
|
+
async invalidate(subject: string, paths: string[]) {
|
|
1086
|
+
this.invalidated.push({ subject, paths })
|
|
1087
|
+
}
|
|
1088
|
+
}
|