@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
@@ -17,6 +17,7 @@ import { Database } from '../db'
17
17
  import { Actor } from '../db/tables/actor'
18
18
  import * as Post from './plugins/post'
19
19
  import * as Threadgate from './plugins/thread-gate'
20
+ import * as Postgate from './plugins/post-gate'
20
21
  import * as Like from './plugins/like'
21
22
  import * as Repost from './plugins/repost'
22
23
  import * as Follow from './plugins/follow'
@@ -38,6 +39,7 @@ export class IndexingService {
38
39
  records: {
39
40
  post: Post.PluginType
40
41
  threadGate: Threadgate.PluginType
42
+ postGate: Postgate.PluginType
41
43
  like: Like.PluginType
42
44
  repost: Repost.PluginType
43
45
  follow: Follow.PluginType
@@ -60,6 +62,7 @@ export class IndexingService {
60
62
  this.records = {
61
63
  post: Post.makePlugin(this.db, this.background),
62
64
  threadGate: Threadgate.makePlugin(this.db, this.background),
65
+ postGate: Postgate.makePlugin(this.db, this.background),
63
66
  like: Like.makePlugin(this.db, this.background),
64
67
  repost: Repost.makePlugin(this.db, this.background),
65
68
  follow: Follow.makePlugin(this.db, this.background),
@@ -365,6 +368,10 @@ export class IndexingService {
365
368
  .deleteFrom('thread_gate')
366
369
  .where('creator', '=', did)
367
370
  .execute()
371
+ await this.db.db
372
+ .deleteFrom('post_gate')
373
+ .where('creator', '=', did)
374
+ .execute()
368
375
  // notifications
369
376
  await this.db.db
370
377
  .deleteFrom('notification')
@@ -0,0 +1,104 @@
1
+ import { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'
2
+ import { InvalidRequestError } from '@atproto/xrpc-server'
3
+ import { CID } from 'multiformats/cid'
4
+ import * as Postgate from '../../../../lexicon/types/app/bsky/feed/postgate'
5
+ import * as lex from '../../../../lexicon/lexicons'
6
+ import { DatabaseSchema, DatabaseSchemaType } from '../../db/database-schema'
7
+ import { Database } from '../../db'
8
+ import RecordProcessor from '../processor'
9
+ import { BackgroundQueue } from '../../background'
10
+
11
+ const lexId = lex.ids.AppBskyFeedPostgate
12
+ type IndexedGate = DatabaseSchemaType['post_gate']
13
+
14
+ const insertFn = async (
15
+ db: DatabaseSchema,
16
+ uri: AtUri,
17
+ cid: CID,
18
+ obj: Postgate.Record,
19
+ timestamp: string,
20
+ ): Promise<IndexedGate | null> => {
21
+ const postUri = new AtUri(obj.post)
22
+ if (postUri.host !== uri.host || postUri.rkey !== uri.rkey) {
23
+ throw new InvalidRequestError(
24
+ 'Creator and rkey of post gate does not match its post',
25
+ )
26
+ }
27
+ const inserted = await db
28
+ .insertInto('post_gate')
29
+ .values({
30
+ uri: uri.toString(),
31
+ cid: cid.toString(),
32
+ creator: uri.host,
33
+ postUri: obj.post,
34
+ createdAt: normalizeDatetimeAlways(obj.createdAt),
35
+ indexedAt: timestamp,
36
+ })
37
+ .onConflict((oc) => oc.doNothing())
38
+ .returningAll()
39
+ .executeTakeFirst()
40
+ await db
41
+ .updateTable('post')
42
+ .where('uri', '=', postUri.toString())
43
+ .set({ hasPostGate: true })
44
+ .executeTakeFirst()
45
+ return inserted || null
46
+ }
47
+
48
+ const findDuplicate = async (
49
+ db: DatabaseSchema,
50
+ _uri: AtUri,
51
+ obj: Postgate.Record,
52
+ ): Promise<AtUri | null> => {
53
+ const found = await db
54
+ .selectFrom('post_gate')
55
+ .where('postUri', '=', obj.post)
56
+ .selectAll()
57
+ .executeTakeFirst()
58
+ return found ? new AtUri(found.uri) : null
59
+ }
60
+
61
+ const notifsForInsert = () => {
62
+ return []
63
+ }
64
+
65
+ const deleteFn = async (
66
+ db: DatabaseSchema,
67
+ uri: AtUri,
68
+ ): Promise<IndexedGate | null> => {
69
+ const deleted = await db
70
+ .deleteFrom('post_gate')
71
+ .where('uri', '=', uri.toString())
72
+ .returningAll()
73
+ .executeTakeFirst()
74
+ if (deleted) {
75
+ await db
76
+ .updateTable('post')
77
+ .where('uri', '=', deleted.postUri)
78
+ .set({ hasPostGate: false })
79
+ .executeTakeFirst()
80
+ }
81
+ return deleted || null
82
+ }
83
+
84
+ const notifsForDelete = () => {
85
+ return { notifs: [], toDelete: [] }
86
+ }
87
+
88
+ export type PluginType = RecordProcessor<Postgate.Record, IndexedGate>
89
+
90
+ export const makePlugin = (
91
+ db: Database,
92
+ background: BackgroundQueue,
93
+ ): PluginType => {
94
+ return new RecordProcessor(db, background, {
95
+ lexId,
96
+ insertFn,
97
+ findDuplicate,
98
+ deleteFn,
99
+ notifsForInsert,
100
+ notifsForDelete,
101
+ })
102
+ }
103
+
104
+ export default makePlugin
@@ -7,6 +7,7 @@ import {
7
7
  ReplyRef,
8
8
  } from '../../../../lexicon/types/app/bsky/feed/post'
9
9
  import { Record as GateRecord } from '../../../../lexicon/types/app/bsky/feed/threadgate'
10
+ import { Record as PostgateRecord } from '../../../../lexicon/types/app/bsky/feed/postgate'
10
11
  import { isMain as isEmbedImage } from '../../../../lexicon/types/app/bsky/embed/images'
11
12
  import { isMain as isEmbedExternal } from '../../../../lexicon/types/app/bsky/embed/external'
12
13
  import { isMain as isEmbedRecord } from '../../../../lexicon/types/app/bsky/embed/record'
@@ -26,9 +27,14 @@ import {
26
27
  getDescendentsQb,
27
28
  invalidReplyRoot as checkInvalidReplyRoot,
28
29
  violatesThreadGate as checkViolatesThreadGate,
29
- postToThreadgateUri,
30
30
  } from '../../util'
31
31
  import { BackgroundQueue } from '../../background'
32
+ import { parsePostgate } from '../../../../views/util'
33
+ import {
34
+ postUriToThreadgateUri,
35
+ postUriToPostgateUri,
36
+ uriToDid,
37
+ } from '../../../../util/uris'
32
38
 
33
39
  type Notif = Insertable<Notification>
34
40
  type Post = Selectable<DatabaseSchemaType['post']>
@@ -52,6 +58,7 @@ type IndexedPost = {
52
58
  embeds?: (PostEmbedImage[] | PostEmbedExternal | PostEmbedRecord)[]
53
59
  ancestors?: PostAncestor[]
54
60
  descendents?: PostDescendent[]
61
+ threadgate?: GateRecord
55
62
  }
56
63
 
57
64
  const lexId = lex.ids.AppBskyFeedPost
@@ -168,6 +175,7 @@ const insertFn = async (
168
175
  await db.insertInto('post_embed_external').values(externalEmbed).execute()
169
176
  } else if (isEmbedRecord(postEmbed)) {
170
177
  const { record } = postEmbed
178
+ const embedUri = new AtUri(record.uri)
171
179
  const recordEmbed = {
172
180
  postUri: uri.toString(),
173
181
  embedUri: record.uri,
@@ -175,9 +183,59 @@ const insertFn = async (
175
183
  }
176
184
  embeds.push(recordEmbed)
177
185
  await db.insertInto('post_embed_record').values(recordEmbed).execute()
186
+
187
+ if (embedUri.collection === lex.ids.AppBskyFeedPost) {
188
+ const quote = {
189
+ uri: uri.toString(),
190
+ cid: cid.toString(),
191
+ subject: record.uri,
192
+ subjectCid: record.cid,
193
+ createdAt: normalizeDatetimeAlways(obj.createdAt),
194
+ indexedAt: timestamp,
195
+ }
196
+ await db
197
+ .insertInto('quote')
198
+ .values(quote)
199
+ .onConflict((oc) => oc.doNothing())
200
+ .returningAll()
201
+ .executeTakeFirst()
202
+
203
+ const quoteCountQb = db
204
+ .insertInto('post_agg')
205
+ .values({
206
+ uri: record.uri.toString(),
207
+ quoteCount: db
208
+ .selectFrom('quote')
209
+ .where('quote.subjectCid', '=', record.cid.toString())
210
+ .select(countAll.as('count')),
211
+ })
212
+ .onConflict((oc) =>
213
+ oc
214
+ .column('uri')
215
+ .doUpdateSet({ quoteCount: excluded(db, 'quoteCount') }),
216
+ )
217
+ await quoteCountQb.execute()
218
+
219
+ const { violatesEmbeddingRules } = await validatePostEmbed(
220
+ db,
221
+ embedUri.toString(),
222
+ uri.toString(),
223
+ )
224
+ Object.assign(insertedPost, {
225
+ violatesEmbeddingRules: violatesEmbeddingRules,
226
+ })
227
+ if (violatesEmbeddingRules) {
228
+ await db
229
+ .updateTable('post')
230
+ .where('uri', '=', insertedPost.uri)
231
+ .set({ violatesEmbeddingRules: violatesEmbeddingRules })
232
+ .executeTakeFirst()
233
+ }
234
+ }
178
235
  }
179
236
  }
180
237
 
238
+ const threadgate = await getThreadgateRecord(db, post.replyRoot || post.uri)
181
239
  const ancestors = await getAncestorsAndSelfQb(db, {
182
240
  uri: post.uri,
183
241
  parentHeight: REPLY_NOTIF_DEPTH,
@@ -200,6 +258,7 @@ const insertFn = async (
200
258
  embeds,
201
259
  ancestors,
202
260
  descendents,
261
+ threadgate,
203
262
  }
204
263
  }
205
264
 
@@ -228,19 +287,22 @@ const notifsForInsert = (obj: IndexedPost) => {
228
287
  })
229
288
  }
230
289
  }
231
- for (const embed of obj.embeds ?? []) {
232
- if ('embedUri' in embed) {
233
- const embedUri = new AtUri(embed.embedUri)
234
- if (embedUri.collection === lex.ids.AppBskyFeedPost) {
235
- maybeNotify({
236
- did: embedUri.host,
237
- reason: 'quote',
238
- reasonSubject: embedUri.toString(),
239
- author: obj.post.creator,
240
- recordUri: obj.post.uri,
241
- recordCid: obj.post.cid,
242
- sortAt: obj.post.sortAt,
243
- })
290
+
291
+ if (!obj.post.violatesEmbeddingRules) {
292
+ for (const embed of obj.embeds ?? []) {
293
+ if ('embedUri' in embed) {
294
+ const embedUri = new AtUri(embed.embedUri)
295
+ if (embedUri.collection === lex.ids.AppBskyFeedPost) {
296
+ maybeNotify({
297
+ did: embedUri.host,
298
+ reason: 'quote',
299
+ reasonSubject: embedUri.toString(),
300
+ author: obj.post.creator,
301
+ recordUri: obj.post.uri,
302
+ recordCid: obj.post.cid,
303
+ sortAt: obj.post.sortAt,
304
+ })
305
+ }
244
306
  }
245
307
  }
246
308
  }
@@ -250,6 +312,8 @@ const notifsForInsert = (obj: IndexedPost) => {
250
312
  return notifs
251
313
  }
252
314
 
315
+ const threadgateHiddenReplies = obj.threadgate?.hiddenReplies || []
316
+
253
317
  // reply notifications
254
318
 
255
319
  for (const ancestor of obj.ancestors ?? []) {
@@ -265,6 +329,8 @@ const notifsForInsert = (obj: IndexedPost) => {
265
329
  recordCid: obj.post.cid,
266
330
  sortAt: obj.post.sortAt,
267
331
  })
332
+ // found hidden reply, don't notify any higher ancestors
333
+ if (threadgateHiddenReplies.includes(ancestorUri.toString())) break
268
334
  }
269
335
  }
270
336
 
@@ -304,6 +370,7 @@ const deleteFn = async (
304
370
  .executeTakeFirst(),
305
371
  db.deleteFrom('feed_item').where('postUri', '=', uriStr).executeTakeFirst(),
306
372
  ])
373
+ await db.deleteFrom('quote').where('subject', '=', uriStr).execute()
307
374
  const deletedEmbeds: (
308
375
  | PostEmbedImage[]
309
376
  | PostEmbedExternal
@@ -333,7 +400,27 @@ const deleteFn = async (
333
400
  deletedEmbeds.push(deletedExternals)
334
401
  }
335
402
  if (deletedPosts) {
403
+ const embedUri = new AtUri(deletedPosts.embedUri)
336
404
  deletedEmbeds.push(deletedPosts)
405
+
406
+ if (embedUri.collection === lex.ids.AppBskyFeedPost) {
407
+ await db.deleteFrom('quote').where('uri', '=', uriStr).execute()
408
+ await db
409
+ .insertInto('post_agg')
410
+ .values({
411
+ uri: deletedPosts.embedUri,
412
+ quoteCount: db
413
+ .selectFrom('quote')
414
+ .where('quote.subjectCid', '=', deletedPosts.embedCid.toString())
415
+ .select(countAll.as('count')),
416
+ })
417
+ .onConflict((oc) =>
418
+ oc
419
+ .column('uri')
420
+ .doUpdateSet({ quoteCount: excluded(db, 'quoteCount') }),
421
+ )
422
+ .execute()
423
+ }
337
424
  }
338
425
  return deleted
339
426
  ? {
@@ -434,7 +521,7 @@ async function validateReply(
434
521
  const violatesThreadGate = await checkViolatesThreadGate(
435
522
  db,
436
523
  creator,
437
- new AtUri(reply.root.uri).hostname,
524
+ uriToDid(reply.root.uri),
438
525
  replyRefs.root?.record ?? null,
439
526
  replyRefs.gate?.record ?? null,
440
527
  )
@@ -444,10 +531,58 @@ async function validateReply(
444
531
  }
445
532
  }
446
533
 
534
+ async function getThreadgateRecord(db: DatabaseSchema, postUri: string) {
535
+ const threadgateRecordUri = postUriToThreadgateUri(postUri)
536
+ const results = await db
537
+ .selectFrom('record')
538
+ .where('record.uri', '=', threadgateRecordUri)
539
+ .selectAll()
540
+ .execute()
541
+ const threadgateRecord = results.find(
542
+ (ref) => ref.uri === threadgateRecordUri,
543
+ )
544
+ if (threadgateRecord) {
545
+ return jsonStringToLex(threadgateRecord.json) as GateRecord
546
+ }
547
+ }
548
+
549
+ async function validatePostEmbed(
550
+ db: DatabaseSchema,
551
+ embedUri: string,
552
+ parentUri: string,
553
+ ) {
554
+ const postgateRecordUri = postUriToPostgateUri(embedUri)
555
+ const postgateRecord = await db
556
+ .selectFrom('record')
557
+ .where('record.uri', '=', postgateRecordUri)
558
+ .selectAll()
559
+ .executeTakeFirst()
560
+ if (!postgateRecord) {
561
+ return {
562
+ violatesEmbeddingRules: false,
563
+ }
564
+ }
565
+ const {
566
+ embeddingRules: { canEmbed },
567
+ } = parsePostgate({
568
+ gate: jsonStringToLex(postgateRecord.json) as PostgateRecord,
569
+ viewerDid: uriToDid(parentUri),
570
+ authorDid: uriToDid(embedUri),
571
+ })
572
+ if (canEmbed) {
573
+ return {
574
+ violatesEmbeddingRules: false,
575
+ }
576
+ }
577
+ return {
578
+ violatesEmbeddingRules: true,
579
+ }
580
+ }
581
+
447
582
  async function getReplyRefs(db: DatabaseSchema, reply: ReplyRef) {
448
583
  const replyRoot = reply.root.uri
449
584
  const replyParent = reply.parent.uri
450
- const replyGate = postToThreadgateUri(replyRoot)
585
+ const replyGate = postUriToThreadgateUri(replyRoot)
451
586
  const results = await db
452
587
  .selectFrom('record')
453
588
  .where('record.uri', 'in', [replyRoot, replyGate, replyParent])
@@ -37,6 +37,11 @@ const insertFn = async (
37
37
  .onConflict((oc) => oc.doNothing())
38
38
  .returningAll()
39
39
  .executeTakeFirst()
40
+ await db
41
+ .updateTable('post')
42
+ .where('uri', '=', postUri.toString())
43
+ .set({ hasThreadGate: true })
44
+ .executeTakeFirst()
40
45
  return inserted || null
41
46
  }
42
47
 
@@ -66,6 +71,13 @@ const deleteFn = async (
66
71
  .where('uri', '=', uri.toString())
67
72
  .returningAll()
68
73
  .executeTakeFirst()
74
+ if (deleted) {
75
+ await db
76
+ .updateTable('post')
77
+ .where('uri', '=', deleted.postUri)
78
+ .set({ hasThreadGate: false })
79
+ .executeTakeFirst()
80
+ }
69
81
  return deleted || null
70
82
  }
71
83
 
@@ -15,6 +15,7 @@ import mutes from './mutes'
15
15
  import notifs from './notifs'
16
16
  import posts from './posts'
17
17
  import profile from './profile'
18
+ import quotes from './quotes'
18
19
  import records from './records'
19
20
  import relationships from './relationships'
20
21
  import reposts from './reposts'
@@ -42,6 +43,7 @@ export default (db: Database, idResolver: IdResolver) =>
42
43
  ...notifs(db),
43
44
  ...posts(db),
44
45
  ...profile(db),
46
+ ...quotes(db),
45
47
  ...records(db),
46
48
  ...relationships(db),
47
49
  ...reposts(db),
@@ -8,7 +8,7 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
8
8
  async getInteractionCounts(req) {
9
9
  const uris = req.refs.map((ref) => ref.uri)
10
10
  if (uris.length === 0) {
11
- return { likes: [], replies: [], reposts: [] }
11
+ return { likes: [], replies: [], reposts: [], quotes: [] }
12
12
  }
13
13
  const res = await db.db
14
14
  .selectFrom('post_agg')
@@ -20,6 +20,7 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
20
20
  likes: uris.map((uri) => byUri[uri]?.likeCount ?? 0),
21
21
  replies: uris.map((uri) => byUri[uri]?.replyCount ?? 0),
22
22
  reposts: uris.map((uri) => byUri[uri]?.repostCount ?? 0),
23
+ quotes: uris.map((uri) => byUri[uri]?.quoteCount ?? 0),
23
24
  }
24
25
  },
25
26
  async getCountsForUsers(req) {
@@ -0,0 +1,32 @@
1
+ import { ServiceImpl } from '@connectrpc/connect'
2
+ import { Service } from '../../../proto/bsky_connect'
3
+ import { Database } from '../db'
4
+ import { paginate, TimeCidKeyset } from '../db/pagination'
5
+
6
+ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
7
+ async getQuotesBySubject(req) {
8
+ const { subject, cursor, limit } = req
9
+ const { ref } = db.db.dynamic
10
+
11
+ if (!subject?.uri) return { uris: [] }
12
+
13
+ let builder = db.db
14
+ .selectFrom('quote')
15
+ .where('quote.subject', '=', subject.uri)
16
+ .select(['quote.uri', 'quote.cid', 'quote.sortAt'])
17
+
18
+ const keyset = new TimeCidKeyset(ref('quote.sortAt'), ref('quote.cid'))
19
+ builder = paginate(builder, {
20
+ limit,
21
+ cursor,
22
+ keyset,
23
+ })
24
+
25
+ const quotes = await builder.execute()
26
+
27
+ return {
28
+ refs: quotes.map((q) => ({ uri: q.uri, cid: q.cid })),
29
+ cursor: keyset.packFromResult(quotes),
30
+ }
31
+ },
32
+ })
@@ -20,6 +20,7 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
20
20
  getProfileRecords: getRecords(db, ids.AppBskyActorProfile),
21
21
  getRepostRecords: getRecords(db, ids.AppBskyFeedRepost),
22
22
  getThreadGateRecords: getRecords(db, ids.AppBskyFeedThreadgate),
23
+ getPostGateRecords: getRecords(db, ids.AppBskyFeedPostgate),
23
24
  getLabelerRecords: getRecords(db, ids.AppBskyLabelerService),
24
25
  getActorChatDeclarationRecords: getRecords(db, ids.ChatBskyActorDeclaration),
25
26
  getStarterPackRecords: getRecords(db, ids.AppBskyGraphStarterpack),
@@ -74,7 +75,13 @@ export const getPostRecords = (db: Database) => {
74
75
  ? await db.db
75
76
  .selectFrom('post')
76
77
  .where('uri', 'in', req.uris)
77
- .select(['uri', 'violatesThreadGate'])
78
+ .select([
79
+ 'uri',
80
+ 'violatesThreadGate',
81
+ 'violatesEmbeddingRules',
82
+ 'hasThreadGate',
83
+ 'hasPostGate',
84
+ ])
78
85
  .execute()
79
86
  : [],
80
87
  ])
@@ -82,6 +89,9 @@ export const getPostRecords = (db: Database) => {
82
89
  const meta = req.uris.map((uri) => {
83
90
  return new PostRecordMeta({
84
91
  violatesThreadGate: !!byKey[uri]?.violatesThreadGate,
92
+ violatesEmbeddingRules: !!byKey[uri]?.violatesEmbeddingRules,
93
+ hasThreadGate: !!byKey[uri]?.hasThreadGate,
94
+ hasPostGate: !!byKey[uri]?.hasPostGate,
85
95
  })
86
96
  })
87
97
  return { records, meta }
@@ -1,6 +1,4 @@
1
1
  import { sql } from 'kysely'
2
- import { AtUri } from '@atproto/syntax'
3
- import { ids } from '../../lexicon/lexicons'
4
2
  import {
5
3
  Record as PostRecord,
6
4
  ReplyRef,
@@ -144,9 +142,3 @@ export const violatesThreadGate = async (
144
142
 
145
143
  return true
146
144
  }
147
-
148
- export const postToThreadgateUri = (postUri: string) => {
149
- const gateUri = new AtUri(postUri)
150
- gateUri.collection = ids.AppBskyFeedThreadgate
151
- return gateUri.toString()
152
- }
@@ -4,6 +4,7 @@ import { Record as LikeRecord } from '../lexicon/types/app/bsky/feed/like'
4
4
  import { Record as RepostRecord } from '../lexicon/types/app/bsky/feed/repost'
5
5
  import { Record as FeedGenRecord } from '../lexicon/types/app/bsky/feed/generator'
6
6
  import { Record as ThreadgateRecord } from '../lexicon/types/app/bsky/feed/threadgate'
7
+ import { Record as PostgateRecord } from '../lexicon/types/app/bsky/feed/postgate'
7
8
  import {
8
9
  HydrationMap,
9
10
  ItemRef,
@@ -12,11 +13,15 @@ import {
12
13
  parseString,
13
14
  split,
14
15
  } from './util'
15
- import { AtUri } from '@atproto/syntax'
16
- import { ids } from '../lexicon/lexicons'
17
16
  import { dedupeStrs } from '@atproto/common'
17
+ import { postUriToThreadgateUri, postUriToPostgateUri } from '../util/uris'
18
18
 
19
- export type Post = RecordInfo<PostRecord> & { violatesThreadGate: boolean }
19
+ export type Post = RecordInfo<PostRecord> & {
20
+ violatesThreadGate: boolean
21
+ violatesEmbeddingRules: boolean
22
+ hasThreadGate: boolean
23
+ hasPostGate: boolean
24
+ }
20
25
  export type Posts = HydrationMap<Post>
21
26
 
22
27
  export type PostViewerState = {
@@ -31,6 +36,7 @@ export type PostAgg = {
31
36
  likes: number
32
37
  replies: number
33
38
  reposts: number
39
+ quotes: number
34
40
  }
35
41
 
36
42
  export type PostAggs = HydrationMap<PostAgg>
@@ -58,6 +64,8 @@ export type FeedGenViewerStates = HydrationMap<FeedGenViewerState>
58
64
 
59
65
  export type Threadgate = RecordInfo<ThreadgateRecord>
60
66
  export type Threadgates = HydrationMap<Threadgate>
67
+ export type Postgate = RecordInfo<PostgateRecord>
68
+ export type Postgates = HydrationMap<Postgate>
61
69
 
62
70
  export type ThreadRef = ItemRef & { threadRoot: string }
63
71
 
@@ -83,7 +91,21 @@ export class FeedHydrator {
83
91
  return need.reduce((acc, uri, i) => {
84
92
  const record = parseRecord<PostRecord>(res.records[i], includeTakedowns)
85
93
  const violatesThreadGate = res.meta[i].violatesThreadGate
86
- return acc.set(uri, record ? { ...record, violatesThreadGate } : null)
94
+ const violatesEmbeddingRules = res.meta[i].violatesEmbeddingRules
95
+ const hasThreadGate = res.meta[i].hasThreadGate
96
+ const hasPostGate = res.meta[i].hasPostGate
97
+ return acc.set(
98
+ uri,
99
+ record
100
+ ? {
101
+ ...record,
102
+ violatesThreadGate,
103
+ violatesEmbeddingRules,
104
+ hasThreadGate,
105
+ hasPostGate,
106
+ }
107
+ : null,
108
+ )
87
109
  }, base)
88
110
  }
89
111
 
@@ -135,6 +157,7 @@ export class FeedHydrator {
135
157
  likes: counts.likes[i] ?? 0,
136
158
  reposts: counts.reposts[i] ?? 0,
137
159
  replies: counts.replies[i] ?? 0,
160
+ quotes: counts.quotes[i] ?? 0,
138
161
  })
139
162
  }, new HydrationMap<PostAgg>())
140
163
  }
@@ -185,14 +208,14 @@ export class FeedHydrator {
185
208
  includeTakedowns = false,
186
209
  ): Promise<Threadgates> {
187
210
  if (!postUris.length) return new HydrationMap<Threadgate>()
188
- const uris = postUris.map((uri) => {
189
- const parsed = new AtUri(uri)
190
- return AtUri.make(
191
- parsed.hostname,
192
- ids.AppBskyFeedThreadgate,
193
- parsed.rkey,
194
- ).toString()
195
- })
211
+ const uris = postUris.map(postUriToThreadgateUri)
212
+ return this.getThreadgateRecords(uris, includeTakedowns)
213
+ }
214
+
215
+ async getThreadgateRecords(
216
+ uris: string[],
217
+ includeTakedowns = false,
218
+ ): Promise<Threadgates> {
196
219
  const res = await this.dataplane.getThreadGateRecords({ uris })
197
220
  return uris.reduce((acc, uri, i) => {
198
221
  const record = parseRecord<ThreadgateRecord>(
@@ -203,6 +226,29 @@ export class FeedHydrator {
203
226
  }, new HydrationMap<Threadgate>())
204
227
  }
205
228
 
229
+ async getPostgatesForPosts(
230
+ postUris: string[],
231
+ includeTakedowns = false,
232
+ ): Promise<Postgates> {
233
+ if (!postUris.length) return new HydrationMap<Postgate>()
234
+ const uris = postUris.map(postUriToPostgateUri)
235
+ return this.getPostgateRecords(uris, includeTakedowns)
236
+ }
237
+
238
+ async getPostgateRecords(
239
+ uris: string[],
240
+ includeTakedowns = false,
241
+ ): Promise<Postgates> {
242
+ const res = await this.dataplane.getPostGateRecords({ uris })
243
+ return uris.reduce((acc, uri, i) => {
244
+ const record = parseRecord<PostgateRecord>(
245
+ res.records[i],
246
+ includeTakedowns,
247
+ )
248
+ return acc.set(uri, record ?? null)
249
+ }, new HydrationMap<Postgate>())
250
+ }
251
+
206
252
  async getLikes(uris: string[], includeTakedowns = false): Promise<Likes> {
207
253
  if (!uris.length) return new HydrationMap<Like>()
208
254
  const res = await this.dataplane.getLikeRecords({ uris })