@atproto/bsky 0.0.147 → 0.0.148

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 (127) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/api/app/bsky/feed/getListFeed.js +1 -1
  3. package/dist/api/app/bsky/feed/getListFeed.js.map +1 -1
  4. package/dist/api/app/bsky/graph/getFollowers.js +1 -1
  5. package/dist/api/app/bsky/graph/getFollowers.js.map +1 -1
  6. package/dist/api/app/bsky/graph/getFollows.js +1 -1
  7. package/dist/api/app/bsky/graph/getFollows.js.map +1 -1
  8. package/dist/api/app/bsky/graph/getList.js +1 -1
  9. package/dist/api/app/bsky/graph/getList.js.map +1 -1
  10. package/dist/api/app/bsky/unspecced/getSuggestedStarterPacks.js +1 -1
  11. package/dist/api/app/bsky/unspecced/getSuggestedStarterPacks.js.map +1 -1
  12. package/dist/api/app/bsky/unspecced/getSuggestedUsers.js +1 -1
  13. package/dist/api/app/bsky/unspecced/getSuggestedUsers.js.map +1 -1
  14. package/dist/api/app/bsky/unspecced/getTrends.js +1 -1
  15. package/dist/api/app/bsky/unspecced/getTrends.js.map +1 -1
  16. package/dist/api/com/atproto/identity/resolveHandle.d.ts.map +1 -1
  17. package/dist/api/com/atproto/identity/resolveHandle.js +3 -13
  18. package/dist/api/com/atproto/identity/resolveHandle.js.map +1 -1
  19. package/dist/api/health.js +1 -1
  20. package/dist/api/health.js.map +1 -1
  21. package/dist/data-plane/server/background.js +1 -1
  22. package/dist/data-plane/server/background.js.map +1 -1
  23. package/dist/data-plane/server/indexing/index.d.ts +2 -0
  24. package/dist/data-plane/server/indexing/index.d.ts.map +1 -1
  25. package/dist/data-plane/server/indexing/index.js +2 -0
  26. package/dist/data-plane/server/indexing/index.js.map +1 -1
  27. package/dist/data-plane/server/indexing/plugins/status.d.ts +7 -0
  28. package/dist/data-plane/server/indexing/plugins/status.d.ts.map +1 -0
  29. package/dist/data-plane/server/indexing/plugins/status.js +73 -0
  30. package/dist/data-plane/server/indexing/plugins/status.js.map +1 -0
  31. package/dist/data-plane/server/routes/index.d.ts.map +1 -1
  32. package/dist/data-plane/server/routes/index.js +0 -2
  33. package/dist/data-plane/server/routes/index.js.map +1 -1
  34. package/dist/data-plane/server/routes/profile.d.ts.map +1 -1
  35. package/dist/data-plane/server/routes/profile.js +8 -1
  36. package/dist/data-plane/server/routes/profile.js.map +1 -1
  37. package/dist/data-plane/server/routes/records.d.ts.map +1 -1
  38. package/dist/data-plane/server/routes/records.js +1 -0
  39. package/dist/data-plane/server/routes/records.js.map +1 -1
  40. package/dist/error.js +1 -1
  41. package/dist/error.js.map +1 -1
  42. package/dist/hydration/actor.d.ts +8 -1
  43. package/dist/hydration/actor.d.ts.map +1 -1
  44. package/dist/hydration/actor.js +18 -2
  45. package/dist/hydration/actor.js.map +1 -1
  46. package/dist/hydration/feed.d.ts +2 -2
  47. package/dist/hydration/feed.d.ts.map +1 -1
  48. package/dist/hydration/feed.js +10 -4
  49. package/dist/hydration/feed.js.map +1 -1
  50. package/dist/hydration/hydrator.d.ts +4 -3
  51. package/dist/hydration/hydrator.d.ts.map +1 -1
  52. package/dist/hydration/hydrator.js +47 -22
  53. package/dist/hydration/hydrator.js.map +1 -1
  54. package/dist/hydration/label.d.ts +1 -1
  55. package/dist/hydration/label.d.ts.map +1 -1
  56. package/dist/hydration/label.js +5 -2
  57. package/dist/hydration/label.js.map +1 -1
  58. package/dist/image/server.js +1 -1
  59. package/dist/image/server.js.map +1 -1
  60. package/dist/lexicon/index.d.ts +3 -0
  61. package/dist/lexicon/index.d.ts.map +1 -1
  62. package/dist/lexicon/index.js +4 -1
  63. package/dist/lexicon/index.js.map +1 -1
  64. package/dist/lexicon/lexicons.d.ts +160 -0
  65. package/dist/lexicon/lexicons.d.ts.map +1 -1
  66. package/dist/lexicon/lexicons.js +81 -0
  67. package/dist/lexicon/lexicons.js.map +1 -1
  68. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +21 -0
  69. package/dist/lexicon/types/app/bsky/actor/defs.d.ts.map +1 -1
  70. package/dist/lexicon/types/app/bsky/actor/defs.js +9 -0
  71. package/dist/lexicon/types/app/bsky/actor/defs.js.map +1 -1
  72. package/dist/lexicon/types/app/bsky/actor/status.d.ts +23 -0
  73. package/dist/lexicon/types/app/bsky/actor/status.d.ts.map +1 -0
  74. package/dist/lexicon/types/app/bsky/actor/status.js +19 -0
  75. package/dist/lexicon/types/app/bsky/actor/status.js.map +1 -0
  76. package/dist/proto/bsky_connect.d.ts +10 -12
  77. package/dist/proto/bsky_connect.d.ts.map +1 -1
  78. package/dist/proto/bsky_connect.js +9 -11
  79. package/dist/proto/bsky_connect.js.map +1 -1
  80. package/dist/proto/bsky_pb.d.ts +58 -37
  81. package/dist/proto/bsky_pb.d.ts.map +1 -1
  82. package/dist/proto/bsky_pb.js +189 -115
  83. package/dist/proto/bsky_pb.js.map +1 -1
  84. package/dist/views/index.d.ts +2 -1
  85. package/dist/views/index.d.ts.map +1 -1
  86. package/dist/views/index.js +37 -10
  87. package/dist/views/index.js.map +1 -1
  88. package/package.json +13 -13
  89. package/proto/bsky.proto +16 -17
  90. package/src/api/app/bsky/feed/getListFeed.ts +1 -1
  91. package/src/api/app/bsky/graph/getFollowers.ts +4 -1
  92. package/src/api/app/bsky/graph/getFollows.ts +4 -1
  93. package/src/api/app/bsky/graph/getList.ts +1 -1
  94. package/src/api/app/bsky/unspecced/getSuggestedStarterPacks.ts +1 -1
  95. package/src/api/app/bsky/unspecced/getSuggestedUsers.ts +1 -1
  96. package/src/api/app/bsky/unspecced/getTrends.ts +1 -1
  97. package/src/api/com/atproto/identity/resolveHandle.ts +3 -16
  98. package/src/api/health.ts +1 -1
  99. package/src/data-plane/server/background.ts +1 -1
  100. package/src/data-plane/server/indexing/index.ts +3 -0
  101. package/src/data-plane/server/indexing/plugins/status.ts +61 -0
  102. package/src/data-plane/server/routes/index.ts +0 -2
  103. package/src/data-plane/server/routes/profile.ts +43 -27
  104. package/src/data-plane/server/routes/records.ts +1 -0
  105. package/src/error.ts +1 -1
  106. package/src/hydration/actor.ts +27 -2
  107. package/src/hydration/feed.ts +16 -4
  108. package/src/hydration/hydrator.ts +64 -21
  109. package/src/hydration/label.ts +8 -2
  110. package/src/image/server.ts +1 -1
  111. package/src/lexicon/index.ts +3 -0
  112. package/src/lexicon/lexicons.ts +85 -0
  113. package/src/lexicon/types/app/bsky/actor/defs.ts +26 -0
  114. package/src/lexicon/types/app/bsky/actor/status.ts +40 -0
  115. package/src/proto/bsky_connect.ts +11 -13
  116. package/src/proto/bsky_pb.ts +173 -119
  117. package/src/views/index.ts +47 -11
  118. package/tests/views/__snapshots__/profile.test.ts.snap +60 -0
  119. package/tests/views/__snapshots__/thread.test.ts.snap +203 -11
  120. package/tests/views/profile.test.ts +189 -1
  121. package/tests/views/thread.test.ts +134 -2
  122. package/tsconfig.build.tsbuildinfo +1 -1
  123. package/dist/data-plane/server/routes/posts.d.ts +0 -6
  124. package/dist/data-plane/server/routes/posts.d.ts.map +0 -1
  125. package/dist/data-plane/server/routes/posts.js +0 -20
  126. package/dist/data-plane/server/routes/posts.js.map +0 -1
  127. package/src/data-plane/server/routes/posts.ts +0 -21
@@ -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
@@ -194,6 +194,9 @@ export const COM_ATPROTO_MODERATION = {
194
194
  DefsReasonOther: 'com.atproto.moderation.defs#reasonOther',
195
195
  DefsReasonAppeal: 'com.atproto.moderation.defs#reasonAppeal',
196
196
  }
197
+ export const APP_BSKY_ACTOR = {
198
+ StatusLive: 'app.bsky.actor.status#live',
199
+ }
197
200
  export const APP_BSKY_FEED = {
198
201
  DefsRequestLess: 'app.bsky.feed.defs#requestLess',
199
202
  DefsRequestMore: 'app.bsky.feed.defs#requestMore',
@@ -4568,6 +4568,10 @@ export const schemaDict = {
4568
4568
  type: 'ref',
4569
4569
  ref: 'lex:app.bsky.actor.defs#verificationState',
4570
4570
  },
4571
+ status: {
4572
+ type: 'ref',
4573
+ ref: 'lex:app.bsky.actor.defs#statusView',
4574
+ },
4571
4575
  },
4572
4576
  },
4573
4577
  profileView: {
@@ -4623,6 +4627,10 @@ export const schemaDict = {
4623
4627
  type: 'ref',
4624
4628
  ref: 'lex:app.bsky.actor.defs#verificationState',
4625
4629
  },
4630
+ status: {
4631
+ type: 'ref',
4632
+ ref: 'lex:app.bsky.actor.defs#statusView',
4633
+ },
4626
4634
  },
4627
4635
  },
4628
4636
  profileViewDetailed: {
@@ -4699,6 +4707,10 @@ export const schemaDict = {
4699
4707
  type: 'ref',
4700
4708
  ref: 'lex:app.bsky.actor.defs#verificationState',
4701
4709
  },
4710
+ status: {
4711
+ type: 'ref',
4712
+ ref: 'lex:app.bsky.actor.defs#statusView',
4713
+ },
4702
4714
  },
4703
4715
  },
4704
4716
  profileAssociated: {
@@ -5240,6 +5252,36 @@ export const schemaDict = {
5240
5252
  },
5241
5253
  },
5242
5254
  },
5255
+ statusView: {
5256
+ type: 'object',
5257
+ required: ['status', 'record'],
5258
+ properties: {
5259
+ status: {
5260
+ type: 'string',
5261
+ description: 'The status for the account.',
5262
+ knownValues: ['app.bsky.actor.status#live'],
5263
+ },
5264
+ record: {
5265
+ type: 'unknown',
5266
+ },
5267
+ embed: {
5268
+ type: 'union',
5269
+ description: 'An optional embed associated with the status.',
5270
+ refs: ['lex:app.bsky.embed.external#view'],
5271
+ },
5272
+ expiresAt: {
5273
+ type: 'string',
5274
+ description:
5275
+ 'The date when this status will expire. The application might choose to no longer return the status after expiration.',
5276
+ format: 'datetime',
5277
+ },
5278
+ isActive: {
5279
+ type: 'boolean',
5280
+ description:
5281
+ 'True if the status is not expired, false if it is expired. Only present if expiration was set.',
5282
+ },
5283
+ },
5284
+ },
5243
5285
  },
5244
5286
  },
5245
5287
  AppBskyActorGetPreferences: {
@@ -5569,6 +5611,48 @@ export const schemaDict = {
5569
5611
  },
5570
5612
  },
5571
5613
  },
5614
+ AppBskyActorStatus: {
5615
+ lexicon: 1,
5616
+ id: 'app.bsky.actor.status',
5617
+ defs: {
5618
+ main: {
5619
+ type: 'record',
5620
+ description: 'A declaration of a Bluesky account status.',
5621
+ key: 'literal:self',
5622
+ record: {
5623
+ type: 'object',
5624
+ required: ['status', 'createdAt'],
5625
+ properties: {
5626
+ status: {
5627
+ type: 'string',
5628
+ description: 'The status for the account.',
5629
+ knownValues: ['app.bsky.actor.status#live'],
5630
+ },
5631
+ embed: {
5632
+ type: 'union',
5633
+ description: 'An optional embed associated with the status.',
5634
+ refs: ['lex:app.bsky.embed.external'],
5635
+ },
5636
+ durationMinutes: {
5637
+ type: 'integer',
5638
+ description:
5639
+ 'The duration of the status in minutes. Applications can choose to impose minimum and maximum limits.',
5640
+ minimum: 1,
5641
+ },
5642
+ createdAt: {
5643
+ type: 'string',
5644
+ format: 'datetime',
5645
+ },
5646
+ },
5647
+ },
5648
+ },
5649
+ live: {
5650
+ type: 'token',
5651
+ description:
5652
+ 'Advertises an account as currently offering live content.',
5653
+ },
5654
+ },
5655
+ },
5572
5656
  AppBskyEmbedDefs: {
5573
5657
  lexicon: 1,
5574
5658
  id: 'app.bsky.embed.defs',
@@ -12497,6 +12581,7 @@ export const ids = {
12497
12581
  AppBskyActorPutPreferences: 'app.bsky.actor.putPreferences',
12498
12582
  AppBskyActorSearchActors: 'app.bsky.actor.searchActors',
12499
12583
  AppBskyActorSearchActorsTypeahead: 'app.bsky.actor.searchActorsTypeahead',
12584
+ AppBskyActorStatus: 'app.bsky.actor.status',
12500
12585
  AppBskyEmbedDefs: 'app.bsky.embed.defs',
12501
12586
  AppBskyEmbedExternal: 'app.bsky.embed.external',
12502
12587
  AppBskyEmbedImages: 'app.bsky.embed.images',