@atproto/api 0.12.4 → 0.12.6

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.
@@ -1,10 +1,17 @@
1
1
  import { TestNetworkNoAppView } from '@atproto/dev-env'
2
+ import { TID } from '@atproto/common-web'
2
3
  import {
3
4
  BskyAgent,
4
5
  ComAtprotoRepoPutRecord,
5
6
  AppBskyActorProfile,
6
7
  DEFAULT_LABEL_SETTINGS,
7
- } from '..'
8
+ } from '../src'
9
+ import {
10
+ savedFeedsToUriArrays,
11
+ getSavedFeedType,
12
+ validateSavedFeed,
13
+ } from '../src/util'
14
+ import { AppBskyActorDefs } from '../dist'
8
15
 
9
16
  describe('agent', () => {
10
17
  let network: TestNetworkNoAppView
@@ -237,6 +244,14 @@ describe('agent', () => {
237
244
 
238
245
  await expect(agent.getPreferences()).resolves.toStrictEqual({
239
246
  feeds: { pinned: undefined, saved: undefined },
247
+ savedFeeds: [
248
+ {
249
+ id: expect.any(String),
250
+ pinned: true,
251
+ type: 'timeline',
252
+ value: 'following',
253
+ },
254
+ ],
240
255
  moderationPrefs: {
241
256
  adultContentEnabled: false,
242
257
  labels: DEFAULT_LABEL_SETTINGS,
@@ -266,6 +281,14 @@ describe('agent', () => {
266
281
  await agent.setAdultContentEnabled(true)
267
282
  await expect(agent.getPreferences()).resolves.toStrictEqual({
268
283
  feeds: { pinned: undefined, saved: undefined },
284
+ savedFeeds: [
285
+ {
286
+ id: expect.any(String),
287
+ pinned: true,
288
+ type: 'timeline',
289
+ value: 'following',
290
+ },
291
+ ],
269
292
  moderationPrefs: {
270
293
  adultContentEnabled: true,
271
294
  labels: DEFAULT_LABEL_SETTINGS,
@@ -295,6 +318,14 @@ describe('agent', () => {
295
318
  await agent.setAdultContentEnabled(false)
296
319
  await expect(agent.getPreferences()).resolves.toStrictEqual({
297
320
  feeds: { pinned: undefined, saved: undefined },
321
+ savedFeeds: [
322
+ {
323
+ id: expect.any(String),
324
+ pinned: true,
325
+ type: 'timeline',
326
+ value: 'following',
327
+ },
328
+ ],
298
329
  moderationPrefs: {
299
330
  adultContentEnabled: false,
300
331
  labels: DEFAULT_LABEL_SETTINGS,
@@ -324,6 +355,14 @@ describe('agent', () => {
324
355
  await agent.setContentLabelPref('misinfo', 'hide')
325
356
  await expect(agent.getPreferences()).resolves.toStrictEqual({
326
357
  feeds: { pinned: undefined, saved: undefined },
358
+ savedFeeds: [
359
+ {
360
+ id: expect.any(String),
361
+ pinned: true,
362
+ type: 'timeline',
363
+ value: 'following',
364
+ },
365
+ ],
327
366
  moderationPrefs: {
328
367
  adultContentEnabled: false,
329
368
  labels: { ...DEFAULT_LABEL_SETTINGS, misinfo: 'hide' },
@@ -353,6 +392,14 @@ describe('agent', () => {
353
392
  await agent.setContentLabelPref('spam', 'ignore')
354
393
  await expect(agent.getPreferences()).resolves.toStrictEqual({
355
394
  feeds: { pinned: undefined, saved: undefined },
395
+ savedFeeds: [
396
+ {
397
+ id: expect.any(String),
398
+ pinned: true,
399
+ type: 'timeline',
400
+ value: 'following',
401
+ },
402
+ ],
356
403
  moderationPrefs: {
357
404
  adultContentEnabled: false,
358
405
  labels: {
@@ -385,6 +432,14 @@ describe('agent', () => {
385
432
 
386
433
  await agent.addSavedFeed('at://bob.com/app.bsky.feed.generator/fake')
387
434
  await expect(agent.getPreferences()).resolves.toStrictEqual({
435
+ savedFeeds: [
436
+ {
437
+ id: expect.any(String),
438
+ pinned: true,
439
+ type: 'timeline',
440
+ value: 'following',
441
+ },
442
+ ],
388
443
  feeds: {
389
444
  pinned: [],
390
445
  saved: ['at://bob.com/app.bsky.feed.generator/fake'],
@@ -421,6 +476,14 @@ describe('agent', () => {
421
476
 
422
477
  await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
423
478
  await expect(agent.getPreferences()).resolves.toStrictEqual({
479
+ savedFeeds: [
480
+ {
481
+ id: expect.any(String),
482
+ pinned: true,
483
+ type: 'timeline',
484
+ value: 'following',
485
+ },
486
+ ],
424
487
  feeds: {
425
488
  pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
426
489
  saved: ['at://bob.com/app.bsky.feed.generator/fake'],
@@ -457,6 +520,14 @@ describe('agent', () => {
457
520
 
458
521
  await agent.removePinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
459
522
  await expect(agent.getPreferences()).resolves.toStrictEqual({
523
+ savedFeeds: [
524
+ {
525
+ id: expect.any(String),
526
+ pinned: true,
527
+ type: 'timeline',
528
+ value: 'following',
529
+ },
530
+ ],
460
531
  feeds: {
461
532
  pinned: [],
462
533
  saved: ['at://bob.com/app.bsky.feed.generator/fake'],
@@ -493,6 +564,14 @@ describe('agent', () => {
493
564
 
494
565
  await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake')
495
566
  await expect(agent.getPreferences()).resolves.toStrictEqual({
567
+ savedFeeds: [
568
+ {
569
+ id: expect.any(String),
570
+ pinned: true,
571
+ type: 'timeline',
572
+ value: 'following',
573
+ },
574
+ ],
496
575
  feeds: {
497
576
  pinned: [],
498
577
  saved: [],
@@ -529,6 +608,14 @@ describe('agent', () => {
529
608
 
530
609
  await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake')
531
610
  await expect(agent.getPreferences()).resolves.toStrictEqual({
611
+ savedFeeds: [
612
+ {
613
+ id: expect.any(String),
614
+ pinned: true,
615
+ type: 'timeline',
616
+ value: 'following',
617
+ },
618
+ ],
532
619
  feeds: {
533
620
  pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
534
621
  saved: ['at://bob.com/app.bsky.feed.generator/fake'],
@@ -565,6 +652,14 @@ describe('agent', () => {
565
652
 
566
653
  await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake2')
567
654
  await expect(agent.getPreferences()).resolves.toStrictEqual({
655
+ savedFeeds: [
656
+ {
657
+ id: expect.any(String),
658
+ pinned: true,
659
+ type: 'timeline',
660
+ value: 'following',
661
+ },
662
+ ],
568
663
  feeds: {
569
664
  pinned: [
570
665
  'at://bob.com/app.bsky.feed.generator/fake',
@@ -607,6 +702,14 @@ describe('agent', () => {
607
702
 
608
703
  await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake')
609
704
  await expect(agent.getPreferences()).resolves.toStrictEqual({
705
+ savedFeeds: [
706
+ {
707
+ id: expect.any(String),
708
+ pinned: true,
709
+ type: 'timeline',
710
+ value: 'following',
711
+ },
712
+ ],
610
713
  feeds: {
611
714
  pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
612
715
  saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
@@ -643,6 +746,14 @@ describe('agent', () => {
643
746
 
644
747
  await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' })
645
748
  await expect(agent.getPreferences()).resolves.toStrictEqual({
749
+ savedFeeds: [
750
+ {
751
+ id: expect.any(String),
752
+ pinned: true,
753
+ type: 'timeline',
754
+ value: 'following',
755
+ },
756
+ ],
646
757
  feeds: {
647
758
  pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
648
759
  saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
@@ -679,6 +790,14 @@ describe('agent', () => {
679
790
 
680
791
  await agent.setFeedViewPrefs('home', { hideReplies: true })
681
792
  await expect(agent.getPreferences()).resolves.toStrictEqual({
793
+ savedFeeds: [
794
+ {
795
+ id: expect.any(String),
796
+ pinned: true,
797
+ type: 'timeline',
798
+ value: 'following',
799
+ },
800
+ ],
682
801
  feeds: {
683
802
  pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
684
803
  saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
@@ -715,6 +834,14 @@ describe('agent', () => {
715
834
 
716
835
  await agent.setFeedViewPrefs('home', { hideReplies: false })
717
836
  await expect(agent.getPreferences()).resolves.toStrictEqual({
837
+ savedFeeds: [
838
+ {
839
+ id: expect.any(String),
840
+ pinned: true,
841
+ type: 'timeline',
842
+ value: 'following',
843
+ },
844
+ ],
718
845
  feeds: {
719
846
  pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
720
847
  saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
@@ -751,6 +878,14 @@ describe('agent', () => {
751
878
 
752
879
  await agent.setFeedViewPrefs('other', { hideReplies: true })
753
880
  await expect(agent.getPreferences()).resolves.toStrictEqual({
881
+ savedFeeds: [
882
+ {
883
+ id: expect.any(String),
884
+ pinned: true,
885
+ type: 'timeline',
886
+ value: 'following',
887
+ },
888
+ ],
754
889
  feeds: {
755
890
  pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
756
891
  saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
@@ -794,6 +929,14 @@ describe('agent', () => {
794
929
 
795
930
  await agent.setThreadViewPrefs({ sort: 'random' })
796
931
  await expect(agent.getPreferences()).resolves.toStrictEqual({
932
+ savedFeeds: [
933
+ {
934
+ id: expect.any(String),
935
+ pinned: true,
936
+ type: 'timeline',
937
+ value: 'following',
938
+ },
939
+ ],
797
940
  feeds: {
798
941
  pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
799
942
  saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
@@ -837,6 +980,14 @@ describe('agent', () => {
837
980
 
838
981
  await agent.setThreadViewPrefs({ sort: 'oldest' })
839
982
  await expect(agent.getPreferences()).resolves.toStrictEqual({
983
+ savedFeeds: [
984
+ {
985
+ id: expect.any(String),
986
+ pinned: true,
987
+ type: 'timeline',
988
+ value: 'following',
989
+ },
990
+ ],
840
991
  feeds: {
841
992
  pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
842
993
  saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
@@ -880,6 +1031,14 @@ describe('agent', () => {
880
1031
 
881
1032
  await agent.setInterestsPref({ tags: ['foo', 'bar'] })
882
1033
  await expect(agent.getPreferences()).resolves.toStrictEqual({
1034
+ savedFeeds: [
1035
+ {
1036
+ id: expect.any(String),
1037
+ pinned: true,
1038
+ type: 'timeline',
1039
+ value: 'following',
1040
+ },
1041
+ ],
883
1042
  feeds: {
884
1043
  pinned: ['at://bob.com/app.bsky.feed.generator/fake2'],
885
1044
  saved: ['at://bob.com/app.bsky.feed.generator/fake2'],
@@ -1039,6 +1198,14 @@ describe('agent', () => {
1039
1198
  ],
1040
1199
  })
1041
1200
  await expect(agent.getPreferences()).resolves.toStrictEqual({
1201
+ savedFeeds: [
1202
+ {
1203
+ id: expect.any(String),
1204
+ type: 'timeline',
1205
+ value: 'following',
1206
+ pinned: true,
1207
+ },
1208
+ ],
1042
1209
  feeds: {
1043
1210
  pinned: [],
1044
1211
  saved: [],
@@ -1084,6 +1251,14 @@ describe('agent', () => {
1084
1251
 
1085
1252
  await agent.setAdultContentEnabled(false)
1086
1253
  await expect(agent.getPreferences()).resolves.toStrictEqual({
1254
+ savedFeeds: [
1255
+ {
1256
+ id: expect.any(String),
1257
+ type: 'timeline',
1258
+ value: 'following',
1259
+ pinned: true,
1260
+ },
1261
+ ],
1087
1262
  feeds: {
1088
1263
  pinned: [],
1089
1264
  saved: [],
@@ -1129,6 +1304,14 @@ describe('agent', () => {
1129
1304
 
1130
1305
  await agent.setContentLabelPref('porn', 'ignore')
1131
1306
  await expect(agent.getPreferences()).resolves.toStrictEqual({
1307
+ savedFeeds: [
1308
+ {
1309
+ id: expect.any(String),
1310
+ type: 'timeline',
1311
+ value: 'following',
1312
+ pinned: true,
1313
+ },
1314
+ ],
1132
1315
  feeds: {
1133
1316
  pinned: [],
1134
1317
  saved: [],
@@ -1175,6 +1358,14 @@ describe('agent', () => {
1175
1358
 
1176
1359
  await agent.removeLabeler('did:plc:other')
1177
1360
  await expect(agent.getPreferences()).resolves.toStrictEqual({
1361
+ savedFeeds: [
1362
+ {
1363
+ id: expect.any(String),
1364
+ type: 'timeline',
1365
+ value: 'following',
1366
+ pinned: true,
1367
+ },
1368
+ ],
1178
1369
  feeds: {
1179
1370
  pinned: [],
1180
1371
  saved: [],
@@ -1221,6 +1412,14 @@ describe('agent', () => {
1221
1412
  pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
1222
1413
  saved: ['at://bob.com/app.bsky.feed.generator/fake'],
1223
1414
  },
1415
+ savedFeeds: [
1416
+ {
1417
+ id: expect.any(String),
1418
+ pinned: true,
1419
+ type: 'timeline',
1420
+ value: 'following',
1421
+ },
1422
+ ],
1224
1423
  moderationPrefs: {
1225
1424
  adultContentEnabled: false,
1226
1425
  labels: {
@@ -1263,6 +1462,14 @@ describe('agent', () => {
1263
1462
  pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
1264
1463
  saved: ['at://bob.com/app.bsky.feed.generator/fake'],
1265
1464
  },
1465
+ savedFeeds: [
1466
+ {
1467
+ id: expect.any(String),
1468
+ pinned: true,
1469
+ type: 'timeline',
1470
+ value: 'following',
1471
+ },
1472
+ ],
1266
1473
  moderationPrefs: {
1267
1474
  adultContentEnabled: false,
1268
1475
  labels: {
@@ -1316,6 +1523,14 @@ describe('agent', () => {
1316
1523
  pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
1317
1524
  saved: ['at://bob.com/app.bsky.feed.generator/fake'],
1318
1525
  },
1526
+ savedFeeds: [
1527
+ {
1528
+ id: expect.any(String),
1529
+ pinned: true,
1530
+ type: 'timeline',
1531
+ value: 'following',
1532
+ },
1533
+ ],
1319
1534
  moderationPrefs: {
1320
1535
  adultContentEnabled: false,
1321
1536
  labels: {
@@ -1353,7 +1568,7 @@ describe('agent', () => {
1353
1568
  })
1354
1569
 
1355
1570
  const res = await agent.app.bsky.actor.getPreferences()
1356
- await expect(res.data.preferences.sort(byType)).toStrictEqual(
1571
+ expect(res.data.preferences.sort(byType)).toStrictEqual(
1357
1572
  [
1358
1573
  {
1359
1574
  $type: 'app.bsky.actor.defs#adultContentPref',
@@ -1382,6 +1597,17 @@ describe('agent', () => {
1382
1597
  pinned: ['at://bob.com/app.bsky.feed.generator/fake'],
1383
1598
  saved: ['at://bob.com/app.bsky.feed.generator/fake'],
1384
1599
  },
1600
+ {
1601
+ $type: 'app.bsky.actor.defs#savedFeedsPrefV2',
1602
+ items: [
1603
+ {
1604
+ id: expect.any(String),
1605
+ pinned: true,
1606
+ type: 'timeline',
1607
+ value: 'following',
1608
+ },
1609
+ ],
1610
+ },
1385
1611
  {
1386
1612
  $type: 'app.bsky.actor.defs#personalDetailsPref',
1387
1613
  birthDate: '2023-09-11T18:05:42.556Z',
@@ -1674,6 +1900,799 @@ describe('agent', () => {
1674
1900
  })
1675
1901
  })
1676
1902
 
1903
+ describe(`saved feeds v2`, () => {
1904
+ let agent: BskyAgent
1905
+ let i = 0
1906
+ const feedUri = () => `at://bob.com/app.bsky.feed.generator/${i++}`
1907
+ const listUri = () => `at://bob.com/app.bsky.graph.list/${i++}`
1908
+
1909
+ beforeAll(async () => {
1910
+ agent = new BskyAgent({ service: network.pds.url })
1911
+ await agent.createAccount({
1912
+ handle: 'user9.test',
1913
+ email: 'user9@test.com',
1914
+ password: 'password',
1915
+ })
1916
+ })
1917
+
1918
+ beforeEach(async () => {
1919
+ await agent.app.bsky.actor.putPreferences({
1920
+ preferences: [],
1921
+ })
1922
+ })
1923
+
1924
+ describe(`addSavedFeeds`, () => {
1925
+ it('works', async () => {
1926
+ const feed = {
1927
+ type: 'feed',
1928
+ value: feedUri(),
1929
+ pinned: false,
1930
+ }
1931
+ await agent.addSavedFeeds([feed])
1932
+ const prefs = await agent.getPreferences()
1933
+ expect(prefs.savedFeeds).toStrictEqual([
1934
+ {
1935
+ ...feed,
1936
+ id: expect.any(String),
1937
+ },
1938
+ ])
1939
+ })
1940
+
1941
+ it('throws if feed is specified and list provided', async () => {
1942
+ const list = listUri()
1943
+ await expect(() =>
1944
+ agent.addSavedFeeds([
1945
+ {
1946
+ type: 'feed',
1947
+ value: list,
1948
+ pinned: true,
1949
+ },
1950
+ ]),
1951
+ ).rejects.toThrow()
1952
+ })
1953
+
1954
+ it('throws if list is specified and feed provided', async () => {
1955
+ const feed = feedUri()
1956
+ await expect(() =>
1957
+ agent.addSavedFeeds([
1958
+ {
1959
+ type: 'list',
1960
+ value: feed,
1961
+ pinned: true,
1962
+ },
1963
+ ]),
1964
+ ).rejects.toThrow()
1965
+ })
1966
+
1967
+ it(`timeline`, async () => {
1968
+ const feeds = await agent.addSavedFeeds([
1969
+ {
1970
+ type: 'timeline',
1971
+ value: 'following',
1972
+ pinned: true,
1973
+ },
1974
+ ])
1975
+ const prefs = await agent.getPreferences()
1976
+ expect(
1977
+ prefs.savedFeeds.filter((f) => f.type === 'timeline'),
1978
+ ).toStrictEqual(feeds)
1979
+ })
1980
+
1981
+ it(`allows duplicates`, async () => {
1982
+ const feed = {
1983
+ type: 'feed',
1984
+ value: feedUri(),
1985
+ pinned: false,
1986
+ }
1987
+ await agent.addSavedFeeds([feed])
1988
+ await agent.addSavedFeeds([feed])
1989
+ const prefs = await agent.getPreferences()
1990
+ expect(prefs.savedFeeds).toStrictEqual([
1991
+ {
1992
+ ...feed,
1993
+ id: expect.any(String),
1994
+ },
1995
+ {
1996
+ ...feed,
1997
+ id: expect.any(String),
1998
+ },
1999
+ ])
2000
+ })
2001
+
2002
+ it(`adds multiple`, async () => {
2003
+ const a = {
2004
+ type: 'feed',
2005
+ value: feedUri(),
2006
+ pinned: true,
2007
+ }
2008
+ const b = {
2009
+ type: 'feed',
2010
+ value: feedUri(),
2011
+ pinned: false,
2012
+ }
2013
+ await agent.addSavedFeeds([a, b])
2014
+ const prefs = await agent.getPreferences()
2015
+ expect(prefs.savedFeeds).toStrictEqual([
2016
+ {
2017
+ ...a,
2018
+ id: expect.any(String),
2019
+ },
2020
+ {
2021
+ ...b,
2022
+ id: expect.any(String),
2023
+ },
2024
+ ])
2025
+ })
2026
+
2027
+ it(`appends multiple`, async () => {
2028
+ const a = {
2029
+ type: 'feed',
2030
+ value: feedUri(),
2031
+ pinned: true,
2032
+ }
2033
+ const b = {
2034
+ type: 'feed',
2035
+ value: feedUri(),
2036
+ pinned: false,
2037
+ }
2038
+ const c = {
2039
+ type: 'feed',
2040
+ value: feedUri(),
2041
+ pinned: true,
2042
+ }
2043
+ const d = {
2044
+ type: 'feed',
2045
+ value: feedUri(),
2046
+ pinned: false,
2047
+ }
2048
+ await agent.addSavedFeeds([a, b])
2049
+ await agent.addSavedFeeds([c, d])
2050
+ const prefs = await agent.getPreferences()
2051
+ expect(prefs.savedFeeds).toStrictEqual([
2052
+ {
2053
+ ...a,
2054
+ id: expect.any(String),
2055
+ },
2056
+ {
2057
+ ...c,
2058
+ id: expect.any(String),
2059
+ },
2060
+ {
2061
+ ...b,
2062
+ id: expect.any(String),
2063
+ },
2064
+ {
2065
+ ...d,
2066
+ id: expect.any(String),
2067
+ },
2068
+ ])
2069
+ })
2070
+ })
2071
+
2072
+ describe(`removeSavedFeeds`, () => {
2073
+ it('works', async () => {
2074
+ const feed = {
2075
+ type: 'feed',
2076
+ value: feedUri(),
2077
+ pinned: true,
2078
+ }
2079
+ const savedFeeds = await agent.addSavedFeeds([feed])
2080
+ await agent.removeSavedFeeds([savedFeeds[0].id])
2081
+ const prefs = await agent.getPreferences()
2082
+ expect(prefs.savedFeeds).toStrictEqual([])
2083
+ })
2084
+ })
2085
+
2086
+ describe(`overwriteSavedFeeds`, () => {
2087
+ it(`dedupes by id, takes last, preserves order based on last found`, async () => {
2088
+ const a = {
2089
+ id: TID.nextStr(),
2090
+ type: 'feed',
2091
+ value: feedUri(),
2092
+ pinned: true,
2093
+ }
2094
+ const b = {
2095
+ id: TID.nextStr(),
2096
+ type: 'feed',
2097
+ value: feedUri(),
2098
+ pinned: true,
2099
+ }
2100
+ await agent.overwriteSavedFeeds([a, b, a])
2101
+ const prefs = await agent.getPreferences()
2102
+ expect(prefs.savedFeeds).toStrictEqual([b, a])
2103
+ })
2104
+
2105
+ it(`preserves order`, async () => {
2106
+ const a = feedUri()
2107
+ const b = feedUri()
2108
+ const c = feedUri()
2109
+ const d = feedUri()
2110
+
2111
+ await agent.overwriteSavedFeeds([
2112
+ {
2113
+ id: TID.nextStr(),
2114
+ type: 'timeline',
2115
+ value: a,
2116
+ pinned: true,
2117
+ },
2118
+ {
2119
+ id: TID.nextStr(),
2120
+ type: 'feed',
2121
+ value: b,
2122
+ pinned: false,
2123
+ },
2124
+ {
2125
+ id: TID.nextStr(),
2126
+ type: 'feed',
2127
+ value: c,
2128
+ pinned: true,
2129
+ },
2130
+ {
2131
+ id: TID.nextStr(),
2132
+ type: 'feed',
2133
+ value: d,
2134
+ pinned: false,
2135
+ },
2136
+ ])
2137
+
2138
+ const { savedFeeds } = await agent.getPreferences()
2139
+ expect(savedFeeds.filter((f) => f.pinned)).toStrictEqual([
2140
+ {
2141
+ id: expect.any(String),
2142
+ type: 'timeline',
2143
+ value: a,
2144
+ pinned: true,
2145
+ },
2146
+ {
2147
+ id: expect.any(String),
2148
+ type: 'feed',
2149
+ value: c,
2150
+ pinned: true,
2151
+ },
2152
+ ])
2153
+ expect(savedFeeds.filter((f) => !f.pinned)).toEqual([
2154
+ {
2155
+ id: expect.any(String),
2156
+ type: 'feed',
2157
+ value: b,
2158
+ pinned: false,
2159
+ },
2160
+ {
2161
+ id: expect.any(String),
2162
+ type: 'feed',
2163
+ value: d,
2164
+ pinned: false,
2165
+ },
2166
+ ])
2167
+ })
2168
+ })
2169
+
2170
+ describe(`updateSavedFeeds`, () => {
2171
+ it(`updates affect order, saved last, new pins last`, async () => {
2172
+ const a = {
2173
+ id: TID.nextStr(),
2174
+ type: 'feed',
2175
+ value: feedUri(),
2176
+ pinned: true,
2177
+ }
2178
+ const b = {
2179
+ id: TID.nextStr(),
2180
+ type: 'feed',
2181
+ value: feedUri(),
2182
+ pinned: true,
2183
+ }
2184
+ const c = {
2185
+ id: TID.nextStr(),
2186
+ type: 'feed',
2187
+ value: feedUri(),
2188
+ pinned: true,
2189
+ }
2190
+
2191
+ await agent.overwriteSavedFeeds([a, b, c])
2192
+ await agent.updateSavedFeeds([
2193
+ {
2194
+ ...b,
2195
+ pinned: false,
2196
+ },
2197
+ ])
2198
+
2199
+ const prefs1 = await agent.getPreferences()
2200
+ expect(prefs1.savedFeeds).toStrictEqual([
2201
+ a,
2202
+ c,
2203
+ {
2204
+ ...b,
2205
+ pinned: false,
2206
+ },
2207
+ ])
2208
+
2209
+ await agent.updateSavedFeeds([
2210
+ {
2211
+ ...b,
2212
+ pinned: true,
2213
+ },
2214
+ ])
2215
+
2216
+ const prefs2 = await agent.getPreferences()
2217
+ expect(prefs2.savedFeeds).toStrictEqual([a, c, b])
2218
+ })
2219
+
2220
+ it(`cannot override original id`, async () => {
2221
+ const a = {
2222
+ id: TID.nextStr(),
2223
+ type: 'feed',
2224
+ value: feedUri(),
2225
+ pinned: true,
2226
+ }
2227
+ await agent.overwriteSavedFeeds([a])
2228
+ await agent.updateSavedFeeds([
2229
+ {
2230
+ ...a,
2231
+ pinned: false,
2232
+ id: TID.nextStr(),
2233
+ },
2234
+ ])
2235
+ const prefs = await agent.getPreferences()
2236
+ expect(prefs.savedFeeds).toStrictEqual([a])
2237
+ })
2238
+
2239
+ it(`updates multiple`, async () => {
2240
+ const a = {
2241
+ id: TID.nextStr(),
2242
+ type: 'feed',
2243
+ value: feedUri(),
2244
+ pinned: false,
2245
+ }
2246
+ const b = {
2247
+ id: TID.nextStr(),
2248
+ type: 'feed',
2249
+ value: feedUri(),
2250
+ pinned: false,
2251
+ }
2252
+ const c = {
2253
+ id: TID.nextStr(),
2254
+ type: 'feed',
2255
+ value: feedUri(),
2256
+ pinned: false,
2257
+ }
2258
+
2259
+ await agent.overwriteSavedFeeds([a, b, c])
2260
+ await agent.updateSavedFeeds([
2261
+ {
2262
+ ...b,
2263
+ pinned: true,
2264
+ },
2265
+ {
2266
+ ...c,
2267
+ pinned: true,
2268
+ },
2269
+ ])
2270
+
2271
+ const prefs1 = await agent.getPreferences()
2272
+ expect(prefs1.savedFeeds).toStrictEqual([
2273
+ {
2274
+ ...b,
2275
+ pinned: true,
2276
+ },
2277
+ {
2278
+ ...c,
2279
+ pinned: true,
2280
+ },
2281
+ a,
2282
+ ])
2283
+ })
2284
+ })
2285
+
2286
+ describe(`utils`, () => {
2287
+ describe(`savedFeedsToUriArrays`, () => {
2288
+ const { saved, pinned } = savedFeedsToUriArrays([
2289
+ {
2290
+ id: '',
2291
+ type: 'feed',
2292
+ value: 'a',
2293
+ pinned: true,
2294
+ },
2295
+ {
2296
+ id: '',
2297
+ type: 'feed',
2298
+ value: 'b',
2299
+ pinned: false,
2300
+ },
2301
+ {
2302
+ id: '',
2303
+ type: 'feed',
2304
+ value: 'c',
2305
+ pinned: true,
2306
+ },
2307
+ ])
2308
+ expect(saved).toStrictEqual(['a', 'b', 'c'])
2309
+ expect(pinned).toStrictEqual(['a', 'c'])
2310
+ })
2311
+
2312
+ describe(`getSavedFeedType`, () => {
2313
+ it(`works`, () => {
2314
+ expect(getSavedFeedType('foo')).toBe('unknown')
2315
+ expect(getSavedFeedType(feedUri())).toBe('feed')
2316
+ expect(getSavedFeedType(listUri())).toBe('list')
2317
+ expect(
2318
+ getSavedFeedType('at://did:plc:fake/app.bsky.graph.follow/fake'),
2319
+ ).toBe('unknown')
2320
+ })
2321
+ })
2322
+
2323
+ describe(`validateSavedFeed`, () => {
2324
+ it(`throws if invalid TID`, () => {
2325
+ // really only checks length at time of writing
2326
+ expect(() =>
2327
+ validateSavedFeed({
2328
+ id: 'a',
2329
+ type: 'feed',
2330
+ value: feedUri(),
2331
+ pinned: false,
2332
+ }),
2333
+ ).toThrow()
2334
+ })
2335
+
2336
+ it(`throws if mismatched types`, () => {
2337
+ expect(() =>
2338
+ validateSavedFeed({
2339
+ id: TID.nextStr(),
2340
+ type: 'list',
2341
+ value: feedUri(),
2342
+ pinned: false,
2343
+ }),
2344
+ ).toThrow()
2345
+ expect(() =>
2346
+ validateSavedFeed({
2347
+ id: TID.nextStr(),
2348
+ type: 'feed',
2349
+ value: listUri(),
2350
+ pinned: false,
2351
+ }),
2352
+ ).toThrow()
2353
+ })
2354
+
2355
+ it(`ignores values it can't validate`, () => {
2356
+ expect(() =>
2357
+ validateSavedFeed({
2358
+ id: TID.nextStr(),
2359
+ type: 'timeline',
2360
+ value: 'following',
2361
+ pinned: false,
2362
+ }),
2363
+ ).not.toThrow()
2364
+ expect(() =>
2365
+ validateSavedFeed({
2366
+ id: TID.nextStr(),
2367
+ type: 'unknown',
2368
+ value: 'could be @nyt4!ng',
2369
+ pinned: false,
2370
+ }),
2371
+ ).not.toThrow()
2372
+ })
2373
+ })
2374
+ })
2375
+ })
2376
+
2377
+ describe(`saved feeds v2: migration scenarios`, () => {
2378
+ let agent: BskyAgent
2379
+ let i = 0
2380
+ const feedUri = () => `at://bob.com/app.bsky.feed.generator/${i++}`
2381
+
2382
+ beforeAll(async () => {
2383
+ agent = new BskyAgent({ service: network.pds.url })
2384
+ await agent.createAccount({
2385
+ handle: 'user10.test',
2386
+ email: 'user10@test.com',
2387
+ password: 'password',
2388
+ })
2389
+ })
2390
+
2391
+ beforeEach(async () => {
2392
+ await agent.app.bsky.actor.putPreferences({
2393
+ preferences: [],
2394
+ })
2395
+ })
2396
+
2397
+ it('CRUD action before migration, no timeline inserted', async () => {
2398
+ const feed = {
2399
+ type: 'feed',
2400
+ value: feedUri(),
2401
+ pinned: false,
2402
+ }
2403
+ await agent.addSavedFeeds([feed])
2404
+ const prefs = await agent.getPreferences()
2405
+ expect(prefs.savedFeeds).toStrictEqual([
2406
+ {
2407
+ ...feed,
2408
+ id: expect.any(String),
2409
+ },
2410
+ ])
2411
+ })
2412
+
2413
+ it('CRUD action AFTER migration, timeline was inserted', async () => {
2414
+ await agent.getPreferences()
2415
+ const feed = {
2416
+ type: 'feed',
2417
+ value: feedUri(),
2418
+ pinned: false,
2419
+ }
2420
+ await agent.addSavedFeeds([feed])
2421
+ const prefs = await agent.getPreferences()
2422
+ expect(prefs.savedFeeds).toStrictEqual([
2423
+ {
2424
+ id: expect.any(String),
2425
+ type: 'timeline',
2426
+ value: 'following',
2427
+ pinned: true,
2428
+ },
2429
+ {
2430
+ ...feed,
2431
+ id: expect.any(String),
2432
+ },
2433
+ ])
2434
+ })
2435
+
2436
+ // fresh account OR an old account with no v1 prefs to migrate from
2437
+ it(`brand new user, v1 remains undefined`, async () => {
2438
+ const prefs = await agent.getPreferences()
2439
+ expect(prefs.savedFeeds).toStrictEqual([
2440
+ {
2441
+ id: expect.any(String),
2442
+ type: 'timeline',
2443
+ value: 'following',
2444
+ pinned: true,
2445
+ },
2446
+ ])
2447
+ // no v1 prefs to populate from
2448
+ expect(prefs.feeds).toStrictEqual({
2449
+ saved: undefined,
2450
+ pinned: undefined,
2451
+ })
2452
+ })
2453
+
2454
+ it(`brand new user, v2 does not write to v1`, async () => {
2455
+ const a = feedUri()
2456
+ // migration happens
2457
+ await agent.getPreferences()
2458
+ await agent.addSavedFeeds([
2459
+ {
2460
+ type: 'feed',
2461
+ value: a,
2462
+ pinned: false,
2463
+ },
2464
+ ])
2465
+ const prefs = await agent.getPreferences()
2466
+ expect(prefs.savedFeeds).toStrictEqual([
2467
+ {
2468
+ id: expect.any(String),
2469
+ type: 'timeline',
2470
+ value: 'following',
2471
+ pinned: true,
2472
+ },
2473
+ {
2474
+ id: expect.any(String),
2475
+ type: 'feed',
2476
+ value: a,
2477
+ pinned: false,
2478
+ },
2479
+ ])
2480
+ // no v1 prefs to populate from
2481
+ expect(prefs.feeds).toStrictEqual({
2482
+ saved: undefined,
2483
+ pinned: undefined,
2484
+ })
2485
+ })
2486
+
2487
+ it(`existing user with v1 prefs, migrates`, async () => {
2488
+ const one = feedUri()
2489
+ const two = feedUri()
2490
+ await agent.app.bsky.actor.putPreferences({
2491
+ preferences: [
2492
+ {
2493
+ $type: 'app.bsky.actor.defs#savedFeedsPref',
2494
+ pinned: [one],
2495
+ saved: [one, two],
2496
+ },
2497
+ ],
2498
+ })
2499
+ const prefs = await agent.getPreferences()
2500
+
2501
+ // deprecated interface receives what it normally would
2502
+ expect(prefs.feeds).toStrictEqual({
2503
+ pinned: [one],
2504
+ saved: [one, two],
2505
+ })
2506
+ // new interface gets new timeline + old pinned feed
2507
+ expect(prefs.savedFeeds).toStrictEqual([
2508
+ {
2509
+ id: expect.any(String),
2510
+ type: 'timeline',
2511
+ value: 'following',
2512
+ pinned: true,
2513
+ },
2514
+ {
2515
+ id: expect.any(String),
2516
+ type: 'feed',
2517
+ value: one,
2518
+ pinned: true,
2519
+ },
2520
+ {
2521
+ id: expect.any(String),
2522
+ type: 'feed',
2523
+ value: two,
2524
+ pinned: false,
2525
+ },
2526
+ ])
2527
+ })
2528
+
2529
+ it('squashes duplicates during migration', async () => {
2530
+ const one = feedUri()
2531
+ const two = feedUri()
2532
+ await agent.app.bsky.actor.putPreferences({
2533
+ preferences: [
2534
+ {
2535
+ $type: 'app.bsky.actor.defs#savedFeedsPref',
2536
+ pinned: [one, two],
2537
+ saved: [one, two],
2538
+ },
2539
+ {
2540
+ $type: 'app.bsky.actor.defs#savedFeedsPref',
2541
+ pinned: [],
2542
+ saved: [],
2543
+ },
2544
+ ],
2545
+ })
2546
+
2547
+ // performs migration
2548
+ const prefs = await agent.getPreferences()
2549
+ expect(prefs.feeds).toStrictEqual({
2550
+ pinned: [],
2551
+ saved: [],
2552
+ })
2553
+ expect(prefs.savedFeeds).toStrictEqual([
2554
+ {
2555
+ id: expect.any(String),
2556
+ type: 'timeline',
2557
+ value: 'following',
2558
+ pinned: true,
2559
+ },
2560
+ ])
2561
+
2562
+ const res = await agent.app.bsky.actor.getPreferences()
2563
+ expect(res.data.preferences).toStrictEqual([
2564
+ {
2565
+ $type: 'app.bsky.actor.defs#savedFeedsPrefV2',
2566
+ items: [
2567
+ {
2568
+ id: expect.any(String),
2569
+ type: 'timeline',
2570
+ value: 'following',
2571
+ pinned: true,
2572
+ },
2573
+ ],
2574
+ },
2575
+ {
2576
+ $type: 'app.bsky.actor.defs#savedFeedsPref',
2577
+ pinned: [],
2578
+ saved: [],
2579
+ },
2580
+ ])
2581
+ })
2582
+
2583
+ it('v2 writes persist to v1, not the inverse', async () => {
2584
+ const a = feedUri()
2585
+ const b = feedUri()
2586
+ const c = feedUri()
2587
+ const d = feedUri()
2588
+ const e = feedUri()
2589
+
2590
+ await agent.app.bsky.actor.putPreferences({
2591
+ preferences: [
2592
+ {
2593
+ $type: 'app.bsky.actor.defs#savedFeedsPref',
2594
+ pinned: [a, b],
2595
+ saved: [a, b],
2596
+ },
2597
+ ],
2598
+ })
2599
+
2600
+ // client updates, migrates to v2
2601
+ // a and b are both pinned
2602
+ await agent.getPreferences()
2603
+
2604
+ // new write to v2, c is saved
2605
+ await agent.addSavedFeeds([
2606
+ {
2607
+ type: 'feed',
2608
+ value: c,
2609
+ pinned: false,
2610
+ },
2611
+ ])
2612
+
2613
+ // v2 write wrote to v1 also
2614
+ const res1 = await agent.app.bsky.actor.getPreferences()
2615
+ const v1Pref = res1.data.preferences.find((p) =>
2616
+ AppBskyActorDefs.isSavedFeedsPref(p),
2617
+ )
2618
+ expect(v1Pref).toStrictEqual({
2619
+ $type: 'app.bsky.actor.defs#savedFeedsPref',
2620
+ pinned: [a, b],
2621
+ saved: [a, b, c],
2622
+ })
2623
+
2624
+ // v1 write occurs, d is added but not to v2
2625
+ await agent.addSavedFeed(d)
2626
+
2627
+ const res3 = await agent.app.bsky.actor.getPreferences()
2628
+ const v1Pref3 = res3.data.preferences.find((p) =>
2629
+ AppBskyActorDefs.isSavedFeedsPref(p),
2630
+ )
2631
+ expect(v1Pref3).toStrictEqual({
2632
+ $type: 'app.bsky.actor.defs#savedFeedsPref',
2633
+ pinned: [a, b],
2634
+ saved: [a, b, c, d],
2635
+ })
2636
+
2637
+ // another new write to v2, pins e
2638
+ await agent.addSavedFeeds([
2639
+ {
2640
+ type: 'feed',
2641
+ value: e,
2642
+ pinned: true,
2643
+ },
2644
+ ])
2645
+
2646
+ const res4 = await agent.app.bsky.actor.getPreferences()
2647
+ const v1Pref4 = res4.data.preferences.find((p) =>
2648
+ AppBskyActorDefs.isSavedFeedsPref(p),
2649
+ )
2650
+ // v1 pref got v2 write
2651
+ expect(v1Pref4).toStrictEqual({
2652
+ $type: 'app.bsky.actor.defs#savedFeedsPref',
2653
+ pinned: [a, b, e],
2654
+ saved: [a, b, c, d, e],
2655
+ })
2656
+
2657
+ const final = await agent.getPreferences()
2658
+ // d not here bc it was written with v1
2659
+ expect(final.savedFeeds).toStrictEqual([
2660
+ {
2661
+ id: expect.any(String),
2662
+ type: 'timeline',
2663
+ value: 'following',
2664
+ pinned: true,
2665
+ },
2666
+ { id: expect.any(String), type: 'feed', value: a, pinned: true },
2667
+ { id: expect.any(String), type: 'feed', value: b, pinned: true },
2668
+ { id: expect.any(String), type: 'feed', value: e, pinned: true },
2669
+ { id: expect.any(String), type: 'feed', value: c, pinned: false },
2670
+ ])
2671
+ })
2672
+
2673
+ it(`filters out invalid values in v1 prefs`, async () => {
2674
+ // v1 prefs must be valid AtUris, but they could be any type in theory
2675
+ await agent.app.bsky.actor.putPreferences({
2676
+ preferences: [
2677
+ {
2678
+ $type: 'app.bsky.actor.defs#savedFeedsPref',
2679
+ pinned: ['at://did:plc:fake/app.bsky.graph.follow/fake'],
2680
+ saved: ['at://did:plc:fake/app.bsky.graph.follow/fake'],
2681
+ },
2682
+ ],
2683
+ })
2684
+ const prefs = await agent.getPreferences()
2685
+ expect(prefs.savedFeeds).toStrictEqual([
2686
+ {
2687
+ id: expect.any(String),
2688
+ type: 'timeline',
2689
+ value: 'following',
2690
+ pinned: true,
2691
+ },
2692
+ ])
2693
+ })
2694
+ })
2695
+
1677
2696
  // end
1678
2697
  })
1679
2698
  })