@atproto/dev-env 0.3.140 → 0.3.142

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.
@@ -0,0 +1,920 @@
1
+ import { AppBskyFeedPost } from '@atproto/api'
2
+ import type { DatabaseSchema } from '@atproto/bsky'
3
+ import { TestNetwork } from '../network'
4
+ import { TestNetworkNoAppView } from '../network-no-appview'
5
+ import { RecordRef, SeedClient } from './client'
6
+
7
+ type User = {
8
+ id: string
9
+ did: string
10
+ email: string
11
+ handle: string
12
+ password: string
13
+ displayName: string
14
+ description: string
15
+ selfLabels: undefined
16
+ }
17
+
18
+ function createUserStub(name: string): User {
19
+ return {
20
+ id: name,
21
+ // @ts-ignore overwritten during seeding
22
+ did: undefined,
23
+ email: `${name}@test.com`,
24
+ handle: `${name}.test`,
25
+ password: `${name}-pass`,
26
+ displayName: name,
27
+ description: `hi im ${name} label_me`,
28
+ selfLabels: undefined,
29
+ }
30
+ }
31
+
32
+ async function createUsers<T extends readonly string[]>(
33
+ seedClient: SeedClient<TestNetwork | TestNetworkNoAppView>,
34
+ prefix: string,
35
+ handles: T,
36
+ ) {
37
+ const stubs = handles.reduce((acc, handle) => {
38
+ acc[handle] = createUserStub(`${prefix}-${handle}`)
39
+ return acc
40
+ }, {}) as Record<(typeof handles)[number], User>
41
+ const users = await Promise.all(
42
+ handles
43
+ .map((h) => prefix + '-' + h)
44
+ .map(async (handle) => {
45
+ const user = createUserStub(handle)
46
+ await seedClient.createAccount(handle, user)
47
+ user.did = seedClient.dids[handle]
48
+ return user
49
+ }),
50
+ )
51
+ return users.reduce((acc, user) => {
52
+ const id = user.id.split('-')[1]
53
+ acc[id].did = user.did
54
+ return acc
55
+ }, stubs)
56
+ }
57
+
58
+ type ReplyFn = (
59
+ replyAuthor: User,
60
+ overridesOrCb?: Partial<AppBskyFeedPost.Record> | ReplyCb,
61
+ maybeReplyCb?: ReplyCb,
62
+ ) => Promise<void>
63
+
64
+ type ReplyCb = (r: ReplyFn) => Promise<void>
65
+
66
+ export const TAG_BUMP_DOWN = 'down'
67
+ export const TAG_HIDE = 'hide'
68
+
69
+ const rootReplyFnBuilder = <T extends TestNetworkNoAppView>(
70
+ sc: SeedClient<T>,
71
+ root: RecordRef,
72
+ parent: RecordRef,
73
+ prevBreadcrumbs: string,
74
+ posts: Record<
75
+ string,
76
+ | Awaited<ReturnType<SeedClient['post']>>
77
+ | Awaited<ReturnType<SeedClient['reply']>>
78
+ >,
79
+ ): ReplyFn => {
80
+ let index = 0
81
+ return async (
82
+ replyAuthor: User,
83
+ overridesOrCb?: Partial<AppBskyFeedPost.Record> | ReplyCb,
84
+ maybeReplyCb?: ReplyCb,
85
+ ) => {
86
+ let overrides: Partial<AppBskyFeedPost.Record> | undefined
87
+ let replyCb: ReplyCb | undefined
88
+ if (overridesOrCb && typeof overridesOrCb === 'function') {
89
+ replyCb = overridesOrCb
90
+ } else {
91
+ overrides = overridesOrCb
92
+ replyCb = maybeReplyCb
93
+ }
94
+
95
+ const breadcrumbs = prevBreadcrumbs
96
+ ? `${prevBreadcrumbs}.${index++}`
97
+ : `${index++}`
98
+ const text = breadcrumbs
99
+ const reply = await sc.reply(
100
+ replyAuthor.did,
101
+ root,
102
+ parent,
103
+ text,
104
+ undefined,
105
+ undefined,
106
+ overrides,
107
+ )
108
+ posts[breadcrumbs] = reply
109
+ // Await for this post to be processed before replying to it.
110
+ replyCb && (await sc.network.processAll())
111
+ await replyCb?.(rootReplyFnBuilder(sc, root, reply.ref, breadcrumbs, posts))
112
+ }
113
+ }
114
+
115
+ const createThread = async <T extends TestNetworkNoAppView>(
116
+ sc: SeedClient<T>,
117
+ rootAuthor: User,
118
+ overridesOrCb?: Partial<AppBskyFeedPost.Record> | ReplyCb,
119
+ maybeReplyCb?: ReplyCb,
120
+ ) => {
121
+ let overrides: Partial<AppBskyFeedPost.Record> | undefined
122
+ let replyCb: ReplyCb | undefined
123
+ if (overridesOrCb && typeof overridesOrCb === 'function') {
124
+ replyCb = overridesOrCb
125
+ } else {
126
+ overrides = overridesOrCb
127
+ replyCb = maybeReplyCb
128
+ }
129
+
130
+ const replies: Record<string, Awaited<ReturnType<SeedClient['reply']>>> = {}
131
+ const breadcrumbs = ''
132
+ const text = 'root'
133
+ const root = await sc.post(
134
+ rootAuthor.did,
135
+ text,
136
+ undefined,
137
+ undefined,
138
+ undefined,
139
+ overrides,
140
+ )
141
+ // Await for this post to be processed before replying to it.
142
+ replyCb && (await sc.network.processAll())
143
+ await replyCb?.(
144
+ rootReplyFnBuilder(sc, root.ref, root.ref, breadcrumbs, replies),
145
+ )
146
+ return { root, replies }
147
+ }
148
+
149
+ export async function simple(sc: SeedClient<TestNetwork>, prefix = 'simple') {
150
+ const users = await createUsers(sc, prefix, [
151
+ 'op',
152
+ 'alice',
153
+ 'bob',
154
+ 'carol',
155
+ ] as const)
156
+ const { op, alice, bob, carol } = users
157
+
158
+ const { root, replies: r } = await createThread(sc, op, async (r) => {
159
+ await r(op, async (r) => {
160
+ await r(op)
161
+ })
162
+ await r(alice)
163
+ await r(bob, async (r) => {
164
+ await r(alice)
165
+ })
166
+ await r(carol)
167
+ })
168
+
169
+ return {
170
+ seedClient: sc,
171
+ users,
172
+ root,
173
+ r,
174
+ }
175
+ }
176
+
177
+ export async function long(sc: SeedClient<TestNetwork>) {
178
+ const users = await createUsers(sc, 'long', [
179
+ 'op',
180
+ 'alice',
181
+ 'bob',
182
+ 'carol',
183
+ 'dan',
184
+ ] as const)
185
+ const { op, alice, bob, carol, dan } = users
186
+
187
+ const { root, replies: r } = await createThread(sc, op, async (r) => {
188
+ await r(op, async (r) => {
189
+ await r(op, async (r) => {
190
+ await r(op, async (r) => {
191
+ await r(op, async (r) => {
192
+ await r(op)
193
+ })
194
+ })
195
+ await r(op)
196
+ })
197
+ })
198
+
199
+ await r(alice)
200
+ await r(bob)
201
+ await r(carol)
202
+
203
+ await r(op, async (r) => {
204
+ await r(op, async (r) => {
205
+ await r(alice, async (r) => {
206
+ await r(op, async (r) => {
207
+ await r(op)
208
+ })
209
+ })
210
+ })
211
+ })
212
+
213
+ await r(alice)
214
+ await r(bob)
215
+ await r(carol)
216
+ })
217
+
218
+ await sc.like(op.did, r['5'].ref)
219
+ await sc.like(bob.did, r['5'].ref)
220
+ await sc.like(carol.did, r['5'].ref)
221
+ await sc.like(dan.did, r['5'].ref)
222
+
223
+ await sc.like(op.did, r['6'].ref)
224
+ await sc.like(alice.did, r['6'].ref)
225
+ await sc.like(carol.did, r['6'].ref)
226
+
227
+ await sc.like(op.did, r['7'].ref)
228
+ await sc.like(bob.did, r['7'].ref)
229
+
230
+ return {
231
+ seedClient: sc,
232
+ users,
233
+ root,
234
+ r,
235
+ }
236
+ }
237
+
238
+ export async function deep(sc: SeedClient<TestNetwork>) {
239
+ const users = await createUsers(sc, 'deep', ['op'] as const)
240
+ const { op } = users
241
+
242
+ let counter = 0
243
+ const { root, replies: r } = await createThread(sc, op, async (r) => {
244
+ const recursiveReply = async (rFn: ReplyFn) => {
245
+ if (counter < 18) {
246
+ counter++
247
+ await rFn(op, async (r) => recursiveReply(r))
248
+ }
249
+ }
250
+ await recursiveReply(r)
251
+ })
252
+
253
+ return {
254
+ seedClient: sc,
255
+ users,
256
+ root,
257
+ r,
258
+ }
259
+ }
260
+
261
+ export async function branchingFactor(sc: SeedClient<TestNetwork>) {
262
+ const users = await createUsers(sc, 'bf', ['op', 'bob'] as const)
263
+ const { op, bob } = users
264
+
265
+ const { root, replies: r } = await createThread(sc, op, async (r) => {
266
+ await r(bob, async (r) => {
267
+ await r(bob, async (r) => {
268
+ await r(bob)
269
+ await r(bob)
270
+ await r(bob)
271
+ await r(bob)
272
+ })
273
+ await r(bob, async (r) => {
274
+ await r(bob)
275
+ await r(bob)
276
+ await r(bob)
277
+ await r(bob)
278
+ })
279
+ await r(bob, async (r) => {
280
+ await r(bob)
281
+ await r(bob)
282
+ await r(bob)
283
+ await r(bob)
284
+ })
285
+ await r(bob, async (r) => {
286
+ await r(bob)
287
+ await r(bob)
288
+ await r(bob)
289
+ await r(bob)
290
+ })
291
+ })
292
+ await r(bob, async (r) => {
293
+ await r(bob, async (r) => {
294
+ // This is the only case in this seed where a reply has 1 reply instead of 4,
295
+ // to have cases of different lengths in the same tree.
296
+ await r(bob)
297
+ })
298
+ await r(bob, async (r) => {
299
+ await r(bob)
300
+ await r(bob)
301
+ await r(bob)
302
+ await r(bob)
303
+ })
304
+ await r(bob, async (r) => {
305
+ await r(bob)
306
+ await r(bob)
307
+ await r(bob)
308
+ await r(bob)
309
+ })
310
+ await r(bob, async (r) => {
311
+ await r(bob)
312
+ await r(bob)
313
+ await r(bob)
314
+ await r(bob)
315
+ })
316
+ })
317
+ await r(bob, async (r) => {
318
+ await r(bob, async (r) => {
319
+ await r(bob)
320
+ await r(bob)
321
+ await r(bob)
322
+ await r(bob)
323
+ })
324
+ await r(bob, async (r) => {
325
+ await r(bob)
326
+ await r(bob)
327
+ await r(bob)
328
+ await r(bob)
329
+ })
330
+ await r(bob, async (r) => {
331
+ await r(bob)
332
+ await r(bob)
333
+ await r(bob)
334
+ await r(bob)
335
+ })
336
+ await r(bob, async (r) => {
337
+ await r(bob)
338
+ await r(bob)
339
+ await r(bob)
340
+ await r(bob)
341
+ })
342
+ })
343
+ await r(bob, async (r) => {
344
+ await r(bob, async (r) => {
345
+ await r(bob)
346
+ await r(bob)
347
+ await r(bob)
348
+ await r(bob)
349
+ // This is the only case in this seed where a reply has 5 replies instead of 4,
350
+ // to have cases of different lengths in the same tree.
351
+ await r(bob)
352
+ })
353
+ await r(bob, async (r) => {
354
+ await r(bob)
355
+ await r(bob)
356
+ await r(bob)
357
+ await r(bob)
358
+ })
359
+ await r(bob, async (r) => {
360
+ await r(bob)
361
+ await r(bob)
362
+ await r(bob)
363
+ await r(bob)
364
+ })
365
+ await r(bob, async (r) => {
366
+ await r(bob)
367
+ await r(bob)
368
+ await r(bob)
369
+ await r(bob)
370
+ })
371
+ })
372
+ })
373
+
374
+ return {
375
+ seedClient: sc,
376
+ users,
377
+ root,
378
+ r,
379
+ }
380
+ }
381
+
382
+ export async function annotateMoreReplies(sc: SeedClient<TestNetwork>) {
383
+ const users = await createUsers(sc, 'mr', ['op', 'alice'] as const)
384
+ const { op, alice } = users
385
+
386
+ const { root, replies: r } = await createThread(sc, op, async (r) => {
387
+ await r(alice, async (r) => {
388
+ await r(alice, async (r) => {
389
+ await r(alice, async (r) => {
390
+ await r(alice, async (r) => {
391
+ // more replies... (below = 4)
392
+ await r(alice, async (r) => {
393
+ await r(alice)
394
+ })
395
+ await r(alice)
396
+ await r(alice, async (r) => {
397
+ await r(alice, async (r) => {
398
+ await r(alice)
399
+ })
400
+ })
401
+ await r(alice)
402
+ await r(alice)
403
+ })
404
+ })
405
+ })
406
+ await r(alice, async (r) => {
407
+ await r(alice, async (r) => {
408
+ await r(alice)
409
+ })
410
+ })
411
+ })
412
+ await r(alice, async (r) => {
413
+ await r(alice, async (r) => {
414
+ await r(alice)
415
+ await r(alice)
416
+ // more replies... (branchingFactor = 2)
417
+ await r(alice)
418
+ await r(alice)
419
+ await r(alice)
420
+ })
421
+ await r(alice, async (r) => {
422
+ await r(alice)
423
+ await r(alice)
424
+ })
425
+ // more replies... (branchingFactor = 2)
426
+ await r(alice)
427
+ })
428
+ await r(alice) // anchor reply not limited by branchingFactor
429
+ })
430
+
431
+ return {
432
+ seedClient: sc,
433
+ users,
434
+ root,
435
+ r,
436
+ }
437
+ }
438
+
439
+ export async function annotateOP(sc: SeedClient<TestNetwork>) {
440
+ const users = await createUsers(sc, 'op', ['op', 'alice', 'bob'] as const)
441
+ const { op, alice, bob } = users
442
+
443
+ const { root, replies: r } = await createThread(sc, op, async (r) => {
444
+ await r(op, async (r) => {
445
+ await r(op, async (r) => {
446
+ await r(op)
447
+ })
448
+ })
449
+ await r(alice, async (r) => {
450
+ await r(alice)
451
+ })
452
+ await r(op, async (r) => {
453
+ await r(bob, async (r) => {
454
+ await r(op)
455
+ })
456
+ })
457
+ })
458
+
459
+ return {
460
+ seedClient: sc,
461
+ users,
462
+ root,
463
+ r,
464
+ }
465
+ }
466
+
467
+ export async function sort(sc: SeedClient<TestNetwork>) {
468
+ const users = await createUsers(sc, 'sort', [
469
+ 'op',
470
+ 'alice',
471
+ 'bob',
472
+ 'carol',
473
+ ] as const)
474
+ const { op, alice, bob, carol } = users
475
+
476
+ const { root, replies: r } = await createThread(sc, op, async (r) => {
477
+ // 0 likes
478
+ await r(alice, async (r) => {
479
+ await r(carol) // 0 likes
480
+ await r(alice) // 2 likes
481
+ await r(bob) // 1 like
482
+ })
483
+ // 3 likes
484
+ await r(carol, async (r) => {
485
+ await r(bob) // 1 like
486
+ await r(carol) // 2 likes
487
+ await r(alice) // 0 likes
488
+ })
489
+ // 2 likes
490
+ await r(bob, async (r) => {
491
+ await r(bob) // 2 likes
492
+ await r(alice) // 1 like
493
+ await r(carol) // 0 likes
494
+ })
495
+ })
496
+
497
+ // likes depth 1
498
+ await sc.like(alice.did, r['2'].ref)
499
+ await sc.like(carol.did, r['2'].ref)
500
+ await sc.like(op.did, r['1'].ref) // op like
501
+ await sc.like(bob.did, r['1'].ref)
502
+ await sc.like(carol.did, r['1'].ref)
503
+
504
+ // likes depth 2
505
+ await sc.like(bob.did, r['0.1'].ref)
506
+ await sc.like(carol.did, r['0.1'].ref)
507
+ await sc.like(op.did, r['0.2'].ref) // op like
508
+ await sc.like(bob.did, r['1.1'].ref)
509
+ await sc.like(carol.did, r['1.1'].ref)
510
+ await sc.like(bob.did, r['1.0'].ref)
511
+ await sc.like(bob.did, r['2.0'].ref)
512
+ await sc.like(carol.did, r['2.0'].ref)
513
+ await sc.like(bob.did, r['2.1'].ref)
514
+
515
+ return {
516
+ seedClient: sc,
517
+ users,
518
+ root,
519
+ r,
520
+ }
521
+ }
522
+
523
+ export async function bumpOpAndViewer(sc: SeedClient<TestNetwork>) {
524
+ const users = await createUsers(sc, 'bumpOV', [
525
+ 'op',
526
+ 'viewer',
527
+ 'alice',
528
+ 'bob',
529
+ 'carol',
530
+ ] as const)
531
+ const { op, viewer, alice, bob, carol } = users
532
+
533
+ const { root, replies: r } = await createThread(sc, op, async (r) => {
534
+ // 1 like
535
+ await r(alice, async (r) => {
536
+ await r(carol) // 0 likes
537
+ await r(alice) // 2 likes
538
+ await r(bob) // 1 like
539
+ await r(viewer) // 0 likes
540
+ await r(op) // 0 likes
541
+ })
542
+ // 3 likes
543
+ await r(carol, async (r) => {
544
+ await r(bob) // 1 like
545
+ await r(carol) // 2 likes
546
+ await r(op) // 0 likes
547
+ await r(viewer) // 1 like
548
+ await r(alice) // 0 likes
549
+ })
550
+ // 2 likes
551
+ await r(bob, async (r) => {
552
+ await r(viewer) // 0 likes
553
+ await r(bob) // 4 likes
554
+ await r(op) // 0 likes
555
+ await r(alice) // 1 like
556
+ await r(carol) // 1 like
557
+ })
558
+ // 0 likes
559
+ await r(op, async (r) => {
560
+ await r(viewer) // 0 likes
561
+ await r(bob) // 0 likes
562
+ await r(op) // 0 likes
563
+ await r(alice) // 0 likes
564
+ await r(carol) // 0 likes
565
+ })
566
+ // 0 likes
567
+ await r(viewer, async (r) => {
568
+ await r(bob) // 1 like
569
+ await r(carol) // 1 like
570
+ await r(op) // 0 likes
571
+ await r(viewer) // 0 likes
572
+ await r(alice) // 0 likes
573
+ })
574
+ })
575
+
576
+ // likes depth 1
577
+ await sc.like(alice.did, r['2'].ref)
578
+ await sc.like(carol.did, r['2'].ref)
579
+ await sc.like(viewer.did, r['0'].ref)
580
+ await sc.like(op.did, r['1'].ref) // op like
581
+ await sc.like(bob.did, r['1'].ref)
582
+ await sc.like(carol.did, r['1'].ref)
583
+
584
+ // likes depth 2
585
+ await sc.like(bob.did, r['0.1'].ref)
586
+ await sc.like(carol.did, r['0.1'].ref)
587
+ await sc.like(op.did, r['0.2'].ref) // op like
588
+ await sc.like(bob.did, r['1.1'].ref)
589
+ await sc.like(carol.did, r['1.1'].ref)
590
+ await sc.like(bob.did, r['1.0'].ref)
591
+ await sc.like(alice.did, r['2.1'].ref)
592
+ await sc.like(bob.did, r['2.1'].ref)
593
+ await sc.like(carol.did, r['2.1'].ref)
594
+ await sc.like(viewer.did, r['2.1'].ref)
595
+ await sc.like(bob.did, r['1.3'].ref)
596
+ await sc.like(bob.did, r['2.3'].ref)
597
+ await sc.like(viewer.did, r['2.4'].ref)
598
+ await sc.like(viewer.did, r['4.0'].ref)
599
+ await sc.like(alice.did, r['4.1'].ref)
600
+
601
+ return {
602
+ seedClient: sc,
603
+ users,
604
+ root,
605
+ r,
606
+ }
607
+ }
608
+
609
+ export async function bumpGroupSorting(sc: SeedClient<TestNetwork>) {
610
+ const users = await createUsers(sc, 'bumpGS', [
611
+ 'op',
612
+ 'viewer',
613
+ 'alice',
614
+ ] as const)
615
+ const { op, viewer, alice } = users
616
+
617
+ const { root, replies: r } = await createThread(sc, op, async (r) => {
618
+ await r(viewer)
619
+ await r(op)
620
+ await r(alice)
621
+ await r(op)
622
+ await r(viewer)
623
+ await r(op)
624
+ await r(alice)
625
+ await r(viewer)
626
+ })
627
+
628
+ return {
629
+ seedClient: sc,
630
+ users,
631
+ root,
632
+ r,
633
+ }
634
+ }
635
+
636
+ export async function bumpFollows(sc: SeedClient<TestNetwork>) {
637
+ const users = await createUsers(sc, 'bumpF', [
638
+ 'op',
639
+ 'viewerF',
640
+ 'viewerNoF',
641
+ 'alice',
642
+ 'bob',
643
+ 'carol',
644
+ ] as const)
645
+
646
+ const { op, viewerF, viewerNoF, alice, bob, carol } = users
647
+
648
+ const { root, replies: r } = await createThread(sc, op, async (r) => {
649
+ await r(alice)
650
+ await r(bob)
651
+ await r(carol)
652
+ await r(op)
653
+ await r(viewerF)
654
+ await r(viewerNoF)
655
+ })
656
+
657
+ await sc.follow(viewerF.did, alice.did)
658
+ await sc.follow(viewerF.did, bob.did)
659
+ // Does not follow carol.
660
+
661
+ return {
662
+ seedClient: sc,
663
+ users,
664
+ root,
665
+ r,
666
+ }
667
+ }
668
+
669
+ export async function blockDeletionAuth(
670
+ sc: SeedClient<TestNetwork>,
671
+ labelerDid: string,
672
+ ) {
673
+ const users = await createUsers(sc, 'bda', [
674
+ 'op',
675
+ 'opBlocked',
676
+ 'alice',
677
+ 'auth',
678
+ 'blocker',
679
+ 'blocked',
680
+ ] as const)
681
+
682
+ const { op, opBlocked, alice, auth, blocker, blocked } = users
683
+
684
+ const { root, replies: r } = await createThread(sc, op, async (r) => {
685
+ // 1p block, hidden for `blocked`.
686
+ await r(blocker, async (r) => {
687
+ await r(alice)
688
+ })
689
+
690
+ // 3p block, hidden for all.
691
+ await r(opBlocked, async (r) => {
692
+ await r(op)
693
+ await r(alice)
694
+ })
695
+
696
+ // Deleted, hidden for all.
697
+ await r(alice, async (r) => {
698
+ await r(alice)
699
+ })
700
+
701
+ // User configured to only be seend by authenticated users.
702
+ // Requires the test sets a `!no-unauthenticated` label for this user.
703
+ await r(auth, async (r) => {
704
+ // Another auth-only to show that the parent chain is preserved in the thread.
705
+ await r(auth, async (r) => {
706
+ await r(alice)
707
+ })
708
+ })
709
+ })
710
+
711
+ await sc.deletePost(alice.did, r['2'].ref.uri)
712
+ await sc.block(blocker.did, blocked.did)
713
+ await sc.block(op.did, opBlocked.did)
714
+
715
+ const db = sc.network.bsky.db.db
716
+ await createLabel(db, {
717
+ src: labelerDid,
718
+ uri: auth.did,
719
+ cid: '',
720
+ val: '!no-unauthenticated',
721
+ })
722
+
723
+ return {
724
+ seedClient: sc,
725
+ users,
726
+ root,
727
+ r,
728
+ }
729
+ }
730
+
731
+ export async function mutes(sc: SeedClient<TestNetwork>) {
732
+ const users = await createUsers(sc, 'mutes', [
733
+ 'op',
734
+ 'opMuted',
735
+ 'alice',
736
+ 'muted',
737
+ 'muter',
738
+ ] as const)
739
+
740
+ const { op, opMuted, alice, muted, muter } = users
741
+
742
+ const { root, replies: r } = await createThread(sc, op, async (r) => {
743
+ await r(opMuted, async (r) => {
744
+ await r(alice)
745
+ await r(muted)
746
+ })
747
+
748
+ await r(muted, async (r) => {
749
+ await r(opMuted)
750
+ await r(alice)
751
+ })
752
+ })
753
+
754
+ await sc.mute(op.did, opMuted.did)
755
+ await sc.mute(muter.did, muted.did)
756
+
757
+ return {
758
+ seedClient: sc,
759
+ users,
760
+ root,
761
+ r,
762
+ }
763
+ }
764
+
765
+ export async function threadgated(sc: SeedClient<TestNetwork>) {
766
+ const users = await createUsers(sc, 'tg', [
767
+ 'op',
768
+ 'opMuted',
769
+ 'viewer',
770
+ 'alice',
771
+ 'bob',
772
+ ] as const)
773
+
774
+ const { op, opMuted, alice, bob } = users
775
+
776
+ const { root, replies: r } = await createThread(sc, op, async (r) => {
777
+ // Muted moves down below threadgated.
778
+ await r(opMuted)
779
+
780
+ // Threadgated moves down.
781
+ await r(alice, async (r) => {
782
+ await r(alice)
783
+ await r(bob)
784
+ await r(op) // OP moves up.
785
+ })
786
+
787
+ await r(bob, async (r) => {
788
+ await r(alice)
789
+ await r(bob) // Threadgated is omitted if fetched from the root.
790
+ await r(op) // OP moves down.
791
+ })
792
+ })
793
+
794
+ await sc.agent.app.bsky.feed.threadgate.create(
795
+ {
796
+ repo: op.did,
797
+ rkey: root.ref.uri.rkey,
798
+ },
799
+ {
800
+ post: root.ref.uriStr,
801
+ createdAt: new Date().toISOString(),
802
+ hiddenReplies: [r['1'].ref.uriStr, r['2.1'].ref.uriStr],
803
+ },
804
+ sc.getHeaders(op.did),
805
+ )
806
+
807
+ // Just throw a mute there to test the prioritization between muted and threadgated.
808
+ await sc.mute(op.did, opMuted.did)
809
+
810
+ return {
811
+ seedClient: sc,
812
+ users,
813
+ root,
814
+ r,
815
+ }
816
+ }
817
+
818
+ export async function tags(sc: SeedClient<TestNetwork>) {
819
+ const users = await createUsers(sc, 'tags', [
820
+ 'op',
821
+ 'alice',
822
+ 'down',
823
+ 'following',
824
+ 'hide',
825
+ 'viewer',
826
+ ] as const)
827
+
828
+ const { op, alice, down, following, hide, viewer } = users
829
+
830
+ const { root, replies: r } = await createThread(sc, op, async (r) => {
831
+ await r(alice, async (r) => {
832
+ await r(alice)
833
+ await r(down)
834
+ await r(hide)
835
+ })
836
+ await r(down, async (r) => {
837
+ await r(alice)
838
+ await r(down)
839
+ await r(hide)
840
+ })
841
+ await r(hide, async (r) => {
842
+ await r(alice)
843
+ await r(down)
844
+ await r(hide)
845
+ })
846
+ await r(op)
847
+ await r(viewer)
848
+ await r(following)
849
+ })
850
+
851
+ await sc.network.processAll()
852
+
853
+ await sc.follow(viewer.did, following.did)
854
+
855
+ const db = sc.network.bsky.db.db
856
+ await createTag(db, { uri: r['1'].ref.uriStr, val: TAG_BUMP_DOWN })
857
+ await createTag(db, { uri: r['0.1'].ref.uriStr, val: TAG_BUMP_DOWN })
858
+ await createTag(db, { uri: r['1.1'].ref.uriStr, val: TAG_BUMP_DOWN })
859
+ await createTag(db, { uri: r['2.1'].ref.uriStr, val: TAG_BUMP_DOWN })
860
+
861
+ await createTag(db, { uri: r['2'].ref.uriStr, val: TAG_HIDE })
862
+ await createTag(db, { uri: r['0.2'].ref.uriStr, val: TAG_HIDE })
863
+ await createTag(db, { uri: r['1.2'].ref.uriStr, val: TAG_HIDE })
864
+ await createTag(db, { uri: r['2.2'].ref.uriStr, val: TAG_HIDE })
865
+
866
+ // Neither tag affect op, viewer.
867
+ await createTag(db, { uri: r['3'].ref.uriStr, val: TAG_BUMP_DOWN })
868
+ await createTag(db, { uri: r['4'].ref.uriStr, val: TAG_HIDE })
869
+
870
+ // Tags affect following depending on the config to prioritize following.
871
+ await createTag(db, { uri: r['5'].ref.uriStr, val: TAG_HIDE })
872
+
873
+ return {
874
+ seedClient: sc,
875
+ users,
876
+ root,
877
+ r,
878
+ }
879
+ }
880
+
881
+ const createLabel = async (
882
+ db: DatabaseSchema,
883
+ opts: {
884
+ src: string
885
+ uri: string
886
+ cid: string
887
+ val: string
888
+ exp?: string
889
+ },
890
+ ) => {
891
+ await db
892
+ .insertInto('label')
893
+ .values({
894
+ uri: opts.uri,
895
+ cid: opts.cid,
896
+ val: opts.val,
897
+ cts: new Date().toISOString(),
898
+ exp: opts.exp ?? null,
899
+ neg: false,
900
+ src: opts.src,
901
+ })
902
+ .execute()
903
+ }
904
+
905
+ const createTag = async (
906
+ db: DatabaseSchema,
907
+ opts: {
908
+ uri: string
909
+ val: string
910
+ },
911
+ ) => {
912
+ await db
913
+ .updateTable('record')
914
+ .set({
915
+ tags: JSON.stringify([opts.val]),
916
+ })
917
+ .where('uri', '=', opts.uri)
918
+ .returningAll()
919
+ .execute()
920
+ }