@atproto/bsky 0.0.147 → 0.0.149

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 (148) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE.txt +1 -1
  3. package/dist/api/app/bsky/feed/getListFeed.js +1 -1
  4. package/dist/api/app/bsky/feed/getListFeed.js.map +1 -1
  5. package/dist/api/app/bsky/graph/getFollowers.js +1 -1
  6. package/dist/api/app/bsky/graph/getFollowers.js.map +1 -1
  7. package/dist/api/app/bsky/graph/getFollows.js +1 -1
  8. package/dist/api/app/bsky/graph/getFollows.js.map +1 -1
  9. package/dist/api/app/bsky/graph/getList.js +1 -1
  10. package/dist/api/app/bsky/graph/getList.js.map +1 -1
  11. package/dist/api/app/bsky/unspecced/getConfig.d.ts.map +1 -1
  12. package/dist/api/app/bsky/unspecced/getConfig.js +1 -0
  13. package/dist/api/app/bsky/unspecced/getConfig.js.map +1 -1
  14. package/dist/api/app/bsky/unspecced/getSuggestedStarterPacks.js +1 -1
  15. package/dist/api/app/bsky/unspecced/getSuggestedStarterPacks.js.map +1 -1
  16. package/dist/api/app/bsky/unspecced/getSuggestedUsers.js +1 -1
  17. package/dist/api/app/bsky/unspecced/getSuggestedUsers.js.map +1 -1
  18. package/dist/api/app/bsky/unspecced/getTrends.js +1 -1
  19. package/dist/api/app/bsky/unspecced/getTrends.js.map +1 -1
  20. package/dist/api/com/atproto/identity/resolveHandle.d.ts.map +1 -1
  21. package/dist/api/com/atproto/identity/resolveHandle.js +3 -13
  22. package/dist/api/com/atproto/identity/resolveHandle.js.map +1 -1
  23. package/dist/api/health.js +1 -1
  24. package/dist/api/health.js.map +1 -1
  25. package/dist/api/well-known.d.ts.map +1 -1
  26. package/dist/api/well-known.js +4 -1
  27. package/dist/api/well-known.js.map +1 -1
  28. package/dist/config.d.ts +7 -0
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +28 -0
  31. package/dist/config.js.map +1 -1
  32. package/dist/data-plane/server/background.js +1 -1
  33. package/dist/data-plane/server/background.js.map +1 -1
  34. package/dist/data-plane/server/indexing/index.d.ts +2 -0
  35. package/dist/data-plane/server/indexing/index.d.ts.map +1 -1
  36. package/dist/data-plane/server/indexing/index.js +2 -0
  37. package/dist/data-plane/server/indexing/index.js.map +1 -1
  38. package/dist/data-plane/server/indexing/plugins/status.d.ts +7 -0
  39. package/dist/data-plane/server/indexing/plugins/status.d.ts.map +1 -0
  40. package/dist/data-plane/server/indexing/plugins/status.js +73 -0
  41. package/dist/data-plane/server/indexing/plugins/status.js.map +1 -0
  42. package/dist/data-plane/server/routes/index.d.ts.map +1 -1
  43. package/dist/data-plane/server/routes/index.js +0 -2
  44. package/dist/data-plane/server/routes/index.js.map +1 -1
  45. package/dist/data-plane/server/routes/profile.d.ts.map +1 -1
  46. package/dist/data-plane/server/routes/profile.js +8 -1
  47. package/dist/data-plane/server/routes/profile.js.map +1 -1
  48. package/dist/data-plane/server/routes/records.d.ts.map +1 -1
  49. package/dist/data-plane/server/routes/records.js +1 -0
  50. package/dist/data-plane/server/routes/records.js.map +1 -1
  51. package/dist/error.js +1 -1
  52. package/dist/error.js.map +1 -1
  53. package/dist/hydration/actor.d.ts +8 -1
  54. package/dist/hydration/actor.d.ts.map +1 -1
  55. package/dist/hydration/actor.js +18 -2
  56. package/dist/hydration/actor.js.map +1 -1
  57. package/dist/hydration/feed.d.ts +2 -2
  58. package/dist/hydration/feed.d.ts.map +1 -1
  59. package/dist/hydration/feed.js +10 -4
  60. package/dist/hydration/feed.js.map +1 -1
  61. package/dist/hydration/hydrator.d.ts +4 -3
  62. package/dist/hydration/hydrator.d.ts.map +1 -1
  63. package/dist/hydration/hydrator.js +47 -22
  64. package/dist/hydration/hydrator.js.map +1 -1
  65. package/dist/hydration/label.d.ts +1 -1
  66. package/dist/hydration/label.d.ts.map +1 -1
  67. package/dist/hydration/label.js +5 -2
  68. package/dist/hydration/label.js.map +1 -1
  69. package/dist/image/server.js +1 -1
  70. package/dist/image/server.js.map +1 -1
  71. package/dist/lexicon/index.d.ts +5 -2
  72. package/dist/lexicon/index.d.ts.map +1 -1
  73. package/dist/lexicon/index.js +8 -5
  74. package/dist/lexicon/index.js.map +1 -1
  75. package/dist/lexicon/lexicons.d.ts +288 -82
  76. package/dist/lexicon/lexicons.d.ts.map +1 -1
  77. package/dist/lexicon/lexicons.js +146 -42
  78. package/dist/lexicon/lexicons.js.map +1 -1
  79. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +21 -0
  80. package/dist/lexicon/types/app/bsky/actor/defs.d.ts.map +1 -1
  81. package/dist/lexicon/types/app/bsky/actor/defs.js +9 -0
  82. package/dist/lexicon/types/app/bsky/actor/defs.js.map +1 -1
  83. package/dist/lexicon/types/app/bsky/actor/status.d.ts +23 -0
  84. package/dist/lexicon/types/app/bsky/actor/status.d.ts.map +1 -0
  85. package/dist/lexicon/types/app/bsky/actor/status.js +19 -0
  86. package/dist/lexicon/types/app/bsky/actor/status.js.map +1 -0
  87. package/dist/lexicon/types/app/bsky/unspecced/getConfig.d.ts +9 -0
  88. package/dist/lexicon/types/app/bsky/unspecced/getConfig.d.ts.map +1 -1
  89. package/dist/lexicon/types/app/bsky/unspecced/getConfig.js +9 -0
  90. package/dist/lexicon/types/app/bsky/unspecced/getConfig.js.map +1 -1
  91. package/dist/proto/bsky_connect.d.ts +10 -12
  92. package/dist/proto/bsky_connect.d.ts.map +1 -1
  93. package/dist/proto/bsky_connect.js +9 -11
  94. package/dist/proto/bsky_connect.js.map +1 -1
  95. package/dist/proto/bsky_pb.d.ts +58 -37
  96. package/dist/proto/bsky_pb.d.ts.map +1 -1
  97. package/dist/proto/bsky_pb.js +189 -115
  98. package/dist/proto/bsky_pb.js.map +1 -1
  99. package/dist/views/index.d.ts +2 -1
  100. package/dist/views/index.d.ts.map +1 -1
  101. package/dist/views/index.js +37 -10
  102. package/dist/views/index.js.map +1 -1
  103. package/package.json +13 -13
  104. package/proto/bsky.proto +16 -17
  105. package/src/api/app/bsky/feed/getListFeed.ts +1 -1
  106. package/src/api/app/bsky/graph/getFollowers.ts +4 -1
  107. package/src/api/app/bsky/graph/getFollows.ts +4 -1
  108. package/src/api/app/bsky/graph/getList.ts +1 -1
  109. package/src/api/app/bsky/unspecced/getConfig.ts +1 -0
  110. package/src/api/app/bsky/unspecced/getSuggestedStarterPacks.ts +1 -1
  111. package/src/api/app/bsky/unspecced/getSuggestedUsers.ts +1 -1
  112. package/src/api/app/bsky/unspecced/getTrends.ts +1 -1
  113. package/src/api/com/atproto/identity/resolveHandle.ts +3 -16
  114. package/src/api/health.ts +1 -1
  115. package/src/api/well-known.ts +4 -1
  116. package/src/config.ts +39 -0
  117. package/src/data-plane/server/background.ts +1 -1
  118. package/src/data-plane/server/indexing/index.ts +3 -0
  119. package/src/data-plane/server/indexing/plugins/status.ts +61 -0
  120. package/src/data-plane/server/routes/index.ts +0 -2
  121. package/src/data-plane/server/routes/profile.ts +43 -27
  122. package/src/data-plane/server/routes/records.ts +1 -0
  123. package/src/error.ts +1 -1
  124. package/src/hydration/actor.ts +27 -2
  125. package/src/hydration/feed.ts +16 -4
  126. package/src/hydration/hydrator.ts +64 -21
  127. package/src/hydration/label.ts +8 -2
  128. package/src/image/server.ts +1 -1
  129. package/src/lexicon/index.ts +12 -9
  130. package/src/lexicon/lexicons.ts +151 -43
  131. package/src/lexicon/types/app/bsky/actor/defs.ts +26 -0
  132. package/src/lexicon/types/app/bsky/actor/status.ts +40 -0
  133. package/src/lexicon/types/app/bsky/unspecced/getConfig.ts +17 -0
  134. package/src/proto/bsky_connect.ts +11 -13
  135. package/src/proto/bsky_pb.ts +173 -119
  136. package/src/views/index.ts +47 -11
  137. package/tests/views/__snapshots__/profile.test.ts.snap +60 -0
  138. package/tests/views/__snapshots__/thread.test.ts.snap +203 -11
  139. package/tests/views/get-config.test.ts +66 -0
  140. package/tests/views/profile.test.ts +189 -1
  141. package/tests/views/thread.test.ts +134 -2
  142. package/tsconfig.build.tsbuildinfo +1 -1
  143. package/tsconfig.tests.tsbuildinfo +1 -1
  144. package/dist/data-plane/server/routes/posts.d.ts +0 -6
  145. package/dist/data-plane/server/routes/posts.d.ts.map +0 -1
  146. package/dist/data-plane/server/routes/posts.js +0 -20
  147. package/dist/data-plane/server/routes/posts.js.map +0 -1
  148. package/src/data-plane/server/routes/posts.ts +0 -21
package/src/config.ts CHANGED
@@ -1,4 +1,10 @@
1
1
  import assert from 'node:assert'
2
+ import { subLogger as log } from './logger'
3
+
4
+ type LiveNowConfig = {
5
+ did: string
6
+ domains: string[]
7
+ }[]
2
8
 
3
9
  export interface ServerConfigValues {
4
10
  // service
@@ -9,6 +15,7 @@ export interface ServerConfigValues {
9
15
  serverDid: string
10
16
  alternateAudienceDids: string[]
11
17
  entrywayJwtPublicKeyHex?: string
18
+ liveNowConfig?: LiveNowConfig
12
19
  // external services
13
20
  etcdHosts: string[]
14
21
  dataplaneUrls: string[]
@@ -81,6 +88,19 @@ export class ServerConfig {
81
88
  const alternateAudienceDids = envList(process.env.BSKY_ALT_AUDIENCE_DIDS)
82
89
  const entrywayJwtPublicKeyHex =
83
90
  process.env.BSKY_ENTRYWAY_JWT_PUBLIC_KEY_HEX || undefined
91
+ let liveNowConfig: LiveNowConfig | undefined
92
+ if (process.env.BSKY_LIVE_NOW_CONFIG) {
93
+ try {
94
+ const parsed = JSON.parse(process.env.BSKY_LIVE_NOW_CONFIG)
95
+ if (isLiveNowConfig(parsed)) {
96
+ liveNowConfig = parsed
97
+ } else {
98
+ throw new Error('Live Now config failed format validation')
99
+ }
100
+ } catch (err) {
101
+ log.error({ err }, 'Invalid BSKY_LIVE_NOW_CONFIG')
102
+ }
103
+ }
84
104
  const handleResolveNameservers = envList(
85
105
  process.env.BSKY_HANDLE_RESOLVE_NAMESERVERS,
86
106
  )
@@ -200,6 +220,7 @@ export class ServerConfig {
200
220
  serverDid,
201
221
  alternateAudienceDids,
202
222
  entrywayJwtPublicKeyHex,
223
+ liveNowConfig,
203
224
  etcdHosts,
204
225
  dataplaneUrls,
205
226
  dataplaneUrlsEtcdKeyPrefix,
@@ -285,6 +306,10 @@ export class ServerConfig {
285
306
  return this.cfg.entrywayJwtPublicKeyHex
286
307
  }
287
308
 
309
+ get liveNowConfig() {
310
+ return this.cfg.liveNowConfig
311
+ }
312
+
288
313
  get etcdHosts() {
289
314
  return this.cfg.etcdHosts
290
315
  }
@@ -482,3 +507,17 @@ function envList(str: string | undefined): string[] {
482
507
  if (str === undefined || str.length === 0) return []
483
508
  return str.split(',')
484
509
  }
510
+
511
+ function isLiveNowConfig(data: any): data is LiveNowConfig {
512
+ return (
513
+ Array.isArray(data) &&
514
+ data.every(
515
+ (item) =>
516
+ typeof item === 'object' &&
517
+ item !== null &&
518
+ typeof item.did === 'string' &&
519
+ Array.isArray(item.domains) &&
520
+ item.domains.every((domain: any) => typeof domain === 'string'),
521
+ )
522
+ )
523
+ }
@@ -16,7 +16,7 @@ export class BackgroundQueue {
16
16
  this.queue
17
17
  .add(() => task(this.db))
18
18
  .catch((err) => {
19
- dbLogger.error(err, 'background queue task failed')
19
+ dbLogger.error({ err }, 'background queue task failed')
20
20
  })
21
21
  }
22
22
 
@@ -31,6 +31,7 @@ import * as Postgate from './plugins/post-gate'
31
31
  import * as Profile from './plugins/profile'
32
32
  import * as Repost from './plugins/repost'
33
33
  import * as StarterPack from './plugins/starter-pack'
34
+ import * as Status from './plugins/status'
34
35
  import * as Threadgate from './plugins/thread-gate'
35
36
  import * as Verification from './plugins/verification'
36
37
  import { RecordProcessor } from './processor'
@@ -53,6 +54,7 @@ export class IndexingService {
53
54
  labeler: Labeler.PluginType
54
55
  chatDeclaration: ChatDeclaration.PluginType
55
56
  verification: Verification.PluginType
57
+ status: Status.PluginType
56
58
  }
57
59
 
58
60
  constructor(
@@ -77,6 +79,7 @@ export class IndexingService {
77
79
  labeler: Labeler.makePlugin(this.db, this.background),
78
80
  chatDeclaration: ChatDeclaration.makePlugin(this.db, this.background),
79
81
  verification: Verification.makePlugin(this.db, this.background),
82
+ status: Status.makePlugin(this.db, this.background),
80
83
  }
81
84
  }
82
85
 
@@ -0,0 +1,61 @@
1
+ import { CID } from 'multiformats/cid'
2
+ import { AtUri } from '@atproto/syntax'
3
+ import * as lex from '../../../../lexicon/lexicons'
4
+ import { BackgroundQueue } from '../../background'
5
+ import { Database } from '../../db'
6
+ import { DatabaseSchema } from '../../db/database-schema'
7
+ import { RecordProcessor } from '../processor'
8
+
9
+ // @NOTE this indexer is a placeholder to ensure it gets indexed in the generic records table
10
+
11
+ const lexId = lex.ids.AppBskyActorStatus
12
+
13
+ const insertFn = async (
14
+ _db: DatabaseSchema,
15
+ uri: AtUri,
16
+ _cid: CID,
17
+ _obj: unknown,
18
+ _timestamp: string,
19
+ ): Promise<unknown | null> => {
20
+ if (uri.rkey !== 'self') return null
21
+ return true
22
+ }
23
+
24
+ const findDuplicate = async (): Promise<AtUri | null> => {
25
+ return null
26
+ }
27
+
28
+ const notifsForInsert = () => {
29
+ return []
30
+ }
31
+
32
+ const deleteFn = async (
33
+ _db: DatabaseSchema,
34
+ uri: AtUri,
35
+ ): Promise<unknown | null> => {
36
+ if (uri.rkey !== 'self') return null
37
+ return true
38
+ }
39
+
40
+ const notifsForDelete = () => {
41
+ return { notifs: [], toDelete: [] }
42
+ }
43
+
44
+ export type PluginType = RecordProcessor<unknown, unknown>
45
+
46
+ export const makePlugin = (
47
+ db: Database,
48
+ background: BackgroundQueue,
49
+ ): PluginType => {
50
+ const processor = new RecordProcessor(db, background, {
51
+ lexId,
52
+ insertFn,
53
+ findDuplicate,
54
+ deleteFn,
55
+ notifsForInsert,
56
+ notifsForDelete,
57
+ })
58
+ return processor
59
+ }
60
+
61
+ export default makePlugin
@@ -14,7 +14,6 @@ import lists from './lists'
14
14
  import moderation from './moderation'
15
15
  import mutes from './mutes'
16
16
  import notifs from './notifs'
17
- import posts from './posts'
18
17
  import profile from './profile'
19
18
  import quotes from './quotes'
20
19
  import records from './records'
@@ -41,7 +40,6 @@ export default (db: Database, idResolver: IdResolver) =>
41
40
  ...moderation(db),
42
41
  ...mutes(db),
43
42
  ...notifs(db),
44
- ...posts(db),
45
43
  ...profile(db),
46
44
  ...quotes(db),
47
45
  ...records(db),
@@ -25,37 +25,46 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
25
25
  const profileUris = dids.map(
26
26
  (did) => `at://${did}/app.bsky.actor.profile/self`,
27
27
  )
28
+ const statusUris = dids.map(
29
+ (did) => `at://${did}/app.bsky.actor.status/self`,
30
+ )
28
31
  const chatDeclarationUris = dids.map(
29
32
  (did) => `at://${did}/chat.bsky.actor.declaration/self`,
30
33
  )
31
34
  const { ref } = db.db.dynamic
32
- const [handlesRes, verificationsReceived, profiles, chatDeclarations] =
33
- await Promise.all([
34
- db.db
35
- .selectFrom('actor')
36
- .leftJoin('actor_state', 'actor_state.did', 'actor.did')
37
- .where('actor.did', 'in', dids)
38
- .selectAll('actor')
39
- .select('actor_state.priorityNotifs')
40
- .select([
41
- db.db
42
- .selectFrom('labeler')
43
- .whereRef('creator', '=', ref('actor.did'))
44
- .select(sql<true>`${true}`.as('val'))
45
- .as('isLabeler'),
46
- ])
47
- .execute(),
48
- db.db
49
- .selectFrom('verification')
50
- .selectAll('verification')
51
- .innerJoin('actor', 'actor.did', 'verification.creator')
52
- .where('verification.subject', 'in', dids)
53
- .where('actor.trustedVerifier', '=', true)
54
- .orderBy('sortedAt', 'asc')
55
- .execute(),
56
- getRecords(db)({ uris: profileUris }),
57
- getRecords(db)({ uris: chatDeclarationUris }),
58
- ])
35
+ const [
36
+ handlesRes,
37
+ verificationsReceived,
38
+ profiles,
39
+ statuses,
40
+ chatDeclarations,
41
+ ] = await Promise.all([
42
+ db.db
43
+ .selectFrom('actor')
44
+ .leftJoin('actor_state', 'actor_state.did', 'actor.did')
45
+ .where('actor.did', 'in', dids)
46
+ .selectAll('actor')
47
+ .select('actor_state.priorityNotifs')
48
+ .select([
49
+ db.db
50
+ .selectFrom('labeler')
51
+ .whereRef('creator', '=', ref('actor.did'))
52
+ .select(sql<true>`${true}`.as('val'))
53
+ .as('isLabeler'),
54
+ ])
55
+ .execute(),
56
+ db.db
57
+ .selectFrom('verification')
58
+ .selectAll('verification')
59
+ .innerJoin('actor', 'actor.did', 'verification.creator')
60
+ .where('verification.subject', 'in', dids)
61
+ .where('actor.trustedVerifier', '=', true)
62
+ .orderBy('sortedAt', 'asc')
63
+ .execute(),
64
+ getRecords(db)({ uris: profileUris }),
65
+ getRecords(db)({ uris: statusUris }),
66
+ getRecords(db)({ uris: chatDeclarationUris }),
67
+ ])
59
68
 
60
69
  const verificationsBySubjectDid = verificationsReceived.reduce(
61
70
  (acc, cur) => {
@@ -70,6 +79,9 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
70
79
  const byDid = keyBy(handlesRes, 'did')
71
80
  const actors = dids.map((did, i) => {
72
81
  const row = byDid.get(did)
82
+
83
+ const status = statuses.records[i]
84
+
73
85
  const chatDeclaration = parseRecordBytes(
74
86
  chatDeclarations.records[i].record,
75
87
  )
@@ -102,11 +114,15 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
102
114
  priorityNotifications: row?.priorityNotifs ?? false,
103
115
  trustedVerifier: row?.trustedVerifier ?? false,
104
116
  verifiedBy,
117
+ statusRecord: status,
118
+ tags: [],
119
+ profileTags: [],
105
120
  }
106
121
  })
107
122
  return { actors }
108
123
  },
109
124
 
125
+ // @TODO handle req.lookupUnidirectional w/ networked handle resolution
110
126
  async getDidsByHandles(req) {
111
127
  const { handles } = req
112
128
  if (handles.length === 0) {
@@ -25,6 +25,7 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
25
25
  getActorChatDeclarationRecords: getRecords(db, ids.ChatBskyActorDeclaration),
26
26
  getStarterPackRecords: getRecords(db, ids.AppBskyGraphStarterpack),
27
27
  getVerificationRecords: getRecords(db, ids.AppBskyGraphVerification),
28
+ getStatusRecords: getRecords(db, ids.AppBskyActorStatus),
28
29
  })
29
30
 
30
31
  export const getRecords =
package/src/error.ts CHANGED
@@ -3,7 +3,7 @@ import { XRPCError } from '@atproto/xrpc-server'
3
3
  import { httpLogger as log } from './logger'
4
4
 
5
5
  export const handler: ErrorRequestHandler = (err, _req, res, next) => {
6
- log.error(err, 'unexpected internal server error')
6
+ log.error({ err }, 'unexpected internal server error')
7
7
  if (res.headersSent) {
8
8
  return next(err)
9
9
  }
@@ -1,6 +1,7 @@
1
1
  import { mapDefined } from '@atproto/common'
2
2
  import { DataPlaneClient } from '../data-plane/client'
3
3
  import { Record as ProfileRecord } from '../lexicon/types/app/bsky/actor/profile'
4
+ import { Record as StatusRecord } from '../lexicon/types/app/bsky/actor/status'
4
5
  import { Record as ChatDeclarationRecord } from '../lexicon/types/chat/bsky/actor/declaration'
5
6
  import { VerificationMeta } from '../proto/bsky_pb'
6
7
  import {
@@ -27,6 +28,7 @@ export type Actor = {
27
28
  priorityNotifications: boolean
28
29
  trustedVerifier?: boolean
29
30
  verifications: VerificationHydrationState[]
31
+ status?: RecordInfo<StatusRecord>
30
32
  }
31
33
 
32
34
  export type VerificationHydrationState = {
@@ -45,6 +47,9 @@ export type ChatDeclaration = RecordInfo<ChatDeclarationRecord>
45
47
 
46
48
  export type ChatDeclarations = HydrationMap<ChatDeclaration>
47
49
 
50
+ export type Status = RecordInfo<StatusRecord>
51
+ export type Statuses = HydrationMap<Status>
52
+
48
53
  export type ProfileViewerState = {
49
54
  muted?: boolean
50
55
  mutedByList?: string
@@ -88,10 +93,16 @@ export class ActorHydrator {
88
93
  }
89
94
  }
90
95
 
91
- async getDids(handleOrDids: string[]): Promise<(string | undefined)[]> {
96
+ async getDids(
97
+ handleOrDids: string[],
98
+ opts?: { lookupUnidirectional?: boolean },
99
+ ): Promise<(string | undefined)[]> {
92
100
  const handles = handleOrDids.filter((actor) => !actor.startsWith('did:'))
93
101
  const res = handles.length
94
- ? await this.dataplane.getDidsByHandles({ handles })
102
+ ? await this.dataplane.getDidsByHandles({
103
+ handles,
104
+ lookupUnidirectional: opts?.lookupUnidirectional,
105
+ })
95
106
  : { dids: [] }
96
107
  const didByHandle = handles.reduce(
97
108
  (acc, cur, i) => {
@@ -141,6 +152,10 @@ export class ActorHydrator {
141
152
  ? parseRecord<ProfileRecord>(actor.profile, includeTakedowns)
142
153
  : undefined
143
154
 
155
+ const status = actor.statusRecord
156
+ ? parseRecord<StatusRecord>(actor.statusRecord, includeTakedowns)
157
+ : undefined
158
+
144
159
  const verifications = mapDefined(
145
160
  Object.entries(actor.verifiedBy),
146
161
  ([actorDid, verificationMeta]) => {
@@ -178,6 +193,7 @@ export class ActorHydrator {
178
193
  priorityNotifications: actor.priorityNotifications,
179
194
  trustedVerifier: actor.trustedVerifier,
180
195
  verifications,
196
+ status: status,
181
197
  })
182
198
  }, new HydrationMap<Actor>())
183
199
  }
@@ -197,6 +213,15 @@ export class ActorHydrator {
197
213
  }, new HydrationMap<ChatDeclaration>())
198
214
  }
199
215
 
216
+ async getStatus(uris: string[], includeTakedowns = false): Promise<Statuses> {
217
+ if (!uris.length) return new HydrationMap<Status>()
218
+ const res = await this.dataplane.getStatusRecords({ uris })
219
+ return uris.reduce((acc, uri, i) => {
220
+ const record = parseRecord<StatusRecord>(res.records[i], includeTakedowns)
221
+ return acc.set(uri, record ?? null)
222
+ }, new HydrationMap<Status>())
223
+ }
224
+
200
225
  // "naive" because this method does not verify the existence of the list itself
201
226
  // a later check in the main hydrator will remove list uris that have been deleted or
202
227
  // repurposed to "curate lists"
@@ -206,9 +206,15 @@ export class FeedHydrator {
206
206
  }, new HydrationMap<ThreadContext>())
207
207
  }
208
208
 
209
- async getPostAggregates(refs: ItemRef[]): Promise<PostAggs> {
209
+ async getPostAggregates(
210
+ refs: ItemRef[],
211
+ viewer: string | null,
212
+ ): Promise<PostAggs> {
210
213
  if (!refs.length) return new HydrationMap<PostAgg>()
211
- const counts = await this.dataplane.getInteractionCounts({ refs })
214
+ const counts = await this.dataplane.getInteractionCounts({
215
+ refs,
216
+ skipCacheForDids: viewer ? [viewer] : undefined,
217
+ })
212
218
  return refs.reduce((acc, { uri }, i) => {
213
219
  return acc.set(uri, {
214
220
  likes: counts.likes[i] ?? 0,
@@ -250,9 +256,15 @@ export class FeedHydrator {
250
256
  }, new HydrationMap<FeedGenViewerState>())
251
257
  }
252
258
 
253
- async getFeedGenAggregates(refs: ItemRef[]): Promise<FeedGenAggs> {
259
+ async getFeedGenAggregates(
260
+ refs: ItemRef[],
261
+ viewer: string | null,
262
+ ): Promise<FeedGenAggs> {
254
263
  if (!refs.length) return new HydrationMap<FeedGenAgg>()
255
- const counts = await this.dataplane.getInteractionCounts({ refs })
264
+ const counts = await this.dataplane.getInteractionCounts({
265
+ refs,
266
+ skipCacheForDids: viewer ? [viewer] : undefined,
267
+ })
256
268
  return refs.reduce((acc, { uri }, i) => {
257
269
  return acc.set(uri, {
258
270
  likes: counts.likes[i] ?? 0,
@@ -37,6 +37,7 @@ import {
37
37
  Threadgates,
38
38
  } from './feed'
39
39
  import {
40
+ BlockEntry,
40
41
  Follows,
41
42
  GraphHydrator,
42
43
  ListAggs,
@@ -265,7 +266,7 @@ export class Hydrator {
265
266
  const [state, profileAggs, bidirectionalBlocks] = await Promise.all([
266
267
  this.hydrateProfiles(allDids, ctx),
267
268
  this.actor.getProfileAggregates(dids),
268
- this.hydrateBidirectionalBlocks(subjectsToKnownFollowersMap),
269
+ this.hydrateBidirectionalBlocks(subjectsToKnownFollowersMap, ctx),
269
270
  ])
270
271
  const starterPackUriSet = new Set<string>()
271
272
  state.actors?.forEach((actor) => {
@@ -486,12 +487,12 @@ export class Hydrator {
486
487
  starterPackState,
487
488
  postgates,
488
489
  ] = await Promise.all([
489
- this.feed.getPostAggregates(allRefs),
490
+ this.feed.getPostAggregates(allRefs, ctx.viewer),
490
491
  ctx.viewer
491
492
  ? this.feed.getPostViewerStates(threadRefs, ctx.viewer)
492
493
  : undefined,
493
494
  this.label.getLabelsForSubjects(allPostUris, ctx.labelers),
494
- this.hydratePostBlocks(posts),
495
+ this.hydratePostBlocks(posts, ctx),
495
496
  this.hydrateProfiles(allPostUris.map(didFromUri), ctx),
496
497
  this.hydrateLists([...nestedListUris, ...threadgateListUris], ctx),
497
498
  this.hydrateFeedGens(nestedFeedGenUris, ctx),
@@ -522,7 +523,10 @@ export class Hydrator {
522
523
  )
523
524
  }
524
525
 
525
- private async hydratePostBlocks(posts: Posts): Promise<PostBlocks> {
526
+ private async hydratePostBlocks(
527
+ posts: Posts,
528
+ ctx: HydrateCtx,
529
+ ): Promise<PostBlocks> {
526
530
  const postBlocks = new HydrationMap<PostBlock>()
527
531
  const postBlocksPairs = new Map<string, PostBlockPairs>()
528
532
  const relationships: RelationshipPair[] = []
@@ -557,6 +561,7 @@ export class Hydrator {
557
561
  // replace embed/parent/root pairs with block state
558
562
  const blocks = await this.hydrateBidirectionalBlocks(
559
563
  pairsToMap(relationships),
564
+ ctx,
560
565
  )
561
566
  for (const [uri, { embed, parent, root }] of postBlocksPairs) {
562
567
  postBlocks.set(uri, {
@@ -683,7 +688,10 @@ export class Hydrator {
683
688
  const [feedgens, feedgenAggs, feedgenViewers, profileState, labels] =
684
689
  await Promise.all([
685
690
  this.feed.getFeedGens(uris, ctx.includeTakedowns),
686
- this.feed.getFeedGenAggregates(uris.map((uri) => ({ uri }))),
691
+ this.feed.getFeedGenAggregates(
692
+ uris.map((uri) => ({ uri })),
693
+ ctx.viewer,
694
+ ),
687
695
  ctx.viewer
688
696
  ? this.feed.getFeedGenViewerStates(uris, ctx.viewer)
689
697
  : undefined,
@@ -782,6 +790,7 @@ export class Hydrator {
782
790
  )
783
791
  const blocks = await this.hydrateBidirectionalBlocks(
784
792
  pairsToMap(listCreatorMemberPairs),
793
+ ctx,
785
794
  )
786
795
  // sample top list items per starter pack based on their follows
787
796
  const listMemberAggs = await this.actor.getProfileAggregates(listMemberDids)
@@ -839,7 +848,7 @@ export class Hydrator {
839
848
  pairs.push([authorDid, didFromUri(uri)])
840
849
  }
841
850
  }
842
- const blocks = await this.hydrateBidirectionalBlocks(pairsToMap(pairs))
851
+ const blocks = await this.hydrateBidirectionalBlocks(pairsToMap(pairs), ctx)
843
852
  const likeBlocks = new HydrationMap<LikeBlock>()
844
853
  for (const [uri, like] of likes) {
845
854
  if (like) {
@@ -925,7 +934,10 @@ export class Hydrator {
925
934
  }
926
935
 
927
936
  // provides partial hydration state within getFollows / getFollowers, mainly for applying rules
928
- async hydrateFollows(uris: string[]): Promise<HydrationState> {
937
+ async hydrateFollows(
938
+ uris: string[],
939
+ ctx: HydrateCtx,
940
+ ): Promise<HydrationState> {
929
941
  const follows = await this.graph.getFollows(uris)
930
942
  const pairs: RelationshipPair[] = []
931
943
  for (const [uri, follow] of follows) {
@@ -933,7 +945,7 @@ export class Hydrator {
933
945
  pairs.push([didFromUri(uri), follow.record.subject])
934
946
  }
935
947
  }
936
- const blocks = await this.hydrateBidirectionalBlocks(pairsToMap(pairs))
948
+ const blocks = await this.hydrateBidirectionalBlocks(pairsToMap(pairs), ctx)
937
949
  const followBlocks = new HydrationMap<FollowBlock>()
938
950
  for (const [uri, follow] of follows) {
939
951
  if (follow) {
@@ -950,6 +962,7 @@ export class Hydrator {
950
962
 
951
963
  async hydrateBidirectionalBlocks(
952
964
  didMap: Map<string, string[]>, // DID -> DID[]
965
+ ctx: HydrateCtx,
953
966
  ): Promise<BidirectionalBlocks> {
954
967
  const pairs: RelationshipPair[] = []
955
968
  for (const [source, targets] of didMap) {
@@ -958,32 +971,56 @@ export class Hydrator {
958
971
  }
959
972
  }
960
973
 
961
- const result = new HydrationMap<HydrationMap<boolean>>()
962
974
  const blocks = await this.graph.getBidirectionalBlocks(pairs)
963
-
964
- // lookup list authors to apply takedown status to blocklists
965
- const listAuthorDids = new Set<string>()
975
+ const listUrisSet = new Set<string>()
966
976
  for (const [source, targets] of didMap) {
967
977
  for (const target of targets) {
968
978
  const block = blocks.get(source, target)
969
979
  if (block?.blockListUri) {
970
- listAuthorDids.add(uriToDid(block.blockListUri))
980
+ listUrisSet.add(block.blockListUri)
971
981
  }
972
982
  }
973
983
  }
984
+ const listUris = [...listUrisSet]
974
985
 
975
- const activeListAuthors = await this.actor.getActors([...listAuthorDids])
986
+ // if a list no longer exists or is not a mod list, then remove from block entry
987
+ const listState = await this.hydrateListsBasic(listUris, ctx)
988
+ for (const [source, targets] of didMap) {
989
+ for (const target of targets) {
990
+ const block = blocks.get(source, target)
991
+ if (!isModList(block?.blockListUri, listState)) {
992
+ delete block?.blockListUri
993
+ }
994
+ }
995
+ }
976
996
 
997
+ const result: BidirectionalBlocks = new HydrationMap<
998
+ HydrationMap<boolean>
999
+ >()
977
1000
  for (const [source, targets] of didMap) {
978
1001
  const didBlocks = new HydrationMap<boolean>()
979
1002
  for (const target of targets) {
980
1003
  const block = blocks.get(source, target)
981
- const isBlocked = !!(
982
- block?.blockUri ||
983
- (block?.blockListUri &&
984
- activeListAuthors.get(uriToDid(block.blockListUri)))
1004
+
1005
+ // If a list no longer exists or is not a mod list, then remove from block entry.
1006
+ // isModList confirms the list exists in listState, which ensures it wasn't taken down.
1007
+ if (!isModList(block?.blockListUri, listState)) {
1008
+ delete block?.blockListUri
1009
+ }
1010
+
1011
+ const blockEntry: BlockEntry = {
1012
+ blockUri: block?.blockUri,
1013
+ blockListUri:
1014
+ block?.blockListUri &&
1015
+ listState.actors?.get(uriToDid(block.blockListUri))
1016
+ ? block.blockListUri
1017
+ : undefined,
1018
+ }
1019
+
1020
+ didBlocks.set(
1021
+ target,
1022
+ !!blockEntry.blockUri || !!blockEntry.blockListUri,
985
1023
  )
986
- didBlocks.set(target, isBlocked)
987
1024
  }
988
1025
  result.set(source, didBlocks)
989
1026
  }
@@ -1002,7 +1039,7 @@ export class Hydrator {
1002
1039
  const [labelers, labelerAggs, labelerViewers, profileState] =
1003
1040
  await Promise.all([
1004
1041
  this.label.getLabelers(dids, ctx.includeTakedowns),
1005
- this.label.getLabelerAggregates(dids),
1042
+ this.label.getLabelerAggregates(dids, ctx.viewer),
1006
1043
  ctx.viewer
1007
1044
  ? this.label.getLabelerViewerStates(dids, ctx.viewer)
1008
1045
  : undefined,
@@ -1093,6 +1130,12 @@ export class Hydrator {
1093
1130
  uri,
1094
1131
  ) ?? undefined
1095
1132
  )
1133
+ } else if (collection === ids.AppBskyActorStatus) {
1134
+ if (parsed.rkey !== 'self') return
1135
+ return (
1136
+ (await this.actor.getStatus([uri], includeTakedowns)).get(uri) ??
1137
+ undefined
1138
+ )
1096
1139
  } else if (collection === ids.AppBskyActorProfile) {
1097
1140
  const did = parsed.hostname
1098
1141
  const actor = (
@@ -1251,7 +1294,7 @@ const getListUrisFromThreadgates = (gates: Threadgates) => {
1251
1294
  }
1252
1295
 
1253
1296
  const isBlocked = (blocks: BidirectionalBlocks, [a, b]: RelationshipPair) => {
1254
- return blocks.get(a)?.get(b) ?? null
1297
+ return blocks.get(a)?.get(b) ?? false
1255
1298
  }
1256
1299
 
1257
1300
  const pairsToMap = (pairs: RelationshipPair[]): Map<string, string[]> => {
@@ -156,9 +156,15 @@ export class LabelHydrator {
156
156
  }, new HydrationMap<LabelerViewerState>())
157
157
  }
158
158
 
159
- async getLabelerAggregates(dids: string[]): Promise<LabelerAggs> {
159
+ async getLabelerAggregates(
160
+ dids: string[],
161
+ viewer: string | null,
162
+ ): Promise<LabelerAggs> {
160
163
  const refs = dids.map((did) => ({ uri: labelerDidToUri(did) }))
161
- const counts = await this.dataplane.getInteractionCounts({ refs })
164
+ const counts = await this.dataplane.getInteractionCounts({
165
+ refs,
166
+ skipCacheForDids: viewer ? [viewer] : undefined,
167
+ })
162
168
  return dids.reduce((acc, did, i) => {
163
169
  return acc.set(did, {
164
170
  likes: counts.likes[i] ?? 0,
@@ -102,7 +102,7 @@ export function createMiddleware(
102
102
  // Cache in the background
103
103
  cache
104
104
  .put(cacheKey, cloneStream(processor))
105
- .catch((err) => log.error(err, 'failed to cache image'))
105
+ .catch((err) => log.error({ err }, 'failed to cache image'))
106
106
 
107
107
  res.statusCode = 200
108
108
  res.setHeader('cache-control', `public, max-age=31536000`) // 1 year