@atproto/bsky 0.0.15 → 0.0.17

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 (236) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/api/com/atproto/moderation/util.d.ts +4 -3
  3. package/dist/cache/read-through.d.ts +30 -0
  4. package/dist/config.d.ts +18 -0
  5. package/dist/context.d.ts +21 -6
  6. package/dist/daemon/config.d.ts +15 -0
  7. package/dist/daemon/context.d.ts +15 -0
  8. package/dist/daemon/index.d.ts +23 -0
  9. package/dist/daemon/logger.d.ts +3 -0
  10. package/dist/daemon/notifications.d.ts +18 -0
  11. package/dist/daemon/services.d.ts +11 -0
  12. package/dist/db/database-schema.d.ts +1 -2
  13. package/dist/db/index.js +41 -1
  14. package/dist/db/index.js.map +3 -3
  15. package/dist/db/migrations/20231003T202833377Z-create-moderation-subject-status.d.ts +3 -0
  16. package/dist/db/migrations/20231205T000257238Z-remove-did-cache.d.ts +3 -0
  17. package/dist/db/migrations/index.d.ts +2 -0
  18. package/dist/db/pagination.d.ts +2 -1
  19. package/dist/db/{periodic-moderation-action-reversal.d.ts → periodic-moderation-event-reversal.d.ts} +3 -5
  20. package/dist/db/tables/moderation.d.ts +24 -34
  21. package/dist/did-cache.d.ts +10 -7
  22. package/dist/feed-gen/types.d.ts +1 -1
  23. package/dist/index.d.ts +6 -1
  24. package/dist/index.js +4370 -2758
  25. package/dist/index.js.map +3 -3
  26. package/dist/indexer/context.d.ts +2 -0
  27. package/dist/indexer/index.d.ts +1 -0
  28. package/dist/lexicon/index.d.ts +23 -18
  29. package/dist/lexicon/lexicons.d.ts +561 -412
  30. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +1 -7
  31. package/dist/lexicon/types/app/bsky/graph/defs.d.ts +1 -0
  32. package/dist/lexicon/types/com/atproto/admin/defs.d.ts +114 -48
  33. package/dist/lexicon/types/com/atproto/admin/{resolveModerationReports.d.ts → deleteAccount.d.ts} +2 -13
  34. package/dist/lexicon/types/com/atproto/admin/{takeModerationAction.d.ts → emitModerationEvent.d.ts} +5 -6
  35. package/dist/lexicon/types/com/atproto/admin/{getModerationAction.d.ts → getModerationEvent.d.ts} +1 -1
  36. package/dist/lexicon/types/com/atproto/admin/{getModerationActions.d.ts → queryModerationEvents.d.ts} +5 -1
  37. package/dist/lexicon/types/com/atproto/admin/{getModerationReports.d.ts → queryModerationStatuses.d.ts} +12 -6
  38. package/dist/lexicon/types/com/atproto/admin/sendEmail.d.ts +1 -0
  39. package/dist/lexicon/types/com/atproto/{admin/getModerationReport.d.ts → temp/importRepo.d.ts} +10 -7
  40. package/dist/lexicon/types/com/atproto/temp/pushBlob.d.ts +25 -0
  41. package/dist/lexicon/types/com/atproto/{admin/reverseModerationAction.d.ts → temp/transferAccount.d.ts} +11 -5
  42. package/dist/logger.d.ts +1 -0
  43. package/dist/migrate-moderation-data.d.ts +1 -0
  44. package/dist/redis.d.ts +10 -1
  45. package/dist/services/actor/index.d.ts +18 -4
  46. package/dist/services/actor/views.d.ts +6 -8
  47. package/dist/services/feed/index.d.ts +7 -4
  48. package/dist/services/feed/util.d.ts +9 -1
  49. package/dist/services/feed/views.d.ts +11 -21
  50. package/dist/services/graph/index.d.ts +5 -29
  51. package/dist/services/graph/types.d.ts +1 -0
  52. package/dist/services/index.d.ts +3 -7
  53. package/dist/services/label/index.d.ts +10 -4
  54. package/dist/services/moderation/index.d.ts +134 -72
  55. package/dist/services/moderation/pagination.d.ts +36 -0
  56. package/dist/services/moderation/status.d.ts +13 -0
  57. package/dist/services/moderation/types.d.ts +35 -0
  58. package/dist/services/moderation/views.d.ts +18 -14
  59. package/dist/services/types.d.ts +3 -0
  60. package/dist/services/util/notification.d.ts +5 -0
  61. package/dist/services/util/post.d.ts +6 -6
  62. package/dist/util/debug.d.ts +1 -1
  63. package/dist/util/retry.d.ts +1 -6
  64. package/package.json +11 -11
  65. package/src/api/app/bsky/feed/getActorFeeds.ts +2 -1
  66. package/src/api/app/bsky/feed/getActorLikes.ts +1 -3
  67. package/src/api/app/bsky/feed/getAuthorFeed.ts +1 -3
  68. package/src/api/app/bsky/feed/getFeed.ts +9 -9
  69. package/src/api/app/bsky/feed/getFeedGenerator.ts +3 -0
  70. package/src/api/app/bsky/feed/getFeedGenerators.ts +2 -1
  71. package/src/api/app/bsky/feed/getListFeed.ts +1 -3
  72. package/src/api/app/bsky/feed/getPostThread.ts +15 -54
  73. package/src/api/app/bsky/feed/getPosts.ts +21 -18
  74. package/src/api/app/bsky/feed/getSuggestedFeeds.ts +2 -1
  75. package/src/api/app/bsky/feed/getTimeline.ts +1 -3
  76. package/src/api/app/bsky/feed/searchPosts.ts +20 -17
  77. package/src/api/app/bsky/graph/getList.ts +6 -3
  78. package/src/api/app/bsky/graph/getListBlocks.ts +3 -2
  79. package/src/api/app/bsky/graph/getListMutes.ts +2 -1
  80. package/src/api/app/bsky/graph/getLists.ts +2 -1
  81. package/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +3 -1
  82. package/src/api/blob-resolver.ts +6 -11
  83. package/src/api/com/atproto/admin/emitModerationEvent.ts +220 -0
  84. package/src/api/com/atproto/admin/{getModerationActions.ts → getModerationEvent.ts} +5 -11
  85. package/src/api/com/atproto/admin/getRecord.ts +1 -0
  86. package/src/api/com/atproto/admin/{getModerationReports.ts → queryModerationEvents.ts} +13 -16
  87. package/src/api/com/atproto/admin/queryModerationStatuses.ts +55 -0
  88. package/src/api/com/atproto/moderation/createReport.ts +9 -7
  89. package/src/api/com/atproto/moderation/util.ts +38 -20
  90. package/src/api/index.ts +8 -14
  91. package/src/auth.ts +29 -21
  92. package/src/auto-moderator/index.ts +26 -19
  93. package/src/cache/read-through.ts +151 -0
  94. package/src/config.ts +90 -1
  95. package/src/context.ts +11 -7
  96. package/src/daemon/config.ts +60 -0
  97. package/src/daemon/context.ts +27 -0
  98. package/src/daemon/index.ts +78 -0
  99. package/src/daemon/logger.ts +6 -0
  100. package/src/daemon/notifications.ts +54 -0
  101. package/src/daemon/services.ts +22 -0
  102. package/src/db/database-schema.ts +0 -2
  103. package/src/db/migrations/20231003T202833377Z-create-moderation-subject-status.ts +123 -0
  104. package/src/db/migrations/20231205T000257238Z-remove-did-cache.ts +14 -0
  105. package/src/db/migrations/index.ts +2 -0
  106. package/src/db/pagination.ts +26 -3
  107. package/src/db/{periodic-moderation-action-reversal.ts → periodic-moderation-event-reversal.ts} +50 -46
  108. package/src/db/tables/moderation.ts +35 -52
  109. package/src/did-cache.ts +33 -56
  110. package/src/feed-gen/bsky-team.ts +1 -1
  111. package/src/feed-gen/hot-classic.ts +1 -1
  112. package/src/feed-gen/index.ts +0 -4
  113. package/src/feed-gen/mutuals.ts +6 -2
  114. package/src/feed-gen/types.ts +1 -1
  115. package/src/index.ts +57 -17
  116. package/src/indexer/context.ts +5 -0
  117. package/src/indexer/index.ts +10 -7
  118. package/src/lexicon/index.ts +80 -67
  119. package/src/lexicon/lexicons.ts +698 -507
  120. package/src/lexicon/types/app/bsky/feed/defs.ts +1 -18
  121. package/src/lexicon/types/app/bsky/graph/defs.ts +1 -0
  122. package/src/lexicon/types/com/atproto/admin/defs.ts +276 -84
  123. package/src/lexicon/types/com/atproto/admin/{resolveModerationReports.ts → deleteAccount.ts} +2 -13
  124. package/src/lexicon/types/com/atproto/admin/{takeModerationAction.ts → emitModerationEvent.ts} +13 -11
  125. package/src/lexicon/types/com/atproto/admin/{getModerationReport.ts → getModerationEvent.ts} +1 -1
  126. package/src/lexicon/types/com/atproto/admin/{getModerationActions.ts → queryModerationEvents.ts} +8 -1
  127. package/src/lexicon/types/com/atproto/admin/{getModerationReports.ts → queryModerationStatuses.ts} +21 -14
  128. package/src/lexicon/types/com/atproto/admin/sendEmail.ts +1 -0
  129. package/src/lexicon/types/com/atproto/{admin/getModerationAction.ts → temp/importRepo.ts} +11 -7
  130. package/src/lexicon/types/com/atproto/temp/pushBlob.ts +39 -0
  131. package/src/lexicon/types/com/atproto/{admin/reverseModerationAction.ts → temp/transferAccount.ts} +18 -5
  132. package/src/logger.ts +2 -0
  133. package/src/migrate-moderation-data.ts +414 -0
  134. package/src/redis.ts +43 -3
  135. package/src/services/actor/index.ts +55 -7
  136. package/src/services/actor/views.ts +18 -21
  137. package/src/services/feed/index.ts +52 -19
  138. package/src/services/feed/util.ts +47 -19
  139. package/src/services/feed/views.ts +87 -13
  140. package/src/services/graph/index.ts +21 -3
  141. package/src/services/graph/types.ts +1 -0
  142. package/src/services/index.ts +14 -14
  143. package/src/services/indexing/index.ts +7 -10
  144. package/src/services/indexing/plugins/block.ts +2 -3
  145. package/src/services/indexing/plugins/feed-generator.ts +2 -3
  146. package/src/services/indexing/plugins/follow.ts +2 -3
  147. package/src/services/indexing/plugins/like.ts +2 -3
  148. package/src/services/indexing/plugins/list-block.ts +2 -3
  149. package/src/services/indexing/plugins/list-item.ts +2 -3
  150. package/src/services/indexing/plugins/list.ts +2 -3
  151. package/src/services/indexing/plugins/post.ts +16 -4
  152. package/src/services/indexing/plugins/repost.ts +2 -3
  153. package/src/services/indexing/plugins/thread-gate.ts +2 -3
  154. package/src/services/label/index.ts +68 -25
  155. package/src/services/moderation/index.ts +380 -395
  156. package/src/services/moderation/pagination.ts +96 -0
  157. package/src/services/moderation/status.ts +241 -0
  158. package/src/services/moderation/types.ts +49 -0
  159. package/src/services/moderation/views.ts +278 -329
  160. package/src/services/types.ts +4 -0
  161. package/src/services/util/notification.ts +70 -0
  162. package/src/util/debug.ts +2 -2
  163. package/src/util/retry.ts +1 -44
  164. package/tests/__snapshots__/feed-generation.test.ts.snap +322 -6
  165. package/tests/__snapshots__/indexing.test.ts.snap +0 -6
  166. package/tests/admin/__snapshots__/get-record.test.ts.snap +30 -132
  167. package/tests/admin/__snapshots__/get-repo.test.ts.snap +14 -60
  168. package/tests/admin/__snapshots__/moderation-events.test.ts.snap +146 -0
  169. package/tests/admin/__snapshots__/moderation-statuses.test.ts.snap +64 -0
  170. package/tests/admin/__snapshots__/moderation.test.ts.snap +0 -125
  171. package/tests/admin/get-record.test.ts +5 -9
  172. package/tests/admin/get-repo.test.ts +10 -12
  173. package/tests/admin/moderation-events.test.ts +221 -0
  174. package/tests/admin/moderation-statuses.test.ts +145 -0
  175. package/tests/admin/moderation.test.ts +512 -860
  176. package/tests/admin/repo-search.test.ts +3 -3
  177. package/tests/algos/hot-classic.test.ts +1 -2
  178. package/tests/auth.test.ts +1 -1
  179. package/tests/auto-moderator/fuzzy-matcher.test.ts +2 -1
  180. package/tests/auto-moderator/labeler.test.ts +19 -20
  181. package/tests/auto-moderator/takedowns.test.ts +61 -28
  182. package/tests/blob-resolver.test.ts +4 -2
  183. package/tests/daemon.test.ts +191 -0
  184. package/tests/did-cache.test.ts +20 -5
  185. package/tests/feed-generation.test.ts +57 -9
  186. package/tests/handle-invalidation.test.ts +1 -5
  187. package/tests/indexing.test.ts +20 -13
  188. package/tests/redis-cache.test.ts +231 -0
  189. package/tests/seeds/basic.ts +3 -0
  190. package/tests/subscription/repo.test.ts +4 -7
  191. package/tests/views/__snapshots__/block-lists.test.ts.snap +3 -9
  192. package/tests/views/__snapshots__/blocks.test.ts.snap +0 -9
  193. package/tests/views/__snapshots__/mute-lists.test.ts.snap +5 -5
  194. package/tests/views/__snapshots__/mutes.test.ts.snap +0 -3
  195. package/tests/views/__snapshots__/thread.test.ts.snap +0 -30
  196. package/tests/views/actor-search.test.ts +2 -3
  197. package/tests/views/author-feed.test.ts +42 -36
  198. package/tests/views/follows.test.ts +40 -35
  199. package/tests/views/list-feed.test.ts +17 -9
  200. package/tests/views/notifications.test.ts +13 -9
  201. package/tests/views/profile.test.ts +20 -19
  202. package/tests/views/thread.test.ts +117 -94
  203. package/tests/views/threadgating.test.ts +89 -19
  204. package/tests/views/timeline.test.ts +21 -13
  205. package/dist/api/com/atproto/admin/resolveModerationReports.d.ts +0 -3
  206. package/dist/api/com/atproto/admin/reverseModerationAction.d.ts +0 -3
  207. package/dist/api/com/atproto/admin/takeModerationAction.d.ts +0 -3
  208. package/dist/db/tables/did-cache.d.ts +0 -10
  209. package/dist/feed-gen/best-of-follows.d.ts +0 -29
  210. package/dist/feed-gen/whats-hot.d.ts +0 -29
  211. package/dist/feed-gen/with-friends.d.ts +0 -3
  212. package/dist/label-cache.d.ts +0 -19
  213. package/src/api/com/atproto/admin/getModerationAction.ts +0 -44
  214. package/src/api/com/atproto/admin/getModerationReport.ts +0 -43
  215. package/src/api/com/atproto/admin/resolveModerationReports.ts +0 -24
  216. package/src/api/com/atproto/admin/reverseModerationAction.ts +0 -115
  217. package/src/api/com/atproto/admin/takeModerationAction.ts +0 -156
  218. package/src/db/tables/did-cache.ts +0 -13
  219. package/src/feed-gen/best-of-follows.ts +0 -74
  220. package/src/feed-gen/whats-hot.ts +0 -101
  221. package/src/feed-gen/with-friends.ts +0 -39
  222. package/src/label-cache.ts +0 -90
  223. package/tests/admin/__snapshots__/get-moderation-action.test.ts.snap +0 -172
  224. package/tests/admin/__snapshots__/get-moderation-actions.test.ts.snap +0 -178
  225. package/tests/admin/__snapshots__/get-moderation-report.test.ts.snap +0 -177
  226. package/tests/admin/__snapshots__/get-moderation-reports.test.ts.snap +0 -307
  227. package/tests/admin/get-moderation-action.test.ts +0 -100
  228. package/tests/admin/get-moderation-actions.test.ts +0 -164
  229. package/tests/admin/get-moderation-report.test.ts +0 -100
  230. package/tests/admin/get-moderation-reports.test.ts +0 -332
  231. package/tests/algos/whats-hot.test.ts +0 -118
  232. package/tests/algos/with-friends.test.ts +0 -145
  233. /package/dist/api/com/atproto/admin/{getModerationAction.d.ts → emitModerationEvent.d.ts} +0 -0
  234. /package/dist/api/com/atproto/admin/{getModerationActions.d.ts → getModerationEvent.d.ts} +0 -0
  235. /package/dist/api/com/atproto/admin/{getModerationReport.d.ts → queryModerationEvents.d.ts} +0 -0
  236. /package/dist/api/com/atproto/admin/{getModerationReports.d.ts → queryModerationStatuses.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { Selectable } from 'kysely'
1
+ import { sql } from 'kysely'
2
2
  import { ArrayEl } from '@atproto/common'
3
3
  import { AtUri } from '@atproto/syntax'
4
4
  import { INVALID_HANDLE } from '@atproto/syntax'
@@ -6,22 +6,25 @@ import { BlobRef, jsonStringToLex } from '@atproto/lexicon'
6
6
  import { Database } from '../../db'
7
7
  import { Actor } from '../../db/tables/actor'
8
8
  import { Record as RecordRow } from '../../db/tables/record'
9
- import { ModerationAction } from '../../db/tables/moderation'
10
9
  import {
10
+ ModEventView,
11
11
  RepoView,
12
12
  RepoViewDetail,
13
13
  RecordView,
14
14
  RecordViewDetail,
15
- ActionView,
16
- ActionViewDetail,
17
- ReportView,
18
15
  ReportViewDetail,
19
16
  BlobView,
17
+ SubjectStatusView,
18
+ ModEventViewDetail,
20
19
  } from '../../lexicon/types/com/atproto/admin/defs'
21
20
  import { OutputSchema as ReportOutput } from '../../lexicon/types/com/atproto/moderation/createReport'
22
21
  import { Label } from '../../lexicon/types/com/atproto/label/defs'
23
- import { ModerationReportRowWithHandle } from '.'
22
+ import {
23
+ ModerationEventRowWithHandle,
24
+ ModerationSubjectStatusRowWithHandle,
25
+ } from './types'
24
26
  import { getSelfLabels } from '../label'
27
+ import { REASONOTHER } from '../../lexicon/types/com/atproto/moderation/defs'
25
28
 
26
29
  export class ModerationViews {
27
30
  constructor(private db: Database) {}
@@ -34,7 +37,7 @@ export class ModerationViews {
34
37
  const results = Array.isArray(result) ? result : [result]
35
38
  if (results.length === 0) return []
36
39
 
37
- const [info, actionResults] = await Promise.all([
40
+ const [info, subjectStatuses] = await Promise.all([
38
41
  await this.db.db
39
42
  .selectFrom('actor')
40
43
  .leftJoin('profile', 'profile.creator', 'actor.did')
@@ -50,31 +53,21 @@ export class ModerationViews {
50
53
  )
51
54
  .select(['actor.did as did', 'profile_record.json as profileJson'])
52
55
  .execute(),
53
- this.db.db
54
- .selectFrom('moderation_action')
55
- .where('reversedAt', 'is', null)
56
- .where('subjectType', '=', 'com.atproto.admin.defs#repoRef')
57
- .where(
58
- 'subjectDid',
59
- 'in',
60
- results.map((r) => r.did),
61
- )
62
- .select(['id', 'action', 'durationInHours', 'subjectDid'])
63
- .execute(),
56
+ this.getSubjectStatus(results.map((r) => ({ did: r.did }))),
64
57
  ])
65
58
 
66
59
  const infoByDid = info.reduce(
67
60
  (acc, cur) => Object.assign(acc, { [cur.did]: cur }),
68
61
  {} as Record<string, ArrayEl<typeof info>>,
69
62
  )
70
- const actionByDid = actionResults.reduce(
71
- (acc, cur) => Object.assign(acc, { [cur.subjectDid ?? '']: cur }),
72
- {} as Record<string, ArrayEl<typeof actionResults>>,
63
+ const subjectStatusByDid = subjectStatuses.reduce(
64
+ (acc, cur) =>
65
+ Object.assign(acc, { [cur.did ?? '']: this.subjectStatus(cur) }),
66
+ {},
73
67
  )
74
68
 
75
69
  const views = results.map((r) => {
76
70
  const { profileJson } = infoByDid[r.did] ?? {}
77
- const action = actionByDid[r.did]
78
71
  const relatedRecords: object[] = []
79
72
  if (profileJson) {
80
73
  relatedRecords.push(
@@ -88,49 +81,148 @@ export class ModerationViews {
88
81
  relatedRecords,
89
82
  indexedAt: r.indexedAt,
90
83
  moderation: {
91
- currentAction: action
84
+ subjectStatus: subjectStatusByDid[r.did] ?? undefined,
85
+ },
86
+ }
87
+ })
88
+
89
+ return Array.isArray(result) ? views : views[0]
90
+ }
91
+ event(result: EventResult): Promise<ModEventView>
92
+ event(result: EventResult[]): Promise<ModEventView[]>
93
+ async event(
94
+ result: EventResult | EventResult[],
95
+ ): Promise<ModEventView | ModEventView[]> {
96
+ const results = Array.isArray(result) ? result : [result]
97
+ if (results.length === 0) return []
98
+
99
+ const views = results.map((res) => {
100
+ const eventView: ModEventView = {
101
+ id: res.id,
102
+ event: {
103
+ $type: res.action,
104
+ comment: res.comment ?? undefined,
105
+ },
106
+ subject:
107
+ res.subjectType === 'com.atproto.admin.defs#repoRef'
92
108
  ? {
93
- id: action.id,
94
- action: action.action,
95
- durationInHours: action.durationInHours ?? undefined,
109
+ $type: 'com.atproto.admin.defs#repoRef',
110
+ did: res.subjectDid,
96
111
  }
97
- : undefined,
98
- },
112
+ : {
113
+ $type: 'com.atproto.repo.strongRef',
114
+ uri: res.subjectUri,
115
+ cid: res.subjectCid,
116
+ },
117
+ subjectBlobCids: [],
118
+ createdBy: res.createdBy,
119
+ createdAt: res.createdAt,
120
+ subjectHandle: res.subjectHandle ?? undefined,
121
+ creatorHandle: res.creatorHandle ?? undefined,
122
+ }
123
+
124
+ if (
125
+ [
126
+ 'com.atproto.admin.defs#modEventTakedown',
127
+ 'com.atproto.admin.defs#modEventMute',
128
+ ].includes(res.action)
129
+ ) {
130
+ eventView.event = {
131
+ ...eventView.event,
132
+ durationInHours: res.durationInHours ?? undefined,
133
+ }
134
+ }
135
+
136
+ if (res.action === 'com.atproto.admin.defs#modEventLabel') {
137
+ eventView.event = {
138
+ ...eventView.event,
139
+ createLabelVals: res.createLabelVals?.length
140
+ ? res.createLabelVals.split(' ')
141
+ : [],
142
+ negateLabelVals: res.negateLabelVals?.length
143
+ ? res.negateLabelVals.split(' ')
144
+ : [],
145
+ }
99
146
  }
147
+
148
+ // This is for legacy data only, for new events, these types of events won't have labels attached
149
+ if (
150
+ [
151
+ 'com.atproto.admin.defs#modEventAcknowledge',
152
+ 'com.atproto.admin.defs#modEventTakedown',
153
+ 'com.atproto.admin.defs#modEventEscalate',
154
+ ].includes(res.action)
155
+ ) {
156
+ if (res.createLabelVals?.length) {
157
+ eventView.event = {
158
+ ...eventView.event,
159
+ createLabelVals: res.createLabelVals.split(' '),
160
+ }
161
+ }
162
+
163
+ if (res.negateLabelVals?.length) {
164
+ eventView.event = {
165
+ ...eventView.event,
166
+ negateLabelVals: res.negateLabelVals.split(' '),
167
+ }
168
+ }
169
+ }
170
+
171
+ if (res.action === 'com.atproto.admin.defs#modEventReport') {
172
+ eventView.event = {
173
+ ...eventView.event,
174
+ reportType: res.meta?.reportType ?? undefined,
175
+ }
176
+ }
177
+
178
+ if (res.action === 'com.atproto.admin.defs#modEventEmail') {
179
+ eventView.event = {
180
+ ...eventView.event,
181
+ subjectLine: res.meta?.subjectLine ?? '',
182
+ }
183
+ }
184
+
185
+ if (
186
+ res.action === 'com.atproto.admin.defs#modEventComment' &&
187
+ res.meta?.sticky
188
+ ) {
189
+ eventView.event.sticky = true
190
+ }
191
+
192
+ return eventView
100
193
  })
101
194
 
102
195
  return Array.isArray(result) ? views : views[0]
103
196
  }
104
197
 
105
- async repoDetail(result: RepoResult): Promise<RepoViewDetail> {
106
- const repo = await this.repo(result)
107
- const [reportResults, actionResults] = await Promise.all([
108
- this.db.db
109
- .selectFrom('moderation_report')
110
- .where('subjectType', '=', 'com.atproto.admin.defs#repoRef')
111
- .where('subjectDid', '=', repo.did)
112
- .orderBy('id', 'desc')
113
- .selectAll()
114
- .execute(),
115
- this.db.db
116
- .selectFrom('moderation_action')
117
- .where('subjectType', '=', 'com.atproto.admin.defs#repoRef')
118
- .where('subjectDid', '=', repo.did)
119
- .orderBy('id', 'desc')
120
- .selectAll()
121
- .execute(),
198
+ async eventDetail(result: EventResult): Promise<ModEventViewDetail> {
199
+ const [event, subject] = await Promise.all([
200
+ this.event(result),
201
+ this.subject(result),
122
202
  ])
123
- const [reports, actions, labels] = await Promise.all([
124
- this.report(reportResults),
125
- this.action(actionResults),
126
- this.labels(repo.did),
203
+ const allBlobs = findBlobRefs(subject.value)
204
+ const subjectBlobs = await this.blob(
205
+ allBlobs.filter((blob) =>
206
+ event.subjectBlobCids.includes(blob.ref.toString()),
207
+ ),
208
+ )
209
+ return {
210
+ ...event,
211
+ subject,
212
+ subjectBlobs,
213
+ }
214
+ }
215
+
216
+ async repoDetail(result: RepoResult): Promise<RepoViewDetail> {
217
+ const [repo, labels] = await Promise.all([
218
+ this.repo(result),
219
+ this.labels(result.did),
127
220
  ])
221
+
128
222
  return {
129
223
  ...repo,
130
224
  moderation: {
131
225
  ...repo.moderation,
132
- reports,
133
- actions,
134
226
  },
135
227
  labels,
136
228
  }
@@ -144,7 +236,7 @@ export class ModerationViews {
144
236
  const results = Array.isArray(result) ? result : [result]
145
237
  if (results.length === 0) return []
146
238
 
147
- const [repoResults, actionResults] = await Promise.all([
239
+ const [repoResults, subjectStatuses] = await Promise.all([
148
240
  this.db.db
149
241
  .selectFrom('actor')
150
242
  .where(
@@ -154,17 +246,7 @@ export class ModerationViews {
154
246
  )
155
247
  .selectAll()
156
248
  .execute(),
157
- this.db.db
158
- .selectFrom('moderation_action')
159
- .where('reversedAt', 'is', null)
160
- .where('subjectType', '=', 'com.atproto.repo.strongRef')
161
- .where(
162
- 'subjectUri',
163
- 'in',
164
- results.map((r) => r.uri),
165
- )
166
- .select(['id', 'action', 'durationInHours', 'subjectUri'])
167
- .execute(),
249
+ this.getSubjectStatus(results.map((r) => didAndRecordPathFromUri(r.uri))),
168
250
  ])
169
251
  const repos = await this.repo(repoResults)
170
252
 
@@ -172,14 +254,18 @@ export class ModerationViews {
172
254
  (acc, cur) => Object.assign(acc, { [cur.did]: cur }),
173
255
  {} as Record<string, ArrayEl<typeof repos>>,
174
256
  )
175
- const actionByUri = actionResults.reduce(
176
- (acc, cur) => Object.assign(acc, { [cur.subjectUri ?? '']: cur }),
177
- {} as Record<string, ArrayEl<typeof actionResults>>,
257
+ const subjectStatusByUri = subjectStatuses.reduce(
258
+ (acc, cur) =>
259
+ Object.assign(acc, {
260
+ [`${cur.did}/${cur.recordPath}` ?? '']: this.subjectStatus(cur),
261
+ }),
262
+ {},
178
263
  )
179
264
 
180
265
  const views = results.map((res) => {
181
266
  const repo = reposByDid[didFromUri(res.uri)]
182
- const action = actionByUri[res.uri]
267
+ const { did, recordPath } = didAndRecordPathFromUri(res.uri)
268
+ const subjectStatus = subjectStatusByUri[`${did}/${recordPath}`]
183
269
  if (!repo) throw new Error(`Record repo is missing: ${res.uri}`)
184
270
  const value = jsonStringToLex(res.json) as Record<string, unknown>
185
271
  return {
@@ -190,13 +276,7 @@ export class ModerationViews {
190
276
  indexedAt: res.indexedAt,
191
277
  repo,
192
278
  moderation: {
193
- currentAction: action
194
- ? {
195
- id: action.id,
196
- action: action.action,
197
- durationInHours: action.durationInHours ?? undefined,
198
- }
199
- : undefined,
279
+ subjectStatus,
200
280
  },
201
281
  }
202
282
  })
@@ -205,29 +285,17 @@ export class ModerationViews {
205
285
  }
206
286
 
207
287
  async recordDetail(result: RecordResult): Promise<RecordViewDetail> {
208
- const [record, reportResults, actionResults] = await Promise.all([
288
+ const [record, subjectStatusResult] = await Promise.all([
209
289
  this.record(result),
210
- this.db.db
211
- .selectFrom('moderation_report')
212
- .where('subjectType', '=', 'com.atproto.repo.strongRef')
213
- .where('subjectUri', '=', result.uri)
214
- .leftJoin('actor', 'actor.did', 'moderation_report.subjectDid')
215
- .orderBy('id', 'desc')
216
- .selectAll()
217
- .execute(),
218
- this.db.db
219
- .selectFrom('moderation_action')
220
- .where('subjectType', '=', 'com.atproto.repo.strongRef')
221
- .where('subjectUri', '=', result.uri)
222
- .orderBy('id', 'desc')
223
- .selectAll()
224
- .execute(),
290
+ this.getSubjectStatus(didAndRecordPathFromUri(result.uri)),
225
291
  ])
226
- const [reports, actions, blobs, labels] = await Promise.all([
227
- this.report(reportResults),
228
- this.action(actionResults),
292
+
293
+ const [blobs, labels, subjectStatus] = await Promise.all([
229
294
  this.blob(findBlobRefs(record.value)),
230
295
  this.labels(record.uri),
296
+ subjectStatusResult?.length
297
+ ? this.subjectStatus(subjectStatusResult[0])
298
+ : Promise.resolve(undefined),
231
299
  ])
232
300
  const selfLabels = getSelfLabels({
233
301
  uri: result.uri,
@@ -239,196 +307,22 @@ export class ModerationViews {
239
307
  blobs,
240
308
  moderation: {
241
309
  ...record.moderation,
242
- reports,
243
- actions,
310
+ subjectStatus,
244
311
  },
245
312
  labels: [...labels, ...selfLabels],
246
313
  }
247
314
  }
248
-
249
- action(result: ActionResult): Promise<ActionView>
250
- action(result: ActionResult[]): Promise<ActionView[]>
251
- async action(
252
- result: ActionResult | ActionResult[],
253
- ): Promise<ActionView | ActionView[]> {
254
- const results = Array.isArray(result) ? result : [result]
255
- if (results.length === 0) return []
256
-
257
- const [resolutions, subjectBlobResults] = await Promise.all([
258
- this.db.db
259
- .selectFrom('moderation_report_resolution')
260
- .select(['reportId as id', 'actionId'])
261
- .where(
262
- 'actionId',
263
- 'in',
264
- results.map((r) => r.id),
265
- )
266
- .orderBy('id', 'desc')
267
- .execute(),
268
- await this.db.db
269
- .selectFrom('moderation_action_subject_blob')
270
- .selectAll()
271
- .where(
272
- 'actionId',
273
- 'in',
274
- results.map((r) => r.id),
275
- )
276
- .execute(),
277
- ])
278
-
279
- const reportIdsByActionId = resolutions.reduce((acc, cur) => {
280
- acc[cur.actionId] ??= []
281
- acc[cur.actionId].push(cur.id)
282
- return acc
283
- }, {} as Record<string, number[]>)
284
- const subjectBlobCidsByActionId = subjectBlobResults.reduce((acc, cur) => {
285
- acc[cur.actionId] ??= []
286
- acc[cur.actionId].push(cur.cid)
287
- return acc
288
- }, {} as Record<string, string[]>)
289
-
290
- const views = results.map((res) => ({
291
- id: res.id,
292
- action: res.action,
293
- durationInHours: res.durationInHours ?? undefined,
294
- subject:
295
- res.subjectType === 'com.atproto.admin.defs#repoRef'
296
- ? {
297
- $type: 'com.atproto.admin.defs#repoRef',
298
- did: res.subjectDid,
299
- }
300
- : {
301
- $type: 'com.atproto.repo.strongRef',
302
- uri: res.subjectUri,
303
- cid: res.subjectCid,
304
- },
305
- subjectBlobCids: subjectBlobCidsByActionId[res.id] ?? [],
306
- reason: res.reason,
307
- createdAt: res.createdAt,
308
- createdBy: res.createdBy,
309
- createLabelVals:
310
- res.createLabelVals && res.createLabelVals.length > 0
311
- ? res.createLabelVals.split(' ')
312
- : undefined,
313
- negateLabelVals:
314
- res.negateLabelVals && res.negateLabelVals.length > 0
315
- ? res.negateLabelVals.split(' ')
316
- : undefined,
317
- reversal:
318
- res.reversedAt !== null &&
319
- res.reversedBy !== null &&
320
- res.reversedReason !== null
321
- ? {
322
- createdAt: res.reversedAt,
323
- createdBy: res.reversedBy,
324
- reason: res.reversedReason,
325
- }
326
- : undefined,
327
- resolvedReportIds: reportIdsByActionId[res.id] ?? [],
328
- }))
329
-
330
- return Array.isArray(result) ? views : views[0]
331
- }
332
-
333
- async actionDetail(result: ActionResult): Promise<ActionViewDetail> {
334
- const action = await this.action(result)
335
- const reportResults = action.resolvedReportIds.length
336
- ? await this.db.db
337
- .selectFrom('moderation_report')
338
- .where('id', 'in', action.resolvedReportIds)
339
- .orderBy('id', 'desc')
340
- .selectAll()
341
- .execute()
342
- : []
343
- const [subject, resolvedReports] = await Promise.all([
344
- this.subject(result),
345
- this.report(reportResults),
346
- ])
347
- const allBlobs = findBlobRefs(subject.value)
348
- const subjectBlobs = await this.blob(
349
- allBlobs.filter((blob) =>
350
- action.subjectBlobCids.includes(blob.ref.toString()),
351
- ),
352
- )
353
- return {
354
- id: action.id,
355
- action: action.action,
356
- durationInHours: action.durationInHours,
357
- subject,
358
- subjectBlobs,
359
- createLabelVals: action.createLabelVals,
360
- negateLabelVals: action.negateLabelVals,
361
- reason: action.reason,
362
- createdAt: action.createdAt,
363
- createdBy: action.createdBy,
364
- reversal: action.reversal,
365
- resolvedReports,
366
- }
367
- }
368
-
369
- report(result: ReportResult): Promise<ReportView>
370
- report(result: ReportResult[]): Promise<ReportView[]>
371
- async report(
372
- result: ReportResult | ReportResult[],
373
- ): Promise<ReportView | ReportView[]> {
374
- const results = Array.isArray(result) ? result : [result]
375
- if (results.length === 0) return []
376
-
377
- const resolutions = await this.db.db
378
- .selectFrom('moderation_report_resolution')
379
- .select(['actionId as id', 'reportId'])
380
- .where(
381
- 'reportId',
382
- 'in',
383
- results.map((r) => r.id),
384
- )
385
- .orderBy('id', 'desc')
386
- .execute()
387
-
388
- const actionIdsByReportId = resolutions.reduce((acc, cur) => {
389
- acc[cur.reportId] ??= []
390
- acc[cur.reportId].push(cur.id)
391
- return acc
392
- }, {} as Record<string, number[]>)
393
-
394
- const views: ReportView[] = results.map((res) => {
395
- const decoratedView: ReportView = {
396
- id: res.id,
397
- createdAt: res.createdAt,
398
- reasonType: res.reasonType,
399
- reason: res.reason ?? undefined,
400
- reportedBy: res.reportedByDid,
401
- subject:
402
- res.subjectType === 'com.atproto.admin.defs#repoRef'
403
- ? {
404
- $type: 'com.atproto.admin.defs#repoRef',
405
- did: res.subjectDid,
406
- }
407
- : {
408
- $type: 'com.atproto.repo.strongRef',
409
- uri: res.subjectUri,
410
- cid: res.subjectCid,
411
- },
412
- resolvedByActionIds: actionIdsByReportId[res.id] ?? [],
413
- }
414
-
415
- if (res.handle) {
416
- decoratedView.subjectRepoHandle = res.handle
417
- }
418
-
419
- return decoratedView
420
- })
421
-
422
- return Array.isArray(result) ? views : views[0]
423
- }
424
-
425
315
  reportPublic(report: ReportResult): ReportOutput {
426
316
  return {
427
317
  id: report.id,
428
318
  createdAt: report.createdAt,
429
- reasonType: report.reasonType,
430
- reason: report.reason ?? undefined,
431
- reportedBy: report.reportedByDid,
319
+ // Ideally, we would never have a report entry that does not have a reasonType but at the schema level
320
+ // we are not guarantying that so in whatever case, if we end up with such entries, default to 'other'
321
+ reasonType: report.meta?.reportType
322
+ ? (report.meta?.reportType as string)
323
+ : REASONOTHER,
324
+ reason: report.comment ?? undefined,
325
+ reportedBy: report.createdBy,
432
326
  subject:
433
327
  report.subjectType === 'com.atproto.admin.defs#repoRef'
434
328
  ? {
@@ -442,32 +336,6 @@ export class ModerationViews {
442
336
  },
443
337
  }
444
338
  }
445
-
446
- async reportDetail(result: ReportResult): Promise<ReportViewDetail> {
447
- const report = await this.report(result)
448
- const actionResults = report.resolvedByActionIds.length
449
- ? await this.db.db
450
- .selectFrom('moderation_action')
451
- .where('id', 'in', report.resolvedByActionIds)
452
- .orderBy('id', 'desc')
453
- .selectAll()
454
- .execute()
455
- : []
456
- const [subject, resolvedByActions] = await Promise.all([
457
- this.subject(result),
458
- this.action(actionResults),
459
- ])
460
- return {
461
- id: report.id,
462
- createdAt: report.createdAt,
463
- reasonType: report.reasonType,
464
- reason: report.reason ?? undefined,
465
- reportedBy: report.reportedBy,
466
- subject,
467
- resolvedByActions,
468
- }
469
- }
470
-
471
339
  // Partial view for subjects
472
340
 
473
341
  async subject(result: SubjectResult): Promise<SubjectView> {
@@ -511,44 +379,35 @@ export class ModerationViews {
511
379
 
512
380
  async blob(blobs: BlobRef[]): Promise<BlobView[]> {
513
381
  if (!blobs.length) return []
514
- const actionResults = await this.db.db
515
- .selectFrom('moderation_action')
516
- .where('reversedAt', 'is', null)
517
- .innerJoin(
518
- 'moderation_action_subject_blob as subject_blob',
519
- 'subject_blob.actionId',
520
- 'moderation_action.id',
521
- )
382
+ const { ref } = this.db.db.dynamic
383
+ const modStatusResults = await this.db.db
384
+ .selectFrom('moderation_subject_status')
522
385
  .where(
523
- 'subject_blob.cid',
524
- 'in',
525
- blobs.map((blob) => blob.ref.toString()),
386
+ sql<string>`${ref(
387
+ 'moderation_subject_status.blobCids',
388
+ )} @> ${JSON.stringify(blobs.map((blob) => blob.ref.toString()))}`,
526
389
  )
527
- .select(['id', 'action', 'durationInHours', 'cid'])
528
- .execute()
529
- const actionByCid = actionResults.reduce(
530
- (acc, cur) => Object.assign(acc, { [cur.cid]: cur }),
531
- {} as Record<string, ArrayEl<typeof actionResults>>,
390
+ .selectAll()
391
+ .executeTakeFirst()
392
+ const statusByCid = (modStatusResults?.blobCids || [])?.reduce(
393
+ (acc, cur) => Object.assign(acc, { [cur]: modStatusResults }),
394
+ {},
532
395
  )
533
396
  // Intentionally missing details field, since we don't have any on appview.
534
397
  // We also don't know when the blob was created, so we use a canned creation time.
535
398
  const unknownTime = new Date(0).toISOString()
536
399
  return blobs.map((blob) => {
537
400
  const cid = blob.ref.toString()
538
- const action = actionByCid[cid]
401
+ const subjectStatus = statusByCid[cid]
402
+ ? this.subjectStatus(statusByCid[cid])
403
+ : undefined
539
404
  return {
540
405
  cid,
541
406
  mimeType: blob.mimeType,
542
407
  size: blob.size,
543
408
  createdAt: unknownTime,
544
409
  moderation: {
545
- currentAction: action
546
- ? {
547
- id: action.id,
548
- action: action.action,
549
- durationInHours: action.durationInHours ?? undefined,
550
- }
551
- : undefined,
410
+ subjectStatus,
552
411
  },
553
412
  }
554
413
  })
@@ -567,27 +426,117 @@ export class ModerationViews {
567
426
  neg: l.neg,
568
427
  }))
569
428
  }
429
+
430
+ async getSubjectStatus(
431
+ subject:
432
+ | { did: string; recordPath?: string }
433
+ | { did: string; recordPath?: string }[],
434
+ ): Promise<ModerationSubjectStatusRowWithHandle[]> {
435
+ const subjectFilters = Array.isArray(subject) ? subject : [subject]
436
+ const filterForSubject =
437
+ ({ did, recordPath }: { did: string; recordPath?: string }) =>
438
+ // TODO: Fix the typing here?
439
+ (clause: any) => {
440
+ clause = clause
441
+ .where('moderation_subject_status.did', '=', did)
442
+ .where('moderation_subject_status.recordPath', '=', recordPath || '')
443
+ return clause
444
+ }
445
+
446
+ const builder = this.db.db
447
+ .selectFrom('moderation_subject_status')
448
+ .leftJoin('actor', 'actor.did', 'moderation_subject_status.did')
449
+ .where((clause) => {
450
+ subjectFilters.forEach(({ did, recordPath }, i) => {
451
+ const applySubjectFilter = filterForSubject({ did, recordPath })
452
+ if (i === 0) {
453
+ clause = clause.where(applySubjectFilter)
454
+ } else {
455
+ clause = clause.orWhere(applySubjectFilter)
456
+ }
457
+ })
458
+
459
+ return clause
460
+ })
461
+ .selectAll('moderation_subject_status')
462
+ .select('actor.handle as handle')
463
+
464
+ return builder.execute()
465
+ }
466
+
467
+ subjectStatus(result: ModerationSubjectStatusRowWithHandle): SubjectStatusView
468
+ subjectStatus(
469
+ result: ModerationSubjectStatusRowWithHandle[],
470
+ ): SubjectStatusView[]
471
+ subjectStatus(
472
+ result:
473
+ | ModerationSubjectStatusRowWithHandle
474
+ | ModerationSubjectStatusRowWithHandle[],
475
+ ): SubjectStatusView | SubjectStatusView[] {
476
+ const results = Array.isArray(result) ? result : [result]
477
+ if (results.length === 0) return []
478
+
479
+ const decoratedSubjectStatuses = results.map((subjectStatus) => ({
480
+ id: subjectStatus.id,
481
+ reviewState: subjectStatus.reviewState,
482
+ createdAt: subjectStatus.createdAt,
483
+ updatedAt: subjectStatus.updatedAt,
484
+ comment: subjectStatus.comment ?? undefined,
485
+ lastReviewedBy: subjectStatus.lastReviewedBy ?? undefined,
486
+ lastReviewedAt: subjectStatus.lastReviewedAt ?? undefined,
487
+ lastReportedAt: subjectStatus.lastReportedAt ?? undefined,
488
+ muteUntil: subjectStatus.muteUntil ?? undefined,
489
+ suspendUntil: subjectStatus.suspendUntil ?? undefined,
490
+ takendown: subjectStatus.takendown ?? undefined,
491
+ subjectRepoHandle: subjectStatus.handle ?? undefined,
492
+ subjectBlobCids: subjectStatus.blobCids || [],
493
+ subject: !subjectStatus.recordPath
494
+ ? {
495
+ $type: 'com.atproto.admin.defs#repoRef',
496
+ did: subjectStatus.did,
497
+ }
498
+ : {
499
+ $type: 'com.atproto.repo.strongRef',
500
+ uri: AtUri.make(
501
+ subjectStatus.did,
502
+ // Not too intuitive but the recordpath is basically <collection>/<rkey>
503
+ // which is what the last 2 params of .make() arguments are
504
+ ...subjectStatus.recordPath.split('/'),
505
+ ).toString(),
506
+ cid: subjectStatus.recordCid,
507
+ },
508
+ }))
509
+
510
+ return Array.isArray(result)
511
+ ? decoratedSubjectStatuses
512
+ : decoratedSubjectStatuses[0]
513
+ }
570
514
  }
571
515
 
572
516
  type RepoResult = Actor
573
517
 
574
- type ActionResult = Selectable<ModerationAction>
518
+ type EventResult = ModerationEventRowWithHandle
575
519
 
576
- type ReportResult = ModerationReportRowWithHandle
520
+ type ReportResult = ModerationEventRowWithHandle
577
521
 
578
522
  type RecordResult = RecordRow
579
523
 
580
524
  type SubjectResult = Pick<
581
- ActionResult & ReportResult,
525
+ EventResult & ReportResult,
582
526
  'id' | 'subjectType' | 'subjectDid' | 'subjectUri' | 'subjectCid'
583
527
  >
584
528
 
585
- type SubjectView = ActionViewDetail['subject'] & ReportViewDetail['subject']
529
+ type SubjectView = ModEventViewDetail['subject'] & ReportViewDetail['subject']
586
530
 
587
531
  function didFromUri(uri: string) {
588
532
  return new AtUri(uri).host
589
533
  }
590
534
 
535
+ function didAndRecordPathFromUri(uri: string) {
536
+ const atUri = new AtUri(uri)
537
+ return { did: atUri.host, recordPath: `${atUri.collection}/${atUri.rkey}` }
538
+ }
539
+
591
540
  function findBlobRefs(value: unknown, refs: BlobRef[] = []) {
592
541
  if (value instanceof BlobRef) {
593
542
  refs.push(value)