@atproto/bsky 0.0.239 → 0.0.241

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 (110) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/api/index.d.ts.map +1 -1
  3. package/dist/api/index.js +3 -0
  4. package/dist/api/index.js.map +1 -1
  5. package/dist/api/internal/bsky/actor/getProfiles.d.ts +4 -0
  6. package/dist/api/internal/bsky/actor/getProfiles.d.ts.map +1 -0
  7. package/dist/api/internal/bsky/actor/getProfiles.js +39 -0
  8. package/dist/api/internal/bsky/actor/getProfiles.js.map +1 -0
  9. package/dist/data-plane/server/db/migrations/20260604T224952774Z-post-embed-gallery-image.d.ts +4 -0
  10. package/dist/data-plane/server/db/migrations/20260604T224952774Z-post-embed-gallery-image.d.ts.map +1 -0
  11. package/dist/data-plane/server/db/migrations/20260604T224952774Z-post-embed-gallery-image.js +17 -0
  12. package/dist/data-plane/server/db/migrations/20260604T224952774Z-post-embed-gallery-image.js.map +1 -0
  13. package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
  14. package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
  15. package/dist/data-plane/server/db/migrations/index.js +1 -0
  16. package/dist/data-plane/server/db/migrations/index.js.map +1 -1
  17. package/dist/data-plane/server/db/tables/post-embed.d.ts +8 -0
  18. package/dist/data-plane/server/db/tables/post-embed.d.ts.map +1 -1
  19. package/dist/data-plane/server/db/tables/post-embed.js +1 -0
  20. package/dist/data-plane/server/db/tables/post-embed.js.map +1 -1
  21. package/dist/data-plane/server/indexing/index.d.ts +1 -1
  22. package/dist/data-plane/server/indexing/index.d.ts.map +1 -1
  23. package/dist/data-plane/server/indexing/index.js +8 -0
  24. package/dist/data-plane/server/indexing/index.js.map +1 -1
  25. package/dist/data-plane/server/indexing/plugins/post.d.ts +2 -1
  26. package/dist/data-plane/server/indexing/plugins/post.d.ts.map +1 -1
  27. package/dist/data-plane/server/indexing/plugins/post.js +39 -1
  28. package/dist/data-plane/server/indexing/plugins/post.js.map +1 -1
  29. package/dist/data-plane/server/routes/feeds.d.ts.map +1 -1
  30. package/dist/data-plane/server/routes/feeds.js +7 -2
  31. package/dist/data-plane/server/routes/feeds.js.map +1 -1
  32. package/dist/hydration/hydrator.d.ts +3 -1
  33. package/dist/hydration/hydrator.d.ts.map +1 -1
  34. package/dist/hydration/hydrator.js +20 -18
  35. package/dist/hydration/hydrator.js.map +1 -1
  36. package/dist/lexicons/chat/bsky/convo/defs.defs.d.ts +4 -0
  37. package/dist/lexicons/chat/bsky/convo/defs.defs.d.ts.map +1 -1
  38. package/dist/lexicons/chat/bsky/convo/defs.defs.js +1 -0
  39. package/dist/lexicons/chat/bsky/convo/defs.defs.js.map +1 -1
  40. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.d.ts +3 -0
  41. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.d.ts.map +1 -0
  42. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.defs.d.ts +19 -0
  43. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.defs.d.ts.map +1 -0
  44. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.defs.js +16 -0
  45. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.defs.js.map +1 -0
  46. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.js +6 -0
  47. package/dist/lexicons/chat/bsky/convo/getUnreadCounts.js.map +1 -0
  48. package/dist/lexicons/chat/bsky/convo/unlockConvo.defs.d.ts +1 -1
  49. package/dist/lexicons/chat/bsky/convo/unlockConvo.defs.d.ts.map +1 -1
  50. package/dist/lexicons/chat/bsky/convo/unlockConvo.defs.js +1 -0
  51. package/dist/lexicons/chat/bsky/convo/unlockConvo.defs.js.map +1 -1
  52. package/dist/lexicons/chat/bsky/convo.d.ts +1 -0
  53. package/dist/lexicons/chat/bsky/convo.d.ts.map +1 -1
  54. package/dist/lexicons/chat/bsky/convo.js +1 -0
  55. package/dist/lexicons/chat/bsky/convo.js.map +1 -1
  56. package/dist/lexicons/chat/bsky/embed/joinLink.defs.d.ts +1 -1
  57. package/dist/lexicons/chat/bsky/embed/joinLink.defs.d.ts.map +1 -1
  58. package/dist/lexicons/chat/bsky/embed/joinLink.defs.js +5 -1
  59. package/dist/lexicons/chat/bsky/embed/joinLink.defs.js.map +1 -1
  60. package/dist/lexicons/chat/bsky/group/createGroup.defs.d.ts +1 -1
  61. package/dist/lexicons/chat/bsky/group/createGroup.defs.js +1 -1
  62. package/dist/lexicons/chat/bsky/group/createGroup.defs.js.map +1 -1
  63. package/dist/lexicons/chat/bsky/group/defs.defs.d.ts +18 -1
  64. package/dist/lexicons/chat/bsky/group/defs.defs.d.ts.map +1 -1
  65. package/dist/lexicons/chat/bsky/group/defs.defs.js +10 -1
  66. package/dist/lexicons/chat/bsky/group/defs.defs.js.map +1 -1
  67. package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.d.ts +3 -3
  68. package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.d.ts.map +1 -1
  69. package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.js +6 -2
  70. package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.js.map +1 -1
  71. package/dist/lexicons/chat/bsky/moderation/subscribeModEvents.defs.d.ts +1 -1
  72. package/dist/lexicons/chat/bsky/moderation/subscribeModEvents.defs.d.ts.map +1 -1
  73. package/dist/lexicons/chat/bsky/moderation/subscribeModEvents.defs.js.map +1 -1
  74. package/dist/lexicons/index.d.ts +1 -0
  75. package/dist/lexicons/index.d.ts.map +1 -1
  76. package/dist/lexicons/index.js +1 -0
  77. package/dist/lexicons/index.js.map +1 -1
  78. package/dist/lexicons/internal/bsky/actor/getProfiles.d.ts +3 -0
  79. package/dist/lexicons/internal/bsky/actor/getProfiles.d.ts.map +1 -0
  80. package/dist/lexicons/internal/bsky/actor/getProfiles.defs.d.ts +38 -0
  81. package/dist/lexicons/internal/bsky/actor/getProfiles.defs.d.ts.map +1 -0
  82. package/dist/lexicons/internal/bsky/actor/getProfiles.defs.js +26 -0
  83. package/dist/lexicons/internal/bsky/actor/getProfiles.defs.js.map +1 -0
  84. package/dist/lexicons/internal/bsky/actor/getProfiles.js +6 -0
  85. package/dist/lexicons/internal/bsky/actor/getProfiles.js.map +1 -0
  86. package/dist/lexicons/internal/bsky/actor.d.ts +2 -0
  87. package/dist/lexicons/internal/bsky/actor.d.ts.map +1 -0
  88. package/dist/lexicons/internal/bsky/actor.js +5 -0
  89. package/dist/lexicons/internal/bsky/actor.js.map +1 -0
  90. package/dist/lexicons/internal/bsky.d.ts +2 -0
  91. package/dist/lexicons/internal/bsky.d.ts.map +1 -0
  92. package/dist/lexicons/internal/bsky.js +5 -0
  93. package/dist/lexicons/internal/bsky.js.map +1 -0
  94. package/dist/lexicons/internal.d.ts +2 -0
  95. package/dist/lexicons/internal.d.ts.map +1 -0
  96. package/dist/lexicons/internal.js +5 -0
  97. package/dist/lexicons/internal.js.map +1 -0
  98. package/package.json +7 -7
  99. package/src/api/index.ts +3 -0
  100. package/src/api/internal/bsky/actor/getProfiles.ts +87 -0
  101. package/src/data-plane/server/db/migrations/20260604T224952774Z-post-embed-gallery-image.ts +19 -0
  102. package/src/data-plane/server/db/migrations/index.ts +1 -0
  103. package/src/data-plane/server/db/tables/post-embed.ts +9 -0
  104. package/src/data-plane/server/indexing/index.ts +8 -0
  105. package/src/data-plane/server/indexing/plugins/post.ts +49 -1
  106. package/src/data-plane/server/routes/feeds.ts +17 -4
  107. package/src/hydration/hydrator.ts +29 -28
  108. package/tests/views/author-feed.test.ts +47 -0
  109. package/tests/views/internal-actor.test.ts +129 -0
  110. package/tsconfig.build.tsbuildinfo +1 -1
@@ -28,6 +28,7 @@ type PostEmbedImage = DatabaseSchemaType['post_embed_image']
28
28
  type PostEmbedExternal = DatabaseSchemaType['post_embed_external']
29
29
  type PostEmbedRecord = DatabaseSchemaType['post_embed_record']
30
30
  type PostEmbedVideo = DatabaseSchemaType['post_embed_video']
31
+ type PostEmbedGalleryImage = DatabaseSchemaType['post_embed_gallery_image']
31
32
  type PostAncestor = {
32
33
  uri: string
33
34
  height: number
@@ -47,6 +48,7 @@ type IndexedPost = {
47
48
  | PostEmbedExternal
48
49
  | PostEmbedRecord
49
50
  | PostEmbedVideo
51
+ | PostEmbedGalleryImage[]
50
52
  )[]
51
53
  ancestors?: PostAncestor[]
52
54
  descendents?: PostDescendent[]
@@ -144,6 +146,7 @@ const insertFn = async (
144
146
  | PostEmbedExternal
145
147
  | PostEmbedRecord
146
148
  | PostEmbedVideo
149
+ | PostEmbedGalleryImage[]
147
150
  )[] = []
148
151
  const postEmbeds = separateEmbeds(obj.embed)
149
152
  for (const postEmbed of postEmbeds) {
@@ -238,6 +241,27 @@ const insertFn = async (
238
241
  embeds.push(videoEmbed)
239
242
 
240
243
  await db.insertInto('post_embed_video').values(videoEmbed).execute()
244
+ } else if (app.bsky.embed.gallery.$matches(postEmbed)) {
245
+ // Gallery items are a union; today only `#image` exists, but we
246
+ // defensively skip unknown variants for forward-compat.
247
+ const galleryImages: PostEmbedGalleryImage[] = []
248
+ postEmbed.items.forEach((item, i) => {
249
+ if (app.bsky.embed.gallery.image.$matches(item)) {
250
+ galleryImages.push({
251
+ postUri: uri.toString(),
252
+ position: i,
253
+ imageCid: getBlobCidString(item.image),
254
+ alt: item.alt,
255
+ })
256
+ }
257
+ })
258
+ if (galleryImages.length > 0) {
259
+ embeds.push(galleryImages)
260
+ await db
261
+ .insertInto('post_embed_gallery_image')
262
+ .values(galleryImages)
263
+ .execute()
264
+ }
241
265
  }
242
266
  }
243
267
 
@@ -381,8 +405,16 @@ const deleteFn = async (
381
405
  | PostEmbedImage[]
382
406
  | PostEmbedExternal
383
407
  | PostEmbedRecord
408
+ | PostEmbedVideo
409
+ | PostEmbedGalleryImage[]
384
410
  )[] = []
385
- const [deletedImgs, deletedExternals, deletedPosts] = await Promise.all([
411
+ const [
412
+ deletedImgs,
413
+ deletedExternals,
414
+ deletedPosts,
415
+ deletedVideo,
416
+ deletedGalleryImgs,
417
+ ] = await Promise.all([
386
418
  db
387
419
  .deleteFrom('post_embed_image')
388
420
  .where('postUri', '=', uriStr)
@@ -398,6 +430,16 @@ const deleteFn = async (
398
430
  .where('postUri', '=', uriStr)
399
431
  .returningAll()
400
432
  .executeTakeFirst(),
433
+ db
434
+ .deleteFrom('post_embed_video')
435
+ .where('postUri', '=', uriStr)
436
+ .returningAll()
437
+ .executeTakeFirst(),
438
+ db
439
+ .deleteFrom('post_embed_gallery_image')
440
+ .where('postUri', '=', uriStr)
441
+ .returningAll()
442
+ .execute(),
401
443
  ])
402
444
  if (deletedImgs.length) {
403
445
  deletedEmbeds.push(deletedImgs)
@@ -405,6 +447,12 @@ const deleteFn = async (
405
447
  if (deletedExternals) {
406
448
  deletedEmbeds.push(deletedExternals)
407
449
  }
450
+ if (deletedVideo) {
451
+ deletedEmbeds.push(deletedVideo)
452
+ }
453
+ if (deletedGalleryImgs.length) {
454
+ deletedEmbeds.push(deletedGalleryImgs)
455
+ }
408
456
  if (deletedPosts) {
409
457
  const embedUri = new AtUri(deletedPosts.embedUri)
410
458
  deletedEmbeds.push(deletedPosts)
@@ -21,11 +21,24 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
21
21
  // only your own posts
22
22
  .where('type', '=', 'post')
23
23
  // only posts with media
24
- .whereExists((qb) =>
24
+ .where((qb) =>
25
25
  qb
26
- .selectFrom('post_embed_image')
27
- .select('post_embed_image.postUri')
28
- .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'),
26
+ .whereExists((iqb) =>
27
+ iqb
28
+ .selectFrom('post_embed_image')
29
+ .select('post_embed_image.postUri')
30
+ .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'),
31
+ )
32
+ .orWhereExists((iqb) =>
33
+ iqb
34
+ .selectFrom('post_embed_gallery_image')
35
+ .select('post_embed_gallery_image.postUri')
36
+ .whereRef(
37
+ 'post_embed_gallery_image.postUri',
38
+ '=',
39
+ 'feed_item.postUri',
40
+ ),
41
+ ),
29
42
  )
30
43
  } else if (feedType === FeedType.POSTS_WITH_VIDEO) {
31
44
  builder = builder
@@ -334,29 +334,31 @@ export class Hydrator {
334
334
  async hydrateProfilesDetailed(
335
335
  dids: DidString[],
336
336
  ctx: HydrateCtx,
337
+ opts?: {
338
+ // when set, restricts known followers hydration to this subset of dids
339
+ knownFollowersDids?: DidString[]
340
+ },
337
341
  ): Promise<HydrationState> {
338
- let knownFollowers: KnownFollowersStates = new HydrationMap()
339
- try {
340
- knownFollowers = await this.actor.getKnownFollowers(dids, ctx.viewer)
341
- } catch (err) {
342
- hydrationLogger.error(
343
- { err },
344
- 'Failed to get known followers for profiles',
345
- )
346
- }
347
-
348
- let activitySubscriptions: ActivitySubscriptionStates = new HydrationMap()
349
- try {
350
- activitySubscriptions = await this.actor.getActivitySubscriptions(
351
- dids,
352
- ctx.viewer,
353
- )
354
- } catch (err) {
355
- hydrationLogger.error(
356
- { err },
357
- 'Failed to get activity subscriptions state for profiles',
358
- )
359
- }
342
+ const [knownFollowers, activitySubscriptions] = await Promise.all([
343
+ this.actor
344
+ .getKnownFollowers(opts?.knownFollowersDids ?? dids, ctx.viewer)
345
+ .catch((err): KnownFollowersStates => {
346
+ hydrationLogger.error(
347
+ { err },
348
+ 'Failed to get known followers for profiles',
349
+ )
350
+ return new HydrationMap()
351
+ }),
352
+ this.actor
353
+ .getActivitySubscriptions(dids, ctx.viewer)
354
+ .catch((err): ActivitySubscriptionStates => {
355
+ hydrationLogger.error(
356
+ { err },
357
+ 'Failed to get activity subscriptions state for profiles',
358
+ )
359
+ return new HydrationMap()
360
+ }),
361
+ ])
360
362
 
361
363
  const subjectsToKnownFollowersMap = new Map<DidString, DidString[]>()
362
364
 
@@ -1051,12 +1053,11 @@ export class Hydrator {
1051
1053
  )
1052
1054
  },
1053
1055
  )
1054
- const blocks = await this.hydrateBidirectionalBlocks(
1055
- pairsToMap(listCreatorMemberPairs),
1056
- ctx,
1057
- )
1058
- // sample top list items per starter pack based on their follows
1059
- const listMemberAggs = await this.actor.getProfileAggregates(listMemberDids)
1056
+ const [blocks, listMemberAggs] = await Promise.all([
1057
+ this.hydrateBidirectionalBlocks(pairsToMap(listCreatorMemberPairs), ctx),
1058
+ // sample top list items per starter pack based on their follows
1059
+ this.actor.getProfileAggregates(listMemberDids),
1060
+ ])
1060
1061
  const listItemUris: AtUriString[] = []
1061
1062
  uris.forEach((uri) => {
1062
1063
  const sp = starterPackState.starterPacks?.get(uri)
@@ -2,6 +2,7 @@ import assert from 'node:assert'
2
2
  import { afterAll, beforeAll, describe, expect, it } from 'vitest'
3
3
  import {
4
4
  AppBskyActorProfile,
5
+ AppBskyEmbedGallery,
5
6
  AppBskyEmbedImages,
6
7
  AppBskyEmbedRecordWithMedia,
7
8
  AppBskyEmbedVideo,
@@ -395,6 +396,52 @@ describe('pds author feed views', () => {
395
396
  expect(danFeed.feed.length).toEqual(0)
396
397
  })
397
398
 
399
+ it('includes gallery posts in posts_with_media', async () => {
400
+ const { data: blob1 } = await pdsAgent.api.com.atproto.repo.uploadBlob(
401
+ Buffer.from('gallery-image-1'),
402
+ {
403
+ headers: sc.getHeaders(sc.dids.dan),
404
+ encoding: 'image/jpeg',
405
+ },
406
+ )
407
+ const { data: blob2 } = await pdsAgent.api.com.atproto.repo.uploadBlob(
408
+ Buffer.from('gallery-image-2'),
409
+ {
410
+ headers: sc.getHeaders(sc.dids.dan),
411
+ encoding: 'image/jpeg',
412
+ },
413
+ )
414
+
415
+ await sc.post(dan, 'gallery post', undefined, undefined, undefined, {
416
+ embed: {
417
+ $type: 'app.bsky.embed.gallery',
418
+ items: [
419
+ {
420
+ $type: 'app.bsky.embed.gallery#image',
421
+ image: blob1.blob,
422
+ alt: 'first',
423
+ aspectRatio: { height: 1, width: 1 },
424
+ },
425
+ {
426
+ $type: 'app.bsky.embed.gallery#image',
427
+ image: blob2.blob,
428
+ alt: 'second',
429
+ aspectRatio: { height: 1, width: 1 },
430
+ },
431
+ ],
432
+ },
433
+ })
434
+ await network.processAll()
435
+
436
+ const { data: danFeed } = await agent.api.app.bsky.feed.getAuthorFeed({
437
+ actor: dan,
438
+ filter: 'posts_with_media',
439
+ })
440
+
441
+ expect(danFeed.feed.length).toEqual(1)
442
+ assert(AppBskyEmbedGallery.isView(danFeed.feed[0].post.embed))
443
+ })
444
+
398
445
  it('filters by posts_no_replies', async () => {
399
446
  const { data: carolFeed } = await agent.api.app.bsky.feed.getAuthorFeed({
400
447
  actor: carol,
@@ -0,0 +1,129 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
2
+ import { AppBskyActorDefs, AtpAgent } from '@atproto/api'
3
+ import { SeedClient, TestNetwork } from '@atproto/dev-env'
4
+ import { knownFollowersSeed } from '../seed/known-followers.js'
5
+
6
+ describe('internal actor views', () => {
7
+ let network: TestNetwork
8
+ let pdsAgent: AtpAgent
9
+ let seedClient: SeedClient
10
+
11
+ let dids: Record<string, string>
12
+
13
+ beforeAll(async () => {
14
+ network = await TestNetwork.create({
15
+ dbPostgresSchema: 'bsky_internal_actor',
16
+ })
17
+ pdsAgent = network.pds.getAgent()
18
+ seedClient = network.getSeedClient()
19
+
20
+ await knownFollowersSeed(seedClient)
21
+
22
+ dids = seedClient.dids
23
+
24
+ /*
25
+ * Mix of blocks and non, mirroring the known-followers test setup so the
26
+ * social proof results exercise block filtering too.
27
+ */
28
+ await pdsAgent.api.app.bsky.graph.block.create(
29
+ { repo: dids.mix_view },
30
+ { createdAt: new Date().toISOString(), subject: dids.mix_fp_block_res },
31
+ seedClient.getHeaders(dids.mix_view),
32
+ )
33
+ await pdsAgent.api.app.bsky.graph.block.create(
34
+ { repo: dids.mix_sub_1 },
35
+ { createdAt: new Date().toISOString(), subject: dids.mix_sp_block_res },
36
+ seedClient.getHeaders(dids.mix_sub_1),
37
+ )
38
+
39
+ await network.processAll()
40
+ })
41
+
42
+ afterAll(async () => {
43
+ await network.close()
44
+ })
45
+
46
+ describe('getProfiles', () => {
47
+ const getProfiles = async (
48
+ params: { dids: string[]; socialProof?: string[]; viewer?: string },
49
+ headers: Record<string, string> = network.bsky.adminAuthHeaders(),
50
+ ) => {
51
+ const search = new URLSearchParams()
52
+ params.dids.forEach((did) => search.append('dids', did))
53
+ params.socialProof?.forEach((did) => search.append('socialProof', did))
54
+ if (params.viewer) search.append('viewer', params.viewer)
55
+ const res = await fetch(
56
+ `${network.bsky.url}/xrpc/internal.bsky.actor.getProfiles?${search.toString()}`,
57
+ { headers },
58
+ )
59
+ return {
60
+ status: res.status,
61
+ body: (await res.json()) as {
62
+ profiles: AppBskyActorDefs.ProfileViewDetailed[]
63
+ },
64
+ }
65
+ }
66
+
67
+ it('requires role auth, rejecting standard user auth', async () => {
68
+ const userHeaders = await network.serviceHeaders(
69
+ dids.mix_view,
70
+ 'internal.bsky.actor.getProfiles',
71
+ )
72
+ const { status } = await getProfiles(
73
+ { dids: [dids.mix_sub_1] },
74
+ userHeaders,
75
+ )
76
+ expect(status).toBe(401)
77
+ })
78
+
79
+ it('returns all profiles, with social proof only for the requested subset', async () => {
80
+ const { status, body } = await getProfiles({
81
+ dids: [dids.mix_sub_1, dids.mix_sub_2, dids.mix_sub_3],
82
+ socialProof: [dids.mix_sub_1],
83
+ viewer: dids.mix_view,
84
+ })
85
+ expect(status).toBe(200)
86
+ expect(body.profiles).toHaveLength(3)
87
+
88
+ const [sub_1, sub_2, sub_3] = body.profiles
89
+ expect(sub_1.viewer?.knownFollowers?.count).toBe(3)
90
+ expect(sub_1.viewer?.knownFollowers?.followers).toHaveLength(1)
91
+ expect(sub_2.viewer?.knownFollowers).toBeUndefined()
92
+ expect(sub_3.viewer?.knownFollowers).toBeUndefined()
93
+ })
94
+
95
+ it('ignores socialProof dids that are not in dids', async () => {
96
+ const { status, body } = await getProfiles({
97
+ dids: [dids.mix_sub_1],
98
+ socialProof: [dids.mix_sub_1, dids.mix_sub_2],
99
+ viewer: dids.mix_view,
100
+ })
101
+ expect(status).toBe(200)
102
+ expect(body.profiles).toHaveLength(1)
103
+ expect(body.profiles[0].did).toBe(dids.mix_sub_1)
104
+ expect(body.profiles[0].viewer?.knownFollowers?.count).toBe(3)
105
+ })
106
+
107
+ it('returns no social proof when socialProof is omitted', async () => {
108
+ const { status, body } = await getProfiles({
109
+ dids: [dids.mix_sub_1, dids.mix_sub_2],
110
+ viewer: dids.mix_view,
111
+ })
112
+ expect(status).toBe(200)
113
+ expect(body.profiles).toHaveLength(2)
114
+ for (const profile of body.profiles) {
115
+ expect(profile.viewer?.knownFollowers).toBeUndefined()
116
+ }
117
+ })
118
+
119
+ it('returns no viewer state when viewer is omitted', async () => {
120
+ const { status, body } = await getProfiles({
121
+ dids: [dids.mix_sub_1],
122
+ socialProof: [dids.mix_sub_1],
123
+ })
124
+ expect(status).toBe(200)
125
+ expect(body.profiles).toHaveLength(1)
126
+ expect(body.profiles[0].viewer).toBeUndefined()
127
+ })
128
+ })
129
+ })