@atproto/bsky 0.0.16 → 0.0.17

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 (109) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/cache/read-through.d.ts +30 -0
  3. package/dist/config.d.ts +18 -0
  4. package/dist/context.d.ts +6 -6
  5. package/dist/daemon/config.d.ts +15 -0
  6. package/dist/daemon/context.d.ts +15 -0
  7. package/dist/daemon/index.d.ts +23 -0
  8. package/dist/daemon/logger.d.ts +3 -0
  9. package/dist/daemon/notifications.d.ts +18 -0
  10. package/dist/daemon/services.d.ts +11 -0
  11. package/dist/db/database-schema.d.ts +1 -2
  12. package/dist/db/index.js +16 -1
  13. package/dist/db/index.js.map +3 -3
  14. package/dist/db/migrations/20231205T000257238Z-remove-did-cache.d.ts +3 -0
  15. package/dist/db/migrations/index.d.ts +1 -0
  16. package/dist/did-cache.d.ts +10 -7
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.js +1917 -934
  19. package/dist/index.js.map +3 -3
  20. package/dist/indexer/context.d.ts +2 -0
  21. package/dist/indexer/index.d.ts +1 -0
  22. package/dist/lexicon/index.d.ts +12 -0
  23. package/dist/lexicon/lexicons.d.ts +134 -0
  24. package/dist/lexicon/types/com/atproto/admin/deleteAccount.d.ts +25 -0
  25. package/dist/lexicon/types/com/atproto/temp/importRepo.d.ts +32 -0
  26. package/dist/lexicon/types/com/atproto/temp/pushBlob.d.ts +25 -0
  27. package/dist/lexicon/types/com/atproto/temp/transferAccount.d.ts +42 -0
  28. package/dist/logger.d.ts +1 -0
  29. package/dist/redis.d.ts +10 -1
  30. package/dist/services/actor/index.d.ts +18 -4
  31. package/dist/services/actor/views.d.ts +4 -3
  32. package/dist/services/feed/index.d.ts +6 -4
  33. package/dist/services/feed/views.d.ts +5 -4
  34. package/dist/services/index.d.ts +3 -7
  35. package/dist/services/label/index.d.ts +10 -4
  36. package/dist/services/moderation/index.d.ts +0 -1
  37. package/dist/services/types.d.ts +3 -0
  38. package/dist/services/util/notification.d.ts +5 -0
  39. package/dist/services/util/post.d.ts +6 -6
  40. package/dist/util/retry.d.ts +1 -6
  41. package/package.json +5 -5
  42. package/src/cache/read-through.ts +151 -0
  43. package/src/config.ts +90 -1
  44. package/src/context.ts +7 -7
  45. package/src/daemon/config.ts +60 -0
  46. package/src/daemon/context.ts +27 -0
  47. package/src/daemon/index.ts +78 -0
  48. package/src/daemon/logger.ts +6 -0
  49. package/src/daemon/notifications.ts +54 -0
  50. package/src/daemon/services.ts +22 -0
  51. package/src/db/database-schema.ts +0 -2
  52. package/src/db/migrations/20231205T000257238Z-remove-did-cache.ts +14 -0
  53. package/src/db/migrations/index.ts +1 -0
  54. package/src/did-cache.ts +33 -56
  55. package/src/feed-gen/index.ts +0 -4
  56. package/src/index.ts +55 -16
  57. package/src/indexer/context.ts +5 -0
  58. package/src/indexer/index.ts +10 -7
  59. package/src/lexicon/index.ts +50 -0
  60. package/src/lexicon/lexicons.ts +156 -0
  61. package/src/lexicon/types/com/atproto/admin/deleteAccount.ts +38 -0
  62. package/src/lexicon/types/com/atproto/temp/importRepo.ts +45 -0
  63. package/src/lexicon/types/com/atproto/temp/pushBlob.ts +39 -0
  64. package/src/lexicon/types/com/atproto/temp/transferAccount.ts +62 -0
  65. package/src/logger.ts +2 -0
  66. package/src/redis.ts +43 -3
  67. package/src/services/actor/index.ts +55 -7
  68. package/src/services/actor/views.ts +13 -7
  69. package/src/services/feed/index.ts +27 -13
  70. package/src/services/feed/views.ts +20 -10
  71. package/src/services/index.ts +14 -14
  72. package/src/services/indexing/index.ts +7 -10
  73. package/src/services/indexing/plugins/post.ts +13 -0
  74. package/src/services/label/index.ts +66 -22
  75. package/src/services/moderation/index.ts +1 -1
  76. package/src/services/moderation/status.ts +1 -4
  77. package/src/services/types.ts +4 -0
  78. package/src/services/util/notification.ts +70 -0
  79. package/src/util/retry.ts +1 -44
  80. package/tests/admin/get-repo.test.ts +5 -3
  81. package/tests/admin/moderation.test.ts +2 -2
  82. package/tests/admin/repo-search.test.ts +1 -0
  83. package/tests/algos/hot-classic.test.ts +1 -2
  84. package/tests/auth.test.ts +1 -1
  85. package/tests/auto-moderator/labeler.test.ts +19 -20
  86. package/tests/auto-moderator/takedowns.test.ts +16 -10
  87. package/tests/blob-resolver.test.ts +4 -2
  88. package/tests/daemon.test.ts +191 -0
  89. package/tests/did-cache.test.ts +20 -5
  90. package/tests/handle-invalidation.test.ts +1 -5
  91. package/tests/indexing.test.ts +20 -13
  92. package/tests/redis-cache.test.ts +231 -0
  93. package/tests/seeds/basic.ts +3 -0
  94. package/tests/subscription/repo.test.ts +4 -7
  95. package/tests/views/profile.test.ts +0 -1
  96. package/tests/views/thread.test.ts +73 -78
  97. package/tests/views/threadgating.test.ts +38 -0
  98. package/dist/db/tables/did-cache.d.ts +0 -10
  99. package/dist/feed-gen/best-of-follows.d.ts +0 -29
  100. package/dist/feed-gen/whats-hot.d.ts +0 -29
  101. package/dist/feed-gen/with-friends.d.ts +0 -3
  102. package/dist/label-cache.d.ts +0 -19
  103. package/src/db/tables/did-cache.ts +0 -13
  104. package/src/feed-gen/best-of-follows.ts +0 -77
  105. package/src/feed-gen/whats-hot.ts +0 -101
  106. package/src/feed-gen/with-friends.ts +0 -43
  107. package/src/label-cache.ts +0 -90
  108. package/tests/algos/whats-hot.test.ts +0 -118
  109. package/tests/algos/with-friends.test.ts +0 -145
@@ -165,22 +165,21 @@ describe('pds thread views', () => {
165
165
 
166
166
  describe('takedown', () => {
167
167
  it('blocks post by actor', async () => {
168
- const { data: modAction } =
169
- await agent.api.com.atproto.admin.emitModerationEvent(
170
- {
171
- event: { $type: 'com.atproto.admin.defs#modEventTakedown' },
172
- subject: {
173
- $type: 'com.atproto.admin.defs#repoRef',
174
- did: alice,
175
- },
176
- createdBy: 'did:example:admin',
177
- reason: 'Y',
178
- },
179
- {
180
- encoding: 'application/json',
181
- headers: network.pds.adminAuthHeaders(),
168
+ await agent.api.com.atproto.admin.emitModerationEvent(
169
+ {
170
+ event: { $type: 'com.atproto.admin.defs#modEventTakedown' },
171
+ subject: {
172
+ $type: 'com.atproto.admin.defs#repoRef',
173
+ did: alice,
182
174
  },
183
- )
175
+ createdBy: 'did:example:admin',
176
+ reason: 'Y',
177
+ },
178
+ {
179
+ encoding: 'application/json',
180
+ headers: network.pds.adminAuthHeaders(),
181
+ },
182
+ )
184
183
 
185
184
  // Same as shallow post thread test, minus alice
186
185
  const promise = agent.api.app.bsky.feed.getPostThread(
@@ -211,22 +210,21 @@ describe('pds thread views', () => {
211
210
  })
212
211
 
213
212
  it('blocks replies by actor', async () => {
214
- const { data: modAction } =
215
- await agent.api.com.atproto.admin.emitModerationEvent(
216
- {
217
- event: { $type: 'com.atproto.admin.defs#modEventTakedown' },
218
- subject: {
219
- $type: 'com.atproto.admin.defs#repoRef',
220
- did: carol,
221
- },
222
- createdBy: 'did:example:admin',
223
- reason: 'Y',
224
- },
225
- {
226
- encoding: 'application/json',
227
- headers: network.pds.adminAuthHeaders(),
213
+ await agent.api.com.atproto.admin.emitModerationEvent(
214
+ {
215
+ event: { $type: 'com.atproto.admin.defs#modEventTakedown' },
216
+ subject: {
217
+ $type: 'com.atproto.admin.defs#repoRef',
218
+ did: carol,
228
219
  },
229
- )
220
+ createdBy: 'did:example:admin',
221
+ reason: 'Y',
222
+ },
223
+ {
224
+ encoding: 'application/json',
225
+ headers: network.pds.adminAuthHeaders(),
226
+ },
227
+ )
230
228
 
231
229
  // Same as deep post thread test, minus carol
232
230
  const thread = await agent.api.app.bsky.feed.getPostThread(
@@ -255,22 +253,21 @@ describe('pds thread views', () => {
255
253
  })
256
254
 
257
255
  it('blocks ancestors by actor', async () => {
258
- const { data: modAction } =
259
- await agent.api.com.atproto.admin.emitModerationEvent(
260
- {
261
- event: { $type: 'com.atproto.admin.defs#modEventTakedown' },
262
- subject: {
263
- $type: 'com.atproto.admin.defs#repoRef',
264
- did: bob,
265
- },
266
- createdBy: 'did:example:admin',
267
- reason: 'Y',
268
- },
269
- {
270
- encoding: 'application/json',
271
- headers: network.pds.adminAuthHeaders(),
256
+ await agent.api.com.atproto.admin.emitModerationEvent(
257
+ {
258
+ event: { $type: 'com.atproto.admin.defs#modEventTakedown' },
259
+ subject: {
260
+ $type: 'com.atproto.admin.defs#repoRef',
261
+ did: bob,
272
262
  },
273
- )
263
+ createdBy: 'did:example:admin',
264
+ reason: 'Y',
265
+ },
266
+ {
267
+ encoding: 'application/json',
268
+ headers: network.pds.adminAuthHeaders(),
269
+ },
270
+ )
274
271
 
275
272
  // Same as ancestor post thread test, minus bob
276
273
  const thread = await agent.api.app.bsky.feed.getPostThread(
@@ -300,23 +297,22 @@ describe('pds thread views', () => {
300
297
 
301
298
  it('blocks post by record', async () => {
302
299
  const postRef = sc.posts[alice][1].ref
303
- const { data: modAction } =
304
- await agent.api.com.atproto.admin.emitModerationEvent(
305
- {
306
- event: { $type: 'com.atproto.admin.defs#modEventTakedown' },
307
- subject: {
308
- $type: 'com.atproto.repo.strongRef',
309
- uri: postRef.uriStr,
310
- cid: postRef.cidStr,
311
- },
312
- createdBy: 'did:example:admin',
313
- reason: 'Y',
314
- },
315
- {
316
- encoding: 'application/json',
317
- headers: network.pds.adminAuthHeaders(),
300
+ await agent.api.com.atproto.admin.emitModerationEvent(
301
+ {
302
+ event: { $type: 'com.atproto.admin.defs#modEventTakedown' },
303
+ subject: {
304
+ $type: 'com.atproto.repo.strongRef',
305
+ uri: postRef.uriStr,
306
+ cid: postRef.cidStr,
318
307
  },
319
- )
308
+ createdBy: 'did:example:admin',
309
+ reason: 'Y',
310
+ },
311
+ {
312
+ encoding: 'application/json',
313
+ headers: network.pds.adminAuthHeaders(),
314
+ },
315
+ )
320
316
 
321
317
  const promise = agent.api.app.bsky.feed.getPostThread(
322
318
  { depth: 1, uri: postRef.uriStr },
@@ -354,23 +350,22 @@ describe('pds thread views', () => {
354
350
 
355
351
  const parent = threadPreTakedown.data.thread.parent?.['post']
356
352
 
357
- const { data: modAction } =
358
- await agent.api.com.atproto.admin.emitModerationEvent(
359
- {
360
- event: { $type: 'com.atproto.admin.defs#modEventTakedown' },
361
- subject: {
362
- $type: 'com.atproto.repo.strongRef',
363
- uri: parent.uri,
364
- cid: parent.cid,
365
- },
366
- createdBy: 'did:example:admin',
367
- reason: 'Y',
368
- },
369
- {
370
- encoding: 'application/json',
371
- headers: network.pds.adminAuthHeaders(),
353
+ await agent.api.com.atproto.admin.emitModerationEvent(
354
+ {
355
+ event: { $type: 'com.atproto.admin.defs#modEventTakedown' },
356
+ subject: {
357
+ $type: 'com.atproto.repo.strongRef',
358
+ uri: parent.uri,
359
+ cid: parent.cid,
372
360
  },
373
- )
361
+ createdBy: 'did:example:admin',
362
+ reason: 'Y',
363
+ },
364
+ {
365
+ encoding: 'application/json',
366
+ headers: network.pds.adminAuthHeaders(),
367
+ },
368
+ )
374
369
 
375
370
  // Same as ancestor post thread test, minus parent post
376
371
  const thread = await agent.api.app.bsky.feed.getPostThread(
@@ -407,7 +402,7 @@ describe('pds thread views', () => {
407
402
  const post1 = threadPreTakedown.data.thread.replies?.[0].post
408
403
  const post2 = threadPreTakedown.data.thread.replies?.[1].replies[0].post
409
404
 
410
- const actionResults = await Promise.all(
405
+ await Promise.all(
411
406
  [post1, post2].map((post) =>
412
407
  agent.api.com.atproto.admin.emitModerationEvent(
413
408
  {
@@ -49,6 +49,7 @@ describe('views with thread gating', () => {
49
49
  { post: post.ref.uriStr, createdAt: iso(), allow: [] },
50
50
  sc.getHeaders(sc.dids.carol),
51
51
  )
52
+ await network.processAll()
52
53
  await sc.reply(sc.dids.alice, post.ref, post.ref, 'empty rules reply')
53
54
  await network.processAll()
54
55
  const {
@@ -64,6 +65,33 @@ describe('views with thread gating', () => {
64
65
  await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, true)
65
66
  })
66
67
 
68
+ it('does not generate notifications when post violates threadgate.', async () => {
69
+ const post = await sc.post(sc.dids.carol, 'notifications')
70
+ await pdsAgent.api.app.bsky.feed.threadgate.create(
71
+ { repo: sc.dids.carol, rkey: post.ref.uri.rkey },
72
+ { post: post.ref.uriStr, createdAt: iso(), allow: [] },
73
+ sc.getHeaders(sc.dids.carol),
74
+ )
75
+ await network.processAll()
76
+ const reply = await sc.reply(
77
+ sc.dids.alice,
78
+ post.ref,
79
+ post.ref,
80
+ 'notifications reply',
81
+ )
82
+ await network.processAll()
83
+ const {
84
+ data: { notifications },
85
+ } = await agent.api.app.bsky.notification.listNotifications(
86
+ {},
87
+ { headers: await network.serviceHeaders(sc.dids.carol) },
88
+ )
89
+ const notificationFromReply = notifications.find(
90
+ (notif) => notif.uri === reply.ref.uriStr,
91
+ )
92
+ expect(notificationFromReply).toBeUndefined()
93
+ })
94
+
67
95
  it('applies gate for mention rule.', async () => {
68
96
  const post = await sc.post(
69
97
  sc.dids.carol,
@@ -92,6 +120,7 @@ describe('views with thread gating', () => {
92
120
  },
93
121
  sc.getHeaders(sc.dids.carol),
94
122
  )
123
+ await network.processAll()
95
124
  await sc.reply(
96
125
  sc.dids.alice,
97
126
  post.ref,
@@ -141,6 +170,7 @@ describe('views with thread gating', () => {
141
170
  },
142
171
  sc.getHeaders(sc.dids.carol),
143
172
  )
173
+ await network.processAll()
144
174
  // carol only follows alice
145
175
  await sc.reply(
146
176
  sc.dids.dan,
@@ -231,6 +261,7 @@ describe('views with thread gating', () => {
231
261
  },
232
262
  sc.getHeaders(sc.dids.carol),
233
263
  )
264
+ await network.processAll()
234
265
  //
235
266
  await sc.reply(sc.dids.bob, post.ref, post.ref, 'list rule reply disallow')
236
267
  const aliceReply = await sc.reply(
@@ -298,6 +329,7 @@ describe('views with thread gating', () => {
298
329
  },
299
330
  sc.getHeaders(sc.dids.carol),
300
331
  )
332
+ await network.processAll()
301
333
  await sc.reply(
302
334
  sc.dids.alice,
303
335
  post.ref,
@@ -339,6 +371,7 @@ describe('views with thread gating', () => {
339
371
  },
340
372
  sc.getHeaders(sc.dids.carol),
341
373
  )
374
+ await network.processAll()
342
375
  // carol only follows alice, and the post mentions dan.
343
376
  await sc.reply(sc.dids.bob, post.ref, post.ref, 'multi rule reply disallow')
344
377
  const aliceReply = await sc.reply(
@@ -397,6 +430,7 @@ describe('views with thread gating', () => {
397
430
  { post: post.ref.uriStr, createdAt: iso() },
398
431
  sc.getHeaders(sc.dids.carol),
399
432
  )
433
+ await network.processAll()
400
434
  const aliceReply = await sc.reply(
401
435
  sc.dids.alice,
402
436
  post.ref,
@@ -432,6 +466,7 @@ describe('views with thread gating', () => {
432
466
  },
433
467
  sc.getHeaders(sc.dids.carol),
434
468
  )
469
+ await network.processAll()
435
470
  // carol only follows alice
436
471
  const orphanedReply = await sc.reply(
437
472
  sc.dids.alice,
@@ -493,6 +528,7 @@ describe('views with thread gating', () => {
493
528
  { post: post.ref.uriStr, createdAt: iso(), allow: [] },
494
529
  sc.getHeaders(sc.dids.carol),
495
530
  )
531
+ await network.processAll()
496
532
  const selfReply = await sc.reply(
497
533
  sc.dids.carol,
498
534
  post.ref,
@@ -527,6 +563,7 @@ describe('views with thread gating', () => {
527
563
  },
528
564
  sc.getHeaders(sc.dids.carol),
529
565
  )
566
+ await network.processAll()
530
567
  // carol only follows alice
531
568
  const badReply = await sc.reply(
532
569
  sc.dids.dan,
@@ -571,6 +608,7 @@ describe('views with thread gating', () => {
571
608
  { post: postB.ref.uriStr, createdAt: iso(), allow: [] },
572
609
  sc.getHeaders(sc.dids.carol),
573
610
  )
611
+ await network.processAll()
574
612
  await sc.reply(sc.dids.alice, postA.ref, postA.ref, 'ungated reply')
575
613
  await sc.reply(sc.dids.alice, postB.ref, postB.ref, 'ungated reply')
576
614
  await network.processAll()
@@ -1,10 +0,0 @@
1
- import { DidDocument } from '@atproto/identity';
2
- export interface DidCache {
3
- did: string;
4
- doc: DidDocument;
5
- updatedAt: number;
6
- }
7
- export declare const tableName = "did_cache";
8
- export declare type PartialDB = {
9
- [tableName]: DidCache;
10
- };
@@ -1,29 +0,0 @@
1
- import { AlgoHandler } from './types';
2
- import { GenericKeyset } from '../db/pagination';
3
- declare const handler: AlgoHandler;
4
- export default handler;
5
- declare type Result = {
6
- score: number;
7
- cid: string;
8
- };
9
- declare type LabeledResult = {
10
- primary: number;
11
- secondary: string;
12
- };
13
- export declare class ScoreKeyset extends GenericKeyset<Result, LabeledResult> {
14
- labelResult(result: Result): {
15
- primary: number;
16
- secondary: string;
17
- };
18
- labeledResultToCursor(labeled: LabeledResult): {
19
- primary: string;
20
- secondary: string;
21
- };
22
- cursorToLabeledResult(cursor: {
23
- primary: string;
24
- secondary: string;
25
- }): {
26
- primary: number;
27
- secondary: string;
28
- };
29
- }
@@ -1,29 +0,0 @@
1
- import { AlgoHandler } from './types';
2
- import { GenericKeyset } from '../db/pagination';
3
- declare const handler: AlgoHandler;
4
- export default handler;
5
- declare type Result = {
6
- score: number;
7
- cid: string;
8
- };
9
- declare type LabeledResult = {
10
- primary: number;
11
- secondary: string;
12
- };
13
- export declare class ScoreKeyset extends GenericKeyset<Result, LabeledResult> {
14
- labelResult(result: Result): {
15
- primary: number;
16
- secondary: string;
17
- };
18
- labeledResultToCursor(labeled: LabeledResult): {
19
- primary: string;
20
- secondary: string;
21
- };
22
- cursorToLabeledResult(cursor: {
23
- primary: string;
24
- secondary: string;
25
- }): {
26
- primary: number;
27
- secondary: string;
28
- };
29
- }
@@ -1,3 +0,0 @@
1
- import { AlgoHandler } from './types';
2
- declare const handler: AlgoHandler;
3
- export default handler;
@@ -1,19 +0,0 @@
1
- import { PrimaryDatabase } from './db';
2
- import { Label } from './db/tables/label';
3
- export declare class LabelCache {
4
- db: PrimaryDatabase;
5
- bySubject: Record<string, Label[]>;
6
- latestLabel: string;
7
- refreshes: number;
8
- destroyed: boolean;
9
- constructor(db: PrimaryDatabase);
10
- start(): void;
11
- fullRefresh(): Promise<void>;
12
- partialRefresh(): Promise<void>;
13
- poll(): Promise<void>;
14
- processLabels(labels: Label[]): void;
15
- wipeCache(): void;
16
- stop(): void;
17
- forSubject(subject: string, includeNeg?: boolean): Label[];
18
- forSubjects(subjects: string[], includeNeg?: boolean): Label[];
19
- }
@@ -1,13 +0,0 @@
1
- import { DidDocument } from '@atproto/identity'
2
-
3
- export interface DidCache {
4
- did: string
5
- doc: DidDocument
6
- updatedAt: number
7
- }
8
-
9
- export const tableName = 'did_cache'
10
-
11
- export type PartialDB = {
12
- [tableName]: DidCache
13
- }
@@ -1,77 +0,0 @@
1
- import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'
2
- import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton'
3
- import { AlgoHandler, AlgoResponse } from './types'
4
- import { GenericKeyset, paginate } from '../db/pagination'
5
- import AppContext from '../context'
6
-
7
- const handler: AlgoHandler = async (
8
- ctx: AppContext,
9
- params: SkeletonParams,
10
- viewer: string | null,
11
- ): Promise<AlgoResponse> => {
12
- if (!viewer) {
13
- throw new AuthRequiredError('This feed requires being logged-in')
14
- }
15
-
16
- const { limit, cursor } = params
17
- const db = ctx.db.getReplica('feed')
18
- const feedService = ctx.services.feed(db)
19
- const { ref } = db.db.dynamic
20
-
21
- // candidates are ranked within a materialized view by like count, depreciated over time.
22
-
23
- let builder = feedService
24
- .selectPostQb()
25
- .innerJoin('algo_whats_hot_view as candidate', 'candidate.uri', 'post.uri')
26
- .where((qb) =>
27
- qb
28
- .where('post.creator', '=', viewer)
29
- .orWhereExists((inner) =>
30
- inner
31
- .selectFrom('follow')
32
- .where('follow.creator', '=', viewer)
33
- .whereRef('follow.subjectDid', '=', 'post.creator'),
34
- ),
35
- )
36
- .select('candidate.score')
37
- .select('candidate.cid')
38
-
39
- const keyset = new ScoreKeyset(ref('candidate.score'), ref('candidate.cid'))
40
- builder = paginate(builder, { limit, cursor, keyset })
41
-
42
- const feedItems = await builder.execute()
43
-
44
- return {
45
- feedItems,
46
- cursor: keyset.packFromResult(feedItems),
47
- }
48
- }
49
-
50
- export default handler
51
-
52
- type Result = { score: number; cid: string }
53
- type LabeledResult = { primary: number; secondary: string }
54
- export class ScoreKeyset extends GenericKeyset<Result, LabeledResult> {
55
- labelResult(result: Result) {
56
- return {
57
- primary: result.score,
58
- secondary: result.cid,
59
- }
60
- }
61
- labeledResultToCursor(labeled: LabeledResult) {
62
- return {
63
- primary: Math.round(labeled.primary).toString(),
64
- secondary: labeled.secondary,
65
- }
66
- }
67
- cursorToLabeledResult(cursor: { primary: string; secondary: string }) {
68
- const score = parseInt(cursor.primary, 10)
69
- if (isNaN(score)) {
70
- throw new InvalidRequestError('Malformed cursor')
71
- }
72
- return {
73
- primary: score,
74
- secondary: cursor.secondary,
75
- }
76
- }
77
- }
@@ -1,101 +0,0 @@
1
- import { NotEmptyArray } from '@atproto/common'
2
- import { InvalidRequestError } from '@atproto/xrpc-server'
3
- import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton'
4
- import { AlgoHandler, AlgoResponse } from './types'
5
- import { GenericKeyset, paginate } from '../db/pagination'
6
- import AppContext from '../context'
7
- import { valuesList } from '../db/util'
8
- import { sql } from 'kysely'
9
- import { FeedItemType } from '../services/feed/types'
10
-
11
- const NO_WHATS_HOT_LABELS: NotEmptyArray<string> = [
12
- '!no-promote',
13
- 'corpse',
14
- 'self-harm',
15
- 'porn',
16
- 'sexual',
17
- 'nudity',
18
- 'underwear',
19
- ]
20
-
21
- const handler: AlgoHandler = async (
22
- ctx: AppContext,
23
- params: SkeletonParams,
24
- _viewer: string | null,
25
- ): Promise<AlgoResponse> => {
26
- const { limit, cursor } = params
27
- const db = ctx.db.getReplica('feed')
28
-
29
- const { ref } = db.db.dynamic
30
-
31
- // candidates are ranked within a materialized view by like count, depreciated over time.
32
-
33
- let builder = db.db
34
- .selectFrom('algo_whats_hot_view as candidate')
35
- .innerJoin('post', 'post.uri', 'candidate.uri')
36
- .leftJoin('post_embed_record', 'post_embed_record.postUri', 'candidate.uri')
37
- .whereNotExists((qb) =>
38
- qb
39
- .selectFrom('label')
40
- .selectAll()
41
- .whereRef('val', 'in', valuesList(NO_WHATS_HOT_LABELS))
42
- .where('neg', '=', false)
43
- .where((clause) =>
44
- clause
45
- .whereRef('label.uri', '=', ref('post.creator'))
46
- .orWhereRef('label.uri', '=', ref('post.uri'))
47
- .orWhereRef('label.uri', '=', ref('post_embed_record.embedUri')),
48
- ),
49
- )
50
- .select([
51
- sql<FeedItemType>`${'post'}`.as('type'),
52
- 'post.uri as uri',
53
- 'post.uri as postUri',
54
- 'post.creator as originatorDid',
55
- 'post.creator as postAuthorDid',
56
- 'post.replyParent as replyParent',
57
- 'post.replyRoot as replyRoot',
58
- 'post.indexedAt as sortAt',
59
- 'candidate.score',
60
- 'candidate.cid',
61
- ])
62
-
63
- const keyset = new ScoreKeyset(ref('candidate.score'), ref('candidate.cid'))
64
- builder = paginate(builder, { limit, cursor, keyset })
65
-
66
- const feedItems = await builder.execute()
67
-
68
- return {
69
- feedItems,
70
- cursor: keyset.packFromResult(feedItems),
71
- }
72
- }
73
-
74
- export default handler
75
-
76
- type Result = { score: number; cid: string }
77
- type LabeledResult = { primary: number; secondary: string }
78
- export class ScoreKeyset extends GenericKeyset<Result, LabeledResult> {
79
- labelResult(result: Result) {
80
- return {
81
- primary: result.score,
82
- secondary: result.cid,
83
- }
84
- }
85
- labeledResultToCursor(labeled: LabeledResult) {
86
- return {
87
- primary: Math.round(labeled.primary).toString(),
88
- secondary: labeled.secondary,
89
- }
90
- }
91
- cursorToLabeledResult(cursor: { primary: string; secondary: string }) {
92
- const score = parseInt(cursor.primary, 10)
93
- if (isNaN(score)) {
94
- throw new InvalidRequestError('Malformed cursor')
95
- }
96
- return {
97
- primary: score,
98
- secondary: cursor.secondary,
99
- }
100
- }
101
- }
@@ -1,43 +0,0 @@
1
- import AppContext from '../context'
2
- import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton'
3
- import { paginate } from '../db/pagination'
4
- import { AlgoHandler, AlgoResponse } from './types'
5
- import { FeedKeyset, getFeedDateThreshold } from '../api/app/bsky/util/feed'
6
- import { AuthRequiredError } from '@atproto/xrpc-server'
7
-
8
- const handler: AlgoHandler = async (
9
- ctx: AppContext,
10
- params: SkeletonParams,
11
- viewer: string | null,
12
- ): Promise<AlgoResponse> => {
13
- if (!viewer) {
14
- throw new AuthRequiredError('This feed requires being logged-in')
15
- }
16
-
17
- const { cursor, limit = 50 } = params
18
- const db = ctx.db.getReplica('feed')
19
- const feedService = ctx.services.feed(db)
20
- const { ref } = db.db.dynamic
21
-
22
- const keyset = new FeedKeyset(ref('post.sortAt'), ref('post.cid'))
23
- const sortFrom = keyset.unpack(cursor)?.primary
24
-
25
- let postsQb = feedService
26
- .selectPostQb()
27
- .innerJoin('follow', 'follow.subjectDid', 'post.creator')
28
- .innerJoin('post_agg', 'post_agg.uri', 'post.uri')
29
- .where('post_agg.likeCount', '>=', 5)
30
- .where('follow.creator', '=', viewer)
31
- .where('post.sortAt', '>', getFeedDateThreshold(sortFrom))
32
-
33
- postsQb = paginate(postsQb, { limit, cursor, keyset, tryIndex: true })
34
-
35
- const feedItems = await postsQb.execute()
36
-
37
- return {
38
- feedItems,
39
- cursor: keyset.packFromResult(feedItems),
40
- }
41
- }
42
-
43
- export default handler