@atproto/ozone 0.1.63 → 0.1.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/api/moderation/queryEvents.d.ts.map +1 -1
  3. package/dist/api/moderation/queryEvents.js +2 -1
  4. package/dist/api/moderation/queryEvents.js.map +1 -1
  5. package/dist/daemon/blob-diverter.d.ts +26 -11
  6. package/dist/daemon/blob-diverter.d.ts.map +1 -1
  7. package/dist/daemon/blob-diverter.js +109 -52
  8. package/dist/daemon/blob-diverter.js.map +1 -1
  9. package/dist/lexicon/lexicons.d.ts +30 -0
  10. package/dist/lexicon/lexicons.d.ts.map +1 -1
  11. package/dist/lexicon/lexicons.js +15 -0
  12. package/dist/lexicon/lexicons.js.map +1 -1
  13. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +2 -0
  14. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
  15. package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
  16. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts +1 -0
  17. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts.map +1 -1
  18. package/dist/mod-service/index.d.ts +1 -0
  19. package/dist/mod-service/index.d.ts.map +1 -1
  20. package/dist/mod-service/index.js +12 -1
  21. package/dist/mod-service/index.js.map +1 -1
  22. package/dist/mod-service/views.d.ts.map +1 -1
  23. package/dist/mod-service/views.js +8 -0
  24. package/dist/mod-service/views.js.map +1 -1
  25. package/dist/setting/constants.d.ts +1 -0
  26. package/dist/setting/constants.d.ts.map +1 -1
  27. package/dist/setting/constants.js +2 -1
  28. package/dist/setting/constants.js.map +1 -1
  29. package/dist/setting/validators.d.ts.map +1 -1
  30. package/dist/setting/validators.js +19 -0
  31. package/dist/setting/validators.js.map +1 -1
  32. package/dist/util.d.ts +2 -3
  33. package/dist/util.d.ts.map +1 -1
  34. package/dist/util.js +9 -20
  35. package/dist/util.js.map +1 -1
  36. package/package.json +10 -11
  37. package/src/api/moderation/queryEvents.ts +2 -0
  38. package/src/daemon/blob-diverter.ts +137 -102
  39. package/src/lexicon/lexicons.ts +17 -0
  40. package/src/lexicon/types/tools/ozone/moderation/defs.ts +2 -0
  41. package/src/lexicon/types/tools/ozone/moderation/queryEvents.ts +1 -0
  42. package/src/mod-service/index.ts +14 -0
  43. package/src/mod-service/views.ts +11 -0
  44. package/src/setting/constants.ts +1 -0
  45. package/src/setting/validators.ts +28 -1
  46. package/src/util.ts +8 -20
  47. package/tests/_util.ts +46 -0
  48. package/tests/blob-divert.test.ts +30 -19
  49. package/tests/moderation.test.ts +3 -1
  50. package/tests/server.test.ts +23 -35
  51. package/tests/takedown.test.ts +64 -0
  52. package/tsconfig.tests.tsbuildinfo +1 -1
@@ -1,10 +1,11 @@
1
- import assert from 'node:assert'
2
1
  import {
3
2
  ModeratorClient,
4
3
  SeedClient,
5
4
  TestNetwork,
6
5
  basicSeed,
7
6
  } from '@atproto/dev-env'
7
+ import { ResponseType, XRPCError } from '@atproto/xrpc'
8
+ import assert from 'node:assert'
8
9
  import { forSnapshot } from './_util'
9
10
 
10
11
  describe('blob divert', () => {
@@ -30,13 +31,16 @@ describe('blob divert', () => {
30
31
  await network.close()
31
32
  })
32
33
 
33
- const mockReportServiceResponse = (result: boolean) => {
34
+ const mockReportServiceResponse = (succeeds: boolean) => {
34
35
  const blobDiverter = network.ozone.ctx.blobDiverter
35
36
  assert(blobDiverter)
36
37
  return jest
37
- .spyOn(blobDiverter, 'sendImage')
38
+ .spyOn(blobDiverter, 'uploadBlob')
38
39
  .mockImplementation(async () => {
39
- return result
40
+ if (!succeeds) {
41
+ // Using an XRPCError to trigger retries
42
+ throw new XRPCError(ResponseType.Unknown, undefined)
43
+ }
40
44
  })
41
45
  }
42
46
 
@@ -46,6 +50,8 @@ describe('blob divert', () => {
46
50
  cid: sc.posts[sc.dids.carol][0].ref.cidStr,
47
51
  })
48
52
 
53
+ const getImages = () => sc.posts[sc.dids.carol][0].images
54
+
49
55
  const emitDivertEvent = async () =>
50
56
  modClient.emitEvent(
51
57
  {
@@ -55,9 +61,7 @@ describe('blob divert', () => {
55
61
  comment: 'Diverting for test',
56
62
  },
57
63
  createdBy: sc.dids.alice,
58
- subjectBlobCids: sc.posts[sc.dids.carol][0].images.map((img) =>
59
- img.image.ref.toString(),
60
- ),
64
+ subjectBlobCids: getImages().map((img) => img.image.ref.toString()),
61
65
  },
62
66
  'moderator',
63
67
  )
@@ -65,25 +69,32 @@ describe('blob divert', () => {
65
69
  it('fails and keeps attempt count when report service fails to accept upload.', async () => {
66
70
  // Simulate failure to fail upload
67
71
  const reportServiceRequest = mockReportServiceResponse(false)
72
+ try {
73
+ await expect(emitDivertEvent()).rejects.toThrow('Failed to process blobs')
68
74
 
69
- await expect(emitDivertEvent()).rejects.toThrow()
70
-
71
- expect(reportServiceRequest).toHaveBeenCalled()
75
+ // 1 initial attempt + 3 retries
76
+ expect(reportServiceRequest).toHaveBeenCalledTimes(getImages().length * 4)
77
+ } finally {
78
+ reportServiceRequest.mockRestore()
79
+ }
72
80
  })
73
81
 
74
82
  it('sends blobs to configured divert service and marks divert date', async () => {
75
- // Simulate failure to accept upload
83
+ // Simulate success to accept upload
76
84
  const reportServiceRequest = mockReportServiceResponse(true)
85
+ try {
86
+ const divertEvent = await emitDivertEvent()
77
87
 
78
- const divertEvent = await emitDivertEvent()
88
+ expect(reportServiceRequest).toHaveBeenCalledTimes(getImages().length)
89
+ expect(forSnapshot(divertEvent)).toMatchSnapshot()
79
90
 
80
- expect(reportServiceRequest).toHaveBeenCalled()
81
- expect(forSnapshot(divertEvent)).toMatchSnapshot()
82
-
83
- const { subjectStatuses } = await modClient.queryStatuses({
84
- subject: getSubject().uri,
85
- })
91
+ const { subjectStatuses } = await modClient.queryStatuses({
92
+ subject: getSubject().uri,
93
+ })
86
94
 
87
- expect(subjectStatuses[0].takendown).toBe(true)
95
+ expect(subjectStatuses[0].takendown).toBe(true)
96
+ } finally {
97
+ reportServiceRequest.mockRestore()
98
+ }
88
99
  })
89
100
  })
@@ -816,7 +816,9 @@ describe('moderation', () => {
816
816
  it.skip('prevents image blob from being served, even when cached.', async () => {
817
817
  const fetchImage = await fetch(imageUri)
818
818
  expect(fetchImage.status).toEqual(404)
819
- expect(await fetchImage.json()).toEqual({ message: 'Image not found' })
819
+ expect(await fetchImage.json()).toMatchObject({
820
+ message: 'Blob not found',
821
+ })
820
822
  })
821
823
 
822
824
  it('invalidates the image in the cdn', async () => {
@@ -1,8 +1,7 @@
1
- import { AddressInfo } from 'net'
1
+ import { TestNetwork, TestOzone } from '@atproto/dev-env'
2
2
  import express from 'express'
3
- import axios, { AxiosError } from 'axios'
4
- import { TestOzone, TestNetwork } from '@atproto/dev-env'
5
3
  import { handler as errorHandler } from '../src/error'
4
+ import { startServer } from './_util'
6
5
 
7
6
  describe('server', () => {
8
7
  let network: TestNetwork
@@ -20,56 +19,45 @@ describe('server', () => {
20
19
  })
21
20
 
22
21
  it('preserves 404s.', async () => {
23
- const promise = axios.get(`${ozone.url}/unknown`)
24
- await expect(promise).rejects.toThrow('failed with status code 404')
22
+ const response = await fetch(`${ozone.url}/unknown`)
23
+ expect(response.status).toEqual(404)
25
24
  })
26
25
 
27
26
  it('error handler turns unknown errors into 500s.', async () => {
28
27
  const app = express()
29
- app.get('/oops', () => {
30
- throw new Error('Oops!')
31
- })
32
- app.use(errorHandler)
33
- const srv = app.listen()
34
- const port = (srv.address() as AddressInfo).port
35
- const promise = axios.get(`http://localhost:${port}/oops`)
36
- await expect(promise).rejects.toThrow('failed with status code 500')
37
- srv.close()
28
+ .get('/oops', () => {
29
+ throw new Error('Oops!')
30
+ })
31
+ .use(errorHandler)
32
+
33
+ const { origin, stop } = await startServer(app)
38
34
  try {
39
- await promise
40
- } catch (err: unknown) {
41
- const axiosError = err as AxiosError
42
- expect(axiosError.response?.status).toEqual(500)
43
- expect(axiosError.response?.data).toEqual({
35
+ const response = await fetch(new URL(`/oops`, origin))
36
+ expect(response.status).toEqual(500)
37
+ await expect(response.json()).resolves.toEqual({
44
38
  error: 'InternalServerError',
45
39
  message: 'Internal Server Error',
46
40
  })
41
+ } finally {
42
+ await stop()
47
43
  }
48
44
  })
49
45
 
50
46
  it('healthcheck succeeds when database is available.', async () => {
51
- const { data, status } = await axios.get(`${ozone.url}/xrpc/_health`)
52
- expect(status).toEqual(200)
53
- expect(data).toEqual({ version: '0.0.0' })
47
+ const response = await fetch(`${network.bsky.url}/xrpc/_health`)
48
+ expect(response.status).toEqual(200)
49
+ await expect(response.json()).resolves.toEqual({ version: 'unknown' })
54
50
  })
55
51
 
56
52
  it('healthcheck fails when database is unavailable.', async () => {
57
53
  // destroy sequencer to release connection that would prevent the db from closing
58
54
  await ozone.ctx.sequencer.destroy()
59
55
  await ozone.ctx.db.close()
60
- let error: AxiosError
61
- try {
62
- await axios.get(`${ozone.url}/xrpc/_health`)
63
- throw new Error('Healthcheck should have failed')
64
- } catch (err) {
65
- if (axios.isAxiosError(err)) {
66
- error = err
67
- } else {
68
- throw err
69
- }
70
- }
71
- expect(error.response?.status).toEqual(503)
72
- expect(error.response?.data).toEqual({
56
+
57
+ const res = await fetch(`${ozone.url}/xrpc/_health`)
58
+
59
+ expect(res.status).toEqual(503)
60
+ await expect(res.json()).resolves.toEqual({
73
61
  version: '0.0.0',
74
62
  error: 'Service Unavailable',
75
63
  })
@@ -0,0 +1,64 @@
1
+ import {
2
+ TestNetwork,
3
+ TestOzone,
4
+ SeedClient,
5
+ basicSeed,
6
+ ModeratorClient,
7
+ } from '@atproto/dev-env'
8
+ import { AtpAgent } from '@atproto/api'
9
+
10
+ describe('moderation', () => {
11
+ let network: TestNetwork
12
+ let ozone: TestOzone
13
+ let agent: AtpAgent
14
+ let bskyAgent: AtpAgent
15
+ let pdsAgent: AtpAgent
16
+ let sc: SeedClient
17
+ let modClient: ModeratorClient
18
+
19
+ const repoSubject = (did: string) => ({
20
+ $type: 'com.atproto.admin.defs#repoRef',
21
+ did,
22
+ })
23
+
24
+ beforeAll(async () => {
25
+ network = await TestNetwork.create({
26
+ dbPostgresSchema: 'ozone_takedown',
27
+ })
28
+ ozone = network.ozone
29
+ agent = network.ozone.getClient()
30
+ bskyAgent = network.bsky.getClient()
31
+ pdsAgent = network.pds.getClient()
32
+ sc = network.getSeedClient()
33
+ modClient = network.ozone.getModClient()
34
+ await basicSeed(sc)
35
+ await network.processAll()
36
+ })
37
+
38
+ afterAll(async () => {
39
+ await network.close()
40
+ })
41
+
42
+ it('allows specifying policy for takedown actions.', async () => {
43
+ await modClient.performTakedown({
44
+ subject: repoSubject(sc.dids.bob),
45
+ policies: ['trolling'],
46
+ })
47
+
48
+ // Verify that that the takedown even exposes the policy specified for it
49
+ const { events } = await modClient.queryEvents({
50
+ subject: sc.dids.bob,
51
+ types: ['tools.ozone.moderation.defs#modEventTakedown'],
52
+ })
53
+
54
+ expect(events[0].event.policies?.[0]).toEqual('trolling')
55
+
56
+ // Verify that event stream can be filtered by policy
57
+ const { events: filteredEvents } = await modClient.queryEvents({
58
+ subject: sc.dids.bob,
59
+ policies: ['trolling'],
60
+ })
61
+
62
+ expect(filteredEvents[0].subject.did).toEqual(sc.dids.bob)
63
+ })
64
+ })
@@ -1 +1 @@
1
- {"root":["./tests/3p-labeler.test.ts","./tests/_util.ts","./tests/ack-all-subjects-of-account.test.ts","./tests/blob-divert.test.ts","./tests/communication-templates.test.ts","./tests/content-tagger.test.ts","./tests/db.test.ts","./tests/get-config.test.ts","./tests/get-lists.test.ts","./tests/get-profiles.test.ts","./tests/get-record.test.ts","./tests/get-records.test.ts","./tests/get-repo.test.ts","./tests/get-repos.test.ts","./tests/get-starter-pack.test.ts","./tests/moderation-appeals.test.ts","./tests/moderation-events.test.ts","./tests/moderation-status-tags.test.ts","./tests/moderation-statuses.test.ts","./tests/moderation.test.ts","./tests/protected-tags.test.ts","./tests/query-labels.test.ts","./tests/record-and-account-events.test.ts","./tests/repo-search.test.ts","./tests/report-muting.test.ts","./tests/sequencer.test.ts","./tests/server.test.ts","./tests/sets.test.ts","./tests/settings.test.ts","./tests/team.test.ts"],"version":"5.6.3"}
1
+ {"root":["./tests/3p-labeler.test.ts","./tests/_util.ts","./tests/ack-all-subjects-of-account.test.ts","./tests/blob-divert.test.ts","./tests/communication-templates.test.ts","./tests/content-tagger.test.ts","./tests/db.test.ts","./tests/get-config.test.ts","./tests/get-lists.test.ts","./tests/get-profiles.test.ts","./tests/get-record.test.ts","./tests/get-records.test.ts","./tests/get-repo.test.ts","./tests/get-repos.test.ts","./tests/get-starter-pack.test.ts","./tests/moderation-appeals.test.ts","./tests/moderation-events.test.ts","./tests/moderation-status-tags.test.ts","./tests/moderation-statuses.test.ts","./tests/moderation.test.ts","./tests/protected-tags.test.ts","./tests/query-labels.test.ts","./tests/record-and-account-events.test.ts","./tests/repo-search.test.ts","./tests/report-muting.test.ts","./tests/sequencer.test.ts","./tests/server.test.ts","./tests/sets.test.ts","./tests/settings.test.ts","./tests/takedown.test.ts","./tests/team.test.ts"],"version":"5.6.3"}