@atproto/bsky 0.0.65 → 0.0.67

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 (89) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/api/app/bsky/actor/getSuggestions.js +1 -1
  3. package/dist/api/app/bsky/actor/getSuggestions.js.map +1 -1
  4. package/dist/api/app/bsky/feed/getLikes.d.ts.map +1 -1
  5. package/dist/api/app/bsky/feed/getLikes.js +6 -2
  6. package/dist/api/app/bsky/feed/getLikes.js.map +1 -1
  7. package/dist/api/app/bsky/feed/getRepostedBy.d.ts.map +1 -1
  8. package/dist/api/app/bsky/feed/getRepostedBy.js +6 -2
  9. package/dist/api/app/bsky/feed/getRepostedBy.js.map +1 -1
  10. package/dist/api/app/bsky/graph/getLists.d.ts.map +1 -1
  11. package/dist/api/app/bsky/graph/getLists.js +10 -1
  12. package/dist/api/app/bsky/graph/getLists.js.map +1 -1
  13. package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.d.ts.map +1 -1
  14. package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js +37 -12
  15. package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js.map +1 -1
  16. package/dist/api/com/atproto/repo/getRecord.d.ts.map +1 -1
  17. package/dist/api/com/atproto/repo/getRecord.js +27 -19
  18. package/dist/api/com/atproto/repo/getRecord.js.map +1 -1
  19. package/dist/config.d.ts +4 -0
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +14 -0
  22. package/dist/config.js.map +1 -1
  23. package/dist/context.d.ts +3 -0
  24. package/dist/context.d.ts.map +1 -1
  25. package/dist/context.js +3 -0
  26. package/dist/context.js.map +1 -1
  27. package/dist/feature-gates.d.ts +25 -0
  28. package/dist/feature-gates.d.ts.map +1 -0
  29. package/dist/feature-gates.js +82 -0
  30. package/dist/feature-gates.js.map +1 -0
  31. package/dist/hydration/hydrator.d.ts +3 -0
  32. package/dist/hydration/hydrator.d.ts.map +1 -1
  33. package/dist/hydration/hydrator.js +67 -44
  34. package/dist/hydration/hydrator.js.map +1 -1
  35. package/dist/hydration/util.d.ts +3 -0
  36. package/dist/hydration/util.d.ts.map +1 -1
  37. package/dist/hydration/util.js +25 -1
  38. package/dist/hydration/util.js.map +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +8 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/lexicon/lexicons.d.ts +35 -0
  43. package/dist/lexicon/lexicons.d.ts.map +1 -1
  44. package/dist/lexicon/lexicons.js +39 -1
  45. package/dist/lexicon/lexicons.js.map +1 -1
  46. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +17 -1
  47. package/dist/lexicon/types/app/bsky/actor/defs.d.ts.map +1 -1
  48. package/dist/lexicon/types/app/bsky/actor/defs.js +21 -1
  49. package/dist/lexicon/types/app/bsky/actor/defs.js.map +1 -1
  50. package/dist/lexicon/types/app/bsky/embed/record.d.ts +1 -1
  51. package/dist/lexicon/types/app/bsky/embed/record.d.ts.map +1 -1
  52. package/dist/lexicon/types/app/bsky/embed/record.js.map +1 -1
  53. package/dist/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.d.ts +2 -0
  54. package/dist/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.d.ts.map +1 -1
  55. package/dist/logger.d.ts +1 -0
  56. package/dist/logger.d.ts.map +1 -1
  57. package/dist/logger.js +2 -1
  58. package/dist/logger.js.map +1 -1
  59. package/dist/views/index.d.ts +3 -3
  60. package/dist/views/index.d.ts.map +1 -1
  61. package/dist/views/index.js +41 -8
  62. package/dist/views/index.js.map +1 -1
  63. package/package.json +5 -4
  64. package/src/api/app/bsky/actor/getSuggestions.ts +1 -1
  65. package/src/api/app/bsky/feed/getLikes.ts +6 -2
  66. package/src/api/app/bsky/feed/getRepostedBy.ts +6 -2
  67. package/src/api/app/bsky/graph/getLists.ts +19 -2
  68. package/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +55 -15
  69. package/src/api/com/atproto/repo/getRecord.ts +28 -19
  70. package/src/config.ts +20 -0
  71. package/src/context.ts +6 -0
  72. package/src/feature-gates.ts +66 -0
  73. package/src/hydration/hydrator.ts +58 -16
  74. package/src/hydration/util.ts +28 -0
  75. package/src/index.ts +9 -0
  76. package/src/lexicon/lexicons.ts +43 -1
  77. package/src/lexicon/types/app/bsky/actor/defs.ts +40 -0
  78. package/src/lexicon/types/app/bsky/embed/record.ts +1 -0
  79. package/src/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.ts +2 -0
  80. package/src/logger.ts +2 -0
  81. package/src/views/index.ts +40 -12
  82. package/tests/__snapshots__/feed-generation.test.ts.snap +136 -0
  83. package/tests/feed-generation.test.ts +80 -0
  84. package/tests/hydration/util.test.ts +82 -0
  85. package/tests/seed/known-followers.ts +110 -0
  86. package/tests/views/__snapshots__/lists.test.ts.snap +42 -0
  87. package/tests/views/known-followers.test.ts +160 -0
  88. package/tests/views/lists.test.ts +62 -0
  89. package/tests/views/profile.test.ts +0 -35
@@ -3343,7 +3343,7 @@ export const schemaDict = {
3343
3343
  main: {
3344
3344
  type: 'query',
3345
3345
  description:
3346
- 'List blob CIDso for an account, since some repo revision. Does not require auth; implemented by PDS.',
3346
+ 'List blob CIDs for an account, since some repo revision. Does not require auth; implemented by PDS.',
3347
3347
  parameters: {
3348
3348
  type: 'params',
3349
3349
  required: ['did'],
@@ -4166,6 +4166,8 @@ export const schemaDict = {
4166
4166
  'lex:app.bsky.actor.defs#interestsPref',
4167
4167
  'lex:app.bsky.actor.defs#mutedWordsPref',
4168
4168
  'lex:app.bsky.actor.defs#hiddenPostsPref',
4169
+ 'lex:app.bsky.actor.defs#bskyAppStatePref',
4170
+ 'lex:app.bsky.actor.defs#labelersPref',
4169
4171
  ],
4170
4172
  },
4171
4173
  },
@@ -4407,6 +4409,39 @@ export const schemaDict = {
4407
4409
  },
4408
4410
  },
4409
4411
  },
4412
+ bskyAppStatePref: {
4413
+ description:
4414
+ "A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this.",
4415
+ type: 'object',
4416
+ properties: {
4417
+ activeProgressGuide: {
4418
+ type: 'ref',
4419
+ ref: 'lex:app.bsky.actor.defs#bskyAppProgressGuide',
4420
+ },
4421
+ queuedNudges: {
4422
+ description:
4423
+ 'An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user.',
4424
+ type: 'array',
4425
+ maxLength: 1000,
4426
+ items: {
4427
+ type: 'string',
4428
+ maxLength: 100,
4429
+ },
4430
+ },
4431
+ },
4432
+ },
4433
+ bskyAppProgressGuide: {
4434
+ description:
4435
+ 'If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress.',
4436
+ type: 'object',
4437
+ required: ['guide'],
4438
+ properties: {
4439
+ guide: {
4440
+ type: 'string',
4441
+ maxLength: 100,
4442
+ },
4443
+ },
4444
+ },
4410
4445
  },
4411
4446
  },
4412
4447
  AppBskyActorGetPreferences: {
@@ -4923,6 +4958,7 @@ export const schemaDict = {
4923
4958
  'lex:app.bsky.feed.defs#generatorView',
4924
4959
  'lex:app.bsky.graph.defs#listView',
4925
4960
  'lex:app.bsky.labeler.defs#labelerView',
4961
+ 'lex:app.bsky.graph.defs#starterPackViewBasic',
4926
4962
  ],
4927
4963
  },
4928
4964
  },
@@ -8676,6 +8712,12 @@ export const schemaDict = {
8676
8712
  cursor: {
8677
8713
  type: 'string',
8678
8714
  },
8715
+ relativeToDid: {
8716
+ type: 'string',
8717
+ format: 'did',
8718
+ description:
8719
+ 'DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer.',
8720
+ },
8679
8721
  },
8680
8722
  },
8681
8723
  output: {
@@ -184,6 +184,8 @@ export type Preferences = (
184
184
  | InterestsPref
185
185
  | MutedWordsPref
186
186
  | HiddenPostsPref
187
+ | BskyAppStatePref
188
+ | LabelersPref
187
189
  | { $type: string; [k: string]: unknown }
188
190
  )[]
189
191
 
@@ -456,3 +458,41 @@ export function isLabelerPrefItem(v: unknown): v is LabelerPrefItem {
456
458
  export function validateLabelerPrefItem(v: unknown): ValidationResult {
457
459
  return lexicons.validate('app.bsky.actor.defs#labelerPrefItem', v)
458
460
  }
461
+
462
+ /** A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this. */
463
+ export interface BskyAppStatePref {
464
+ activeProgressGuide?: BskyAppProgressGuide
465
+ /** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */
466
+ queuedNudges?: string[]
467
+ [k: string]: unknown
468
+ }
469
+
470
+ export function isBskyAppStatePref(v: unknown): v is BskyAppStatePref {
471
+ return (
472
+ isObj(v) &&
473
+ hasProp(v, '$type') &&
474
+ v.$type === 'app.bsky.actor.defs#bskyAppStatePref'
475
+ )
476
+ }
477
+
478
+ export function validateBskyAppStatePref(v: unknown): ValidationResult {
479
+ return lexicons.validate('app.bsky.actor.defs#bskyAppStatePref', v)
480
+ }
481
+
482
+ /** If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress. */
483
+ export interface BskyAppProgressGuide {
484
+ guide: string
485
+ [k: string]: unknown
486
+ }
487
+
488
+ export function isBskyAppProgressGuide(v: unknown): v is BskyAppProgressGuide {
489
+ return (
490
+ isObj(v) &&
491
+ hasProp(v, '$type') &&
492
+ v.$type === 'app.bsky.actor.defs#bskyAppProgressGuide'
493
+ )
494
+ }
495
+
496
+ export function validateBskyAppProgressGuide(v: unknown): ValidationResult {
497
+ return lexicons.validate('app.bsky.actor.defs#bskyAppProgressGuide', v)
498
+ }
@@ -41,6 +41,7 @@ export interface View {
41
41
  | AppBskyFeedDefs.GeneratorView
42
42
  | AppBskyGraphDefs.ListView
43
43
  | AppBskyLabelerDefs.LabelerView
44
+ | AppBskyGraphDefs.StarterPackViewBasic
44
45
  | { $type: string; [k: string]: unknown }
45
46
  [k: string]: unknown
46
47
  }
@@ -14,6 +14,8 @@ export interface QueryParams {
14
14
  viewer?: string
15
15
  limit: number
16
16
  cursor?: string
17
+ /** DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer. */
18
+ relativeToDid?: string
17
19
  }
18
20
 
19
21
  export type InputSchema = undefined
package/src/logger.ts CHANGED
@@ -12,6 +12,8 @@ export const labelerLogger: ReturnType<typeof subsystemLogger> =
12
12
  subsystemLogger('bsky:labeler')
13
13
  export const hydrationLogger: ReturnType<typeof subsystemLogger> =
14
14
  subsystemLogger('bsky:hydration')
15
+ export const featureGatesLogger: ReturnType<typeof subsystemLogger> =
16
+ subsystemLogger('bsky:featuregates')
15
17
  export const httpLogger: ReturnType<typeof subsystemLogger> =
16
18
  subsystemLogger('bsky')
17
19
 
@@ -2,7 +2,6 @@ import { AtUri, INVALID_HANDLE, normalizeDatetimeAlways } from '@atproto/syntax'
2
2
  import { mapDefined } from '@atproto/common'
3
3
  import { ImageUriBuilder } from '../image/uri'
4
4
  import { HydrationState } from '../hydration/hydrator'
5
- import { ProfileViewerState as HydratorProfileViewerState } from '../hydration/actor'
6
5
  import { ids } from '../lexicon/lexicons'
7
6
  import {
8
7
  ProfileViewDetailed,
@@ -113,10 +112,7 @@ export class Views {
113
112
  if (!actor) return
114
113
  const baseView = this.profile(did, state)
115
114
  if (!baseView) return
116
- const knownFollowersSkeleton = state.knownFollowers?.get(did)
117
- const knownFollowers = knownFollowersSkeleton
118
- ? this.knownFollowers(knownFollowersSkeleton, state)
119
- : undefined
115
+ const knownFollowers = this.knownFollowers(did, state)
120
116
  const profileAggs = state.profileAggs?.get(did)
121
117
  return {
122
118
  ...baseView,
@@ -213,6 +209,26 @@ export class Views {
213
209
  }
214
210
  }
215
211
 
212
+ profileKnownFollowers(
213
+ did: string,
214
+ state: HydrationState,
215
+ ): ProfileView | undefined {
216
+ const actor = state.actors?.get(did)
217
+ if (!actor) return
218
+ const baseView = this.profile(did, state)
219
+ if (!baseView) return
220
+ const knownFollowers = this.knownFollowers(did, state)
221
+ return {
222
+ ...baseView,
223
+ viewer: baseView.viewer
224
+ ? {
225
+ ...baseView.viewer,
226
+ knownFollowers,
227
+ }
228
+ : undefined,
229
+ }
230
+ }
231
+
216
232
  profileViewer(
217
233
  did: string,
218
234
  state: HydrationState,
@@ -237,15 +253,22 @@ export class Views {
237
253
  }
238
254
  }
239
255
 
240
- knownFollowers(
241
- knownFollowers: Required<HydratorProfileViewerState>['knownFollowers'],
242
- state: HydrationState,
243
- ) {
244
- const followers = mapDefined(knownFollowers.followers, (did) => {
245
- if (this.viewerBlockExists(did, state)) {
256
+ knownFollowers(did: string, state: HydrationState) {
257
+ const knownFollowers = state.knownFollowers?.get(did)
258
+ if (!knownFollowers) return
259
+ const blocks = state.bidirectionalBlocks?.get(did)
260
+ const followers = mapDefined(knownFollowers.followers, (followerDid) => {
261
+ if (this.viewerBlockExists(followerDid, state)) {
246
262
  return undefined
247
263
  }
248
- return this.profileBasic(did, state)
264
+ if (blocks?.get(followerDid) === true) {
265
+ return undefined
266
+ }
267
+ if (this.actorIsNoHosted(followerDid, state)) {
268
+ // @TODO only needed right now to work around getProfile's { includeTakedowns: true }
269
+ return undefined
270
+ }
271
+ return this.profileBasic(followerDid, state)
249
272
  })
250
273
  return { count: knownFollowers.count, followers }
251
274
  }
@@ -936,6 +959,11 @@ export class Views {
936
959
  if (!view) return this.embedNotFound(uri)
937
960
  view.$type = 'app.bsky.labeler.defs#labelerView'
938
961
  return this.recordEmbedWrapper(view, withTypeTag)
962
+ } else if (parsedUri.collection === ids.AppBskyGraphStarterpack) {
963
+ const view = this.starterPackBasic(uri, state)
964
+ if (!view) return this.embedNotFound(uri)
965
+ view.$type = 'app.bsky.graph.defs#starterPackViewBasic'
966
+ return this.recordEmbedWrapper(view, withTypeTag)
939
967
  }
940
968
  return this.embedNotFound(uri)
941
969
  }
@@ -47,6 +47,53 @@ Object {
47
47
  }
48
48
  `;
49
49
 
50
+ exports[`feed generation does not embed taken-down starter pack records in posts 1`] = `
51
+ Object {
52
+ "author": Object {
53
+ "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg",
54
+ "createdAt": "1970-01-01T00:00:00.000Z",
55
+ "did": "user(0)",
56
+ "displayName": "bobby",
57
+ "handle": "bob.test",
58
+ "labels": Array [],
59
+ "viewer": Object {
60
+ "blockedBy": false,
61
+ "muted": false,
62
+ },
63
+ },
64
+ "cid": "cids(0)",
65
+ "embed": Object {
66
+ "$type": "app.bsky.embed.record#view",
67
+ "record": Object {
68
+ "$type": "app.bsky.embed.record#viewNotFound",
69
+ "notFound": true,
70
+ "uri": "record(1)",
71
+ },
72
+ },
73
+ "indexedAt": "1970-01-01T00:00:00.000Z",
74
+ "labels": Array [],
75
+ "likeCount": 0,
76
+ "record": Object {
77
+ "$type": "app.bsky.feed.post",
78
+ "createdAt": "1970-01-01T00:00:00.000Z",
79
+ "embed": Object {
80
+ "$type": "app.bsky.embed.record",
81
+ "record": Object {
82
+ "cid": "cids(2)",
83
+ "uri": "record(1)",
84
+ },
85
+ },
86
+ "text": "annoying starter pack",
87
+ },
88
+ "replyCount": 0,
89
+ "repostCount": 0,
90
+ "uri": "record(0)",
91
+ "viewer": Object {
92
+ "threadMuted": false,
93
+ },
94
+ }
95
+ `;
96
+
50
97
  exports[`feed generation embeds feed generator records in posts 1`] = `
51
98
  Object {
52
99
  "author": Object {
@@ -134,6 +181,95 @@ Object {
134
181
  }
135
182
  `;
136
183
 
184
+ exports[`feed generation embeds starter pack records in posts 1`] = `
185
+ Object {
186
+ "author": Object {
187
+ "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg",
188
+ "createdAt": "1970-01-01T00:00:00.000Z",
189
+ "did": "user(0)",
190
+ "displayName": "bobby",
191
+ "handle": "bob.test",
192
+ "labels": Array [],
193
+ "viewer": Object {
194
+ "blockedBy": false,
195
+ "muted": false,
196
+ },
197
+ },
198
+ "cid": "cids(0)",
199
+ "embed": Object {
200
+ "$type": "app.bsky.embed.record#view",
201
+ "record": Object {
202
+ "$type": "app.bsky.graph.defs#starterPackViewBasic",
203
+ "cid": "cids(2)",
204
+ "creator": Object {
205
+ "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg",
206
+ "createdAt": "1970-01-01T00:00:00.000Z",
207
+ "did": "user(2)",
208
+ "displayName": "ali",
209
+ "handle": "alice.test",
210
+ "labels": Array [
211
+ Object {
212
+ "cid": "cids(3)",
213
+ "cts": "1970-01-01T00:00:00.000Z",
214
+ "src": "user(2)",
215
+ "uri": "record(5)",
216
+ "val": "self-label-a",
217
+ },
218
+ Object {
219
+ "cid": "cids(3)",
220
+ "cts": "1970-01-01T00:00:00.000Z",
221
+ "src": "user(2)",
222
+ "uri": "record(5)",
223
+ "val": "self-label-b",
224
+ },
225
+ ],
226
+ "viewer": Object {
227
+ "blockedBy": false,
228
+ "followedBy": "record(4)",
229
+ "following": "record(3)",
230
+ "muted": false,
231
+ },
232
+ },
233
+ "indexedAt": "1970-01-01T00:00:00.000Z",
234
+ "joinedAllTimeCount": 0,
235
+ "joinedWeekCount": 0,
236
+ "labels": Array [],
237
+ "record": Object {
238
+ "$type": "app.bsky.graph.starterpack",
239
+ "createdAt": "1970-01-01T00:00:00.000Z",
240
+ "description": "",
241
+ "descriptionFacets": Array [],
242
+ "feeds": Array [],
243
+ "list": "record(2)",
244
+ "name": "awesome starter pack!",
245
+ },
246
+ "uri": "record(1)",
247
+ },
248
+ },
249
+ "indexedAt": "1970-01-01T00:00:00.000Z",
250
+ "labels": Array [],
251
+ "likeCount": 0,
252
+ "record": Object {
253
+ "$type": "app.bsky.feed.post",
254
+ "createdAt": "1970-01-01T00:00:00.000Z",
255
+ "embed": Object {
256
+ "$type": "app.bsky.embed.record",
257
+ "record": Object {
258
+ "cid": "cids(2)",
259
+ "uri": "record(1)",
260
+ },
261
+ },
262
+ "text": "sick starter pack!",
263
+ },
264
+ "replyCount": 0,
265
+ "repostCount": 0,
266
+ "uri": "record(0)",
267
+ "viewer": Object {
268
+ "threadMuted": false,
269
+ },
270
+ }
271
+ `;
272
+
137
273
  exports[`feed generation getActorFeeds fetches feed generators by actor. 1`] = `
138
274
  Array [
139
275
  Object {
@@ -34,6 +34,7 @@ describe('feed generation', () => {
34
34
  let feedUriPrime: string // Taken-down
35
35
  let feedUriPrimeRef: RecordRef
36
36
  let feedUriNeedsAuth: string
37
+ let starterPackRef: { uri: string; cid: string }
37
38
 
38
39
  beforeAll(async () => {
39
40
  network = await TestNetwork.create({
@@ -251,6 +252,85 @@ describe('feed generation', () => {
251
252
  expect(forSnapshot(view.data.posts[0])).toMatchSnapshot()
252
253
  })
253
254
 
255
+ it('embeds starter pack records in posts', async () => {
256
+ const listRes = await pdsAgent.api.app.bsky.graph.list.create(
257
+ {
258
+ repo: sc.dids.alice,
259
+ },
260
+ {
261
+ name: 'awesome starter pack!',
262
+ description: '',
263
+ descriptionFacets: [],
264
+ avatar: undefined,
265
+ createdAt: new Date().toISOString(),
266
+ purpose: 'app.bsky.graph.defs#referencelist',
267
+ },
268
+ sc.getHeaders(sc.dids.alice),
269
+ )
270
+ const starterPackRes = await pdsAgent.api.app.bsky.graph.starterpack.create(
271
+ {
272
+ repo: sc.dids.alice,
273
+ },
274
+ {
275
+ name: 'awesome starter pack!',
276
+ description: '',
277
+ descriptionFacets: [],
278
+ feeds: [],
279
+ list: listRes.uri,
280
+ createdAt: new Date().toISOString(),
281
+ },
282
+ sc.getHeaders(sc.dids.alice),
283
+ )
284
+ starterPackRef = {
285
+ uri: starterPackRes.uri,
286
+ cid: starterPackRes.cid,
287
+ }
288
+ const res = await pdsAgent.api.app.bsky.feed.post.create(
289
+ { repo: sc.dids.bob },
290
+ {
291
+ text: 'sick starter pack!',
292
+ embed: {
293
+ $type: 'app.bsky.embed.record',
294
+ record: starterPackRef,
295
+ },
296
+ createdAt: new Date().toISOString(),
297
+ },
298
+ sc.getHeaders(sc.dids.bob),
299
+ )
300
+ await network.processAll()
301
+ const view = await agent.api.app.bsky.feed.getPosts(
302
+ { uris: [res.uri] },
303
+ { headers: await network.serviceHeaders(sc.dids.bob) },
304
+ )
305
+ expect(view.data.posts.length).toBe(1)
306
+ expect(forSnapshot(view.data.posts[0])).toMatchSnapshot()
307
+ })
308
+
309
+ it('does not embed taken-down starter pack records in posts', async () => {
310
+ await network.bsky.ctx.dataplane.takedownRecord({
311
+ recordUri: starterPackRef.uri,
312
+ })
313
+ const res = await pdsAgent.api.app.bsky.feed.post.create(
314
+ { repo: sc.dids.bob },
315
+ {
316
+ text: 'annoying starter pack',
317
+ embed: {
318
+ $type: 'app.bsky.embed.record',
319
+ record: starterPackRef,
320
+ },
321
+ createdAt: new Date().toISOString(),
322
+ },
323
+ sc.getHeaders(sc.dids.bob),
324
+ )
325
+ await network.processAll()
326
+ const view = await agent.api.app.bsky.feed.getPosts(
327
+ { uris: [res.uri] },
328
+ { headers: await network.serviceHeaders(sc.dids.bob) },
329
+ )
330
+ expect(view.data.posts.length).toBe(1)
331
+ expect(forSnapshot(view.data.posts[0])).toMatchSnapshot()
332
+ })
333
+
254
334
  describe('getFeedGenerator', () => {
255
335
  it('describes a feed gen & returns online status', async () => {
256
336
  const resEven = await agent.api.app.bsky.feed.getFeedGenerator(
@@ -0,0 +1,82 @@
1
+ import {
2
+ HydrationMap,
3
+ mergeMaps,
4
+ mergeManyMaps,
5
+ mergeNestedMaps,
6
+ } from '../../src/hydration/util'
7
+
8
+ const mapToObj = (map: HydrationMap<any>) => {
9
+ const obj: Record<string, any> = {}
10
+ for (const [key, value] of map) {
11
+ obj[key] = value
12
+ }
13
+ return obj
14
+ }
15
+
16
+ describe('hydration util', () => {
17
+ it(`mergeMaps: merges two maps`, () => {
18
+ const compare = new HydrationMap<string>()
19
+ compare.set('a', 'a')
20
+ compare.set('b', 'b')
21
+
22
+ const a = new HydrationMap<string>().set('a', 'a')
23
+ const b = new HydrationMap<string>().set('b', 'b')
24
+ const merged = mergeMaps(a, b)
25
+
26
+ expect(mapToObj(merged!)).toEqual(mapToObj(compare))
27
+ })
28
+
29
+ it(`mergeManyMaps: merges three maps`, () => {
30
+ const compare = new HydrationMap<string>()
31
+ compare.set('a', 'a')
32
+ compare.set('b', 'b')
33
+ compare.set('c', 'c')
34
+
35
+ const a = new HydrationMap<string>().set('a', 'a')
36
+ const b = new HydrationMap<string>().set('b', 'b')
37
+ const c = new HydrationMap<string>().set('c', 'c')
38
+ const merged = mergeManyMaps(a, b, c)
39
+
40
+ expect(mapToObj(merged!)).toEqual(mapToObj(compare))
41
+ })
42
+
43
+ it(`mergeNestedMaps: merges two nested maps`, () => {
44
+ const compare = new HydrationMap<HydrationMap<string>>()
45
+ const compareA = new HydrationMap<string>().set('a', 'a')
46
+ const compareB = new HydrationMap<string>().set('b', 'b')
47
+ compare.set('a', compareA)
48
+ compare.set('b', compareB)
49
+
50
+ const a = new HydrationMap<HydrationMap<string>>().set(
51
+ 'a',
52
+ new HydrationMap<string>().set('a', 'a'),
53
+ )
54
+ const b = new HydrationMap<HydrationMap<string>>().set(
55
+ 'b',
56
+ new HydrationMap<string>().set('b', 'b'),
57
+ )
58
+ const merged = mergeNestedMaps(a, b)
59
+
60
+ expect(mapToObj(merged!)).toEqual(mapToObj(compare))
61
+ })
62
+
63
+ it(`mergeNestedMaps: merges two nested maps with common keys`, () => {
64
+ const compare = new HydrationMap<HydrationMap<boolean>>()
65
+ const compareA = new HydrationMap<boolean>()
66
+ compareA.set('b', true)
67
+ compareA.set('c', true)
68
+ compare.set('a', compareA)
69
+
70
+ const a = new HydrationMap<HydrationMap<boolean>>().set(
71
+ 'a',
72
+ new HydrationMap<boolean>().set('b', true),
73
+ )
74
+ const b = new HydrationMap<HydrationMap<boolean>>().set(
75
+ 'a',
76
+ new HydrationMap<boolean>().set('c', true),
77
+ )
78
+ const merged = mergeNestedMaps(a, b)
79
+
80
+ expect(mapToObj(merged!)).toEqual(mapToObj(compare))
81
+ })
82
+ })
@@ -0,0 +1,110 @@
1
+ import { TestNetwork, SeedClient, TestNetworkNoAppView } from '@atproto/dev-env'
2
+
3
+ export type User = {
4
+ email: string
5
+ handle: string
6
+ password: string
7
+ displayName: string
8
+ description: string
9
+ selfLabels: undefined
10
+ }
11
+
12
+ function createUser(name: string): User {
13
+ return {
14
+ email: `${name}@test.com`,
15
+ handle: `${name}.test`,
16
+ password: `${name}-pass`,
17
+ displayName: name,
18
+ description: `hi im ${name} label_me`,
19
+ selfLabels: undefined,
20
+ }
21
+ }
22
+
23
+ const users = {
24
+ /*
25
+ * Base test. One known follower.
26
+ */
27
+ base_sub: createUser('base-sub'),
28
+ base_res_1: createUser('base-res-1'),
29
+ base_view: createUser('base-view'),
30
+
31
+ /*
32
+ * First-part block of a single known follower.
33
+ */
34
+ fp_block_sub: createUser('fp-block-sub'),
35
+ fp_block_res_1: createUser('fp-block-res-1'),
36
+ fp_block_view: createUser('fp-block-view'),
37
+
38
+ /*
39
+ * Second-party block of a single known follower.
40
+ */
41
+ sp_block_sub: createUser('sp-block-sub'),
42
+ sp_block_res_1: createUser('sp-block-res-1'),
43
+ sp_block_view: createUser('sp-block-view'),
44
+
45
+ /*
46
+ * Mix of known followers and blocks.
47
+ */
48
+ mix_sub_1: createUser('mix-sub-1'),
49
+ mix_sub_2: createUser('mix-sub-2'),
50
+ mix_sub_3: createUser('mix-sub-3'),
51
+ mix_res: createUser('mix-res'),
52
+ mix_fp_block_res: createUser('mix-fp-block-res'),
53
+ mix_sp_block_res: createUser('mix-sp-block-res'),
54
+ mix_view: createUser('mix-view'),
55
+ }
56
+
57
+ export async function knownFollowersSeed(
58
+ sc: SeedClient<TestNetwork | TestNetworkNoAppView>,
59
+ ) {
60
+ await sc.createAccount('base_sub', users.base_sub)
61
+ await sc.createAccount('base_res_1', users.base_res_1)
62
+ await sc.createAccount('base_view', users.base_view)
63
+
64
+ await sc.createAccount('fp_block_sub', users.fp_block_sub)
65
+ await sc.createAccount('fp_block_res_1', users.fp_block_res_1)
66
+ await sc.createAccount('fp_block_view', users.fp_block_view)
67
+
68
+ await sc.createAccount('sp_block_sub', users.sp_block_sub)
69
+ await sc.createAccount('sp_block_res_1', users.sp_block_res_1)
70
+ await sc.createAccount('sp_block_view', users.sp_block_view)
71
+
72
+ await sc.createAccount('mix_sub_1', users.mix_sub_1)
73
+ await sc.createAccount('mix_sub_2', users.mix_sub_2)
74
+ await sc.createAccount('mix_sub_3', users.mix_sub_3)
75
+ await sc.createAccount('mix_res', users.mix_res)
76
+ await sc.createAccount('mix_fp_block_res', users.mix_fp_block_res)
77
+ await sc.createAccount('mix_sp_block_res', users.mix_sp_block_res)
78
+ await sc.createAccount('mix_view', users.mix_view)
79
+
80
+ const dids = sc.dids
81
+
82
+ await sc.follow(dids.base_res_1, dids.base_sub)
83
+ await sc.follow(dids.base_view, dids.base_res_1)
84
+
85
+ await sc.follow(dids.fp_block_res_1, dids.fp_block_sub)
86
+ await sc.follow(dids.fp_block_view, dids.fp_block_res_1)
87
+
88
+ await sc.follow(dids.sp_block_res_1, dids.sp_block_sub)
89
+ await sc.follow(dids.sp_block_view, dids.sp_block_res_1)
90
+
91
+ await sc.follow(dids.mix_res, dids.mix_sub_1)
92
+ await sc.follow(dids.mix_res, dids.mix_sub_2)
93
+ await sc.follow(dids.mix_res, dids.mix_sub_3)
94
+ await sc.follow(dids.mix_fp_block_res, dids.mix_sub_1)
95
+ await sc.follow(dids.mix_fp_block_res, dids.mix_sub_2)
96
+ await sc.follow(dids.mix_fp_block_res, dids.mix_sub_3)
97
+ await sc.follow(dids.mix_sp_block_res, dids.mix_sub_1)
98
+ await sc.follow(dids.mix_sp_block_res, dids.mix_sub_2)
99
+ await sc.follow(dids.mix_sp_block_res, dids.mix_sub_3)
100
+ await sc.follow(dids.mix_view, dids.mix_res)
101
+ await sc.follow(dids.mix_view, dids.mix_fp_block_res)
102
+ await sc.follow(dids.mix_view, dids.mix_sp_block_res)
103
+
104
+ await sc.network.processAll()
105
+
106
+ return {
107
+ users,
108
+ seedClient: sc,
109
+ }
110
+ }