@atproto/bsky 0.0.155 → 0.0.157

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 (144) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/api/app/bsky/{unspecced/getPostThreadHiddenV2.d.ts → notification/getPreferences.d.ts} +1 -1
  3. package/dist/api/app/bsky/notification/getPreferences.d.ts.map +1 -0
  4. package/dist/api/app/bsky/notification/getPreferences.js +39 -0
  5. package/dist/api/app/bsky/notification/getPreferences.js.map +1 -0
  6. package/dist/api/app/bsky/notification/putPreferencesV2.d.ts +4 -0
  7. package/dist/api/app/bsky/notification/putPreferencesV2.d.ts.map +1 -0
  8. package/dist/api/app/bsky/notification/putPreferencesV2.js +47 -0
  9. package/dist/api/app/bsky/notification/putPreferencesV2.js.map +1 -0
  10. package/dist/api/app/bsky/notification/util.d.ts +9 -0
  11. package/dist/api/app/bsky/notification/util.d.ts.map +1 -0
  12. package/dist/api/app/bsky/notification/util.js +84 -0
  13. package/dist/api/app/bsky/notification/util.js.map +1 -0
  14. package/dist/api/app/bsky/unspecced/getPostThreadOtherV2.d.ts +4 -0
  15. package/dist/api/app/bsky/unspecced/getPostThreadOtherV2.d.ts.map +1 -0
  16. package/dist/api/app/bsky/unspecced/{getPostThreadHiddenV2.js → getPostThreadOtherV2.js} +5 -5
  17. package/dist/api/app/bsky/unspecced/getPostThreadOtherV2.js.map +1 -0
  18. package/dist/api/app/bsky/unspecced/getPostThreadV2.js +2 -2
  19. package/dist/api/app/bsky/unspecced/getPostThreadV2.js.map +1 -1
  20. package/dist/api/index.d.ts.map +1 -1
  21. package/dist/api/index.js +6 -2
  22. package/dist/api/index.js.map +1 -1
  23. package/dist/context.d.ts +3 -0
  24. package/dist/context.d.ts.map +1 -1
  25. package/dist/context.js +3 -0
  26. package/dist/context.js.map +1 -1
  27. package/dist/data-plane/bsync/index.d.ts.map +1 -1
  28. package/dist/data-plane/bsync/index.js +84 -0
  29. package/dist/data-plane/bsync/index.js.map +1 -1
  30. package/dist/data-plane/server/db/database-schema.d.ts +2 -1
  31. package/dist/data-plane/server/db/database-schema.d.ts.map +1 -1
  32. package/dist/data-plane/server/db/migrations/20250602T190357447Z-add-private-data.d.ts +4 -0
  33. package/dist/data-plane/server/db/migrations/20250602T190357447Z-add-private-data.d.ts.map +1 -0
  34. package/dist/data-plane/server/db/migrations/20250602T190357447Z-add-private-data.js +24 -0
  35. package/dist/data-plane/server/db/migrations/20250602T190357447Z-add-private-data.js.map +1 -0
  36. package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
  37. package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
  38. package/dist/data-plane/server/db/migrations/index.js +2 -1
  39. package/dist/data-plane/server/db/migrations/index.js.map +1 -1
  40. package/dist/data-plane/server/db/tables/private-data.d.ts +13 -0
  41. package/dist/data-plane/server/db/tables/private-data.d.ts.map +1 -0
  42. package/dist/data-plane/server/db/tables/private-data.js +5 -0
  43. package/dist/data-plane/server/db/tables/private-data.js.map +1 -0
  44. package/dist/data-plane/server/routes/index.d.ts.map +1 -1
  45. package/dist/data-plane/server/routes/index.js +2 -0
  46. package/dist/data-plane/server/routes/index.js.map +1 -1
  47. package/dist/data-plane/server/routes/private-data.d.ts +9 -0
  48. package/dist/data-plane/server/routes/private-data.d.ts.map +1 -0
  49. package/dist/data-plane/server/routes/private-data.js +63 -0
  50. package/dist/data-plane/server/routes/private-data.js.map +1 -0
  51. package/dist/data-plane/server/util.d.ts +6 -6
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +3 -0
  54. package/dist/index.js.map +1 -1
  55. package/dist/lexicon/index.d.ts +8 -4
  56. package/dist/lexicon/index.d.ts.map +1 -1
  57. package/dist/lexicon/index.js +14 -6
  58. package/dist/lexicon/index.js.map +1 -1
  59. package/dist/lexicon/lexicons.d.ts +518 -100
  60. package/dist/lexicon/lexicons.d.ts.map +1 -1
  61. package/dist/lexicon/lexicons.js +277 -52
  62. package/dist/lexicon/lexicons.js.map +1 -1
  63. package/dist/lexicon/types/app/bsky/notification/defs.d.ts +40 -0
  64. package/dist/lexicon/types/app/bsky/notification/defs.d.ts.map +1 -1
  65. package/dist/lexicon/types/app/bsky/notification/defs.js +36 -0
  66. package/dist/lexicon/types/app/bsky/notification/defs.js.map +1 -1
  67. package/dist/lexicon/types/app/bsky/notification/getPreferences.d.ts +35 -0
  68. package/dist/lexicon/types/app/bsky/notification/getPreferences.d.ts.map +1 -0
  69. package/dist/lexicon/types/app/bsky/notification/getPreferences.js +7 -0
  70. package/dist/lexicon/types/app/bsky/notification/getPreferences.js.map +1 -0
  71. package/dist/lexicon/types/app/bsky/notification/putPreferencesV2.d.ts +52 -0
  72. package/dist/lexicon/types/app/bsky/notification/putPreferencesV2.d.ts.map +1 -0
  73. package/dist/lexicon/types/app/bsky/notification/putPreferencesV2.js +7 -0
  74. package/dist/lexicon/types/app/bsky/notification/putPreferencesV2.js.map +1 -0
  75. package/dist/lexicon/types/app/bsky/unspecced/{getPostThreadHiddenV2.d.ts → getPostThreadOtherV2.d.ts} +7 -7
  76. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadOtherV2.d.ts.map +1 -0
  77. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadOtherV2.js +16 -0
  78. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadOtherV2.js.map +1 -0
  79. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadV2.d.ts +2 -2
  80. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadV2.d.ts.map +1 -1
  81. package/dist/proto/bsky_connect.d.ts +10 -1
  82. package/dist/proto/bsky_connect.d.ts.map +1 -1
  83. package/dist/proto/bsky_connect.js +9 -0
  84. package/dist/proto/bsky_connect.js.map +1 -1
  85. package/dist/proto/bsky_pb.d.ts +234 -0
  86. package/dist/proto/bsky_pb.d.ts.map +1 -1
  87. package/dist/proto/bsky_pb.js +693 -5
  88. package/dist/proto/bsky_pb.js.map +1 -1
  89. package/dist/proto/bsync_pb.d.ts +10 -10
  90. package/dist/proto/bsync_pb.d.ts.map +1 -1
  91. package/dist/proto/bsync_pb.js +15 -15
  92. package/dist/proto/bsync_pb.js.map +1 -1
  93. package/dist/stash.d.ts +26 -0
  94. package/dist/stash.d.ts.map +1 -0
  95. package/dist/stash.js +56 -0
  96. package/dist/stash.js.map +1 -0
  97. package/dist/views/index.d.ts +8 -8
  98. package/dist/views/index.d.ts.map +1 -1
  99. package/dist/views/index.js +32 -32
  100. package/dist/views/index.js.map +1 -1
  101. package/dist/views/threads-v2.d.ts +11 -11
  102. package/dist/views/threads-v2.d.ts.map +1 -1
  103. package/dist/views/threads-v2.js.map +1 -1
  104. package/package.json +4 -4
  105. package/proto/bsky.proto +61 -0
  106. package/src/api/app/bsky/notification/getPreferences.ts +50 -0
  107. package/src/api/app/bsky/notification/putPreferencesV2.ts +62 -0
  108. package/src/api/app/bsky/notification/util.ts +123 -0
  109. package/src/api/app/bsky/unspecced/{getPostThreadHiddenV2.ts → getPostThreadOtherV2.ts} +5 -5
  110. package/src/api/app/bsky/unspecced/getPostThreadV2.ts +2 -2
  111. package/src/api/index.ts +6 -2
  112. package/src/context.ts +6 -0
  113. package/src/data-plane/bsync/index.ts +109 -1
  114. package/src/data-plane/server/db/database-schema.ts +3 -1
  115. package/src/data-plane/server/db/migrations/20250602T190357447Z-add-private-data.ts +22 -0
  116. package/src/data-plane/server/db/migrations/index.ts +1 -0
  117. package/src/data-plane/server/db/tables/private-data.ts +13 -0
  118. package/src/data-plane/server/routes/index.ts +2 -0
  119. package/src/data-plane/server/routes/private-data.ts +90 -0
  120. package/src/index.ts +4 -0
  121. package/src/lexicon/index.ts +38 -14
  122. package/src/lexicon/lexicons.ts +281 -54
  123. package/src/lexicon/types/app/bsky/notification/defs.ts +76 -0
  124. package/src/lexicon/types/app/bsky/notification/getPreferences.ts +52 -0
  125. package/src/lexicon/types/app/bsky/notification/putPreferencesV2.ts +69 -0
  126. package/src/lexicon/types/app/bsky/unspecced/{getPostThreadHiddenV2.ts → getPostThreadOtherV2.ts} +10 -10
  127. package/src/lexicon/types/app/bsky/unspecced/getPostThreadV2.ts +2 -2
  128. package/src/proto/bsky_connect.ts +11 -0
  129. package/src/proto/bsky_pb.ts +669 -0
  130. package/src/proto/bsync_pb.ts +15 -15
  131. package/src/stash.ts +75 -0
  132. package/src/views/index.ts +46 -46
  133. package/src/views/threads-v2.ts +23 -23
  134. package/tests/stash.test.ts +156 -0
  135. package/tests/views/__snapshots__/thread-v2.test.ts.snap +7 -7
  136. package/tests/views/notifications.test.ts +221 -0
  137. package/tests/views/thread-v2.test.ts +93 -93
  138. package/tsconfig.build.tsbuildinfo +1 -1
  139. package/tsconfig.tests.tsbuildinfo +1 -1
  140. package/dist/api/app/bsky/unspecced/getPostThreadHiddenV2.d.ts.map +0 -1
  141. package/dist/api/app/bsky/unspecced/getPostThreadHiddenV2.js.map +0 -1
  142. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.d.ts.map +0 -1
  143. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.js +0 -16
  144. package/dist/lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2.js.map +0 -1
package/proto/bsky.proto CHANGED
@@ -753,6 +753,66 @@ message GetBlocklistSubscriptionsResponse {
753
753
  // Notifications
754
754
  //
755
755
 
756
+ message GetNotificationPreferencesRequest {
757
+ repeated string dids = 1;
758
+ }
759
+
760
+ message NotificationChannelList {
761
+ bool enabled = 1;
762
+ }
763
+
764
+ message NotificationChannelPush {
765
+ bool enabled = 1;
766
+ }
767
+
768
+ enum NotificationFilter {
769
+ NOTIFICATION_FILTER_UNSPECIFIED = 0;
770
+ NOTIFICATION_FILTER_ALL = 1;
771
+ NOTIFICATION_FILTER_FOLLOWS = 2;
772
+ }
773
+
774
+ message FilterableNotificationPreference {
775
+ NotificationFilter filter = 1;
776
+ NotificationChannelList list = 2;
777
+ NotificationChannelPush push = 3;
778
+ }
779
+
780
+ message NotificationPreference {
781
+ NotificationChannelList list = 1;
782
+ NotificationChannelPush push = 2;
783
+ }
784
+
785
+ enum ChatNotificationFilter {
786
+ CHAT_NOTIFICATION_FILTER_UNSPECIFIED = 0;
787
+ CHAT_NOTIFICATION_FILTER_ALL = 1;
788
+ CHAT_NOTIFICATION_FILTER_ACCEPTED = 2;
789
+ }
790
+
791
+ message ChatNotificationPreference {
792
+ ChatNotificationFilter filter = 1;
793
+ NotificationChannelPush push = 2;
794
+ }
795
+
796
+ message NotificationPreferences {
797
+ ChatNotificationPreference chat = 1;
798
+ FilterableNotificationPreference follow = 2;
799
+ FilterableNotificationPreference like = 3;
800
+ FilterableNotificationPreference like_via_repost = 4;
801
+ FilterableNotificationPreference mention = 5;
802
+ FilterableNotificationPreference quote = 6;
803
+ FilterableNotificationPreference reply = 7;
804
+ FilterableNotificationPreference repost = 8;
805
+ FilterableNotificationPreference repost_via_repost = 9;
806
+ NotificationPreference starterpack_joined = 10;
807
+ NotificationPreference subscribed_post = 11;
808
+ NotificationPreference unverified = 12;
809
+ NotificationPreference verified = 13;
810
+ }
811
+
812
+ message GetNotificationPreferencesResponse {
813
+ repeated NotificationPreferences preferences = 1;
814
+ }
815
+
756
816
  // - list recent notifications for a user
757
817
  // - notifications should include a uri for the record that caused the notif & a “reason” for the notification (reply, like, quotepost, etc)
758
818
  // - this should include both read & unread notifs
@@ -1256,6 +1316,7 @@ service Service {
1256
1316
  rpc GetBlocklistSubscriptions(GetBlocklistSubscriptionsRequest) returns (GetBlocklistSubscriptionsResponse);
1257
1317
 
1258
1318
  // Notifications
1319
+ rpc GetNotificationPreferences(GetNotificationPreferencesRequest) returns (GetNotificationPreferencesResponse);
1259
1320
  rpc GetNotifications(GetNotificationsRequest) returns (GetNotificationsResponse);
1260
1321
  rpc GetNotificationSeen(GetNotificationSeenRequest) returns (GetNotificationSeenResponse);
1261
1322
  rpc GetUnreadNotificationCount(GetUnreadNotificationCountRequest) returns (GetUnreadNotificationCountResponse);
@@ -0,0 +1,50 @@
1
+ import assert from 'node:assert'
2
+ import { Un$Typed } from '@atproto/api'
3
+ import { UpstreamFailureError } from '@atproto/xrpc-server'
4
+ import { AppContext } from '../../../../context'
5
+ import { Server } from '../../../../lexicon'
6
+ import { Preferences } from '../../../../lexicon/types/app/bsky/notification/defs'
7
+ import { GetNotificationPreferencesResponse } from '../../../../proto/bsky_pb'
8
+ import { protobufToLex } from './util'
9
+
10
+ export default function (server: Server, ctx: AppContext) {
11
+ server.app.bsky.notification.getPreferences({
12
+ auth: ctx.authVerifier.standard,
13
+ handler: async ({ auth }) => {
14
+ const actorDid = auth.credentials.iss
15
+ const preferences = await computePreferences(ctx, actorDid)
16
+ return {
17
+ encoding: 'application/json',
18
+ body: {
19
+ preferences,
20
+ },
21
+ }
22
+ },
23
+ })
24
+ }
25
+
26
+ const computePreferences = async (
27
+ ctx: AppContext,
28
+ actorDid: string,
29
+ ): Promise<Un$Typed<Preferences>> => {
30
+ let res: GetNotificationPreferencesResponse
31
+ try {
32
+ res = await ctx.dataplane.getNotificationPreferences({
33
+ dids: [actorDid],
34
+ })
35
+ } catch (err) {
36
+ throw new UpstreamFailureError(
37
+ 'cannot get current notification preferences',
38
+ 'NotificationPreferencesFailed',
39
+ { cause: err },
40
+ )
41
+ }
42
+
43
+ assert(
44
+ res.preferences.length === 1,
45
+ `expected exactly one preferences entry, got ${res.preferences.length}`,
46
+ )
47
+
48
+ const currentPreferences = protobufToLex(res.preferences[0])
49
+ return currentPreferences
50
+ }
@@ -0,0 +1,62 @@
1
+ import assert from 'node:assert'
2
+ import { Un$Typed } from '@atproto/api'
3
+ import { UpstreamFailureError } from '@atproto/xrpc-server'
4
+ import { AppContext } from '../../../../context'
5
+ import { Server } from '../../../../lexicon'
6
+ import { Preferences } from '../../../../lexicon/types/app/bsky/notification/defs'
7
+ import { HandlerInput } from '../../../../lexicon/types/app/bsky/notification/putPreferencesV2'
8
+ import { GetNotificationPreferencesResponse } from '../../../../proto/bsky_pb'
9
+ import { protobufToLex } from './util'
10
+
11
+ export default function (server: Server, ctx: AppContext) {
12
+ server.app.bsky.notification.putPreferencesV2({
13
+ auth: ctx.authVerifier.standard,
14
+ handler: async ({ auth, input }) => {
15
+ const actorDid = auth.credentials.iss
16
+ const preferences = await computePreferences(ctx, actorDid, input)
17
+
18
+ // Notification preferences are created automatically on the dataplane on signup, so we just update.
19
+ await ctx.stashClient.update({
20
+ actorDid,
21
+ namespace: 'app.bsky.notification.defs#preferences',
22
+ key: 'self',
23
+ payload: preferences,
24
+ })
25
+
26
+ return {
27
+ encoding: 'application/json',
28
+ body: {
29
+ preferences,
30
+ },
31
+ }
32
+ },
33
+ })
34
+ }
35
+
36
+ const computePreferences = async (
37
+ ctx: AppContext,
38
+ actorDid: string,
39
+ input: HandlerInput,
40
+ ): Promise<Un$Typed<Preferences>> => {
41
+ let res: GetNotificationPreferencesResponse
42
+ try {
43
+ res = await ctx.dataplane.getNotificationPreferences({
44
+ dids: [actorDid],
45
+ })
46
+ } catch (err) {
47
+ throw new UpstreamFailureError(
48
+ 'cannot get current notification preferences',
49
+ 'NotificationPreferencesFailed',
50
+ { cause: err },
51
+ )
52
+ }
53
+
54
+ assert(
55
+ res.preferences.length === 1,
56
+ `expected exactly one preferences entry, got ${res.preferences.length}`,
57
+ )
58
+
59
+ const currentPreferences = protobufToLex(res.preferences[0])
60
+ const preferences = { ...currentPreferences, ...input.body }
61
+ return preferences
62
+ }
@@ -0,0 +1,123 @@
1
+ import { Un$Typed } from '@atproto/api'
2
+ import {
3
+ ChatPreference,
4
+ FilterablePreference,
5
+ Preference,
6
+ Preferences,
7
+ } from '../../../../lexicon/types/app/bsky/notification/defs'
8
+ import {
9
+ ChatNotificationFilter,
10
+ ChatNotificationPreference,
11
+ FilterableNotificationPreference,
12
+ NotificationFilter,
13
+ NotificationPreference,
14
+ NotificationPreferences,
15
+ } from '../../../../proto/bsky_pb'
16
+
17
+ type DeepPartial<T> = T extends object
18
+ ? {
19
+ [P in keyof T]?: DeepPartial<T[P]>
20
+ }
21
+ : T
22
+
23
+ const ensureChatPreference = (
24
+ p?: DeepPartial<ChatPreference>,
25
+ ): ChatPreference => {
26
+ const filters = ['all', 'accepted']
27
+ return {
28
+ filter:
29
+ typeof p?.filter === 'string' && filters.includes(p.filter)
30
+ ? p.filter
31
+ : 'all',
32
+ push: p?.push ?? true,
33
+ }
34
+ }
35
+
36
+ const ensureFilterablePreference = (
37
+ p?: DeepPartial<FilterablePreference>,
38
+ ): FilterablePreference => {
39
+ const filters = ['all', 'follows']
40
+ return {
41
+ filter:
42
+ typeof p?.filter === 'string' && filters.includes(p.filter)
43
+ ? p.filter
44
+ : 'all',
45
+ list: p?.list ?? true,
46
+ push: p?.push ?? true,
47
+ }
48
+ }
49
+
50
+ const ensurePreference = (p?: DeepPartial<Preference>): Preference => {
51
+ return {
52
+ list: p?.list ?? true,
53
+ push: p?.push ?? true,
54
+ }
55
+ }
56
+
57
+ const ensurePreferences = (
58
+ p: DeepPartial<Preferences>,
59
+ ): Un$Typed<Preferences> => {
60
+ return {
61
+ chat: ensureChatPreference(p.chat),
62
+ follow: ensureFilterablePreference(p.follow),
63
+ like: ensureFilterablePreference(p.like),
64
+ likeViaRepost: ensureFilterablePreference(p.likeViaRepost),
65
+ mention: ensureFilterablePreference(p.mention),
66
+ quote: ensureFilterablePreference(p.quote),
67
+ reply: ensureFilterablePreference(p.reply),
68
+ repost: ensureFilterablePreference(p.repost),
69
+ repostViaRepost: ensureFilterablePreference(p.repostViaRepost),
70
+ starterpackJoined: ensurePreference(p.starterpackJoined),
71
+ subscribedPost: ensurePreference(p.subscribedPost),
72
+ unverified: ensurePreference(p.unverified),
73
+ verified: ensurePreference(p.verified),
74
+ }
75
+ }
76
+
77
+ const protobufChatPreferenceToLex = (
78
+ p?: DeepPartial<ChatNotificationPreference>,
79
+ ): DeepPartial<ChatPreference> => {
80
+ return {
81
+ filter: p?.filter === ChatNotificationFilter.ACCEPTED ? 'accepted' : 'all',
82
+ push: p?.push?.enabled,
83
+ }
84
+ }
85
+
86
+ const protobufFilterablePreferenceToLex = (
87
+ p?: DeepPartial<FilterableNotificationPreference>,
88
+ ): DeepPartial<FilterablePreference> => {
89
+ return {
90
+ filter: p?.filter === NotificationFilter.FOLLOWS ? 'follows' : 'all',
91
+ list: p?.list?.enabled,
92
+ push: p?.push?.enabled,
93
+ }
94
+ }
95
+
96
+ const protobufPreferenceToLex = (
97
+ p?: DeepPartial<NotificationPreference>,
98
+ ): DeepPartial<Preference> => {
99
+ return {
100
+ list: p?.list?.enabled,
101
+ push: p?.push?.enabled,
102
+ }
103
+ }
104
+
105
+ export const protobufToLex = (
106
+ res: DeepPartial<NotificationPreferences>,
107
+ ): Un$Typed<Preferences> => {
108
+ return ensurePreferences({
109
+ chat: protobufChatPreferenceToLex(res.chat),
110
+ follow: protobufFilterablePreferenceToLex(res.follow),
111
+ like: protobufFilterablePreferenceToLex(res.like),
112
+ likeViaRepost: protobufFilterablePreferenceToLex(res.likeViaRepost),
113
+ mention: protobufFilterablePreferenceToLex(res.mention),
114
+ quote: protobufFilterablePreferenceToLex(res.quote),
115
+ reply: protobufFilterablePreferenceToLex(res.reply),
116
+ repost: protobufFilterablePreferenceToLex(res.repost),
117
+ repostViaRepost: protobufFilterablePreferenceToLex(res.repostViaRepost),
118
+ starterpackJoined: protobufPreferenceToLex(res.starterpackJoined),
119
+ subscribedPost: protobufPreferenceToLex(res.subscribedPost),
120
+ unverified: protobufPreferenceToLex(res.unverified),
121
+ verified: protobufPreferenceToLex(res.verified),
122
+ })
123
+ }
@@ -3,7 +3,7 @@ import { AppContext } from '../../../../context'
3
3
  import { Code, DataPlaneClient, isDataplaneError } from '../../../../data-plane'
4
4
  import { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'
5
5
  import { Server } from '../../../../lexicon'
6
- import { QueryParams } from '../../../../lexicon/types/app/bsky/unspecced/getPostThreadHiddenV2'
6
+ import { QueryParams } from '../../../../lexicon/types/app/bsky/unspecced/getPostThreadOtherV2'
7
7
  import {
8
8
  HydrationFnInput,
9
9
  PresentationFnInput,
@@ -25,13 +25,13 @@ const BELOW = 1
25
25
  const BRANCHING_FACTOR = 0
26
26
 
27
27
  export default function (server: Server, ctx: AppContext) {
28
- const getPostThreadHidden = createPipeline(
28
+ const getPostThreadOther = createPipeline(
29
29
  skeleton,
30
30
  hydration,
31
31
  noRules, // handled in presentation: 3p block-violating replies are turned to #blockedPost, viewer blocks turned to #notFoundPost.
32
32
  presentation,
33
33
  )
34
- server.app.bsky.unspecced.getPostThreadHiddenV2({
34
+ server.app.bsky.unspecced.getPostThreadOtherV2({
35
35
  auth: ctx.authVerifier.optionalStandardOrRole,
36
36
  handler: async ({ params, auth, req }) => {
37
37
  const { viewer, includeTakedowns, include3pBlocks } =
@@ -46,7 +46,7 @@ export default function (server: Server, ctx: AppContext) {
46
46
 
47
47
  return {
48
48
  encoding: 'application/json',
49
- body: await getPostThreadHidden({ ...params, hydrateCtx }, ctx),
49
+ body: await getPostThreadOther({ ...params, hydrateCtx }, ctx),
50
50
  headers: resHeaders({
51
51
  labelers: hydrateCtx.labelers,
52
52
  }),
@@ -94,7 +94,7 @@ const presentation = (
94
94
  inputs: PresentationFnInput<Context, Params, Skeleton>,
95
95
  ) => {
96
96
  const { ctx, params, skeleton, hydration } = inputs
97
- const thread = ctx.views.threadHiddenV2(skeleton, hydration, {
97
+ const thread = ctx.views.threadOtherV2(skeleton, hydration, {
98
98
  below: BELOW,
99
99
  branchingFactor: BRANCHING_FACTOR,
100
100
  prioritizeFollowedUsers: params.prioritizeFollowedUsers,
@@ -85,7 +85,7 @@ const presentation = (
85
85
  inputs: PresentationFnInput<Context, Params, Skeleton>,
86
86
  ) => {
87
87
  const { ctx, params, skeleton, hydration } = inputs
88
- const { hasHiddenReplies, thread } = ctx.views.threadV2(skeleton, hydration, {
88
+ const { hasOtherReplies, thread } = ctx.views.threadV2(skeleton, hydration, {
89
89
  above: calculateAbove(ctx, params),
90
90
  below: calculateBelow(ctx, skeleton.anchor, params),
91
91
  branchingFactor: params.branchingFactor,
@@ -100,7 +100,7 @@ const presentation = (
100
100
  postUriToThreadgateUri(rootUri),
101
101
  hydration,
102
102
  )
103
- return { hasHiddenReplies, thread, threadgate }
103
+ return { hasOtherReplies, thread, threadgate }
104
104
  }
105
105
 
106
106
  type Context = {
package/src/api/index.ts CHANGED
@@ -42,14 +42,16 @@ import unmuteActor from './app/bsky/graph/unmuteActor'
42
42
  import unmuteActorList from './app/bsky/graph/unmuteActorList'
43
43
  import unmuteThread from './app/bsky/graph/unmuteThread'
44
44
  import getLabelerServices from './app/bsky/labeler/getServices'
45
+ import getPreferences from './app/bsky/notification/getPreferences'
45
46
  import getUnreadCount from './app/bsky/notification/getUnreadCount'
46
47
  import listNotifications from './app/bsky/notification/listNotifications'
47
48
  import putPreferences from './app/bsky/notification/putPreferences'
49
+ import putPreferencesV2 from './app/bsky/notification/putPreferencesV2'
48
50
  import registerPush from './app/bsky/notification/registerPush'
49
51
  import updateSeen from './app/bsky/notification/updateSeen'
50
52
  import getConfig from './app/bsky/unspecced/getConfig'
51
53
  import getPopularFeedGenerators from './app/bsky/unspecced/getPopularFeedGenerators'
52
- import getPostThreadHiddenV2 from './app/bsky/unspecced/getPostThreadHiddenV2'
54
+ import getPostThreadOtherV2 from './app/bsky/unspecced/getPostThreadOtherV2'
53
55
  import getPostThreadV2 from './app/bsky/unspecced/getPostThreadV2'
54
56
  import getUnspeccedSuggestedFeeds from './app/bsky/unspecced/getSuggestedFeeds'
55
57
  import getSuggestedStarterPacks from './app/bsky/unspecced/getSuggestedStarterPacks'
@@ -84,7 +86,7 @@ export default function (server: Server, ctx: AppContext) {
84
86
  getListFeed(server, ctx)
85
87
  getQuotes(server, ctx)
86
88
  getPostThread(server, ctx)
87
- getPostThreadHiddenV2(server, ctx)
89
+ getPostThreadOtherV2(server, ctx)
88
90
  getPostThreadV2(server, ctx)
89
91
  getPosts(server, ctx)
90
92
  searchPosts(server, ctx)
@@ -122,10 +124,12 @@ export default function (server: Server, ctx: AppContext) {
122
124
  searchActors(server, ctx)
123
125
  searchActorsTypeahead(server, ctx)
124
126
  getSuggestions(server, ctx)
127
+ getPreferences(server, ctx)
125
128
  getUnreadCount(server, ctx)
126
129
  listNotifications(server, ctx)
127
130
  updateSeen(server, ctx)
128
131
  putPreferences(server, ctx)
132
+ putPreferencesV2(server, ctx)
129
133
  registerPush(server, ctx)
130
134
  getConfig(server, ctx)
131
135
  getPopularFeedGenerators(server, ctx)
package/src/context.ts CHANGED
@@ -13,6 +13,7 @@ import { DataPlaneClient, HostList } from './data-plane/client'
13
13
  import { FeatureGates } from './feature-gates'
14
14
  import { Hydrator } from './hydration/hydrator'
15
15
  import { httpLogger as log } from './logger'
16
+ import { StashClient } from './stash'
16
17
  import {
17
18
  ParsedLabelers,
18
19
  defaultLabelerHeader,
@@ -35,6 +36,7 @@ export class AppContext {
35
36
  signingKey: Keypair
36
37
  idResolver: IdResolver
37
38
  bsyncClient: BsyncClient
39
+ stashClient: StashClient
38
40
  courierClient: CourierClient | undefined
39
41
  authVerifier: AuthVerifier
40
42
  featureGates: FeatureGates
@@ -94,6 +96,10 @@ export class AppContext {
94
96
  return this.opts.bsyncClient
95
97
  }
96
98
 
99
+ get stashClient(): StashClient {
100
+ return this.opts.stashClient
101
+ }
102
+
97
103
  get courierClient(): CourierClient | undefined {
98
104
  return this.opts.courierClient
99
105
  }
@@ -4,10 +4,15 @@ import http from 'node:http'
4
4
  import { ConnectRouter } from '@connectrpc/connect'
5
5
  import { expressConnectMiddleware } from '@connectrpc/connect-express'
6
6
  import express from 'express'
7
+ import { TID } from '@atproto/common'
7
8
  import { AtUri } from '@atproto/syntax'
8
9
  import { ids } from '../../lexicon/lexicons'
9
10
  import { Service } from '../../proto/bsync_connect'
10
- import { MuteOperation_Type } from '../../proto/bsync_pb'
11
+ import {
12
+ Method,
13
+ MuteOperation_Type,
14
+ PutOperationRequest,
15
+ } from '../../proto/bsync_pb'
11
16
  import { Database } from '../server/db'
12
17
 
13
18
  export class MockBsync {
@@ -138,7 +143,110 @@ const createRoutes = (db: Database) => (router: ConnectRouter) =>
138
143
  throw new Error('not implemented')
139
144
  },
140
145
 
146
+ async putOperation(req) {
147
+ const { actorDid, namespace, key, method, payload } = req
148
+ if (
149
+ method !== Method.CREATE &&
150
+ method !== Method.UPDATE &&
151
+ method !== Method.DELETE
152
+ ) {
153
+ throw new Error(`Unsupported method: ${method}`)
154
+ }
155
+
156
+ const now = new Date().toISOString()
157
+ if (namespace === 'app.bsky.notification.defs#preferences') {
158
+ await handleNotificationPreferencesOperation(db, req, now)
159
+ } else {
160
+ await handleGenericOperation(db, req, now)
161
+ }
162
+
163
+ return {
164
+ operation: {
165
+ id: TID.nextStr(),
166
+ actorDid,
167
+ namespace,
168
+ key,
169
+ method,
170
+ payload,
171
+ },
172
+ }
173
+ },
174
+
175
+ async scanOperations() {
176
+ throw new Error('not implemented')
177
+ },
178
+
141
179
  async ping() {
142
180
  return {}
143
181
  },
144
182
  })
183
+
184
+ const handleNotificationPreferencesOperation = async (
185
+ db: Database,
186
+ req: PutOperationRequest,
187
+ now: string,
188
+ ) => {
189
+ const { actorDid, namespace, key, method, payload } = req
190
+ if (method === Method.CREATE || method === Method.UPDATE) {
191
+ return db.db
192
+ .insertInto('private_data')
193
+ .values({
194
+ actorDid,
195
+ namespace,
196
+ key,
197
+ payload: Buffer.from(payload).toString('utf8'),
198
+ indexedAt: now,
199
+ updatedAt: now,
200
+ })
201
+ .onConflict((oc) =>
202
+ oc.columns(['actorDid', 'namespace', 'key']).doUpdateSet({
203
+ payload: Buffer.from(payload).toString('utf8'),
204
+ updatedAt: now,
205
+ }),
206
+ )
207
+ .execute()
208
+ }
209
+
210
+ return handleGenericOperation(db, req, now)
211
+ }
212
+
213
+ const handleGenericOperation = async (
214
+ db: Database,
215
+ req: PutOperationRequest,
216
+ now: string,
217
+ ) => {
218
+ const { actorDid, namespace, key, method, payload } = req
219
+ if (method === Method.CREATE) {
220
+ return db.db
221
+ .insertInto('private_data')
222
+ .values({
223
+ actorDid,
224
+ namespace,
225
+ key,
226
+ payload: Buffer.from(payload).toString('utf8'),
227
+ indexedAt: now,
228
+ updatedAt: now,
229
+ })
230
+ .execute()
231
+ }
232
+
233
+ if (method === Method.UPDATE) {
234
+ return db.db
235
+ .updateTable('private_data')
236
+ .where('actorDid', '=', actorDid)
237
+ .where('namespace', '=', namespace)
238
+ .where('key', '=', key)
239
+ .set({
240
+ payload: Buffer.from(payload).toString('utf8'),
241
+ updatedAt: now,
242
+ })
243
+ .execute()
244
+ }
245
+
246
+ return db.db
247
+ .deleteFrom('private_data')
248
+ .where('actorDid', '=', actorDid)
249
+ .where('namespace', '=', namespace)
250
+ .where('key', '=', key)
251
+ .execute()
252
+ }
@@ -24,6 +24,7 @@ import * as post from './tables/post'
24
24
  import * as postAgg from './tables/post-agg'
25
25
  import * as postEmbed from './tables/post-embed'
26
26
  import * as postgate from './tables/post-gate'
27
+ import * as privateData from './tables/private-data'
27
28
  import * as profile from './tables/profile'
28
29
  import * as profileAgg from './tables/profile-agg'
29
30
  import * as quote from './tables/quote'
@@ -77,6 +78,7 @@ export type DatabaseSchemaType = duplicateRecord.PartialDB &
77
78
  starterPack.PartialDB &
78
79
  taggedSuggestion.PartialDB &
79
80
  quote.PartialDB &
80
- verification.PartialDB
81
+ verification.PartialDB &
82
+ privateData.PartialDB
81
83
 
82
84
  export type DatabaseSchema = Kysely<DatabaseSchemaType>
@@ -0,0 +1,22 @@
1
+ import { Kysely } from 'kysely'
2
+
3
+ export async function up(db: Kysely<unknown>): Promise<void> {
4
+ await db.schema
5
+ .createTable('private_data')
6
+ .addColumn('actorDid', 'varchar', (col) => col.notNull())
7
+ .addColumn('namespace', 'varchar', (col) => col.notNull())
8
+ .addColumn('key', 'varchar', (col) => col.notNull())
9
+ .addColumn('payload', 'text', (col) => col.notNull())
10
+ .addColumn('indexedAt', 'varchar', (col) => col.notNull())
11
+ .addColumn('updatedAt', 'varchar', (col) => col.notNull())
12
+ .addPrimaryKeyConstraint('private_data_pkey', [
13
+ 'actorDid',
14
+ 'namespace',
15
+ 'key',
16
+ ])
17
+ .execute()
18
+ }
19
+
20
+ export async function down(db: Kysely<unknown>): Promise<void> {
21
+ await db.schema.dropTable('private_data').execute()
22
+ }
@@ -50,3 +50,4 @@ export * as _20250207T174822012Z from './20250207T174822012Z-add-label-exp'
50
50
  export * as _20250404T163421487Z from './20250404T163421487Z-verifications'
51
51
  export * as _20250526T023712742Z from './20250526T023712742Z-like-repost-via'
52
52
  export * as _20250528T221913281Z from './20250528T221913281Z-add-record-tags'
53
+ export * as _20250602T190357447Z from './20250602T190357447Z-add-private-data'
@@ -0,0 +1,13 @@
1
+ export interface PrivateData {
2
+ actorDid: string
3
+ namespace: string
4
+ key: string
5
+ // JSON-encoded
6
+ payload: string
7
+ indexedAt: string
8
+ updatedAt: string
9
+ }
10
+
11
+ export const tableName = 'private_data'
12
+
13
+ export type PartialDB = { [tableName]: PrivateData }
@@ -14,6 +14,7 @@ import lists from './lists'
14
14
  import moderation from './moderation'
15
15
  import mutes from './mutes'
16
16
  import notifs from './notifs'
17
+ import privateData from './private-data'
17
18
  import profile from './profile'
18
19
  import quotes from './quotes'
19
20
  import records from './records'
@@ -40,6 +41,7 @@ export default (db: Database, idResolver: IdResolver) =>
40
41
  ...moderation(db),
41
42
  ...mutes(db),
42
43
  ...notifs(db),
44
+ ...privateData(db),
43
45
  ...profile(db),
44
46
  ...quotes(db),
45
47
  ...records(db),