@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
@@ -2,6 +2,7 @@ import { ConnectRouter } from '@connectrpc/connect'
2
2
  import { IdResolver } from '@atproto/identity'
3
3
  import { Service } from '../../../proto/bsky_connect'
4
4
  import { Database } from '../db'
5
+ import activitySubscription from './activity-subscription'
5
6
  import blocks from './blocks'
6
7
  import feedGens from './feed-gens'
7
8
  import feeds from './feeds'
@@ -14,7 +15,6 @@ import lists from './lists'
14
15
  import moderation from './moderation'
15
16
  import mutes from './mutes'
16
17
  import notifs from './notifs'
17
- import privateData from './private-data'
18
18
  import profile from './profile'
19
19
  import quotes from './quotes'
20
20
  import records from './records'
@@ -29,6 +29,7 @@ import threads from './threads'
29
29
  export default (db: Database, idResolver: IdResolver) =>
30
30
  (router: ConnectRouter) =>
31
31
  router.service(Service, {
32
+ ...activitySubscription(db),
32
33
  ...blocks(db),
33
34
  ...feedGens(db),
34
35
  ...feeds(db),
@@ -41,7 +42,6 @@ export default (db: Database, idResolver: IdResolver) =>
41
42
  ...moderation(db),
42
43
  ...mutes(db),
43
44
  ...notifs(db),
44
- ...privateData(db),
45
45
  ...profile(db),
46
46
  ...quotes(db),
47
47
  ...records(db),
@@ -1,7 +1,24 @@
1
1
  import { Timestamp } from '@bufbuild/protobuf'
2
2
  import { ServiceImpl } from '@connectrpc/connect'
3
3
  import { sql } from 'kysely'
4
+ import { keyBy } from '@atproto/common'
5
+ import { jsonStringToLex } from '@atproto/lexicon'
6
+ import {
7
+ ChatPreference,
8
+ FilterablePreference,
9
+ Preference,
10
+ Preferences,
11
+ } from '../../../lexicon/types/app/bsky/notification/defs'
4
12
  import { Service } from '../../../proto/bsky_connect'
13
+ import {
14
+ ChatNotificationInclude,
15
+ ChatNotificationPreference,
16
+ FilterableNotificationPreference,
17
+ NotificationInclude,
18
+ NotificationPreference,
19
+ NotificationPreferences,
20
+ } from '../../../proto/bsky_pb'
21
+ import { Namespaces } from '../../../stash'
5
22
  import { Database } from '../db'
6
23
  import { IsoSortAtKey } from '../db/pagination'
7
24
  import { countAll, notSoftDeletedClause } from '../db/util'
@@ -151,4 +168,82 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
151
168
  .onConflict((oc) => oc.doNothing())
152
169
  .executeTakeFirst()
153
170
  },
171
+
172
+ async getNotificationPreferences(req) {
173
+ const { dids } = req
174
+ if (dids.length === 0) {
175
+ return { preferences: [] }
176
+ }
177
+
178
+ const res = await db.db
179
+ .selectFrom('private_data')
180
+ .selectAll()
181
+ .where('actorDid', 'in', dids)
182
+ .where('namespace', '=', Namespaces.AppBskyNotificationDefsPreferences)
183
+ .where('key', '=', 'self')
184
+ .execute()
185
+
186
+ const byDid = keyBy(res, 'actorDid')
187
+ const preferences = dids.map((did) => {
188
+ const row = byDid.get(did)
189
+ if (!row) {
190
+ return {}
191
+ }
192
+ const p = jsonStringToLex(row.payload) as Preferences
193
+ return notificationPreferencesLexToProtobuf(p, row.payload)
194
+ })
195
+
196
+ return { preferences }
197
+ },
154
198
  })
199
+
200
+ export const notificationPreferencesLexToProtobuf = (
201
+ p: Preferences,
202
+ json: string,
203
+ ): NotificationPreferences => {
204
+ const lexChatPreferenceToProtobuf = (
205
+ p: ChatPreference,
206
+ ): ChatNotificationPreference =>
207
+ new ChatNotificationPreference({
208
+ include:
209
+ p.include === 'accepted'
210
+ ? ChatNotificationInclude.ACCEPTED
211
+ : ChatNotificationInclude.ALL,
212
+ push: { enabled: p.push ?? true },
213
+ })
214
+
215
+ const lexFilterablePreferenceToProtobuf = (
216
+ p: FilterablePreference,
217
+ ): FilterableNotificationPreference =>
218
+ new FilterableNotificationPreference({
219
+ include:
220
+ p.include === 'follows'
221
+ ? NotificationInclude.FOLLOWS
222
+ : NotificationInclude.ALL,
223
+ list: { enabled: p.list ?? true },
224
+ push: { enabled: p.push ?? true },
225
+ })
226
+
227
+ const lexPreferenceToProtobuf = (p: Preference): NotificationPreference =>
228
+ new NotificationPreference({
229
+ list: { enabled: p.list ?? true },
230
+ push: { enabled: p.push ?? true },
231
+ })
232
+
233
+ return new NotificationPreferences({
234
+ entry: Buffer.from(json),
235
+ chat: lexChatPreferenceToProtobuf(p.chat),
236
+ follow: lexFilterablePreferenceToProtobuf(p.follow),
237
+ like: lexFilterablePreferenceToProtobuf(p.like),
238
+ likeViaRepost: lexFilterablePreferenceToProtobuf(p.likeViaRepost),
239
+ mention: lexFilterablePreferenceToProtobuf(p.mention),
240
+ quote: lexFilterablePreferenceToProtobuf(p.quote),
241
+ reply: lexFilterablePreferenceToProtobuf(p.reply),
242
+ repost: lexFilterablePreferenceToProtobuf(p.repost),
243
+ repostViaRepost: lexFilterablePreferenceToProtobuf(p.repostViaRepost),
244
+ starterpackJoined: lexPreferenceToProtobuf(p.starterpackJoined),
245
+ subscribedPost: lexPreferenceToProtobuf(p.subscribedPost),
246
+ unverified: lexPreferenceToProtobuf(p.unverified),
247
+ verified: lexPreferenceToProtobuf(p.verified),
248
+ })
249
+ }
@@ -1,6 +1,10 @@
1
1
  import { Timestamp } from '@bufbuild/protobuf'
2
2
  import { ServiceImpl } from '@connectrpc/connect'
3
3
  import { Selectable, sql } from 'kysely'
4
+ import {
5
+ AppBskyNotificationDeclaration,
6
+ ChatBskyActorDeclaration,
7
+ } from '@atproto/api'
4
8
  import { keyBy } from '@atproto/common'
5
9
  import { parseRecordBytes } from '../../../hydration/util'
6
10
  import { Service } from '../../../proto/bsky_connect'
@@ -31,6 +35,9 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
31
35
  const chatDeclarationUris = dids.map(
32
36
  (did) => `at://${did}/chat.bsky.actor.declaration/self`,
33
37
  )
38
+ const notifDeclarationUris = dids.map(
39
+ (did) => `at://${did}/app.bsky.notification.declaration/self`,
40
+ )
34
41
  const { ref } = db.db.dynamic
35
42
  const [
36
43
  handlesRes,
@@ -38,6 +45,7 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
38
45
  profiles,
39
46
  statuses,
40
47
  chatDeclarations,
48
+ notifDeclarations,
41
49
  ] = await Promise.all([
42
50
  db.db
43
51
  .selectFrom('actor')
@@ -64,6 +72,7 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
64
72
  getRecords(db)({ uris: profileUris }),
65
73
  getRecords(db)({ uris: statusUris }),
66
74
  getRecords(db)({ uris: chatDeclarationUris }),
75
+ getRecords(db)({ uris: notifDeclarationUris }),
67
76
  ])
68
77
 
69
78
  const verificationsBySubjectDid = verificationsReceived.reduce(
@@ -82,7 +91,7 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
82
91
 
83
92
  const status = statuses.records[i]
84
93
 
85
- const chatDeclaration = parseRecordBytes(
94
+ const chatDeclaration = parseRecordBytes<ChatBskyActorDeclaration.Record>(
86
95
  chatDeclarations.records[i].record,
87
96
  )
88
97
 
@@ -97,6 +106,28 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
97
106
  return acc
98
107
  }, {} as VerifiedBy)
99
108
 
109
+ const activitySubscription = () => {
110
+ const record = parseRecordBytes<AppBskyNotificationDeclaration.Record>(
111
+ notifDeclarations.records[i].record,
112
+ )
113
+
114
+ // The dataplane is responsible for setting the default of "followers" (default according to the lexicon).
115
+ const defaultVal = 'followers'
116
+
117
+ if (typeof record?.allowSubscriptions !== 'string') {
118
+ return defaultVal
119
+ }
120
+
121
+ switch (record.allowSubscriptions) {
122
+ case 'followers':
123
+ case 'mutuals':
124
+ case 'none':
125
+ return record.allowSubscriptions
126
+ default:
127
+ return defaultVal
128
+ }
129
+ }
130
+
100
131
  return {
101
132
  exists: !!row,
102
133
  handle: row?.handle ?? undefined,
@@ -117,6 +148,7 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
117
148
  statusRecord: status,
118
149
  tags: [],
119
150
  profileTags: [],
151
+ allowActivitySubscriptionsFrom: activitySubscription(),
120
152
  }
121
153
  })
122
154
  return { actors }
@@ -23,6 +23,10 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
23
23
  getPostgateRecords: getRecords(db, ids.AppBskyFeedPostgate),
24
24
  getLabelerRecords: getRecords(db, ids.AppBskyLabelerService),
25
25
  getActorChatDeclarationRecords: getRecords(db, ids.ChatBskyActorDeclaration),
26
+ getNotificationDeclarationRecords: getRecords(
27
+ db,
28
+ ids.AppBskyNotificationDeclaration,
29
+ ),
26
30
  getStarterPackRecords: getRecords(db, ids.AppBskyGraphStarterpack),
27
31
  getVerificationRecords: getRecords(db, ids.AppBskyGraphVerification),
28
32
  getStatusRecords: getRecords(db, ids.AppBskyActorStatus),
@@ -1,17 +1,25 @@
1
+ import { AppBskyNotificationDeclaration } from '@atproto/api'
1
2
  import { mapDefined } from '@atproto/common'
2
3
  import { DataPlaneClient } from '../data-plane/client'
3
4
  import { Record as ProfileRecord } from '../lexicon/types/app/bsky/actor/profile'
4
5
  import { Record as StatusRecord } from '../lexicon/types/app/bsky/actor/status'
6
+ import { Record as NotificationDeclarationRecord } from '../lexicon/types/app/bsky/notification/declaration'
5
7
  import { Record as ChatDeclarationRecord } from '../lexicon/types/chat/bsky/actor/declaration'
6
- import { VerificationMeta } from '../proto/bsky_pb'
8
+ import { ActivitySubscription, VerificationMeta } from '../proto/bsky_pb'
7
9
  import {
8
10
  HydrationMap,
9
11
  RecordInfo,
12
+ isActivitySubscriptionEnabled,
10
13
  parseRecord,
11
14
  parseString,
12
15
  safeTakedownRef,
13
16
  } from './util'
14
17
 
18
+ type AllowActivitySubscriptions = Extract<
19
+ AppBskyNotificationDeclaration.Record['allowSubscriptions'],
20
+ 'followers' | 'mutuals' | 'none'
21
+ >
22
+
15
23
  export type Actor = {
16
24
  did: string
17
25
  handle?: string
@@ -29,6 +37,7 @@ export type Actor = {
29
37
  trustedVerifier?: boolean
30
38
  verifications: VerificationHydrationState[]
31
39
  status?: RecordInfo<StatusRecord>
40
+ allowActivitySubscriptionsFrom: AllowActivitySubscriptions
32
41
  }
33
42
 
34
43
  export type VerificationHydrationState = {
@@ -46,6 +55,9 @@ export type Actors = HydrationMap<Actor>
46
55
  export type ChatDeclaration = RecordInfo<ChatDeclarationRecord>
47
56
 
48
57
  export type ChatDeclarations = HydrationMap<ChatDeclaration>
58
+ export type NotificationDeclaration = RecordInfo<NotificationDeclarationRecord>
59
+
60
+ export type NotificationDeclarations = HydrationMap<NotificationDeclaration>
49
61
 
50
62
  export type Status = RecordInfo<StatusRecord>
51
63
  export type Statuses = HydrationMap<Status>
@@ -59,15 +71,25 @@ export type ProfileViewerState = {
59
71
  blockingByList?: string
60
72
  following?: string
61
73
  followedBy?: string
62
- knownFollowers?: {
63
- count: number
64
- followers: string[]
65
- }
66
74
  }
67
75
 
68
76
  export type ProfileViewerStates = HydrationMap<ProfileViewerState>
69
77
 
70
- export type KnownFollowers = HydrationMap<ProfileViewerState['knownFollowers']>
78
+ type ActivitySubscriptionState = {
79
+ post: boolean
80
+ reply: boolean
81
+ }
82
+
83
+ export type ActivitySubscriptionStates = HydrationMap<
84
+ ActivitySubscriptionState | undefined
85
+ >
86
+
87
+ type KnownFollowersState = {
88
+ count: number
89
+ followers: string[]
90
+ }
91
+
92
+ export type KnownFollowersStates = HydrationMap<KnownFollowersState | undefined>
71
93
 
72
94
  export type ProfileAgg = {
73
95
  followers: number
@@ -177,6 +199,20 @@ export class ActorHydrator {
177
199
  },
178
200
  )
179
201
 
202
+ const allowActivitySubscriptionsFrom = (
203
+ val: string,
204
+ ): AllowActivitySubscriptions => {
205
+ switch (val) {
206
+ case 'followers':
207
+ case 'mutuals':
208
+ case 'none':
209
+ return val
210
+ default:
211
+ // The dataplane should set the default of "FOLLOWERS". Just in case.
212
+ return 'followers'
213
+ }
214
+ }
215
+
180
216
  return acc.set(did, {
181
217
  did,
182
218
  handle: parseString(actor.handle),
@@ -194,6 +230,9 @@ export class ActorHydrator {
194
230
  trustedVerifier: actor.trustedVerifier,
195
231
  verifications,
196
232
  status: status,
233
+ allowActivitySubscriptionsFrom: allowActivitySubscriptionsFrom(
234
+ actor.allowActivitySubscriptionsFrom,
235
+ ),
197
236
  })
198
237
  }, new HydrationMap<Actor>())
199
238
  }
@@ -213,6 +252,21 @@ export class ActorHydrator {
213
252
  }, new HydrationMap<ChatDeclaration>())
214
253
  }
215
254
 
255
+ async getNotificationDeclarations(
256
+ uris: string[],
257
+ includeTakedowns = false,
258
+ ): Promise<NotificationDeclarations> {
259
+ if (!uris.length) return new HydrationMap<NotificationDeclaration>()
260
+ const res = await this.dataplane.getActorChatDeclarationRecords({ uris })
261
+ return uris.reduce((acc, uri, i) => {
262
+ const record = parseRecord<NotificationDeclarationRecord>(
263
+ res.records[i],
264
+ includeTakedowns,
265
+ )
266
+ return acc.set(uri, record ?? null)
267
+ }, new HydrationMap<NotificationDeclaration>())
268
+ }
269
+
216
270
  async getStatus(uris: string[], includeTakedowns = false): Promise<Statuses> {
217
271
  if (!uris.length) return new HydrationMap<Status>()
218
272
  const res = await this.dataplane.getStatusRecords({ uris })
@@ -234,10 +288,11 @@ export class ActorHydrator {
234
288
  actorDid: viewer,
235
289
  targetDids: dids,
236
290
  })
291
+
237
292
  return dids.reduce((acc, did, i) => {
238
293
  const rels = res.relationships[i]
239
294
  if (viewer === did) {
240
- // ignore self-follows, self-mutes, self-blocks
295
+ // ignore self-follows, self-mutes, self-blocks, self-activity-subscriptions
241
296
  return acc.set(did, {})
242
297
  }
243
298
  return acc.set(did, {
@@ -256,8 +311,8 @@ export class ActorHydrator {
256
311
  async getKnownFollowers(
257
312
  dids: string[],
258
313
  viewer: string | null,
259
- ): Promise<KnownFollowers> {
260
- if (!viewer) return new HydrationMap<ProfileViewerState['knownFollowers']>()
314
+ ): Promise<KnownFollowersStates> {
315
+ if (!viewer) return new HydrationMap<KnownFollowersState | undefined>()
261
316
  const { results: knownFollowersResults } = await this.dataplane
262
317
  .getFollowsFollowing(
263
318
  {
@@ -280,7 +335,39 @@ export class ActorHydrator {
280
335
  }
281
336
  : undefined,
282
337
  )
283
- }, new HydrationMap<ProfileViewerState['knownFollowers']>())
338
+ }, new HydrationMap<KnownFollowersState | undefined>())
339
+ }
340
+
341
+ async getActivitySubscriptions(
342
+ dids: string[],
343
+ viewer: string | null,
344
+ ): Promise<ActivitySubscriptionStates> {
345
+ if (!viewer) {
346
+ return new HydrationMap<ActivitySubscriptionState | undefined>()
347
+ }
348
+
349
+ const activitySubscription = (val: ActivitySubscription | undefined) => {
350
+ if (!val) return undefined
351
+
352
+ const result = {
353
+ post: !!val.post,
354
+ reply: !!val.reply,
355
+ }
356
+ if (!isActivitySubscriptionEnabled(result)) return undefined
357
+
358
+ return result
359
+ }
360
+
361
+ const { subscriptions } = await this.dataplane
362
+ .getActivitySubscriptionsByActorAndSubjects(
363
+ { actorDid: viewer, subjectDids: dids },
364
+ { signal: AbortSignal.timeout(100) },
365
+ )
366
+ .catch(() => ({ subscriptions: [] }))
367
+
368
+ return dids.reduce((acc, did, i) => {
369
+ return acc.set(did, activitySubscription(subscriptions[i]))
370
+ }, new HydrationMap<ActivitySubscriptionState | undefined>())
284
371
  }
285
372
 
286
373
  async getProfileAggregates(dids: string[]): Promise<ProfileAggs> {
@@ -12,9 +12,10 @@ import { Notification } from '../proto/bsky_pb'
12
12
  import { ParsedLabelers } from '../util'
13
13
  import { uriToDid, uriToDid as didFromUri } from '../util/uris'
14
14
  import {
15
+ ActivitySubscriptionStates,
15
16
  ActorHydrator,
16
17
  Actors,
17
- KnownFollowers,
18
+ KnownFollowersStates,
18
19
  ProfileAggs,
19
20
  ProfileViewerState,
20
21
  ProfileViewerStates,
@@ -121,7 +122,8 @@ export type HydrationState = {
121
122
  labelers?: Labelers
122
123
  labelerViewers?: LabelerViewerStates
123
124
  labelerAggs?: LabelerAggs
124
- knownFollowers?: KnownFollowers
125
+ knownFollowers?: KnownFollowersStates
126
+ activitySubscriptions?: ActivitySubscriptionStates
125
127
  bidirectionalBlocks?: BidirectionalBlocks
126
128
  verifications?: Verifications
127
129
  }
@@ -175,12 +177,12 @@ export class Hydrator {
175
177
  viewer,
176
178
  )
177
179
  const listUris: string[] = []
178
- profileViewers?.forEach((item) => {
180
+ profileViewers.forEach((item) => {
179
181
  listUris.push(...listUrisFromProfileViewer(item))
180
182
  })
181
183
  const listState = await this.hydrateListsBasic(listUris, ctx)
182
184
  // if a list no longer exists or is not a mod list, then remove from viewer state
183
- profileViewers?.forEach((item) => {
185
+ profileViewers.forEach((item) => {
184
186
  removeNonModListsFromProfileViewer(item, listState)
185
187
  })
186
188
 
@@ -239,8 +241,7 @@ export class Hydrator {
239
241
  dids: string[],
240
242
  ctx: HydrateCtx,
241
243
  ): Promise<HydrationState> {
242
- let knownFollowers: KnownFollowers = new HydrationMap()
243
-
244
+ let knownFollowers: KnownFollowersStates = new HydrationMap()
244
245
  try {
245
246
  knownFollowers = await this.actor.getKnownFollowers(dids, ctx.viewer)
246
247
  } catch (err) {
@@ -250,6 +251,19 @@ export class Hydrator {
250
251
  )
251
252
  }
252
253
 
254
+ let activitySubscriptions: ActivitySubscriptionStates = new HydrationMap()
255
+ try {
256
+ activitySubscriptions = await this.actor.getActivitySubscriptions(
257
+ dids,
258
+ ctx.viewer,
259
+ )
260
+ } catch (err) {
261
+ hydrationLogger.error(
262
+ { err },
263
+ 'Failed to get activity subscriptions state for profiles',
264
+ )
265
+ }
266
+
253
267
  const subjectsToKnownFollowersMap = Array.from(
254
268
  knownFollowers.keys(),
255
269
  ).reduce((acc, did) => {
@@ -281,6 +295,7 @@ export class Hydrator {
281
295
  return mergeManyStates(state, starterPackState, {
282
296
  profileAggs,
283
297
  knownFollowers,
298
+ activitySubscriptions,
284
299
  ctx,
285
300
  bidirectionalBlocks,
286
301
  })
@@ -1130,6 +1145,13 @@ export class Hydrator {
1130
1145
  uri,
1131
1146
  ) ?? undefined
1132
1147
  )
1148
+ } else if (collection === ids.AppBskyNotificationDeclaration) {
1149
+ if (parsed.rkey !== 'self') return
1150
+ return (
1151
+ (
1152
+ await this.actor.getNotificationDeclarations([uri], includeTakedowns)
1153
+ ).get(uri) ?? undefined
1154
+ )
1133
1155
  } else if (collection === ids.AppBskyActorStatus) {
1134
1156
  if (parsed.rkey !== 'self') return
1135
1157
  return (
@@ -1348,6 +1370,10 @@ export const mergeStates = (
1348
1370
  labelerAggs: mergeMaps(stateA.labelerAggs, stateB.labelerAggs),
1349
1371
  labelerViewers: mergeMaps(stateA.labelerViewers, stateB.labelerViewers),
1350
1372
  knownFollowers: mergeMaps(stateA.knownFollowers, stateB.knownFollowers),
1373
+ activitySubscriptions: mergeMaps(
1374
+ stateA.activitySubscriptions,
1375
+ stateB.activitySubscriptions,
1376
+ ),
1351
1377
  bidirectionalBlocks: mergeNestedMaps(
1352
1378
  stateA.bidirectionalBlocks,
1353
1379
  stateB.bidirectionalBlocks,
@@ -156,3 +156,11 @@ export const safeTakedownRef = (obj?: {
156
156
  if (obj.takedownRef) return obj.takedownRef
157
157
  if (obj.takenDown) return 'BSKY-TAKEDOWN-UNKNOWN'
158
158
  }
159
+
160
+ export const isActivitySubscriptionEnabled = ({
161
+ post,
162
+ reply,
163
+ }: {
164
+ post: boolean
165
+ reply: boolean
166
+ }): boolean => post || reply
@@ -4790,10 +4790,14 @@ export const schemaDict = {
4790
4790
  format: 'at-uri',
4791
4791
  },
4792
4792
  knownFollowers: {
4793
+ description:
4794
+ 'This property is present only in selected cases, as an optimization.',
4793
4795
  type: 'ref',
4794
4796
  ref: 'lex:app.bsky.actor.defs#knownFollowers',
4795
4797
  },
4796
4798
  activitySubscription: {
4799
+ description:
4800
+ 'This property is present only in selected cases, as an optimization.',
4797
4801
  type: 'ref',
4798
4802
  ref: 'lex:app.bsky.notification.defs#activitySubscription',
4799
4803
  },
@@ -22,6 +22,10 @@ import {
22
22
  DeleteActorMuteResponse,
23
23
  DeleteThreadMuteRequest,
24
24
  DeleteThreadMuteResponse,
25
+ GetActivitySubscriptionDidsRequest,
26
+ GetActivitySubscriptionDidsResponse,
27
+ GetActivitySubscriptionsByActorAndSubjectsRequest,
28
+ GetActivitySubscriptionsByActorAndSubjectsResponse,
25
29
  GetActorChatDeclarationRecordsRequest,
26
30
  GetActorChatDeclarationRecordsResponse,
27
31
  GetActorFeedsRequest,
@@ -126,6 +130,8 @@ import {
126
130
  GetMutesResponse,
127
131
  GetNewUserCountForRangeRequest,
128
132
  GetNewUserCountForRangeResponse,
133
+ GetNotificationDeclarationRecordsRequest,
134
+ GetNotificationDeclarationRecordsResponse,
129
135
  GetNotificationPreferencesRequest,
130
136
  GetNotificationPreferencesResponse,
131
137
  GetNotificationSeenRequest,
@@ -307,6 +313,15 @@ export const Service = {
307
313
  O: GetActorChatDeclarationRecordsResponse,
308
314
  kind: MethodKind.Unary,
309
315
  },
316
+ /**
317
+ * @generated from rpc bsky.Service.GetNotificationDeclarationRecords
318
+ */
319
+ getNotificationDeclarationRecords: {
320
+ name: 'GetNotificationDeclarationRecords',
321
+ I: GetNotificationDeclarationRecordsRequest,
322
+ O: GetNotificationDeclarationRecordsResponse,
323
+ kind: MethodKind.Unary,
324
+ },
310
325
  /**
311
326
  * @generated from rpc bsky.Service.GetStatusRecords
312
327
  */
@@ -769,6 +784,24 @@ export const Service = {
769
784
  O: GetUnreadNotificationCountResponse,
770
785
  kind: MethodKind.Unary,
771
786
  },
787
+ /**
788
+ * @generated from rpc bsky.Service.GetActivitySubscriptionDids
789
+ */
790
+ getActivitySubscriptionDids: {
791
+ name: 'GetActivitySubscriptionDids',
792
+ I: GetActivitySubscriptionDidsRequest,
793
+ O: GetActivitySubscriptionDidsResponse,
794
+ kind: MethodKind.Unary,
795
+ },
796
+ /**
797
+ * @generated from rpc bsky.Service.GetActivitySubscriptionsByActorAndSubjects
798
+ */
799
+ getActivitySubscriptionsByActorAndSubjects: {
800
+ name: 'GetActivitySubscriptionsByActorAndSubjects',
801
+ I: GetActivitySubscriptionsByActorAndSubjectsRequest,
802
+ O: GetActivitySubscriptionsByActorAndSubjectsResponse,
803
+ kind: MethodKind.Unary,
804
+ },
772
805
  /**
773
806
  * @generated from rpc bsky.Service.UpdateNotificationSeen
774
807
  */