@atproto/ozone 0.0.17-next.0 → 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.
Files changed (146) hide show
  1. package/dist/api/util.d.ts +10 -0
  2. package/dist/auth-verifier.d.ts +2 -2
  3. package/dist/communication-service/template.d.ts +2 -2
  4. package/dist/config/config.d.ts +5 -0
  5. package/dist/config/env.d.ts +2 -0
  6. package/dist/context.d.ts +6 -0
  7. package/dist/daemon/blob-diverter.d.ts +26 -0
  8. package/dist/daemon/event-pusher.d.ts +4 -0
  9. package/dist/daemon/index.d.ts +1 -0
  10. package/dist/db/index.js +21 -1
  11. package/dist/db/index.js.map +3 -3
  12. package/dist/db/migrations/20240228T003647759Z-add-label-sigs.d.ts +3 -0
  13. package/dist/db/migrations/index.d.ts +1 -0
  14. package/dist/db/schema/index.d.ts +2 -1
  15. package/dist/db/schema/label.d.ts +4 -0
  16. package/dist/db/schema/moderation_event.d.ts +1 -1
  17. package/dist/db/schema/moderation_subject_status.d.ts +1 -1
  18. package/dist/db/schema/signing_key.d.ts +9 -0
  19. package/dist/index.js +10949 -10628
  20. package/dist/index.js.map +3 -3
  21. package/dist/lexicon/index.d.ts +48 -28
  22. package/dist/lexicon/lexicons.d.ts +4641 -4650
  23. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +7 -7
  24. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +1 -0
  25. package/dist/lexicon/types/app/bsky/graph/defs.d.ts +3 -0
  26. package/dist/lexicon/types/com/atproto/admin/defs.d.ts +0 -305
  27. package/dist/lexicon/types/com/atproto/label/defs.d.ts +5 -0
  28. package/dist/lexicon/types/{com/atproto/admin/createCommunicationTemplate.d.ts → tools/ozone/communication/createTemplate.d.ts} +2 -2
  29. package/dist/lexicon/types/tools/ozone/communication/defs.d.ts +14 -0
  30. package/dist/lexicon/types/{com/atproto/admin/listCommunicationTemplates.d.ts → tools/ozone/communication/listTemplates.d.ts} +2 -2
  31. package/dist/lexicon/types/{com/atproto/admin/updateCommunicationTemplate.d.ts → tools/ozone/communication/updateTemplate.d.ts} +2 -2
  32. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +269 -0
  33. package/dist/lexicon/types/{com/atproto/admin/emitModerationEvent.d.ts → tools/ozone/moderation/emitEvent.d.ts} +5 -4
  34. package/dist/lexicon/types/{com/atproto/admin/getModerationEvent.d.ts → tools/ozone/moderation/getEvent.d.ts} +2 -2
  35. package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRecord.d.ts +2 -2
  36. package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRepo.d.ts +2 -2
  37. package/dist/lexicon/types/{com/atproto/admin/queryModerationEvents.d.ts → tools/ozone/moderation/queryEvents.d.ts} +2 -2
  38. package/dist/lexicon/types/{com/atproto/admin/queryModerationStatuses.d.ts → tools/ozone/moderation/queryStatuses.d.ts} +2 -2
  39. package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/searchRepos.d.ts +2 -2
  40. package/dist/mod-service/index.d.ts +16 -15
  41. package/dist/mod-service/subject.d.ts +1 -1
  42. package/dist/mod-service/types.d.ts +2 -2
  43. package/dist/mod-service/util.d.ts +6 -0
  44. package/dist/mod-service/views.d.ts +9 -3
  45. package/dist/sequencer/sequencer.d.ts +6 -4
  46. package/dist/util.d.ts +2 -0
  47. package/package.json +7 -7
  48. package/src/api/{admin/createCommunicationTemplate.ts → communication/createTemplate.ts} +1 -1
  49. package/src/api/{admin/deleteCommunicationTemplate.ts → communication/deleteTemplate.ts} +1 -1
  50. package/src/api/{admin/listCommunicationTemplates.ts → communication/listTemplates.ts} +1 -1
  51. package/src/api/{admin/updateCommunicationTemplate.ts → communication/updateTemplate.ts} +1 -1
  52. package/src/api/index.ts +21 -21
  53. package/src/api/{temp → label}/fetchLabels.ts +4 -2
  54. package/src/api/label/queryLabels.ts +4 -2
  55. package/src/api/moderation/emitEvent.ts +218 -0
  56. package/src/api/{admin/getModerationEvent.ts → moderation/getEvent.ts} +1 -1
  57. package/src/api/{admin → moderation}/getRecord.ts +2 -2
  58. package/src/api/{admin → moderation}/getRepo.ts +2 -2
  59. package/src/api/{admin/queryModerationEvents.ts → moderation/queryEvents.ts} +2 -2
  60. package/src/api/{admin/queryModerationStatuses.ts → moderation/queryStatuses.ts} +2 -2
  61. package/src/api/{admin → moderation}/searchRepos.ts +1 -1
  62. package/src/api/{moderation → report}/createReport.ts +1 -1
  63. package/src/api/util.ts +119 -0
  64. package/src/auth-verifier.ts +2 -2
  65. package/src/communication-service/template.ts +2 -2
  66. package/src/config/config.ts +14 -0
  67. package/src/config/env.ts +4 -0
  68. package/src/context.ts +35 -9
  69. package/src/daemon/blob-diverter.ts +150 -0
  70. package/src/daemon/context.ts +9 -5
  71. package/src/daemon/event-pusher.ts +49 -14
  72. package/src/daemon/index.ts +1 -0
  73. package/src/db/migrations/20240228T003647759Z-add-label-sigs.ts +25 -0
  74. package/src/db/migrations/index.ts +1 -0
  75. package/src/db/schema/index.ts +2 -0
  76. package/src/db/schema/label.ts +3 -0
  77. package/src/db/schema/moderation_event.ts +11 -11
  78. package/src/db/schema/moderation_subject_status.ts +1 -1
  79. package/src/db/schema/signing_key.ts +10 -0
  80. package/src/lexicon/index.ts +178 -138
  81. package/src/lexicon/lexicons.ts +6078 -6106
  82. package/src/lexicon/types/app/bsky/actor/defs.ts +11 -11
  83. package/src/lexicon/types/app/bsky/feed/defs.ts +1 -0
  84. package/src/lexicon/types/app/bsky/graph/defs.ts +3 -0
  85. package/src/lexicon/types/com/atproto/admin/defs.ts +0 -697
  86. package/src/lexicon/types/com/atproto/label/defs.ts +10 -0
  87. package/src/lexicon/types/{com/atproto/admin/createCommunicationTemplate.ts → tools/ozone/communication/createTemplate.ts} +2 -2
  88. package/src/lexicon/types/tools/ozone/communication/defs.ts +35 -0
  89. package/src/lexicon/types/{com/atproto/admin/listCommunicationTemplates.ts → tools/ozone/communication/listTemplates.ts} +2 -2
  90. package/src/lexicon/types/{com/atproto/admin/updateCommunicationTemplate.ts → tools/ozone/communication/updateTemplate.ts} +2 -2
  91. package/src/lexicon/types/tools/ozone/moderation/defs.ts +641 -0
  92. package/src/lexicon/types/{com/atproto/admin/emitModerationEvent.ts → tools/ozone/moderation/emitEvent.ts} +15 -14
  93. package/src/lexicon/types/{com/atproto/admin/getModerationEvent.ts → tools/ozone/moderation/getEvent.ts} +2 -2
  94. package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRecord.ts +2 -2
  95. package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRepo.ts +2 -2
  96. package/src/lexicon/types/{com/atproto/admin/queryModerationEvents.ts → tools/ozone/moderation/queryEvents.ts} +3 -3
  97. package/src/lexicon/types/{com/atproto/admin/queryModerationStatuses.ts → tools/ozone/moderation/queryStatuses.ts} +2 -2
  98. package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/searchRepos.ts +2 -2
  99. package/src/mod-service/index.ts +42 -47
  100. package/src/mod-service/lang.ts +1 -1
  101. package/src/mod-service/status.ts +19 -16
  102. package/src/mod-service/subject.ts +1 -1
  103. package/src/mod-service/types.ts +10 -10
  104. package/src/mod-service/util.ts +49 -5
  105. package/src/mod-service/views.ts +45 -18
  106. package/src/sequencer/sequencer.ts +12 -11
  107. package/src/util.ts +21 -0
  108. package/tests/__snapshots__/blob-divert.test.ts.snap +22 -0
  109. package/tests/__snapshots__/get-record.test.ts.snap +10 -2
  110. package/tests/__snapshots__/get-repo.test.ts.snap +5 -1
  111. package/tests/__snapshots__/moderation-events.test.ts.snap +8 -8
  112. package/tests/__snapshots__/moderation-statuses.test.ts.snap +6 -6
  113. package/tests/_util.ts +5 -0
  114. package/tests/blob-divert.test.ts +87 -0
  115. package/tests/communication-templates.test.ts +30 -34
  116. package/tests/db.test.ts +6 -6
  117. package/tests/get-record.test.ts +6 -6
  118. package/tests/get-repo.test.ts +11 -11
  119. package/tests/moderation-appeals.test.ts +28 -28
  120. package/tests/moderation-events.test.ts +44 -44
  121. package/tests/moderation-status-tags.test.ts +8 -10
  122. package/tests/moderation-statuses.test.ts +27 -27
  123. package/tests/moderation.test.ts +50 -57
  124. package/tests/query-labels.test.ts +86 -10
  125. package/tests/repo-search.test.ts +8 -8
  126. package/tests/sequencer.test.ts +6 -3
  127. package/dist/api/admin/util.d.ts +0 -5
  128. package/dist/api/moderation/util.d.ts +0 -4
  129. package/src/api/admin/emitModerationEvent.ts +0 -174
  130. package/src/api/admin/util.ts +0 -54
  131. package/src/api/moderation/util.ts +0 -67
  132. /package/dist/api/{admin/createCommunicationTemplate.d.ts → communication/createTemplate.d.ts} +0 -0
  133. /package/dist/api/{admin/deleteCommunicationTemplate.d.ts → communication/deleteTemplate.d.ts} +0 -0
  134. /package/dist/api/{admin/emitModerationEvent.d.ts → communication/listTemplates.d.ts} +0 -0
  135. /package/dist/api/{admin/getModerationEvent.d.ts → communication/updateTemplate.d.ts} +0 -0
  136. /package/dist/api/{temp → label}/fetchLabels.d.ts +0 -0
  137. /package/dist/api/{admin/getRecord.d.ts → moderation/emitEvent.d.ts} +0 -0
  138. /package/dist/api/{admin/getRepo.d.ts → moderation/getEvent.d.ts} +0 -0
  139. /package/dist/api/{admin/listCommunicationTemplates.d.ts → moderation/getRecord.d.ts} +0 -0
  140. /package/dist/api/{admin/queryModerationEvents.d.ts → moderation/getRepo.d.ts} +0 -0
  141. /package/dist/api/{admin/queryModerationStatuses.d.ts → moderation/queryEvents.d.ts} +0 -0
  142. /package/dist/api/{admin/searchRepos.d.ts → moderation/queryStatuses.d.ts} +0 -0
  143. /package/dist/api/{admin/updateCommunicationTemplate.d.ts → moderation/searchRepos.d.ts} +0 -0
  144. /package/dist/api/{moderation → report}/createReport.d.ts +0 -0
  145. /package/dist/lexicon/types/{com/atproto/admin/deleteCommunicationTemplate.d.ts → tools/ozone/communication/deleteTemplate.d.ts} +0 -0
  146. /package/src/lexicon/types/{com/atproto/admin/deleteCommunicationTemplate.ts → tools/ozone/communication/deleteTemplate.ts} +0 -0
@@ -3,7 +3,7 @@ import { Server } from '../../lexicon'
3
3
  import AppContext from '../../context'
4
4
 
5
5
  export default function (server: Server, ctx: AppContext) {
6
- server.com.atproto.admin.deleteCommunicationTemplate({
6
+ server.tools.ozone.communication.deleteTemplate({
7
7
  auth: ctx.authVerifier.modOrAdminToken,
8
8
  handler: async ({ input, auth }) => {
9
9
  const access = auth.credentials
@@ -3,7 +3,7 @@ import { Server } from '../../lexicon'
3
3
  import AppContext from '../../context'
4
4
 
5
5
  export default function (server: Server, ctx: AppContext) {
6
- server.com.atproto.admin.listCommunicationTemplates({
6
+ server.tools.ozone.communication.listTemplates({
7
7
  auth: ctx.authVerifier.modOrAdminToken,
8
8
  handler: async ({ auth }) => {
9
9
  const access = auth.credentials
@@ -3,7 +3,7 @@ import { Server } from '../../lexicon'
3
3
  import AppContext from '../../context'
4
4
 
5
5
  export default function (server: Server, ctx: AppContext) {
6
- server.com.atproto.admin.updateCommunicationTemplate({
6
+ server.tools.ozone.communication.updateTemplate({
7
7
  auth: ctx.authVerifier.modOrAdminToken,
8
8
  handler: async ({ input, auth }) => {
9
9
  const access = auth.credentials
package/src/api/index.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  import { Server } from '../lexicon'
2
2
  import AppContext from '../context'
3
- import createReport from './moderation/createReport'
4
- import emitModerationEvent from './admin/emitModerationEvent'
5
- import searchRepos from './admin/searchRepos'
6
- import adminGetRecord from './admin/getRecord'
7
- import getRepo from './admin/getRepo'
8
- import queryModerationStatuses from './admin/queryModerationStatuses'
9
- import queryModerationEvents from './admin/queryModerationEvents'
10
- import getModerationEvent from './admin/getModerationEvent'
3
+ import createReport from './report/createReport'
4
+ import emitEvent from './moderation/emitEvent'
5
+ import searchRepos from './moderation/searchRepos'
6
+ import adminGetRecord from './moderation/getRecord'
7
+ import getRepo from './moderation/getRepo'
8
+ import queryStatuses from './moderation/queryStatuses'
9
+ import queryEvents from './moderation/queryEvents'
10
+ import getEvent from './moderation/getEvent'
11
11
  import queryLabels from './label/queryLabels'
12
12
  import subscribeLabels from './label/subscribeLabels'
13
- import fetchLabels from './temp/fetchLabels'
14
- import createCommunicationTemplate from './admin/createCommunicationTemplate'
15
- import updateCommunicationTemplate from './admin/updateCommunicationTemplate'
16
- import deleteCommunicationTemplate from './admin/deleteCommunicationTemplate'
17
- import listCommunicationTemplates from './admin/listCommunicationTemplates'
13
+ import fetchLabels from './label/fetchLabels'
14
+ import createTemplate from './communication/createTemplate'
15
+ import updateTemplate from './communication/updateTemplate'
16
+ import deleteTemplate from './communication/deleteTemplate'
17
+ import listTemplates from './communication/listTemplates'
18
18
  import proxied from './proxied'
19
19
 
20
20
  export * as health from './health'
@@ -23,20 +23,20 @@ export * as wellKnown from './well-known'
23
23
 
24
24
  export default function (server: Server, ctx: AppContext) {
25
25
  createReport(server, ctx)
26
- emitModerationEvent(server, ctx)
26
+ emitEvent(server, ctx)
27
27
  searchRepos(server, ctx)
28
28
  adminGetRecord(server, ctx)
29
29
  getRepo(server, ctx)
30
- getModerationEvent(server, ctx)
31
- queryModerationEvents(server, ctx)
32
- queryModerationStatuses(server, ctx)
30
+ getEvent(server, ctx)
31
+ queryEvents(server, ctx)
32
+ queryStatuses(server, ctx)
33
33
  queryLabels(server, ctx)
34
34
  subscribeLabels(server, ctx)
35
35
  fetchLabels(server, ctx)
36
- listCommunicationTemplates(server, ctx)
37
- createCommunicationTemplate(server, ctx)
38
- updateCommunicationTemplate(server, ctx)
39
- deleteCommunicationTemplate(server, ctx)
36
+ listTemplates(server, ctx)
37
+ createTemplate(server, ctx)
38
+ updateTemplate(server, ctx)
39
+ deleteTemplate(server, ctx)
40
40
  proxied(server, ctx)
41
41
  return server
42
42
  }
@@ -1,6 +1,5 @@
1
1
  import { Server } from '../../lexicon'
2
2
  import AppContext from '../../context'
3
- import { formatLabel } from '../../mod-service/util'
4
3
  import {
5
4
  UNSPECCED_TAKEDOWN_BLOBS_LABEL,
6
5
  UNSPECCED_TAKEDOWN_LABEL,
@@ -29,7 +28,10 @@ export default function (server: Server, ctx: AppContext) {
29
28
  .limit(limit)
30
29
  .execute()
31
30
 
32
- const labels = labelRes.map((l) => formatLabel(l))
31
+ const modSrvc = ctx.modService(ctx.db)
32
+ const labels = await Promise.all(
33
+ labelRes.map((l) => modSrvc.views.formatLabelAndEnsureSig(l)),
34
+ )
33
35
 
34
36
  return {
35
37
  encoding: 'application/json',
@@ -2,7 +2,6 @@ import { Server } from '../../lexicon'
2
2
  import AppContext from '../../context'
3
3
  import { InvalidRequestError } from '@atproto/xrpc-server'
4
4
  import { sql } from 'kysely'
5
- import { formatLabel } from '../../mod-service/util'
6
5
 
7
6
  export default function (server: Server, ctx: AppContext) {
8
7
  server.com.atproto.label.queryLabels(async ({ params }) => {
@@ -44,7 +43,10 @@ export default function (server: Server, ctx: AppContext) {
44
43
 
45
44
  const res = await builder.execute()
46
45
 
47
- const labels = res.map((l) => formatLabel(l))
46
+ const modSrvc = ctx.modService(ctx.db)
47
+ const labels = await Promise.all(
48
+ res.map((l) => modSrvc.views.formatLabelAndEnsureSig(l)),
49
+ )
48
50
  const resCursor = res.at(-1)?.id.toString(10)
49
51
 
50
52
  return {
@@ -0,0 +1,218 @@
1
+ import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'
2
+ import { Server } from '../../lexicon'
3
+ import AppContext from '../../context'
4
+ import {
5
+ isModEventDivert,
6
+ isModEventEmail,
7
+ isModEventLabel,
8
+ isModEventReverseTakedown,
9
+ isModEventTakedown,
10
+ } from '../../lexicon/types/tools/ozone/moderation/defs'
11
+ import { HandlerInput } from '../../lexicon/types/tools/ozone/moderation/emitEvent'
12
+ import { subjectFromInput } from '../../mod-service/subject'
13
+ import { ModerationLangService } from '../../mod-service/lang'
14
+ import { retryHttp } from '../../util'
15
+ import { ModeratorOutput, AdminTokenOutput } from '../../auth-verifier'
16
+
17
+ const handleModerationEvent = async ({
18
+ ctx,
19
+ input,
20
+ auth,
21
+ }: {
22
+ ctx: AppContext
23
+ input: HandlerInput
24
+ auth: ModeratorOutput | AdminTokenOutput
25
+ }) => {
26
+ const access = auth.credentials
27
+ const createdBy =
28
+ auth.credentials.type === 'moderator'
29
+ ? auth.credentials.iss
30
+ : input.body.createdBy
31
+ const db = ctx.db
32
+ const moderationService = ctx.modService(db)
33
+ const { event } = input.body
34
+ const isTakedownEvent = isModEventTakedown(event)
35
+ const isReverseTakedownEvent = isModEventReverseTakedown(event)
36
+ const isLabelEvent = isModEventLabel(event)
37
+ const subject = subjectFromInput(
38
+ input.body.subject,
39
+ input.body.subjectBlobCids,
40
+ )
41
+
42
+ // apply access rules
43
+
44
+ // if less than moderator access then can only take ack and escalation actions
45
+ if (isTakedownEvent || isReverseTakedownEvent) {
46
+ if (!access.isModerator) {
47
+ throw new AuthRequiredError(
48
+ 'Must be a full moderator to take this type of action',
49
+ )
50
+ }
51
+
52
+ // Non admins should not be able to take down feed generators
53
+ if (
54
+ !access.isAdmin &&
55
+ subject.recordPath?.includes('app.bsky.feed.generator/')
56
+ ) {
57
+ throw new AuthRequiredError(
58
+ 'Must be a full admin to take this type of action on feed generators',
59
+ )
60
+ }
61
+ }
62
+ // if less than moderator access then can not apply labels
63
+ if (!access.isModerator && isLabelEvent) {
64
+ throw new AuthRequiredError('Must be a full moderator to label content')
65
+ }
66
+
67
+ if (isLabelEvent) {
68
+ validateLabels([
69
+ ...(event.createLabelVals ?? []),
70
+ ...(event.negateLabelVals ?? []),
71
+ ])
72
+ }
73
+
74
+ if (isTakedownEvent || isReverseTakedownEvent) {
75
+ const status = await moderationService.getStatus(subject)
76
+
77
+ if (status?.takendown && isTakedownEvent) {
78
+ throw new InvalidRequestError(`Subject is already taken down`)
79
+ }
80
+
81
+ if (!status?.takendown && isReverseTakedownEvent) {
82
+ throw new InvalidRequestError(`Subject is not taken down`)
83
+ }
84
+
85
+ if (status?.takendown && isReverseTakedownEvent && subject.isRecord()) {
86
+ // due to the way blob status is modeled, we should reverse takedown on all
87
+ // blobs for the record being restored, which aren't taken down on another record.
88
+ subject.blobCids = status.blobCids ?? []
89
+ }
90
+ }
91
+
92
+ if (isModEventEmail(event) && event.content) {
93
+ // sending email prior to logging the event to avoid a long transaction below
94
+ if (!subject.isRepo()) {
95
+ throw new InvalidRequestError('Email can only be sent to a repo subject')
96
+ }
97
+ const { content, subjectLine } = event
98
+ await retryHttp(() =>
99
+ ctx.modService(db).sendEmail({
100
+ subject: subjectLine,
101
+ content,
102
+ recipientDid: subject.did,
103
+ }),
104
+ )
105
+ }
106
+
107
+ if (isModEventDivert(event) && subject.isRecord()) {
108
+ if (!ctx.blobDiverter) {
109
+ throw new InvalidRequestError(
110
+ 'BlobDiverter not configured for this service',
111
+ )
112
+ }
113
+ await ctx.blobDiverter.uploadBlobOnService(subject.info())
114
+ }
115
+
116
+ const moderationEvent = await db.transaction(async (dbTxn) => {
117
+ const moderationTxn = ctx.modService(dbTxn)
118
+
119
+ const result = await moderationTxn.logEvent({
120
+ event,
121
+ subject,
122
+ createdBy,
123
+ })
124
+
125
+ const moderationLangService = new ModerationLangService(moderationTxn)
126
+ await moderationLangService.tagSubjectWithLang({
127
+ subject,
128
+ createdBy: ctx.cfg.service.did,
129
+ subjectStatus: result.subjectStatus,
130
+ })
131
+
132
+ if (subject.isRepo()) {
133
+ if (isTakedownEvent) {
134
+ const isSuspend = !!result.event.durationInHours
135
+ await moderationTxn.takedownRepo(subject, result.event.id, isSuspend)
136
+ } else if (isReverseTakedownEvent) {
137
+ await moderationTxn.reverseTakedownRepo(subject)
138
+ }
139
+ }
140
+
141
+ if (subject.isRecord()) {
142
+ if (isTakedownEvent) {
143
+ await moderationTxn.takedownRecord(subject, result.event.id)
144
+ } else if (isReverseTakedownEvent) {
145
+ await moderationTxn.reverseTakedownRecord(subject)
146
+ }
147
+ }
148
+
149
+ if (isLabelEvent) {
150
+ await moderationTxn.formatAndCreateLabels(
151
+ result.event.subjectUri ?? result.event.subjectDid,
152
+ result.event.subjectCid,
153
+ {
154
+ create: result.event.createLabelVals?.length
155
+ ? result.event.createLabelVals.split(' ')
156
+ : undefined,
157
+ negate: result.event.negateLabelVals?.length
158
+ ? result.event.negateLabelVals.split(' ')
159
+ : undefined,
160
+ },
161
+ )
162
+ }
163
+
164
+ return result.event
165
+ })
166
+
167
+ return moderationService.views.formatEvent(moderationEvent)
168
+ }
169
+
170
+ export default function (server: Server, ctx: AppContext) {
171
+ server.tools.ozone.moderation.emitEvent({
172
+ auth: ctx.authVerifier.modOrAdminToken,
173
+ handler: async ({ input, auth }) => {
174
+ const moderationEvent = await handleModerationEvent({
175
+ input,
176
+ auth,
177
+ ctx,
178
+ })
179
+
180
+ // On divert events, we need to automatically take down the blobs
181
+ if (isModEventDivert(input.body.event)) {
182
+ await handleModerationEvent({
183
+ auth,
184
+ ctx,
185
+ input: {
186
+ ...input,
187
+ body: {
188
+ ...input.body,
189
+ event: {
190
+ ...input.body.event,
191
+ $type: 'tools.ozone.moderation.defs#modEventTakedown',
192
+ comment:
193
+ '[DIVERT_SIDE_EFFECT]: Automatically taking down after divert event',
194
+ },
195
+ },
196
+ },
197
+ })
198
+ }
199
+
200
+ return {
201
+ encoding: 'application/json',
202
+ body: moderationEvent,
203
+ }
204
+ },
205
+ })
206
+ }
207
+
208
+ const validateLabels = (labels: string[]) => {
209
+ for (const label of labels) {
210
+ for (const char of badChars) {
211
+ if (label.includes(char)) {
212
+ throw new InvalidRequestError(`Invalid label: ${label}`)
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ const badChars = [' ', ',', ';', `'`, `"`]
@@ -2,7 +2,7 @@ import { Server } from '../../lexicon'
2
2
  import AppContext from '../../context'
3
3
 
4
4
  export default function (server: Server, ctx: AppContext) {
5
- server.com.atproto.admin.getModerationEvent({
5
+ server.tools.ozone.moderation.getEvent({
6
6
  auth: ctx.authVerifier.modOrAdminToken,
7
7
  handler: async ({ params }) => {
8
8
  const { id } = params
@@ -1,11 +1,11 @@
1
1
  import { InvalidRequestError } from '@atproto/xrpc-server'
2
2
  import { Server } from '../../lexicon'
3
3
  import AppContext from '../../context'
4
- import { addAccountInfoToRepoView, getPdsAccountInfo } from './util'
4
+ import { addAccountInfoToRepoView, getPdsAccountInfo } from '../util'
5
5
  import { AtUri } from '@atproto/syntax'
6
6
 
7
7
  export default function (server: Server, ctx: AppContext) {
8
- server.com.atproto.admin.getRecord({
8
+ server.tools.ozone.moderation.getRecord({
9
9
  auth: ctx.authVerifier.modOrAdminToken,
10
10
  handler: async ({ params, auth }) => {
11
11
  const db = ctx.db
@@ -1,10 +1,10 @@
1
1
  import { InvalidRequestError } from '@atproto/xrpc-server'
2
2
  import { Server } from '../../lexicon'
3
3
  import AppContext from '../../context'
4
- import { addAccountInfoToRepoViewDetail, getPdsAccountInfo } from './util'
4
+ import { addAccountInfoToRepoViewDetail, getPdsAccountInfo } from '../util'
5
5
 
6
6
  export default function (server: Server, ctx: AppContext) {
7
- server.com.atproto.admin.getRepo({
7
+ server.tools.ozone.moderation.getRepo({
8
8
  auth: ctx.authVerifier.modOrAdminToken,
9
9
  handler: async ({ params, auth }) => {
10
10
  const { did } = params
@@ -1,9 +1,9 @@
1
1
  import { Server } from '../../lexicon'
2
2
  import AppContext from '../../context'
3
- import { getEventType } from '../moderation/util'
3
+ import { getEventType } from '../util'
4
4
 
5
5
  export default function (server: Server, ctx: AppContext) {
6
- server.com.atproto.admin.queryModerationEvents({
6
+ server.tools.ozone.moderation.queryEvents({
7
7
  auth: ctx.authVerifier.modOrAdminToken,
8
8
  handler: async ({ params }) => {
9
9
  const {
@@ -1,9 +1,9 @@
1
1
  import { Server } from '../../lexicon'
2
2
  import AppContext from '../../context'
3
- import { getReviewState } from '../moderation/util'
3
+ import { getReviewState } from '../util'
4
4
 
5
5
  export default function (server: Server, ctx: AppContext) {
6
- server.com.atproto.admin.queryModerationStatuses({
6
+ server.tools.ozone.moderation.queryStatuses({
7
7
  auth: ctx.authVerifier.modOrAdminToken,
8
8
  handler: async ({ params }) => {
9
9
  const {
@@ -3,7 +3,7 @@ import AppContext from '../../context'
3
3
  import { mapDefined } from '@atproto/common'
4
4
 
5
5
  export default function (server: Server, ctx: AppContext) {
6
- server.com.atproto.admin.searchRepos({
6
+ server.tools.ozone.moderation.searchRepos({
7
7
  auth: ctx.authVerifier.modOrAdminToken,
8
8
  handler: async ({ params }) => {
9
9
  const modService = ctx.modService(ctx.db)
@@ -1,6 +1,6 @@
1
1
  import { Server } from '../../lexicon'
2
2
  import AppContext from '../../context'
3
- import { getReasonType } from './util'
3
+ import { getReasonType } from '../util'
4
4
  import { subjectFromInput } from '../../mod-service/subject'
5
5
  import { REASONAPPEAL } from '../../lexicon/types/com/atproto/moderation/defs'
6
6
  import { ForbiddenError } from '@atproto/xrpc-server'
@@ -0,0 +1,119 @@
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 { AccountView } from '../lexicon/types/com/atproto/admin/defs'
13
+ import {
14
+ RepoView,
15
+ RepoViewDetail,
16
+ REVIEWCLOSED,
17
+ REVIEWESCALATED,
18
+ REVIEWOPEN,
19
+ } from '../lexicon/types/tools/ozone/moderation/defs'
20
+ import { ModerationEvent } from '../db/schema/moderation_event'
21
+ import { ModerationSubjectStatusRow } from '../mod-service/types'
22
+ import AppContext from '../context'
23
+
24
+ export const getPdsAccountInfo = async (
25
+ ctx: AppContext,
26
+ did: string,
27
+ ): Promise<AccountView | null> => {
28
+ const agent = ctx.pdsAgent
29
+ if (!agent) return null
30
+ const auth = await ctx.pdsAuth()
31
+ if (!auth) return null
32
+ try {
33
+ const res = await agent.api.com.atproto.admin.getAccountInfo({ did }, auth)
34
+ return res.data
35
+ } catch {
36
+ return null
37
+ }
38
+ }
39
+
40
+ export const addAccountInfoToRepoViewDetail = (
41
+ repoView: RepoViewDetail,
42
+ accountInfo: AccountView | null,
43
+ includeEmail = false,
44
+ ): RepoViewDetail => {
45
+ if (!accountInfo) return repoView
46
+ return {
47
+ ...repoView,
48
+ email: includeEmail ? accountInfo.email : undefined,
49
+ invitedBy: accountInfo.invitedBy,
50
+ invitesDisabled: accountInfo.invitesDisabled,
51
+ inviteNote: accountInfo.inviteNote,
52
+ invites: accountInfo.invites,
53
+ emailConfirmedAt: accountInfo.emailConfirmedAt,
54
+ }
55
+ }
56
+
57
+ export const addAccountInfoToRepoView = (
58
+ repoView: RepoView,
59
+ accountInfo: AccountView | null,
60
+ includeEmail = false,
61
+ ): RepoView => {
62
+ if (!accountInfo) return repoView
63
+ return {
64
+ ...repoView,
65
+ email: includeEmail ? accountInfo.email : undefined,
66
+ invitedBy: accountInfo.invitedBy,
67
+ invitesDisabled: accountInfo.invitesDisabled,
68
+ inviteNote: accountInfo.inviteNote,
69
+ }
70
+ }
71
+
72
+ export const getReasonType = (reasonType: ReportInput['reasonType']) => {
73
+ if (reasonTypes.has(reasonType)) {
74
+ return reasonType as NonNullable<ModerationEvent['meta']>['reportType']
75
+ }
76
+ throw new InvalidRequestError('Invalid reason type')
77
+ }
78
+
79
+ export const getEventType = (type: string) => {
80
+ if (eventTypes.has(type)) {
81
+ return type as ModerationEvent['action']
82
+ }
83
+ throw new InvalidRequestError('Invalid event type')
84
+ }
85
+
86
+ export const getReviewState = (reviewState?: string) => {
87
+ if (!reviewState) return undefined
88
+ if (reviewStates.has(reviewState)) {
89
+ return reviewState as ModerationSubjectStatusRow['reviewState']
90
+ }
91
+ throw new InvalidRequestError('Invalid review state')
92
+ }
93
+
94
+ const reviewStates = new Set([REVIEWCLOSED, REVIEWESCALATED, REVIEWOPEN])
95
+
96
+ const reasonTypes = new Set([
97
+ REASONOTHER,
98
+ REASONSPAM,
99
+ REASONMISLEADING,
100
+ REASONRUDE,
101
+ REASONSEXUAL,
102
+ REASONVIOLATION,
103
+ REASONAPPEAL,
104
+ ])
105
+
106
+ const eventTypes = new Set([
107
+ 'tools.ozone.moderation.defs#modEventTakedown',
108
+ 'tools.ozone.moderation.defs#modEventAcknowledge',
109
+ 'tools.ozone.moderation.defs#modEventEscalate',
110
+ 'tools.ozone.moderation.defs#modEventComment',
111
+ 'tools.ozone.moderation.defs#modEventLabel',
112
+ 'tools.ozone.moderation.defs#modEventReport',
113
+ 'tools.ozone.moderation.defs#modEventMute',
114
+ 'tools.ozone.moderation.defs#modEventUnmute',
115
+ 'tools.ozone.moderation.defs#modEventReverseTakedown',
116
+ 'tools.ozone.moderation.defs#modEventEmail',
117
+ 'tools.ozone.moderation.defs#modEventResolveAppeal',
118
+ 'tools.ozone.moderation.defs#modEventTag',
119
+ ])
@@ -7,7 +7,7 @@ type ReqCtx = {
7
7
  req: express.Request
8
8
  }
9
9
 
10
- type AdminTokenOutput = {
10
+ export type AdminTokenOutput = {
11
11
  credentials: {
12
12
  type: 'admin_token'
13
13
  isAdmin: true
@@ -16,7 +16,7 @@ type AdminTokenOutput = {
16
16
  }
17
17
  }
18
18
 
19
- type ModeratorOutput = {
19
+ export type ModeratorOutput = {
20
20
  credentials: {
21
21
  type: 'moderator'
22
22
  aud: string
@@ -1,7 +1,7 @@
1
1
  import Database from '../db'
2
2
  import { Selectable } from 'kysely'
3
3
  import { CommunicationTemplate } from '../db/schema/communication_template'
4
- import { CommunicationTemplateView } from '../lexicon/types/com/atproto/admin/defs'
4
+ import { TemplateView } from '../lexicon/types/tools/ozone/communication/defs'
5
5
 
6
6
  export type CommunicationTemplateServiceCreator = (
7
7
  db: Database,
@@ -90,7 +90,7 @@ export class CommunicationTemplateService {
90
90
  .execute()
91
91
  }
92
92
 
93
- view(template: Selectable<CommunicationTemplate>): CommunicationTemplateView {
93
+ view(template: Selectable<CommunicationTemplate>): TemplateView {
94
94
  return {
95
95
  id: `${template.id}`,
96
96
  name: template.name,
@@ -50,6 +50,13 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => {
50
50
  plcUrl: env.didPlcUrl,
51
51
  }
52
52
 
53
+ const blobDivertServiceCfg =
54
+ env.blobDivertUrl && env.blobDivertAdminPassword
55
+ ? {
56
+ url: env.blobDivertUrl,
57
+ adminPassword: env.blobDivertAdminPassword,
58
+ }
59
+ : null
53
60
  const accessCfg: OzoneConfig['access'] = {
54
61
  admins: env.adminDids,
55
62
  moderators: env.moderatorDids,
@@ -63,6 +70,7 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => {
63
70
  pds: pdsCfg,
64
71
  cdn: cdnCfg,
65
72
  identity: identityCfg,
73
+ blobDivert: blobDivertServiceCfg,
66
74
  access: accessCfg,
67
75
  }
68
76
  }
@@ -74,6 +82,7 @@ export type OzoneConfig = {
74
82
  pds: PdsConfig | null
75
83
  cdn: CdnConfig
76
84
  identity: IdentityConfig
85
+ blobDivert: BlobDivertConfig | null
77
86
  access: AccessConfig
78
87
  }
79
88
 
@@ -85,6 +94,11 @@ export type ServiceConfig = {
85
94
  devMode?: boolean
86
95
  }
87
96
 
97
+ export type BlobDivertConfig = {
98
+ url: string
99
+ adminPassword: string
100
+ }
101
+
88
102
  export type DatabaseConfig = {
89
103
  postgresUrl: string
90
104
  postgresSchema?: string
package/src/config/env.ts CHANGED
@@ -25,6 +25,8 @@ export const readEnv = (): OzoneEnvironment => {
25
25
  triageDids: envList('OZONE_TRIAGE_DIDS'),
26
26
  adminPassword: envStr('OZONE_ADMIN_PASSWORD'),
27
27
  signingKeyHex: envStr('OZONE_SIGNING_KEY_HEX'),
28
+ blobDivertUrl: envStr('OZONE_BLOB_DIVERT_URL'),
29
+ blobDivertAdminPassword: envStr('OZONE_BLOB_DIVERT_ADMIN_PASSWORD'),
28
30
  }
29
31
  }
30
32
 
@@ -52,4 +54,6 @@ export type OzoneEnvironment = {
52
54
  triageDids: string[]
53
55
  adminPassword?: string
54
56
  signingKeyHex?: string
57
+ blobDivertUrl?: string
58
+ blobDivertAdminPassword?: string
55
59
  }