birdclaw 0.5.1 → 0.7.0
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.
- package/CHANGELOG.md +92 -1
- package/README.md +75 -5
- package/package.json +8 -2
- package/scripts/browser-perf.mjs +1 -0
- package/scripts/start-test-server.mjs +16 -3
- package/src/cli.ts +812 -37
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +37 -7
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/DmWorkspace.tsx +18 -8
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +818 -0
- package/src/components/ProfileAnalysisStream.tsx +428 -0
- package/src/components/ProfilePreview.tsx +120 -9
- package/src/components/SavedTimelineView.tsx +30 -8
- package/src/components/SyncNowButton.tsx +60 -25
- package/src/components/ThemeSlider.tsx +55 -50
- package/src/components/TimelineCard.tsx +240 -92
- package/src/components/TimelineRouteFrame.tsx +38 -8
- package/src/components/TweetMediaGrid.tsx +87 -38
- package/src/components/TweetRichText.tsx +45 -17
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +97 -13
- package/src/lib/account-sync-job.ts +666 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +128 -53
- package/src/lib/archive-finder.ts +78 -63
- package/src/lib/archive-import.ts +1593 -1291
- package/src/lib/authored-live.ts +262 -204
- package/src/lib/avatar-cache.ts +208 -43
- package/src/lib/backup.ts +1536 -954
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +582 -194
- package/src/lib/blocklist.ts +40 -23
- package/src/lib/blocks-write.ts +129 -80
- package/src/lib/blocks.ts +165 -97
- package/src/lib/bookmark-sync-job.ts +250 -160
- package/src/lib/config.ts +35 -2
- package/src/lib/conversation-surface.ts +79 -48
- package/src/lib/data-sources.ts +219 -0
- package/src/lib/db.ts +95 -4
- package/src/lib/dms-live.ts +720 -66
- package/src/lib/effect-runtime.ts +45 -0
- package/src/lib/follow-graph.ts +224 -180
- package/src/lib/geocoding.ts +296 -0
- package/src/lib/http-effect.ts +222 -0
- package/src/lib/inbox.ts +74 -43
- package/src/lib/link-index.ts +88 -76
- package/src/lib/link-insights.ts +24 -0
- package/src/lib/link-preview-metadata.ts +472 -52
- package/src/lib/location.ts +137 -0
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +445 -288
- package/src/lib/mentions-live.ts +549 -354
- package/src/lib/moderation-target.ts +102 -65
- package/src/lib/moderation-write.ts +77 -18
- package/src/lib/mutes-write.ts +129 -80
- package/src/lib/mutes.ts +8 -1
- package/src/lib/network-map.ts +382 -0
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +1317 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-analysis.ts +1272 -0
- package/src/lib/profile-bio-entities.ts +1 -1
- package/src/lib/profile-hydration.ts +124 -72
- package/src/lib/profile-replies.ts +60 -43
- package/src/lib/profile-resolver.ts +402 -294
- package/src/lib/queries.ts +983 -203
- package/src/lib/research.ts +165 -120
- package/src/lib/search-discussion.ts +1016 -0
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +325 -51
- package/src/lib/tweet-account-edges.ts +2 -0
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/tweet-render.ts +141 -1
- package/src/lib/tweet-search-live.ts +565 -0
- package/src/lib/types.ts +75 -3
- package/src/lib/ui.ts +31 -8
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +222 -149
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl-rate-limits.ts +267 -0
- package/src/lib/xurl.ts +1185 -405
- package/src/routeTree.gen.ts +273 -0
- package/src/routes/__root.tsx +24 -5
- package/src/routes/api/action.tsx +127 -78
- package/src/routes/api/avatar.tsx +39 -30
- package/src/routes/api/blocks.tsx +26 -23
- package/src/routes/api/conversation.tsx +25 -14
- package/src/routes/api/data-sources.tsx +24 -0
- package/src/routes/api/inbox.tsx +27 -21
- package/src/routes/api/link-insights.tsx +31 -25
- package/src/routes/api/link-preview.tsx +25 -21
- package/src/routes/api/network-map.tsx +55 -0
- package/src/routes/api/period-digest.tsx +133 -0
- package/src/routes/api/profile-analysis.tsx +152 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +80 -55
- package/src/routes/api/search-discussion.tsx +169 -0
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +75 -29
- package/src/routes/api/xurl-rate-limits.tsx +24 -0
- package/src/routes/data-sources.tsx +255 -0
- package/src/routes/discuss.tsx +419 -0
- package/src/routes/dms.tsx +95 -28
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/network-map.tsx +1035 -0
- package/src/routes/profile-analyze.tsx +112 -0
- package/src/routes/profiles.$handle.tsx +228 -0
- package/src/routes/rate-limits.tsx +309 -0
- package/src/routes/today.tsx +455 -0
- package/src/styles.css +22 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Database } from "./sqlite";
|
|
2
|
-
import {
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { listThreadViaBirdEffect } from "./bird";
|
|
3
4
|
import { getNativeDb } from "./db";
|
|
5
|
+
import { runEffectPromise } from "./effect-runtime";
|
|
4
6
|
import { buildMediaJsonFromIncludes, countTweetMedia } from "./media-includes";
|
|
5
7
|
import type {
|
|
6
8
|
XurlMentionData,
|
|
@@ -11,7 +13,7 @@ import type {
|
|
|
11
13
|
} from "./types";
|
|
12
14
|
import { upsertTweetAccountEdge } from "./tweet-account-edges";
|
|
13
15
|
import { ensureStubProfileForXUser, upsertProfileFromXUser } from "./x-profile";
|
|
14
|
-
import {
|
|
16
|
+
import { getTweetByIdEffect, searchRecentByConversationIdEffect } from "./xurl";
|
|
15
17
|
|
|
16
18
|
const DEFAULT_LIMIT = 30;
|
|
17
19
|
const DEFAULT_DELAY_MS = 1500;
|
|
@@ -21,6 +23,24 @@ const DEFAULT_FALLBACK_DEPTH = 12;
|
|
|
21
23
|
const MAX_XURL_SEARCH_RESULTS = 100;
|
|
22
24
|
|
|
23
25
|
export type MentionThreadsMode = "bird" | "xurl";
|
|
26
|
+
export interface MentionThreadsProgress {
|
|
27
|
+
source: MentionThreadsMode;
|
|
28
|
+
processed: number;
|
|
29
|
+
total: number;
|
|
30
|
+
fetched: number;
|
|
31
|
+
done: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface SyncMentionThreadsOptions {
|
|
34
|
+
account?: string;
|
|
35
|
+
mode?: string;
|
|
36
|
+
limit?: number;
|
|
37
|
+
tweetIds?: string[];
|
|
38
|
+
delayMs?: number;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
all?: boolean;
|
|
41
|
+
maxPages?: number;
|
|
42
|
+
onProgress?: (progress: MentionThreadsProgress) => void;
|
|
43
|
+
}
|
|
24
44
|
|
|
25
45
|
interface LocalMention {
|
|
26
46
|
id: string;
|
|
@@ -28,6 +48,15 @@ interface LocalMention {
|
|
|
28
48
|
conversationId?: string;
|
|
29
49
|
rawTweet?: XurlMentionData;
|
|
30
50
|
}
|
|
51
|
+
interface ThreadFetchResult {
|
|
52
|
+
strategy: string;
|
|
53
|
+
payload: XurlMentionsResponse;
|
|
54
|
+
pages?: number;
|
|
55
|
+
fallbackDepth?: number;
|
|
56
|
+
generalReadTweets: number;
|
|
57
|
+
truncated?: boolean;
|
|
58
|
+
warnings: string[];
|
|
59
|
+
}
|
|
31
60
|
|
|
32
61
|
function assertPositiveInteger(value: number, name: string) {
|
|
33
62
|
if (!Number.isFinite(value) || value < 1) {
|
|
@@ -54,8 +83,15 @@ function parseMode(value: string | undefined): MentionThreadsMode {
|
|
|
54
83
|
return mode;
|
|
55
84
|
}
|
|
56
85
|
|
|
57
|
-
function
|
|
58
|
-
return new
|
|
86
|
+
function toError(error: unknown) {
|
|
87
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function trySync<T>(try_: () => T) {
|
|
91
|
+
return Effect.try({
|
|
92
|
+
try: try_,
|
|
93
|
+
catch: toError,
|
|
94
|
+
});
|
|
59
95
|
}
|
|
60
96
|
|
|
61
97
|
function getRemainingThreadTimeoutMs(
|
|
@@ -241,6 +277,62 @@ function listRecentMentions(
|
|
|
241
277
|
});
|
|
242
278
|
}
|
|
243
279
|
|
|
280
|
+
function listMentionsByIds(
|
|
281
|
+
db: Database,
|
|
282
|
+
accountId: string,
|
|
283
|
+
tweetIds: string[],
|
|
284
|
+
limit: number,
|
|
285
|
+
): LocalMention[] {
|
|
286
|
+
const ids = [...new Set(tweetIds.filter((id) => id.trim().length > 0))].slice(
|
|
287
|
+
0,
|
|
288
|
+
limit,
|
|
289
|
+
);
|
|
290
|
+
if (ids.length === 0) return [];
|
|
291
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
292
|
+
const rows = db
|
|
293
|
+
.prepare(
|
|
294
|
+
`
|
|
295
|
+
select
|
|
296
|
+
t.id,
|
|
297
|
+
t.created_at as createdAt,
|
|
298
|
+
t.reply_to_id as replyToId,
|
|
299
|
+
coalesce(edge.raw_json, '{}') as rawJson
|
|
300
|
+
from tweets t
|
|
301
|
+
left join tweet_account_edges edge
|
|
302
|
+
on edge.tweet_id = t.id
|
|
303
|
+
and edge.account_id = ?
|
|
304
|
+
and edge.kind = 'mention'
|
|
305
|
+
where t.id in (${placeholders})
|
|
306
|
+
and (
|
|
307
|
+
edge.tweet_id is not null
|
|
308
|
+
or (t.kind = 'mention' and t.account_id = ?)
|
|
309
|
+
)
|
|
310
|
+
order by t.created_at desc
|
|
311
|
+
limit ?
|
|
312
|
+
`,
|
|
313
|
+
)
|
|
314
|
+
.all(accountId, ...ids, accountId, limit) as Array<{
|
|
315
|
+
id: string;
|
|
316
|
+
createdAt: string;
|
|
317
|
+
replyToId: string | null;
|
|
318
|
+
rawJson: string | null;
|
|
319
|
+
}>;
|
|
320
|
+
|
|
321
|
+
return rows.map((row) => {
|
|
322
|
+
const rawTweet = parseRawTweet(row.rawJson);
|
|
323
|
+
return {
|
|
324
|
+
id: row.id,
|
|
325
|
+
replyToId:
|
|
326
|
+
row.replyToId ?? (rawTweet ? getReplyToId(rawTweet) : undefined),
|
|
327
|
+
conversationId:
|
|
328
|
+
typeof rawTweet?.conversation_id === "string"
|
|
329
|
+
? rawTweet.conversation_id
|
|
330
|
+
: undefined,
|
|
331
|
+
rawTweet,
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
244
336
|
function mergeMentionThreadIntoLocalStore({
|
|
245
337
|
db,
|
|
246
338
|
accountId,
|
|
@@ -341,7 +433,7 @@ function mergeMentionThreadIntoLocalStore({
|
|
|
341
433
|
})();
|
|
342
434
|
}
|
|
343
435
|
|
|
344
|
-
|
|
436
|
+
function fetchConversationViaRecentSearchEffect({
|
|
345
437
|
conversationId,
|
|
346
438
|
all,
|
|
347
439
|
maxPages,
|
|
@@ -354,39 +446,46 @@ async function fetchConversationViaRecentSearch({
|
|
|
354
446
|
timeoutMs: number;
|
|
355
447
|
deadlineMs: number;
|
|
356
448
|
}) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
449
|
+
return Effect.gen(function* () {
|
|
450
|
+
const pages: XurlTweetsResponse[] = [];
|
|
451
|
+
let nextToken: string | undefined;
|
|
452
|
+
let pageCount = 0;
|
|
453
|
+
|
|
454
|
+
do {
|
|
455
|
+
const payload = yield* searchRecentByConversationIdEffect(
|
|
456
|
+
conversationId,
|
|
457
|
+
{
|
|
458
|
+
maxResults: MAX_XURL_SEARCH_RESULTS,
|
|
459
|
+
paginationToken: nextToken,
|
|
460
|
+
timeoutMs: yield* trySync(() =>
|
|
461
|
+
getRemainingThreadTimeoutMs(deadlineMs, timeoutMs),
|
|
462
|
+
),
|
|
463
|
+
},
|
|
464
|
+
);
|
|
465
|
+
pages.push(payload);
|
|
466
|
+
nextToken =
|
|
467
|
+
typeof payload.meta?.next_token === "string"
|
|
468
|
+
? payload.meta.next_token
|
|
469
|
+
: undefined;
|
|
470
|
+
pageCount += 1;
|
|
471
|
+
} while (
|
|
472
|
+
(all || maxPages !== undefined) &&
|
|
473
|
+
nextToken &&
|
|
474
|
+
(maxPages === undefined || pageCount < maxPages)
|
|
475
|
+
);
|
|
378
476
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
477
|
+
const payload = mergePayloads(pages);
|
|
478
|
+
const paginationRequested = all || maxPages !== undefined;
|
|
479
|
+
return {
|
|
480
|
+
payload,
|
|
481
|
+
pages: pageCount,
|
|
482
|
+
truncated: paginationRequested && Boolean(nextToken),
|
|
483
|
+
generalReadTweets: payload.data.length,
|
|
484
|
+
};
|
|
485
|
+
});
|
|
387
486
|
}
|
|
388
487
|
|
|
389
|
-
|
|
488
|
+
function fetchParentChainViaXurlEffect({
|
|
390
489
|
mention,
|
|
391
490
|
maxDepth,
|
|
392
491
|
timeoutMs,
|
|
@@ -397,76 +496,82 @@ async function fetchParentChainViaXurl({
|
|
|
397
496
|
timeoutMs: number;
|
|
398
497
|
deadlineMs: number;
|
|
399
498
|
}) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
let shouldUseRawAnchor = Boolean(rawAnchorPayload);
|
|
412
|
-
|
|
413
|
-
if (!nextParentId) {
|
|
414
|
-
const anchorPayload = await getTweetById(mention.id, {
|
|
415
|
-
timeoutMs: getRemainingThreadTimeoutMs(deadlineMs, timeoutMs),
|
|
416
|
-
});
|
|
417
|
-
pages.push(anchorPayload);
|
|
418
|
-
generalReadTweets += anchorPayload.data.length;
|
|
419
|
-
const anchorTweet = anchorPayload.data[0];
|
|
420
|
-
if (anchorTweet) {
|
|
421
|
-
shouldUseRawAnchor = false;
|
|
422
|
-
seenTweetIds.add(anchorTweet.id);
|
|
423
|
-
nextParentId = anchorTweet.in_reply_to_user_id
|
|
424
|
-
? getReplyToId(anchorTweet)
|
|
499
|
+
return Effect.gen(function* () {
|
|
500
|
+
const pages: XurlTweetsResponse[] = [];
|
|
501
|
+
const warnings: string[] = [];
|
|
502
|
+
const seenTweetIds = new Set([mention.id]);
|
|
503
|
+
let nextParentId = mention.replyToId;
|
|
504
|
+
let fallbackDepth = 0;
|
|
505
|
+
let generalReadTweets = 0;
|
|
506
|
+
|
|
507
|
+
const rawAnchorPayload =
|
|
508
|
+
mention.rawTweet && mention.rawTweet.id === mention.id
|
|
509
|
+
? ({ data: [mention.rawTweet] } satisfies XurlTweetsResponse)
|
|
425
510
|
: undefined;
|
|
426
|
-
|
|
427
|
-
}
|
|
511
|
+
let shouldUseRawAnchor = Boolean(rawAnchorPayload);
|
|
428
512
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
513
|
+
if (!nextParentId) {
|
|
514
|
+
const anchorPayload = yield* getTweetByIdEffect(mention.id, {
|
|
515
|
+
timeoutMs: yield* trySync(() =>
|
|
516
|
+
getRemainingThreadTimeoutMs(deadlineMs, timeoutMs),
|
|
517
|
+
),
|
|
518
|
+
});
|
|
519
|
+
pages.push(anchorPayload);
|
|
520
|
+
generalReadTweets += anchorPayload.data.length;
|
|
521
|
+
const anchorTweet = anchorPayload.data[0];
|
|
522
|
+
if (anchorTweet) {
|
|
523
|
+
shouldUseRawAnchor = false;
|
|
524
|
+
seenTweetIds.add(anchorTweet.id);
|
|
525
|
+
nextParentId = anchorTweet.in_reply_to_user_id
|
|
526
|
+
? getReplyToId(anchorTweet)
|
|
527
|
+
: undefined;
|
|
528
|
+
}
|
|
439
529
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
);
|
|
444
|
-
break;
|
|
530
|
+
|
|
531
|
+
if (shouldUseRawAnchor && rawAnchorPayload) {
|
|
532
|
+
pages.unshift(rawAnchorPayload);
|
|
445
533
|
}
|
|
446
534
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
535
|
+
while (nextParentId) {
|
|
536
|
+
if (fallbackDepth >= maxDepth) {
|
|
537
|
+
warnings.push(
|
|
538
|
+
`fallback parent-chain depth cap reached for ${mention.id} after ${maxDepth} hops`,
|
|
539
|
+
);
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
if (seenTweetIds.has(nextParentId)) {
|
|
543
|
+
warnings.push(
|
|
544
|
+
`fallback parent-chain cycle detected for ${mention.id} at ${nextParentId}`,
|
|
545
|
+
);
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
fallbackDepth += 1;
|
|
550
|
+
const parentPayload = yield* getTweetByIdEffect(nextParentId, {
|
|
551
|
+
timeoutMs: yield* trySync(() =>
|
|
552
|
+
getRemainingThreadTimeoutMs(deadlineMs, timeoutMs),
|
|
553
|
+
),
|
|
554
|
+
});
|
|
555
|
+
pages.push(parentPayload);
|
|
556
|
+
generalReadTweets += parentPayload.data.length;
|
|
557
|
+
const parentTweet = parentPayload.data[0];
|
|
558
|
+
if (!parentTweet) {
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
seenTweetIds.add(parentTweet.id);
|
|
562
|
+
nextParentId = parentTweet.in_reply_to_user_id
|
|
563
|
+
? getReplyToId(parentTweet)
|
|
564
|
+
: undefined;
|
|
456
565
|
}
|
|
457
|
-
seenTweetIds.add(parentTweet.id);
|
|
458
|
-
nextParentId = parentTweet.in_reply_to_user_id
|
|
459
|
-
? getReplyToId(parentTweet)
|
|
460
|
-
: undefined;
|
|
461
|
-
}
|
|
462
566
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
567
|
+
const payload = mergePayloads(pages);
|
|
568
|
+
return {
|
|
569
|
+
payload,
|
|
570
|
+
fallbackDepth,
|
|
571
|
+
warnings,
|
|
572
|
+
generalReadTweets,
|
|
573
|
+
};
|
|
574
|
+
});
|
|
470
575
|
}
|
|
471
576
|
|
|
472
577
|
function findMissingAncestorId(
|
|
@@ -505,7 +610,7 @@ function findMissingAncestorId(
|
|
|
505
610
|
return undefined;
|
|
506
611
|
}
|
|
507
612
|
|
|
508
|
-
|
|
613
|
+
function fetchThreadContextViaXurlEffect({
|
|
509
614
|
mention,
|
|
510
615
|
all,
|
|
511
616
|
maxPages,
|
|
@@ -518,183 +623,230 @@ async function fetchThreadContextViaXurl({
|
|
|
518
623
|
maxFallbackDepth: number;
|
|
519
624
|
timeoutMs: number;
|
|
520
625
|
}) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
if (mention.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
626
|
+
return Effect.gen(function* () {
|
|
627
|
+
const deadlineMs = Date.now() + timeoutMs;
|
|
628
|
+
if (!mention.conversationId) {
|
|
629
|
+
if (mention.replyToId) {
|
|
630
|
+
const fallback = yield* fetchParentChainViaXurlEffect({
|
|
631
|
+
mention,
|
|
632
|
+
maxDepth: maxFallbackDepth,
|
|
633
|
+
timeoutMs,
|
|
634
|
+
deadlineMs,
|
|
635
|
+
});
|
|
636
|
+
return {
|
|
637
|
+
strategy: "parent_walk" as const,
|
|
638
|
+
pages: 0,
|
|
639
|
+
truncated: false,
|
|
640
|
+
payload: fallback.payload,
|
|
641
|
+
fallbackDepth: fallback.fallbackDepth,
|
|
642
|
+
generalReadTweets: fallback.generalReadTweets,
|
|
643
|
+
warnings: [
|
|
644
|
+
`missing conversation_id for ${mention.id}; used parent walk`,
|
|
645
|
+
...fallback.warnings,
|
|
646
|
+
],
|
|
647
|
+
};
|
|
648
|
+
}
|
|
530
649
|
return {
|
|
531
|
-
strategy: "
|
|
650
|
+
strategy: "skipped:no_conversation_id" as const,
|
|
651
|
+
payload: { data: [] } satisfies XurlMentionsResponse,
|
|
532
652
|
pages: 0,
|
|
653
|
+
fallbackDepth: 0,
|
|
654
|
+
generalReadTweets: 0,
|
|
655
|
+
warnings: [`skipped ${mention.id}: missing conversation_id`],
|
|
533
656
|
truncated: false,
|
|
534
|
-
payload: fallback.payload,
|
|
535
|
-
fallbackDepth: fallback.fallbackDepth,
|
|
536
|
-
generalReadTweets: fallback.generalReadTweets,
|
|
537
|
-
warnings: [
|
|
538
|
-
`missing conversation_id for ${mention.id}; used parent walk`,
|
|
539
|
-
...fallback.warnings,
|
|
540
|
-
],
|
|
541
657
|
};
|
|
542
658
|
}
|
|
543
|
-
return {
|
|
544
|
-
strategy: "skipped:no_conversation_id" as const,
|
|
545
|
-
payload: { data: [] } satisfies XurlMentionsResponse,
|
|
546
|
-
pages: 0,
|
|
547
|
-
fallbackDepth: 0,
|
|
548
|
-
generalReadTweets: 0,
|
|
549
|
-
warnings: [`skipped ${mention.id}: missing conversation_id`],
|
|
550
|
-
truncated: false,
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
659
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
660
|
+
const search = yield* fetchConversationViaRecentSearchEffect({
|
|
661
|
+
conversationId: mention.conversationId,
|
|
662
|
+
all,
|
|
663
|
+
maxPages,
|
|
664
|
+
timeoutMs,
|
|
665
|
+
deadlineMs,
|
|
666
|
+
});
|
|
667
|
+
if (search.payload.data.length > 0) {
|
|
668
|
+
const missingAncestorId = findMissingAncestorId(mention, search.payload);
|
|
669
|
+
if (missingAncestorId) {
|
|
670
|
+
const fallback = yield* fetchParentChainViaXurlEffect({
|
|
671
|
+
mention,
|
|
672
|
+
maxDepth: maxFallbackDepth,
|
|
673
|
+
timeoutMs,
|
|
674
|
+
deadlineMs,
|
|
675
|
+
});
|
|
676
|
+
return {
|
|
677
|
+
strategy: "conversation_search+parent_walk" as const,
|
|
678
|
+
pages: search.pages,
|
|
679
|
+
truncated: search.truncated,
|
|
680
|
+
payload: mergePayloads([search.payload, fallback.payload]),
|
|
681
|
+
fallbackDepth: fallback.fallbackDepth,
|
|
682
|
+
generalReadTweets:
|
|
683
|
+
search.generalReadTweets + fallback.generalReadTweets,
|
|
684
|
+
warnings: [
|
|
685
|
+
`recent search missed ancestor ${missingAncestorId} for conversation ${mention.conversationId}; used parent walk`,
|
|
686
|
+
...fallback.warnings,
|
|
687
|
+
],
|
|
688
|
+
};
|
|
689
|
+
}
|
|
570
690
|
return {
|
|
571
|
-
strategy: "conversation_search
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
fallbackDepth: fallback.fallbackDepth,
|
|
576
|
-
generalReadTweets:
|
|
577
|
-
search.generalReadTweets + fallback.generalReadTweets,
|
|
578
|
-
warnings: [
|
|
579
|
-
`recent search missed ancestor ${missingAncestorId} for conversation ${mention.conversationId}; used parent walk`,
|
|
580
|
-
...fallback.warnings,
|
|
581
|
-
],
|
|
691
|
+
strategy: "conversation_search" as const,
|
|
692
|
+
fallbackDepth: 0,
|
|
693
|
+
warnings: [] as string[],
|
|
694
|
+
...search,
|
|
582
695
|
};
|
|
583
696
|
}
|
|
697
|
+
|
|
698
|
+
const fallback = yield* fetchParentChainViaXurlEffect({
|
|
699
|
+
mention,
|
|
700
|
+
maxDepth: maxFallbackDepth,
|
|
701
|
+
timeoutMs,
|
|
702
|
+
deadlineMs,
|
|
703
|
+
});
|
|
584
704
|
return {
|
|
585
|
-
strategy: "
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
705
|
+
strategy: "parent_walk" as const,
|
|
706
|
+
pages: search.pages,
|
|
707
|
+
truncated: search.truncated,
|
|
708
|
+
payload: fallback.payload,
|
|
709
|
+
fallbackDepth: fallback.fallbackDepth,
|
|
710
|
+
generalReadTweets: search.generalReadTweets + fallback.generalReadTweets,
|
|
711
|
+
warnings: [
|
|
712
|
+
`recent search returned no tweets for conversation ${mention.conversationId}; used parent walk`,
|
|
713
|
+
...fallback.warnings,
|
|
714
|
+
],
|
|
589
715
|
};
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
const fallback = await fetchParentChainViaXurl({
|
|
593
|
-
mention,
|
|
594
|
-
maxDepth: maxFallbackDepth,
|
|
595
|
-
timeoutMs,
|
|
596
|
-
deadlineMs,
|
|
597
716
|
});
|
|
598
|
-
return {
|
|
599
|
-
strategy: "parent_walk" as const,
|
|
600
|
-
pages: search.pages,
|
|
601
|
-
truncated: search.truncated,
|
|
602
|
-
payload: fallback.payload,
|
|
603
|
-
fallbackDepth: fallback.fallbackDepth,
|
|
604
|
-
generalReadTweets: search.generalReadTweets + fallback.generalReadTweets,
|
|
605
|
-
warnings: [
|
|
606
|
-
`recent search returned no tweets for conversation ${mention.conversationId}; used parent walk`,
|
|
607
|
-
...fallback.warnings,
|
|
608
|
-
],
|
|
609
|
-
};
|
|
610
717
|
}
|
|
611
718
|
|
|
612
|
-
export
|
|
719
|
+
export function syncMentionThreadsEffect({
|
|
613
720
|
account,
|
|
614
721
|
mode = DEFAULT_MODE,
|
|
615
722
|
limit = DEFAULT_LIMIT,
|
|
723
|
+
tweetIds,
|
|
616
724
|
delayMs = DEFAULT_DELAY_MS,
|
|
617
725
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
618
726
|
all = false,
|
|
619
727
|
maxPages,
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
db,
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
728
|
+
onProgress,
|
|
729
|
+
}: SyncMentionThreadsOptions) {
|
|
730
|
+
return Effect.gen(function* () {
|
|
731
|
+
const parsedMode = yield* trySync(() => parseMode(mode));
|
|
732
|
+
const parsedLimit = yield* trySync(() =>
|
|
733
|
+
assertPositiveInteger(limit, "--limit"),
|
|
734
|
+
);
|
|
735
|
+
const parsedDelayMs =
|
|
736
|
+
(yield* trySync(() => parseNonNegativeInteger(delayMs, "--delay-ms"))) ??
|
|
737
|
+
0;
|
|
738
|
+
const parsedTimeoutMs = yield* trySync(() =>
|
|
739
|
+
assertPositiveInteger(timeoutMs, "--timeout-ms"),
|
|
740
|
+
);
|
|
741
|
+
const parsedMaxPages = yield* trySync(() =>
|
|
742
|
+
parseNonNegativeInteger(maxPages, "--max-pages"),
|
|
743
|
+
);
|
|
744
|
+
const db = yield* trySync(() => getNativeDb());
|
|
745
|
+
const resolvedAccount = yield* trySync(() => resolveAccount(db, account));
|
|
746
|
+
const mentions = yield* trySync(() =>
|
|
747
|
+
tweetIds
|
|
748
|
+
? listMentionsByIds(
|
|
749
|
+
db,
|
|
750
|
+
resolvedAccount.accountId,
|
|
751
|
+
tweetIds,
|
|
752
|
+
parsedLimit,
|
|
753
|
+
)
|
|
754
|
+
: listRecentMentions(db, resolvedAccount.accountId, parsedLimit),
|
|
755
|
+
);
|
|
756
|
+
const mentionIds = mentions.map((mention) => mention.id);
|
|
757
|
+
const mentionIdSet = new Set(mentionIds);
|
|
758
|
+
const results: Array<{
|
|
759
|
+
tweetId: string;
|
|
760
|
+
conversationId?: string | null;
|
|
761
|
+
ok: boolean;
|
|
762
|
+
count: number;
|
|
763
|
+
strategy?: string;
|
|
764
|
+
pages?: number;
|
|
765
|
+
fallbackDepth?: number;
|
|
766
|
+
truncated?: boolean;
|
|
767
|
+
warnings?: string[];
|
|
768
|
+
error?: string;
|
|
769
|
+
}> = [];
|
|
770
|
+
let mergedTweets = 0;
|
|
771
|
+
let generalReadTweets = 0;
|
|
772
|
+
const uniqueTweetIds = new Set<string>();
|
|
773
|
+
const warnings: string[] = [];
|
|
774
|
+
|
|
775
|
+
for (const [index, mention] of mentions.entries()) {
|
|
776
|
+
if (index > 0 && parsedDelayMs > 0) {
|
|
777
|
+
yield* Effect.sleep(parsedDelayMs);
|
|
778
|
+
}
|
|
779
|
+
const fetchEffect: Effect.Effect<ThreadFetchResult, unknown, never> =
|
|
666
780
|
parsedMode === "bird"
|
|
667
|
-
? {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
781
|
+
? listThreadViaBirdEffect({
|
|
782
|
+
tweetId: mention.id,
|
|
783
|
+
all,
|
|
784
|
+
maxPages: parsedMaxPages,
|
|
785
|
+
timeoutMs: parsedTimeoutMs,
|
|
786
|
+
}).pipe(
|
|
787
|
+
Effect.map((payload) => ({
|
|
788
|
+
strategy: "bird" as const,
|
|
789
|
+
payload,
|
|
790
|
+
pages: undefined,
|
|
791
|
+
fallbackDepth: undefined,
|
|
792
|
+
generalReadTweets: 0,
|
|
793
|
+
truncated: undefined,
|
|
794
|
+
warnings: [] as string[],
|
|
795
|
+
})),
|
|
796
|
+
)
|
|
797
|
+
: fetchThreadContextViaXurlEffect({
|
|
682
798
|
mention,
|
|
683
799
|
all,
|
|
684
800
|
maxPages: parsedMaxPages,
|
|
685
801
|
maxFallbackDepth: DEFAULT_FALLBACK_DEPTH,
|
|
686
802
|
timeoutMs: parsedTimeoutMs,
|
|
687
803
|
});
|
|
804
|
+
const fetched = yield* fetchEffect.pipe(
|
|
805
|
+
Effect.flatMap((fetchResult) =>
|
|
806
|
+
trySync(() =>
|
|
807
|
+
mergeMentionThreadIntoLocalStore({
|
|
808
|
+
db,
|
|
809
|
+
accountId: resolvedAccount.accountId,
|
|
810
|
+
accountHandle: resolvedAccount.handle,
|
|
811
|
+
mentionIds: mentionIdSet,
|
|
812
|
+
payload: fetchResult.payload,
|
|
813
|
+
source: parsedMode,
|
|
814
|
+
writeThreadContextEdges: parsedMode === "xurl",
|
|
815
|
+
}),
|
|
816
|
+
).pipe(Effect.as(fetchResult)),
|
|
817
|
+
),
|
|
818
|
+
Effect.map((fetchResult) => ({ ok: true as const, fetchResult })),
|
|
819
|
+
Effect.catchAll((error) =>
|
|
820
|
+
Effect.succeed({ ok: false as const, error }),
|
|
821
|
+
),
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
if (!fetched.ok) {
|
|
825
|
+
results.push({
|
|
826
|
+
tweetId: mention.id,
|
|
827
|
+
conversationId: mention.conversationId ?? null,
|
|
828
|
+
ok: false,
|
|
829
|
+
count: 0,
|
|
830
|
+
strategy: parsedMode,
|
|
831
|
+
error:
|
|
832
|
+
fetched.error instanceof Error
|
|
833
|
+
? fetched.error.message
|
|
834
|
+
: String(fetched.error),
|
|
835
|
+
});
|
|
836
|
+
yield* Effect.sync(() =>
|
|
837
|
+
onProgress?.({
|
|
838
|
+
source: parsedMode,
|
|
839
|
+
processed: index + 1,
|
|
840
|
+
total: mentions.length,
|
|
841
|
+
fetched: uniqueTweetIds.size,
|
|
842
|
+
done: index + 1 === mentions.length,
|
|
843
|
+
}),
|
|
844
|
+
);
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const { fetchResult } = fetched;
|
|
688
849
|
const { payload } = fetchResult;
|
|
689
|
-
mergeMentionThreadIntoLocalStore({
|
|
690
|
-
db,
|
|
691
|
-
accountId: resolvedAccount.accountId,
|
|
692
|
-
accountHandle: resolvedAccount.handle,
|
|
693
|
-
mentionIds: mentionIdSet,
|
|
694
|
-
payload,
|
|
695
|
-
source: parsedMode,
|
|
696
|
-
writeThreadContextEdges: parsedMode === "xurl",
|
|
697
|
-
});
|
|
698
850
|
for (const tweet of payload.data) {
|
|
699
851
|
uniqueTweetIds.add(tweet.id);
|
|
700
852
|
}
|
|
@@ -713,47 +865,52 @@ export async function syncMentionThreads({
|
|
|
713
865
|
warnings:
|
|
714
866
|
fetchResult.warnings.length > 0 ? fetchResult.warnings : undefined,
|
|
715
867
|
});
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
868
|
+
yield* Effect.sync(() =>
|
|
869
|
+
onProgress?.({
|
|
870
|
+
source: parsedMode,
|
|
871
|
+
processed: index + 1,
|
|
872
|
+
total: mentions.length,
|
|
873
|
+
fetched: uniqueTweetIds.size,
|
|
874
|
+
done: index + 1 === mentions.length,
|
|
875
|
+
}),
|
|
876
|
+
);
|
|
725
877
|
}
|
|
726
|
-
}
|
|
727
878
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
879
|
+
const failures = results.filter((item) => !item.ok);
|
|
880
|
+
const skipped = results.filter((item) =>
|
|
881
|
+
item.strategy?.startsWith("skipped:"),
|
|
882
|
+
);
|
|
883
|
+
const partial = results.some((item) => item.truncated === true);
|
|
884
|
+
return {
|
|
885
|
+
ok: true,
|
|
886
|
+
source: parsedMode,
|
|
887
|
+
accountId: resolvedAccount.accountId,
|
|
888
|
+
mentions: mentionIds.length,
|
|
889
|
+
threads: results.length,
|
|
890
|
+
succeeded: results.length - failures.length - skipped.length,
|
|
891
|
+
skipped: skipped.length,
|
|
892
|
+
failed: failures.length,
|
|
893
|
+
mergedTweets,
|
|
894
|
+
uniqueTweets: uniqueTweetIds.size,
|
|
895
|
+
generalReadTweets: parsedMode === "xurl" ? generalReadTweets : 0,
|
|
896
|
+
partial,
|
|
897
|
+
options: {
|
|
898
|
+
mode: parsedMode,
|
|
899
|
+
limit: parsedLimit,
|
|
900
|
+
delayMs: parsedDelayMs,
|
|
901
|
+
timeoutMs: parsedTimeoutMs,
|
|
902
|
+
all,
|
|
903
|
+
maxPages: parsedMaxPages ?? null,
|
|
904
|
+
maxFallbackDepth: DEFAULT_FALLBACK_DEPTH,
|
|
905
|
+
tweetIds: tweetIds ?? null,
|
|
906
|
+
},
|
|
907
|
+
results,
|
|
908
|
+
failures,
|
|
909
|
+
warnings,
|
|
910
|
+
};
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
export function syncMentionThreads(options: SyncMentionThreadsOptions) {
|
|
915
|
+
return runEffectPromise(syncMentionThreadsEffect(options));
|
|
759
916
|
}
|