@atproto/bsky 0.0.251 → 0.0.253

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 (459) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/api/age-assurance/const.js +19 -19
  3. package/dist/api/age-assurance/const.js.map +1 -1
  4. package/dist/kws.d.ts +8 -0
  5. package/dist/kws.d.ts.map +1 -1
  6. package/dist/kws.js +53 -4
  7. package/dist/kws.js.map +1 -1
  8. package/package.json +28 -23
  9. package/bin/migration-create.ts +0 -46
  10. package/buf.gen.yaml +0 -12
  11. package/proto/bsky.proto +0 -1907
  12. package/proto/courier.proto +0 -84
  13. package/proto/rolodex.proto +0 -116
  14. package/src/api/age-assurance/const.ts +0 -203
  15. package/src/api/age-assurance/index.ts +0 -34
  16. package/src/api/age-assurance/kws/age-verified.test.ts +0 -98
  17. package/src/api/age-assurance/kws/age-verified.ts +0 -81
  18. package/src/api/age-assurance/kws/const.ts +0 -33
  19. package/src/api/age-assurance/kws/external-payload.test.ts +0 -72
  20. package/src/api/age-assurance/kws/external-payload.ts +0 -149
  21. package/src/api/age-assurance/redirects/kws-age-verified.ts +0 -107
  22. package/src/api/age-assurance/stash.ts +0 -23
  23. package/src/api/age-assurance/types.ts +0 -10
  24. package/src/api/age-assurance/util.ts +0 -68
  25. package/src/api/age-assurance/webhooks/kws-age-verified.ts +0 -75
  26. package/src/api/app/bsky/actor/getProfile.ts +0 -106
  27. package/src/api/app/bsky/actor/getProfiles.ts +0 -99
  28. package/src/api/app/bsky/actor/getSuggestions.ts +0 -163
  29. package/src/api/app/bsky/actor/searchActors.ts +0 -171
  30. package/src/api/app/bsky/actor/searchActorsTypeahead.ts +0 -164
  31. package/src/api/app/bsky/ageassurance/begin.ts +0 -175
  32. package/src/api/app/bsky/ageassurance/getConfig.ts +0 -16
  33. package/src/api/app/bsky/ageassurance/getState.ts +0 -58
  34. package/src/api/app/bsky/bookmark/createBookmark.ts +0 -39
  35. package/src/api/app/bsky/bookmark/deleteBookmark.ts +0 -32
  36. package/src/api/app/bsky/bookmark/getBookmarks.ts +0 -97
  37. package/src/api/app/bsky/bookmark/util.ts +0 -13
  38. package/src/api/app/bsky/contact/dismissMatch.ts +0 -26
  39. package/src/api/app/bsky/contact/getMatches.ts +0 -111
  40. package/src/api/app/bsky/contact/getSyncStatus.ts +0 -38
  41. package/src/api/app/bsky/contact/importContacts.ts +0 -119
  42. package/src/api/app/bsky/contact/removeData.ts +0 -25
  43. package/src/api/app/bsky/contact/sendNotification.ts +0 -33
  44. package/src/api/app/bsky/contact/startPhoneVerification.ts +0 -26
  45. package/src/api/app/bsky/contact/util.ts +0 -92
  46. package/src/api/app/bsky/contact/verifyPhone.ts +0 -29
  47. package/src/api/app/bsky/draft/createDraft.ts +0 -45
  48. package/src/api/app/bsky/draft/deleteDraft.ts +0 -20
  49. package/src/api/app/bsky/draft/getDrafts.ts +0 -44
  50. package/src/api/app/bsky/draft/updateDraft.ts +0 -26
  51. package/src/api/app/bsky/embed/getEmbedExternalView.ts +0 -154
  52. package/src/api/app/bsky/feed/getActorFeeds.ts +0 -101
  53. package/src/api/app/bsky/feed/getActorLikes.ts +0 -128
  54. package/src/api/app/bsky/feed/getAuthorFeed.ts +0 -279
  55. package/src/api/app/bsky/feed/getFeed.ts +0 -282
  56. package/src/api/app/bsky/feed/getFeedGenerator.ts +0 -68
  57. package/src/api/app/bsky/feed/getFeedGenerators.ts +0 -86
  58. package/src/api/app/bsky/feed/getLikes.ts +0 -151
  59. package/src/api/app/bsky/feed/getListFeed.ts +0 -147
  60. package/src/api/app/bsky/feed/getPostThread.ts +0 -156
  61. package/src/api/app/bsky/feed/getPosts.ts +0 -100
  62. package/src/api/app/bsky/feed/getQuotes.ts +0 -122
  63. package/src/api/app/bsky/feed/getRepostedBy.ts +0 -121
  64. package/src/api/app/bsky/feed/getSuggestedFeeds.ts +0 -39
  65. package/src/api/app/bsky/feed/getTimeline.ts +0 -127
  66. package/src/api/app/bsky/feed/searchPosts.ts +0 -274
  67. package/src/api/app/bsky/feed/searchPostsV2.ts +0 -243
  68. package/src/api/app/bsky/graph/getActorStarterPacks.ts +0 -103
  69. package/src/api/app/bsky/graph/getBlocks.ts +0 -94
  70. package/src/api/app/bsky/graph/getFollowers.ts +0 -153
  71. package/src/api/app/bsky/graph/getFollows.ts +0 -151
  72. package/src/api/app/bsky/graph/getKnownFollowers.ts +0 -123
  73. package/src/api/app/bsky/graph/getList.ts +0 -152
  74. package/src/api/app/bsky/graph/getListBlocks.ts +0 -87
  75. package/src/api/app/bsky/graph/getListMutes.ts +0 -87
  76. package/src/api/app/bsky/graph/getLists.ts +0 -125
  77. package/src/api/app/bsky/graph/getListsWithMembership.ts +0 -146
  78. package/src/api/app/bsky/graph/getMutes.ts +0 -87
  79. package/src/api/app/bsky/graph/getRelationships.ts +0 -58
  80. package/src/api/app/bsky/graph/getStarterPack.ts +0 -82
  81. package/src/api/app/bsky/graph/getStarterPacks.ts +0 -86
  82. package/src/api/app/bsky/graph/getStarterPacksWithMembership.ts +0 -136
  83. package/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +0 -171
  84. package/src/api/app/bsky/graph/muteActor.ts +0 -24
  85. package/src/api/app/bsky/graph/muteActorList.ts +0 -19
  86. package/src/api/app/bsky/graph/muteThread.ts +0 -19
  87. package/src/api/app/bsky/graph/searchStarterPacks.ts +0 -169
  88. package/src/api/app/bsky/graph/unmuteActor.ts +0 -21
  89. package/src/api/app/bsky/graph/unmuteActorList.ts +0 -19
  90. package/src/api/app/bsky/graph/unmuteThread.ts +0 -19
  91. package/src/api/app/bsky/labeler/getServices.ts +0 -41
  92. package/src/api/app/bsky/notification/getPreferences.ts +0 -49
  93. package/src/api/app/bsky/notification/getUnreadCount.ts +0 -83
  94. package/src/api/app/bsky/notification/listActivitySubscriptions.ts +0 -116
  95. package/src/api/app/bsky/notification/listNotifications.ts +0 -272
  96. package/src/api/app/bsky/notification/putActivitySubscription.ts +0 -69
  97. package/src/api/app/bsky/notification/putPreferences.ts +0 -17
  98. package/src/api/app/bsky/notification/putPreferencesV2.ts +0 -64
  99. package/src/api/app/bsky/notification/registerPush.ts +0 -40
  100. package/src/api/app/bsky/notification/unregisterPush.ts +0 -39
  101. package/src/api/app/bsky/notification/updateSeen.ts +0 -52
  102. package/src/api/app/bsky/notification/util.ts +0 -134
  103. package/src/api/app/bsky/unspecced/getAgeAssuranceState.ts +0 -48
  104. package/src/api/app/bsky/unspecced/getConfig.ts +0 -20
  105. package/src/api/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.ts +0 -162
  106. package/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +0 -91
  107. package/src/api/app/bsky/unspecced/getPostThreadOtherV2.ts +0 -126
  108. package/src/api/app/bsky/unspecced/getPostThreadV2.ts +0 -148
  109. package/src/api/app/bsky/unspecced/getSuggestedFeeds.ts +0 -100
  110. package/src/api/app/bsky/unspecced/getSuggestedOnboardingUsers.ts +0 -143
  111. package/src/api/app/bsky/unspecced/getSuggestedStarterPacks.ts +0 -165
  112. package/src/api/app/bsky/unspecced/getSuggestedUsers.ts +0 -183
  113. package/src/api/app/bsky/unspecced/getSuggestedUsersForDiscover.ts +0 -175
  114. package/src/api/app/bsky/unspecced/getSuggestedUsersForExplore.ts +0 -178
  115. package/src/api/app/bsky/unspecced/getSuggestedUsersForSeeMore.ts +0 -178
  116. package/src/api/app/bsky/unspecced/getTaggedSuggestions.ts +0 -27
  117. package/src/api/app/bsky/unspecced/getTrendingTopics.ts +0 -96
  118. package/src/api/app/bsky/unspecced/getTrends.ts +0 -162
  119. package/src/api/app/bsky/unspecced/initAgeAssurance.ts +0 -150
  120. package/src/api/blob-dispatcher.ts +0 -41
  121. package/src/api/blob-resolver.ts +0 -401
  122. package/src/api/com/atproto/admin/getAccountInfos.ts +0 -49
  123. package/src/api/com/atproto/admin/getSubjectStatus.ts +0 -79
  124. package/src/api/com/atproto/admin/updateSubjectStatus.ts +0 -80
  125. package/src/api/com/atproto/identity/resolveHandle.ts +0 -19
  126. package/src/api/com/atproto/label/queryLabels.ts +0 -42
  127. package/src/api/com/atproto/repo/getRecord.ts +0 -44
  128. package/src/api/com/atproto/temp/fetchLabels.ts +0 -9
  129. package/src/api/external.ts +0 -15
  130. package/src/api/health.ts +0 -47
  131. package/src/api/index.ts +0 -216
  132. package/src/api/internal/bsky/actor/getProfiles.ts +0 -87
  133. package/src/api/kws/api.ts +0 -113
  134. package/src/api/kws/index.ts +0 -29
  135. package/src/api/kws/types.ts +0 -78
  136. package/src/api/kws/util.test.ts +0 -50
  137. package/src/api/kws/util.ts +0 -109
  138. package/src/api/kws/webhook.ts +0 -134
  139. package/src/api/sitemap.ts +0 -76
  140. package/src/api/util.ts +0 -45
  141. package/src/api/well-known.ts +0 -44
  142. package/src/auth-verifier.ts +0 -522
  143. package/src/bsync.ts +0 -41
  144. package/src/cache/read-through.ts +0 -157
  145. package/src/config.ts +0 -719
  146. package/src/context.ts +0 -143
  147. package/src/courier.ts +0 -41
  148. package/src/data-plane/client/hosts.ts +0 -103
  149. package/src/data-plane/client/index.ts +0 -125
  150. package/src/data-plane/client/util.test.ts +0 -29
  151. package/src/data-plane/client/util.ts +0 -74
  152. package/src/data-plane/index.ts +0 -2
  153. package/src/data-plane/server/background.ts +0 -66
  154. package/src/data-plane/server/bsync-subscription.ts +0 -590
  155. package/src/data-plane/server/db/database-schema.ts +0 -90
  156. package/src/data-plane/server/db/db.ts +0 -204
  157. package/src/data-plane/server/db/index.ts +0 -2
  158. package/src/data-plane/server/db/migrations/20230309T045948368Z-init.ts +0 -305
  159. package/src/data-plane/server/db/migrations/20230408T152211201Z-notification-init.ts +0 -25
  160. package/src/data-plane/server/db/migrations/20230417T210628672Z-moderation-init.ts +0 -127
  161. package/src/data-plane/server/db/migrations/20230420T211446071Z-did-cache.ts +0 -14
  162. package/src/data-plane/server/db/migrations/20230427T194702079Z-notif-record-index.ts +0 -14
  163. package/src/data-plane/server/db/migrations/20230605T144730094Z-post-profile-aggs.ts +0 -23
  164. package/src/data-plane/server/db/migrations/20230607T211442112Z-feed-generator-init.ts +0 -34
  165. package/src/data-plane/server/db/migrations/20230608T155101190Z-algo-whats-hot-view.ts +0 -85
  166. package/src/data-plane/server/db/migrations/20230608T201813132Z-mute-lists.ts +0 -78
  167. package/src/data-plane/server/db/migrations/20230608T205147239Z-mutes.ts +0 -15
  168. package/src/data-plane/server/db/migrations/20230609T153623961Z-blocks.ts +0 -33
  169. package/src/data-plane/server/db/migrations/20230609T232122649Z-actor-deletion-indexes.ts +0 -27
  170. package/src/data-plane/server/db/migrations/20230610T203555962Z-suggested-follows.ts +0 -13
  171. package/src/data-plane/server/db/migrations/20230611T215300060Z-actor-state.ts +0 -13
  172. package/src/data-plane/server/db/migrations/20230620T161134972Z-post-langs.ts +0 -9
  173. package/src/data-plane/server/db/migrations/20230627T212437895Z-optional-handle.ts +0 -15
  174. package/src/data-plane/server/db/migrations/20230629T220835893Z-remove-post-hierarchy.ts +0 -32
  175. package/src/data-plane/server/db/migrations/20230703T045536691Z-feed-and-label-indices.ts +0 -31
  176. package/src/data-plane/server/db/migrations/20230720T164800037Z-posts-cursor-idx.ts +0 -19
  177. package/src/data-plane/server/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts +0 -14
  178. package/src/data-plane/server/db/migrations/20230808T172902639Z-repo-rev.ts +0 -12
  179. package/src/data-plane/server/db/migrations/20230810T203349843Z-action-duration.ts +0 -23
  180. package/src/data-plane/server/db/migrations/20230817T195936007Z-native-notifications.ts +0 -16
  181. package/src/data-plane/server/db/migrations/20230830T205507322Z-suggested-feeds.ts +0 -13
  182. package/src/data-plane/server/db/migrations/20230904T211011773Z-block-lists.ts +0 -24
  183. package/src/data-plane/server/db/migrations/20230906T222220386Z-thread-gating.ts +0 -27
  184. package/src/data-plane/server/db/migrations/20230920T213858047Z-add-tags-to-post.ts +0 -9
  185. package/src/data-plane/server/db/migrations/20230929T192920807Z-record-cursor-indexes.ts +0 -40
  186. package/src/data-plane/server/db/migrations/20231003T202833377Z-create-moderation-subject-status.ts +0 -123
  187. package/src/data-plane/server/db/migrations/20231220T225126090Z-blob-takedowns.ts +0 -66
  188. package/src/data-plane/server/db/migrations/20240124T023719200Z-tagged-suggestions.ts +0 -15
  189. package/src/data-plane/server/db/migrations/20240226T225725627Z-labelers.ts +0 -27
  190. package/src/data-plane/server/db/migrations/20240530T170337073Z-account-deactivation.ts +0 -12
  191. package/src/data-plane/server/db/migrations/20240606T171229898Z-thread-mutes.ts +0 -15
  192. package/src/data-plane/server/db/migrations/20240606T222548219Z-starter-packs.ts +0 -45
  193. package/src/data-plane/server/db/migrations/20240719T203853939Z-priority-notifs.ts +0 -25
  194. package/src/data-plane/server/db/migrations/20240723T220700077Z-quotes-post-aggs.ts +0 -12
  195. package/src/data-plane/server/db/migrations/20240723T220703655Z-quotes.ts +0 -28
  196. package/src/data-plane/server/db/migrations/20240801T193939827Z-post-gate.ts +0 -17
  197. package/src/data-plane/server/db/migrations/20240808T224251220Z-post-gate-flags.ts +0 -25
  198. package/src/data-plane/server/db/migrations/20240829T211238293Z-simplify-actor-sync.ts +0 -23
  199. package/src/data-plane/server/db/migrations/20240831T134810923Z-pinned-posts.ts +0 -17
  200. package/src/data-plane/server/db/migrations/20241114T153108102Z-add-starter-packs-name.ts +0 -19
  201. package/src/data-plane/server/db/migrations/20250116T222618297Z-post-embed-video.ts +0 -17
  202. package/src/data-plane/server/db/migrations/20250207T174822012Z-add-label-exp.ts +0 -9
  203. package/src/data-plane/server/db/migrations/20250404T163421487Z-verifications.ts +0 -39
  204. package/src/data-plane/server/db/migrations/20250526T023712742Z-like-repost-via.ts +0 -17
  205. package/src/data-plane/server/db/migrations/20250528T221913281Z-add-record-tags.ts +0 -9
  206. package/src/data-plane/server/db/migrations/20250602T190357447Z-add-private-data.ts +0 -22
  207. package/src/data-plane/server/db/migrations/20250611T140649895Z-add-activity-subscription.ts +0 -22
  208. package/src/data-plane/server/db/migrations/20250627T025331240Z-add-actor-age-assurance-columns.ts +0 -22
  209. package/src/data-plane/server/db/migrations/20250812T183735692Z-add-bookmarks.ts +0 -23
  210. package/src/data-plane/server/db/migrations/20250813T174955711Z-add-post-agg-bookmarks.ts +0 -12
  211. package/src/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.ts +0 -28
  212. package/src/data-plane/server/db/migrations/20260112T133951271Z-add-drafts.ts +0 -24
  213. package/src/data-plane/server/db/migrations/20260604T224952774Z-post-embed-gallery-image.ts +0 -19
  214. package/src/data-plane/server/db/migrations/index.ts +0 -60
  215. package/src/data-plane/server/db/migrations/provider.ts +0 -26
  216. package/src/data-plane/server/db/pagination.ts +0 -341
  217. package/src/data-plane/server/db/tables/activity-subscription.ts +0 -12
  218. package/src/data-plane/server/db/tables/actor-block.ts +0 -14
  219. package/src/data-plane/server/db/tables/actor-state.ts +0 -10
  220. package/src/data-plane/server/db/tables/actor-sync.ts +0 -9
  221. package/src/data-plane/server/db/tables/actor.ts +0 -19
  222. package/src/data-plane/server/db/tables/algo.ts +0 -11
  223. package/src/data-plane/server/db/tables/blob-takedown.ts +0 -9
  224. package/src/data-plane/server/db/tables/bookmark.ts +0 -11
  225. package/src/data-plane/server/db/tables/did-cache.ts +0 -13
  226. package/src/data-plane/server/db/tables/draft.ts +0 -11
  227. package/src/data-plane/server/db/tables/duplicate-record.ts +0 -12
  228. package/src/data-plane/server/db/tables/feed-generator.ts +0 -21
  229. package/src/data-plane/server/db/tables/feed-item.ts +0 -12
  230. package/src/data-plane/server/db/tables/follow.ts +0 -15
  231. package/src/data-plane/server/db/tables/label.ts +0 -13
  232. package/src/data-plane/server/db/tables/labeler.ts +0 -16
  233. package/src/data-plane/server/db/tables/like.ts +0 -18
  234. package/src/data-plane/server/db/tables/list-block.ts +0 -15
  235. package/src/data-plane/server/db/tables/list-item.ts +0 -16
  236. package/src/data-plane/server/db/tables/list-mute.ts +0 -9
  237. package/src/data-plane/server/db/tables/list.ts +0 -19
  238. package/src/data-plane/server/db/tables/mute.ts +0 -9
  239. package/src/data-plane/server/db/tables/notification-push-token.ts +0 -10
  240. package/src/data-plane/server/db/tables/notification.ts +0 -16
  241. package/src/data-plane/server/db/tables/post-agg.ts +0 -16
  242. package/src/data-plane/server/db/tables/post-embed.ts +0 -47
  243. package/src/data-plane/server/db/tables/post-gate.ts +0 -12
  244. package/src/data-plane/server/db/tables/post.ts +0 -28
  245. package/src/data-plane/server/db/tables/private-data.ts +0 -13
  246. package/src/data-plane/server/db/tables/profile-agg.ts +0 -14
  247. package/src/data-plane/server/db/tables/profile.ts +0 -17
  248. package/src/data-plane/server/db/tables/quote.ts +0 -15
  249. package/src/data-plane/server/db/tables/record.ts +0 -15
  250. package/src/data-plane/server/db/tables/repost.ts +0 -18
  251. package/src/data-plane/server/db/tables/starter-pack.ts +0 -17
  252. package/src/data-plane/server/db/tables/subscription.ts +0 -9
  253. package/src/data-plane/server/db/tables/suggested-feed.ts +0 -10
  254. package/src/data-plane/server/db/tables/suggested-follow.ts +0 -10
  255. package/src/data-plane/server/db/tables/tagged-suggestion.ts +0 -11
  256. package/src/data-plane/server/db/tables/thread-gate.ts +0 -12
  257. package/src/data-plane/server/db/tables/thread-mute.ts +0 -9
  258. package/src/data-plane/server/db/tables/verification.ts +0 -20
  259. package/src/data-plane/server/db/tables/view-param.ts +0 -12
  260. package/src/data-plane/server/db/types.ts +0 -12
  261. package/src/data-plane/server/db/util.ts +0 -67
  262. package/src/data-plane/server/index.ts +0 -44
  263. package/src/data-plane/server/indexing/index.ts +0 -458
  264. package/src/data-plane/server/indexing/plugins/block.ts +0 -84
  265. package/src/data-plane/server/indexing/plugins/chat-declaration.ts +0 -60
  266. package/src/data-plane/server/indexing/plugins/feed-generator.ts +0 -80
  267. package/src/data-plane/server/indexing/plugins/follow.ts +0 -132
  268. package/src/data-plane/server/indexing/plugins/germ-declaration.ts +0 -57
  269. package/src/data-plane/server/indexing/plugins/labeler.ts +0 -74
  270. package/src/data-plane/server/indexing/plugins/like.ts +0 -151
  271. package/src/data-plane/server/indexing/plugins/list-block.ts +0 -84
  272. package/src/data-plane/server/indexing/plugins/list-item.ts +0 -92
  273. package/src/data-plane/server/indexing/plugins/list.ts +0 -80
  274. package/src/data-plane/server/indexing/plugins/notif-declaration.ts +0 -57
  275. package/src/data-plane/server/indexing/plugins/post-gate.ts +0 -101
  276. package/src/data-plane/server/indexing/plugins/post.ts +0 -675
  277. package/src/data-plane/server/indexing/plugins/profile.ts +0 -90
  278. package/src/data-plane/server/indexing/plugins/repost.ts +0 -176
  279. package/src/data-plane/server/indexing/plugins/starter-pack.ts +0 -74
  280. package/src/data-plane/server/indexing/plugins/status.ts +0 -57
  281. package/src/data-plane/server/indexing/plugins/thread-gate.ts +0 -101
  282. package/src/data-plane/server/indexing/plugins/verification.ts +0 -113
  283. package/src/data-plane/server/indexing/processor.ts +0 -292
  284. package/src/data-plane/server/routes/activity-subscription.ts +0 -84
  285. package/src/data-plane/server/routes/blocks.ts +0 -127
  286. package/src/data-plane/server/routes/bookmarks.ts +0 -83
  287. package/src/data-plane/server/routes/drafts.ts +0 -37
  288. package/src/data-plane/server/routes/feed-gens.ts +0 -89
  289. package/src/data-plane/server/routes/feeds.ts +0 -176
  290. package/src/data-plane/server/routes/follows.ts +0 -141
  291. package/src/data-plane/server/routes/identity.ts +0 -59
  292. package/src/data-plane/server/routes/index.ts +0 -67
  293. package/src/data-plane/server/routes/interactions.ts +0 -120
  294. package/src/data-plane/server/routes/labels.ts +0 -55
  295. package/src/data-plane/server/routes/likes.ts +0 -92
  296. package/src/data-plane/server/routes/lists.ts +0 -88
  297. package/src/data-plane/server/routes/moderation.ts +0 -102
  298. package/src/data-plane/server/routes/mutes.ts +0 -195
  299. package/src/data-plane/server/routes/notifs.ts +0 -239
  300. package/src/data-plane/server/routes/profile.ts +0 -220
  301. package/src/data-plane/server/routes/quotes.ts +0 -32
  302. package/src/data-plane/server/routes/records.ts +0 -119
  303. package/src/data-plane/server/routes/relationships.ts +0 -174
  304. package/src/data-plane/server/routes/reposts.ts +0 -76
  305. package/src/data-plane/server/routes/search.ts +0 -181
  306. package/src/data-plane/server/routes/site-standard.ts +0 -12
  307. package/src/data-plane/server/routes/sitemap.ts +0 -43
  308. package/src/data-plane/server/routes/starter-packs.ts +0 -32
  309. package/src/data-plane/server/routes/suggestions.ts +0 -175
  310. package/src/data-plane/server/routes/sync.ts +0 -16
  311. package/src/data-plane/server/routes/threads.ts +0 -33
  312. package/src/data-plane/server/subscription.ts +0 -152
  313. package/src/data-plane/server/util.ts +0 -202
  314. package/src/error.ts +0 -12
  315. package/src/etcd.ts +0 -90
  316. package/src/feature-gates/README.md +0 -83
  317. package/src/feature-gates/gates.ts +0 -25
  318. package/src/feature-gates/index.ts +0 -235
  319. package/src/feature-gates/metrics.test.ts +0 -196
  320. package/src/feature-gates/metrics.ts +0 -107
  321. package/src/feature-gates/types.ts +0 -62
  322. package/src/feature-gates/utils.test.ts +0 -66
  323. package/src/feature-gates/utils.ts +0 -135
  324. package/src/hydration/actor.ts +0 -578
  325. package/src/hydration/external.test.ts +0 -281
  326. package/src/hydration/external.ts +0 -268
  327. package/src/hydration/feed.ts +0 -455
  328. package/src/hydration/graph.ts +0 -354
  329. package/src/hydration/hydrator.ts +0 -1835
  330. package/src/hydration/label.ts +0 -207
  331. package/src/hydration/util.ts +0 -199
  332. package/src/image/index.ts +0 -2
  333. package/src/image/invalidator.ts +0 -33
  334. package/src/image/logger.ts +0 -6
  335. package/src/image/server.ts +0 -228
  336. package/src/image/sharp.ts +0 -89
  337. package/src/image/uri.ts +0 -114
  338. package/src/image/util.ts +0 -41
  339. package/src/index.ts +0 -312
  340. package/src/kws.ts +0 -197
  341. package/src/logger.ts +0 -40
  342. package/src/pipeline.ts +0 -60
  343. package/src/redis.ts +0 -205
  344. package/src/rolodex.ts +0 -42
  345. package/src/stash.ts +0 -76
  346. package/src/util/debug.ts +0 -10
  347. package/src/util/http.ts +0 -42
  348. package/src/util/retry.ts +0 -10
  349. package/src/util/standard-site.test.ts +0 -423
  350. package/src/util/standard-site.ts +0 -196
  351. package/src/util/uris.ts +0 -38
  352. package/src/util.ts +0 -48
  353. package/src/views/index.ts +0 -2783
  354. package/src/views/threads-v2.ts +0 -510
  355. package/src/views/types.ts +0 -193
  356. package/src/views/util.ts +0 -114
  357. package/test.env +0 -2
  358. package/tests/__snapshots__/feed-generation.test.ts.snap +0 -2510
  359. package/tests/_util.ts +0 -292
  360. package/tests/admin/admin-auth.test.ts +0 -190
  361. package/tests/admin/moderation.test.ts +0 -222
  362. package/tests/auth.test.ts +0 -90
  363. package/tests/blob-resolver.test.ts +0 -125
  364. package/tests/data-plane/__snapshots__/indexing.test.ts.snap +0 -887
  365. package/tests/data-plane/db.test.ts +0 -189
  366. package/tests/data-plane/duplicate-records.test.ts +0 -156
  367. package/tests/data-plane/handle-invalidation.test.ts +0 -153
  368. package/tests/data-plane/indexing.test.ts +0 -827
  369. package/tests/data-plane/subscription.test.ts +0 -128
  370. package/tests/data-plane/thread-mutes.test.ts +0 -175
  371. package/tests/entryway-auth.test.ts +0 -177
  372. package/tests/etcd.test.ts +0 -302
  373. package/tests/feed-generation.test.ts +0 -965
  374. package/tests/hydration/util.test.ts +0 -119
  375. package/tests/image/server.test.ts +0 -97
  376. package/tests/image/sharp.test.ts +0 -199
  377. package/tests/image/uri.test.ts +0 -106
  378. package/tests/label-hydration.test.ts +0 -245
  379. package/tests/postgates.test.ts +0 -182
  380. package/tests/query-labels.test.ts +0 -85
  381. package/tests/redis-cache.test.ts +0 -230
  382. package/tests/seed/feed-hidden-replies.ts +0 -63
  383. package/tests/seed/get-suggested-starter-packs.ts +0 -64
  384. package/tests/seed/get-trends.ts +0 -71
  385. package/tests/seed/known-followers.ts +0 -111
  386. package/tests/seed/postgates.ts +0 -57
  387. package/tests/server.test.ts +0 -115
  388. package/tests/sitemap.test.ts +0 -75
  389. package/tests/stash.test.ts +0 -180
  390. package/tests/utils.test.ts +0 -36
  391. package/tests/views/__snapshots__/actor-search.test.ts.snap +0 -155
  392. package/tests/views/__snapshots__/author-feed.test.ts.snap +0 -6301
  393. package/tests/views/__snapshots__/block-lists.test.ts.snap +0 -690
  394. package/tests/views/__snapshots__/blocks.test.ts.snap +0 -462
  395. package/tests/views/__snapshots__/bookmarks.test.ts.snap +0 -379
  396. package/tests/views/__snapshots__/follows.test.ts.snap +0 -798
  397. package/tests/views/__snapshots__/labeler-service.test.ts.snap +0 -233
  398. package/tests/views/__snapshots__/likes.test.ts.snap +0 -124
  399. package/tests/views/__snapshots__/list-feed.test.ts.snap +0 -883
  400. package/tests/views/__snapshots__/lists.test.ts.snap +0 -650
  401. package/tests/views/__snapshots__/mute-lists.test.ts.snap +0 -705
  402. package/tests/views/__snapshots__/mutes.test.ts.snap +0 -353
  403. package/tests/views/__snapshots__/notifications.test.ts.snap +0 -3074
  404. package/tests/views/__snapshots__/posts.test.ts.snap +0 -1019
  405. package/tests/views/__snapshots__/profile.test.ts.snap +0 -811
  406. package/tests/views/__snapshots__/quotes.test.ts.snap +0 -445
  407. package/tests/views/__snapshots__/reposts.test.ts.snap +0 -112
  408. package/tests/views/__snapshots__/starter-packs.test.ts.snap +0 -836
  409. package/tests/views/__snapshots__/thread-v2.test.ts.snap +0 -1298
  410. package/tests/views/__snapshots__/thread.test.ts.snap +0 -2051
  411. package/tests/views/__snapshots__/threadgating.test.ts.snap +0 -189
  412. package/tests/views/__snapshots__/timeline.test.ts.snap +0 -7252
  413. package/tests/views/account-deactivation.test.ts +0 -81
  414. package/tests/views/actor-likes.test.ts +0 -138
  415. package/tests/views/actor-search.test.ts +0 -261
  416. package/tests/views/age-assurance-v2.test.ts +0 -823
  417. package/tests/views/age-assurance.test.ts +0 -479
  418. package/tests/views/author-feed.test.ts +0 -694
  419. package/tests/views/block-lists.test.ts +0 -661
  420. package/tests/views/blocks.test.ts +0 -889
  421. package/tests/views/bookmarks.test.ts +0 -345
  422. package/tests/views/drafts.test.ts +0 -430
  423. package/tests/views/feed-hidden-replies.test.ts +0 -243
  424. package/tests/views/feed-view-post.test.ts +0 -501
  425. package/tests/views/follows.test.ts +0 -372
  426. package/tests/views/get-config.test.ts +0 -64
  427. package/tests/views/get-suggested-onboarding-users.test.ts +0 -193
  428. package/tests/views/get-suggested-starter-packs.test.ts +0 -137
  429. package/tests/views/get-trends.test.ts +0 -141
  430. package/tests/views/internal-actor.test.ts +0 -127
  431. package/tests/views/known-followers.test.ts +0 -193
  432. package/tests/views/labeler-service.test.ts +0 -242
  433. package/tests/views/labels-needs-review.test.ts +0 -316
  434. package/tests/views/labels-takedown.test.ts +0 -247
  435. package/tests/views/likes.test.ts +0 -192
  436. package/tests/views/list-feed.test.ts +0 -195
  437. package/tests/views/lists.test.ts +0 -412
  438. package/tests/views/mute-lists.test.ts +0 -509
  439. package/tests/views/mutes.test.ts +0 -308
  440. package/tests/views/notifications.test.ts +0 -1581
  441. package/tests/views/post-search.test.ts +0 -281
  442. package/tests/views/posts-debug.test.ts +0 -79
  443. package/tests/views/posts.test.ts +0 -427
  444. package/tests/views/profile-debug.test.ts +0 -75
  445. package/tests/views/profile.test.ts +0 -795
  446. package/tests/views/quotes.test.ts +0 -133
  447. package/tests/views/reposts.test.ts +0 -112
  448. package/tests/views/starter-packs.test.ts +0 -395
  449. package/tests/views/suggestions.test.ts +0 -145
  450. package/tests/views/thread-v2.test.ts +0 -2005
  451. package/tests/views/thread.test.ts +0 -637
  452. package/tests/views/threadgating.test.ts +0 -843
  453. package/tests/views/timeline.test.ts +0 -328
  454. package/tests/views/verification.test.ts +0 -296
  455. package/tsconfig.build.json +0 -10
  456. package/tsconfig.build.tsbuildinfo +0 -1
  457. package/tsconfig.json +0 -7
  458. package/tsconfig.tests.json +0 -8
  459. package/vitest.config.ts +0 -10
@@ -1,2783 +0,0 @@
1
- import { HOUR, MINUTE, dedupeStrs, mapDefined } from '@atproto/common'
2
- import {
3
- $Typed,
4
- Un$Typed,
5
- Unknown$TypedObject,
6
- UriString,
7
- atUri,
8
- getBlobCidString,
9
- } from '@atproto/lex'
10
- import {
11
- AtUri,
12
- AtUriString,
13
- DatetimeString,
14
- DidString,
15
- INVALID_HANDLE,
16
- normalizeDatetimeAlways,
17
- } from '@atproto/syntax'
18
- import { Actor, ProfileViewerState } from '../hydration/actor.js'
19
- import {
20
- AssociatedSiteStandardRecord,
21
- SiteStandardDocument,
22
- SiteStandardPublication,
23
- getSiteStandardRecordsFromHydrationMapsByRefs,
24
- } from '../hydration/external.js'
25
- import { FeedItem, Like, Post, Repost } from '../hydration/feed.js'
26
- import { Follow, Verification } from '../hydration/graph.js'
27
- import { HydrationState } from '../hydration/hydrator.js'
28
- import { Label } from '../hydration/label.js'
29
- import { RecordInfo, parseString } from '../hydration/util.js'
30
- import { ImageUriBuilder } from '../image/uri.js'
31
- import { app, site } from '../lexicons/index.js'
32
- import { viewsLogger } from '../logger.js'
33
- import { Notification } from '../proto/bsky_pb.js'
34
- import {
35
- estimateReadingTimeMinutes,
36
- validateStandardSiteForUrl,
37
- } from '../util/standard-site.js'
38
- import {
39
- postUriToPostgateUri,
40
- postUriToThreadgateUri,
41
- safePinnedPost,
42
- uriToDid,
43
- uriToDid as creatorFromUri,
44
- } from '../util/uris.js'
45
- import {
46
- ThreadItemValueBlocked,
47
- ThreadItemValueNoUnauthenticated,
48
- ThreadItemValueNotFound,
49
- ThreadItemValuePost,
50
- ThreadOtherAnchorPostNode,
51
- ThreadOtherItemValuePost,
52
- ThreadOtherPostNode,
53
- ThreadTree,
54
- ThreadTreeVisible,
55
- sortTrimFlattenThreadTree,
56
- } from './threads-v2.js'
57
- import {
58
- ActivitySubscription,
59
- BlockedPost,
60
- BookmarkView,
61
- Embed,
62
- EmbedView,
63
- ExternalEmbed,
64
- ExternalEmbedColorRgb,
65
- ExternalEmbedSourceThemeView,
66
- ExternalEmbedSourceView,
67
- ExternalEmbedView,
68
- FeedViewPost,
69
- FollowRecord,
70
- GalleryEmbed,
71
- GalleryEmbedView,
72
- GalleryImageEmbed,
73
- GalleryImageEmbedView,
74
- GeneratorView,
75
- GetPostThreadV2QueryParams,
76
- ImagesEmbed,
77
- ImagesEmbedView,
78
- KnownFollowers,
79
- LabelerRecord,
80
- LabelerView,
81
- LabelerViewDetailed,
82
- LikeRecord,
83
- ListItemView,
84
- ListView,
85
- ListViewBasic,
86
- MaybePostView,
87
- NotFoundPost,
88
- NotificationRecordDeleted,
89
- NotificationView,
90
- PostEmbedView,
91
- PostRecord,
92
- PostView,
93
- ProfileAssociatedActivitySubscription,
94
- ProfileAssociatedChat,
95
- ProfileRecord,
96
- ProfileView,
97
- ProfileViewBasic,
98
- ProfileViewDetailed,
99
- ProfileViewer,
100
- ReasonPin,
101
- ReasonRepost,
102
- RecordEmbed,
103
- RecordEmbedView,
104
- RecordEmbedViewInternal,
105
- RecordWithMedia,
106
- RecordWithMediaView,
107
- ReplyRef,
108
- RepostRecord,
109
- SiteStandardPublicationRecord,
110
- StarterPackView,
111
- StarterPackViewBasic,
112
- StatusView,
113
- ThreadItem,
114
- ThreadOtherItem,
115
- ThreadViewPost,
116
- ThreadgateView,
117
- VerificationRecord,
118
- VerificationState,
119
- VerificationView,
120
- VideoEmbed,
121
- VideoEmbedView,
122
- isExternalEmbedType,
123
- isGalleryEmbedType,
124
- isGalleryImageEmbedType,
125
- isImagesEmbedType,
126
- isLabelerRecordType,
127
- isListRuleType,
128
- isPostRecordType,
129
- isPostViewType,
130
- isProfileRecordType,
131
- isRecordEmbedType,
132
- isRecordWithMediaType,
133
- isSelfLabelsType,
134
- isVideoEmbedType,
135
- } from './types.js'
136
- import { VideoUriBuilder, parsePostgate, parseThreadGate } from './util.js'
137
-
138
- const notificationDeletedRecord =
139
- app.bsky.notification.defs.recordDeleted.$build({})
140
-
141
- // Pre-computed CID for the `notificationDeletedRecord`.
142
- const notificationDeletedRecordCid =
143
- 'bafyreidad6nyekfa4a67yfb573ptxiv6s7kyxyg2ra6qbbemcruadvtuim'
144
-
145
- // Soft-limit for `app.bsky.embed.gallery#main.items`. The lexicon's
146
- // schema-level cap is 20, but clients are expected to enforce a soft limit
147
- // of 10 today. The AppView trims defensively at the view boundary.
148
- const GALLERY_SOFT_LIMIT = 10
149
-
150
- export class Views {
151
- public imgUriBuilder: ImageUriBuilder = this.opts.imgUriBuilder
152
- public videoUriBuilder: VideoUriBuilder = this.opts.videoUriBuilder
153
- public indexedAtEpoch: Date | undefined = this.opts.indexedAtEpoch
154
- private threadTagsBumpDown: readonly string[] = this.opts.threadTagsBumpDown
155
- private threadTagsHide: readonly string[] = this.opts.threadTagsHide
156
- private visibilityTagHide: string = this.opts.visibilityTagHide
157
- private visibilityTagRankPrefix: string = this.opts.visibilityTagRankPrefix
158
- constructor(
159
- private opts: {
160
- imgUriBuilder: ImageUriBuilder
161
- videoUriBuilder: VideoUriBuilder
162
- indexedAtEpoch: Date | undefined
163
- threadTagsBumpDown: readonly string[]
164
- threadTagsHide: readonly string[]
165
- visibilityTagHide: string
166
- visibilityTagRankPrefix: string
167
- },
168
- ) {}
169
-
170
- // Actor
171
- // ------------
172
-
173
- actorIsNoHosted(did: DidString, state: HydrationState): boolean {
174
- return (
175
- this.actorIsDeactivated(did, state) || this.actorIsTakendown(did, state)
176
- )
177
- }
178
-
179
- actorIsDeactivated(did: DidString, state: HydrationState): boolean {
180
- if (state.actors?.get(did)?.upstreamStatus === 'deactivated') return true
181
- return false
182
- }
183
-
184
- actorIsTakendown(did: DidString, state: HydrationState): boolean {
185
- const actor = state.actors?.get(did)
186
- if (actor?.takedownRef) return true
187
- if (actor?.upstreamStatus === 'takendown') return true
188
- if (actor?.upstreamStatus === 'suspended') return true
189
- if (state.labels?.get(did)?.isTakendown) return true
190
- return false
191
- }
192
-
193
- noUnauthenticatedPost(state: HydrationState, post: PostView): boolean {
194
- const isNoUnauthenticated = post.author.labels?.some(
195
- (l) => l.val === '!no-unauthenticated',
196
- )
197
- return !state.ctx?.viewer && !!isNoUnauthenticated
198
- }
199
-
200
- viewerBlockExists(did: DidString, state: HydrationState): boolean {
201
- if (state.ctx?.skipViewerBlocks) return false
202
- const viewer = state.profileViewers?.get(did)
203
- if (!viewer) return false
204
- return !!(
205
- viewer.blockedBy ||
206
- viewer.blocking ||
207
- this.blockedByList(viewer, state) ||
208
- this.blockingByList(viewer, state)
209
- )
210
- }
211
-
212
- viewerMuteExists(did: DidString, state: HydrationState): boolean {
213
- const viewer = state.profileViewers?.get(did)
214
- if (!viewer) return false
215
- return !!(viewer.muted || this.mutedByList(viewer, state))
216
- }
217
-
218
- blockingByList(
219
- viewer: ProfileViewerState,
220
- state: HydrationState,
221
- ): undefined | AtUriString {
222
- return (
223
- viewer.blockingByList && this.recordActive(viewer.blockingByList, state)
224
- )
225
- }
226
-
227
- blockedByList(
228
- viewer: ProfileViewerState,
229
- state: HydrationState,
230
- ): undefined | AtUriString {
231
- return (
232
- viewer.blockedByList && this.recordActive(viewer.blockedByList, state)
233
- )
234
- }
235
-
236
- mutedByList(
237
- viewer: ProfileViewerState,
238
- state: HydrationState,
239
- ): undefined | AtUriString {
240
- return viewer.mutedByList && this.recordActive(viewer.mutedByList, state)
241
- }
242
-
243
- recordActive(
244
- uri: AtUriString,
245
- state: HydrationState,
246
- ): AtUriString | undefined {
247
- const did = uriToDid(uri)
248
- const actor = state.actors?.get(did)
249
- if (!actor || this.actorIsTakendown(did, state)) {
250
- // actor may not be present when takedowns are eagerly applied during hydration.
251
- // so it's important to _try_ to hydrate the actor for records checked this way.
252
- return
253
- }
254
- return uri
255
- }
256
-
257
- viewerSeesNeedsReview(
258
- { did, uri }: { did?: DidString; uri?: AtUriString },
259
- state: HydrationState,
260
- ): boolean {
261
- const { labels, profileViewers, ctx } = state
262
- did = did || (uri && uriToDid(uri))
263
- if (!did) {
264
- return true
265
- }
266
- if (
267
- labels?.get(did)?.needsReview ||
268
- (uri && labels?.get(uri)?.needsReview)
269
- ) {
270
- // content marked as needs review
271
- return ctx?.viewer === did || !!profileViewers?.get(did)?.following
272
- }
273
- return true
274
- }
275
-
276
- replyIsHiddenByThreadgate(
277
- replyUri: AtUriString,
278
- rootPostUri: AtUriString,
279
- state: HydrationState,
280
- ) {
281
- const threadgateUri = postUriToThreadgateUri(rootPostUri)
282
- const threadgate = state.threadgates?.get(threadgateUri)
283
- return !!threadgate?.record?.hiddenReplies?.includes(replyUri)
284
- }
285
-
286
- profileDetailed(
287
- did: DidString,
288
- state: HydrationState,
289
- ): Un$Typed<ProfileViewDetailed> | undefined {
290
- const actor = state.actors?.get(did)
291
- if (!actor) return
292
- const baseView = this.profile(did, state)
293
- if (!baseView) return
294
- const knownFollowers = this.knownFollowers(did, state)
295
- const profileAggs = state.profileAggs?.get(did)
296
-
297
- return {
298
- ...baseView,
299
- website: this.profileWebsite(did, state),
300
- viewer: baseView.viewer
301
- ? {
302
- ...baseView.viewer,
303
- knownFollowers,
304
- }
305
- : undefined,
306
- banner: actor.profile?.banner
307
- ? this.imgUriBuilder.getPresetUri(
308
- 'banner',
309
- did,
310
- getBlobCidString(actor.profile.banner),
311
- )
312
- : undefined,
313
- followersCount: profileAggs?.followers ?? 0,
314
- followsCount: profileAggs?.follows ?? 0,
315
- postsCount: profileAggs?.posts ?? 0,
316
- associated: {
317
- lists: profileAggs?.lists,
318
- feedgens: profileAggs?.feeds,
319
- starterPacks: profileAggs?.starterPacks,
320
- labeler: actor.isLabeler,
321
- chat: this.profileAssociatedChat(actor),
322
- activitySubscription: this.profileAssociatedActivitySubscription(actor),
323
- germ: actor.germ?.record.messageMe
324
- ? {
325
- showButtonTo: actor.germ.record.messageMe.showButtonTo,
326
- messageMeUrl: actor.germ.record.messageMe.messageMeUrl,
327
- }
328
- : undefined,
329
- },
330
- joinedViaStarterPack: actor.profile?.joinedViaStarterPack
331
- ? this.starterPackBasic(actor.profile.joinedViaStarterPack.uri, state)
332
- : undefined,
333
- pinnedPost: safePinnedPost(actor.profile?.pinnedPost),
334
- }
335
- }
336
- profile(
337
- did: DidString,
338
- state: HydrationState,
339
- ): Un$Typed<ProfileView> | undefined {
340
- const actor = state.actors?.get(did)
341
- if (!actor) return
342
- const basicView = this.profileBasic(did, state)
343
- if (!basicView) return
344
- return {
345
- ...basicView,
346
- description: actor.profile?.description || undefined,
347
- indexedAt:
348
- actor.indexedAt && actor.sortedAt
349
- ? (this.indexedAt({
350
- sortedAt: actor.sortedAt,
351
- indexedAt: actor.indexedAt,
352
- }).toISOString() as DatetimeString)
353
- : undefined,
354
- }
355
- }
356
-
357
- profileBasic(
358
- did: DidString,
359
- state: HydrationState,
360
- ): Un$Typed<ProfileViewBasic> | undefined {
361
- const actor = state.actors?.get(did)
362
- if (!actor) return
363
- const profileUri = atUri(did, app.bsky.actor.profile)
364
- const labels = [
365
- ...(state.labels?.getBySubject(did) ?? []),
366
- ...(state.labels?.getBySubject(profileUri) ?? []),
367
- ...this.selfLabels({
368
- uri: profileUri,
369
- cid: actor.profileCid?.toString(),
370
- record: actor.profile,
371
- }),
372
- ]
373
- return {
374
- did,
375
- handle: actor.handle ?? INVALID_HANDLE,
376
- displayName: actor.profile?.displayName,
377
- pronouns: actor.profile?.pronouns,
378
- avatar: actor.profile?.avatar
379
- ? this.imgUriBuilder.getPresetUri(
380
- 'avatar',
381
- did,
382
- getBlobCidString(actor.profile.avatar),
383
- )
384
- : undefined,
385
- // associated.feedgens and associated.lists info not necessarily included
386
- // on profile and profile-basic views, but should be on profile-detailed.
387
- associated: {
388
- labeler: actor.isLabeler ? true : undefined,
389
- chat: this.profileAssociatedChat(actor),
390
- activitySubscription: this.profileAssociatedActivitySubscription(actor),
391
- germ: actor.germ?.record.messageMe
392
- ? {
393
- showButtonTo: actor.germ.record.messageMe.showButtonTo,
394
- messageMeUrl: actor.germ.record.messageMe.messageMeUrl,
395
- }
396
- : undefined,
397
- },
398
- viewer: this.profileViewer(did, state),
399
- labels,
400
- createdAt: actor.createdAt
401
- ? (actor.createdAt.toISOString() as DatetimeString)
402
- : undefined,
403
- verification: this.verification(did, state),
404
- status: this.status(did, state),
405
- debug: state.ctx?.includeDebugField ? actor.debug : undefined,
406
- }
407
- }
408
-
409
- profileAssociatedChat(actor: Actor): ProfileAssociatedChat | undefined {
410
- if (!actor.allowIncomingChatsFrom) return undefined
411
- return {
412
- allowIncoming: actor.allowIncomingChatsFrom,
413
- allowGroupInvites: actor.allowGroupChatInvitesFrom,
414
- }
415
- }
416
-
417
- profileAssociatedActivitySubscription(
418
- actor: Actor,
419
- ): ProfileAssociatedActivitySubscription {
420
- return { allowSubscriptions: actor.allowActivitySubscriptionsFrom }
421
- }
422
-
423
- profileKnownFollowers(
424
- did: DidString,
425
- state: HydrationState,
426
- ): ProfileView | undefined {
427
- const actor = state.actors?.get(did)
428
- if (!actor) return
429
- const baseView = this.profile(did, state)
430
- if (!baseView) return
431
- const knownFollowers = this.knownFollowers(did, state)
432
- return {
433
- ...baseView,
434
- viewer: baseView.viewer
435
- ? {
436
- ...baseView.viewer,
437
- knownFollowers,
438
- }
439
- : undefined,
440
- }
441
- }
442
-
443
- profileViewer(
444
- did: DidString,
445
- state: HydrationState,
446
- ): ProfileViewer | undefined {
447
- const viewer = state.profileViewers?.get(did)
448
- if (!viewer) return
449
- const blockedByList = this.blockedByList(viewer, state)
450
- const blockedByUri = viewer.blockedBy || blockedByList
451
- const blockingByList = this.blockingByList(viewer, state)
452
- const blockingUri = viewer.blocking || blockingByList
453
- const block = !!blockedByUri || !!blockingUri
454
- const mutedByList = this.mutedByList(viewer, state)
455
- return {
456
- muted: !!(viewer.muted || mutedByList),
457
- mutedByList: mutedByList ? this.listBasic(mutedByList, state) : undefined,
458
- blockedBy: !!blockedByUri,
459
- blocking: blockingUri,
460
- blockingByList: blockingByList
461
- ? this.listBasic(blockingByList, state)
462
- : undefined,
463
- following: viewer.following && !block ? viewer.following : undefined,
464
- followedBy: viewer.followedBy && !block ? viewer.followedBy : undefined,
465
- activitySubscription: this.profileViewerActivitySubscription(
466
- viewer,
467
- did,
468
- state,
469
- ),
470
- }
471
- }
472
-
473
- profileViewerActivitySubscription(
474
- profileViewer: ProfileViewerState,
475
- did: DidString,
476
- state: HydrationState,
477
- ): ActivitySubscription | undefined {
478
- const actor = state.actors?.get(did)
479
- if (!actor) return undefined
480
-
481
- const activitySubscription = state.activitySubscriptions?.get(did)
482
- if (!activitySubscription) return undefined
483
-
484
- const allowFrom = actor.allowActivitySubscriptionsFrom
485
- const actorFollowsViewer = !!profileViewer.followedBy
486
- const viewerFollowsActor = !!profileViewer.following
487
- if (
488
- (allowFrom === 'followers' && viewerFollowsActor) ||
489
- (allowFrom === 'mutuals' && actorFollowsViewer && viewerFollowsActor)
490
- ) {
491
- return activitySubscription
492
- }
493
- return undefined
494
- }
495
-
496
- profileWebsite(did: DidString, state: HydrationState): UriString | undefined {
497
- const actor = state.actors?.get(did)
498
- if (!actor?.profile?.website) return
499
- const { website } = actor.profile
500
-
501
- // The record property accepts any URI, but we don't want
502
- // to pass the client any schemes other than HTTPS.
503
- return website.startsWith('https://') ? website : undefined
504
- }
505
-
506
- knownFollowers(
507
- did: DidString,
508
- state: HydrationState,
509
- ): KnownFollowers | undefined {
510
- const knownFollowers = state.knownFollowers?.get(did)
511
- if (!knownFollowers) return
512
- const blocks = state.bidirectionalBlocks?.get(did)
513
- const followers = mapDefined(knownFollowers.followers, (followerDid) => {
514
- if (this.viewerBlockExists(followerDid, state)) {
515
- return undefined
516
- }
517
- if (blocks?.get(followerDid)) {
518
- return undefined
519
- }
520
- if (this.actorIsNoHosted(followerDid, state)) {
521
- // @TODO only needed right now to work around getProfile's { includeTakedowns: true }
522
- return undefined
523
- }
524
- return this.profileBasic(followerDid, state)
525
- })
526
- return { count: knownFollowers.count, followers }
527
- }
528
-
529
- verification(
530
- did: DidString,
531
- state: HydrationState,
532
- ): VerificationState | undefined {
533
- const actor = state.actors?.get(did)
534
- if (!actor) return
535
-
536
- // Currently, the handle comes as "handle.invalid" from the production dataplane.
537
- // But the contract allows for empty handle, so we cover both cases.
538
- if (!actor.handle || actor.handle === INVALID_HANDLE) return
539
-
540
- const isImpersonation = state.labels?.get(did)?.isImpersonation
541
-
542
- const verifications = actor.verifications.map(
543
- ({ issuer, uri, displayName, handle, createdAt }): VerificationView => {
544
- // @NOTE: We don't factor-in impersonation when evaluating the validity of each verification,
545
- // only in the overall profile verification validity.
546
- // The verification record's `displayName`/`handle` are the *subject's* snapshot at
547
- // verification time; we compare them to the subject's current values to determine validity.
548
- const isValid =
549
- !!displayName &&
550
- displayName === actor.profile?.displayName &&
551
- !!handle &&
552
- handle === actor.handle
553
-
554
- // Expose the *issuer's* current handle/displayName, sourced from the
555
- // issuer's hydrated actor record (see `Hydrator.hydrateProfiles`).
556
- const issuerActor = state.actors?.get(issuer)
557
-
558
- return {
559
- issuer,
560
- issuerDisplayName: issuerActor?.profile?.displayName,
561
- issuerHandle: issuerActor?.handle,
562
- uri,
563
- isValid,
564
- createdAt,
565
- }
566
- },
567
- )
568
- const hasValidVerification = verifications.some((v) => v.isValid)
569
-
570
- const verifiedStatus = verifications.length
571
- ? hasValidVerification && !isImpersonation
572
- ? 'valid'
573
- : 'invalid'
574
- : 'none'
575
- const trustedVerifierStatus = actor.trustedVerifier
576
- ? isImpersonation
577
- ? 'invalid'
578
- : 'valid'
579
- : 'none'
580
-
581
- if (
582
- verifications.length === 0 &&
583
- verifiedStatus === 'none' &&
584
- trustedVerifierStatus === 'none'
585
- ) {
586
- return undefined
587
- }
588
-
589
- return {
590
- verifications,
591
- verifiedStatus,
592
- trustedVerifierStatus,
593
- }
594
- }
595
-
596
- status(did: DidString, state: HydrationState): StatusView | undefined {
597
- const actor = state.actors?.get(did)
598
- if (!actor?.status) return
599
-
600
- const isViewerStatusOwner = did === state.ctx?.viewer
601
- const { status } = actor
602
- const { record, sortedAt, cid, takedownRef } = status
603
- const isTakenDown = !!takedownRef
604
-
605
- /*
606
- * Manual filter for takendown status records. If this is ever removed, we
607
- * need to reinstate `includeTakedowns` handling in the `Actor.getActors`
608
- * hydrator.
609
- */
610
- if (isTakenDown && !isViewerStatusOwner) {
611
- return undefined
612
- }
613
-
614
- const uri = atUri(did, app.bsky.actor.status)
615
- const labels = state.labels?.getBySubject(uri)
616
-
617
- const minDuration = 5 * MINUTE
618
- const maxDuration = 4 * HOUR
619
-
620
- const expiresAtMs = record.durationMinutes
621
- ? sortedAt.getTime() +
622
- Math.max(
623
- Math.min(record.durationMinutes * MINUTE, maxDuration),
624
- minDuration,
625
- )
626
- : undefined
627
- const expiresAt = expiresAtMs
628
- ? (new Date(expiresAtMs).toISOString() as DatetimeString)
629
- : undefined
630
-
631
- const isActive = expiresAtMs ? expiresAtMs > Date.now() : undefined
632
-
633
- const response: StatusView = {
634
- uri,
635
- cid,
636
- record: record,
637
- status: record.status,
638
- embed:
639
- record.embed && isExternalEmbedType(record.embed)
640
- ? this.externalEmbed(did, record.embed, state)
641
- : undefined,
642
- labels,
643
- expiresAt,
644
- isActive,
645
- }
646
-
647
- if (isViewerStatusOwner) {
648
- response.isDisabled = isTakenDown
649
- }
650
-
651
- return response
652
- }
653
-
654
- blockedProfileViewer(
655
- did: DidString,
656
- state: HydrationState,
657
- ): ProfileViewer | undefined {
658
- const viewer = state.profileViewers?.get(did)
659
- if (!viewer) return
660
- const blockedByUri = viewer.blockedBy || this.blockedByList(viewer, state)
661
- const blockingUri = viewer.blocking || this.blockingByList(viewer, state)
662
- return {
663
- blockedBy: !!blockedByUri,
664
- blocking: blockingUri,
665
- }
666
- }
667
-
668
- // Graph
669
- // ------------
670
-
671
- list(
672
- uri: AtUriString,
673
- state: HydrationState,
674
- ): Un$Typed<ListView> | undefined {
675
- const creatorDid = creatorFromUri(uri)
676
- const list = state.lists?.get(uri)
677
- if (!list) return
678
- const creator = this.profile(creatorDid, state)
679
- if (!creator) return
680
- const basicView = this.listBasic(uri, state)
681
- if (!basicView) return
682
-
683
- return {
684
- ...basicView,
685
- creator,
686
- description: list.record.description,
687
- descriptionFacets: list.record.descriptionFacets,
688
- indexedAt: this.indexedAt(list).toISOString() as DatetimeString,
689
- }
690
- }
691
-
692
- listBasic(
693
- uri: AtUriString,
694
- state: HydrationState,
695
- ): Un$Typed<ListViewBasic> | undefined {
696
- const list = state.lists?.get(uri)
697
- if (!list) {
698
- return undefined
699
- }
700
- const listAgg = state.listAggs?.get(uri)
701
- const listViewer = state.listViewers?.get(uri)
702
- const labels = state.labels?.getBySubject(uri) ?? []
703
- const creator = creatorFromUri(uri)
704
- return {
705
- uri,
706
- cid: list.cid,
707
- name: list.record.name,
708
- purpose: list.record.purpose,
709
- avatar: list.record.avatar
710
- ? this.imgUriBuilder.getPresetUri(
711
- 'avatar',
712
- creator,
713
- getBlobCidString(list.record.avatar),
714
- )
715
- : undefined,
716
- listItemCount: listAgg?.listItems ?? 0,
717
- indexedAt: this.indexedAt(list).toISOString() as DatetimeString,
718
- labels,
719
- viewer: listViewer
720
- ? {
721
- muted: !!listViewer.viewerMuted,
722
- blocked: listViewer.viewerListBlockUri,
723
- }
724
- : undefined,
725
- }
726
- }
727
-
728
- listItemView(
729
- uri: AtUriString,
730
- did: DidString,
731
- state: HydrationState,
732
- ): Un$Typed<ListItemView> | undefined {
733
- const subject = this.profile(did, state)
734
- if (!subject) return
735
- return { uri, subject }
736
- }
737
-
738
- starterPackBasic(
739
- uri: AtUriString,
740
- state: HydrationState,
741
- ): Un$Typed<StarterPackViewBasic> | undefined {
742
- const sp = state.starterPacks?.get(uri)
743
- if (!sp) return
744
- const parsedUri = new AtUri(uri)
745
- const creator = this.profileBasic(parsedUri.did, state)
746
- if (!creator) return
747
- const agg = state.starterPackAggs?.get(uri)
748
- const labels = state.labels?.getBySubject(uri) ?? []
749
- return {
750
- uri,
751
- cid: sp.cid,
752
- record: sp.record,
753
- creator,
754
- joinedAllTimeCount: agg?.joinedAllTime ?? 0,
755
- joinedWeekCount: agg?.joinedWeek ?? 0,
756
- labels,
757
- indexedAt: this.indexedAt(sp).toISOString() as DatetimeString,
758
- }
759
- }
760
-
761
- starterPack(
762
- uri: AtUriString,
763
- state: HydrationState,
764
- ): Un$Typed<StarterPackView> | undefined {
765
- const sp = state.starterPacks?.get(uri)
766
- const basicView = this.starterPackBasic(uri, state)
767
- if (!sp || !basicView) return
768
- const agg = state.starterPackAggs?.get(uri)
769
- const feeds = mapDefined(sp.record.feeds ?? [], (feed) =>
770
- this.feedGenerator(feed.uri, state),
771
- )
772
- const list = this.listBasic(sp.record.list, state)
773
- const listItemsSample = mapDefined(agg?.listItemSampleUris ?? [], (uri) => {
774
- const li = state.listItems?.get(uri)
775
- if (!li) return
776
- const subject = this.profile(li.record.subject, state)
777
- if (!subject) return
778
- return { uri, subject }
779
- })
780
- return {
781
- ...basicView,
782
- feeds,
783
- list,
784
- listItemsSample,
785
- }
786
- }
787
-
788
- // Labels
789
- // ------------
790
-
791
- selfLabels({
792
- uri,
793
- cid,
794
- record,
795
- }: {
796
- uri?: AtUriString
797
- cid?: string
798
- record?:
799
- | PostRecord
800
- | LikeRecord
801
- | RepostRecord
802
- | FollowRecord
803
- | ProfileRecord
804
- | LabelerRecord
805
- | VerificationRecord
806
- | NotificationRecordDeleted
807
- }): Label[] {
808
- if (!uri || !cid || !record) return []
809
-
810
- // Only these have a "labels" property:
811
- if (
812
- !isPostRecordType(record) &&
813
- !isProfileRecordType(record) &&
814
- !isLabelerRecordType(record)
815
- ) {
816
- return []
817
- }
818
-
819
- // Ignore if no labels defines
820
- if (
821
- !record.labels ||
822
- !isSelfLabelsType(record.labels) ||
823
- !record.labels.values.length
824
- ) {
825
- return []
826
- }
827
-
828
- const src = creatorFromUri(uri) // record creator
829
- const cts =
830
- typeof record.createdAt === 'string'
831
- ? normalizeDatetimeAlways(record.createdAt)
832
- : (new Date(0).toISOString() as DatetimeString)
833
- return record.labels.values.map(({ val }) => {
834
- return { src, uri, cid, val, cts }
835
- })
836
- }
837
-
838
- labeler(
839
- did: DidString,
840
- state: HydrationState,
841
- ): Un$Typed<LabelerView> | undefined {
842
- const labeler = state.labelers?.get(did)
843
- if (!labeler) return
844
- const creator = this.profile(did, state)
845
- if (!creator) return
846
- const viewer = state.labelerViewers?.get(did)
847
- const aggs = state.labelerAggs?.get(did)
848
-
849
- const uri = atUri(did, app.bsky.labeler.service)
850
- const labels = [
851
- ...(state.labels?.getBySubject(uri) ?? []),
852
- ...this.selfLabels({
853
- uri,
854
- cid: labeler.cid.toString(),
855
- record: labeler.record,
856
- }),
857
- ]
858
-
859
- return {
860
- uri,
861
- cid: labeler.cid.toString(),
862
- creator,
863
- likeCount: aggs?.likes ?? 0,
864
- viewer: viewer
865
- ? {
866
- like: viewer.like,
867
- }
868
- : undefined,
869
- indexedAt: this.indexedAt(labeler).toISOString() as DatetimeString,
870
- labels,
871
- }
872
- }
873
-
874
- labelerDetailed(
875
- did: DidString,
876
- state: HydrationState,
877
- ): Un$Typed<LabelerViewDetailed> | undefined {
878
- const baseView = this.labeler(did, state)
879
- if (!baseView) return
880
- const labeler = state.labelers?.get(did)
881
- if (!labeler) return
882
-
883
- return {
884
- ...baseView,
885
- policies: labeler.record.policies,
886
- reasonTypes: labeler.record.reasonTypes,
887
- subjectTypes: labeler.record.subjectTypes,
888
- subjectCollections: labeler.record.subjectCollections,
889
- }
890
- }
891
-
892
- // Feed
893
- // ------------
894
-
895
- feedItemBlocksAndMutes(
896
- item: FeedItem,
897
- state: HydrationState,
898
- ): {
899
- originatorMuted: boolean
900
- originatorBlocked: boolean
901
- authorMuted: boolean
902
- authorBlocked: boolean
903
- ancestorAuthorBlocked: boolean
904
- } {
905
- const authorDid = creatorFromUri(item.post.uri)
906
- const originatorDid = item.repost
907
- ? creatorFromUri(item.repost.uri)
908
- : authorDid
909
- const post = state.posts?.get(item.post.uri)
910
- const parentUri = post?.record.reply?.parent.uri
911
- const parentAuthorDid = parentUri && creatorFromUri(parentUri)
912
- const parent = parentUri ? state.posts?.get(parentUri) : undefined
913
- const grandparentUri = parent?.record.reply?.parent.uri
914
- const grandparentAuthorDid =
915
- grandparentUri && creatorFromUri(grandparentUri)
916
- return {
917
- originatorMuted: this.viewerMuteExists(originatorDid, state),
918
- originatorBlocked: this.viewerBlockExists(originatorDid, state),
919
- authorMuted: this.viewerMuteExists(authorDid, state),
920
- authorBlocked: this.viewerBlockExists(authorDid, state),
921
- ancestorAuthorBlocked:
922
- (!!parentAuthorDid && this.viewerBlockExists(parentAuthorDid, state)) ||
923
- (!!grandparentAuthorDid &&
924
- this.viewerBlockExists(grandparentAuthorDid, state)),
925
- }
926
- }
927
-
928
- feedGenerator(
929
- uri: AtUriString,
930
- state: HydrationState,
931
- ): Un$Typed<GeneratorView> | undefined {
932
- const feedgen = state.feedgens?.get(uri)
933
- if (!feedgen) return
934
- const creatorDid = creatorFromUri(uri)
935
- const creator = this.profile(creatorDid, state)
936
- if (!creator) return
937
- const viewer = state.feedgenViewers?.get(uri)
938
- const aggs = state.feedgenAggs?.get(uri)
939
- const labels = state.labels?.getBySubject(uri) ?? []
940
-
941
- return {
942
- uri,
943
- cid: feedgen.cid,
944
- did: feedgen.record.did,
945
- creator,
946
- displayName: feedgen.record.displayName,
947
- description: feedgen.record.description,
948
- descriptionFacets: feedgen.record.descriptionFacets,
949
- avatar: feedgen.record.avatar
950
- ? this.imgUriBuilder.getPresetUri(
951
- 'avatar',
952
- creatorDid,
953
- getBlobCidString(feedgen.record.avatar),
954
- )
955
- : undefined,
956
- likeCount: aggs?.likes ?? 0,
957
- acceptsInteractions: feedgen.record.acceptsInteractions,
958
- labels,
959
- viewer: viewer
960
- ? {
961
- like: viewer.like,
962
- }
963
- : undefined,
964
- contentMode: feedgen.record.contentMode,
965
- indexedAt: this.indexedAt(feedgen).toISOString() as DatetimeString,
966
- }
967
- }
968
-
969
- threadgate(
970
- uri: AtUriString,
971
- state: HydrationState,
972
- ): Un$Typed<ThreadgateView> | undefined {
973
- const gate = state.threadgates?.get(uri)
974
- if (!gate) return
975
- return {
976
- uri,
977
- cid: gate.cid,
978
- record: gate.record,
979
- lists: mapDefined(gate.record.allow ?? [], (rule) => {
980
- if (!isListRuleType(rule)) return
981
- return this.listBasic(rule.list, state)
982
- }),
983
- }
984
- }
985
-
986
- post(
987
- uri: AtUriString,
988
- state: HydrationState,
989
- depth = 0,
990
- ): Un$Typed<PostView> | undefined {
991
- const post = state.posts?.get(uri)
992
- if (!post) return
993
- const parsedUri = new AtUri(uri)
994
- const authorDid = parsedUri.did
995
- const author = this.profileBasic(authorDid, state)
996
- if (!author) return
997
- const aggs = state.postAggs?.get(uri)
998
- const viewer = state.postViewers?.get(uri)
999
- const threadgateUri = postUriToThreadgateUri(uri)
1000
- const labels = [
1001
- ...(state.labels?.getBySubject(uri) ?? []),
1002
- ...this.selfLabels({
1003
- uri,
1004
- cid: post.cid,
1005
- record: post.record,
1006
- }),
1007
- ]
1008
- return {
1009
- uri,
1010
- cid: post.cid,
1011
- author,
1012
- record: post.record,
1013
- embed:
1014
- depth < 2 && post.record.embed
1015
- ? this.embed(uri, post.record.embed, state, depth + 1)
1016
- : undefined,
1017
- bookmarkCount: aggs?.bookmarks ?? 0,
1018
- replyCount: aggs?.replies ?? 0,
1019
- repostCount: aggs?.reposts ?? 0,
1020
- likeCount: aggs?.likes ?? 0,
1021
- quoteCount: aggs?.quotes ?? 0,
1022
- indexedAt: this.indexedAt(post).toISOString() as DatetimeString,
1023
- viewer: viewer
1024
- ? {
1025
- repost: viewer.repost,
1026
- like: viewer.like,
1027
- bookmarked: viewer.bookmarked,
1028
- threadMuted: viewer.threadMuted,
1029
- replyDisabled: this.userReplyDisabled(uri, state),
1030
- embeddingDisabled: this.userPostEmbeddingDisabled(uri, state),
1031
- pinned: this.viewerPinned(uri, state, authorDid),
1032
- }
1033
- : undefined,
1034
- labels,
1035
- threadgate: !post.record.reply // only hydrate gate on root post
1036
- ? this.threadgate(threadgateUri, state)
1037
- : undefined,
1038
- debug: state.ctx?.includeDebugField
1039
- ? { post: post.debug, author: author.debug }
1040
- : undefined,
1041
- }
1042
- }
1043
-
1044
- feedViewPost(
1045
- item: FeedItem,
1046
- state: HydrationState,
1047
- ): Un$Typed<FeedViewPost> | undefined {
1048
- const postInfo = state.posts?.get(item.post.uri)
1049
- let reason: $Typed<ReasonRepost> | $Typed<ReasonPin> | undefined
1050
- if (item.authorPinned) {
1051
- reason = this.reasonPin()
1052
- } else if (item.repost) {
1053
- const repost = state.reposts?.get(item.repost.uri)
1054
- if (!repost) return
1055
- if (repost.record.subject.uri !== item.post.uri) return
1056
- reason = this.reasonRepost(item.repost.uri, repost, state)
1057
- if (!reason) return
1058
- }
1059
- const post = this.post(item.post.uri, state)
1060
- if (!post) return
1061
- const reply = !postInfo?.violatesThreadGate
1062
- ? this.replyRef(item.post.uri, state)
1063
- : undefined
1064
- return {
1065
- post,
1066
- reason,
1067
- reply,
1068
- }
1069
- }
1070
-
1071
- replyRef(
1072
- uri: AtUriString,
1073
- state: HydrationState,
1074
- ): Un$Typed<ReplyRef> | undefined {
1075
- const postRecord = state.posts?.get(uri)?.record
1076
- if (!postRecord?.reply) return
1077
- let root = this.maybePost(postRecord.reply.root.uri, state)
1078
- let parent = this.maybePost(postRecord.reply.parent.uri, state)
1079
- if (!state.ctx?.include3pBlocks) {
1080
- const childBlocks = state.postBlocks?.get(uri)
1081
- const parentBlocks = state.postBlocks?.get(parent.uri)
1082
- // if child blocks parent, block parent
1083
- if (isPostViewType(parent) && childBlocks?.parent) {
1084
- parent = this.blockedPost(parent.uri, parent.author.did, state)
1085
- }
1086
- // if child or parent blocks root, block root
1087
- if (isPostViewType(root) && (childBlocks?.root || parentBlocks?.root)) {
1088
- root = this.blockedPost(root.uri, root.author.did, state)
1089
- }
1090
- }
1091
- let grandparentAuthor: ProfileViewBasic | undefined
1092
- if (isPostViewType(parent)) {
1093
- // @NOTE The "parent.record" property is of type "unknown" in the lexicon
1094
- // schema, which means that it is typed as LexMap here. In order to avoid
1095
- // (expensive) validation using "isPostRecord(parent.record)", we only
1096
- // check that the "$type" property is a post record type, then use a
1097
- // try/catch to "validate" the post uri.
1098
- if (isPostRecordType(parent.record) && parent.record.reply != null) {
1099
- const uri = (parent.record.reply as any).parent?.uri
1100
- if (typeof uri === 'string') {
1101
- try {
1102
- grandparentAuthor = this.profileBasic(creatorFromUri(uri), state)
1103
- } catch {
1104
- // ignore (just as if validation had failed)
1105
- }
1106
- }
1107
- }
1108
- }
1109
- return {
1110
- root,
1111
- parent,
1112
- grandparentAuthor,
1113
- }
1114
- }
1115
-
1116
- maybePost(uri: AtUriString, state: HydrationState): $Typed<MaybePostView> {
1117
- const post = this.post(uri, state)
1118
- if (!post) {
1119
- return this.notFoundPost(uri)
1120
- }
1121
- if (this.viewerBlockExists(post.author.did, state)) {
1122
- return this.blockedPost(uri, post.author.did, state)
1123
- }
1124
- return app.bsky.feed.defs.postView.$build(post)
1125
- }
1126
-
1127
- blockedPost(
1128
- uri: AtUriString,
1129
- authorDid: DidString,
1130
- state: HydrationState,
1131
- ): $Typed<BlockedPost> {
1132
- return app.bsky.feed.defs.blockedPost.$build({
1133
- uri,
1134
- blocked: true,
1135
- author: {
1136
- did: authorDid,
1137
- viewer: this.blockedProfileViewer(authorDid, state),
1138
- },
1139
- })
1140
- }
1141
-
1142
- notFoundPost(uri: AtUriString): $Typed<NotFoundPost> {
1143
- return app.bsky.feed.defs.notFoundPost.$build({
1144
- uri,
1145
- notFound: true,
1146
- })
1147
- }
1148
-
1149
- reasonRepost(
1150
- uri: AtUriString,
1151
- repost: Repost,
1152
- state: HydrationState,
1153
- ): $Typed<ReasonRepost> | undefined {
1154
- const creatorDid = creatorFromUri(uri)
1155
- const creator = this.profileBasic(creatorDid, state)
1156
- if (!creator) return
1157
- return app.bsky.feed.defs.reasonRepost.$build({
1158
- by: creator,
1159
- uri,
1160
- cid: repost.cid,
1161
- indexedAt: this.indexedAt(repost).toISOString() as DatetimeString,
1162
- })
1163
- }
1164
-
1165
- reasonPin(): $Typed<ReasonPin> {
1166
- return app.bsky.feed.defs.reasonPin.$build({})
1167
- }
1168
-
1169
- // Bookmarks
1170
- // ------------
1171
- bookmark(
1172
- key: string,
1173
- state: HydrationState,
1174
- ): Un$Typed<BookmarkView> | undefined {
1175
- const viewer = state.ctx?.viewer
1176
- if (!viewer) return
1177
-
1178
- const bookmark = state.bookmarks?.get(viewer)?.get(key)
1179
- if (!bookmark) return
1180
-
1181
- const atUri = new AtUri(bookmark.subjectUri)
1182
- if (atUri.collection !== app.bsky.feed.post.$type) return
1183
-
1184
- const item = this.maybePost(atUri.href, state)
1185
- return {
1186
- createdAt: bookmark.indexedAt
1187
- ? (bookmark.indexedAt.toISOString() as DatetimeString)
1188
- : undefined,
1189
- subject: {
1190
- uri: atUri.href,
1191
- cid: bookmark.subjectCid,
1192
- },
1193
- item,
1194
- }
1195
- }
1196
-
1197
- // Threads
1198
- // ------------
1199
-
1200
- thread(
1201
- skele: { anchor: AtUriString; uris: AtUriString[] },
1202
- state: HydrationState,
1203
- opts: { height: number; depth: number },
1204
- ): $Typed<ThreadViewPost> | $Typed<NotFoundPost> | $Typed<BlockedPost> {
1205
- const { anchor, uris } = skele
1206
- const post = this.post(anchor, state)
1207
- const postInfo = state.posts?.get(anchor)
1208
- if (!postInfo || !post) return this.notFoundPost(anchor)
1209
- if (this.viewerBlockExists(post.author.did, state)) {
1210
- return this.blockedPost(anchor, post.author.did, state)
1211
- }
1212
- const includedPosts = new Set<string>([anchor])
1213
- const childrenByParentUri: Record<AtUriString, AtUriString[]> = {}
1214
- uris.forEach((uri) => {
1215
- const post = state.posts?.get(uri)
1216
- const parentUri = post?.record.reply?.parent.uri
1217
- if (!parentUri) return
1218
- if (includedPosts.has(uri)) return
1219
- includedPosts.add(uri)
1220
- childrenByParentUri[parentUri] ??= []
1221
- childrenByParentUri[parentUri].push(uri)
1222
- })
1223
- const rootUri = getRootUri(anchor, postInfo)
1224
- const violatesThreadGate = postInfo.violatesThreadGate
1225
-
1226
- return app.bsky.feed.defs.threadViewPost.$build({
1227
- post,
1228
- parent: !violatesThreadGate
1229
- ? this.threadParent(anchor, rootUri, state, opts.height)
1230
- : undefined,
1231
- replies: !violatesThreadGate
1232
- ? this.threadReplies(
1233
- anchor,
1234
- rootUri,
1235
- childrenByParentUri,
1236
- state,
1237
- opts.depth,
1238
- )
1239
- : undefined,
1240
- threadContext: {
1241
- rootAuthorLike: state.threadContexts?.get(post.uri)?.like,
1242
- },
1243
- })
1244
- }
1245
-
1246
- threadParent(
1247
- childUri: AtUriString,
1248
- rootUri: string,
1249
- state: HydrationState,
1250
- height: number,
1251
- ):
1252
- | $Typed<ThreadViewPost>
1253
- | $Typed<NotFoundPost>
1254
- | $Typed<BlockedPost>
1255
- | undefined {
1256
- if (height < 1) return undefined
1257
- const parentUri = state.posts?.get(childUri)?.record.reply?.parent.uri
1258
- if (!parentUri) return undefined
1259
- if (
1260
- !state.ctx?.include3pBlocks &&
1261
- state.postBlocks?.get(childUri)?.parent
1262
- ) {
1263
- return this.blockedPost(parentUri, creatorFromUri(parentUri), state)
1264
- }
1265
- const post = this.post(parentUri, state)
1266
- const postInfo = state.posts?.get(parentUri)
1267
- if (!postInfo || !post) return this.notFoundPost(parentUri)
1268
- if (rootUri !== getRootUri(parentUri, postInfo)) return // outside thread boundary
1269
- if (this.viewerBlockExists(post.author.did, state)) {
1270
- return this.blockedPost(parentUri, post.author.did, state)
1271
- }
1272
- return app.bsky.feed.defs.threadViewPost.$build({
1273
- post,
1274
- parent: this.threadParent(parentUri, rootUri, state, height - 1),
1275
- threadContext: {
1276
- rootAuthorLike: state.threadContexts?.get(post.uri)?.like,
1277
- },
1278
- })
1279
- }
1280
-
1281
- threadReplies(
1282
- parentUri: AtUriString,
1283
- rootUri: AtUriString,
1284
- childrenByParentUri: Record<AtUriString, AtUriString[]>,
1285
- state: HydrationState,
1286
- depth: number,
1287
- ): ($Typed<ThreadViewPost> | $Typed<BlockedPost>)[] | undefined {
1288
- if (depth < 1) return undefined
1289
- const childrenUris = childrenByParentUri[parentUri] ?? []
1290
- return mapDefined(childrenUris, (uri) => {
1291
- const postInfo = state.posts?.get(uri)
1292
- if (postInfo?.violatesThreadGate) {
1293
- return undefined
1294
- }
1295
- if (!state.ctx?.include3pBlocks && state.postBlocks?.get(uri)?.parent) {
1296
- return undefined
1297
- }
1298
- const post = this.post(uri, state)
1299
- if (!postInfo || !post) {
1300
- // in the future we might consider keeping a placeholder for deleted
1301
- // posts that have replies under them, but not supported at the moment.
1302
- // this case is mostly likely hit when a takedown was applied to a post.
1303
- return undefined
1304
- }
1305
- if (rootUri !== getRootUri(uri, postInfo)) return // outside thread boundary
1306
- if (this.viewerBlockExists(post.author.did, state)) {
1307
- return this.blockedPost(uri, post.author.did, state)
1308
- }
1309
- if (!this.viewerSeesNeedsReview({ uri, did: post.author.did }, state)) {
1310
- return undefined
1311
- }
1312
- return app.bsky.feed.defs.threadViewPost.$build({
1313
- post,
1314
- replies: this.threadReplies(
1315
- uri,
1316
- rootUri,
1317
- childrenByParentUri,
1318
- state,
1319
- depth - 1,
1320
- ),
1321
- threadContext: {
1322
- rootAuthorLike: state.threadContexts?.get(post.uri)?.like,
1323
- },
1324
- })
1325
- })
1326
- }
1327
-
1328
- // Threads V2
1329
- // ------------
1330
-
1331
- threadV2(
1332
- skeleton: { anchor: AtUriString; uris: AtUriString[] },
1333
- state: HydrationState,
1334
- {
1335
- above,
1336
- below,
1337
- branchingFactor,
1338
- sort,
1339
- }: {
1340
- above: number
1341
- below: number
1342
- branchingFactor: number
1343
- sort: GetPostThreadV2QueryParams['sort']
1344
- },
1345
- ): { hasOtherReplies: boolean; thread: ThreadItem[] } {
1346
- const { anchor: anchorUri, uris } = skeleton
1347
-
1348
- // Not found.
1349
- const postView = this.post(anchorUri, state)
1350
- const post = state.posts?.get(anchorUri)
1351
- if (!post || !postView) {
1352
- return {
1353
- hasOtherReplies: false,
1354
- thread: [
1355
- this.threadV2ItemNotFound({
1356
- uri: anchorUri,
1357
- depth: 0,
1358
- }),
1359
- ],
1360
- }
1361
- }
1362
-
1363
- // Blocked (only 1p for anchor).
1364
- if (this.viewerBlockExists(postView.author.did, state)) {
1365
- return {
1366
- hasOtherReplies: false,
1367
- thread: [
1368
- this.threadV2ItemBlocked({
1369
- uri: anchorUri,
1370
- depth: 0,
1371
- authorDid: postView.author.did,
1372
- state,
1373
- }),
1374
- ],
1375
- }
1376
- }
1377
-
1378
- const childrenByParentUri = this.groupThreadChildrenByParent(
1379
- anchorUri,
1380
- uris,
1381
- state,
1382
- )
1383
- const rootUri = getRootUri(anchorUri, post)
1384
- const opDid = uriToDid(rootUri)
1385
- const authorDid = postView.author.did
1386
- const isOPPost = authorDid === opDid
1387
- const anchorViolatesThreadGate = post.violatesThreadGate
1388
-
1389
- // Builds the parent tree, and whether it is a contiguous OP thread.
1390
- const parentTree = !anchorViolatesThreadGate
1391
- ? this.threadV2Parent(
1392
- {
1393
- childUri: anchorUri,
1394
- opDid,
1395
- rootUri,
1396
-
1397
- above,
1398
- depth: -1,
1399
- },
1400
- state,
1401
- )
1402
- : undefined
1403
-
1404
- const { tree: parent, isOPThread: isOPThreadFromRootToParent } =
1405
- parentTree ?? { tree: undefined, isOPThread: false }
1406
-
1407
- const isOPThread = parent
1408
- ? isOPThreadFromRootToParent && isOPPost
1409
- : isOPPost
1410
-
1411
- const anchorDepth = 0 // The depth of the anchor post is always 0.
1412
- let anchorTree: ThreadTree
1413
- let hasOtherReplies = false
1414
-
1415
- if (this.noUnauthenticatedPost(state, postView)) {
1416
- anchorTree = {
1417
- type: 'noUnauthenticated',
1418
- item: this.threadV2ItemNoUnauthenticated({
1419
- uri: anchorUri,
1420
- depth: anchorDepth,
1421
- }),
1422
- parent,
1423
- }
1424
- } else {
1425
- const { replies, hasOtherReplies: hasOtherRepliesShadow } =
1426
- !anchorViolatesThreadGate
1427
- ? this.threadV2Replies(
1428
- {
1429
- parentUri: anchorUri,
1430
- isOPThread,
1431
- opDid,
1432
- rootUri,
1433
- childrenByParentUri,
1434
- below,
1435
- depth: 1,
1436
- branchingFactor,
1437
- },
1438
- state,
1439
- )
1440
- : { replies: undefined, hasOtherReplies: false }
1441
- hasOtherReplies = hasOtherRepliesShadow
1442
-
1443
- anchorTree = {
1444
- type: 'post',
1445
- item: this.threadV2ItemPost({
1446
- depth: anchorDepth,
1447
- isOPThread,
1448
- postView,
1449
- repliesAllowance: Infinity, // While we don't have pagination.
1450
- uri: anchorUri,
1451
- }),
1452
- tags: post.tags,
1453
- hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
1454
- parent,
1455
- replies,
1456
- }
1457
- }
1458
-
1459
- const thread = sortTrimFlattenThreadTree(
1460
- anchorTree,
1461
- {
1462
- opDid,
1463
- branchingFactor,
1464
- sort,
1465
- viewer: state.ctx?.viewer ?? null,
1466
- threadTagsBumpDown: this.threadTagsBumpDown,
1467
- threadTagsHide: this.threadTagsHide,
1468
- visibilityTagRankPrefix: this.visibilityTagRankPrefix,
1469
- },
1470
- state.ctx?.features.checkGate(
1471
- state.ctx.features.Gate.ThreadsReplyRankingExplorationEnable,
1472
- ),
1473
- )
1474
-
1475
- return {
1476
- hasOtherReplies,
1477
- thread,
1478
- }
1479
- }
1480
-
1481
- private threadV2Parent(
1482
- {
1483
- childUri,
1484
- opDid,
1485
- rootUri,
1486
- above,
1487
- depth,
1488
- }: {
1489
- childUri: AtUriString
1490
- opDid: DidString
1491
- rootUri: AtUriString
1492
- above: number
1493
- depth: number
1494
- },
1495
- state: HydrationState,
1496
- ): { tree: ThreadTreeVisible; isOPThread: boolean } | undefined {
1497
- // Reached the `above` limit.
1498
- if (Math.abs(depth) > above) {
1499
- return undefined
1500
- }
1501
-
1502
- // Not found.
1503
- const uri = state.posts?.get(childUri)?.record.reply?.parent.uri
1504
- if (!uri) {
1505
- return undefined
1506
- }
1507
- const postView = this.post(uri, state)
1508
- const post = state.posts?.get(uri)
1509
- if (!post || !postView) {
1510
- return {
1511
- tree: {
1512
- type: 'notFound',
1513
- item: this.threadV2ItemNotFound({ uri, depth }),
1514
- },
1515
- isOPThread: false,
1516
- }
1517
- }
1518
- if (rootUri !== getRootUri(uri, post)) {
1519
- // Outside thread boundary.
1520
- return undefined
1521
- }
1522
-
1523
- // Blocked (1p and 3p for parent).
1524
- const authorDid = postView.author.did
1525
- const has1pBlock = this.viewerBlockExists(authorDid, state)
1526
- const has3pBlock =
1527
- !state.ctx?.include3pBlocks && state.postBlocks?.get(childUri)?.parent
1528
- if (has1pBlock || has3pBlock) {
1529
- return {
1530
- tree: {
1531
- type: 'blocked',
1532
- item: this.threadV2ItemBlocked({
1533
- uri,
1534
- depth,
1535
- authorDid,
1536
- state,
1537
- }),
1538
- },
1539
- isOPThread: false,
1540
- }
1541
- }
1542
-
1543
- // Recurse up.
1544
- const parentTree = this.threadV2Parent(
1545
- {
1546
- childUri: uri,
1547
- opDid,
1548
- rootUri,
1549
- above,
1550
- depth: depth - 1,
1551
- },
1552
- state,
1553
- )
1554
- const { tree: parent, isOPThread: isOPThreadFromRootToParent } =
1555
- parentTree ?? { tree: undefined, isOPThread: false }
1556
-
1557
- const isOPPost = authorDid === opDid
1558
- const isOPThread = parent
1559
- ? isOPThreadFromRootToParent && isOPPost
1560
- : isOPPost
1561
-
1562
- if (this.noUnauthenticatedPost(state, postView)) {
1563
- return {
1564
- tree: {
1565
- type: 'noUnauthenticated',
1566
- item: this.threadV2ItemNoUnauthenticated({
1567
- uri,
1568
- depth,
1569
- }),
1570
- parent,
1571
- },
1572
- isOPThread,
1573
- }
1574
- }
1575
-
1576
- const parentUri = post.record.reply?.parent.uri
1577
- const hasMoreParents = !!parentUri && !parent
1578
-
1579
- return {
1580
- tree: {
1581
- type: 'post',
1582
- item: this.threadV2ItemPost({
1583
- depth,
1584
- isOPThread,
1585
- moreParents: hasMoreParents,
1586
- postView,
1587
- uri,
1588
- }),
1589
- tags: post.tags,
1590
- hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
1591
- parent,
1592
- replies: undefined,
1593
- },
1594
- isOPThread,
1595
- }
1596
- }
1597
-
1598
- private threadV2Replies(
1599
- {
1600
- parentUri,
1601
- isOPThread: isOPThreadFromRootToParent,
1602
- opDid,
1603
- rootUri,
1604
- childrenByParentUri,
1605
- below,
1606
- depth,
1607
- branchingFactor,
1608
- }: {
1609
- parentUri: AtUriString
1610
- isOPThread: boolean
1611
- opDid: string
1612
- rootUri: AtUriString
1613
- childrenByParentUri: Record<AtUriString, AtUriString[]>
1614
- below: number
1615
- depth: number
1616
- branchingFactor: number
1617
- },
1618
- state: HydrationState,
1619
- ): { replies: ThreadTreeVisible[] | undefined; hasOtherReplies: boolean } {
1620
- // Reached the `below` limit.
1621
- if (depth > below) {
1622
- return { replies: undefined, hasOtherReplies: false }
1623
- }
1624
-
1625
- const childrenUris: AtUriString[] = childrenByParentUri[parentUri] ?? []
1626
- let hasOtherReplies = false
1627
- const replies = mapDefined(childrenUris, (uri) => {
1628
- const replyInclusion = this.checkThreadV2ReplyInclusion({
1629
- uri,
1630
- rootUri,
1631
- state,
1632
- })
1633
- if (!replyInclusion) {
1634
- return undefined
1635
- }
1636
- const { authorDid, post, postView } = replyInclusion
1637
-
1638
- // Hidden.
1639
- const { isOther } = this.isOtherThreadPost(
1640
- { post, postView, rootUri, uri },
1641
- state,
1642
- )
1643
- if (isOther) {
1644
- // Only care about anchor replies
1645
- if (depth === 1) {
1646
- hasOtherReplies = true
1647
- }
1648
- return undefined
1649
- }
1650
-
1651
- // Recurse down.
1652
- const isOPThread = isOPThreadFromRootToParent && authorDid === opDid
1653
- const { replies: nestedReplies } = this.threadV2Replies(
1654
- {
1655
- parentUri: uri,
1656
- isOPThread,
1657
- opDid,
1658
- rootUri,
1659
- childrenByParentUri,
1660
- below,
1661
- depth: depth + 1,
1662
- branchingFactor,
1663
- },
1664
- state,
1665
- )
1666
-
1667
- const reachedDepth = depth === below
1668
- const repliesAllowance = reachedDepth ? 0 : branchingFactor
1669
-
1670
- const tree: ThreadTree = {
1671
- type: 'post',
1672
- item: this.threadV2ItemPost({
1673
- depth,
1674
- isOPThread,
1675
- postView,
1676
- repliesAllowance,
1677
- uri,
1678
- }),
1679
- tags: post.tags,
1680
- hasOPLike: !!state.threadContexts?.get(postView.uri)?.like,
1681
- parent: undefined,
1682
- replies: nestedReplies,
1683
- }
1684
-
1685
- return tree
1686
- })
1687
-
1688
- return {
1689
- replies,
1690
- hasOtherReplies,
1691
- }
1692
- }
1693
-
1694
- private threadV2ItemPost({
1695
- depth,
1696
- isOPThread,
1697
- moreParents,
1698
- postView,
1699
- repliesAllowance,
1700
- uri,
1701
- }: {
1702
- depth: number
1703
- isOPThread: boolean
1704
- moreParents?: boolean
1705
- postView: PostView
1706
- repliesAllowance?: number
1707
- uri: AtUriString
1708
- }): ThreadItemValuePost {
1709
- const moreReplies =
1710
- repliesAllowance === undefined
1711
- ? 0
1712
- : Math.max((postView.replyCount ?? 0) - repliesAllowance, 0)
1713
-
1714
- return {
1715
- uri,
1716
- depth,
1717
- value: app.bsky.unspecced.defs.threadItemPost.$build({
1718
- post: postView,
1719
- moreParents: moreParents ?? false,
1720
- moreReplies,
1721
- opThread: isOPThread,
1722
- hiddenByThreadgate: false, // Hidden posts are handled by threadOtherV2
1723
- mutedByViewer: false, // Hidden posts are handled by threadOtherV2
1724
- }),
1725
- }
1726
- }
1727
-
1728
- private threadV2ItemNoUnauthenticated({
1729
- uri,
1730
- depth,
1731
- }: {
1732
- uri: AtUriString
1733
- depth: number
1734
- }): ThreadItemValueNoUnauthenticated {
1735
- return {
1736
- uri,
1737
- depth,
1738
- value: app.bsky.unspecced.defs.threadItemNoUnauthenticated.$build({}),
1739
- }
1740
- }
1741
-
1742
- private threadV2ItemNotFound({
1743
- uri,
1744
- depth,
1745
- }: {
1746
- uri: AtUriString
1747
- depth: number
1748
- }): ThreadItemValueNotFound {
1749
- return {
1750
- uri,
1751
- depth,
1752
- value: app.bsky.unspecced.defs.threadItemNotFound.$build({}),
1753
- }
1754
- }
1755
-
1756
- private threadV2ItemBlocked({
1757
- uri,
1758
- depth,
1759
- authorDid,
1760
- state,
1761
- }: {
1762
- uri: AtUriString
1763
- depth: number
1764
- authorDid: DidString
1765
- state: HydrationState
1766
- }): ThreadItemValueBlocked {
1767
- return {
1768
- uri,
1769
- depth,
1770
- value: app.bsky.unspecced.defs.threadItemBlocked.$build({
1771
- author: {
1772
- did: authorDid,
1773
- viewer: this.blockedProfileViewer(authorDid, state),
1774
- },
1775
- }),
1776
- }
1777
- }
1778
-
1779
- threadOtherV2(
1780
- skeleton: { anchor: AtUriString; uris: AtUriString[] },
1781
- state: HydrationState,
1782
- {
1783
- below,
1784
- branchingFactor,
1785
- }: {
1786
- below: number
1787
- branchingFactor: number
1788
- },
1789
- ): ThreadOtherItem[] {
1790
- const { anchor: anchorUri, uris } = skeleton
1791
-
1792
- // Not found.
1793
- const postView = this.post(anchorUri, state)
1794
- const post = state.posts?.get(anchorUri)
1795
- if (!post || !postView) {
1796
- return []
1797
- }
1798
-
1799
- // Blocked (only 1p for anchor).
1800
- if (this.viewerBlockExists(postView.author.did, state)) {
1801
- return []
1802
- }
1803
-
1804
- const childrenByParentUri = this.groupThreadChildrenByParent(
1805
- anchorUri,
1806
- uris,
1807
- state,
1808
- )
1809
- const rootUri = getRootUri(anchorUri, post)
1810
- const opDid = uriToDid(rootUri)
1811
-
1812
- const anchorTree: ThreadOtherAnchorPostNode = {
1813
- type: 'hiddenAnchor',
1814
- item: this.threadOtherV2ItemPostAnchor({ depth: 0, uri: anchorUri }),
1815
- replies: this.threadOtherV2Replies(
1816
- {
1817
- parentUri: anchorUri,
1818
- rootUri,
1819
- childrenByParentUri,
1820
- below,
1821
- depth: 1,
1822
- },
1823
- state,
1824
- ),
1825
- }
1826
-
1827
- return sortTrimFlattenThreadTree(
1828
- anchorTree,
1829
- {
1830
- opDid,
1831
- branchingFactor,
1832
- viewer: state.ctx?.viewer ?? null,
1833
- threadTagsBumpDown: this.threadTagsBumpDown,
1834
- threadTagsHide: this.threadTagsHide,
1835
- visibilityTagRankPrefix: this.visibilityTagRankPrefix,
1836
- },
1837
- state.ctx?.features.checkGate(
1838
- state.ctx.features.Gate.ThreadsReplyRankingExplorationEnable,
1839
- ),
1840
- )
1841
- }
1842
-
1843
- private threadOtherV2Replies(
1844
- {
1845
- parentUri,
1846
- rootUri,
1847
- childrenByParentUri,
1848
- below,
1849
- depth,
1850
- }: {
1851
- parentUri: AtUriString
1852
- rootUri: AtUriString
1853
- childrenByParentUri: Record<AtUriString, AtUriString[]>
1854
- below: number
1855
- depth: number
1856
- },
1857
- state: HydrationState,
1858
- ): ThreadOtherPostNode[] | undefined {
1859
- // Reached the `below` limit.
1860
- if (depth > below) {
1861
- return undefined
1862
- }
1863
-
1864
- const childrenUris: AtUriString[] = childrenByParentUri[parentUri] ?? []
1865
- return mapDefined(childrenUris, (uri) => {
1866
- const replyInclusion = this.checkThreadV2ReplyInclusion({
1867
- uri,
1868
- rootUri,
1869
- state,
1870
- })
1871
- if (!replyInclusion) {
1872
- return undefined
1873
- }
1874
- const { post, postView } = replyInclusion
1875
-
1876
- // Other posts to pull out
1877
- const { isOther, hiddenByThreadgate, mutedByViewer } =
1878
- this.isOtherThreadPost({ post, postView, rootUri, uri }, state)
1879
- if (isOther) {
1880
- // Only show hidden anchor replies, not all hidden.
1881
- if (depth > 1) {
1882
- return undefined
1883
- }
1884
- } else if (depth === 1) {
1885
- // Don't include non-hidden anchor replies.
1886
- return undefined
1887
- }
1888
-
1889
- // Recurse down.
1890
- const replies = this.threadOtherV2Replies(
1891
- {
1892
- parentUri: uri,
1893
- rootUri,
1894
- childrenByParentUri,
1895
- below,
1896
- depth: depth + 1,
1897
- },
1898
- state,
1899
- )
1900
-
1901
- const item = this.threadOtherV2ItemPost({
1902
- depth,
1903
- hiddenByThreadgate,
1904
- mutedByViewer,
1905
- postView,
1906
- uri,
1907
- })
1908
-
1909
- const tree: ThreadOtherPostNode = {
1910
- type: 'hiddenPost',
1911
- item: item,
1912
- tags: post.tags,
1913
- replies,
1914
- }
1915
-
1916
- return tree
1917
- })
1918
- }
1919
-
1920
- private threadOtherV2ItemPostAnchor({
1921
- depth,
1922
- uri,
1923
- }: {
1924
- depth: number
1925
- uri: AtUriString
1926
- }): ThreadOtherAnchorPostNode['item'] {
1927
- return {
1928
- uri,
1929
- depth,
1930
- // In hidden replies, the anchor value is undefined, so it doesn't include the anchor in the result.
1931
- // This is helpful so we can use the same internal structure for hidden and non-hidden, while omitting anchor for hidden.
1932
- value: undefined,
1933
- }
1934
- }
1935
-
1936
- private threadOtherV2ItemPost({
1937
- depth,
1938
- hiddenByThreadgate,
1939
- mutedByViewer,
1940
- postView,
1941
- uri,
1942
- }: {
1943
- depth: number
1944
- hiddenByThreadgate: boolean
1945
- mutedByViewer: boolean
1946
- postView: PostView
1947
- uri: AtUriString
1948
- }): ThreadOtherItemValuePost {
1949
- const base = this.threadOtherV2ItemPostAnchor({ depth, uri })
1950
- return {
1951
- ...base,
1952
- value: app.bsky.unspecced.defs.threadItemPost.$build({
1953
- post: postView,
1954
- hiddenByThreadgate,
1955
- mutedByViewer,
1956
- moreParents: false, // "Other" replies don't have parents.
1957
- moreReplies: 0, // "Other" replies don't have replies hydrated.
1958
- opThread: false, // "Other" replies don't contain OP threads.
1959
- }),
1960
- }
1961
- }
1962
-
1963
- private checkThreadV2ReplyInclusion({
1964
- uri,
1965
- rootUri,
1966
- state,
1967
- }: {
1968
- uri: AtUriString
1969
- rootUri: string
1970
- state: HydrationState
1971
- }): {
1972
- authorDid: string
1973
- post: Post
1974
- postView: PostView
1975
- } | null {
1976
- // Not found.
1977
- const post = state.posts?.get(uri)
1978
- if (post?.violatesThreadGate) {
1979
- return null
1980
- }
1981
- const postView = this.post(uri, state)
1982
- if (!post || !postView) {
1983
- return null
1984
- }
1985
- const authorDid = postView.author.did
1986
- if (rootUri !== getRootUri(uri, post)) {
1987
- // outside thread boundary
1988
- return null
1989
- }
1990
-
1991
- // Blocked (1p and 3p for replies).
1992
- const has1pBlock = this.viewerBlockExists(authorDid, state)
1993
- const has3pBlock =
1994
- !state.ctx?.include3pBlocks && state.postBlocks?.get(uri)?.parent
1995
- if (has1pBlock || has3pBlock) {
1996
- return null
1997
- }
1998
- if (!this.viewerSeesNeedsReview({ uri, did: authorDid }, state)) {
1999
- return null
2000
- }
2001
-
2002
- // No unauthenticated.
2003
- if (this.noUnauthenticatedPost(state, postView)) {
2004
- return null
2005
- }
2006
-
2007
- return { authorDid, post, postView }
2008
- }
2009
-
2010
- private isOtherThreadPost(
2011
- {
2012
- post,
2013
- postView,
2014
- rootUri,
2015
- uri,
2016
- }: {
2017
- post: Post
2018
- postView: PostView
2019
- rootUri: AtUriString
2020
- uri: AtUriString
2021
- },
2022
- state: HydrationState,
2023
- ): {
2024
- isOther: boolean
2025
- hiddenByTag: boolean
2026
- hiddenByThreadgate: boolean
2027
- mutedByViewer: boolean
2028
- } {
2029
- const opDid = creatorFromUri(rootUri)
2030
- const authorDid = creatorFromUri(uri)
2031
-
2032
- let hiddenByTag = false
2033
- if (
2034
- state.ctx?.features.checkGate(
2035
- state.ctx.features.Gate.ThreadsReplyRankingExplorationEnable,
2036
- )
2037
- ) {
2038
- hiddenByTag = authorDid !== opDid && post.tags.has(this.visibilityTagHide)
2039
- } else {
2040
- const showBecauseFollowing = !!postView.author.viewer?.following
2041
- hiddenByTag =
2042
- authorDid !== opDid &&
2043
- authorDid !== state.ctx?.viewer &&
2044
- !showBecauseFollowing &&
2045
- this.threadTagsHide.some((t) => post.tags.has(t))
2046
- }
2047
-
2048
- const hiddenByThreadgate =
2049
- state.ctx?.viewer !== authorDid &&
2050
- this.replyIsHiddenByThreadgate(uri, rootUri, state)
2051
-
2052
- const mutedByViewer = this.viewerMuteExists(authorDid, state)
2053
- const isPushPin =
2054
- isPostRecordType(post.record) && post.record.text.trim() === '📌'
2055
-
2056
- return {
2057
- isOther: hiddenByTag || hiddenByThreadgate || mutedByViewer || isPushPin,
2058
- hiddenByTag,
2059
- hiddenByThreadgate,
2060
- mutedByViewer,
2061
- }
2062
- }
2063
-
2064
- private groupThreadChildrenByParent(
2065
- anchorUri: AtUriString,
2066
- uris: AtUriString[],
2067
- state: HydrationState,
2068
- ): Record<AtUriString, AtUriString[]> {
2069
- // Groups children of each parent.
2070
- const includedPosts = new Set<AtUriString>([anchorUri])
2071
- const childrenByParentUri: Record<AtUriString, AtUriString[]> = {}
2072
- uris.forEach((uri) => {
2073
- const post = state.posts?.get(uri)
2074
- const parentUri = post?.record.reply?.parent.uri
2075
- if (!parentUri) return
2076
- if (includedPosts.has(uri)) return
2077
- includedPosts.add(uri)
2078
- childrenByParentUri[parentUri] ??= []
2079
- childrenByParentUri[parentUri].push(uri)
2080
- })
2081
- return childrenByParentUri
2082
- }
2083
-
2084
- // Embeds
2085
- // ------------
2086
-
2087
- embed(
2088
- postUri: AtUriString,
2089
- embed: $Typed<Embed> | Unknown$TypedObject,
2090
- state: HydrationState,
2091
- depth: number,
2092
- ): $Typed<EmbedView> | undefined {
2093
- if (isImagesEmbedType(embed)) {
2094
- return this.imagesEmbed(creatorFromUri(postUri), embed)
2095
- } else if (isVideoEmbedType(embed)) {
2096
- return this.videoEmbed(creatorFromUri(postUri), embed)
2097
- } else if (isGalleryEmbedType(embed)) {
2098
- return this.galleryEmbed(creatorFromUri(postUri), embed)
2099
- } else if (isExternalEmbedType(embed)) {
2100
- return this.externalEmbed(creatorFromUri(postUri), embed, state)
2101
- } else if (isRecordEmbedType(embed)) {
2102
- return this.recordEmbed(postUri, embed, state, depth)
2103
- } else if (isRecordWithMediaType(embed)) {
2104
- return this.recordWithMediaEmbed(postUri, embed, state, depth)
2105
- } else {
2106
- return undefined
2107
- }
2108
- }
2109
-
2110
- imagesEmbed(did: DidString, embed: ImagesEmbed): $Typed<ImagesEmbedView> {
2111
- const imgViews = embed.images.map((img) => ({
2112
- thumb: this.imgUriBuilder.getPresetUri(
2113
- 'feed_thumbnail',
2114
- did,
2115
- getBlobCidString(img.image),
2116
- ),
2117
- fullsize: this.imgUriBuilder.getPresetUri(
2118
- 'feed_fullsize',
2119
- did,
2120
- getBlobCidString(img.image),
2121
- ),
2122
- alt: img.alt,
2123
- aspectRatio: img.aspectRatio,
2124
- }))
2125
- return app.bsky.embed.images.view.$build({
2126
- images: imgViews,
2127
- })
2128
- }
2129
-
2130
- videoEmbed(did: DidString, embed: VideoEmbed): $Typed<VideoEmbedView> {
2131
- const cid = getBlobCidString(embed.video)
2132
- return app.bsky.embed.video.view.$build({
2133
- cid,
2134
- playlist: this.videoUriBuilder.playlist({ did, cid }),
2135
- thumbnail: this.videoUriBuilder.thumbnail({ did, cid }),
2136
- alt: embed.alt,
2137
- aspectRatio: embed.aspectRatio,
2138
- presentation: embed.presentation,
2139
- })
2140
- }
2141
-
2142
- galleryEmbed(did: DidString, embed: GalleryEmbed): $Typed<GalleryEmbedView> {
2143
- // The lexicon's schema-level cap is 20, but clients are expected to
2144
- // enforce a soft limit of 10. Trim defensively at the view boundary so
2145
- // viewers see at most 10 items regardless of what was authored.
2146
- const items = embed.items.slice(0, GALLERY_SOFT_LIMIT).flatMap((item) => {
2147
- const view = this.galleryItemView(did, item)
2148
- return view ? [view] : []
2149
- })
2150
- return app.bsky.embed.gallery.view.$build({ items })
2151
- }
2152
-
2153
- private galleryItemView(
2154
- did: DidString,
2155
- item: GalleryEmbed['items'][number],
2156
- ): $Typed<GalleryImageEmbedView> | undefined {
2157
- if (isGalleryImageEmbedType(item)) {
2158
- return this.galleryImageView(did, item)
2159
- }
2160
- return undefined
2161
- }
2162
-
2163
- private galleryImageView(
2164
- did: DidString,
2165
- item: GalleryImageEmbed,
2166
- ): $Typed<GalleryImageEmbedView> {
2167
- return app.bsky.embed.gallery.viewImage.$build({
2168
- thumbnail: this.imgUriBuilder.getPresetUri(
2169
- 'feed_thumbnail',
2170
- did,
2171
- getBlobCidString(item.image),
2172
- ),
2173
- fullsize: this.imgUriBuilder.getPresetUri(
2174
- 'feed_fullsize',
2175
- did,
2176
- getBlobCidString(item.image),
2177
- ),
2178
- alt: item.alt,
2179
- aspectRatio: item.aspectRatio,
2180
- })
2181
- }
2182
-
2183
- externalEmbed(
2184
- did: DidString,
2185
- embed: ExternalEmbed,
2186
- state: HydrationState,
2187
- ): $Typed<ExternalEmbedView> {
2188
- // Start from the post-author-supplied embed values, then spread the
2189
- // SS-derived view on top so any field the hydrated doc/publication
2190
- // supplies wins (title, description, thumb, source, etc). When no SS
2191
- // records were hydrated `ssView` is `undefined` and the spread is a
2192
- // no-op, leaving the base values in place. `associatedRefs` is set
2193
- // last because the post is authoritative about what was pinned.
2194
- const ssView = this.externalEmbedFromStandardSite({
2195
- associatedRefs: embed.external.associatedRefs,
2196
- state,
2197
- assumedUrl: embed.external.uri,
2198
- })
2199
- // The author-supplied (scraped) thumbnail always wins when present —
2200
- // it's the per-article OG image. Only when the embed has no thumb do
2201
- // we fall back to whatever the SS overlay provides (the document's
2202
- // `coverImage`). `thumb` is set after the `...ssView` spread so the
2203
- // overlay's `coverImage`-derived thumb can't clobber the embed's.
2204
- const embeddedThumb = embed.external.thumb
2205
- ? this.imgUriBuilder.getPresetUri(
2206
- 'feed_thumbnail',
2207
- did,
2208
- getBlobCidString(embed.external.thumb),
2209
- )
2210
- : undefined
2211
- return app.bsky.embed.external.view.$build({
2212
- external: {
2213
- uri: embed.external.uri,
2214
- title: embed.external.title,
2215
- description: embed.external.description,
2216
- ...ssView,
2217
- thumb: embeddedThumb ?? ssView?.thumb,
2218
- associatedRefs: embed.external.associatedRefs,
2219
- },
2220
- })
2221
- }
2222
-
2223
- /**
2224
- * Read-path entry point: caller has the post's `external.associatedRefs[]`
2225
- * and the global hydration state, and we resolve the matching SS records
2226
- * by ref. Used by `externalEmbed`.
2227
- *
2228
- * Returns a fully-populated `viewExternal['external']` shape (including
2229
- * the required `uri`/`title`/`description`), or `undefined` if the
2230
- * records didn't hydrate, didn't pass URL validation, or couldn't
2231
- * supply the required fields. Callers spread the result over a base
2232
- * view to layer SS-derived fields on top of post-author-supplied ones.
2233
- *
2234
- * `assumedUrl` is the canonical web URL the embed claims to represent
2235
- * (the post's `external.uri` on the read path, the request's `url` on
2236
- * compose). Validation logic uses it to confirm the SS records actually
2237
- * back that URL, and the same value is echoed back as `uri` on the
2238
- * returned overlay.
2239
- */
2240
- externalEmbedFromStandardSite({
2241
- associatedRefs,
2242
- state,
2243
- assumedUrl,
2244
- }: {
2245
- associatedRefs: ExternalEmbed['external']['associatedRefs']
2246
- state: HydrationState
2247
- assumedUrl: string
2248
- }): ExternalEmbedView['external'] | undefined {
2249
- const { document, publication } =
2250
- getSiteStandardRecordsFromHydrationMapsByRefs(
2251
- associatedRefs,
2252
- state.siteStandardDocuments,
2253
- state.siteStandardPublications,
2254
- )
2255
- return this.externalEmbedFromStandardSiteRecords({
2256
- document,
2257
- publication,
2258
- state,
2259
- assumedUrl,
2260
- })
2261
- }
2262
-
2263
- /**
2264
- * Compose-path entry point: caller already knows which document and
2265
- * publication backed the request and has them in hand (e.g. from
2266
- * iterating the SS hydration maps directly). Skips the by-ref lookup
2267
- * that `externalEmbedFromStandardSite` does. Used by
2268
- * `getEmbedExternalView`.
2269
- *
2270
- * Returns a fully-populated `viewExternal['external']` shape (including
2271
- * the required `uri`/`title`/`description`), or `undefined` if any of
2272
- * the validation gates rejected the pair or the records couldn't
2273
- * supply the required fields.
2274
- *
2275
- * `state` is still needed for label hydration. `assumedUrl` is the
2276
- * canonical web URL the embed claims to represent (the post's
2277
- * `external.uri` on the read path, the request's `url` on compose);
2278
- * validation logic uses it to confirm the SS records actually back that
2279
- * URL, and the same value is echoed back as `uri` on the returned
2280
- * overlay.
2281
- */
2282
- externalEmbedFromStandardSiteRecords({
2283
- document,
2284
- publication,
2285
- state,
2286
- assumedUrl,
2287
- }: {
2288
- document: AssociatedSiteStandardRecord<SiteStandardDocument> | undefined
2289
- publication:
2290
- | AssociatedSiteStandardRecord<SiteStandardPublication>
2291
- | undefined
2292
- state: HydrationState
2293
- assumedUrl: string
2294
- }): ExternalEmbedView['external'] | undefined {
2295
- // Three layers of validation gate this overlay:
2296
- //
2297
- // 1. Hydrator nulls taken-down records and mirrors the null across
2298
- // doc/pub pairs (see `actionSiteStandardTakedownLabels` in
2299
- // `hydration/hydrator.ts`).
2300
- // 2. The lookups guarantee structural agreement between doc and pub
2301
- // — matching `site`/`uri`, no orphan doc claiming a missing
2302
- // publication (see `getSiteStandardRecordsFromHydrationMapsByRefs`
2303
- // and `…ByDocumentUri` in `hydration/external.ts`).
2304
- // 3. `validateStandardSiteForUrl` below checks that the records back
2305
- // the URL the embed claims (see `util/standard-site.ts`).
2306
- //
2307
- // If any layer rejected, callers see `undefined` and fall back to the
2308
- // bare embed rather than render partial / disagreeing enrichment.
2309
- if (!document && !publication) return undefined
2310
- if (!validateStandardSiteForUrl(document, publication, assumedUrl)) {
2311
- viewsLogger.warn(
2312
- {
2313
- documentUri: document?.ref.uri,
2314
- publicationUri: publication?.ref.uri,
2315
- },
2316
- 'site.standard URL validation failed',
2317
- )
2318
- return undefined
2319
- }
2320
-
2321
- // viewExternal requires `title` and `description`. If neither side of
2322
- // the pair supplies usable values for both, there's no enrichment to
2323
- // render — return undefined and let the caller fall back.
2324
- let title: string
2325
- let description: string
2326
- if (document?.info.record) {
2327
- // Treat the document as authoritative for both fields as a unit, so
2328
- // we never blend a doc's title with a publication's description.
2329
- title = document.info.record.title
2330
- description = document.info.record.description ?? ''
2331
- } else if (publication) {
2332
- title = publication.info.record.name
2333
- description = publication.info.record.description ?? ''
2334
- } else {
2335
- return undefined
2336
- }
2337
-
2338
- // if we don't have a title at this point, something is wrong with the SS
2339
- // record (it's a required field) and therefore the enrichment isn't worth
2340
- // showing
2341
- if (!title) return undefined
2342
-
2343
- const overlay: ExternalEmbedView['external'] = {
2344
- // @ts-ignore this is mis-typed
2345
- uri: assumedUrl,
2346
- title,
2347
- description,
2348
- }
2349
-
2350
- const docCover = document?.info.record.coverImage
2351
- if (docCover) {
2352
- overlay.thumb = this.imgUriBuilder.getPresetUri(
2353
- 'feed_thumbnail',
2354
- creatorFromUri(document.ref.uri),
2355
- getBlobCidString(docCover),
2356
- )
2357
- }
2358
-
2359
- if (document?.info.record.publishedAt) {
2360
- overlay.createdAt = document.info.record.publishedAt
2361
- }
2362
- if (document?.info.record.updatedAt) {
2363
- overlay.updatedAt = document.info.record.updatedAt
2364
- }
2365
- const readingTime = document?.info.record.textContent
2366
- ? estimateReadingTimeMinutes(document.info.record.textContent)
2367
- : undefined
2368
- if (readingTime !== undefined) overlay.readingTime = readingTime
2369
-
2370
- // Merge labels applied to either the document or the publication onto the
2371
- // same `viewExternal.labels` array — clients moderate the embed as single
2372
- // unit, so doc-scoped and publication-scoped labels end up in the same
2373
- // bucket.
2374
- const labels = [
2375
- ...(document ? state.labels?.getBySubject(document.ref.uri) ?? [] : []),
2376
- ...(publication
2377
- ? state.labels?.getBySubject(publication.ref.uri) ?? []
2378
- : []),
2379
- ]
2380
- if (labels.length) overlay.labels = labels
2381
-
2382
- if (publication) {
2383
- overlay.source = this.externalEmbedSource(publication)
2384
- }
2385
-
2386
- // Profiles of the owners of the records backing this embed. Hydrator
2387
- // covers these DIDs alongside post-author profiles, so misses here
2388
- // only happen when an actor is unavailable (suspended, deleted, etc.)
2389
- // — drop those rather than emit `undefined` slots.
2390
- const uniqueDids = dedupeStrs(
2391
- mapDefined([document?.ref.uri, publication?.ref.uri], (uri) =>
2392
- uri ? uriToDid(uri) : undefined,
2393
- ) as DidString[],
2394
- )
2395
- const associatedProfiles = mapDefined(uniqueDids, (did) =>
2396
- this.profileBasic(did, state),
2397
- ) as ProfileViewBasic[]
2398
- if (associatedProfiles.length)
2399
- overlay.associatedProfiles = associatedProfiles
2400
-
2401
- return overlay
2402
- }
2403
-
2404
- externalEmbedSource(
2405
- publication: AssociatedSiteStandardRecord<SiteStandardPublication>,
2406
- ): $Typed<ExternalEmbedSourceView> {
2407
- const { record } = publication.info
2408
- const pubDid = creatorFromUri(publication.ref.uri)
2409
- return app.bsky.embed.external.viewExternalSource.$build({
2410
- uri: record.url,
2411
- icon: record.icon
2412
- ? this.imgUriBuilder.getPresetUri(
2413
- 'avatar',
2414
- pubDid,
2415
- getBlobCidString(record.icon),
2416
- )
2417
- : undefined,
2418
- title: record.name,
2419
- description: record.description,
2420
- theme: record.basicTheme
2421
- ? externalEmbedSourceTheme(record.basicTheme)
2422
- : undefined,
2423
- })
2424
- }
2425
-
2426
- embedNotFound(uri: AtUriString): $Typed<RecordEmbedView> {
2427
- return app.bsky.embed.record.view.$build({
2428
- record: app.bsky.embed.record.viewNotFound.$build({
2429
- uri,
2430
- notFound: true,
2431
- }),
2432
- })
2433
- }
2434
-
2435
- embedDetached(uri: AtUriString): $Typed<RecordEmbedView> {
2436
- return app.bsky.embed.record.view.$build({
2437
- record: app.bsky.embed.record.viewDetached.$build({
2438
- uri,
2439
- detached: true,
2440
- }),
2441
- })
2442
- }
2443
-
2444
- embedBlocked(
2445
- uri: AtUriString,
2446
- state: HydrationState,
2447
- ): $Typed<RecordEmbedView> {
2448
- const creator = creatorFromUri(uri)
2449
- return app.bsky.embed.record.view.$build({
2450
- record: app.bsky.embed.record.viewBlocked.$build({
2451
- uri,
2452
- blocked: true,
2453
- author: {
2454
- did: creator,
2455
- viewer: this.blockedProfileViewer(creator, state),
2456
- },
2457
- }),
2458
- })
2459
- }
2460
-
2461
- embedPostView(
2462
- uri: AtUriString,
2463
- state: HydrationState,
2464
- depth: number,
2465
- ): $Typed<PostEmbedView> | undefined {
2466
- const postView = this.post(uri, state, depth)
2467
- if (!postView) return
2468
- return app.bsky.embed.record.viewRecord.$build({
2469
- uri: postView.uri,
2470
- cid: postView.cid,
2471
- author: postView.author,
2472
- value: postView.record,
2473
- labels: postView.labels,
2474
- likeCount: postView.likeCount,
2475
- replyCount: postView.replyCount,
2476
- repostCount: postView.repostCount,
2477
- quoteCount: postView.quoteCount,
2478
- indexedAt: postView.indexedAt,
2479
- embeds: depth > 1 ? undefined : postView.embed ? [postView.embed] : [],
2480
- })
2481
- }
2482
-
2483
- recordEmbed(
2484
- postUri: AtUriString,
2485
- embed: RecordEmbed,
2486
- state: HydrationState,
2487
- depth: number,
2488
- withTypeTag: false,
2489
- ): RecordEmbedView
2490
- recordEmbed(
2491
- postUri: AtUriString,
2492
- embed: RecordEmbed,
2493
- state: HydrationState,
2494
- depth: number,
2495
- withTypeTag?: true,
2496
- ): $Typed<RecordEmbedView>
2497
- recordEmbed(
2498
- postUri: AtUriString,
2499
- embed: RecordEmbed,
2500
- state: HydrationState,
2501
- depth: number,
2502
- withTypeTag = true,
2503
- ): RecordEmbedView {
2504
- const uri = embed.record.uri
2505
- const parsedUri = new AtUri(uri)
2506
- if (
2507
- this.viewerBlockExists(parsedUri.did, state) ||
2508
- (!state.ctx?.include3pBlocks && state.postBlocks?.get(postUri)?.embed)
2509
- ) {
2510
- return this.embedBlocked(uri, state)
2511
- }
2512
-
2513
- const post = state.posts?.get(postUri)
2514
- if (post?.violatesEmbeddingRules) {
2515
- return this.embedDetached(uri)
2516
- }
2517
-
2518
- if (parsedUri.collection === app.bsky.feed.post.$type) {
2519
- const view = this.embedPostView(uri, state, depth)
2520
- if (!view) return this.embedNotFound(uri)
2521
- const postgateRecordUri = postUriToPostgateUri(parsedUri.toString())
2522
- const postgate = state.postgates?.get(postgateRecordUri)
2523
- if (postgate?.record?.detachedEmbeddingUris?.includes(postUri)) {
2524
- return this.embedDetached(uri)
2525
- }
2526
- return this.recordEmbedWrapper(view, withTypeTag)
2527
- } else if (parsedUri.collection === app.bsky.feed.generator.$type) {
2528
- const view = this.feedGenerator(uri, state)
2529
- if (!view) return this.embedNotFound(uri)
2530
- return this.recordEmbedWrapper(
2531
- app.bsky.feed.defs.generatorView.$build(view),
2532
- withTypeTag,
2533
- )
2534
- } else if (parsedUri.collection === app.bsky.graph.list.$type) {
2535
- const view = this.list(uri, state)
2536
- if (!view) return this.embedNotFound(uri)
2537
- return this.recordEmbedWrapper(
2538
- app.bsky.graph.defs.listView.$build(view),
2539
- withTypeTag,
2540
- )
2541
- } else if (parsedUri.collection === app.bsky.labeler.service.$type) {
2542
- const view = this.labeler(parsedUri.did, state)
2543
- if (!view) return this.embedNotFound(uri)
2544
- return this.recordEmbedWrapper(
2545
- app.bsky.labeler.defs.labelerView.$build(view),
2546
- withTypeTag,
2547
- )
2548
- } else if (parsedUri.collection === app.bsky.graph.starterpack.$type) {
2549
- const view = this.starterPackBasic(uri, state)
2550
- if (!view) return this.embedNotFound(uri)
2551
- return this.recordEmbedWrapper(
2552
- app.bsky.graph.defs.starterPackViewBasic.$build(view),
2553
- withTypeTag,
2554
- )
2555
- }
2556
- return this.embedNotFound(uri)
2557
- }
2558
-
2559
- private recordEmbedWrapper<T extends $Typed<RecordEmbedViewInternal>>(
2560
- record: T,
2561
- withTypeTag: boolean,
2562
- ): RecordEmbedView {
2563
- return withTypeTag
2564
- ? app.bsky.embed.record.view.$build({ record })
2565
- : { record }
2566
- }
2567
-
2568
- recordWithMediaEmbed(
2569
- postUri: AtUriString,
2570
- embed: RecordWithMedia,
2571
- state: HydrationState,
2572
- depth: number,
2573
- ): $Typed<RecordWithMediaView> | undefined {
2574
- const creator = creatorFromUri(postUri)
2575
- let mediaEmbed:
2576
- | $Typed<ImagesEmbedView>
2577
- | $Typed<VideoEmbedView>
2578
- | $Typed<GalleryEmbedView>
2579
- | $Typed<ExternalEmbedView>
2580
- if (isImagesEmbedType(embed.media)) {
2581
- mediaEmbed = this.imagesEmbed(creator, embed.media)
2582
- } else if (isVideoEmbedType(embed.media)) {
2583
- mediaEmbed = this.videoEmbed(creator, embed.media)
2584
- } else if (isGalleryEmbedType(embed.media)) {
2585
- mediaEmbed = this.galleryEmbed(creator, embed.media)
2586
- } else if (isExternalEmbedType(embed.media)) {
2587
- mediaEmbed = this.externalEmbed(creator, embed.media, state)
2588
- } else {
2589
- return
2590
- }
2591
- return app.bsky.embed.recordWithMedia.view.$build({
2592
- media: mediaEmbed,
2593
- record: this.recordEmbed(postUri, embed.record, state, depth, false),
2594
- })
2595
- }
2596
-
2597
- userReplyDisabled(
2598
- uri: AtUriString,
2599
- state: HydrationState,
2600
- ): boolean | undefined {
2601
- const post = state.posts?.get(uri)
2602
- if (post?.violatesThreadGate) {
2603
- return true
2604
- }
2605
- const rootUriStr = post?.record.reply?.root.uri ?? uri
2606
- const gate = state.threadgates?.get(
2607
- postUriToThreadgateUri(rootUriStr),
2608
- )?.record
2609
- const viewer = state.ctx?.viewer
2610
- if (!gate || !viewer) {
2611
- return undefined
2612
- }
2613
- const rootPost = state.posts?.get(rootUriStr)?.record
2614
- const ownerDid = creatorFromUri(rootUriStr)
2615
- const {
2616
- canReply,
2617
- allowFollower,
2618
- allowFollowing,
2619
- allowListUris = [],
2620
- } = parseThreadGate(viewer, ownerDid, rootPost ?? null, gate)
2621
- if (canReply) {
2622
- return false
2623
- }
2624
- if (allowFollower && state.profileViewers?.get(ownerDid)?.following) {
2625
- return false
2626
- }
2627
- if (allowFollowing && state.profileViewers?.get(ownerDid)?.followedBy) {
2628
- return false
2629
- }
2630
- for (const listUri of allowListUris) {
2631
- const list = state.listViewers?.get(listUri)
2632
- if (list?.viewerInList) {
2633
- return false
2634
- }
2635
- }
2636
- return true
2637
- }
2638
-
2639
- userPostEmbeddingDisabled(
2640
- uri: AtUriString,
2641
- state: HydrationState,
2642
- ): boolean | undefined {
2643
- const post = state.posts?.get(uri)
2644
- if (!post) {
2645
- return true
2646
- }
2647
- const postgateRecordUri = postUriToPostgateUri(uri)
2648
- const gate = state.postgates?.get(postgateRecordUri)?.record
2649
- const viewerDid = state.ctx?.viewer ?? undefined
2650
- const {
2651
- embeddingRules: { canEmbed },
2652
- } = parsePostgate({
2653
- gate,
2654
- viewerDid,
2655
- authorDid: creatorFromUri(uri),
2656
- })
2657
- if (canEmbed) {
2658
- return false
2659
- }
2660
- return true
2661
- }
2662
-
2663
- viewerPinned(uri: AtUriString, state: HydrationState, authorDid: string) {
2664
- if (!state.ctx?.viewer || state.ctx.viewer !== authorDid) return
2665
- const actor = state.actors?.get(authorDid)
2666
- if (!actor) return
2667
- const pinnedPost = safePinnedPost(actor.profile?.pinnedPost)
2668
- if (!pinnedPost) return undefined
2669
- return pinnedPost.uri === uri
2670
- }
2671
-
2672
- notification(
2673
- notif: Notification,
2674
- lastSeenAt: string | undefined,
2675
- state: HydrationState,
2676
- ): Un$Typed<NotificationView> | undefined {
2677
- if (!notif.timestamp || !notif.reason) return
2678
- const uri = new AtUri(notif.uri)
2679
-
2680
- const author = this.profile(uri.did, state)
2681
- if (!author) return
2682
-
2683
- let recordInfo:
2684
- | Post
2685
- | Like
2686
- | Repost
2687
- | Follow
2688
- | RecordInfo<ProfileRecord>
2689
- | Verification
2690
- | Pick<RecordInfo<Required<NotificationRecordDeleted>>, 'cid' | 'record'>
2691
- | undefined
2692
- | null
2693
-
2694
- if (uri.collection === app.bsky.feed.post.$type) {
2695
- recordInfo = state.posts?.get(notif.uri as AtUriString)
2696
- } else if (uri.collection === app.bsky.feed.like.$type) {
2697
- recordInfo = state.likes?.get(notif.uri as AtUriString)
2698
- } else if (uri.collection === app.bsky.feed.repost.$type) {
2699
- recordInfo = state.reposts?.get(notif.uri as AtUriString)
2700
- } else if (uri.collection === app.bsky.graph.follow.$type) {
2701
- recordInfo = state.follows?.get(notif.uri as AtUriString)
2702
- } else if (uri.collection === app.bsky.graph.verification.$type) {
2703
- // When a verification record is removed, the record won't be found,
2704
- // both for the `verified` and `unverified` notifications.
2705
- recordInfo = state.verifications?.get(notif.uri as AtUriString) ?? {
2706
- record: notificationDeletedRecord,
2707
- cid: notificationDeletedRecordCid,
2708
- }
2709
- } else if (uri.collection === app.bsky.actor.profile.$type) {
2710
- const actor = state.actors?.get(author.did)
2711
- recordInfo =
2712
- actor && actor.profile && actor.profileCid
2713
- ? ({
2714
- record: actor.profile,
2715
- cid: actor.profileCid,
2716
- sortedAt: actor.sortedAt ?? new Date(0), // @NOTE will be present since profile record is present
2717
- indexedAt: actor.indexedAt ?? new Date(0), // @NOTE will be present since profile record is present
2718
- takedownRef: actor.profileTakedownRef,
2719
- } satisfies RecordInfo<ProfileRecord>)
2720
- : undefined
2721
- }
2722
- if (!recordInfo) return
2723
-
2724
- const labels = state.labels?.getBySubject(notif.uri as AtUriString) ?? []
2725
- const selfLabels = this.selfLabels({
2726
- uri: parseString<AtUriString>(notif.uri),
2727
- cid: recordInfo.cid,
2728
- record: recordInfo.record,
2729
- })
2730
- const indexedAt = notif.timestamp.toDate().toISOString()
2731
- return {
2732
- uri: notif.uri as AtUriString,
2733
- cid: recordInfo.cid,
2734
- author,
2735
- reason: notif.reason,
2736
- reasonSubject: parseString<AtUriString>(notif.reasonSubject),
2737
- record: recordInfo.record,
2738
- // @NOTE works with a hack in listNotifications so that when there's no last-seen time,
2739
- // the user's first notification is marked unread, and all previous read. in this case,
2740
- // the last seen time will be equal to the first notification's indexed time.
2741
- isRead: lastSeenAt ? lastSeenAt > indexedAt : true,
2742
- indexedAt: notif.timestamp.toDate().toISOString() as DatetimeString,
2743
- labels: [...labels, ...selfLabels],
2744
- }
2745
- }
2746
-
2747
- indexedAt({ sortedAt, indexedAt }: { sortedAt: Date; indexedAt: Date }) {
2748
- if (!this.indexedAtEpoch) return sortedAt
2749
- return indexedAt && indexedAt > this.indexedAtEpoch ? indexedAt : sortedAt
2750
- }
2751
- }
2752
-
2753
- const getRootUri = (uri: AtUriString, post: Post): AtUriString => {
2754
- return post.record.reply?.root.uri ?? uri
2755
- }
2756
-
2757
- const externalEmbedSourceTheme = (
2758
- theme: SiteStandardPublicationRecord['basicTheme'],
2759
- ): ExternalEmbedSourceThemeView | undefined => {
2760
- if (!theme) return undefined
2761
- const view: ExternalEmbedSourceThemeView = {}
2762
- const background = colorRGB(theme.background)
2763
- const foreground = colorRGB(theme.foreground)
2764
- const accent = colorRGB(theme.accent)
2765
- const accentForeground = colorRGB(theme.accentForeground)
2766
- if (background) view.backgroundRGB = background
2767
- if (foreground) view.foregroundRGB = foreground
2768
- if (accent) view.accentRGB = accent
2769
- if (accentForeground) view.accentForegroundRGB = accentForeground
2770
- // No fields set -> don't decorate the view at all.
2771
- return Object.keys(view).length === 0 ? undefined : view
2772
- }
2773
-
2774
- const colorRGB = (
2775
- color: { $type?: unknown } | undefined,
2776
- ): ExternalEmbedColorRgb | undefined => {
2777
- if (!color || !site.standard.theme.color.rgb.isTypeOf(color)) return undefined
2778
- // `isTypeOf` narrows $type but not the shape; pull rgb fields off the
2779
- // unchecked input. Records that fail full validation are dropped above
2780
- // by the SS record parser, so by the time we get here the shape matches.
2781
- const { r, g, b } = color as unknown as { r: number; g: number; b: number }
2782
- return app.bsky.embed.external.colorRGB.$build({ r, g, b })
2783
- }