@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.
Files changed (157) hide show
  1. package/dist/api/util.d.ts +10 -0
  2. package/dist/auth-verifier.d.ts +8 -12
  3. package/dist/communication-service/template.d.ts +2 -2
  4. package/dist/config/config.d.ts +6 -0
  5. package/dist/config/env.d.ts +3 -2
  6. package/dist/config/secrets.d.ts +0 -2
  7. package/dist/context.d.ts +6 -0
  8. package/dist/daemon/blob-diverter.d.ts +26 -0
  9. package/dist/daemon/event-pusher.d.ts +6 -0
  10. package/dist/daemon/index.d.ts +1 -0
  11. package/dist/db/index.js +21 -1
  12. package/dist/db/index.js.map +3 -3
  13. package/dist/db/migrations/20240228T003647759Z-add-label-sigs.d.ts +3 -0
  14. package/dist/db/migrations/index.d.ts +1 -0
  15. package/dist/db/schema/index.d.ts +2 -1
  16. package/dist/db/schema/label.d.ts +4 -0
  17. package/dist/db/schema/moderation_event.d.ts +1 -1
  18. package/dist/db/schema/moderation_subject_status.d.ts +2 -2
  19. package/dist/db/schema/signing_key.d.ts +9 -0
  20. package/dist/index.js +10400 -10313
  21. package/dist/index.js.map +3 -3
  22. package/dist/lexicon/index.d.ts +55 -27
  23. package/dist/lexicon/lexicons.d.ts +5046 -4757
  24. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +23 -1
  25. package/dist/lexicon/types/app/bsky/embed/record.d.ts +2 -1
  26. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +1 -0
  27. package/dist/lexicon/types/app/bsky/graph/defs.d.ts +3 -0
  28. package/dist/lexicon/types/app/bsky/labeler/defs.d.ts +41 -0
  29. package/dist/lexicon/types/app/bsky/labeler/getServices.d.ts +36 -0
  30. package/dist/lexicon/types/app/bsky/labeler/service.d.ts +14 -0
  31. package/dist/lexicon/types/com/atproto/admin/defs.d.ts +0 -304
  32. package/dist/lexicon/types/com/atproto/label/defs.d.ts +23 -0
  33. package/dist/lexicon/types/{com/atproto/admin/createCommunicationTemplate.d.ts → tools/ozone/communication/createTemplate.d.ts} +2 -2
  34. package/dist/lexicon/types/tools/ozone/communication/defs.d.ts +14 -0
  35. package/dist/lexicon/types/{com/atproto/admin/listCommunicationTemplates.d.ts → tools/ozone/communication/listTemplates.d.ts} +2 -2
  36. package/dist/lexicon/types/{com/atproto/admin/updateCommunicationTemplate.d.ts → tools/ozone/communication/updateTemplate.d.ts} +2 -2
  37. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +269 -0
  38. package/dist/lexicon/types/{com/atproto/admin/emitModerationEvent.d.ts → tools/ozone/moderation/emitEvent.d.ts} +5 -4
  39. package/dist/lexicon/types/{com/atproto/admin/getModerationEvent.d.ts → tools/ozone/moderation/getEvent.d.ts} +2 -2
  40. package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRecord.d.ts +2 -2
  41. package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRepo.d.ts +2 -2
  42. package/dist/lexicon/types/{com/atproto/admin/queryModerationEvents.d.ts → tools/ozone/moderation/queryEvents.d.ts} +2 -2
  43. package/dist/lexicon/types/{com/atproto/admin/queryModerationStatuses.d.ts → tools/ozone/moderation/queryStatuses.d.ts} +2 -2
  44. package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/searchRepos.d.ts +2 -2
  45. package/dist/mod-service/index.d.ts +16 -15
  46. package/dist/mod-service/subject.d.ts +1 -1
  47. package/dist/mod-service/types.d.ts +2 -2
  48. package/dist/mod-service/util.d.ts +6 -0
  49. package/dist/mod-service/views.d.ts +9 -3
  50. package/dist/sequencer/sequencer.d.ts +6 -4
  51. package/dist/util.d.ts +2 -0
  52. package/package.json +9 -8
  53. package/src/api/{admin/createCommunicationTemplate.ts → communication/createTemplate.ts} +2 -2
  54. package/src/api/{admin/deleteCommunicationTemplate.ts → communication/deleteTemplate.ts} +2 -2
  55. package/src/api/{admin/listCommunicationTemplates.ts → communication/listTemplates.ts} +2 -2
  56. package/src/api/{admin/updateCommunicationTemplate.ts → communication/updateTemplate.ts} +2 -2
  57. package/src/api/index.ts +21 -21
  58. package/src/api/{temp → label}/fetchLabels.ts +5 -3
  59. package/src/api/label/queryLabels.ts +4 -2
  60. package/src/api/moderation/emitEvent.ts +218 -0
  61. package/src/api/{admin/getModerationEvent.ts → moderation/getEvent.ts} +2 -2
  62. package/src/api/{admin → moderation}/getRecord.ts +3 -3
  63. package/src/api/{admin → moderation}/getRepo.ts +3 -3
  64. package/src/api/{admin/queryModerationEvents.ts → moderation/queryEvents.ts} +3 -3
  65. package/src/api/{admin/queryModerationStatuses.ts → moderation/queryStatuses.ts} +3 -3
  66. package/src/api/{admin → moderation}/searchRepos.ts +2 -2
  67. package/src/api/proxied.ts +8 -8
  68. package/src/api/{moderation → report}/createReport.ts +2 -3
  69. package/src/api/util.ts +119 -0
  70. package/src/auth-verifier.ts +20 -30
  71. package/src/communication-service/template.ts +2 -2
  72. package/src/config/config.ts +24 -7
  73. package/src/config/env.ts +6 -4
  74. package/src/config/secrets.ts +0 -6
  75. package/src/context.ts +36 -12
  76. package/src/daemon/blob-diverter.ts +150 -0
  77. package/src/daemon/context.ts +11 -7
  78. package/src/daemon/event-pusher.ts +58 -15
  79. package/src/daemon/index.ts +1 -0
  80. package/src/db/migrations/20240228T003647759Z-add-label-sigs.ts +25 -0
  81. package/src/db/migrations/index.ts +1 -0
  82. package/src/db/schema/index.ts +2 -0
  83. package/src/db/schema/label.ts +3 -0
  84. package/src/db/schema/moderation_event.ts +11 -11
  85. package/src/db/schema/moderation_subject_status.ts +7 -2
  86. package/src/db/schema/signing_key.ts +10 -0
  87. package/src/lexicon/index.ts +200 -137
  88. package/src/lexicon/lexicons.ts +6310 -6012
  89. package/src/lexicon/types/app/bsky/actor/defs.ts +57 -1
  90. package/src/lexicon/types/app/bsky/embed/record.ts +2 -0
  91. package/src/lexicon/types/app/bsky/feed/defs.ts +1 -0
  92. package/src/lexicon/types/app/bsky/graph/defs.ts +3 -0
  93. package/src/lexicon/types/app/bsky/labeler/defs.ts +93 -0
  94. package/src/lexicon/types/app/bsky/labeler/getServices.ts +51 -0
  95. package/src/lexicon/types/app/bsky/labeler/service.ts +31 -0
  96. package/src/lexicon/types/com/atproto/admin/defs.ts +0 -694
  97. package/src/lexicon/types/com/atproto/label/defs.ts +78 -0
  98. package/src/lexicon/types/{com/atproto/admin/createCommunicationTemplate.ts → tools/ozone/communication/createTemplate.ts} +2 -2
  99. package/src/lexicon/types/tools/ozone/communication/defs.ts +35 -0
  100. package/src/lexicon/types/{com/atproto/admin/listCommunicationTemplates.ts → tools/ozone/communication/listTemplates.ts} +2 -2
  101. package/src/lexicon/types/{com/atproto/admin/updateCommunicationTemplate.ts → tools/ozone/communication/updateTemplate.ts} +2 -2
  102. package/src/lexicon/types/tools/ozone/moderation/defs.ts +641 -0
  103. package/src/lexicon/types/{com/atproto/admin/emitModerationEvent.ts → tools/ozone/moderation/emitEvent.ts} +15 -14
  104. package/src/lexicon/types/{com/atproto/admin/getModerationEvent.ts → tools/ozone/moderation/getEvent.ts} +2 -2
  105. package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRecord.ts +2 -2
  106. package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRepo.ts +2 -2
  107. package/src/lexicon/types/{com/atproto/admin/queryModerationEvents.ts → tools/ozone/moderation/queryEvents.ts} +3 -3
  108. package/src/lexicon/types/{com/atproto/admin/queryModerationStatuses.ts → tools/ozone/moderation/queryStatuses.ts} +2 -2
  109. package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/searchRepos.ts +2 -2
  110. package/src/mod-service/index.ts +46 -50
  111. package/src/mod-service/lang.ts +1 -1
  112. package/src/mod-service/status.ts +60 -41
  113. package/src/mod-service/subject.ts +1 -1
  114. package/src/mod-service/types.ts +10 -10
  115. package/src/mod-service/util.ts +49 -5
  116. package/src/mod-service/views.ts +45 -18
  117. package/src/sequencer/sequencer.ts +12 -11
  118. package/src/util.ts +21 -0
  119. package/tests/__snapshots__/blob-divert.test.ts.snap +22 -0
  120. package/tests/__snapshots__/get-record.test.ts.snap +14 -6
  121. package/tests/__snapshots__/get-repo.test.ts.snap +7 -3
  122. package/tests/__snapshots__/moderation-events.test.ts.snap +8 -8
  123. package/tests/__snapshots__/moderation-statuses.test.ts.snap +6 -6
  124. package/tests/_util.ts +5 -0
  125. package/tests/blob-divert.test.ts +87 -0
  126. package/tests/communication-templates.test.ts +33 -37
  127. package/tests/db.test.ts +6 -6
  128. package/tests/get-record.test.ts +22 -12
  129. package/tests/get-repo.test.ts +33 -21
  130. package/tests/moderation-appeals.test.ts +39 -67
  131. package/tests/moderation-events.test.ts +99 -142
  132. package/tests/moderation-status-tags.test.ts +20 -37
  133. package/tests/moderation-statuses.test.ts +132 -65
  134. package/tests/moderation.test.ts +147 -301
  135. package/tests/query-labels.test.ts +86 -10
  136. package/tests/repo-search.test.ts +18 -11
  137. package/tests/sequencer.test.ts +6 -3
  138. package/dist/api/admin/util.d.ts +0 -5
  139. package/dist/api/moderation/util.d.ts +0 -4
  140. package/src/api/admin/emitModerationEvent.ts +0 -170
  141. package/src/api/admin/util.ts +0 -54
  142. package/src/api/moderation/util.ts +0 -67
  143. /package/dist/api/{admin/createCommunicationTemplate.d.ts → communication/createTemplate.d.ts} +0 -0
  144. /package/dist/api/{admin/deleteCommunicationTemplate.d.ts → communication/deleteTemplate.d.ts} +0 -0
  145. /package/dist/api/{admin/emitModerationEvent.d.ts → communication/listTemplates.d.ts} +0 -0
  146. /package/dist/api/{admin/getModerationEvent.d.ts → communication/updateTemplate.d.ts} +0 -0
  147. /package/dist/api/{temp → label}/fetchLabels.d.ts +0 -0
  148. /package/dist/api/{admin/getRecord.d.ts → moderation/emitEvent.d.ts} +0 -0
  149. /package/dist/api/{admin/getRepo.d.ts → moderation/getEvent.d.ts} +0 -0
  150. /package/dist/api/{admin/listCommunicationTemplates.d.ts → moderation/getRecord.d.ts} +0 -0
  151. /package/dist/api/{admin/queryModerationEvents.d.ts → moderation/getRepo.d.ts} +0 -0
  152. /package/dist/api/{admin/queryModerationStatuses.d.ts → moderation/queryEvents.d.ts} +0 -0
  153. /package/dist/api/{admin/searchRepos.d.ts → moderation/queryStatuses.d.ts} +0 -0
  154. /package/dist/api/{admin/updateCommunicationTemplate.d.ts → moderation/searchRepos.d.ts} +0 -0
  155. /package/dist/api/{moderation → report}/createReport.d.ts +0 -0
  156. /package/dist/lexicon/types/{com/atproto/admin/deleteCommunicationTemplate.d.ts → tools/ozone/communication/deleteTemplate.d.ts} +0 -0
  157. /package/src/lexicon/types/{com/atproto/admin/deleteCommunicationTemplate.ts → tools/ozone/communication/deleteTemplate.ts} +0 -0
@@ -3,8 +3,8 @@ 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.createCommunicationTemplate({
7
- auth: ctx.authVerifier.modOrRole,
6
+ server.tools.ozone.communication.createTemplate({
7
+ auth: ctx.authVerifier.modOrAdminToken,
8
8
  handler: async ({ input, auth }) => {
9
9
  const access = auth.credentials
10
10
  const db = ctx.db
@@ -3,8 +3,8 @@ 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({
7
- auth: ctx.authVerifier.modOrRole,
6
+ server.tools.ozone.communication.deleteTemplate({
7
+ auth: ctx.authVerifier.modOrAdminToken,
8
8
  handler: async ({ input, auth }) => {
9
9
  const access = auth.credentials
10
10
  const db = ctx.db
@@ -3,8 +3,8 @@ 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({
7
- auth: ctx.authVerifier.modOrRole,
6
+ server.tools.ozone.communication.listTemplates({
7
+ auth: ctx.authVerifier.modOrAdminToken,
8
8
  handler: async ({ auth }) => {
9
9
  const access = auth.credentials
10
10
  const db = ctx.db
@@ -3,8 +3,8 @@ 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({
7
- auth: ctx.authVerifier.modOrRole,
6
+ server.tools.ozone.communication.updateTemplate({
7
+ auth: ctx.authVerifier.modOrAdminToken,
8
8
  handler: async ({ input, auth }) => {
9
9
  const access = auth.credentials
10
10
  const db = ctx.db
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,
@@ -8,7 +7,7 @@ import {
8
7
 
9
8
  export default function (server: Server, ctx: AppContext) {
10
9
  server.com.atproto.temp.fetchLabels({
11
- auth: ctx.authVerifier.standardOptionalOrRole,
10
+ auth: ctx.authVerifier.standardOptionalOrAdminToken,
12
11
  handler: async ({ auth, params }) => {
13
12
  const { limit } = params
14
13
  const since =
@@ -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,8 +2,8 @@ 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({
6
- auth: ctx.authVerifier.modOrRole,
5
+ server.tools.ozone.moderation.getEvent({
6
+ auth: ctx.authVerifier.modOrAdminToken,
7
7
  handler: async ({ params }) => {
8
8
  const { id } = params
9
9
  const db = ctx.db
@@ -1,12 +1,12 @@
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({
9
- auth: ctx.authVerifier.modOrRole,
8
+ server.tools.ozone.moderation.getRecord({
9
+ auth: ctx.authVerifier.modOrAdminToken,
10
10
  handler: async ({ params, auth }) => {
11
11
  const db = ctx.db
12
12
 
@@ -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 { 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({
8
- auth: ctx.authVerifier.modOrRole,
7
+ server.tools.ozone.moderation.getRepo({
8
+ auth: ctx.authVerifier.modOrAdminToken,
9
9
  handler: async ({ params, auth }) => {
10
10
  const { did } = params
11
11
  const db = ctx.db
@@ -1,10 +1,10 @@
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({
7
- auth: ctx.authVerifier.modOrRole,
6
+ server.tools.ozone.moderation.queryEvents({
7
+ auth: ctx.authVerifier.modOrAdminToken,
8
8
  handler: async ({ params }) => {
9
9
  const {
10
10
  subject,
@@ -1,10 +1,10 @@
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({
7
- auth: ctx.authVerifier.modOrRole,
6
+ server.tools.ozone.moderation.queryStatuses({
7
+ auth: ctx.authVerifier.modOrAdminToken,
8
8
  handler: async ({ params }) => {
9
9
  const {
10
10
  subject,
@@ -3,8 +3,8 @@ 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({
7
- auth: ctx.authVerifier.modOrRole,
6
+ server.tools.ozone.moderation.searchRepos({
7
+ auth: ctx.authVerifier.modOrAdminToken,
8
8
  handler: async ({ params }) => {
9
9
  const modService = ctx.modService(ctx.db)
10
10
 
@@ -3,7 +3,7 @@ import AppContext from '../context'
3
3
 
4
4
  export default function (server: Server, ctx: AppContext) {
5
5
  server.app.bsky.actor.getProfile({
6
- auth: ctx.authVerifier.modOrRole,
6
+ auth: ctx.authVerifier.moderator,
7
7
  handler: async (request) => {
8
8
  const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile(
9
9
  request.params,
@@ -17,7 +17,7 @@ export default function (server: Server, ctx: AppContext) {
17
17
  })
18
18
 
19
19
  server.app.bsky.actor.getProfiles({
20
- auth: ctx.authVerifier.modOrRole,
20
+ auth: ctx.authVerifier.moderator,
21
21
  handler: async (request) => {
22
22
  const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles(
23
23
  request.params,
@@ -31,7 +31,7 @@ export default function (server: Server, ctx: AppContext) {
31
31
  })
32
32
 
33
33
  server.app.bsky.feed.getAuthorFeed({
34
- auth: ctx.authVerifier.modOrRole,
34
+ auth: ctx.authVerifier.moderator,
35
35
  handler: async (request) => {
36
36
  const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed(
37
37
  request.params,
@@ -45,7 +45,7 @@ export default function (server: Server, ctx: AppContext) {
45
45
  })
46
46
 
47
47
  server.app.bsky.feed.getPostThread({
48
- auth: ctx.authVerifier.modOrRole,
48
+ auth: ctx.authVerifier.moderator,
49
49
  handler: async (request) => {
50
50
  const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread(
51
51
  request.params,
@@ -59,7 +59,7 @@ export default function (server: Server, ctx: AppContext) {
59
59
  })
60
60
 
61
61
  server.app.bsky.feed.getFeedGenerator({
62
- auth: ctx.authVerifier.modOrRole,
62
+ auth: ctx.authVerifier.moderator,
63
63
  handler: async (request) => {
64
64
  const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator(
65
65
  request.params,
@@ -73,7 +73,7 @@ export default function (server: Server, ctx: AppContext) {
73
73
  })
74
74
 
75
75
  server.app.bsky.graph.getFollows({
76
- auth: ctx.authVerifier.modOrRole,
76
+ auth: ctx.authVerifier.moderator,
77
77
  handler: async (request) => {
78
78
  const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows(
79
79
  request.params,
@@ -87,7 +87,7 @@ export default function (server: Server, ctx: AppContext) {
87
87
  })
88
88
 
89
89
  server.app.bsky.graph.getFollowers({
90
- auth: ctx.authVerifier.modOrRole,
90
+ auth: ctx.authVerifier.moderator,
91
91
  handler: async (request) => {
92
92
  const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers(
93
93
  request.params,
@@ -101,7 +101,7 @@ export default function (server: Server, ctx: AppContext) {
101
101
  })
102
102
 
103
103
  server.app.bsky.graph.getList({
104
- auth: ctx.authVerifier.modOrRole,
104
+ auth: ctx.authVerifier.moderator,
105
105
  handler: async (request) => {
106
106
  const res = await ctx.appviewAgent.api.app.bsky.graph.getList(
107
107
  request.params,
@@ -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'
@@ -8,8 +8,7 @@ import { ModerationLangService } from '../../mod-service/lang'
8
8
 
9
9
  export default function (server: Server, ctx: AppContext) {
10
10
  server.com.atproto.moderation.createReport({
11
- // @TODO anonymous reports w/ optional auth are a temporary measure
12
- auth: ctx.authVerifier.standardOptionalOrRole,
11
+ auth: ctx.authVerifier.standard,
13
12
  handler: async ({ input, auth }) => {
14
13
  const requester =
15
14
  'iss' in auth.credentials ? auth.credentials.iss : ctx.cfg.service.did
@@ -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
+ ])