@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
@@ -2,24 +2,28 @@
2
2
  * GENERATED CODE - DO NOT MODIFY
3
3
  */
4
4
  import express from 'express'
5
+ import stream from 'stream'
5
6
  import { ValidationResult, BlobRef } from '@atproto/lexicon'
6
7
  import { lexicons } from '../../../../lexicons'
7
8
  import { isObj, hasProp } from '../../../../util'
8
9
  import { CID } from 'multiformats/cid'
9
10
  import { HandlerAuth } from '@atproto/xrpc-server'
10
- import * as ComAtprotoAdminDefs from './defs'
11
11
 
12
12
  export interface QueryParams {
13
- id: number
13
+ /** The DID of the repo. */
14
+ did: string
14
15
  }
15
16
 
16
- export type InputSchema = undefined
17
- export type OutputSchema = ComAtprotoAdminDefs.ActionViewDetail
18
- export type HandlerInput = undefined
17
+ export type InputSchema = string | Uint8Array
18
+
19
+ export interface HandlerInput {
20
+ encoding: 'application/vnd.ipld.car'
21
+ body: stream.Readable
22
+ }
19
23
 
20
24
  export interface HandlerSuccess {
21
- encoding: 'application/json'
22
- body: OutputSchema
25
+ encoding: 'text/plain'
26
+ body: Uint8Array | stream.Readable
23
27
  headers?: { [key: string]: string }
24
28
  }
25
29
 
@@ -0,0 +1,39 @@
1
+ /**
2
+ * GENERATED CODE - DO NOT MODIFY
3
+ */
4
+ import express from 'express'
5
+ import stream from 'stream'
6
+ import { ValidationResult, BlobRef } from '@atproto/lexicon'
7
+ import { lexicons } from '../../../../lexicons'
8
+ import { isObj, hasProp } from '../../../../util'
9
+ import { CID } from 'multiformats/cid'
10
+ import { HandlerAuth } from '@atproto/xrpc-server'
11
+
12
+ export interface QueryParams {
13
+ /** The DID of the repo. */
14
+ did: string
15
+ }
16
+
17
+ export type InputSchema = string | Uint8Array
18
+
19
+ export interface HandlerInput {
20
+ encoding: '*/*'
21
+ body: stream.Readable
22
+ }
23
+
24
+ export interface HandlerError {
25
+ status: number
26
+ message?: string
27
+ }
28
+
29
+ export type HandlerOutput = HandlerError | void
30
+ export type HandlerReqCtx<HA extends HandlerAuth = never> = {
31
+ auth: HA
32
+ params: QueryParams
33
+ input: HandlerInput
34
+ req: express.Request
35
+ res: express.Response
36
+ }
37
+ export type Handler<HA extends HandlerAuth = never> = (
38
+ ctx: HandlerReqCtx<HA>,
39
+ ) => Promise<HandlerOutput> | HandlerOutput
@@ -7,18 +7,23 @@ import { lexicons } from '../../../../lexicons'
7
7
  import { isObj, hasProp } from '../../../../util'
8
8
  import { CID } from 'multiformats/cid'
9
9
  import { HandlerAuth } from '@atproto/xrpc-server'
10
- import * as ComAtprotoAdminDefs from './defs'
11
10
 
12
11
  export interface QueryParams {}
13
12
 
14
13
  export interface InputSchema {
15
- id: number
16
- reason: string
17
- createdBy: string
14
+ handle: string
15
+ did: string
16
+ plcOp: {}
18
17
  [k: string]: unknown
19
18
  }
20
19
 
21
- export type OutputSchema = ComAtprotoAdminDefs.ActionView
20
+ export interface OutputSchema {
21
+ accessJwt: string
22
+ refreshJwt: string
23
+ handle: string
24
+ did: string
25
+ [k: string]: unknown
26
+ }
22
27
 
23
28
  export interface HandlerInput {
24
29
  encoding: 'application/json'
@@ -34,6 +39,14 @@ export interface HandlerSuccess {
34
39
  export interface HandlerError {
35
40
  status: number
36
41
  message?: string
42
+ error?:
43
+ | 'InvalidHandle'
44
+ | 'InvalidPassword'
45
+ | 'InvalidInviteCode'
46
+ | 'HandleNotAvailable'
47
+ | 'UnsupportedDomain'
48
+ | 'UnresolvableDid'
49
+ | 'IncompatibleDidDoc'
37
50
  }
38
51
 
39
52
  export type HandlerOutput = HandlerError | HandlerSuccess
package/src/logger.ts CHANGED
@@ -3,6 +3,8 @@ import { subsystemLogger } from '@atproto/common'
3
3
 
4
4
  export const dbLogger: ReturnType<typeof subsystemLogger> =
5
5
  subsystemLogger('bsky:db')
6
+ export const cacheLogger: ReturnType<typeof subsystemLogger> =
7
+ subsystemLogger('bsky:cache')
6
8
  export const subLogger: ReturnType<typeof subsystemLogger> =
7
9
  subsystemLogger('bsky:sub')
8
10
  export const labelerLogger: ReturnType<typeof subsystemLogger> =
@@ -0,0 +1,414 @@
1
+ import { sql } from 'kysely'
2
+ import { DatabaseCoordinator, PrimaryDatabase } from './index'
3
+ import { adjustModerationSubjectStatus } from './services/moderation/status'
4
+ import { ModerationEventRow } from './services/moderation/types'
5
+
6
+ type ModerationActionRow = Omit<ModerationEventRow, 'comment' | 'meta'> & {
7
+ reason: string | null
8
+ }
9
+
10
+ const getEnv = () => ({
11
+ DB_URL:
12
+ process.env.MODERATION_MIGRATION_DB_URL ||
13
+ 'postgresql://pg:password@127.0.0.1:5433/postgres',
14
+ DB_POOL_SIZE: Number(process.env.MODERATION_MIGRATION_DB_POOL_SIZE) || 10,
15
+ DB_SCHEMA: process.env.MODERATION_MIGRATION_DB_SCHEMA || 'bsky',
16
+ })
17
+
18
+ const countEntries = async (db: PrimaryDatabase) => {
19
+ const [allActions, allReports] = await Promise.all([
20
+ db.db
21
+ // @ts-ignore
22
+ .selectFrom('moderation_action')
23
+ // @ts-ignore
24
+ .select((eb) => eb.fn.count<number>('id').as('count'))
25
+ .executeTakeFirstOrThrow(),
26
+ db.db
27
+ // @ts-ignore
28
+ .selectFrom('moderation_report')
29
+ // @ts-ignore
30
+ .select((eb) => eb.fn.count<number>('id').as('count'))
31
+ .executeTakeFirstOrThrow(),
32
+ ])
33
+
34
+ return { reportsCount: allReports.count, actionsCount: allActions.count }
35
+ }
36
+
37
+ const countEvents = async (db: PrimaryDatabase) => {
38
+ const events = await db.db
39
+ .selectFrom('moderation_event')
40
+ .select((eb) => eb.fn.count<number>('id').as('count'))
41
+ .executeTakeFirstOrThrow()
42
+
43
+ return events.count
44
+ }
45
+
46
+ const getLatestReportLegacyRefId = async (db: PrimaryDatabase) => {
47
+ const events = await db.db
48
+ .selectFrom('moderation_event')
49
+ .select((eb) => eb.fn.max('legacyRefId').as('latestLegacyRefId'))
50
+ .where('action', '=', 'com.atproto.admin.defs#modEventReport')
51
+ .executeTakeFirstOrThrow()
52
+
53
+ return events.latestLegacyRefId
54
+ }
55
+
56
+ const countStatuses = async (db: PrimaryDatabase) => {
57
+ const events = await db.db
58
+ .selectFrom('moderation_subject_status')
59
+ .select((eb) => eb.fn.count<number>('id').as('count'))
60
+ .executeTakeFirstOrThrow()
61
+
62
+ return events.count
63
+ }
64
+
65
+ const processLegacyReports = async (
66
+ db: PrimaryDatabase,
67
+ legacyIds: number[],
68
+ ) => {
69
+ if (!legacyIds.length) {
70
+ console.log('No legacy reports to process')
71
+ return
72
+ }
73
+ const reports = await db.db
74
+ .selectFrom('moderation_event')
75
+ .where('action', '=', 'com.atproto.admin.defs#modEventReport')
76
+ .where('legacyRefId', 'in', legacyIds)
77
+ .orderBy('legacyRefId', 'asc')
78
+ .selectAll()
79
+ .execute()
80
+
81
+ console.log(`Processing ${reports.length} reports from ${legacyIds.length}`)
82
+ await db.transaction(async (tx) => {
83
+ // This will be slow but we need to run this in sequence
84
+ for (const report of reports) {
85
+ await adjustModerationSubjectStatus(tx, report)
86
+ }
87
+ })
88
+ console.log(`Completed processing ${reports.length} reports`)
89
+ }
90
+
91
+ const getReportEventsAboveLegacyId = async (
92
+ db: PrimaryDatabase,
93
+ aboveLegacyId: number,
94
+ ) => {
95
+ return await db.db
96
+ .selectFrom('moderation_event')
97
+ .where('action', '=', 'com.atproto.admin.defs#modEventReport')
98
+ .where('legacyRefId', '>', aboveLegacyId)
99
+ .select(sql<number>`"legacyRefId"`.as('legacyRefId'))
100
+ .execute()
101
+ }
102
+
103
+ const createEvents = async (
104
+ db: PrimaryDatabase,
105
+ opts?: { onlyReportsAboveId: number },
106
+ ) => {
107
+ const commonColumnsToSelect = [
108
+ 'subjectDid',
109
+ 'subjectUri',
110
+ 'subjectType',
111
+ 'subjectCid',
112
+ sql`reason`.as('comment'),
113
+ 'createdAt',
114
+ ]
115
+ const commonColumnsToInsert = [
116
+ 'subjectDid',
117
+ 'subjectUri',
118
+ 'subjectType',
119
+ 'subjectCid',
120
+ 'comment',
121
+ 'createdAt',
122
+ 'action',
123
+ 'createdBy',
124
+ ] as const
125
+
126
+ let totalActions: number
127
+ if (!opts?.onlyReportsAboveId) {
128
+ await db.db
129
+ .insertInto('moderation_event')
130
+ .columns([
131
+ 'id',
132
+ ...commonColumnsToInsert,
133
+ 'createLabelVals',
134
+ 'negateLabelVals',
135
+ 'durationInHours',
136
+ 'expiresAt',
137
+ ])
138
+ .expression((eb) =>
139
+ eb
140
+ // @ts-ignore
141
+ .selectFrom('moderation_action')
142
+ // @ts-ignore
143
+ .select([
144
+ 'id',
145
+ ...commonColumnsToSelect,
146
+ sql`CONCAT('com.atproto.admin.defs#modEvent', UPPER(SUBSTRING(SPLIT_PART(action, '#', 2) FROM 1 FOR 1)), SUBSTRING(SPLIT_PART(action, '#', 2) FROM 2))`.as(
147
+ 'action',
148
+ ),
149
+ 'createdBy',
150
+ 'createLabelVals',
151
+ 'negateLabelVals',
152
+ 'durationInHours',
153
+ 'expiresAt',
154
+ ])
155
+ .orderBy('id', 'asc'),
156
+ )
157
+ .execute()
158
+
159
+ totalActions = await countEvents(db)
160
+ console.log(`Created ${totalActions} events from actions`)
161
+
162
+ await sql`SELECT setval(pg_get_serial_sequence('moderation_event', 'id'), (select max(id) from moderation_event))`.execute(
163
+ db.db,
164
+ )
165
+ console.log('Reset the id sequence for moderation_event')
166
+ } else {
167
+ totalActions = await countEvents(db)
168
+ }
169
+
170
+ await db.db
171
+ .insertInto('moderation_event')
172
+ .columns([...commonColumnsToInsert, 'meta', 'legacyRefId'])
173
+ .expression((eb) => {
174
+ const builder = eb
175
+ // @ts-ignore
176
+ .selectFrom('moderation_report')
177
+ // @ts-ignore
178
+ .select([
179
+ ...commonColumnsToSelect,
180
+ sql`'com.atproto.admin.defs#modEventReport'`.as('action'),
181
+ sql`"reportedByDid"`.as('createdBy'),
182
+ sql`json_build_object('reportType', "reasonType")`.as('meta'),
183
+ sql`id`.as('legacyRefId'),
184
+ ])
185
+
186
+ if (opts?.onlyReportsAboveId) {
187
+ // @ts-ignore
188
+ return builder.where('id', '>', opts.onlyReportsAboveId)
189
+ }
190
+
191
+ return builder
192
+ })
193
+ .execute()
194
+
195
+ const totalEvents = await countEvents(db)
196
+ console.log(`Created ${totalEvents - totalActions} events from reports`)
197
+
198
+ return
199
+ }
200
+
201
+ const setReportedAtTimestamp = async (db: PrimaryDatabase) => {
202
+ console.log('Initiating lastReportedAt timestamp sync')
203
+ const didUpdate = await sql`
204
+ UPDATE moderation_subject_status
205
+ SET "lastReportedAt" = reports."createdAt"
206
+ FROM (
207
+ select "subjectDid", "subjectUri", MAX("createdAt") as "createdAt"
208
+ from moderation_report
209
+ where "subjectUri" is null
210
+ group by "subjectDid", "subjectUri"
211
+ ) as reports
212
+ WHERE reports."subjectDid" = moderation_subject_status."did"
213
+ AND "recordPath" = ''
214
+ AND ("lastReportedAt" is null OR "lastReportedAt" < reports."createdAt")
215
+ `.execute(db.db)
216
+
217
+ console.log(
218
+ `Updated lastReportedAt for ${didUpdate.numUpdatedOrDeletedRows} did subject`,
219
+ )
220
+
221
+ const contentUpdate = await sql`
222
+ UPDATE moderation_subject_status
223
+ SET "lastReportedAt" = reports."createdAt"
224
+ FROM (
225
+ select "subjectDid", "subjectUri", MAX("createdAt") as "createdAt"
226
+ from moderation_report
227
+ where "subjectUri" is not null
228
+ group by "subjectDid", "subjectUri"
229
+ ) as reports
230
+ WHERE reports."subjectDid" = moderation_subject_status."did"
231
+ AND "recordPath" is not null
232
+ AND POSITION(moderation_subject_status."recordPath" IN reports."subjectUri") > 0
233
+ AND ("lastReportedAt" is null OR "lastReportedAt" < reports."createdAt")
234
+ `.execute(db.db)
235
+
236
+ console.log(
237
+ `Updated lastReportedAt for ${contentUpdate.numUpdatedOrDeletedRows} subject with uri`,
238
+ )
239
+ }
240
+
241
+ const createStatusFromActions = async (db: PrimaryDatabase) => {
242
+ const allEvents = await db.db
243
+ // @ts-ignore
244
+ .selectFrom('moderation_action')
245
+ // @ts-ignore
246
+ .where('reversedAt', 'is', null)
247
+ // @ts-ignore
248
+ .select((eb) => eb.fn.count<number>('id').as('count'))
249
+ .executeTakeFirstOrThrow()
250
+
251
+ const chunkSize = 2500
252
+ const totalChunks = Math.ceil(allEvents.count / chunkSize)
253
+
254
+ console.log(`Processing ${allEvents.count} actions in ${totalChunks} chunks`)
255
+
256
+ await db.transaction(async (tx) => {
257
+ // This is not used for pagination but only for logging purposes
258
+ let currentChunk = 1
259
+ let lastProcessedId: undefined | number = 0
260
+ do {
261
+ const eventsQuery = tx.db
262
+ // @ts-ignore
263
+ .selectFrom('moderation_action')
264
+ // @ts-ignore
265
+ .where('reversedAt', 'is', null)
266
+ // @ts-ignore
267
+ .where('id', '>', lastProcessedId)
268
+ .limit(chunkSize)
269
+ .selectAll()
270
+ const events = (await eventsQuery.execute()) as ModerationActionRow[]
271
+
272
+ for (const event of events) {
273
+ // Remap action to event data type
274
+ const actionParts = event.action.split('#')
275
+ await adjustModerationSubjectStatus(tx, {
276
+ ...event,
277
+ action: `com.atproto.admin.defs#modEvent${actionParts[1]
278
+ .charAt(0)
279
+ .toUpperCase()}${actionParts[1].slice(
280
+ 1,
281
+ )}` as ModerationEventRow['action'],
282
+ comment: event.reason,
283
+ meta: null,
284
+ })
285
+ }
286
+
287
+ console.log(`Processed events chunk ${currentChunk} of ${totalChunks}`)
288
+ lastProcessedId = events.at(-1)?.id
289
+ currentChunk++
290
+ } while (lastProcessedId !== undefined)
291
+ })
292
+
293
+ console.log(`Events migration complete!`)
294
+
295
+ const totalStatuses = await countStatuses(db)
296
+ console.log(`Created ${totalStatuses} statuses`)
297
+ }
298
+
299
+ const remapFlagToAcknlowedge = async (db: PrimaryDatabase) => {
300
+ console.log('Initiating flag to ack remap')
301
+ const results = await sql`
302
+ UPDATE moderation_event
303
+ SET "action" = 'com.atproto.admin.defs#modEventAcknowledge'
304
+ WHERE action = 'com.atproto.admin.defs#modEventFlag'
305
+ `.execute(db.db)
306
+ console.log(`Remapped ${results.numUpdatedOrDeletedRows} flag actions to ack`)
307
+ }
308
+
309
+ const syncBlobCids = async (db: PrimaryDatabase) => {
310
+ console.log('Initiating blob cid sync')
311
+ const results = await sql`
312
+ UPDATE moderation_subject_status
313
+ SET "blobCids" = blob_action."cids"
314
+ FROM (
315
+ SELECT moderation_action."subjectUri", moderation_action."subjectDid", jsonb_agg(moderation_action_subject_blob."cid") as cids
316
+ FROM moderation_action_subject_blob
317
+ JOIN moderation_action
318
+ ON moderation_action.id = moderation_action_subject_blob."actionId"
319
+ WHERE moderation_action."reversedAt" is NULL
320
+ GROUP by moderation_action."subjectUri", moderation_action."subjectDid"
321
+ ) as blob_action
322
+ WHERE did = "subjectDid" AND position("recordPath" IN "subjectUri") > 0
323
+ `.execute(db.db)
324
+ console.log(`Updated blob cids on ${results.numUpdatedOrDeletedRows} rows`)
325
+ }
326
+
327
+ async function updateStatusFromUnresolvedReports(db: PrimaryDatabase) {
328
+ const { ref } = db.db.dynamic
329
+ const reports = await db.db
330
+ // @ts-ignore
331
+ .selectFrom('moderation_report')
332
+ .whereNotExists((qb) =>
333
+ qb
334
+ .selectFrom('moderation_report_resolution')
335
+ .selectAll()
336
+ // @ts-ignore
337
+ .whereRef('reportId', '=', ref('moderation_report.id')),
338
+ )
339
+ .select(sql<number>`moderation_report.id`.as('legacyId'))
340
+ .execute()
341
+
342
+ console.log('Updating statuses based on unresolved reports')
343
+ await processLegacyReports(
344
+ db,
345
+ reports.map((report) => report.legacyId),
346
+ )
347
+ console.log('Completed updating statuses based on unresolved reports')
348
+ }
349
+
350
+ export async function MigrateModerationData() {
351
+ const env = getEnv()
352
+ const db = new DatabaseCoordinator({
353
+ schema: env.DB_SCHEMA,
354
+ primary: {
355
+ url: env.DB_URL,
356
+ poolSize: env.DB_POOL_SIZE,
357
+ },
358
+ replicas: [],
359
+ })
360
+
361
+ const primaryDb = db.getPrimary()
362
+
363
+ const [counts, existingEventsCount] = await Promise.all([
364
+ countEntries(primaryDb),
365
+ countEvents(primaryDb),
366
+ ])
367
+
368
+ // If there are existing events in the moderation_event table, we assume that the migration has already been run
369
+ // so we just bring over any new reports since last run
370
+ if (existingEventsCount) {
371
+ console.log(
372
+ `Found ${existingEventsCount} existing events. Migrating ${counts.reportsCount} reports only, ignoring actions`,
373
+ )
374
+ const reportMigrationStartedAt = Date.now()
375
+ const latestReportLegacyRefId = await getLatestReportLegacyRefId(primaryDb)
376
+
377
+ if (latestReportLegacyRefId) {
378
+ await createEvents(primaryDb, {
379
+ onlyReportsAboveId: latestReportLegacyRefId,
380
+ })
381
+ const newReportEvents = await getReportEventsAboveLegacyId(
382
+ primaryDb,
383
+ latestReportLegacyRefId,
384
+ )
385
+ await processLegacyReports(
386
+ primaryDb,
387
+ newReportEvents.map((evt) => evt.legacyRefId),
388
+ )
389
+ await setReportedAtTimestamp(primaryDb)
390
+ } else {
391
+ console.log('No reports have been migrated into events yet, bailing.')
392
+ }
393
+
394
+ console.log(
395
+ `Time spent: ${(Date.now() - reportMigrationStartedAt) / 1000} seconds`,
396
+ )
397
+ console.log('Migration complete!')
398
+ return
399
+ }
400
+
401
+ const totalEntries = counts.actionsCount + counts.reportsCount
402
+ console.log(`Migrating ${totalEntries} rows of actions and reports`)
403
+ const startedAt = Date.now()
404
+ await createEvents(primaryDb)
405
+ // Important to run this before creation statuses from actions to ensure that we are not attempting to map flag actions
406
+ await remapFlagToAcknlowedge(primaryDb)
407
+ await createStatusFromActions(primaryDb)
408
+ await updateStatusFromUnresolvedReports(primaryDb)
409
+ await setReportedAtTimestamp(primaryDb)
410
+ await syncBlobCids(primaryDb)
411
+
412
+ console.log(`Time spent: ${(Date.now() - startedAt) / 1000 / 60} minutes`)
413
+ console.log('Migration complete!')
414
+ }
package/src/redis.ts CHANGED
@@ -11,12 +11,16 @@ export class Redis {
11
11
  name: opts.sentinel,
12
12
  sentinels: opts.hosts.map((h) => addressParts(h, 26379)),
13
13
  password: opts.password,
14
+ db: opts.db,
15
+ commandTimeout: opts.commandTimeout,
14
16
  })
15
17
  } else if ('host' in opts) {
16
18
  assert(opts.host)
17
19
  this.driver = new RedisDriver({
18
20
  ...addressParts(opts.host),
19
21
  password: opts.password,
22
+ db: opts.db,
23
+ commandTimeout: opts.commandTimeout,
20
24
  })
21
25
  } else {
22
26
  assert(opts.driver)
@@ -25,6 +29,10 @@ export class Redis {
25
29
  this.namespace = opts.namespace
26
30
  }
27
31
 
32
+ withNamespace(namespace: string): Redis {
33
+ return new Redis({ driver: this.driver, namespace })
34
+ }
35
+
28
36
  async readStreams(
29
37
  streams: StreamRef[],
30
38
  opts: { count: number; blockMs?: number },
@@ -97,8 +105,38 @@ export class Redis {
97
105
  return await this.driver.get(this.ns(key))
98
106
  }
99
107
 
100
- async set(key: string, val: string | number) {
101
- await this.driver.set(this.ns(key), val)
108
+ async set(key: string, val: string | number, ttlMs?: number) {
109
+ if (ttlMs !== undefined) {
110
+ await this.driver.set(this.ns(key), val, 'PX', ttlMs)
111
+ } else {
112
+ await this.driver.set(this.ns(key), val)
113
+ }
114
+ }
115
+
116
+ async getMulti(keys: string[]) {
117
+ const namespaced = keys.map((k) => this.ns(k))
118
+ const got = await this.driver.mget(...namespaced)
119
+ const results = {}
120
+ for (let i = 0; i < keys.length; i++) {
121
+ const key = keys[i]
122
+ results[key] = got[i]
123
+ }
124
+ return results
125
+ }
126
+
127
+ async setMulti(vals: Record<string, string | number>, ttlMs?: number) {
128
+ if (Object.keys(vals).length === 0) {
129
+ return
130
+ }
131
+ let builder = this.driver.multi({ pipeline: true })
132
+ for (const key of Object.keys(vals)) {
133
+ if (ttlMs !== undefined) {
134
+ builder = builder.set(this.ns(key), vals[key], 'PX', ttlMs)
135
+ } else {
136
+ builder = builder.set(this.ns(key), vals[key])
137
+ }
138
+ }
139
+ await builder.exec()
102
140
  }
103
141
 
104
142
  async del(key: string) {
@@ -152,9 +190,11 @@ export type RedisOptions = (
152
190
  ) & {
153
191
  password?: string
154
192
  namespace?: string
193
+ db?: number
194
+ commandTimeout?: number
155
195
  }
156
196
 
157
- function addressParts(
197
+ export function addressParts(
158
198
  addr: string,
159
199
  defaultPort = 6379,
160
200
  ): { host: string; port: number } {