@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
@@ -0,0 +1,60 @@
1
+ import assert from 'assert'
2
+
3
+ export interface DaemonConfigValues {
4
+ version: string
5
+ dbPostgresUrl: string
6
+ dbPostgresSchema?: string
7
+ notificationsDaemonFromDid?: string
8
+ }
9
+
10
+ export class DaemonConfig {
11
+ constructor(private cfg: DaemonConfigValues) {}
12
+
13
+ static readEnv(overrides?: Partial<DaemonConfigValues>) {
14
+ const version = process.env.BSKY_VERSION || '0.0.0'
15
+ const dbPostgresUrl =
16
+ overrides?.dbPostgresUrl || process.env.DB_PRIMARY_POSTGRES_URL
17
+ const dbPostgresSchema =
18
+ overrides?.dbPostgresSchema || process.env.DB_POSTGRES_SCHEMA
19
+ const notificationsDaemonFromDid =
20
+ overrides?.notificationsDaemonFromDid ||
21
+ process.env.BSKY_NOTIFS_DAEMON_FROM_DID ||
22
+ undefined
23
+ assert(dbPostgresUrl)
24
+ return new DaemonConfig({
25
+ version,
26
+ dbPostgresUrl,
27
+ dbPostgresSchema,
28
+ notificationsDaemonFromDid,
29
+ ...stripUndefineds(overrides ?? {}),
30
+ })
31
+ }
32
+
33
+ get version() {
34
+ return this.cfg.version
35
+ }
36
+
37
+ get dbPostgresUrl() {
38
+ return this.cfg.dbPostgresUrl
39
+ }
40
+
41
+ get dbPostgresSchema() {
42
+ return this.cfg.dbPostgresSchema
43
+ }
44
+
45
+ get notificationsDaemonFromDid() {
46
+ return this.cfg.notificationsDaemonFromDid
47
+ }
48
+ }
49
+
50
+ function stripUndefineds(
51
+ obj: Record<string, unknown>,
52
+ ): Record<string, unknown> {
53
+ const result = {}
54
+ Object.entries(obj).forEach(([key, val]) => {
55
+ if (val !== undefined) {
56
+ result[key] = val
57
+ }
58
+ })
59
+ return result
60
+ }
@@ -0,0 +1,27 @@
1
+ import { PrimaryDatabase } from '../db'
2
+ import { DaemonConfig } from './config'
3
+ import { Services } from './services'
4
+
5
+ export class DaemonContext {
6
+ constructor(
7
+ private opts: {
8
+ db: PrimaryDatabase
9
+ cfg: DaemonConfig
10
+ services: Services
11
+ },
12
+ ) {}
13
+
14
+ get db(): PrimaryDatabase {
15
+ return this.opts.db
16
+ }
17
+
18
+ get cfg(): DaemonConfig {
19
+ return this.opts.cfg
20
+ }
21
+
22
+ get services(): Services {
23
+ return this.opts.services
24
+ }
25
+ }
26
+
27
+ export default DaemonContext
@@ -0,0 +1,78 @@
1
+ import { PrimaryDatabase } from '../db'
2
+ import { dbLogger } from '../logger'
3
+ import { DaemonConfig } from './config'
4
+ import { DaemonContext } from './context'
5
+ import { createServices } from './services'
6
+ import { ImageUriBuilder } from '../image/uri'
7
+ import { NotificationsDaemon } from './notifications'
8
+ import logger from './logger'
9
+
10
+ export { DaemonConfig } from './config'
11
+ export type { DaemonConfigValues } from './config'
12
+
13
+ export class BskyDaemon {
14
+ public ctx: DaemonContext
15
+ public notifications: NotificationsDaemon
16
+ private dbStatsInterval: NodeJS.Timer
17
+ private notifStatsInterval: NodeJS.Timer
18
+
19
+ constructor(opts: {
20
+ ctx: DaemonContext
21
+ notifications: NotificationsDaemon
22
+ }) {
23
+ this.ctx = opts.ctx
24
+ this.notifications = opts.notifications
25
+ }
26
+
27
+ static create(opts: { db: PrimaryDatabase; cfg: DaemonConfig }): BskyDaemon {
28
+ const { db, cfg } = opts
29
+ const imgUriBuilder = new ImageUriBuilder('https://daemon.invalid') // will not be used by daemon
30
+ const services = createServices({
31
+ imgUriBuilder,
32
+ })
33
+ const ctx = new DaemonContext({
34
+ db,
35
+ cfg,
36
+ services,
37
+ })
38
+ const notifications = new NotificationsDaemon(ctx)
39
+ return new BskyDaemon({ ctx, notifications })
40
+ }
41
+
42
+ async start() {
43
+ const { db, cfg } = this.ctx
44
+ const pool = db.pool
45
+ this.notifications.run({
46
+ startFromDid: cfg.notificationsDaemonFromDid,
47
+ })
48
+ this.dbStatsInterval = setInterval(() => {
49
+ dbLogger.info(
50
+ {
51
+ idleCount: pool.idleCount,
52
+ totalCount: pool.totalCount,
53
+ waitingCount: pool.waitingCount,
54
+ },
55
+ 'db pool stats',
56
+ )
57
+ }, 10000)
58
+ this.notifStatsInterval = setInterval(() => {
59
+ logger.info(
60
+ {
61
+ count: this.notifications.count,
62
+ lastDid: this.notifications.lastDid,
63
+ },
64
+ 'notifications daemon stats',
65
+ )
66
+ }, 10000)
67
+ return this
68
+ }
69
+
70
+ async destroy(): Promise<void> {
71
+ await this.notifications.destroy()
72
+ await this.ctx.db.close()
73
+ clearInterval(this.dbStatsInterval)
74
+ clearInterval(this.notifStatsInterval)
75
+ }
76
+ }
77
+
78
+ export default BskyDaemon
@@ -0,0 +1,6 @@
1
+ import { subsystemLogger } from '@atproto/common'
2
+
3
+ const logger: ReturnType<typeof subsystemLogger> =
4
+ subsystemLogger('bsky:daemon')
5
+
6
+ export default logger
@@ -0,0 +1,54 @@
1
+ import { tidyNotifications } from '../services/util/notification'
2
+ import DaemonContext from './context'
3
+ import logger from './logger'
4
+
5
+ export class NotificationsDaemon {
6
+ ac = new AbortController()
7
+ running: Promise<void> | undefined
8
+ count = 0
9
+ lastDid: string | null = null
10
+
11
+ constructor(private ctx: DaemonContext) {}
12
+
13
+ run(opts?: RunOptions) {
14
+ if (this.running) return
15
+ this.count = 0
16
+ this.lastDid = null
17
+ this.ac = new AbortController()
18
+ this.running = this.tidyNotifications({
19
+ ...opts,
20
+ forever: opts?.forever !== false, // run forever by default
21
+ })
22
+ .catch((err) => {
23
+ // allow this to cause an unhandled rejection, let deployment handle the crash.
24
+ logger.error({ err }, 'notifications daemon crashed')
25
+ throw err
26
+ })
27
+ .finally(() => (this.running = undefined))
28
+ }
29
+
30
+ private async tidyNotifications(opts: RunOptions) {
31
+ const actorService = this.ctx.services.actor(this.ctx.db)
32
+ for await (const { did } of actorService.all(opts)) {
33
+ if (this.ac.signal.aborted) return
34
+ try {
35
+ await tidyNotifications(this.ctx.db, did)
36
+ this.count++
37
+ this.lastDid = did
38
+ } catch (err) {
39
+ logger.warn({ err, did }, 'failed to tidy notifications for actor')
40
+ }
41
+ }
42
+ }
43
+
44
+ async destroy() {
45
+ this.ac.abort()
46
+ await this.running
47
+ }
48
+ }
49
+
50
+ type RunOptions = {
51
+ forever?: boolean
52
+ batchSize?: number
53
+ startFromDid?: string
54
+ }
@@ -0,0 +1,22 @@
1
+ import { PrimaryDatabase } from '../db'
2
+ import { ActorService } from '../services/actor'
3
+ import { ImageUriBuilder } from '../image/uri'
4
+ import { GraphService } from '../services/graph'
5
+ import { LabelService } from '../services/label'
6
+
7
+ export function createServices(resources: {
8
+ imgUriBuilder: ImageUriBuilder
9
+ }): Services {
10
+ const { imgUriBuilder } = resources
11
+ const graph = GraphService.creator(imgUriBuilder)
12
+ const label = LabelService.creator(null)
13
+ return {
14
+ actor: ActorService.creator(imgUriBuilder, graph, label),
15
+ }
16
+ }
17
+
18
+ export type Services = {
19
+ actor: FromDbPrimary<ActorService>
20
+ }
21
+
22
+ type FromDbPrimary<T> = (db: PrimaryDatabase) => T
@@ -24,7 +24,6 @@ import * as actorSync from './tables/actor-sync'
24
24
  import * as record from './tables/record'
25
25
  import * as notification from './tables/notification'
26
26
  import * as notificationPushToken from './tables/notification-push-token'
27
- import * as didCache from './tables/did-cache'
28
27
  import * as moderation from './tables/moderation'
29
28
  import * as label from './tables/label'
30
29
  import * as algo from './tables/algo'
@@ -57,7 +56,6 @@ export type DatabaseSchemaType = duplicateRecord.PartialDB &
57
56
  record.PartialDB &
58
57
  notification.PartialDB &
59
58
  notificationPushToken.PartialDB &
60
- didCache.PartialDB &
61
59
  moderation.PartialDB &
62
60
  label.PartialDB &
63
61
  algo.PartialDB &
@@ -0,0 +1,123 @@
1
+ import { Kysely } from 'kysely'
2
+
3
+ export async function up(db: Kysely<unknown>): Promise<void> {
4
+ await db.schema
5
+ .createTable('moderation_event')
6
+ .addColumn('id', 'serial', (col) => col.primaryKey())
7
+ .addColumn('action', 'varchar', (col) => col.notNull())
8
+ .addColumn('subjectType', 'varchar', (col) => col.notNull())
9
+ .addColumn('subjectDid', 'varchar', (col) => col.notNull())
10
+ .addColumn('subjectUri', 'varchar')
11
+ .addColumn('subjectCid', 'varchar')
12
+ .addColumn('comment', 'text')
13
+ .addColumn('meta', 'jsonb')
14
+ .addColumn('createdAt', 'varchar', (col) => col.notNull())
15
+ .addColumn('createdBy', 'varchar', (col) => col.notNull())
16
+ .addColumn('reversedAt', 'varchar')
17
+ .addColumn('reversedBy', 'varchar')
18
+ .addColumn('durationInHours', 'integer')
19
+ .addColumn('expiresAt', 'varchar')
20
+ .addColumn('reversedReason', 'text')
21
+ .addColumn('createLabelVals', 'varchar')
22
+ .addColumn('negateLabelVals', 'varchar')
23
+ .addColumn('legacyRefId', 'integer')
24
+ .execute()
25
+ await db.schema
26
+ .createTable('moderation_subject_status')
27
+ .addColumn('id', 'serial', (col) => col.primaryKey())
28
+
29
+ // Identifiers
30
+ .addColumn('did', 'varchar', (col) => col.notNull())
31
+ // Default to '' so that we can apply unique constraints on did and recordPath columns
32
+ .addColumn('recordPath', 'varchar', (col) => col.notNull().defaultTo(''))
33
+ .addColumn('blobCids', 'jsonb')
34
+ .addColumn('recordCid', 'varchar')
35
+
36
+ // human review team state
37
+ .addColumn('reviewState', 'varchar', (col) => col.notNull())
38
+ .addColumn('comment', 'varchar')
39
+ .addColumn('muteUntil', 'varchar')
40
+ .addColumn('lastReviewedAt', 'varchar')
41
+ .addColumn('lastReviewedBy', 'varchar')
42
+
43
+ // report state
44
+ .addColumn('lastReportedAt', 'varchar')
45
+
46
+ // visibility/intervention state
47
+ .addColumn('takendown', 'boolean', (col) => col.defaultTo(false).notNull())
48
+ .addColumn('suspendUntil', 'varchar')
49
+
50
+ // timestamps
51
+ .addColumn('createdAt', 'varchar', (col) => col.notNull())
52
+ .addColumn('updatedAt', 'varchar', (col) => col.notNull())
53
+ .addUniqueConstraint('moderation_status_unique_idx', ['did', 'recordPath'])
54
+ .execute()
55
+
56
+ await db.schema
57
+ .createIndex('moderation_subject_status_blob_cids_idx')
58
+ .on('moderation_subject_status')
59
+ .using('gin')
60
+ .column('blobCids')
61
+ .execute()
62
+
63
+ // Move foreign keys from moderation_action to moderation_event
64
+ await db.schema
65
+ .alterTable('record')
66
+ .dropConstraint('record_takedown_id_fkey')
67
+ .execute()
68
+ await db.schema
69
+ .alterTable('actor')
70
+ .dropConstraint('actor_takedown_id_fkey')
71
+ .execute()
72
+ await db.schema
73
+ .alterTable('actor')
74
+ .addForeignKeyConstraint(
75
+ 'actor_takedown_id_fkey',
76
+ ['takedownId'],
77
+ 'moderation_event',
78
+ ['id'],
79
+ )
80
+ .execute()
81
+ await db.schema
82
+ .alterTable('record')
83
+ .addForeignKeyConstraint(
84
+ 'record_takedown_id_fkey',
85
+ ['takedownId'],
86
+ 'moderation_event',
87
+ ['id'],
88
+ )
89
+ .execute()
90
+ }
91
+
92
+ export async function down(db: Kysely<unknown>): Promise<void> {
93
+ await db.schema.dropTable('moderation_event').execute()
94
+ await db.schema.dropTable('moderation_subject_status').execute()
95
+
96
+ // Revert foreign key constraints
97
+ await db.schema
98
+ .alterTable('record')
99
+ .dropConstraint('record_takedown_id_fkey')
100
+ .execute()
101
+ await db.schema
102
+ .alterTable('actor')
103
+ .dropConstraint('actor_takedown_id_fkey')
104
+ .execute()
105
+ await db.schema
106
+ .alterTable('actor')
107
+ .addForeignKeyConstraint(
108
+ 'actor_takedown_id_fkey',
109
+ ['takedownId'],
110
+ 'moderation_action',
111
+ ['id'],
112
+ )
113
+ .execute()
114
+ await db.schema
115
+ .alterTable('record')
116
+ .addForeignKeyConstraint(
117
+ 'record_takedown_id_fkey',
118
+ ['takedownId'],
119
+ 'moderation_action',
120
+ ['id'],
121
+ )
122
+ .execute()
123
+ }
@@ -0,0 +1,14 @@
1
+ import { Kysely } from 'kysely'
2
+
3
+ export async function up(db: Kysely<unknown>): Promise<void> {
4
+ await db.schema.dropTable('did_cache').execute()
5
+ }
6
+
7
+ export async function down(db: Kysely<unknown>): Promise<void> {
8
+ await db.schema
9
+ .createTable('did_cache')
10
+ .addColumn('did', 'varchar', (col) => col.primaryKey())
11
+ .addColumn('doc', 'jsonb', (col) => col.notNull())
12
+ .addColumn('updatedAt', 'bigint', (col) => col.notNull())
13
+ .execute()
14
+ }
@@ -30,3 +30,5 @@ export * as _20230904T211011773Z from './20230904T211011773Z-block-lists'
30
30
  export * as _20230906T222220386Z from './20230906T222220386Z-thread-gating'
31
31
  export * as _20230920T213858047Z from './20230920T213858047Z-add-tags-to-post'
32
32
  export * as _20230929T192920807Z from './20230929T192920807Z-record-cursor-indexes'
33
+ export * as _20231003T202833377Z from './20231003T202833377Z-create-moderation-subject-status'
34
+ export * as _20231205T000257238Z from './20231205T000257238Z-remove-did-cache'
@@ -117,13 +117,36 @@ export const paginate = <
117
117
  direction?: 'asc' | 'desc'
118
118
  keyset: K
119
119
  tryIndex?: boolean
120
+ // By default, pg does nullsFirst
121
+ nullsLast?: boolean
120
122
  },
121
123
  ): QB => {
122
- const { limit, cursor, keyset, direction = 'desc', tryIndex } = opts
124
+ const {
125
+ limit,
126
+ cursor,
127
+ keyset,
128
+ direction = 'desc',
129
+ tryIndex,
130
+ nullsLast,
131
+ } = opts
123
132
  const keysetSql = keyset.getSql(keyset.unpack(cursor), direction, tryIndex)
124
133
  return qb
125
134
  .if(!!limit, (q) => q.limit(limit as number))
126
- .orderBy(keyset.primary, direction)
127
- .orderBy(keyset.secondary, direction)
135
+ .if(!nullsLast, (q) =>
136
+ q.orderBy(keyset.primary, direction).orderBy(keyset.secondary, direction),
137
+ )
138
+ .if(!!nullsLast, (q) =>
139
+ q
140
+ .orderBy(
141
+ direction === 'asc'
142
+ ? sql`${keyset.primary} asc nulls last`
143
+ : sql`${keyset.primary} desc nulls last`,
144
+ )
145
+ .orderBy(
146
+ direction === 'asc'
147
+ ? sql`${keyset.secondary} asc nulls last`
148
+ : sql`${keyset.secondary} desc nulls last`,
149
+ ),
150
+ )
128
151
  .if(!!keysetSql, (qb) => (keysetSql ? qb.where(keysetSql) : qb)) as QB
129
152
  }
@@ -2,13 +2,15 @@ import { wait } from '@atproto/common'
2
2
  import { Leader } from './leader'
3
3
  import { dbLogger } from '../logger'
4
4
  import AppContext from '../context'
5
+ import { AtUri } from '@atproto/api'
6
+ import { ModerationSubjectStatusRow } from '../services/moderation/types'
7
+ import { CID } from 'multiformats/cid'
5
8
  import AtpAgent from '@atproto/api'
6
- import { LabelService } from '../services/label'
7
- import { ModerationActionRow } from '../services/moderation'
9
+ import { retryHttp } from '../util/retry'
8
10
 
9
11
  export const MODERATION_ACTION_REVERSAL_ID = 1011
10
12
 
11
- export class PeriodicModerationActionReversal {
13
+ export class PeriodicModerationEventReversal {
12
14
  leader = new Leader(
13
15
  MODERATION_ACTION_REVERSAL_ID,
14
16
  this.appContext.db.getPrimary(),
@@ -20,48 +22,50 @@ export class PeriodicModerationActionReversal {
20
22
  this.pushAgent = appContext.moderationPushAgent
21
23
  }
22
24
 
23
- // invert label creation & negations
24
- async reverseLabels(labelTxn: LabelService, actionRow: ModerationActionRow) {
25
- let uri: string
26
- let cid: string | null = null
27
-
28
- if (actionRow.subjectUri && actionRow.subjectCid) {
29
- uri = actionRow.subjectUri
30
- cid = actionRow.subjectCid
31
- } else {
32
- uri = actionRow.subjectDid
33
- }
34
-
35
- await labelTxn.formatAndCreate(this.appContext.cfg.labelerDid, uri, cid, {
36
- create: actionRow.negateLabelVals
37
- ? actionRow.negateLabelVals.split(' ')
38
- : undefined,
39
- negate: actionRow.createLabelVals
40
- ? actionRow.createLabelVals.split(' ')
41
- : undefined,
42
- })
43
- }
44
-
45
- async revertAction(actionRow: ModerationActionRow) {
46
- const reverseAction = {
47
- id: actionRow.id,
48
- createdBy: actionRow.createdBy,
49
- createdAt: new Date(),
50
- reason: `[SCHEDULED_REVERSAL] Reverting action as originally scheduled`,
51
- }
52
-
53
- if (this.pushAgent) {
54
- await this.pushAgent.com.atproto.admin.reverseModerationAction(
55
- reverseAction,
56
- )
57
- return
58
- }
59
-
25
+ async revertState(eventRow: ModerationSubjectStatusRow) {
60
26
  await this.appContext.db.getPrimary().transaction(async (dbTxn) => {
61
27
  const moderationTxn = this.appContext.services.moderation(dbTxn)
62
- await moderationTxn.revertAction(reverseAction)
63
- const labelTxn = this.appContext.services.label(dbTxn)
64
- await this.reverseLabels(labelTxn, actionRow)
28
+ const originalEvent =
29
+ await moderationTxn.getLastReversibleEventForSubject(eventRow)
30
+ if (originalEvent) {
31
+ const { restored } = await moderationTxn.revertState({
32
+ action: originalEvent.action,
33
+ createdBy: originalEvent.createdBy,
34
+ comment:
35
+ '[SCHEDULED_REVERSAL] Reverting action as originally scheduled',
36
+ subject:
37
+ eventRow.recordPath && eventRow.recordCid
38
+ ? {
39
+ uri: AtUri.make(
40
+ eventRow.did,
41
+ ...eventRow.recordPath.split('/'),
42
+ ),
43
+ cid: CID.parse(eventRow.recordCid),
44
+ }
45
+ : { did: eventRow.did },
46
+ createdAt: new Date(),
47
+ })
48
+
49
+ const { pushAgent } = this
50
+ if (
51
+ originalEvent.action === 'com.atproto.admin.defs#modEventTakedown' &&
52
+ restored?.subjects?.length &&
53
+ pushAgent
54
+ ) {
55
+ await Promise.allSettled(
56
+ restored.subjects.map((subject) =>
57
+ retryHttp(() =>
58
+ pushAgent.api.com.atproto.admin.updateSubjectStatus({
59
+ subject,
60
+ takedown: {
61
+ applied: false,
62
+ },
63
+ }),
64
+ ),
65
+ ),
66
+ )
67
+ }
68
+ }
65
69
  })
66
70
  }
67
71
 
@@ -69,12 +73,12 @@ export class PeriodicModerationActionReversal {
69
73
  const moderationService = this.appContext.services.moderation(
70
74
  this.appContext.db.getPrimary(),
71
75
  )
72
- const actionsDueForReversal =
73
- await moderationService.getActionsDueForReversal()
76
+ const subjectsDueForReversal =
77
+ await moderationService.getSubjectsDueForReversal()
74
78
 
75
79
  // We shouldn't have too many actions due for reversal at any given time, so running in parallel is probably fine
76
80
  // Internally, each reversal runs within its own transaction
77
- await Promise.all(actionsDueForReversal.map(this.revertAction.bind(this)))
81
+ await Promise.all(subjectsDueForReversal.map(this.revertState.bind(this)))
78
82
  }
79
83
 
80
84
  async run() {