@atproto/bsky 0.0.15 → 0.0.16

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 (170) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/api/com/atproto/moderation/util.d.ts +4 -3
  3. package/dist/context.d.ts +15 -0
  4. package/dist/db/index.js +26 -1
  5. package/dist/db/index.js.map +3 -3
  6. package/dist/db/migrations/20231003T202833377Z-create-moderation-subject-status.d.ts +3 -0
  7. package/dist/db/migrations/index.d.ts +1 -0
  8. package/dist/db/pagination.d.ts +2 -1
  9. package/dist/db/{periodic-moderation-action-reversal.d.ts → periodic-moderation-event-reversal.d.ts} +3 -5
  10. package/dist/db/tables/moderation.d.ts +24 -34
  11. package/dist/feed-gen/types.d.ts +1 -1
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.js +2750 -2121
  14. package/dist/index.js.map +3 -3
  15. package/dist/lexicon/index.d.ts +11 -18
  16. package/dist/lexicon/lexicons.d.ts +414 -399
  17. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +1 -7
  18. package/dist/lexicon/types/app/bsky/graph/defs.d.ts +1 -0
  19. package/dist/lexicon/types/com/atproto/admin/defs.d.ts +114 -48
  20. package/dist/lexicon/types/com/atproto/admin/{takeModerationAction.d.ts → emitModerationEvent.d.ts} +5 -6
  21. package/dist/lexicon/types/com/atproto/admin/{getModerationAction.d.ts → getModerationEvent.d.ts} +1 -1
  22. package/dist/lexicon/types/com/atproto/admin/{getModerationActions.d.ts → queryModerationEvents.d.ts} +5 -1
  23. package/dist/lexicon/types/com/atproto/admin/{getModerationReports.d.ts → queryModerationStatuses.d.ts} +12 -6
  24. package/dist/lexicon/types/com/atproto/admin/sendEmail.d.ts +1 -0
  25. package/dist/migrate-moderation-data.d.ts +1 -0
  26. package/dist/services/actor/views.d.ts +2 -5
  27. package/dist/services/feed/index.d.ts +1 -0
  28. package/dist/services/feed/util.d.ts +9 -1
  29. package/dist/services/feed/views.d.ts +6 -17
  30. package/dist/services/graph/index.d.ts +5 -29
  31. package/dist/services/graph/types.d.ts +1 -0
  32. package/dist/services/moderation/index.d.ts +135 -72
  33. package/dist/services/moderation/pagination.d.ts +36 -0
  34. package/dist/services/moderation/status.d.ts +13 -0
  35. package/dist/services/moderation/types.d.ts +35 -0
  36. package/dist/services/moderation/views.d.ts +18 -14
  37. package/dist/util/debug.d.ts +1 -1
  38. package/package.json +11 -11
  39. package/src/api/app/bsky/feed/getActorFeeds.ts +2 -1
  40. package/src/api/app/bsky/feed/getActorLikes.ts +1 -3
  41. package/src/api/app/bsky/feed/getAuthorFeed.ts +1 -3
  42. package/src/api/app/bsky/feed/getFeed.ts +9 -9
  43. package/src/api/app/bsky/feed/getFeedGenerator.ts +3 -0
  44. package/src/api/app/bsky/feed/getFeedGenerators.ts +2 -1
  45. package/src/api/app/bsky/feed/getListFeed.ts +1 -3
  46. package/src/api/app/bsky/feed/getPostThread.ts +15 -54
  47. package/src/api/app/bsky/feed/getPosts.ts +21 -18
  48. package/src/api/app/bsky/feed/getSuggestedFeeds.ts +2 -1
  49. package/src/api/app/bsky/feed/getTimeline.ts +1 -3
  50. package/src/api/app/bsky/feed/searchPosts.ts +20 -17
  51. package/src/api/app/bsky/graph/getList.ts +6 -3
  52. package/src/api/app/bsky/graph/getListBlocks.ts +3 -2
  53. package/src/api/app/bsky/graph/getListMutes.ts +2 -1
  54. package/src/api/app/bsky/graph/getLists.ts +2 -1
  55. package/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +3 -1
  56. package/src/api/blob-resolver.ts +6 -11
  57. package/src/api/com/atproto/admin/emitModerationEvent.ts +220 -0
  58. package/src/api/com/atproto/admin/{getModerationActions.ts → getModerationEvent.ts} +5 -11
  59. package/src/api/com/atproto/admin/getRecord.ts +1 -0
  60. package/src/api/com/atproto/admin/{getModerationReports.ts → queryModerationEvents.ts} +13 -16
  61. package/src/api/com/atproto/admin/queryModerationStatuses.ts +55 -0
  62. package/src/api/com/atproto/moderation/createReport.ts +9 -7
  63. package/src/api/com/atproto/moderation/util.ts +38 -20
  64. package/src/api/index.ts +8 -14
  65. package/src/auth.ts +29 -21
  66. package/src/auto-moderator/index.ts +26 -19
  67. package/src/context.ts +4 -0
  68. package/src/db/migrations/20231003T202833377Z-create-moderation-subject-status.ts +123 -0
  69. package/src/db/migrations/index.ts +1 -0
  70. package/src/db/pagination.ts +26 -3
  71. package/src/db/{periodic-moderation-action-reversal.ts → periodic-moderation-event-reversal.ts} +50 -46
  72. package/src/db/tables/moderation.ts +35 -52
  73. package/src/feed-gen/best-of-follows.ts +6 -3
  74. package/src/feed-gen/bsky-team.ts +1 -1
  75. package/src/feed-gen/hot-classic.ts +1 -1
  76. package/src/feed-gen/mutuals.ts +6 -2
  77. package/src/feed-gen/types.ts +1 -1
  78. package/src/feed-gen/whats-hot.ts +1 -1
  79. package/src/feed-gen/with-friends.ts +7 -3
  80. package/src/index.ts +2 -1
  81. package/src/lexicon/index.ts +30 -67
  82. package/src/lexicon/lexicons.ts +526 -491
  83. package/src/lexicon/types/app/bsky/feed/defs.ts +1 -18
  84. package/src/lexicon/types/app/bsky/graph/defs.ts +1 -0
  85. package/src/lexicon/types/com/atproto/admin/defs.ts +276 -84
  86. package/src/lexicon/types/com/atproto/admin/{takeModerationAction.ts → emitModerationEvent.ts} +13 -11
  87. package/src/lexicon/types/com/atproto/admin/{getModerationReport.ts → getModerationEvent.ts} +1 -1
  88. package/src/lexicon/types/com/atproto/admin/{getModerationActions.ts → queryModerationEvents.ts} +8 -1
  89. package/src/lexicon/types/com/atproto/admin/{getModerationReports.ts → queryModerationStatuses.ts} +21 -14
  90. package/src/lexicon/types/com/atproto/admin/sendEmail.ts +1 -0
  91. package/src/migrate-moderation-data.ts +414 -0
  92. package/src/services/actor/views.ts +5 -14
  93. package/src/services/feed/index.ts +26 -7
  94. package/src/services/feed/util.ts +47 -19
  95. package/src/services/feed/views.ts +68 -4
  96. package/src/services/graph/index.ts +21 -3
  97. package/src/services/graph/types.ts +1 -0
  98. package/src/services/indexing/plugins/block.ts +2 -3
  99. package/src/services/indexing/plugins/feed-generator.ts +2 -3
  100. package/src/services/indexing/plugins/follow.ts +2 -3
  101. package/src/services/indexing/plugins/like.ts +2 -3
  102. package/src/services/indexing/plugins/list-block.ts +2 -3
  103. package/src/services/indexing/plugins/list-item.ts +2 -3
  104. package/src/services/indexing/plugins/list.ts +2 -3
  105. package/src/services/indexing/plugins/post.ts +3 -4
  106. package/src/services/indexing/plugins/repost.ts +2 -3
  107. package/src/services/indexing/plugins/thread-gate.ts +2 -3
  108. package/src/services/label/index.ts +2 -3
  109. package/src/services/moderation/index.ts +380 -395
  110. package/src/services/moderation/pagination.ts +96 -0
  111. package/src/services/moderation/status.ts +244 -0
  112. package/src/services/moderation/types.ts +49 -0
  113. package/src/services/moderation/views.ts +278 -329
  114. package/src/util/debug.ts +2 -2
  115. package/tests/__snapshots__/feed-generation.test.ts.snap +322 -6
  116. package/tests/__snapshots__/indexing.test.ts.snap +0 -6
  117. package/tests/admin/__snapshots__/get-record.test.ts.snap +30 -132
  118. package/tests/admin/__snapshots__/get-repo.test.ts.snap +14 -60
  119. package/tests/admin/__snapshots__/moderation-events.test.ts.snap +146 -0
  120. package/tests/admin/__snapshots__/moderation-statuses.test.ts.snap +64 -0
  121. package/tests/admin/__snapshots__/moderation.test.ts.snap +0 -125
  122. package/tests/admin/get-record.test.ts +5 -9
  123. package/tests/admin/get-repo.test.ts +5 -9
  124. package/tests/admin/moderation-events.test.ts +221 -0
  125. package/tests/admin/moderation-statuses.test.ts +145 -0
  126. package/tests/admin/moderation.test.ts +512 -860
  127. package/tests/admin/repo-search.test.ts +2 -3
  128. package/tests/auto-moderator/fuzzy-matcher.test.ts +2 -1
  129. package/tests/auto-moderator/takedowns.test.ts +45 -18
  130. package/tests/feed-generation.test.ts +57 -9
  131. package/tests/views/__snapshots__/block-lists.test.ts.snap +3 -9
  132. package/tests/views/__snapshots__/blocks.test.ts.snap +0 -9
  133. package/tests/views/__snapshots__/mute-lists.test.ts.snap +5 -5
  134. package/tests/views/__snapshots__/mutes.test.ts.snap +0 -3
  135. package/tests/views/__snapshots__/thread.test.ts.snap +0 -30
  136. package/tests/views/actor-search.test.ts +2 -3
  137. package/tests/views/author-feed.test.ts +42 -36
  138. package/tests/views/follows.test.ts +40 -35
  139. package/tests/views/list-feed.test.ts +17 -9
  140. package/tests/views/notifications.test.ts +13 -9
  141. package/tests/views/profile.test.ts +20 -18
  142. package/tests/views/thread.test.ts +54 -26
  143. package/tests/views/threadgating.test.ts +51 -19
  144. package/tests/views/timeline.test.ts +21 -13
  145. package/dist/api/com/atproto/admin/resolveModerationReports.d.ts +0 -3
  146. package/dist/api/com/atproto/admin/reverseModerationAction.d.ts +0 -3
  147. package/dist/api/com/atproto/admin/takeModerationAction.d.ts +0 -3
  148. package/dist/lexicon/types/com/atproto/admin/getModerationReport.d.ts +0 -29
  149. package/dist/lexicon/types/com/atproto/admin/resolveModerationReports.d.ts +0 -36
  150. package/dist/lexicon/types/com/atproto/admin/reverseModerationAction.d.ts +0 -36
  151. package/src/api/com/atproto/admin/getModerationAction.ts +0 -44
  152. package/src/api/com/atproto/admin/getModerationReport.ts +0 -43
  153. package/src/api/com/atproto/admin/resolveModerationReports.ts +0 -24
  154. package/src/api/com/atproto/admin/reverseModerationAction.ts +0 -115
  155. package/src/api/com/atproto/admin/takeModerationAction.ts +0 -156
  156. package/src/lexicon/types/com/atproto/admin/getModerationAction.ts +0 -41
  157. package/src/lexicon/types/com/atproto/admin/resolveModerationReports.ts +0 -49
  158. package/src/lexicon/types/com/atproto/admin/reverseModerationAction.ts +0 -49
  159. package/tests/admin/__snapshots__/get-moderation-action.test.ts.snap +0 -172
  160. package/tests/admin/__snapshots__/get-moderation-actions.test.ts.snap +0 -178
  161. package/tests/admin/__snapshots__/get-moderation-report.test.ts.snap +0 -177
  162. package/tests/admin/__snapshots__/get-moderation-reports.test.ts.snap +0 -307
  163. package/tests/admin/get-moderation-action.test.ts +0 -100
  164. package/tests/admin/get-moderation-actions.test.ts +0 -164
  165. package/tests/admin/get-moderation-report.test.ts +0 -100
  166. package/tests/admin/get-moderation-reports.test.ts +0 -332
  167. /package/dist/api/com/atproto/admin/{getModerationAction.d.ts → emitModerationEvent.d.ts} +0 -0
  168. /package/dist/api/com/atproto/admin/{getModerationActions.d.ts → getModerationEvent.d.ts} +0 -0
  169. /package/dist/api/com/atproto/admin/{getModerationReport.d.ts → queryModerationEvents.d.ts} +0 -0
  170. /package/dist/api/com/atproto/admin/{getModerationReports.d.ts → queryModerationStatuses.d.ts} +0 -0
@@ -0,0 +1,220 @@
1
+ import { CID } from 'multiformats/cid'
2
+ import { AtUri } from '@atproto/syntax'
3
+ import {
4
+ AuthRequiredError,
5
+ InvalidRequestError,
6
+ UpstreamFailureError,
7
+ } from '@atproto/xrpc-server'
8
+ import { Server } from '../../../../lexicon'
9
+ import AppContext from '../../../../context'
10
+ import { getSubject } from '../moderation/util'
11
+ import {
12
+ isModEventLabel,
13
+ isModEventReverseTakedown,
14
+ isModEventTakedown,
15
+ } from '../../../../lexicon/types/com/atproto/admin/defs'
16
+ import { TakedownSubjects } from '../../../../services/moderation'
17
+ import { retryHttp } from '../../../../util/retry'
18
+
19
+ export default function (server: Server, ctx: AppContext) {
20
+ server.com.atproto.admin.emitModerationEvent({
21
+ auth: ctx.roleVerifier,
22
+ handler: async ({ input, auth }) => {
23
+ const access = auth.credentials
24
+ const db = ctx.db.getPrimary()
25
+ const moderationService = ctx.services.moderation(db)
26
+ const { subject, createdBy, subjectBlobCids, event } = input.body
27
+ const isTakedownEvent = isModEventTakedown(event)
28
+ const isReverseTakedownEvent = isModEventReverseTakedown(event)
29
+ const isLabelEvent = isModEventLabel(event)
30
+
31
+ // apply access rules
32
+
33
+ // if less than moderator access then can not takedown an account
34
+ if (!access.moderator && isTakedownEvent && 'did' in subject) {
35
+ throw new AuthRequiredError(
36
+ 'Must be a full moderator to perform an account takedown',
37
+ )
38
+ }
39
+ // if less than moderator access then can only take ack and escalation actions
40
+ if (!access.moderator && (isTakedownEvent || isReverseTakedownEvent)) {
41
+ throw new AuthRequiredError(
42
+ 'Must be a full moderator to take this type of action',
43
+ )
44
+ }
45
+ // if less than moderator access then can not apply labels
46
+ if (!access.moderator && isLabelEvent) {
47
+ throw new AuthRequiredError('Must be a full moderator to label content')
48
+ }
49
+
50
+ if (isLabelEvent) {
51
+ validateLabels([
52
+ ...(event.createLabelVals ?? []),
53
+ ...(event.negateLabelVals ?? []),
54
+ ])
55
+ }
56
+
57
+ const subjectInfo = getSubject(subject)
58
+
59
+ if (isTakedownEvent || isReverseTakedownEvent) {
60
+ const isSubjectTakendown = await moderationService.isSubjectTakendown(
61
+ subjectInfo,
62
+ )
63
+
64
+ if (isSubjectTakendown && isTakedownEvent) {
65
+ throw new InvalidRequestError(`Subject is already taken down`)
66
+ }
67
+
68
+ if (!isSubjectTakendown && isReverseTakedownEvent) {
69
+ throw new InvalidRequestError(`Subject is not taken down`)
70
+ }
71
+ }
72
+
73
+ const { result: moderationEvent, takenDown } = await db.transaction(
74
+ async (dbTxn) => {
75
+ const moderationTxn = ctx.services.moderation(dbTxn)
76
+ const labelTxn = ctx.services.label(dbTxn)
77
+
78
+ const result = await moderationTxn.logEvent({
79
+ event,
80
+ subject: subjectInfo,
81
+ subjectBlobCids:
82
+ subjectBlobCids?.map((cid) => CID.parse(cid)) ?? [],
83
+ createdBy,
84
+ })
85
+
86
+ let takenDown: TakedownSubjects | undefined
87
+
88
+ if (
89
+ result.subjectType === 'com.atproto.admin.defs#repoRef' &&
90
+ result.subjectDid
91
+ ) {
92
+ // No credentials to revoke on appview
93
+ if (isTakedownEvent) {
94
+ takenDown = await moderationTxn.takedownRepo({
95
+ takedownId: result.id,
96
+ did: result.subjectDid,
97
+ })
98
+ }
99
+
100
+ if (isReverseTakedownEvent) {
101
+ await moderationTxn.reverseTakedownRepo({
102
+ did: result.subjectDid,
103
+ })
104
+ takenDown = {
105
+ subjects: [
106
+ {
107
+ $type: 'com.atproto.admin.defs#repoRef',
108
+ did: result.subjectDid,
109
+ },
110
+ ],
111
+ did: result.subjectDid,
112
+ }
113
+ }
114
+ }
115
+
116
+ if (
117
+ result.subjectType === 'com.atproto.repo.strongRef' &&
118
+ result.subjectUri
119
+ ) {
120
+ const blobCids = subjectBlobCids?.map((cid) => CID.parse(cid)) ?? []
121
+ if (isTakedownEvent) {
122
+ takenDown = await moderationTxn.takedownRecord({
123
+ takedownId: result.id,
124
+ uri: new AtUri(result.subjectUri),
125
+ // TODO: I think this will always be available for strongRefs?
126
+ cid: CID.parse(result.subjectCid as string),
127
+ blobCids,
128
+ })
129
+ }
130
+
131
+ if (isReverseTakedownEvent) {
132
+ await moderationTxn.reverseTakedownRecord({
133
+ uri: new AtUri(result.subjectUri),
134
+ })
135
+ takenDown = {
136
+ did: result.subjectDid,
137
+ subjects: [
138
+ {
139
+ $type: 'com.atproto.repo.strongRef',
140
+ uri: result.subjectUri,
141
+ cid: result.subjectCid ?? '',
142
+ },
143
+ ...blobCids.map((cid) => ({
144
+ $type: 'com.atproto.admin.defs#repoBlobRef',
145
+ did: result.subjectDid,
146
+ cid: cid.toString(),
147
+ recordUri: result.subjectUri,
148
+ })),
149
+ ],
150
+ }
151
+ }
152
+ }
153
+
154
+ if (isLabelEvent) {
155
+ await labelTxn.formatAndCreate(
156
+ ctx.cfg.labelerDid,
157
+ result.subjectUri ?? result.subjectDid,
158
+ result.subjectCid,
159
+ {
160
+ create: result.createLabelVals?.length
161
+ ? result.createLabelVals.split(' ')
162
+ : undefined,
163
+ negate: result.negateLabelVals?.length
164
+ ? result.negateLabelVals.split(' ')
165
+ : undefined,
166
+ },
167
+ )
168
+ }
169
+
170
+ return { result, takenDown }
171
+ },
172
+ )
173
+
174
+ if (takenDown && ctx.moderationPushAgent) {
175
+ const { did, subjects } = takenDown
176
+ if (did && subjects.length > 0) {
177
+ const agent = ctx.moderationPushAgent
178
+ const results = await Promise.allSettled(
179
+ subjects.map((subject) =>
180
+ retryHttp(() =>
181
+ agent.api.com.atproto.admin.updateSubjectStatus({
182
+ subject,
183
+ takedown: isTakedownEvent
184
+ ? {
185
+ applied: true,
186
+ ref: moderationEvent.id.toString(),
187
+ }
188
+ : {
189
+ applied: false,
190
+ },
191
+ }),
192
+ ),
193
+ ),
194
+ )
195
+ const hadFailure = results.some((r) => r.status === 'rejected')
196
+ if (hadFailure) {
197
+ throw new UpstreamFailureError('failed to apply action on PDS')
198
+ }
199
+ }
200
+ }
201
+
202
+ return {
203
+ encoding: 'application/json',
204
+ body: await moderationService.views.event(moderationEvent),
205
+ }
206
+ },
207
+ })
208
+ }
209
+
210
+ const validateLabels = (labels: string[]) => {
211
+ for (const label of labels) {
212
+ for (const char of badChars) {
213
+ if (label.includes(char)) {
214
+ throw new InvalidRequestError(`Invalid label: ${label}`)
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ const badChars = [' ', ',', ';', `'`, `"`]
@@ -2,23 +2,17 @@ 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.getModerationActions({
5
+ server.com.atproto.admin.getModerationEvent({
6
6
  auth: ctx.roleVerifier,
7
7
  handler: async ({ params }) => {
8
- const { subject, limit = 50, cursor } = params
8
+ const { id } = params
9
9
  const db = ctx.db.getPrimary()
10
10
  const moderationService = ctx.services.moderation(db)
11
- const results = await moderationService.getActions({
12
- subject,
13
- limit,
14
- cursor,
15
- })
11
+ const event = await moderationService.getEventOrThrow(id)
12
+ const eventDetail = await moderationService.views.eventDetail(event)
16
13
  return {
17
14
  encoding: 'application/json',
18
- body: {
19
- cursor: results.at(-1)?.id.toString() ?? undefined,
20
- actions: await moderationService.views.action(results),
21
- },
15
+ body: eventDetail,
22
16
  }
23
17
  },
24
18
  })
@@ -18,6 +18,7 @@ export default function (server: Server, ctx: AppContext) {
18
18
  if (!result) {
19
19
  throw new InvalidRequestError('Record not found', 'RecordNotFound')
20
20
  }
21
+
21
22
  const [record, accountInfo] = await Promise.all([
22
23
  ctx.services.moderation(db).views.recordDetail(result),
23
24
  getPdsAccountInfo(ctx, result.did),
@@ -1,39 +1,36 @@
1
1
  import { Server } from '../../../../lexicon'
2
2
  import AppContext from '../../../../context'
3
+ import { getEventType } from '../moderation/util'
3
4
 
4
5
  export default function (server: Server, ctx: AppContext) {
5
- server.com.atproto.admin.getModerationReports({
6
+ server.com.atproto.admin.queryModerationEvents({
6
7
  auth: ctx.roleVerifier,
7
8
  handler: async ({ params }) => {
8
9
  const {
9
10
  subject,
10
- resolved,
11
- actionType,
12
11
  limit = 50,
13
12
  cursor,
14
- ignoreSubjects,
15
- reverse = false,
16
- reporters = [],
17
- actionedBy,
13
+ sortDirection = 'desc',
14
+ types,
15
+ includeAllUserRecords = false,
16
+ createdBy,
18
17
  } = params
19
18
  const db = ctx.db.getPrimary()
20
19
  const moderationService = ctx.services.moderation(db)
21
- const results = await moderationService.getReports({
20
+ const results = await moderationService.getEvents({
21
+ types: types?.length ? types.map(getEventType) : [],
22
22
  subject,
23
- resolved,
24
- actionType,
23
+ createdBy,
25
24
  limit,
26
25
  cursor,
27
- ignoreSubjects,
28
- reverse,
29
- reporters,
30
- actionedBy,
26
+ sortDirection,
27
+ includeAllUserRecords,
31
28
  })
32
29
  return {
33
30
  encoding: 'application/json',
34
31
  body: {
35
- cursor: results.at(-1)?.id.toString() ?? undefined,
36
- reports: await moderationService.views.report(results),
32
+ cursor: results.cursor,
33
+ events: await moderationService.views.event(results.events),
37
34
  },
38
35
  }
39
36
  },
@@ -0,0 +1,55 @@
1
+ import { Server } from '../../../../lexicon'
2
+ import AppContext from '../../../../context'
3
+ import { getReviewState } from '../moderation/util'
4
+
5
+ export default function (server: Server, ctx: AppContext) {
6
+ server.com.atproto.admin.queryModerationStatuses({
7
+ auth: ctx.roleVerifier,
8
+ handler: async ({ params }) => {
9
+ const {
10
+ subject,
11
+ takendown,
12
+ reviewState,
13
+ reviewedAfter,
14
+ reviewedBefore,
15
+ reportedAfter,
16
+ reportedBefore,
17
+ ignoreSubjects,
18
+ lastReviewedBy,
19
+ sortDirection = 'desc',
20
+ sortField = 'lastReportedAt',
21
+ includeMuted = false,
22
+ limit = 50,
23
+ cursor,
24
+ } = params
25
+ const db = ctx.db.getPrimary()
26
+ const moderationService = ctx.services.moderation(db)
27
+ const results = await moderationService.getSubjectStatuses({
28
+ reviewState: getReviewState(reviewState),
29
+ subject,
30
+ takendown,
31
+ reviewedAfter,
32
+ reviewedBefore,
33
+ reportedAfter,
34
+ reportedBefore,
35
+ includeMuted,
36
+ ignoreSubjects,
37
+ sortDirection,
38
+ lastReviewedBy,
39
+ sortField,
40
+ limit,
41
+ cursor,
42
+ })
43
+ const subjectStatuses = moderationService.views.subjectStatus(
44
+ results.statuses,
45
+ )
46
+ return {
47
+ encoding: 'application/json',
48
+ body: {
49
+ cursor: results.cursor,
50
+ subjectStatuses,
51
+ },
52
+ }
53
+ },
54
+ })
55
+ }
@@ -22,15 +22,17 @@ export default function (server: Server, ctx: AppContext) {
22
22
  }
23
23
  }
24
24
 
25
- const moderationService = ctx.services.moderation(db)
26
-
27
- const report = await moderationService.report({
28
- reasonType: getReasonType(reasonType),
29
- reason,
30
- subject: getSubject(subject),
31
- reportedBy: requester || ctx.cfg.serverDid,
25
+ const report = await db.transaction(async (dbTxn) => {
26
+ const moderationTxn = ctx.services.moderation(dbTxn)
27
+ return moderationTxn.report({
28
+ reasonType: getReasonType(reasonType),
29
+ reason,
30
+ subject: getSubject(subject),
31
+ reportedBy: requester || ctx.cfg.serverDid,
32
+ })
32
33
  })
33
34
 
35
+ const moderationService = ctx.services.moderation(db)
34
36
  return {
35
37
  encoding: 'application/json',
36
38
  body: moderationService.views.reportPublic(report),
@@ -1,16 +1,8 @@
1
1
  import { CID } from 'multiformats/cid'
2
2
  import { InvalidRequestError } from '@atproto/xrpc-server'
3
3
  import { AtUri } from '@atproto/syntax'
4
- import { ModerationAction } from '../../../../db/tables/moderation'
5
- import { ModerationReport } from '../../../../db/tables/moderation'
6
4
  import { InputSchema as ReportInput } from '../../../../lexicon/types/com/atproto/moderation/createReport'
7
- import { InputSchema as ActionInput } from '../../../../lexicon/types/com/atproto/admin/takeModerationAction'
8
- import {
9
- ACKNOWLEDGE,
10
- FLAG,
11
- TAKEDOWN,
12
- ESCALATE,
13
- } from '../../../../lexicon/types/com/atproto/admin/defs'
5
+ import { InputSchema as ActionInput } from '../../../../lexicon/types/com/atproto/admin/emitModerationEvent'
14
6
  import {
15
7
  REASONOTHER,
16
8
  REASONSPAM,
@@ -19,6 +11,13 @@ import {
19
11
  REASONSEXUAL,
20
12
  REASONVIOLATION,
21
13
  } from '../../../../lexicon/types/com/atproto/moderation/defs'
14
+ import {
15
+ REVIEWCLOSED,
16
+ REVIEWESCALATED,
17
+ REVIEWOPEN,
18
+ } from '../../../../lexicon/types/com/atproto/admin/defs'
19
+ import { ModerationEvent } from '../../../../db/tables/moderation'
20
+ import { ModerationSubjectStatusRow } from '../../../../services/moderation/types'
22
21
 
23
22
  type SubjectInput = ReportInput['subject'] | ActionInput['subject']
24
23
 
@@ -34,8 +33,9 @@ export const getSubject = (subject: SubjectInput) => {
34
33
  typeof subject.uri === 'string' &&
35
34
  typeof subject.cid === 'string'
36
35
  ) {
36
+ const uri = new AtUri(subject.uri)
37
37
  return {
38
- uri: new AtUri(subject.uri),
38
+ uri,
39
39
  cid: CID.parse(subject.cid),
40
40
  }
41
41
  }
@@ -44,23 +44,28 @@ export const getSubject = (subject: SubjectInput) => {
44
44
 
45
45
  export const getReasonType = (reasonType: ReportInput['reasonType']) => {
46
46
  if (reasonTypes.has(reasonType)) {
47
- return reasonType as ModerationReport['reasonType']
47
+ return reasonType as NonNullable<ModerationEvent['meta']>['reportType']
48
48
  }
49
49
  throw new InvalidRequestError('Invalid reason type')
50
50
  }
51
51
 
52
- export const getAction = (action: ActionInput['action']) => {
53
- if (
54
- action === TAKEDOWN ||
55
- action === FLAG ||
56
- action === ACKNOWLEDGE ||
57
- action === ESCALATE
58
- ) {
59
- return action as ModerationAction['action']
52
+ export const getEventType = (type: string) => {
53
+ if (eventTypes.has(type)) {
54
+ return type as ModerationEvent['action']
60
55
  }
61
- throw new InvalidRequestError('Invalid action')
56
+ throw new InvalidRequestError('Invalid event type')
62
57
  }
63
58
 
59
+ export const getReviewState = (reviewState?: string) => {
60
+ if (!reviewState) return undefined
61
+ if (reviewStates.has(reviewState)) {
62
+ return reviewState as ModerationSubjectStatusRow['reviewState']
63
+ }
64
+ throw new InvalidRequestError('Invalid review state')
65
+ }
66
+
67
+ const reviewStates = new Set([REVIEWCLOSED, REVIEWESCALATED, REVIEWOPEN])
68
+
64
69
  const reasonTypes = new Set([
65
70
  REASONOTHER,
66
71
  REASONSPAM,
@@ -69,3 +74,16 @@ const reasonTypes = new Set([
69
74
  REASONSEXUAL,
70
75
  REASONVIOLATION,
71
76
  ])
77
+
78
+ const eventTypes = new Set([
79
+ 'com.atproto.admin.defs#modEventTakedown',
80
+ 'com.atproto.admin.defs#modEventAcknowledge',
81
+ 'com.atproto.admin.defs#modEventEscalate',
82
+ 'com.atproto.admin.defs#modEventComment',
83
+ 'com.atproto.admin.defs#modEventLabel',
84
+ 'com.atproto.admin.defs#modEventReport',
85
+ 'com.atproto.admin.defs#modEventMute',
86
+ 'com.atproto.admin.defs#modEventUnmute',
87
+ 'com.atproto.admin.defs#modEventReverseTakedown',
88
+ 'com.atproto.admin.defs#modEventEmail',
89
+ ])
package/src/api/index.ts CHANGED
@@ -41,18 +41,15 @@ import registerPush from './app/bsky/notification/registerPush'
41
41
  import getPopularFeedGenerators from './app/bsky/unspecced/getPopularFeedGenerators'
42
42
  import getTimelineSkeleton from './app/bsky/unspecced/getTimelineSkeleton'
43
43
  import createReport from './com/atproto/moderation/createReport'
44
- import resolveModerationReports from './com/atproto/admin/resolveModerationReports'
45
- import reverseModerationAction from './com/atproto/admin/reverseModerationAction'
46
- import takeModerationAction from './com/atproto/admin/takeModerationAction'
44
+ import emitModerationEvent from './com/atproto/admin/emitModerationEvent'
47
45
  import searchRepos from './com/atproto/admin/searchRepos'
48
46
  import adminGetRecord from './com/atproto/admin/getRecord'
49
47
  import getRepo from './com/atproto/admin/getRepo'
50
- import getModerationAction from './com/atproto/admin/getModerationAction'
51
- import getModerationActions from './com/atproto/admin/getModerationActions'
52
- import getModerationReport from './com/atproto/admin/getModerationReport'
53
- import getModerationReports from './com/atproto/admin/getModerationReports'
48
+ import queryModerationStatuses from './com/atproto/admin/queryModerationStatuses'
54
49
  import resolveHandle from './com/atproto/identity/resolveHandle'
55
50
  import getRecord from './com/atproto/repo/getRecord'
51
+ import queryModerationEvents from './com/atproto/admin/queryModerationEvents'
52
+ import getModerationEvent from './com/atproto/admin/getModerationEvent'
56
53
  import fetchLabels from './com/atproto/temp/fetchLabels'
57
54
 
58
55
  export * as health from './health'
@@ -105,16 +102,13 @@ export default function (server: Server, ctx: AppContext) {
105
102
  getTimelineSkeleton(server, ctx)
106
103
  // com.atproto
107
104
  createReport(server, ctx)
108
- resolveModerationReports(server, ctx)
109
- reverseModerationAction(server, ctx)
110
- takeModerationAction(server, ctx)
105
+ emitModerationEvent(server, ctx)
111
106
  searchRepos(server, ctx)
112
107
  adminGetRecord(server, ctx)
113
108
  getRepo(server, ctx)
114
- getModerationAction(server, ctx)
115
- getModerationActions(server, ctx)
116
- getModerationReport(server, ctx)
117
- getModerationReports(server, ctx)
109
+ getModerationEvent(server, ctx)
110
+ queryModerationEvents(server, ctx)
111
+ queryModerationStatuses(server, ctx)
118
112
  resolveHandle(server, ctx)
119
113
  getRecord(server, ctx)
120
114
  fetchLabels(server, ctx)
package/src/auth.ts CHANGED
@@ -7,35 +7,43 @@ import { ServerConfig } from './config'
7
7
  const BASIC = 'Basic '
8
8
  const BEARER = 'Bearer '
9
9
 
10
- export const authVerifier =
11
- (idResolver: IdResolver, opts: { aud: string | null }) =>
12
- async (reqCtx: { req: express.Request; res: express.Response }) => {
10
+ export const authVerifier = (
11
+ idResolver: IdResolver,
12
+ opts: { aud: string | null },
13
+ ) => {
14
+ const getSigningKey = async (
15
+ did: string,
16
+ forceRefresh: boolean,
17
+ ): Promise<string> => {
18
+ const atprotoData = await idResolver.did.resolveAtprotoData(
19
+ did,
20
+ forceRefresh,
21
+ )
22
+ return atprotoData.signingKey
23
+ }
24
+
25
+ return async (reqCtx: { req: express.Request; res: express.Response }) => {
13
26
  const jwtStr = getJwtStrFromReq(reqCtx.req)
14
27
  if (!jwtStr) {
15
28
  throw new AuthRequiredError('missing jwt', 'MissingJwt')
16
29
  }
17
- const payload = await verifyJwt(
18
- jwtStr,
19
- opts.aud,
20
- async (did, forceRefresh) => {
21
- const atprotoData = await idResolver.did.resolveAtprotoData(
22
- did,
23
- forceRefresh,
24
- )
25
- return atprotoData.signingKey
26
- },
27
- )
30
+ const payload = await verifyJwt(jwtStr, opts.aud, getSigningKey)
28
31
  return { credentials: { did: payload.iss }, artifacts: { aud: opts.aud } }
29
32
  }
33
+ }
30
34
 
31
- export const authOptionalVerifier =
32
- (idResolver: IdResolver, opts: { aud: string | null }) =>
33
- async (reqCtx: { req: express.Request; res: express.Response }) => {
35
+ export const authOptionalVerifier = (
36
+ idResolver: IdResolver,
37
+ opts: { aud: string | null },
38
+ ) => {
39
+ const verifyAccess = authVerifier(idResolver, opts)
40
+ return async (reqCtx: { req: express.Request; res: express.Response }) => {
34
41
  if (!reqCtx.req.headers.authorization) {
35
42
  return { credentials: { did: null } }
36
43
  }
37
- return authVerifier(idResolver, opts)(reqCtx)
44
+ return verifyAccess(reqCtx)
38
45
  }
46
+ }
39
47
 
40
48
  export const authOptionalAccessOrRoleVerifier = (
41
49
  idResolver: IdResolver,
@@ -127,9 +135,9 @@ export const buildBasicAuth = (username: string, password: string): string => {
127
135
  }
128
136
 
129
137
  export const getJwtStrFromReq = (req: express.Request): string | null => {
130
- const { authorization = '' } = req.headers
131
- if (!authorization.startsWith(BEARER)) {
138
+ const { authorization } = req.headers
139
+ if (!authorization?.startsWith(BEARER)) {
132
140
  return null
133
141
  }
134
- return authorization.replace(BEARER, '').trim()
142
+ return authorization.slice(BEARER.length).trim()
135
143
  }