@atproto/bsky 0.0.76 → 0.0.77

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 (219) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/api/app/bsky/feed/getActorLikes.js +2 -2
  3. package/dist/api/app/bsky/feed/getActorLikes.js.map +1 -1
  4. package/dist/api/app/bsky/feed/getLikes.js +6 -6
  5. package/dist/api/app/bsky/feed/getLikes.js.map +1 -1
  6. package/dist/api/app/bsky/feed/getPosts.js +4 -4
  7. package/dist/api/app/bsky/feed/getPosts.js.map +1 -1
  8. package/dist/api/app/bsky/feed/getQuotes.d.ts +4 -0
  9. package/dist/api/app/bsky/feed/getQuotes.d.ts.map +1 -0
  10. package/dist/api/app/bsky/feed/getQuotes.js +67 -0
  11. package/dist/api/app/bsky/feed/getQuotes.js.map +1 -0
  12. package/dist/api/app/bsky/feed/getRepostedBy.js +6 -6
  13. package/dist/api/app/bsky/feed/getRepostedBy.js.map +1 -1
  14. package/dist/api/app/bsky/feed/searchPosts.js +4 -4
  15. package/dist/api/app/bsky/feed/searchPosts.js.map +1 -1
  16. package/dist/api/app/bsky/graph/getFollowers.js +8 -8
  17. package/dist/api/app/bsky/graph/getFollowers.js.map +1 -1
  18. package/dist/api/app/bsky/graph/getList.js +3 -3
  19. package/dist/api/app/bsky/graph/getList.js.map +1 -1
  20. package/dist/api/app/bsky/notification/listNotifications.d.ts.map +1 -1
  21. package/dist/api/app/bsky/notification/listNotifications.js +29 -8
  22. package/dist/api/app/bsky/notification/listNotifications.js.map +1 -1
  23. package/dist/api/index.d.ts.map +1 -1
  24. package/dist/api/index.js +2 -0
  25. package/dist/api/index.js.map +1 -1
  26. package/dist/data-plane/server/db/database-schema.d.ts +4 -2
  27. package/dist/data-plane/server/db/database-schema.d.ts.map +1 -1
  28. package/dist/data-plane/server/db/migrations/20240723T220700077Z-quotes-post-aggs.d.ts +4 -0
  29. package/dist/data-plane/server/db/migrations/20240723T220700077Z-quotes-post-aggs.d.ts.map +1 -0
  30. package/dist/data-plane/server/db/migrations/20240723T220700077Z-quotes-post-aggs.js +15 -0
  31. package/dist/data-plane/server/db/migrations/20240723T220700077Z-quotes-post-aggs.js.map +1 -0
  32. package/dist/data-plane/server/db/migrations/20240723T220703655Z-quotes.d.ts +4 -0
  33. package/dist/data-plane/server/db/migrations/20240723T220703655Z-quotes.d.ts.map +1 -0
  34. package/dist/data-plane/server/db/migrations/20240723T220703655Z-quotes.js +30 -0
  35. package/dist/data-plane/server/db/migrations/20240723T220703655Z-quotes.js.map +1 -0
  36. package/dist/data-plane/server/db/migrations/20240801T193939827Z-post-gate.d.ts +4 -0
  37. package/dist/data-plane/server/db/migrations/20240801T193939827Z-post-gate.d.ts.map +1 -0
  38. package/dist/data-plane/server/db/migrations/20240801T193939827Z-post-gate.js +20 -0
  39. package/dist/data-plane/server/db/migrations/20240801T193939827Z-post-gate.js.map +1 -0
  40. package/dist/data-plane/server/db/migrations/20240808T224251220Z-post-gate-flags.d.ts +4 -0
  41. package/dist/data-plane/server/db/migrations/20240808T224251220Z-post-gate-flags.d.ts.map +1 -0
  42. package/dist/data-plane/server/db/migrations/20240808T224251220Z-post-gate-flags.js +28 -0
  43. package/dist/data-plane/server/db/migrations/20240808T224251220Z-post-gate-flags.js.map +1 -0
  44. package/dist/data-plane/server/db/migrations/index.d.ts +4 -0
  45. package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
  46. package/dist/data-plane/server/db/migrations/index.js +5 -1
  47. package/dist/data-plane/server/db/migrations/index.js.map +1 -1
  48. package/dist/data-plane/server/db/tables/post-agg.d.ts +1 -0
  49. package/dist/data-plane/server/db/tables/post-agg.d.ts.map +1 -1
  50. package/dist/data-plane/server/db/tables/post-gate.d.ts +14 -0
  51. package/dist/data-plane/server/db/tables/post-gate.d.ts.map +1 -0
  52. package/dist/data-plane/server/db/tables/post-gate.js +4 -0
  53. package/dist/data-plane/server/db/tables/post-gate.js.map +1 -0
  54. package/dist/data-plane/server/db/tables/post.d.ts +3 -0
  55. package/dist/data-plane/server/db/tables/post.d.ts.map +1 -1
  56. package/dist/data-plane/server/db/tables/quote.d.ts +16 -0
  57. package/dist/data-plane/server/db/tables/quote.d.ts.map +1 -0
  58. package/dist/data-plane/server/db/tables/quote.js +4 -0
  59. package/dist/data-plane/server/db/tables/quote.js.map +1 -0
  60. package/dist/data-plane/server/indexing/index.d.ts +2 -0
  61. package/dist/data-plane/server/indexing/index.d.ts.map +1 -1
  62. package/dist/data-plane/server/indexing/index.js +6 -0
  63. package/dist/data-plane/server/indexing/index.js.map +1 -1
  64. package/dist/data-plane/server/indexing/plugins/post-gate.d.ts +10 -0
  65. package/dist/data-plane/server/indexing/plugins/post-gate.d.ts.map +1 -0
  66. package/dist/data-plane/server/indexing/plugins/post-gate.js +101 -0
  67. package/dist/data-plane/server/indexing/plugins/post-gate.js.map +1 -0
  68. package/dist/data-plane/server/indexing/plugins/post.d.ts +2 -0
  69. package/dist/data-plane/server/indexing/plugins/post.d.ts.map +1 -1
  70. package/dist/data-plane/server/indexing/plugins/post.js +122 -15
  71. package/dist/data-plane/server/indexing/plugins/post.js.map +1 -1
  72. package/dist/data-plane/server/indexing/plugins/thread-gate.d.ts.map +1 -1
  73. package/dist/data-plane/server/indexing/plugins/thread-gate.js +12 -0
  74. package/dist/data-plane/server/indexing/plugins/thread-gate.js.map +1 -1
  75. package/dist/data-plane/server/routes/index.d.ts.map +1 -1
  76. package/dist/data-plane/server/routes/index.js +2 -0
  77. package/dist/data-plane/server/routes/index.js.map +1 -1
  78. package/dist/data-plane/server/routes/interactions.d.ts.map +1 -1
  79. package/dist/data-plane/server/routes/interactions.js +2 -1
  80. package/dist/data-plane/server/routes/interactions.js.map +1 -1
  81. package/dist/data-plane/server/routes/quotes.d.ts +6 -0
  82. package/dist/data-plane/server/routes/quotes.d.ts.map +1 -0
  83. package/dist/data-plane/server/routes/quotes.js +27 -0
  84. package/dist/data-plane/server/routes/quotes.js.map +1 -0
  85. package/dist/data-plane/server/routes/records.d.ts.map +1 -1
  86. package/dist/data-plane/server/routes/records.js +11 -1
  87. package/dist/data-plane/server/routes/records.js.map +1 -1
  88. package/dist/data-plane/server/util.d.ts +6 -7
  89. package/dist/data-plane/server/util.d.ts.map +1 -1
  90. package/dist/data-plane/server/util.js +1 -9
  91. package/dist/data-plane/server/util.js.map +1 -1
  92. package/dist/hydration/feed.d.ts +10 -0
  93. package/dist/hydration/feed.d.ts.map +1 -1
  94. package/dist/hydration/feed.js +31 -7
  95. package/dist/hydration/feed.js.map +1 -1
  96. package/dist/hydration/hydrator.d.ts +4 -2
  97. package/dist/hydration/hydrator.d.ts.map +1 -1
  98. package/dist/hydration/hydrator.js +89 -34
  99. package/dist/hydration/hydrator.js.map +1 -1
  100. package/dist/hydration/util.d.ts +0 -1
  101. package/dist/hydration/util.d.ts.map +1 -1
  102. package/dist/hydration/util.js +1 -5
  103. package/dist/hydration/util.js.map +1 -1
  104. package/dist/lexicon/index.d.ts +2 -0
  105. package/dist/lexicon/index.d.ts.map +1 -1
  106. package/dist/lexicon/index.js +4 -0
  107. package/dist/lexicon/index.js.map +1 -1
  108. package/dist/lexicon/lexicons.d.ts +141 -0
  109. package/dist/lexicon/lexicons.d.ts.map +1 -1
  110. package/dist/lexicon/lexicons.js +142 -0
  111. package/dist/lexicon/lexicons.js.map +1 -1
  112. package/dist/lexicon/types/app/bsky/embed/record.d.ts +8 -1
  113. package/dist/lexicon/types/app/bsky/embed/record.d.ts.map +1 -1
  114. package/dist/lexicon/types/app/bsky/embed/record.js +11 -1
  115. package/dist/lexicon/types/app/bsky/embed/record.js.map +1 -1
  116. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +2 -0
  117. package/dist/lexicon/types/app/bsky/feed/defs.d.ts.map +1 -1
  118. package/dist/lexicon/types/app/bsky/feed/defs.js.map +1 -1
  119. package/dist/lexicon/types/app/bsky/feed/getQuotes.d.ts +44 -0
  120. package/dist/lexicon/types/app/bsky/feed/getQuotes.d.ts.map +1 -0
  121. package/dist/lexicon/types/app/bsky/feed/getQuotes.js +3 -0
  122. package/dist/lexicon/types/app/bsky/feed/getQuotes.js.map +1 -0
  123. package/dist/lexicon/types/app/bsky/feed/postgate.d.ts +25 -0
  124. package/dist/lexicon/types/app/bsky/feed/postgate.d.ts.map +1 -0
  125. package/dist/lexicon/types/app/bsky/feed/postgate.js +27 -0
  126. package/dist/lexicon/types/app/bsky/feed/postgate.js.map +1 -0
  127. package/dist/lexicon/types/app/bsky/feed/threadgate.d.ts +2 -0
  128. package/dist/lexicon/types/app/bsky/feed/threadgate.d.ts.map +1 -1
  129. package/dist/lexicon/types/app/bsky/feed/threadgate.js.map +1 -1
  130. package/dist/proto/bsky_connect.d.ts +21 -1
  131. package/dist/proto/bsky_connect.d.ts.map +1 -1
  132. package/dist/proto/bsky_connect.js +20 -0
  133. package/dist/proto/bsky_connect.js.map +1 -1
  134. package/dist/proto/bsky_pb.d.ts +96 -0
  135. package/dist/proto/bsky_pb.d.ts.map +1 -1
  136. package/dist/proto/bsky_pb.js +306 -4
  137. package/dist/proto/bsky_pb.js.map +1 -1
  138. package/dist/util/uris.d.ts +12 -0
  139. package/dist/util/uris.d.ts.map +1 -0
  140. package/dist/util/uris.js +34 -0
  141. package/dist/util/uris.js.map +1 -0
  142. package/dist/views/index.d.ts +8 -2
  143. package/dist/views/index.d.ts.map +1 -1
  144. package/dist/views/index.js +83 -39
  145. package/dist/views/index.js.map +1 -1
  146. package/dist/views/types.d.ts +1 -1
  147. package/dist/views/types.d.ts.map +1 -1
  148. package/dist/views/types.js.map +1 -1
  149. package/dist/views/util.d.ts +11 -1
  150. package/dist/views/util.d.ts.map +1 -1
  151. package/dist/views/util.js +19 -8
  152. package/dist/views/util.js.map +1 -1
  153. package/package.json +4 -4
  154. package/proto/bsky.proto +33 -0
  155. package/src/api/app/bsky/feed/getActorLikes.ts +1 -1
  156. package/src/api/app/bsky/feed/getLikes.ts +1 -1
  157. package/src/api/app/bsky/feed/getPosts.ts +1 -1
  158. package/src/api/app/bsky/feed/getQuotes.ts +105 -0
  159. package/src/api/app/bsky/feed/getRepostedBy.ts +1 -1
  160. package/src/api/app/bsky/feed/searchPosts.ts +1 -1
  161. package/src/api/app/bsky/graph/getFollowers.ts +1 -1
  162. package/src/api/app/bsky/graph/getList.ts +1 -1
  163. package/src/api/app/bsky/notification/listNotifications.ts +32 -6
  164. package/src/api/index.ts +2 -0
  165. package/src/data-plane/server/db/database-schema.ts +7 -3
  166. package/src/data-plane/server/db/migrations/20240723T220700077Z-quotes-post-aggs.ts +12 -0
  167. package/src/data-plane/server/db/migrations/20240723T220703655Z-quotes.ts +28 -0
  168. package/src/data-plane/server/db/migrations/20240801T193939827Z-post-gate.ts +17 -0
  169. package/src/data-plane/server/db/migrations/20240808T224251220Z-post-gate-flags.ts +25 -0
  170. package/src/data-plane/server/db/migrations/index.ts +4 -0
  171. package/src/data-plane/server/db/tables/post-agg.ts +1 -0
  172. package/src/data-plane/server/db/tables/post-gate.ts +12 -0
  173. package/src/data-plane/server/db/tables/post.ts +3 -0
  174. package/src/data-plane/server/db/tables/quote.ts +15 -0
  175. package/src/data-plane/server/indexing/index.ts +7 -0
  176. package/src/data-plane/server/indexing/plugins/post-gate.ts +104 -0
  177. package/src/data-plane/server/indexing/plugins/post.ts +151 -16
  178. package/src/data-plane/server/indexing/plugins/thread-gate.ts +12 -0
  179. package/src/data-plane/server/routes/index.ts +2 -0
  180. package/src/data-plane/server/routes/interactions.ts +2 -1
  181. package/src/data-plane/server/routes/quotes.ts +32 -0
  182. package/src/data-plane/server/routes/records.ts +11 -1
  183. package/src/data-plane/server/util.ts +0 -8
  184. package/src/hydration/feed.ts +58 -12
  185. package/src/hydration/hydrator.ts +94 -22
  186. package/src/hydration/util.ts +0 -4
  187. package/src/lexicon/index.ts +12 -0
  188. package/src/lexicon/lexicons.ts +145 -0
  189. package/src/lexicon/types/app/bsky/embed/record.ts +19 -0
  190. package/src/lexicon/types/app/bsky/feed/defs.ts +2 -0
  191. package/src/lexicon/types/app/bsky/feed/getQuotes.ts +54 -0
  192. package/src/lexicon/types/app/bsky/feed/postgate.ts +47 -0
  193. package/src/lexicon/types/app/bsky/feed/threadgate.ts +2 -0
  194. package/src/proto/bsky_connect.ts +24 -0
  195. package/src/proto/bsky_pb.ts +289 -0
  196. package/src/util/uris.ts +31 -0
  197. package/src/views/index.ts +90 -35
  198. package/src/views/types.ts +1 -0
  199. package/src/views/util.ts +37 -7
  200. package/tests/__snapshots__/feed-generation.test.ts.snap +37 -0
  201. package/tests/data-plane/__snapshots__/indexing.test.ts.snap +18 -0
  202. package/tests/data-plane/indexing.test.ts +1 -0
  203. package/tests/postgates.test.ts +186 -0
  204. package/tests/seed/feed-hidden-replies.ts +62 -0
  205. package/tests/seed/postgates.ts +56 -0
  206. package/tests/views/__snapshots__/author-feed.test.ts.snap +56 -0
  207. package/tests/views/__snapshots__/block-lists.test.ts.snap +6 -0
  208. package/tests/views/__snapshots__/blocks.test.ts.snap +10 -0
  209. package/tests/views/__snapshots__/list-feed.test.ts.snap +22 -0
  210. package/tests/views/__snapshots__/mute-lists.test.ts.snap +8 -0
  211. package/tests/views/__snapshots__/mutes.test.ts.snap +6 -0
  212. package/tests/views/__snapshots__/posts.test.ts.snap +12 -0
  213. package/tests/views/__snapshots__/quotes.test.ts.snap +399 -0
  214. package/tests/views/__snapshots__/thread.test.ts.snap +50 -0
  215. package/tests/views/__snapshots__/timeline.test.ts.snap +170 -0
  216. package/tests/views/author-feed.test.ts +3 -9
  217. package/tests/views/feed-hidden-replies.test.ts +246 -0
  218. package/tests/views/feed-view-post.test.ts +501 -0
  219. package/tests/views/quotes.test.ts +105 -0
@@ -0,0 +1,501 @@
1
+ import { AtpAgent, AppBskyFeedDefs, AtUri } from '@atproto/api'
2
+ import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env'
3
+ import { ids } from '../../src/lexicon/lexicons'
4
+
5
+ /**
6
+ * The frontend computes feed slices for display using at-most one
7
+ * `FeedViewPost` slice. If that slice results in an "orphaned" thread e.g.
8
+ * parent or root is blocked, that slice is tossed out and the next
9
+ * `FeedViewPost` slice is considered. That process continues until a
10
+ * contiguous `root -> child` slice can be found.
11
+ *
12
+ * For the tests below, we test with up to 4 slices: one root and up to 3
13
+ * replies. Some tests focus on the first slice, and others ensure that at
14
+ * least one contiguous slice is returned.
15
+ */
16
+ const LIMIT = 4
17
+
18
+ describe('pds thread views', () => {
19
+ let network: TestNetwork
20
+ let agent: AtpAgent
21
+ let pdsAgent: AtpAgent
22
+ let sc: SeedClient
23
+
24
+ // account dids, for convenience
25
+ let alice: string
26
+ let bob: string
27
+ let carol: string
28
+ let dan: string
29
+
30
+ beforeAll(async () => {
31
+ network = await TestNetwork.create({
32
+ dbPostgresSchema: 'bsky_views_feed_view_post',
33
+ })
34
+ agent = network.bsky.getClient()
35
+ pdsAgent = network.pds.getClient()
36
+ sc = network.getSeedClient()
37
+ await basicSeed(sc)
38
+ alice = sc.dids.alice
39
+ bob = sc.dids.bob
40
+ carol = sc.dids.carol
41
+ dan = sc.dids.dan
42
+
43
+ await sc.follow(carol, alice)
44
+ await sc.follow(carol, bob)
45
+ await sc.follow(carol, dan)
46
+ await sc.follow(dan, alice)
47
+ await sc.follow(dan, bob)
48
+ await sc.follow(dan, carol)
49
+
50
+ await network.processAll()
51
+ })
52
+
53
+ afterAll(async () => {
54
+ await network.close()
55
+ })
56
+
57
+ it(`[A] -> [B], A blocks B, viewed as C`, async () => {
58
+ const A = await sc.post(alice, `A`)
59
+ await network.processAll()
60
+ const B = await sc.reply(bob, A.ref, A.ref, `B`)
61
+ await network.processAll()
62
+ const block = await pdsAgent.api.app.bsky.graph.block.create(
63
+ { repo: alice },
64
+ { createdAt: new Date().toISOString(), subject: bob },
65
+ sc.getHeaders(alice),
66
+ )
67
+
68
+ await network.processAll()
69
+
70
+ const timeline = await agent.api.app.bsky.feed.getTimeline(
71
+ { limit: LIMIT },
72
+ {
73
+ headers: await network.serviceHeaders(
74
+ carol,
75
+ ids.AppBskyFeedGetTimeline,
76
+ ),
77
+ },
78
+ )
79
+
80
+ const sliceA = timeline.data.feed.find((f) => f.post.uri === A.ref.uriStr)
81
+ const sliceB = timeline.data.feed.find((f) => f.post.uri === B.ref.uriStr)
82
+
83
+ expect(sliceA).toBeDefined()
84
+ expect(sliceB).toBeDefined()
85
+
86
+ if (!sliceA || !sliceB) {
87
+ throw new Error('sliceA or sliceB is undefined')
88
+ }
89
+
90
+ expect(AppBskyFeedDefs.isBlockedPost(sliceB.reply?.parent)).toBe(true)
91
+ expect(AppBskyFeedDefs.isBlockedPost(sliceB.reply?.root)).toBe(true)
92
+
93
+ await pdsAgent.api.app.bsky.graph.block.delete(
94
+ { repo: alice, rkey: new AtUri(block.uri).rkey },
95
+ sc.getHeaders(alice),
96
+ )
97
+ })
98
+
99
+ it(`[A] -> [B] -> [C], A blocks B, viewed as C`, async () => {
100
+ const A = await sc.post(alice, `A`)
101
+ await network.processAll()
102
+ const B = await sc.reply(bob, A.ref, A.ref, `B`)
103
+ await network.processAll()
104
+ const C = await sc.reply(carol, A.ref, B.ref, `C`)
105
+ const block = await pdsAgent.api.app.bsky.graph.block.create(
106
+ { repo: alice },
107
+ { createdAt: new Date().toISOString(), subject: bob },
108
+ sc.getHeaders(alice),
109
+ )
110
+
111
+ await network.processAll()
112
+
113
+ const timeline = await agent.api.app.bsky.feed.getTimeline(
114
+ { limit: LIMIT },
115
+ {
116
+ headers: await network.serviceHeaders(
117
+ carol,
118
+ ids.AppBskyFeedGetTimeline,
119
+ ),
120
+ },
121
+ )
122
+
123
+ const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr)
124
+
125
+ expect(sliceC).toBeDefined()
126
+ expect(sliceC?.reply).toBeDefined()
127
+
128
+ if (!sliceC || !sliceC.reply) {
129
+ throw new Error('sliceC is undefined')
130
+ }
131
+
132
+ expect(sliceC.reply.parent.uri).toEqual(B.ref.uriStr)
133
+ expect(sliceC.reply.root.uri).toEqual(A.ref.uriStr)
134
+ expect(AppBskyFeedDefs.isPostView(sliceC.reply.parent)).toBe(true)
135
+ expect(AppBskyFeedDefs.isBlockedPost(sliceC.reply.root)).toBe(true)
136
+
137
+ await pdsAgent.api.app.bsky.graph.block.delete(
138
+ { repo: alice, rkey: new AtUri(block.uri).rkey },
139
+ sc.getHeaders(alice),
140
+ )
141
+ })
142
+
143
+ it(`[A] -> [B] -> [C], C blocks A, viewed as C`, async () => {
144
+ const A = await sc.post(alice, `A`)
145
+ await network.processAll()
146
+ const B = await sc.reply(bob, A.ref, A.ref, `B`)
147
+ await network.processAll()
148
+ const C = await sc.reply(carol, A.ref, B.ref, `C`)
149
+ const block = await pdsAgent.api.app.bsky.graph.block.create(
150
+ { repo: carol },
151
+ { createdAt: new Date().toISOString(), subject: alice },
152
+ sc.getHeaders(carol),
153
+ )
154
+
155
+ await network.processAll()
156
+
157
+ const timeline = await agent.api.app.bsky.feed.getTimeline(
158
+ // make sure we process all slices in this test
159
+ { limit: LIMIT },
160
+ {
161
+ headers: await network.serviceHeaders(
162
+ carol,
163
+ ids.AppBskyFeedGetTimeline,
164
+ ),
165
+ },
166
+ )
167
+
168
+ const sliceB = timeline.data.feed.find((f) => f.post.uri === B.ref.uriStr)
169
+ const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr)
170
+
171
+ expect(sliceB).toBeUndefined()
172
+ expect(sliceC).toBeUndefined()
173
+
174
+ await pdsAgent.api.app.bsky.graph.block.delete(
175
+ { repo: carol, rkey: new AtUri(block.uri).rkey },
176
+ sc.getHeaders(carol),
177
+ )
178
+ })
179
+
180
+ it(`[A] -> [B] -> [C], C blocks B, viewed as C`, async () => {
181
+ const A = await sc.post(alice, `A`)
182
+ await network.processAll()
183
+ const B = await sc.reply(bob, A.ref, A.ref, `B`)
184
+ await network.processAll()
185
+ const C = await sc.reply(carol, A.ref, B.ref, `C`)
186
+ const block = await pdsAgent.api.app.bsky.graph.block.create(
187
+ { repo: carol },
188
+ { createdAt: new Date().toISOString(), subject: bob },
189
+ sc.getHeaders(carol),
190
+ )
191
+
192
+ await network.processAll()
193
+
194
+ const timeline = await agent.api.app.bsky.feed.getTimeline(
195
+ // make sure we process all slices in this test
196
+ { limit: LIMIT },
197
+ {
198
+ headers: await network.serviceHeaders(
199
+ carol,
200
+ ids.AppBskyFeedGetTimeline,
201
+ ),
202
+ },
203
+ )
204
+
205
+ const sliceA = timeline.data.feed.find((f) => f.post.uri === A.ref.uriStr)
206
+ const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr)
207
+
208
+ expect(sliceA).toBeDefined()
209
+ expect(sliceC).toBeUndefined()
210
+
211
+ await pdsAgent.api.app.bsky.graph.block.delete(
212
+ { repo: carol, rkey: new AtUri(block.uri).rkey },
213
+ sc.getHeaders(carol),
214
+ )
215
+ })
216
+
217
+ it(`[A] -> [B] -> [C] -> [D], A blocks C, viewed as C`, async () => {
218
+ const A = await sc.post(alice, `A`)
219
+ await network.processAll()
220
+ const B = await sc.reply(bob, A.ref, A.ref, `B`)
221
+ await network.processAll()
222
+ const C = await sc.reply(carol, A.ref, B.ref, `C`)
223
+ await network.processAll()
224
+ const D = await sc.reply(dan, A.ref, C.ref, `D`)
225
+ const block = await pdsAgent.api.app.bsky.graph.block.create(
226
+ { repo: alice },
227
+ { createdAt: new Date().toISOString(), subject: carol },
228
+ sc.getHeaders(alice),
229
+ )
230
+
231
+ await network.processAll()
232
+
233
+ const timeline = await agent.api.app.bsky.feed.getTimeline(
234
+ { limit: LIMIT },
235
+ {
236
+ headers: await network.serviceHeaders(
237
+ carol,
238
+ ids.AppBskyFeedGetTimeline,
239
+ ),
240
+ },
241
+ )
242
+
243
+ const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)
244
+
245
+ expect(sliceD).toBeDefined()
246
+ expect(sliceD?.reply).toBeDefined()
247
+
248
+ if (!sliceD || !sliceD.reply) {
249
+ throw new Error('sliceD is undefined')
250
+ }
251
+
252
+ expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)
253
+ expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)
254
+ expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true)
255
+ expect(AppBskyFeedDefs.isBlockedPost(sliceD.reply.root)).toBe(true)
256
+
257
+ await pdsAgent.api.app.bsky.graph.block.delete(
258
+ { repo: alice, rkey: new AtUri(block.uri).rkey },
259
+ sc.getHeaders(alice),
260
+ )
261
+ })
262
+
263
+ it(`[A] -> [B] -> [C] -> [D], A blocks C, viewed as D`, async () => {
264
+ const A = await sc.post(alice, `A`)
265
+ await network.processAll()
266
+ const B = await sc.reply(bob, A.ref, A.ref, `B`)
267
+ await network.processAll()
268
+ const C = await sc.reply(carol, A.ref, B.ref, `C`)
269
+ await network.processAll()
270
+ const D = await sc.reply(dan, A.ref, C.ref, `D`)
271
+ const block = await pdsAgent.api.app.bsky.graph.block.create(
272
+ { repo: alice },
273
+ { createdAt: new Date().toISOString(), subject: carol },
274
+ sc.getHeaders(alice),
275
+ )
276
+
277
+ await network.processAll()
278
+
279
+ const timeline = await agent.api.app.bsky.feed.getTimeline(
280
+ { limit: LIMIT },
281
+ {
282
+ headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),
283
+ },
284
+ )
285
+
286
+ const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)
287
+
288
+ expect(sliceD).toBeDefined()
289
+ expect(sliceD?.reply).toBeDefined()
290
+
291
+ if (!sliceD || !sliceD.reply) {
292
+ throw new Error('sliceD is undefined')
293
+ }
294
+
295
+ expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)
296
+ expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)
297
+ expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true)
298
+ expect(AppBskyFeedDefs.isBlockedPost(sliceD.reply.root)).toBe(true)
299
+
300
+ await pdsAgent.api.app.bsky.graph.block.delete(
301
+ { repo: alice, rkey: new AtUri(block.uri).rkey },
302
+ sc.getHeaders(alice),
303
+ )
304
+ })
305
+
306
+ it(`[A] -> [B] -> [C] -> [D], A blocks B, viewed as C`, async () => {
307
+ const A = await sc.post(alice, `A`)
308
+ await network.processAll()
309
+ const B = await sc.reply(bob, A.ref, A.ref, `B`)
310
+ await network.processAll()
311
+ const C = await sc.reply(carol, A.ref, B.ref, `C`)
312
+ await network.processAll()
313
+ const D = await sc.reply(dan, A.ref, C.ref, `D`)
314
+ const block = await pdsAgent.api.app.bsky.graph.block.create(
315
+ { repo: alice },
316
+ { createdAt: new Date().toISOString(), subject: bob },
317
+ sc.getHeaders(alice),
318
+ )
319
+
320
+ await network.processAll()
321
+
322
+ const timeline = await agent.api.app.bsky.feed.getTimeline(
323
+ { limit: LIMIT },
324
+ {
325
+ headers: await network.serviceHeaders(
326
+ carol,
327
+ ids.AppBskyFeedGetTimeline,
328
+ ),
329
+ },
330
+ )
331
+
332
+ const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)
333
+
334
+ expect(sliceD).toBeDefined()
335
+ expect(sliceD?.reply).toBeDefined()
336
+
337
+ if (!sliceD || !sliceD.reply) {
338
+ throw new Error('sliceD is undefined')
339
+ }
340
+
341
+ expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)
342
+ expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)
343
+ expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true)
344
+ /*
345
+ * We don't walk the reply ancestors past whats available in the ReplyRef
346
+ */
347
+ expect(AppBskyFeedDefs.isPostView(sliceD.reply.root)).toBe(true)
348
+
349
+ await pdsAgent.api.app.bsky.graph.block.delete(
350
+ { repo: alice, rkey: new AtUri(block.uri).rkey },
351
+ sc.getHeaders(alice),
352
+ )
353
+ })
354
+
355
+ it(`[A] -> [B] -> [C] -> [D], B blocks C, viewed as D`, async () => {
356
+ const A = await sc.post(alice, `A`)
357
+ await network.processAll()
358
+ const B = await sc.reply(bob, A.ref, A.ref, `B`)
359
+ await network.processAll()
360
+ const C = await sc.reply(carol, A.ref, B.ref, `C`)
361
+ await network.processAll()
362
+ const D = await sc.reply(dan, A.ref, C.ref, `D`)
363
+ const block = await pdsAgent.api.app.bsky.graph.block.create(
364
+ { repo: bob },
365
+ { createdAt: new Date().toISOString(), subject: carol },
366
+ sc.getHeaders(bob),
367
+ )
368
+
369
+ await network.processAll()
370
+
371
+ const timeline = await agent.api.app.bsky.feed.getTimeline(
372
+ { limit: LIMIT },
373
+ {
374
+ headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),
375
+ },
376
+ )
377
+
378
+ const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)
379
+
380
+ expect(sliceD).toBeDefined()
381
+ expect(sliceD?.reply).toBeDefined()
382
+
383
+ if (!sliceD || !sliceD.reply) {
384
+ throw new Error('sliceD is undefined')
385
+ }
386
+
387
+ expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)
388
+ expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)
389
+ expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true)
390
+ /*
391
+ * We don't walk the reply ancestors past whats available in the ReplyRef
392
+ */
393
+ expect(AppBskyFeedDefs.isPostView(sliceD.reply.root)).toBe(true)
394
+
395
+ await pdsAgent.api.app.bsky.graph.block.delete(
396
+ { repo: bob, rkey: new AtUri(block.uri).rkey },
397
+ sc.getHeaders(bob),
398
+ )
399
+ })
400
+
401
+ it(`[A] -> [B] -> [C] -> [D], A blocks D, viewed as C`, async () => {
402
+ const A = await sc.post(alice, `A`)
403
+ await network.processAll()
404
+ const B = await sc.reply(bob, A.ref, A.ref, `B`)
405
+ await network.processAll()
406
+ const C = await sc.reply(carol, A.ref, B.ref, `C`)
407
+ await network.processAll()
408
+ const D = await sc.reply(dan, A.ref, C.ref, `D`)
409
+ const block = await pdsAgent.api.app.bsky.graph.block.create(
410
+ { repo: alice },
411
+ { createdAt: new Date().toISOString(), subject: dan },
412
+ sc.getHeaders(alice),
413
+ )
414
+
415
+ await network.processAll()
416
+
417
+ const timeline = await agent.api.app.bsky.feed.getTimeline(
418
+ { limit: LIMIT },
419
+ {
420
+ headers: await network.serviceHeaders(
421
+ carol,
422
+ ids.AppBskyFeedGetTimeline,
423
+ ),
424
+ },
425
+ )
426
+
427
+ const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)
428
+ const sliceC = timeline.data.feed.find((f) => f.post.uri === C.ref.uriStr)
429
+
430
+ expect(sliceD).toBeDefined()
431
+ expect(sliceC).toBeDefined()
432
+
433
+ if (!sliceD || !sliceC) {
434
+ throw new Error('sliceD or sliceC is undefined')
435
+ }
436
+
437
+ expect(AppBskyFeedDefs.isBlockedPost(sliceD.reply?.root)).toBe(true)
438
+ expect(AppBskyFeedDefs.isPostView(sliceC.reply?.parent)).toBe(true)
439
+ expect(AppBskyFeedDefs.isPostView(sliceC.reply?.root)).toBe(true)
440
+
441
+ await pdsAgent.api.app.bsky.graph.block.delete(
442
+ { repo: alice, rkey: new AtUri(block.uri).rkey },
443
+ sc.getHeaders(alice),
444
+ )
445
+ })
446
+
447
+ it(`[A] -> [B] -> [C] -> [D], A blocks C, viewed as D, A/B/C are outside first page`, async () => {
448
+ const A = await sc.post(alice, `A`)
449
+ await network.processAll()
450
+ const B = await sc.reply(bob, A.ref, A.ref, `B`)
451
+ await network.processAll()
452
+ const C = await sc.reply(carol, A.ref, B.ref, `C`)
453
+ await network.processAll()
454
+
455
+ // push A/B/C to send page of results
456
+ await sc.post(alice, `Aa`)
457
+ await sc.post(alice, `Ab`)
458
+ await sc.post(alice, `Ac`)
459
+ await sc.post(alice, `Ad`)
460
+
461
+ await network.processAll()
462
+
463
+ const D = await sc.reply(dan, A.ref, C.ref, `D`)
464
+ const block = await pdsAgent.api.app.bsky.graph.block.create(
465
+ { repo: alice },
466
+ { createdAt: new Date().toISOString(), subject: carol },
467
+ sc.getHeaders(alice),
468
+ )
469
+
470
+ await network.processAll()
471
+
472
+ const timeline = await agent.api.app.bsky.feed.getTimeline(
473
+ { limit: LIMIT },
474
+ {
475
+ headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),
476
+ },
477
+ )
478
+
479
+ const sliceD = timeline.data.feed.find((f) => f.post.uri === D.ref.uriStr)
480
+ const sliceA = timeline.data.feed.find((f) => f.post.uri === A.ref.uriStr)
481
+
482
+ expect(sliceD).toBeDefined()
483
+ expect(sliceD?.reply).toBeDefined()
484
+ // not in first page of results
485
+ expect(sliceA).toBeUndefined()
486
+
487
+ if (!sliceD || !sliceD.reply) {
488
+ throw new Error('sliceD is undefined')
489
+ }
490
+
491
+ expect(sliceD.reply.parent.uri).toEqual(C.ref.uriStr)
492
+ expect(sliceD.reply.root.uri).toEqual(A.ref.uriStr)
493
+ expect(AppBskyFeedDefs.isPostView(sliceD.reply.parent)).toBe(true)
494
+ expect(AppBskyFeedDefs.isBlockedPost(sliceD.reply.root)).toBe(true)
495
+
496
+ await pdsAgent.api.app.bsky.graph.block.delete(
497
+ { repo: alice, rkey: new AtUri(block.uri).rkey },
498
+ sc.getHeaders(alice),
499
+ )
500
+ })
501
+ })
@@ -0,0 +1,105 @@
1
+ import { quotesSeed, SeedClient, TestNetwork } from '@atproto/dev-env'
2
+ import AtpAgent from '@atproto/api'
3
+ import { forSnapshot } from '../_util'
4
+ import { ids } from '../../src/lexicon/lexicons'
5
+
6
+ describe('pds quote views', () => {
7
+ let network: TestNetwork
8
+ let agent: AtpAgent
9
+ let sc: SeedClient
10
+
11
+ // account dids, for convenience
12
+ let alice: string
13
+ let bob: string
14
+ let eve: string
15
+
16
+ beforeAll(async () => {
17
+ network = await TestNetwork.create({
18
+ dbPostgresSchema: 'bsky_views_quotes',
19
+ })
20
+ agent = network.bsky.getClient()
21
+ sc = network.getSeedClient()
22
+ await quotesSeed(sc)
23
+ await network.processAll()
24
+ alice = sc.dids.alice
25
+ bob = sc.dids.bob
26
+ eve = sc.dids.eve
27
+ })
28
+
29
+ afterAll(async () => {
30
+ await network.close()
31
+ })
32
+
33
+ it('fetches post quotes', async () => {
34
+ const alicePostQuotes = await agent.api.app.bsky.feed.getQuotes(
35
+ { uri: sc.posts[alice][0].ref.uriStr, limit: 30 },
36
+ { headers: await network.serviceHeaders(eve, ids.AppBskyFeedGetQuotes) },
37
+ )
38
+
39
+ expect(alicePostQuotes.data.posts.length).toBe(2)
40
+ expect(forSnapshot(alicePostQuotes.data)).toMatchSnapshot()
41
+ })
42
+
43
+ it('utilizes limit parameter and cursor', async () => {
44
+ const alicePostQuotes1 = await agent.api.app.bsky.feed.getQuotes(
45
+ { uri: sc.posts[alice][1].ref.uriStr, limit: 3 },
46
+ { headers: await network.serviceHeaders(eve, ids.AppBskyFeedGetQuotes) },
47
+ )
48
+
49
+ expect(alicePostQuotes1.data.posts.length).toBe(3)
50
+ expect(alicePostQuotes1.data.cursor).toBeDefined()
51
+
52
+ const alicePostQuotes2 = await agent.api.app.bsky.feed.getQuotes(
53
+ {
54
+ uri: sc.posts[alice][1].ref.uriStr,
55
+ limit: 3,
56
+ cursor: alicePostQuotes1.data.cursor,
57
+ },
58
+ { headers: await network.serviceHeaders(eve, ids.AppBskyFeedGetQuotes) },
59
+ )
60
+
61
+ expect(alicePostQuotes2.data.posts.length).toBe(2)
62
+ })
63
+
64
+ it('does not return post when quote is deleted', async () => {
65
+ await sc.deletePost(eve, sc.posts[eve][0].ref.uri)
66
+ await network.processAll()
67
+
68
+ const alicePostQuotes = await agent.api.app.bsky.feed.getQuotes(
69
+ { uri: sc.posts[alice][0].ref.uriStr, limit: 30 },
70
+ {
71
+ headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetQuotes),
72
+ },
73
+ )
74
+
75
+ expect(alicePostQuotes.data.posts.length).toBe(1)
76
+ expect(forSnapshot(alicePostQuotes.data)).toMatchSnapshot()
77
+ })
78
+
79
+ it('does not return any quotes when the quoted post is deleted', async () => {
80
+ await sc.deletePost(alice, sc.posts[alice][0].ref.uri)
81
+ await network.processAll()
82
+
83
+ const alicePostQuotesAfter = await agent.api.app.bsky.feed.getQuotes(
84
+ { uri: sc.posts[alice][0].ref.uriStr, limit: 30 },
85
+ {
86
+ headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetQuotes),
87
+ },
88
+ )
89
+
90
+ expect(alicePostQuotesAfter.data.posts.length).toBe(0)
91
+ })
92
+
93
+ it('decrements quote count when a quote is deleted', async () => {
94
+ await sc.deletePost(eve, sc.posts[eve][2].ref.uri)
95
+ await network.processAll()
96
+
97
+ const bobPost = await agent.api.app.bsky.feed.getPosts(
98
+ { uris: [sc.replies[bob][0].ref.uriStr] },
99
+ { headers: await network.serviceHeaders(bob, ids.AppBskyFeedGetPosts) },
100
+ )
101
+
102
+ expect(bobPost.data.posts[0].quoteCount).toEqual(0)
103
+ expect(forSnapshot(bobPost.data)).toMatchSnapshot()
104
+ })
105
+ })