@atproto/bsky 0.0.166 → 0.0.168

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 (148) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/api/app/bsky/notification/listActivitySubscriptions.d.ts +4 -0
  3. package/dist/api/app/bsky/notification/listActivitySubscriptions.d.ts.map +1 -0
  4. package/dist/api/app/bsky/notification/listActivitySubscriptions.js +63 -0
  5. package/dist/api/app/bsky/notification/listActivitySubscriptions.js.map +1 -0
  6. package/dist/api/app/bsky/notification/putActivitySubscription.d.ts +4 -0
  7. package/dist/api/app/bsky/notification/putActivitySubscription.d.ts.map +1 -0
  8. package/dist/api/app/bsky/notification/putActivitySubscription.js +63 -0
  9. package/dist/api/app/bsky/notification/putActivitySubscription.js.map +1 -0
  10. package/dist/api/app/bsky/notification/putPreferencesV2.d.ts.map +1 -1
  11. package/dist/api/app/bsky/notification/putPreferencesV2.js +2 -1
  12. package/dist/api/app/bsky/notification/putPreferencesV2.js.map +1 -1
  13. package/dist/api/index.d.ts.map +1 -1
  14. package/dist/api/index.js +4 -0
  15. package/dist/api/index.js.map +1 -1
  16. package/dist/data-plane/bsync/index.d.ts.map +1 -1
  17. package/dist/data-plane/bsync/index.js +52 -38
  18. package/dist/data-plane/bsync/index.js.map +1 -1
  19. package/dist/data-plane/server/db/database-schema.d.ts +2 -1
  20. package/dist/data-plane/server/db/database-schema.d.ts.map +1 -1
  21. package/dist/data-plane/server/db/migrations/20250611T140649895Z-add-activity-subscription.d.ts +4 -0
  22. package/dist/data-plane/server/db/migrations/20250611T140649895Z-add-activity-subscription.d.ts.map +1 -0
  23. package/dist/data-plane/server/db/migrations/20250611T140649895Z-add-activity-subscription.js +24 -0
  24. package/dist/data-plane/server/db/migrations/20250611T140649895Z-add-activity-subscription.js.map +1 -0
  25. package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
  26. package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
  27. package/dist/data-plane/server/db/migrations/index.js +2 -1
  28. package/dist/data-plane/server/db/migrations/index.js.map +1 -1
  29. package/dist/data-plane/server/db/pagination.d.ts +22 -0
  30. package/dist/data-plane/server/db/pagination.d.ts.map +1 -1
  31. package/dist/data-plane/server/db/pagination.js +30 -1
  32. package/dist/data-plane/server/db/pagination.js.map +1 -1
  33. package/dist/data-plane/server/db/tables/activity-subscription.d.ts +13 -0
  34. package/dist/data-plane/server/db/tables/activity-subscription.d.ts.map +1 -0
  35. package/dist/data-plane/server/db/tables/activity-subscription.js +5 -0
  36. package/dist/data-plane/server/db/tables/activity-subscription.js.map +1 -0
  37. package/dist/data-plane/server/indexing/index.d.ts +2 -0
  38. package/dist/data-plane/server/indexing/index.d.ts.map +1 -1
  39. package/dist/data-plane/server/indexing/index.js +2 -0
  40. package/dist/data-plane/server/indexing/index.js.map +1 -1
  41. package/dist/data-plane/server/indexing/plugins/notif-declaration.d.ts +7 -0
  42. package/dist/data-plane/server/indexing/plugins/notif-declaration.d.ts.map +1 -0
  43. package/dist/data-plane/server/indexing/plugins/notif-declaration.js +72 -0
  44. package/dist/data-plane/server/indexing/plugins/notif-declaration.js.map +1 -0
  45. package/dist/data-plane/server/routes/activity-subscription.d.ts +6 -0
  46. package/dist/data-plane/server/routes/activity-subscription.d.ts.map +1 -0
  47. package/dist/data-plane/server/routes/activity-subscription.js +67 -0
  48. package/dist/data-plane/server/routes/activity-subscription.js.map +1 -0
  49. package/dist/data-plane/server/routes/index.js +2 -2
  50. package/dist/data-plane/server/routes/index.js.map +1 -1
  51. package/dist/data-plane/server/routes/notifs.d.ts +3 -0
  52. package/dist/data-plane/server/routes/notifs.d.ts.map +1 -1
  53. package/dist/data-plane/server/routes/notifs.js +64 -0
  54. package/dist/data-plane/server/routes/notifs.js.map +1 -1
  55. package/dist/data-plane/server/routes/profile.d.ts.map +1 -1
  56. package/dist/data-plane/server/routes/profile.js +20 -1
  57. package/dist/data-plane/server/routes/profile.js.map +1 -1
  58. package/dist/data-plane/server/routes/records.d.ts.map +1 -1
  59. package/dist/data-plane/server/routes/records.js +1 -0
  60. package/dist/data-plane/server/routes/records.js.map +1 -1
  61. package/dist/data-plane/server/util.d.ts +6 -6
  62. package/dist/hydration/actor.d.ts +20 -6
  63. package/dist/hydration/actor.d.ts.map +1 -1
  64. package/dist/hydration/actor.js +44 -1
  65. package/dist/hydration/actor.js.map +1 -1
  66. package/dist/hydration/hydrator.d.ts +4 -3
  67. package/dist/hydration/hydrator.d.ts.map +1 -1
  68. package/dist/hydration/hydrator.js +16 -2
  69. package/dist/hydration/hydrator.js.map +1 -1
  70. package/dist/hydration/util.d.ts +4 -0
  71. package/dist/hydration/util.d.ts.map +1 -1
  72. package/dist/hydration/util.js +3 -1
  73. package/dist/hydration/util.js.map +1 -1
  74. package/dist/lexicon/lexicons.d.ts +4 -0
  75. package/dist/lexicon/lexicons.d.ts.map +1 -1
  76. package/dist/lexicon/lexicons.js +2 -0
  77. package/dist/lexicon/lexicons.js.map +1 -1
  78. package/dist/proto/bsky_connect.d.ts +28 -1
  79. package/dist/proto/bsky_connect.d.ts.map +1 -1
  80. package/dist/proto/bsky_connect.js +27 -0
  81. package/dist/proto/bsky_connect.js.map +1 -1
  82. package/dist/proto/bsky_pb.d.ts +189 -0
  83. package/dist/proto/bsky_pb.d.ts.map +1 -1
  84. package/dist/proto/bsky_pb.js +598 -5
  85. package/dist/proto/bsky_pb.js.map +1 -1
  86. package/dist/stash.d.ts +1 -0
  87. package/dist/stash.d.ts.map +1 -1
  88. package/dist/stash.js +1 -0
  89. package/dist/stash.js.map +1 -1
  90. package/dist/views/index.d.ts +5 -3
  91. package/dist/views/index.d.ts.map +1 -1
  92. package/dist/views/index.js +29 -9
  93. package/dist/views/index.js.map +1 -1
  94. package/package.json +4 -4
  95. package/proto/bsky.proto +45 -0
  96. package/src/api/app/bsky/notification/listActivitySubscriptions.ts +110 -0
  97. package/src/api/app/bsky/notification/putActivitySubscription.ts +69 -0
  98. package/src/api/app/bsky/notification/putPreferencesV2.ts +2 -1
  99. package/src/api/index.ts +4 -0
  100. package/src/data-plane/bsync/index.ts +75 -44
  101. package/src/data-plane/server/db/database-schema.ts +3 -1
  102. package/src/data-plane/server/db/migrations/20250611T140649895Z-add-activity-subscription.ts +22 -0
  103. package/src/data-plane/server/db/migrations/index.ts +1 -0
  104. package/src/data-plane/server/db/pagination.ts +37 -0
  105. package/src/data-plane/server/db/tables/activity-subscription.ts +12 -0
  106. package/src/data-plane/server/indexing/index.ts +3 -0
  107. package/src/data-plane/server/indexing/plugins/notif-declaration.ts +59 -0
  108. package/src/data-plane/server/routes/activity-subscription.ts +83 -0
  109. package/src/data-plane/server/routes/index.ts +2 -2
  110. package/src/data-plane/server/routes/notifs.ts +95 -0
  111. package/src/data-plane/server/routes/profile.ts +33 -1
  112. package/src/data-plane/server/routes/records.ts +4 -0
  113. package/src/hydration/actor.ts +97 -10
  114. package/src/hydration/hydrator.ts +32 -6
  115. package/src/hydration/util.ts +8 -0
  116. package/src/lexicon/lexicons.ts +4 -0
  117. package/src/proto/bsky_connect.ts +33 -0
  118. package/src/proto/bsky_pb.ts +648 -0
  119. package/src/stash.ts +6 -1
  120. package/src/views/index.ts +48 -11
  121. package/tests/__snapshots__/feed-generation.test.ts.snap +213 -0
  122. package/tests/data-plane/__snapshots__/indexing.test.ts.snap +88 -0
  123. package/tests/views/__snapshots__/author-feed.test.ts.snap +498 -0
  124. package/tests/views/__snapshots__/block-lists.test.ts.snap +56 -0
  125. package/tests/views/__snapshots__/blocks.test.ts.snap +28 -0
  126. package/tests/views/__snapshots__/follows.test.ts.snap +170 -0
  127. package/tests/views/__snapshots__/labeler-service.test.ts.snap +15 -0
  128. package/tests/views/__snapshots__/likes.test.ts.snap +23 -0
  129. package/tests/views/__snapshots__/list-feed.test.ts.snap +68 -0
  130. package/tests/views/__snapshots__/lists.test.ts.snap +120 -0
  131. package/tests/views/__snapshots__/mute-lists.test.ts.snap +63 -0
  132. package/tests/views/__snapshots__/mutes.test.ts.snap +55 -0
  133. package/tests/views/__snapshots__/notifications.test.ts.snap +299 -0
  134. package/tests/views/__snapshots__/posts.test.ts.snap +58 -0
  135. package/tests/views/__snapshots__/profile.test.ts.snap +74 -0
  136. package/tests/views/__snapshots__/quotes.test.ts.snap +35 -0
  137. package/tests/views/__snapshots__/reposts.test.ts.snap +26 -0
  138. package/tests/views/__snapshots__/starter-packs.test.ts.snap +113 -0
  139. package/tests/views/__snapshots__/thread-v2.test.ts.snap +115 -0
  140. package/tests/views/__snapshots__/thread.test.ts.snap +145 -0
  141. package/tests/views/__snapshots__/timeline.test.ts.snap +566 -0
  142. package/tests/views/notifications.test.ts +355 -19
  143. package/tsconfig.build.tsbuildinfo +1 -1
  144. package/dist/data-plane/server/routes/private-data.d.ts +0 -9
  145. package/dist/data-plane/server/routes/private-data.d.ts.map +0 -1
  146. package/dist/data-plane/server/routes/private-data.js +0 -65
  147. package/dist/data-plane/server/routes/private-data.js.map +0 -1
  148. package/src/data-plane/server/routes/private-data.ts +0 -95
@@ -1,16 +1,22 @@
1
- import { AtpAgent } from '@atproto/api'
1
+ import { AppBskyNotificationDeclaration, AtpAgent } from '@atproto/api'
2
2
  import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'
3
- import { Namespaces } from '../../dist/stash'
4
3
  import { delayCursor } from '../../src/api/app/bsky/notification/listNotifications'
5
4
  import { ids } from '../../src/lexicon/lexicons'
5
+ import { ProfileView } from '../../src/lexicon/types/app/bsky/actor/defs'
6
6
  import {
7
+ ActivitySubscription,
7
8
  ChatPreference,
8
9
  FilterablePreference,
9
10
  Preference,
10
11
  Preferences,
11
12
  } from '../../src/lexicon/types/app/bsky/notification/defs'
13
+ import {
14
+ OutputSchema,
15
+ QueryParams,
16
+ } from '../../src/lexicon/types/app/bsky/notification/listActivitySubscriptions'
12
17
  import { Notification } from '../../src/lexicon/types/app/bsky/notification/listNotifications'
13
18
  import { InputSchema } from '../../src/lexicon/types/app/bsky/notification/putPreferencesV2'
19
+ import { Namespaces } from '../../src/stash'
14
20
  import { forSnapshot, paginateAll } from '../_util'
15
21
 
16
22
  type Database = TestNetwork['bsky']['db']
@@ -20,12 +26,19 @@ describe('notification views', () => {
20
26
  let db: Database
21
27
 
22
28
  let agent: AtpAgent
29
+ let pdsAgent: AtpAgent
23
30
  let sc: SeedClient
24
31
 
25
32
  // account dids, for convenience
26
33
  let alice: string
34
+ let bob: string
27
35
  let carol: string
28
36
  let dan: string
37
+ let eve: string
38
+ let fred: string
39
+ let greg: string
40
+ let han: string
41
+ let blocked: string
29
42
 
30
43
  beforeAll(async () => {
31
44
  network = await TestNetwork.create({
@@ -33,6 +46,7 @@ describe('notification views', () => {
33
46
  })
34
47
  db = network.bsky.db
35
48
  agent = network.bsky.getClient()
49
+ pdsAgent = network.pds.getClient()
36
50
  sc = network.getSeedClient()
37
51
  await basicSeed(sc)
38
52
  await network.bsky.db.db
@@ -40,17 +54,49 @@ describe('notification views', () => {
40
54
  .set({ trustedVerifier: true })
41
55
  .where('did', '=', alice)
42
56
  .execute()
57
+ await sc.createAccount('eve', {
58
+ email: 'eve@test.com',
59
+ handle: 'eve.test',
60
+ password: 'eve-pass',
61
+ })
62
+ await sc.createAccount('fred', {
63
+ email: 'fred@test.com',
64
+ handle: 'fred.test',
65
+ password: 'fred-pass',
66
+ })
67
+ await sc.createAccount('greg', {
68
+ email: 'greg@test.com',
69
+ handle: 'greg.test',
70
+ password: 'greg-pass',
71
+ })
72
+ await sc.createAccount('han', {
73
+ email: 'han@test.com',
74
+ handle: 'han.test',
75
+ password: 'han-pass',
76
+ })
77
+ await sc.createAccount('blocked', {
78
+ email: 'blocked@test.com',
79
+ handle: 'blocked.test',
80
+ password: 'blocked-pass',
81
+ })
43
82
  await network.processAll()
83
+
44
84
  alice = sc.dids.alice
85
+ bob = sc.dids.bob
45
86
  carol = sc.dids.carol
46
87
  dan = sc.dids.dan
88
+ eve = sc.dids.eve
89
+ fred = sc.dids.fred
90
+ greg = sc.dids.greg
91
+ han = sc.dids.han
92
+ blocked = sc.dids.blocked
47
93
  })
48
94
 
49
95
  afterAll(async () => {
50
96
  await network.close()
51
97
  })
52
98
 
53
- const sort = (notifs: Notification[]) => {
99
+ const sortNotifs = (notifs: Notification[]) => {
54
100
  // Need to sort because notification ordering is not well-defined
55
101
  return notifs.sort((a, b) => {
56
102
  const stableUriA = a.uri.replace(
@@ -170,7 +216,9 @@ describe('notification views', () => {
170
216
  ),
171
217
  },
172
218
  )
173
- expect(forSnapshot(sort(notifsDan.data.notifications))).toMatchSnapshot()
219
+ expect(
220
+ forSnapshot(sortNotifs(notifsDan.data.notifications)),
221
+ ).toMatchSnapshot()
174
222
  })
175
223
 
176
224
  it('generates notifications for likes', async () => {
@@ -184,7 +232,7 @@ describe('notification views', () => {
184
232
  },
185
233
  )
186
234
 
187
- const na = sort(
235
+ const na = sortNotifs(
188
236
  notifsAlice.data.notifications.filter((n) => n.reason === 'like'),
189
237
  )
190
238
  expect(na).toHaveLength(5)
@@ -202,7 +250,7 @@ describe('notification views', () => {
202
250
  },
203
251
  )
204
252
 
205
- const na = sort(
253
+ const na = sortNotifs(
206
254
  notifsAlice.data.notifications.filter((n) => n.reason === 'repost'),
207
255
  )
208
256
  expect(na).toHaveLength(2)
@@ -228,7 +276,7 @@ describe('notification views', () => {
228
276
  },
229
277
  )
230
278
 
231
- const no = sort(
279
+ const no = sortNotifs(
232
280
  notifsOp.data.notifications.filter((n) => n.reason === 'like'),
233
281
  )
234
282
  // Like from `alice` in this test.
@@ -245,7 +293,7 @@ describe('notification views', () => {
245
293
  },
246
294
  )
247
295
 
248
- const nr = sort(
296
+ const nr = sortNotifs(
249
297
  notifsReposter.data.notifications.filter(
250
298
  (n) => n.reason === 'like-via-repost',
251
299
  ),
@@ -273,7 +321,7 @@ describe('notification views', () => {
273
321
  },
274
322
  )
275
323
 
276
- const no = sort(
324
+ const no = sortNotifs(
277
325
  notifsOp.data.notifications.filter((n) => n.reason === 'like'),
278
326
  )
279
327
  // Like from `alice` in previous test + `carol` on this test.
@@ -290,7 +338,7 @@ describe('notification views', () => {
290
338
  },
291
339
  )
292
340
 
293
- const nr = sort(
341
+ const nr = sortNotifs(
294
342
  notifsReposter.data.notifications.filter(
295
343
  (n) => n.reason === 'like-via-repost',
296
344
  ),
@@ -319,7 +367,7 @@ describe('notification views', () => {
319
367
  },
320
368
  )
321
369
 
322
- const no = sort(
370
+ const no = sortNotifs(
323
371
  notifsOp.data.notifications.filter((n) => n.reason === 'repost'),
324
372
  )
325
373
  // Repost from `carol` in seeds + `alice` on this test.
@@ -336,7 +384,7 @@ describe('notification views', () => {
336
384
  },
337
385
  )
338
386
 
339
- const nr = sort(
387
+ const nr = sortNotifs(
340
388
  notifsReposter.data.notifications.filter(
341
389
  (n) => n.reason === 'repost-via-repost',
342
390
  ),
@@ -363,7 +411,9 @@ describe('notification views', () => {
363
411
  ),
364
412
  },
365
413
  )
366
- expect(forSnapshot(sort(notifsBob1.data.notifications))).toMatchSnapshot()
414
+ expect(
415
+ forSnapshot(sortNotifs(notifsBob1.data.notifications)),
416
+ ).toMatchSnapshot()
367
417
 
368
418
  await sc.unverify(sc.dids.alice, sc.dids.bob)
369
419
  await network.processAll()
@@ -376,7 +426,9 @@ describe('notification views', () => {
376
426
  ),
377
427
  },
378
428
  )
379
- expect(forSnapshot(sort(notifsBob2.data.notifications))).toMatchSnapshot()
429
+ expect(
430
+ forSnapshot(sortNotifs(notifsBob2.data.notifications)),
431
+ ).toMatchSnapshot()
380
432
  })
381
433
 
382
434
  it('fetches notifications without a last-seen', async () => {
@@ -396,12 +448,12 @@ describe('notification views', () => {
396
448
  const readStates = notifs.map((notif) => notif.isRead)
397
449
  expect(readStates).toEqual(notifs.map((_, i) => i !== 0)) // only first appears unread
398
450
 
399
- expect(forSnapshot(sort(notifs))).toMatchSnapshot()
451
+ expect(forSnapshot(sortNotifs(notifs))).toMatchSnapshot()
400
452
  })
401
453
 
402
454
  it('paginates', async () => {
403
455
  const results = (results) =>
404
- sort(results.flatMap((res) => res.notifications))
456
+ sortNotifs(results.flatMap((res) => res.notifications))
405
457
  const paginator = async (cursor?: string) => {
406
458
  const res = await agent.api.app.bsky.notification.listNotifications(
407
459
  { cursor, limit: 6 },
@@ -574,7 +626,7 @@ describe('notification views', () => {
574
626
  },
575
627
  )
576
628
 
577
- const notifs = sort(notifRes.data.notifications)
629
+ const notifs = sortNotifs(notifRes.data.notifications)
578
630
  expect(notifs.length).toBe(11)
579
631
  expect(forSnapshot(notifs)).toMatchSnapshot()
580
632
  expect(notifCount.data.count).toBe(11)
@@ -693,7 +745,7 @@ describe('notification views', () => {
693
745
 
694
746
  it('paginates filtered notifications', async () => {
695
747
  const results = (results) =>
696
- sort(results.flatMap((res) => res.notifications))
748
+ sortNotifs(results.flatMap((res) => res.notifications))
697
749
  const paginator = async (cursor?: string) => {
698
750
  const res = await agent.app.bsky.notification.listNotifications(
699
751
  { reasons: ['mention', 'reply'], cursor, limit: 2 },
@@ -787,7 +839,7 @@ describe('notification views', () => {
787
839
  jest.setSystemTime(new Date(firstNotification.sortAt))
788
840
 
789
841
  const results = (results) =>
790
- sort(results.flatMap((res) => res.notifications))
842
+ sortNotifs(results.flatMap((res) => res.notifications))
791
843
  const paginator = async (cursor?: string) => {
792
844
  const res =
793
845
  await delayAgent.api.app.bsky.notification.listNotifications(
@@ -1131,8 +1183,292 @@ describe('notification views', () => {
1131
1183
  await putAndAssert(input1, expected1)
1132
1184
  })
1133
1185
  })
1186
+
1187
+ describe('activity subscriptions', () => {
1188
+ const sortProfiles = (profiles: ProfileView[]) => {
1189
+ return profiles.sort((a, b) => (a.handle > b.handle ? 1 : -1))
1190
+ }
1191
+
1192
+ const declare = async (actor: string, value: string) => {
1193
+ await pdsAgent.com.atproto.repo.createRecord(
1194
+ {
1195
+ repo: actor,
1196
+ collection: ids.AppBskyNotificationDeclaration,
1197
+ rkey: 'self',
1198
+ record: {
1199
+ allowSubscriptions: value,
1200
+ } as AppBskyNotificationDeclaration.Record,
1201
+ },
1202
+ { headers: sc.getHeaders(actor), encoding: 'application/json' },
1203
+ )
1204
+ }
1205
+
1206
+ const put = async (
1207
+ actor: string,
1208
+ subject: string,
1209
+ val: ActivitySubscription,
1210
+ ) =>
1211
+ agent.app.bsky.notification.putActivitySubscription(
1212
+ {
1213
+ subject,
1214
+ activitySubscription: val,
1215
+ },
1216
+ {
1217
+ headers: await network.serviceHeaders(
1218
+ actor,
1219
+ ids.AppBskyNotificationPutActivitySubscription,
1220
+ ),
1221
+ },
1222
+ )
1223
+
1224
+ const list = async (actor: string, params?: QueryParams) =>
1225
+ agent.app.bsky.notification.listActivitySubscriptions(params ?? {}, {
1226
+ headers: await network.serviceHeaders(
1227
+ actor,
1228
+ ids.AppBskyNotificationListActivitySubscriptions,
1229
+ ),
1230
+ })
1231
+
1232
+ const associatedAllowSub = async (actor: string, subject: string) => {
1233
+ const { data } = await agent.app.bsky.actor.getProfile(
1234
+ { actor: subject },
1235
+ {
1236
+ headers: await network.serviceHeaders(
1237
+ actor,
1238
+ ids.AppBskyActorGetProfile,
1239
+ ),
1240
+ },
1241
+ )
1242
+ return data.associated?.activitySubscription?.allowSubscriptions
1243
+ }
1244
+
1245
+ const viewerActivitySub = async (actor: string, subject: string) => {
1246
+ const { data } = await agent.app.bsky.actor.getProfile(
1247
+ { actor: subject },
1248
+ {
1249
+ headers: await network.serviceHeaders(
1250
+ actor,
1251
+ ids.AppBskyActorGetProfile,
1252
+ ),
1253
+ },
1254
+ )
1255
+ return data.viewer?.activitySubscription
1256
+ }
1257
+
1258
+ beforeAll(async () => {
1259
+ // 'none' declaration.
1260
+ await declare(bob, 'none')
1261
+
1262
+ // 'mutuals' declaration and both follow.
1263
+ await declare(carol, 'mutuals')
1264
+ await sc.follow(alice, carol)
1265
+ await sc.follow(carol, alice)
1266
+
1267
+ // 'mutuals' declaration but only actor follows.
1268
+ await declare(dan, 'mutuals')
1269
+ await sc.follow(alice, dan)
1270
+
1271
+ // 'mutuals' declaration but only subject follows.
1272
+ await declare(eve, 'mutuals')
1273
+ await sc.follow(eve, alice)
1274
+
1275
+ // 'followers' declaration and viewer follows.
1276
+ await declare(fred, 'followers')
1277
+ await sc.follow(alice, fred)
1278
+
1279
+ // 'followers' declaration but viewer does not follow.
1280
+ await declare(greg, 'followers')
1281
+
1282
+ // blocked.
1283
+ await declare(blocked, 'followers')
1284
+ await sc.block(alice, blocked)
1285
+
1286
+ await network.processAll()
1287
+ })
1288
+
1289
+ beforeEach(async () => {
1290
+ await clearActivitySubscription(db)
1291
+ })
1292
+
1293
+ it('lists an empty list of subscriptions', async () => {
1294
+ const actorDid = alice
1295
+
1296
+ const { data } = await list(actorDid)
1297
+
1298
+ expect(data.cursor).toBeUndefined()
1299
+ expect(data.subscriptions).toHaveLength(0)
1300
+ })
1301
+
1302
+ it('does not allow subscribing to self', async () => {
1303
+ const actorDid = alice
1304
+ const promise = put(actorDid, actorDid, { post: true, reply: false })
1305
+
1306
+ await expect(promise).rejects.toThrow('Cannot subscribe to own activity')
1307
+ })
1308
+
1309
+ it('inserts a subscription entry if it does not exist', async () => {
1310
+ const actorDid = alice
1311
+ const subjectDid = fred
1312
+ const val = { post: true, reply: false }
1313
+
1314
+ const { data: createData } = await put(actorDid, subjectDid, val)
1315
+ expect(createData).toStrictEqual({
1316
+ subject: subjectDid,
1317
+ activitySubscription: val,
1318
+ })
1319
+
1320
+ const { data: listData } = await list(actorDid)
1321
+ expect(listData).toEqual({
1322
+ cursor: expect.any(String),
1323
+ subscriptions: [
1324
+ expect.objectContaining({
1325
+ did: subjectDid,
1326
+ viewer: expect.objectContaining({ activitySubscription: val }),
1327
+ }),
1328
+ ],
1329
+ })
1330
+ })
1331
+
1332
+ it('updates a subscription entry if it exists', async () => {
1333
+ const actorDid = alice
1334
+ const subjectDid = fred
1335
+ const valCreate = { post: true, reply: false }
1336
+ const valUpdate = { post: false, reply: true }
1337
+
1338
+ const { data: createData } = await put(actorDid, subjectDid, valCreate)
1339
+ expect(createData).toStrictEqual({
1340
+ subject: subjectDid,
1341
+ activitySubscription: valCreate,
1342
+ })
1343
+
1344
+ const { data: updateData } = await put(actorDid, subjectDid, valUpdate)
1345
+ expect(updateData).toStrictEqual({
1346
+ subject: subjectDid,
1347
+ activitySubscription: valUpdate,
1348
+ })
1349
+
1350
+ const { data: listData } = await list(actorDid)
1351
+ expect(listData).toEqual({
1352
+ cursor: expect.any(String),
1353
+ subscriptions: [
1354
+ expect.objectContaining({
1355
+ did: subjectDid,
1356
+ viewer: expect.objectContaining({
1357
+ activitySubscription: valUpdate,
1358
+ }),
1359
+ }),
1360
+ ],
1361
+ })
1362
+ })
1363
+
1364
+ it('deletes a subscription entry when all options are turned off', async () => {
1365
+ const actorDid = alice
1366
+ const subjectDid = fred
1367
+ const valCreate = { post: true, reply: false }
1368
+ const valDelete = { post: false, reply: false }
1369
+
1370
+ await put(actorDid, subjectDid, valCreate)
1371
+ const { data: list0 } = await list(actorDid)
1372
+ expect(list0.subscriptions).toHaveLength(1)
1373
+
1374
+ await put(actorDid, subjectDid, valDelete)
1375
+ const { data: list1 } = await list(actorDid)
1376
+ expect(list1.subscriptions).toHaveLength(0)
1377
+ })
1378
+
1379
+ it('paginates', async () => {
1380
+ const actorDid = alice
1381
+ const limit = 2
1382
+ const val = { post: true, reply: false }
1383
+
1384
+ await put(actorDid, bob, val)
1385
+ await put(actorDid, carol, val)
1386
+ await put(actorDid, dan, val)
1387
+ await put(actorDid, eve, val)
1388
+ await put(actorDid, fred, val)
1389
+ await put(actorDid, blocked, val) // blocked is removed from the list.
1390
+
1391
+ const results = (results: OutputSchema[]) =>
1392
+ sortProfiles(results.flatMap((res: OutputSchema) => res.subscriptions))
1393
+ const paginator = async (cursor?: string) => {
1394
+ const { data } = await list(actorDid, { cursor, limit })
1395
+ return data
1396
+ }
1397
+
1398
+ const paginatedAll = await paginateAll(paginator)
1399
+ paginatedAll.forEach((res) =>
1400
+ expect(res.subscriptions.length).toBeLessThanOrEqual(limit),
1401
+ )
1402
+
1403
+ const full = await list(actorDid)
1404
+ expect(full.data.subscriptions.length).toEqual(5)
1405
+ expect(results(paginatedAll)).toEqual(results([full.data]))
1406
+ })
1407
+
1408
+ it('gets the declaration record', async () => {
1409
+ const declaration = await pdsAgent.com.atproto.repo.getRecord({
1410
+ repo: carol,
1411
+ collection: 'app.bsky.notification.declaration',
1412
+ rkey: 'self',
1413
+ })
1414
+
1415
+ expect(declaration.data.value.allowSubscriptions).toEqual('mutuals')
1416
+ })
1417
+
1418
+ describe('activity subscription declaration', () => {
1419
+ it('includes the declaration in the profile view', async () => {
1420
+ await expect(associatedAllowSub(alice, bob)).resolves.toBe('none')
1421
+ await expect(associatedAllowSub(alice, carol)).resolves.toBe('mutuals')
1422
+ await expect(associatedAllowSub(alice, dan)).resolves.toBe('mutuals')
1423
+ await expect(associatedAllowSub(alice, eve)).resolves.toBe('mutuals')
1424
+ await expect(associatedAllowSub(alice, fred)).resolves.toBe('followers')
1425
+ await expect(associatedAllowSub(alice, greg)).resolves.toBe('followers')
1426
+ })
1427
+ })
1428
+
1429
+ describe('activity subscription viewer state', () => {
1430
+ it('includes the relationship in the profile view', async () => {
1431
+ const viewer = alice
1432
+ const val = { post: true, reply: true }
1433
+
1434
+ // 'none' declaration.
1435
+ await put(viewer, bob, val)
1436
+ await expect(viewerActivitySub(viewer, bob)).resolves.toBeUndefined()
1437
+
1438
+ // 'mutuals' declaration and both follow.
1439
+ await put(viewer, carol, val)
1440
+ await expect(viewerActivitySub(viewer, carol)).resolves.toStrictEqual(
1441
+ val,
1442
+ )
1443
+
1444
+ // 'mutuals' declaration but only actor follows.
1445
+ await put(viewer, dan, val)
1446
+ await expect(viewerActivitySub(viewer, dan)).resolves.toBeUndefined()
1447
+
1448
+ // 'mutuals' declaration but only subject follows.
1449
+ await put(viewer, eve, val)
1450
+ await expect(viewerActivitySub(viewer, eve)).resolves.toBeUndefined()
1451
+
1452
+ // 'followers' declaration and viewer follows.
1453
+ await put(viewer, fred, val)
1454
+ await expect(viewerActivitySub(viewer, carol)).resolves.toStrictEqual(
1455
+ val,
1456
+ )
1457
+
1458
+ // 'followers' declaration but viewer does not follow.
1459
+ await expect(viewerActivitySub(viewer, greg)).resolves.toBeUndefined()
1460
+
1461
+ // no declaration
1462
+ await expect(viewerActivitySub(viewer, han)).resolves.toBeUndefined()
1463
+ })
1464
+ })
1465
+ })
1134
1466
  })
1135
1467
 
1136
1468
  const clearPrivateData = async (db: Database) => {
1137
1469
  await db.db.deleteFrom('private_data').execute()
1138
1470
  }
1471
+
1472
+ const clearActivitySubscription = async (db: Database) => {
1473
+ await db.db.deleteFrom('activity_subscription').execute()
1474
+ }