@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,27 +1,37 @@
1
1
  import { sql } from 'kysely'
2
+ import { wait } from '@atproto/common'
2
3
  import { Database } from '../../db'
3
4
  import { notSoftDeletedClause } from '../../db/util'
4
5
  import { ActorViews } from './views'
5
6
  import { ImageUriBuilder } from '../../image/uri'
6
7
  import { Actor } from '../../db/tables/actor'
7
- import { LabelCache } from '../../label-cache'
8
8
  import { TimeCidKeyset, paginate } from '../../db/pagination'
9
9
  import { SearchKeyset, getUserSearchQuery } from '../util/search'
10
+ import { FromDb } from '../types'
11
+ import { GraphService } from '../graph'
12
+ import { LabelService } from '../label'
10
13
 
11
14
  export * from './types'
12
15
 
13
16
  export class ActorService {
17
+ views: ActorViews
18
+
14
19
  constructor(
15
20
  public db: Database,
16
21
  public imgUriBuilder: ImageUriBuilder,
17
- public labelCache: LabelCache,
18
- ) {}
19
-
20
- static creator(imgUriBuilder: ImageUriBuilder, labelCache: LabelCache) {
21
- return (db: Database) => new ActorService(db, imgUriBuilder, labelCache)
22
+ private graph: FromDb<GraphService>,
23
+ private label: FromDb<LabelService>,
24
+ ) {
25
+ this.views = new ActorViews(this.db, this.imgUriBuilder, graph, label)
22
26
  }
23
27
 
24
- views = new ActorViews(this.db, this.imgUriBuilder, this.labelCache)
28
+ static creator(
29
+ imgUriBuilder: ImageUriBuilder,
30
+ graph: FromDb<GraphService>,
31
+ label: FromDb<LabelService>,
32
+ ) {
33
+ return (db: Database) => new ActorService(db, imgUriBuilder, graph, label)
34
+ }
25
35
 
26
36
  async getActorDid(handleOrDid: string): Promise<string | null> {
27
37
  if (handleOrDid.startsWith('did:')) {
@@ -144,6 +154,44 @@ export class ActorService {
144
154
  .executeTakeFirst()
145
155
  return res?.repoRev ?? null
146
156
  }
157
+
158
+ async *all(
159
+ opts: {
160
+ batchSize?: number
161
+ forever?: boolean
162
+ cooldownMs?: number
163
+ startFromDid?: string
164
+ } = {},
165
+ ) {
166
+ const {
167
+ cooldownMs = 1000,
168
+ batchSize = 1000,
169
+ forever = false,
170
+ startFromDid,
171
+ } = opts
172
+ const baseQuery = this.db.db
173
+ .selectFrom('actor')
174
+ .selectAll()
175
+ .orderBy('did')
176
+ .limit(batchSize)
177
+ while (true) {
178
+ let cursor = startFromDid
179
+ do {
180
+ const actors = cursor
181
+ ? await baseQuery.where('did', '>', cursor).execute()
182
+ : await baseQuery.execute()
183
+ for (const actor of actors) {
184
+ yield actor
185
+ }
186
+ cursor = actors.at(-1)?.did
187
+ } while (cursor)
188
+ if (forever) {
189
+ await wait(cooldownMs)
190
+ } else {
191
+ return
192
+ }
193
+ }
194
+ }
147
195
  }
148
196
 
149
197
  type ActorResult = Actor
@@ -11,7 +11,6 @@ import { Actor } from '../../db/tables/actor'
11
11
  import { ImageUriBuilder } from '../../image/uri'
12
12
  import { LabelService, Labels, getSelfLabels } from '../label'
13
13
  import { BlockAndMuteState, GraphService } from '../graph'
14
- import { LabelCache } from '../../label-cache'
15
14
  import {
16
15
  ActorInfoMap,
17
16
  ProfileDetailHydrationState,
@@ -21,17 +20,24 @@ import {
21
20
  toMapByDid,
22
21
  } from './types'
23
22
  import { ListInfoMap } from '../graph/types'
23
+ import { FromDb } from '../types'
24
24
 
25
25
  export class ActorViews {
26
+ services: {
27
+ label: LabelService
28
+ graph: GraphService
29
+ }
30
+
26
31
  constructor(
27
32
  private db: Database,
28
33
  private imgUriBuilder: ImageUriBuilder,
29
- private labelCache: LabelCache,
30
- ) {}
31
-
32
- services = {
33
- label: LabelService.creator(this.labelCache)(this.db),
34
- graph: GraphService.creator(this.imgUriBuilder)(this.db),
34
+ private graph: FromDb<GraphService>,
35
+ private label: FromDb<LabelService>,
36
+ ) {
37
+ this.services = {
38
+ label: label(db),
39
+ graph: graph(db),
40
+ }
35
41
  }
36
42
 
37
43
  async profiles(
@@ -45,10 +51,7 @@ export class ActorViews {
45
51
  viewer,
46
52
  ...opts,
47
53
  })
48
- return this.profilePresentation(dids, hydrated, {
49
- viewer,
50
- ...opts,
51
- })
54
+ return this.profilePresentation(dids, hydrated, viewer)
52
55
  }
53
56
 
54
57
  async profilesBasic(
@@ -62,10 +65,7 @@ export class ActorViews {
62
65
  viewer,
63
66
  includeSoftDeleted: opts?.includeSoftDeleted,
64
67
  })
65
- return this.profileBasicPresentation(dids, hydrated, {
66
- viewer,
67
- omitLabels: opts?.omitLabels,
68
- })
68
+ return this.profileBasicPresentation(dids, hydrated, viewer, opts)
69
69
  }
70
70
 
71
71
  async profilesList(
@@ -293,11 +293,8 @@ export class ActorViews {
293
293
  labels: Labels
294
294
  bam: BlockAndMuteState
295
295
  },
296
- opts?: {
297
- viewer?: string | null
298
- },
296
+ viewer: string | null,
299
297
  ): ProfileViewMap {
300
- const { viewer } = opts ?? {}
301
298
  const { profiles, lists, labels, bam } = state
302
299
  return dids.reduce((acc, did) => {
303
300
  const prof = profiles[did]
@@ -357,12 +354,12 @@ export class ActorViews {
357
354
  profileBasicPresentation(
358
355
  dids: string[],
359
356
  state: ProfileHydrationState,
357
+ viewer: string | null,
360
358
  opts?: {
361
- viewer?: string | null
362
359
  omitLabels?: boolean
363
360
  },
364
361
  ): ProfileViewMap {
365
- const result = this.profilePresentation(dids, state, opts)
362
+ const result = this.profilePresentation(dids, state, viewer)
366
363
  return Object.values(result).reduce((acc, prof) => {
367
364
  const profileBasic = {
368
365
  did: prof.did,
@@ -1,6 +1,7 @@
1
1
  import { sql } from 'kysely'
2
2
  import { AtUri } from '@atproto/syntax'
3
3
  import { jsonStringToLex } from '@atproto/lexicon'
4
+ import { mapDefined } from '@atproto/common'
4
5
  import { Database } from '../../db'
5
6
  import { countAll, noMatch, notSoftDeletedClause } from '../../db/util'
6
7
  import { ImageUriBuilder } from '../../image/uri'
@@ -42,28 +43,42 @@ import {
42
43
  RelationshipPair,
43
44
  } from '../graph'
44
45
  import { FeedViews } from './views'
45
- import { LabelCache } from '../../label-cache'
46
46
  import { threadgateToPostUri, postToThreadgateUri } from './util'
47
+ import { FromDb } from '../types'
47
48
 
48
49
  export * from './types'
49
50
 
50
51
  export class FeedService {
52
+ views: FeedViews
53
+ services: {
54
+ label: LabelService
55
+ actor: ActorService
56
+ graph: GraphService
57
+ }
58
+
51
59
  constructor(
52
60
  public db: Database,
53
61
  public imgUriBuilder: ImageUriBuilder,
54
- public labelCache: LabelCache,
55
- ) {}
56
-
57
- views = new FeedViews(this.db, this.imgUriBuilder, this.labelCache)
58
-
59
- services = {
60
- label: LabelService.creator(this.labelCache)(this.db),
61
- actor: ActorService.creator(this.imgUriBuilder, this.labelCache)(this.db),
62
- graph: GraphService.creator(this.imgUriBuilder)(this.db),
62
+ private actor: FromDb<ActorService>,
63
+ private label: FromDb<LabelService>,
64
+ private graph: FromDb<GraphService>,
65
+ ) {
66
+ this.views = new FeedViews(this.db, this.imgUriBuilder, actor, graph)
67
+ this.services = {
68
+ label: label(this.db),
69
+ actor: actor(this.db),
70
+ graph: graph(this.db),
71
+ }
63
72
  }
64
73
 
65
- static creator(imgUriBuilder: ImageUriBuilder, labelCache: LabelCache) {
66
- return (db: Database) => new FeedService(db, imgUriBuilder, labelCache)
74
+ static creator(
75
+ imgUriBuilder: ImageUriBuilder,
76
+ actor: FromDb<ActorService>,
77
+ label: FromDb<LabelService>,
78
+ graph: FromDb<GraphService>,
79
+ ) {
80
+ return (db: Database) =>
81
+ new FeedService(db, imgUriBuilder, actor, label, graph)
67
82
  }
68
83
 
69
84
  selectPostQb() {
@@ -205,6 +220,11 @@ export class FeedService {
205
220
  }, {} as Record<string, FeedRow>)
206
221
  }
207
222
 
223
+ async postUrisToFeedItems(uris: string[]): Promise<FeedRow[]> {
224
+ const feedItems = await this.getFeedItems(uris)
225
+ return mapDefined(uris, (uri) => feedItems[uri])
226
+ }
227
+
208
228
  feedItemRefs(items: FeedRow[]) {
209
229
  const actorDids = new Set<string>()
210
230
  const postUris = new Set<string>()
@@ -399,20 +419,32 @@ export class FeedService {
399
419
  const actorInfos = this.services.actor.views.profileBasicPresentation(
400
420
  [...nestedDids],
401
421
  feedState,
402
- { viewer },
422
+ viewer,
403
423
  )
404
424
  const recordEmbedViews: RecordEmbedViewRecordMap = {}
405
425
  for (const uri of nestedUris) {
406
426
  const collection = new AtUri(uri).collection
407
427
  if (collection === ids.AppBskyFeedGenerator && feedGenInfos[uri]) {
408
- recordEmbedViews[uri] = {
409
- $type: 'app.bsky.feed.defs#generatorView',
410
- ...this.views.formatFeedGeneratorView(feedGenInfos[uri], actorInfos),
428
+ const genView = this.views.formatFeedGeneratorView(
429
+ feedGenInfos[uri],
430
+ actorInfos,
431
+ )
432
+ if (genView) {
433
+ recordEmbedViews[uri] = {
434
+ $type: 'app.bsky.feed.defs#generatorView',
435
+ ...genView,
436
+ }
411
437
  }
412
438
  } else if (collection === ids.AppBskyGraphList && listViews[uri]) {
413
- recordEmbedViews[uri] = {
414
- $type: 'app.bsky.graph.defs#listView',
415
- ...this.services.graph.formatListView(listViews[uri], actorInfos),
439
+ const listView = this.services.graph.formatListView(
440
+ listViews[uri],
441
+ actorInfos,
442
+ )
443
+ if (listView) {
444
+ recordEmbedViews[uri] = {
445
+ $type: 'app.bsky.graph.defs#listView',
446
+ ...listView,
447
+ }
416
448
  }
417
449
  } else if (collection === ids.AppBskyFeedPost && feedState.posts[uri]) {
418
450
  const formatted = this.views.formatPostView(
@@ -423,6 +455,7 @@ export class FeedService {
423
455
  feedState.embeds,
424
456
  feedState.labels,
425
457
  feedState.lists,
458
+ viewer,
426
459
  )
427
460
  recordEmbedViews[uri] = this.views.getRecordEmbedView(
428
461
  uri,
@@ -36,43 +36,72 @@ export const invalidReplyRoot = (
36
36
  return parent.record.reply?.root.uri !== replyRoot
37
37
  }
38
38
 
39
- export const violatesThreadGate = async (
40
- db: DatabaseSchema,
41
- did: string,
42
- owner: string,
43
- root: PostRecord | null,
39
+ type ParsedThreadGate = {
40
+ canReply?: boolean
41
+ allowMentions?: boolean
42
+ allowFollowing?: boolean
43
+ allowListUris?: string[]
44
+ }
45
+
46
+ export const parseThreadGate = (
47
+ replierDid: string,
48
+ ownerDid: string,
49
+ rootPost: PostRecord | null,
44
50
  gate: GateRecord | null,
45
- ) => {
46
- if (did === owner) return false
47
- if (!gate?.allow) return false
51
+ ): ParsedThreadGate => {
52
+ if (replierDid === ownerDid) {
53
+ return { canReply: true }
54
+ }
55
+ // if gate.allow is unset then *any* reply is allowed, if it is an empty array then *no* reply is allowed
56
+ if (!gate || !gate.allow) {
57
+ return { canReply: true }
58
+ }
48
59
 
49
- const allowMentions = gate.allow.find(isMentionRule)
50
- const allowFollowing = gate.allow.find(isFollowingRule)
60
+ const allowMentions = !!gate.allow.find(isMentionRule)
61
+ const allowFollowing = !!gate.allow.find(isFollowingRule)
51
62
  const allowListUris = gate.allow?.filter(isListRule).map((item) => item.list)
52
63
 
53
64
  // check mentions first since it's quick and synchronous
54
65
  if (allowMentions) {
55
- const isMentioned = root?.facets?.some((facet) => {
56
- return facet.features.some((item) => isMention(item) && item.did === did)
66
+ const isMentioned = rootPost?.facets?.some((facet) => {
67
+ return facet.features.some(
68
+ (item) => isMention(item) && item.did === replierDid,
69
+ )
57
70
  })
58
71
  if (isMentioned) {
59
- return false
72
+ return { canReply: true, allowMentions, allowFollowing, allowListUris }
60
73
  }
61
74
  }
75
+ return { allowMentions, allowFollowing, allowListUris }
76
+ }
62
77
 
63
- // check follows and list containment
64
- if (!allowFollowing && !allowListUris.length) {
78
+ export const violatesThreadGate = async (
79
+ db: DatabaseSchema,
80
+ replierDid: string,
81
+ ownerDid: string,
82
+ rootPost: PostRecord | null,
83
+ gate: GateRecord | null,
84
+ ) => {
85
+ const {
86
+ canReply,
87
+ allowFollowing,
88
+ allowListUris = [],
89
+ } = parseThreadGate(replierDid, ownerDid, rootPost, gate)
90
+ if (canReply) {
91
+ return false
92
+ }
93
+ if (!allowFollowing && !allowListUris?.length) {
65
94
  return true
66
95
  }
67
96
  const { ref } = db.dynamic
68
97
  const nullResult = sql<null>`${null}`
69
98
  const check = await db
70
- .selectFrom(valuesList([did]).as(sql`subject (did)`))
99
+ .selectFrom(valuesList([replierDid]).as(sql`subject (did)`))
71
100
  .select([
72
101
  allowFollowing
73
102
  ? db
74
103
  .selectFrom('follow')
75
- .where('creator', '=', owner)
104
+ .where('creator', '=', ownerDid)
76
105
  .whereRef('subjectDid', '=', ref('subject.did'))
77
106
  .select('creator')
78
107
  .as('isFollowed')
@@ -91,8 +120,7 @@ export const violatesThreadGate = async (
91
120
 
92
121
  if (allowFollowing && check?.isFollowed) {
93
122
  return false
94
- }
95
- if (allowListUris.length && check?.isInList) {
123
+ } else if (allowListUris.length && check?.isInList) {
96
124
  return false
97
125
  }
98
126
 
@@ -1,11 +1,11 @@
1
1
  import { mapDefined } from '@atproto/common'
2
+ import { AtUri } from '@atproto/syntax'
2
3
  import { Database } from '../../db'
3
4
  import {
4
5
  FeedViewPost,
5
6
  GeneratorView,
6
7
  PostView,
7
8
  } from '../../lexicon/types/app/bsky/feed/defs'
8
- import { isListRule } from '../../lexicon/types/app/bsky/feed/threadgate'
9
9
  import {
10
10
  Main as EmbedImages,
11
11
  isMain as isEmbedImages,
@@ -22,6 +22,8 @@ import {
22
22
  ViewNotFound,
23
23
  ViewRecord,
24
24
  } from '../../lexicon/types/app/bsky/embed/record'
25
+ import { Record as PostRecord } from '../../lexicon/types/app/bsky/feed/post'
26
+ import { isListRule } from '../../lexicon/types/app/bsky/feed/threadgate'
25
27
  import {
26
28
  PostEmbedViews,
27
29
  FeedGenInfo,
@@ -36,31 +38,45 @@ import {
36
38
  } from './types'
37
39
  import { Labels, getSelfLabels } from '../label'
38
40
  import { ImageUriBuilder } from '../../image/uri'
39
- import { LabelCache } from '../../label-cache'
40
41
  import { ActorInfoMap, ActorService } from '../actor'
41
42
  import { ListInfoMap, GraphService } from '../graph'
43
+ import { FromDb } from '../types'
44
+ import { parseThreadGate } from './util'
42
45
 
43
46
  export class FeedViews {
47
+ services: {
48
+ actor: ActorService
49
+ graph: GraphService
50
+ }
51
+
44
52
  constructor(
45
53
  public db: Database,
46
54
  public imgUriBuilder: ImageUriBuilder,
47
- public labelCache: LabelCache,
48
- ) {}
49
-
50
- static creator(imgUriBuilder: ImageUriBuilder, labelCache: LabelCache) {
51
- return (db: Database) => new FeedViews(db, imgUriBuilder, labelCache)
55
+ private actor: FromDb<ActorService>,
56
+ private graph: FromDb<GraphService>,
57
+ ) {
58
+ this.services = {
59
+ actor: actor(this.db),
60
+ graph: graph(this.db),
61
+ }
52
62
  }
53
63
 
54
- services = {
55
- actor: ActorService.creator(this.imgUriBuilder, this.labelCache)(this.db),
56
- graph: GraphService.creator(this.imgUriBuilder)(this.db),
64
+ static creator(
65
+ imgUriBuilder: ImageUriBuilder,
66
+ actor: FromDb<ActorService>,
67
+ graph: FromDb<GraphService>,
68
+ ) {
69
+ return (db: Database) => new FeedViews(db, imgUriBuilder, actor, graph)
57
70
  }
58
71
 
59
72
  formatFeedGeneratorView(
60
73
  info: FeedGenInfo,
61
74
  profiles: ActorInfoMap,
62
- ): GeneratorView {
75
+ ): GeneratorView | undefined {
63
76
  const profile = profiles[info.creator]
77
+ if (!profile) {
78
+ return undefined
79
+ }
64
80
  return {
65
81
  uri: info.uri,
66
82
  cid: info.cid,
@@ -91,8 +107,8 @@ export class FeedViews {
91
107
  formatFeed(
92
108
  items: FeedRow[],
93
109
  state: FeedHydrationState,
110
+ viewer: string | null,
94
111
  opts?: {
95
- viewer?: string | null
96
112
  usePostViewUnion?: boolean
97
113
  },
98
114
  ): FeedViewPost[] {
@@ -101,7 +117,7 @@ export class FeedViews {
101
117
  const actors = this.services.actor.views.profileBasicPresentation(
102
118
  Object.keys(profiles),
103
119
  state,
104
- opts,
120
+ viewer,
105
121
  )
106
122
  const feed: FeedViewPost[] = []
107
123
  for (const item of items) {
@@ -114,6 +130,7 @@ export class FeedViews {
114
130
  embeds,
115
131
  labels,
116
132
  lists,
133
+ viewer,
117
134
  )
118
135
  // skip over not found & blocked posts
119
136
  if (!post || blocks[post.uri]?.reply) {
@@ -149,6 +166,7 @@ export class FeedViews {
149
166
  labels,
150
167
  lists,
151
168
  blocks,
169
+ viewer,
152
170
  opts,
153
171
  )
154
172
  const replyRoot = this.formatMaybePostView(
@@ -160,6 +178,7 @@ export class FeedViews {
160
178
  labels,
161
179
  lists,
162
180
  blocks,
181
+ viewer,
163
182
  opts,
164
183
  )
165
184
  if (replyRoot && replyParent) {
@@ -182,6 +201,7 @@ export class FeedViews {
182
201
  embeds: PostEmbedViews,
183
202
  labels: Labels,
184
203
  lists: ListInfoMap,
204
+ viewer: string | null,
185
205
  ): PostView | undefined {
186
206
  const post = posts[uri]
187
207
  const gate = threadgates[uri]
@@ -207,6 +227,14 @@ export class FeedViews {
207
227
  ? {
208
228
  repost: post.requesterRepost ?? undefined,
209
229
  like: post.requesterLike ?? undefined,
230
+ replyDisabled: this.userReplyDisabled(
231
+ uri,
232
+ actors,
233
+ posts,
234
+ threadgates,
235
+ lists,
236
+ viewer,
237
+ ),
210
238
  }
211
239
  : undefined,
212
240
  labels: [...postLabels, ...postSelfLabels],
@@ -217,6 +245,50 @@ export class FeedViews {
217
245
  }
218
246
  }
219
247
 
248
+ userReplyDisabled(
249
+ uri: string,
250
+ actors: ActorInfoMap,
251
+ posts: PostInfoMap,
252
+ threadgates: ThreadgateInfoMap,
253
+ lists: ListInfoMap,
254
+ viewer: string | null,
255
+ ): boolean | undefined {
256
+ if (viewer === null) {
257
+ return undefined
258
+ } else if (posts[uri]?.violatesThreadGate) {
259
+ return true
260
+ }
261
+
262
+ const rootUriStr: string =
263
+ posts[uri]?.record?.['reply']?.['root']?.['uri'] ?? uri
264
+ const gate = threadgates[rootUriStr]?.record
265
+ if (!gate) {
266
+ return undefined
267
+ }
268
+ const rootPost = posts[rootUriStr]?.record as PostRecord | undefined
269
+ const ownerDid = new AtUri(rootUriStr).hostname
270
+
271
+ const {
272
+ canReply,
273
+ allowFollowing,
274
+ allowListUris = [],
275
+ } = parseThreadGate(viewer, ownerDid, rootPost ?? null, gate ?? null)
276
+
277
+ if (canReply) {
278
+ return false
279
+ }
280
+ if (allowFollowing && actors[ownerDid]?.viewer?.followedBy) {
281
+ return false
282
+ }
283
+ for (const listUri of allowListUris) {
284
+ const list = lists[listUri]
285
+ if (list?.viewerInList) {
286
+ return false
287
+ }
288
+ }
289
+ return true
290
+ }
291
+
220
292
  formatMaybePostView(
221
293
  uri: string,
222
294
  actors: ActorInfoMap,
@@ -226,6 +298,7 @@ export class FeedViews {
226
298
  labels: Labels,
227
299
  lists: ListInfoMap,
228
300
  blocks: PostBlocksMap,
301
+ viewer: string | null,
229
302
  opts?: {
230
303
  usePostViewUnion?: boolean
231
304
  },
@@ -238,6 +311,7 @@ export class FeedViews {
238
311
  embeds,
239
312
  labels,
240
313
  lists,
314
+ viewer,
241
315
  )
242
316
  if (!post) {
243
317
  if (!opts?.usePostViewUnion) return
@@ -4,6 +4,10 @@ import { ImageUriBuilder } from '../../image/uri'
4
4
  import { valuesList } from '../../db/util'
5
5
  import { ListInfo } from './types'
6
6
  import { ActorInfoMap } from '../actor'
7
+ import {
8
+ ListView,
9
+ ListViewBasic,
10
+ } from '../../lexicon/types/app/bsky/graph/defs'
7
11
 
8
12
  export * from './types'
9
13
 
@@ -91,6 +95,12 @@ export class GraphService {
91
95
  .whereRef('list_block.subjectUri', '=', ref('list.uri'))
92
96
  .select('list_block.uri')
93
97
  .as('viewerListBlockUri'),
98
+ this.db.db
99
+ .selectFrom('list_item')
100
+ .whereRef('list_item.listUri', '=', ref('list.uri'))
101
+ .where('list_item.subjectDid', '=', viewer ?? '')
102
+ .select('list_item.uri')
103
+ .as('viewerInList'),
94
104
  ])
95
105
  }
96
106
 
@@ -99,7 +109,11 @@ export class GraphService {
99
109
  .selectFrom('list_item')
100
110
  .innerJoin('actor as subject', 'subject.did', 'list_item.subjectDid')
101
111
  .selectAll('subject')
102
- .select(['list_item.cid as cid', 'list_item.sortAt as sortAt'])
112
+ .select([
113
+ 'list_item.uri as uri',
114
+ 'list_item.cid as cid',
115
+ 'list_item.sortAt as sortAt',
116
+ ])
103
117
  }
104
118
 
105
119
  async getBlockAndMuteState(
@@ -229,7 +243,10 @@ export class GraphService {
229
243
  )
230
244
  }
231
245
 
232
- formatListView(list: ListInfo, profiles: ActorInfoMap) {
246
+ formatListView(list: ListInfo, profiles: ActorInfoMap): ListView | undefined {
247
+ if (!profiles[list.creator]) {
248
+ return undefined
249
+ }
233
250
  return {
234
251
  ...this.formatListViewBasic(list),
235
252
  creator: profiles[list.creator],
@@ -237,10 +254,11 @@ export class GraphService {
237
254
  descriptionFacets: list.descriptionFacets
238
255
  ? JSON.parse(list.descriptionFacets)
239
256
  : undefined,
257
+ indexedAt: list.sortAt,
240
258
  }
241
259
  }
242
260
 
243
- formatListViewBasic(list: ListInfo) {
261
+ formatListViewBasic(list: ListInfo): ListViewBasic {
244
262
  return {
245
263
  uri: list.uri,
246
264
  cid: list.cid,
@@ -4,6 +4,7 @@ import { List } from '../../db/tables/list'
4
4
  export type ListInfo = Selectable<List> & {
5
5
  viewerMuted: string | null
6
6
  viewerListBlockUri: string | null
7
+ viewerInList: string | null
7
8
  }
8
9
 
9
10
  export type ListInfoMap = Record<string, ListInfo>