@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.
@@ -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
- constructor(db: Database, backgroundQueue: BackgroundQueue, eventPusher: EventPusher, appviewAgent: AtpAgent, appviewAuth: AppviewAuth, serverDid: string);
23
- static creator(backgroundQueue: BackgroundQueue, eventPusher: EventPusher, appviewAgent: AtpAgent, appviewAuth: AppviewAuth, serverDid: string): (db: Database) => ModerationService;
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.13",
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.2",
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.2",
50
- "@atproto/dev-env": "^0.2.34",
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.2",
52
+ "@atproto/pds": "^0.4.4",
53
53
  "@atproto/xrpc": "^0.4.2"
54
54
  },
55
55
  "scripts": {
@@ -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'
@@ -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 { jsonb } from '../db/types'
46
- import { LabelChannel } from '../db/schema/label'
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.all(
565
- blobEvts.map((evt) => this.eventPusher.attemptBlobEvent(evt.id)),
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
  }
@@ -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
+ }