@atproto/bsky 0.0.112 → 0.0.113

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 (47) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/api/app/bsky/actor/getSuggestions.d.ts.map +1 -1
  3. package/dist/api/app/bsky/actor/getSuggestions.js.map +1 -1
  4. package/dist/api/app/bsky/feed/getAuthorFeed.d.ts.map +1 -1
  5. package/dist/api/app/bsky/feed/getAuthorFeed.js +4 -2
  6. package/dist/api/app/bsky/feed/getAuthorFeed.js.map +1 -1
  7. package/dist/api/app/bsky/feed/getFeed.d.ts.map +1 -1
  8. package/dist/api/app/bsky/feed/getFeed.js +1 -0
  9. package/dist/api/app/bsky/feed/getFeed.js.map +1 -1
  10. package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.d.ts.map +1 -1
  11. package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js.map +1 -1
  12. package/dist/api/blob-resolver.d.ts.map +1 -1
  13. package/dist/api/blob-resolver.js +2 -0
  14. package/dist/api/blob-resolver.js.map +1 -1
  15. package/dist/api/util.d.ts +1 -0
  16. package/dist/api/util.d.ts.map +1 -1
  17. package/dist/api/util.js +2 -1
  18. package/dist/api/util.js.map +1 -1
  19. package/dist/data-plane/server/routes/relationships.d.ts.map +1 -1
  20. package/dist/data-plane/server/routes/relationships.js +51 -31
  21. package/dist/data-plane/server/routes/relationships.js.map +1 -1
  22. package/dist/hydration/graph.d.ts +7 -4
  23. package/dist/hydration/graph.d.ts.map +1 -1
  24. package/dist/hydration/graph.js +17 -19
  25. package/dist/hydration/graph.js.map +1 -1
  26. package/dist/hydration/hydrator.d.ts +3 -1
  27. package/dist/hydration/hydrator.d.ts.map +1 -1
  28. package/dist/hydration/hydrator.js +49 -17
  29. package/dist/hydration/hydrator.js.map +1 -1
  30. package/dist/views/index.d.ts +8 -3
  31. package/dist/views/index.d.ts.map +1 -1
  32. package/dist/views/index.js +39 -18
  33. package/dist/views/index.js.map +1 -1
  34. package/package.json +11 -11
  35. package/src/api/app/bsky/actor/getSuggestions.ts +4 -3
  36. package/src/api/app/bsky/feed/getAuthorFeed.ts +8 -2
  37. package/src/api/app/bsky/feed/getFeed.ts +2 -1
  38. package/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +3 -2
  39. package/src/api/blob-resolver.ts +3 -0
  40. package/src/api/util.ts +1 -0
  41. package/src/data-plane/server/routes/relationships.ts +64 -40
  42. package/src/hydration/graph.ts +23 -23
  43. package/src/hydration/hydrator.ts +66 -19
  44. package/src/views/index.ts +57 -26
  45. package/tests/label-hydration.test.ts +5 -2
  46. package/tests/views/author-feed.test.ts +1 -1
  47. package/tests/views/labels-takedown.test.ts +29 -0
@@ -43,7 +43,7 @@ import {
43
43
  mergeNestedMaps,
44
44
  mergeManyMaps,
45
45
  } from './util'
46
- import { uriToDid as didFromUri } from '../util/uris'
46
+ import { uriToDid as didFromUri, uriToDid } from '../util/uris'
47
47
  import {
48
48
  FeedGenAggs,
49
49
  FeedGens,
@@ -279,10 +279,11 @@ export class Hydrator {
279
279
  // - profile basic
280
280
  async hydrateLists(uris: string[], ctx: HydrateCtx): Promise<HydrationState> {
281
281
  const [listsState, profilesState] = await Promise.all([
282
- await this.hydrateListsBasic(uris, ctx),
283
- await this.hydrateProfilesBasic(uris.map(didFromUri), ctx),
282
+ this.hydrateListsBasic(uris, ctx, {
283
+ skipAuthors: true, // handled via author profile hydration
284
+ }),
285
+ this.hydrateProfilesBasic(uris.map(didFromUri), ctx),
284
286
  ])
285
-
286
287
  return mergeStates(listsState, profilesState)
287
288
  }
288
289
 
@@ -291,19 +292,26 @@ export class Hydrator {
291
292
  async hydrateListsBasic(
292
293
  uris: string[],
293
294
  ctx: HydrateCtx,
295
+ opts?: { skipAuthors: boolean },
294
296
  ): Promise<HydrationState> {
295
- const [lists, listAggs, listViewers, labels] = await Promise.all([
297
+ const includeAuthorDids = opts?.skipAuthors ? [] : uris.map(uriToDid)
298
+ const [lists, listAggs, listViewers, labels, actors] = await Promise.all([
296
299
  this.graph.getLists(uris, ctx.includeTakedowns),
297
300
  this.graph.getListAggregates(uris.map((uri) => ({ uri }))),
298
301
  ctx.viewer ? this.graph.getListViewerStates(uris, ctx.viewer) : undefined,
299
- this.label.getLabelsForSubjects(uris, ctx.labelers),
302
+ this.label.getLabelsForSubjects(
303
+ [...uris, ...includeAuthorDids],
304
+ ctx.labelers,
305
+ ),
306
+ this.actor.getActors(includeAuthorDids, ctx.includeTakedowns),
300
307
  ])
301
308
 
302
309
  if (!ctx.includeTakedowns) {
303
310
  actionTakedownLabels(uris, lists, labels)
311
+ actionTakedownLabels(includeAuthorDids, actors, labels)
304
312
  }
305
313
 
306
- return { lists, listAggs, listViewers, labels, ctx }
314
+ return { lists, listAggs, listViewers, labels, actors, ctx }
307
315
  }
308
316
 
309
317
  // app.bsky.graph.defs#listItemView
@@ -533,12 +541,14 @@ export class Hydrator {
533
541
  }
534
542
  }
535
543
  // replace embed/parent/root pairs with block state
536
- const blocks = await this.graph.getBidirectionalBlocks(relationships)
544
+ const blocks = await this.hydrateBidirectionalBlocks(
545
+ pairsToMap(relationships),
546
+ )
537
547
  for (const [uri, { embed, parent, root }] of postBlocksPairs) {
538
548
  postBlocks.set(uri, {
539
- embed: !!embed && blocks.isBlocked(...embed),
540
- parent: !!parent && blocks.isBlocked(...parent),
541
- root: !!root && blocks.isBlocked(...root),
549
+ embed: !!embed && !!isBlocked(blocks, embed),
550
+ parent: !!parent && !!isBlocked(blocks, parent),
551
+ root: !!root && !!isBlocked(blocks, root),
542
552
  })
543
553
  }
544
554
  return postBlocks
@@ -756,8 +766,8 @@ export class Hydrator {
756
766
  )
757
767
  },
758
768
  )
759
- const blocks = await this.graph.getBidirectionalBlocks(
760
- listCreatorMemberPairs,
769
+ const blocks = await this.hydrateBidirectionalBlocks(
770
+ pairsToMap(listCreatorMemberPairs),
761
771
  )
762
772
  // sample top list items per starter pack based on their follows
763
773
  const listMemberAggs = await this.actor.getProfileAggregates(listMemberDids)
@@ -772,7 +782,8 @@ export class Hydrator {
772
782
  // update aggregation with list items for top 12 most followed members
773
783
  agg.listItemSampleUris = [
774
784
  ...members.listitems.filter(
775
- (li) => ctx.viewer === creator || !blocks?.isBlocked(creator, li.did),
785
+ (li) =>
786
+ ctx.viewer === creator || !isBlocked(blocks, [creator, li.did]),
776
787
  ),
777
788
  ]
778
789
  .sort((li1, li2) => {
@@ -814,11 +825,11 @@ export class Hydrator {
814
825
  pairs.push([authorDid, didFromUri(uri)])
815
826
  }
816
827
  }
817
- const blocks = await this.graph.getBidirectionalBlocks(pairs)
828
+ const blocks = await this.hydrateBidirectionalBlocks(pairsToMap(pairs))
818
829
  const likeBlocks = new HydrationMap<LikeBlock>()
819
830
  for (const [uri, like] of likes) {
820
831
  if (like) {
821
- likeBlocks.set(uri, blocks.isBlocked(authorDid, didFromUri(uri)))
832
+ likeBlocks.set(uri, isBlocked(blocks, [authorDid, didFromUri(uri)]))
822
833
  } else {
823
834
  likeBlocks.set(uri, null)
824
835
  }
@@ -898,13 +909,13 @@ export class Hydrator {
898
909
  pairs.push([didFromUri(uri), follow.record.subject])
899
910
  }
900
911
  }
901
- const blocks = await this.graph.getBidirectionalBlocks(pairs)
912
+ const blocks = await this.hydrateBidirectionalBlocks(pairsToMap(pairs))
902
913
  const followBlocks = new HydrationMap<FollowBlock>()
903
914
  for (const [uri, follow] of follows) {
904
915
  if (follow) {
905
916
  followBlocks.set(
906
917
  uri,
907
- blocks.isBlocked(didFromUri(uri), follow.record.subject),
918
+ isBlocked(blocks, [didFromUri(uri), follow.record.subject]),
908
919
  )
909
920
  } else {
910
921
  followBlocks.set(uri, null)
@@ -926,10 +937,32 @@ export class Hydrator {
926
937
  const result = new HydrationMap<HydrationMap<boolean>>()
927
938
  const blocks = await this.graph.getBidirectionalBlocks(pairs)
928
939
 
940
+ // lookup list authors to apply takedown status to blocklists
941
+ const listAuthorDids = new Set<string>()
942
+ for (const [source, targets] of didMap) {
943
+ for (const target of targets) {
944
+ const block = blocks.get(source, target)
945
+ if (block?.blockListUri) {
946
+ listAuthorDids.add(uriToDid(block.blockListUri))
947
+ }
948
+ }
949
+ }
950
+
951
+ const activeListAuthors = await this.actor.getActors(
952
+ [...listAuthorDids],
953
+ false,
954
+ )
955
+
929
956
  for (const [source, targets] of didMap) {
930
957
  const didBlocks = new HydrationMap<boolean>()
931
958
  for (const target of targets) {
932
- didBlocks.set(target, blocks.isBlocked(source, target))
959
+ const block = blocks.get(source, target)
960
+ const isBlocked = !!(
961
+ block?.blockUri ||
962
+ (block?.blockListUri &&
963
+ activeListAuthors.get(uriToDid(block.blockListUri)))
964
+ )
965
+ didBlocks.set(target, isBlocked)
933
966
  }
934
967
  result.set(source, didBlocks)
935
968
  }
@@ -1198,6 +1231,20 @@ const getListUrisFromThreadgates = (gates: Threadgates) => {
1198
1231
  return uris
1199
1232
  }
1200
1233
 
1234
+ const isBlocked = (blocks: BidirectionalBlocks, [a, b]: RelationshipPair) => {
1235
+ return blocks.get(a)?.get(b) ?? null
1236
+ }
1237
+
1238
+ const pairsToMap = (pairs: RelationshipPair[]): Map<string, string[]> => {
1239
+ const map = new Map<string, string[]>()
1240
+ for (const [a, b] of pairs) {
1241
+ const list = map.get(a) ?? []
1242
+ list.push(b)
1243
+ map.set(a, list)
1244
+ }
1245
+ return map
1246
+ }
1247
+
1201
1248
  export const mergeStates = (
1202
1249
  stateA: HydrationState,
1203
1250
  stateB: HydrationState,
@@ -7,7 +7,7 @@ import {
7
7
  ProfileViewDetailed,
8
8
  ProfileView,
9
9
  ProfileViewBasic,
10
- ViewerState as ProfileViewerState,
10
+ ViewerState as ProfileViewer,
11
11
  } from '../lexicon/types/app/bsky/actor/defs'
12
12
  import {
13
13
  BlockedPost,
@@ -35,7 +35,11 @@ import {
35
35
  VideoUriBuilder,
36
36
  parsePostgate,
37
37
  } from './util'
38
- import { uriToDid as creatorFromUri, safePinnedPost } from '../util/uris'
38
+ import {
39
+ uriToDid as creatorFromUri,
40
+ safePinnedPost,
41
+ uriToDid,
42
+ } from '../util/uris'
39
43
  import { isListRule } from '../lexicon/types/app/bsky/feed/threadgate'
40
44
  import { isSelfLabels } from '../lexicon/types/com/atproto/label/defs'
41
45
  import {
@@ -73,6 +77,7 @@ import {
73
77
  } from '../lexicon/types/app/bsky/labeler/defs'
74
78
  import { Notification } from '../proto/bsky_pb'
75
79
  import { postUriToThreadgateUri, postUriToPostgateUri } from '../util/uris'
80
+ import { ProfileViewerState } from '../hydration/actor'
76
81
 
77
82
  export class Views {
78
83
  public imgUriBuilder: ImageUriBuilder = this.opts.imgUriBuilder
@@ -101,9 +106,10 @@ export class Views {
101
106
  }
102
107
 
103
108
  actorIsTakendown(did: string, state: HydrationState): boolean {
104
- if (state.actors?.get(did)?.takedownRef) return true
105
- if (state.actors?.get(did)?.upstreamStatus === 'takendown') return true
106
- if (state.actors?.get(did)?.upstreamStatus === 'suspended') return true
109
+ const actor = state.actors?.get(did)
110
+ if (actor?.takedownRef) return true
111
+ if (actor?.upstreamStatus === 'takendown') return true
112
+ if (actor?.upstreamStatus === 'suspended') return true
107
113
  if (state.labels?.get(did)?.isTakendown) return true
108
114
  return false
109
115
  }
@@ -111,18 +117,45 @@ export class Views {
111
117
  viewerBlockExists(did: string, state: HydrationState): boolean {
112
118
  const actor = state.profileViewers?.get(did)
113
119
  if (!actor) return false
114
- return (
115
- !!actor.blockedBy ||
116
- !!actor.blocking ||
117
- !!actor.blockedByList ||
118
- !!actor.blockingByList
120
+ return !!(
121
+ actor.blockedBy ||
122
+ actor.blocking ||
123
+ this.blockedByList(actor, state) ||
124
+ this.blockingByList(actor, state)
119
125
  )
120
126
  }
121
127
 
122
128
  viewerMuteExists(did: string, state: HydrationState): boolean {
123
129
  const actor = state.profileViewers?.get(did)
124
130
  if (!actor) return false
125
- return actor.muted || !!actor.mutedByList
131
+ return !!(actor.muted || this.mutedByList(actor, state))
132
+ }
133
+
134
+ blockingByList(viewer: ProfileViewerState, state: HydrationState) {
135
+ return (
136
+ viewer.blockingByList && this.recordActive(viewer.blockingByList, state)
137
+ )
138
+ }
139
+
140
+ blockedByList(viewer: ProfileViewerState, state: HydrationState) {
141
+ return (
142
+ viewer.blockedByList && this.recordActive(viewer.blockedByList, state)
143
+ )
144
+ }
145
+
146
+ mutedByList(viewer: ProfileViewerState, state: HydrationState) {
147
+ return viewer.mutedByList && this.recordActive(viewer.mutedByList, state)
148
+ }
149
+
150
+ recordActive(uri: string, state: HydrationState) {
151
+ const did = uriToDid(uri)
152
+ const actor = state.actors?.get(did)
153
+ if (!actor || this.actorIsTakendown(did, state)) {
154
+ // actor may not be present when takedowns are eagerly applied during hydration.
155
+ // so it's important to _try_ to hydrate the actor for records checked this way.
156
+ return
157
+ }
158
+ return uri
126
159
  }
127
160
 
128
161
  viewerSeesNeedsReview(did: string, state: HydrationState): boolean {
@@ -276,24 +309,22 @@ export class Views {
276
309
  }
277
310
  }
278
311
 
279
- profileViewer(
280
- did: string,
281
- state: HydrationState,
282
- ): ProfileViewerState | undefined {
312
+ profileViewer(did: string, state: HydrationState): ProfileViewer | undefined {
283
313
  const viewer = state.profileViewers?.get(did)
284
314
  if (!viewer) return
285
- const blockedByUri = viewer.blockedBy || viewer.blockedByList
286
- const blockingUri = viewer.blocking || viewer.blockingByList
315
+ const blockedByList = this.blockedByList(viewer, state)
316
+ const blockedByUri = viewer.blockedBy || blockedByList
317
+ const blockingByList = this.blockingByList(viewer, state)
318
+ const blockingUri = viewer.blocking || blockingByList
287
319
  const block = !!blockedByUri || !!blockingUri
320
+ const mutedByList = this.mutedByList(viewer, state)
288
321
  return {
289
- muted: viewer.muted || !!viewer.mutedByList,
290
- mutedByList: viewer.mutedByList
291
- ? this.listBasic(viewer.mutedByList, state)
292
- : undefined,
322
+ muted: !!(viewer.muted || mutedByList),
323
+ mutedByList: mutedByList ? this.listBasic(mutedByList, state) : undefined,
293
324
  blockedBy: !!blockedByUri,
294
325
  blocking: blockingUri,
295
- blockingByList: viewer.blockingByList
296
- ? this.listBasic(viewer.blockingByList, state)
326
+ blockingByList: blockingByList
327
+ ? this.listBasic(blockingByList, state)
297
328
  : undefined,
298
329
  following: viewer.following && !block ? viewer.following : undefined,
299
330
  followedBy: viewer.followedBy && !block ? viewer.followedBy : undefined,
@@ -323,11 +354,11 @@ export class Views {
323
354
  blockedProfileViewer(
324
355
  did: string,
325
356
  state: HydrationState,
326
- ): ProfileViewerState | undefined {
357
+ ): ProfileViewer | undefined {
327
358
  const viewer = state.profileViewers?.get(did)
328
359
  if (!viewer) return
329
- const blockedByUri = viewer.blockedBy || viewer.blockedByList
330
- const blockingUri = viewer.blocking || viewer.blockingByList
360
+ const blockedByUri = viewer.blockedBy || this.blockedByList(viewer, state)
361
+ const blockingUri = viewer.blocking || this.blockingByList(viewer, state)
331
362
  return {
332
363
  blockedBy: !!blockedByUri,
333
364
  blocking: blockingUri,
@@ -73,8 +73,11 @@ describe('label hydration', () => {
73
73
  expect(res.data.labels?.find((l) => l.src === labelerDid)?.val).toEqual(
74
74
  'misleading',
75
75
  )
76
- const labelerHeaderDids = res.headers['atproto-content-labelers'].split(',')
77
- expect(labelerHeaderDids.sort()).toEqual(
76
+ const labelerHeaderDids = res.headers['atproto-content-labelers']
77
+ ?.split(',')
78
+ .sort()
79
+
80
+ expect(labelerHeaderDids).toEqual(
78
81
  [alice, `${bob};redact`, labelerDid].sort(),
79
82
  )
80
83
  })
@@ -487,7 +487,7 @@ describe('pds author feed views', () => {
487
487
  await sc.post(alice, 'not pinned post')
488
488
  const post = await createAndPinPost()
489
489
  await sc.post(alice, 'not pinned post')
490
-
490
+ await network.processAll()
491
491
  const { data } = await agent.api.app.bsky.feed.getAuthorFeed(
492
492
  { actor: sc.accounts[alice].handle, includePins: true },
493
493
  {
@@ -37,6 +37,16 @@ describe('bsky takedown labels', () => {
37
37
  sc.getHeaders(sc.dids.carol),
38
38
  )
39
39
  carolListRef = await sc.createList(sc.dids.carol, 'carol list', 'mod')
40
+ // alice blocks dan via carol's list, and carol is takendown
41
+ await sc.addToList(sc.dids.carol, sc.dids.dan, carolListRef)
42
+ await pdsAgent.app.bsky.graph.listblock.create(
43
+ { repo: sc.dids.alice },
44
+ {
45
+ subject: carolListRef.uriStr,
46
+ createdAt: new Date().toISOString(),
47
+ },
48
+ sc.getHeaders(sc.dids.alice),
49
+ )
40
50
  aliceGenRef = await sc.createFeedGen(
41
51
  sc.dids.alice,
42
52
  'did:web:example.com',
@@ -190,6 +200,25 @@ describe('bsky takedown labels', () => {
190
200
  expect(profile.viewer?.blockingByList).toBeUndefined()
191
201
  })
192
202
 
203
+ it('author takedown halts application of mod lists', async () => {
204
+ const { data: profile } = await agent.app.bsky.actor.getProfile(
205
+ {
206
+ actor: sc.dids.dan, // blocked via carol's list, and carol is takendown
207
+ },
208
+ {
209
+ headers: await network.serviceHeaders(
210
+ sc.dids.alice,
211
+ ids.AppBskyActorGetProfile,
212
+ ),
213
+ },
214
+ )
215
+ expect(profile.did).toBe(sc.dids.dan)
216
+ expect(profile.viewer).not.toBeUndefined()
217
+ expect(profile.viewer?.blockedBy).toBe(false)
218
+ expect(profile.viewer?.blocking).toBeUndefined()
219
+ expect(profile.viewer?.blockingByList).toBeUndefined()
220
+ })
221
+
193
222
  it('takesdown feed generators', async () => {
194
223
  const res = await agent.api.app.bsky.feed.getFeedGenerators({
195
224
  feeds: [aliceGenRef.uriStr, bobGenRef.uriStr, carolGenRef.uriStr],