birdclaw 0.6.0 → 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 +48 -0
- package/README.md +25 -0
- package/package.json +6 -1
- package/src/cli.ts +438 -26
- package/src/components/AppNav.tsx +10 -0
- package/src/components/MarkdownViewer.tsx +438 -72
- 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 +5 -2
- package/src/components/TimelineCard.tsx +20 -4
- package/src/components/TimelineRouteFrame.tsx +16 -0
- package/src/components/TweetRichText.tsx +36 -12
- package/src/components/useTimelineRouteData.ts +74 -6
- package/src/lib/account-sync-job.ts +15 -3
- package/src/lib/archive-finder.ts +1 -1
- package/src/lib/archive-import.ts +245 -7
- package/src/lib/authored-live.ts +1 -0
- package/src/lib/avatar-cache.ts +50 -0
- package/src/lib/backup.ts +4 -3
- package/src/lib/bird.ts +33 -0
- package/src/lib/config.ts +35 -2
- package/src/lib/data-sources.ts +219 -0
- package/src/lib/db.ts +62 -1
- package/src/lib/geocoding.ts +296 -0
- package/src/lib/location.ts +137 -0
- package/src/lib/mention-threads-live.ts +94 -1
- package/src/lib/mentions-live.ts +187 -40
- package/src/lib/network-map.ts +382 -0
- package/src/lib/period-digest.ts +377 -13
- package/src/lib/profile-analysis.ts +1272 -0
- package/src/lib/profile-bio-entities.ts +1 -1
- package/src/lib/queries.ts +14 -4
- package/src/lib/search-discussion.ts +1016 -0
- package/src/lib/timeline-live.ts +272 -19
- package/src/lib/tweet-account-edges.ts +2 -0
- package/src/lib/tweet-render.ts +141 -1
- package/src/lib/tweet-search-live.ts +565 -0
- package/src/lib/types.ts +37 -2
- package/src/lib/ui.ts +1 -1
- package/src/lib/web-sync.ts +7 -2
- package/src/lib/xurl-rate-limits.ts +267 -0
- package/src/lib/xurl.ts +551 -41
- package/src/routeTree.gen.ts +231 -0
- package/src/routes/__root.tsx +5 -6
- package/src/routes/api/data-sources.tsx +24 -0
- package/src/routes/api/network-map.tsx +55 -0
- package/src/routes/api/period-digest.tsx +11 -1
- package/src/routes/api/profile-analysis.tsx +152 -0
- package/src/routes/api/query.tsx +1 -0
- package/src/routes/api/search-discussion.tsx +169 -0
- 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/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 +22 -8
- package/src/styles.css +22 -0
|
@@ -61,8 +61,45 @@ export const ARCHIVE_IMPORT_SLICES = [
|
|
|
61
61
|
|
|
62
62
|
export type ArchiveImportSlice = (typeof ARCHIVE_IMPORT_SLICES)[number];
|
|
63
63
|
|
|
64
|
+
export type ImportProgressSlice =
|
|
65
|
+
| "tweets"
|
|
66
|
+
| "noteTweets"
|
|
67
|
+
| "directMessages"
|
|
68
|
+
| "likes"
|
|
69
|
+
| "bookmarks"
|
|
70
|
+
| "media"
|
|
71
|
+
| "followers"
|
|
72
|
+
| "following";
|
|
73
|
+
|
|
74
|
+
export type ImportWritePhase =
|
|
75
|
+
| "profiles"
|
|
76
|
+
| "tweets"
|
|
77
|
+
| "collections"
|
|
78
|
+
| "dmMessages";
|
|
79
|
+
|
|
80
|
+
export type ImportProgressEvent =
|
|
81
|
+
| { kind: "scanned"; entryCount: number }
|
|
82
|
+
| { kind: "slice-start"; slice: ImportProgressSlice; files: number }
|
|
83
|
+
| {
|
|
84
|
+
kind: "slice-file";
|
|
85
|
+
slice: ImportProgressSlice;
|
|
86
|
+
processed: number;
|
|
87
|
+
files: number;
|
|
88
|
+
}
|
|
89
|
+
| { kind: "slice-done"; slice: ImportProgressSlice; count: number }
|
|
90
|
+
| { kind: "writing" }
|
|
91
|
+
| { kind: "write-start"; phase: ImportWritePhase; total: number }
|
|
92
|
+
| {
|
|
93
|
+
kind: "write-progress";
|
|
94
|
+
phase: ImportWritePhase;
|
|
95
|
+
processed: number;
|
|
96
|
+
total: number;
|
|
97
|
+
}
|
|
98
|
+
| { kind: "done" };
|
|
99
|
+
|
|
64
100
|
export interface ImportArchiveOptions {
|
|
65
101
|
select?: ArchiveImportSlice[];
|
|
102
|
+
onProgress?: (event: ImportProgressEvent) => void;
|
|
66
103
|
}
|
|
67
104
|
|
|
68
105
|
type ArchiveRecord = Record<string, unknown>;
|
|
@@ -662,7 +699,9 @@ function importArchiveInternalEffect(
|
|
|
662
699
|
options: ImportArchiveOptions = {},
|
|
663
700
|
): Effect.Effect<ImportedArchiveSummary, unknown> {
|
|
664
701
|
return Effect.gen(function* () {
|
|
702
|
+
const onProgress = options.onProgress ?? (() => {});
|
|
665
703
|
const entries = yield* listArchiveEntriesEffect(archivePath);
|
|
704
|
+
onProgress({ kind: "scanned", entryCount: entries.length });
|
|
666
705
|
const selection = selectedSlices(options);
|
|
667
706
|
const includeTweets = includesSlice(selection, "tweets");
|
|
668
707
|
const includeLikes = includesSlice(selection, "likes");
|
|
@@ -770,7 +809,14 @@ function importArchiveInternalEffect(
|
|
|
770
809
|
tweetRowsById.set(row.id, row);
|
|
771
810
|
}
|
|
772
811
|
|
|
773
|
-
|
|
812
|
+
if (tweetEntries.length > 0) {
|
|
813
|
+
onProgress({
|
|
814
|
+
kind: "slice-start",
|
|
815
|
+
slice: "tweets",
|
|
816
|
+
files: tweetEntries.length,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
for (const [tweetFileIndex, entry] of tweetEntries.entries()) {
|
|
774
820
|
const content = yield* readArchiveEntryEffect(archivePath, entry);
|
|
775
821
|
for (const wrapper of parseArchiveArray(content)) {
|
|
776
822
|
const tweet = asRecord(wrapper.tweet);
|
|
@@ -821,9 +867,30 @@ function importArchiveInternalEffect(
|
|
|
821
867
|
: null,
|
|
822
868
|
});
|
|
823
869
|
}
|
|
870
|
+
onProgress({
|
|
871
|
+
kind: "slice-file",
|
|
872
|
+
slice: "tweets",
|
|
873
|
+
processed: tweetFileIndex + 1,
|
|
874
|
+
files: tweetEntries.length,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
if (tweetEntries.length > 0) {
|
|
878
|
+
onProgress({
|
|
879
|
+
kind: "slice-done",
|
|
880
|
+
slice: "tweets",
|
|
881
|
+
count: tweetRows.length,
|
|
882
|
+
});
|
|
824
883
|
}
|
|
825
884
|
|
|
826
|
-
|
|
885
|
+
if (noteTweetEntries.length > 0) {
|
|
886
|
+
onProgress({
|
|
887
|
+
kind: "slice-start",
|
|
888
|
+
slice: "noteTweets",
|
|
889
|
+
files: noteTweetEntries.length,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
const tweetRowsBeforeNotes = tweetRows.length;
|
|
893
|
+
for (const [noteFileIndex, entry] of noteTweetEntries.entries()) {
|
|
827
894
|
const content = yield* readArchiveEntryEffect(archivePath, entry);
|
|
828
895
|
for (const wrapper of parseArchiveArray(content)) {
|
|
829
896
|
const noteTweet = asRecord(wrapper.noteTweet);
|
|
@@ -846,6 +913,19 @@ function importArchiveInternalEffect(
|
|
|
846
913
|
quotedTweetId: null,
|
|
847
914
|
});
|
|
848
915
|
}
|
|
916
|
+
onProgress({
|
|
917
|
+
kind: "slice-file",
|
|
918
|
+
slice: "noteTweets",
|
|
919
|
+
processed: noteFileIndex + 1,
|
|
920
|
+
files: noteTweetEntries.length,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
if (noteTweetEntries.length > 0) {
|
|
924
|
+
onProgress({
|
|
925
|
+
kind: "slice-done",
|
|
926
|
+
slice: "noteTweets",
|
|
927
|
+
count: tweetRows.length - tweetRowsBeforeNotes,
|
|
928
|
+
});
|
|
849
929
|
}
|
|
850
930
|
const authoredTweetCount = tweetRows.length;
|
|
851
931
|
|
|
@@ -1275,7 +1355,14 @@ function importArchiveInternalEffect(
|
|
|
1275
1355
|
return resolved;
|
|
1276
1356
|
}
|
|
1277
1357
|
|
|
1278
|
-
|
|
1358
|
+
if (dmEntries.length > 0) {
|
|
1359
|
+
onProgress({
|
|
1360
|
+
kind: "slice-start",
|
|
1361
|
+
slice: "directMessages",
|
|
1362
|
+
files: dmEntries.length,
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
for (const [dmFileIndex, entry] of dmEntries.entries()) {
|
|
1279
1366
|
const content = yield* readArchiveEntryEffect(archivePath, entry);
|
|
1280
1367
|
for (const wrapper of parseArchiveArray(content)) {
|
|
1281
1368
|
const dmConversation = asRecord(wrapper.dmConversation);
|
|
@@ -1458,10 +1545,30 @@ function importArchiveInternalEffect(
|
|
|
1458
1545
|
needsReply: lastMessage.direction === "inbound" ? 1 : 0,
|
|
1459
1546
|
});
|
|
1460
1547
|
}
|
|
1548
|
+
onProgress({
|
|
1549
|
+
kind: "slice-file",
|
|
1550
|
+
slice: "directMessages",
|
|
1551
|
+
processed: dmFileIndex + 1,
|
|
1552
|
+
files: dmEntries.length,
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
if (dmEntries.length > 0) {
|
|
1556
|
+
onProgress({
|
|
1557
|
+
kind: "slice-done",
|
|
1558
|
+
slice: "directMessages",
|
|
1559
|
+
count: dmMessages.length,
|
|
1560
|
+
});
|
|
1461
1561
|
}
|
|
1462
1562
|
|
|
1563
|
+
if (likeEntries.length > 0) {
|
|
1564
|
+
onProgress({
|
|
1565
|
+
kind: "slice-start",
|
|
1566
|
+
slice: "likes",
|
|
1567
|
+
files: likeEntries.length,
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1463
1570
|
let likeCount = 0;
|
|
1464
|
-
for (const entry of likeEntries) {
|
|
1571
|
+
for (const [likeFileIndex, entry] of likeEntries.entries()) {
|
|
1465
1572
|
const content = yield* readArchiveEntryEffect(archivePath, entry);
|
|
1466
1573
|
const likes = parseArchiveArray(content);
|
|
1467
1574
|
for (const like of likes) {
|
|
@@ -1492,10 +1599,26 @@ function importArchiveInternalEffect(
|
|
|
1492
1599
|
});
|
|
1493
1600
|
}
|
|
1494
1601
|
likeCount += likes.length;
|
|
1602
|
+
onProgress({
|
|
1603
|
+
kind: "slice-file",
|
|
1604
|
+
slice: "likes",
|
|
1605
|
+
processed: likeFileIndex + 1,
|
|
1606
|
+
files: likeEntries.length,
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
if (likeEntries.length > 0) {
|
|
1610
|
+
onProgress({ kind: "slice-done", slice: "likes", count: likeCount });
|
|
1495
1611
|
}
|
|
1496
1612
|
|
|
1613
|
+
if (bookmarkEntries.length > 0) {
|
|
1614
|
+
onProgress({
|
|
1615
|
+
kind: "slice-start",
|
|
1616
|
+
slice: "bookmarks",
|
|
1617
|
+
files: bookmarkEntries.length,
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1497
1620
|
let bookmarkCount = 0;
|
|
1498
|
-
for (const entry of bookmarkEntries) {
|
|
1621
|
+
for (const [bookmarkFileIndex, entry] of bookmarkEntries.entries()) {
|
|
1499
1622
|
const content = yield* readArchiveEntryEffect(archivePath, entry);
|
|
1500
1623
|
const bookmarks = parseArchiveArray(content);
|
|
1501
1624
|
for (const bookmark of bookmarks) {
|
|
@@ -1526,29 +1649,91 @@ function importArchiveInternalEffect(
|
|
|
1526
1649
|
});
|
|
1527
1650
|
}
|
|
1528
1651
|
bookmarkCount += bookmarks.length;
|
|
1652
|
+
onProgress({
|
|
1653
|
+
kind: "slice-file",
|
|
1654
|
+
slice: "bookmarks",
|
|
1655
|
+
processed: bookmarkFileIndex + 1,
|
|
1656
|
+
files: bookmarkEntries.length,
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
if (bookmarkEntries.length > 0) {
|
|
1660
|
+
onProgress({
|
|
1661
|
+
kind: "slice-done",
|
|
1662
|
+
slice: "bookmarks",
|
|
1663
|
+
count: bookmarkCount,
|
|
1664
|
+
});
|
|
1529
1665
|
}
|
|
1530
1666
|
|
|
1667
|
+
onProgress({ kind: "slice-start", slice: "media", files: 0 });
|
|
1531
1668
|
const mediaFileCounts = yield* extractArchiveMediaFilesEffect(
|
|
1532
1669
|
archivePath,
|
|
1533
1670
|
selectedArchiveMediaKinds(selection),
|
|
1534
1671
|
);
|
|
1672
|
+
onProgress({
|
|
1673
|
+
kind: "slice-done",
|
|
1674
|
+
slice: "media",
|
|
1675
|
+
count: Object.values(mediaFileCounts).reduce(
|
|
1676
|
+
(total, value) => total + value,
|
|
1677
|
+
0,
|
|
1678
|
+
),
|
|
1679
|
+
});
|
|
1535
1680
|
|
|
1536
|
-
|
|
1681
|
+
if (followerEntries.length > 0) {
|
|
1682
|
+
onProgress({
|
|
1683
|
+
kind: "slice-start",
|
|
1684
|
+
slice: "followers",
|
|
1685
|
+
files: followerEntries.length,
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
for (const [followerFileIndex, entry] of followerEntries.entries()) {
|
|
1537
1689
|
const content = yield* readArchiveEntryEffect(archivePath, entry);
|
|
1538
1690
|
for (const row of getArchiveFollowRows(content, "follower")) {
|
|
1539
1691
|
if (followerIds.has(row.externalUserId)) continue;
|
|
1540
1692
|
followerIds.add(row.externalUserId);
|
|
1541
1693
|
followerRows.push(row);
|
|
1542
1694
|
}
|
|
1695
|
+
onProgress({
|
|
1696
|
+
kind: "slice-file",
|
|
1697
|
+
slice: "followers",
|
|
1698
|
+
processed: followerFileIndex + 1,
|
|
1699
|
+
files: followerEntries.length,
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
if (followerEntries.length > 0) {
|
|
1703
|
+
onProgress({
|
|
1704
|
+
kind: "slice-done",
|
|
1705
|
+
slice: "followers",
|
|
1706
|
+
count: followerRows.length,
|
|
1707
|
+
});
|
|
1543
1708
|
}
|
|
1544
1709
|
|
|
1545
|
-
|
|
1710
|
+
if (followingEntries.length > 0) {
|
|
1711
|
+
onProgress({
|
|
1712
|
+
kind: "slice-start",
|
|
1713
|
+
slice: "following",
|
|
1714
|
+
files: followingEntries.length,
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
for (const [followingFileIndex, entry] of followingEntries.entries()) {
|
|
1546
1718
|
const content = yield* readArchiveEntryEffect(archivePath, entry);
|
|
1547
1719
|
for (const row of getArchiveFollowRows(content, "following")) {
|
|
1548
1720
|
if (followingIds.has(row.externalUserId)) continue;
|
|
1549
1721
|
followingIds.add(row.externalUserId);
|
|
1550
1722
|
followingRows.push(row);
|
|
1551
1723
|
}
|
|
1724
|
+
onProgress({
|
|
1725
|
+
kind: "slice-file",
|
|
1726
|
+
slice: "following",
|
|
1727
|
+
processed: followingFileIndex + 1,
|
|
1728
|
+
files: followingEntries.length,
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
if (followingEntries.length > 0) {
|
|
1732
|
+
onProgress({
|
|
1733
|
+
kind: "slice-done",
|
|
1734
|
+
slice: "following",
|
|
1735
|
+
count: followingRows.length,
|
|
1736
|
+
});
|
|
1552
1737
|
}
|
|
1553
1738
|
|
|
1554
1739
|
for (const row of [...followerRows, ...followingRows]) {
|
|
@@ -2255,6 +2440,17 @@ function importArchiveInternalEffect(
|
|
|
2255
2440
|
deleteArchiveFollowEdges.run("acct_primary", direction);
|
|
2256
2441
|
}
|
|
2257
2442
|
|
|
2443
|
+
onProgress({ kind: "writing" });
|
|
2444
|
+
const WRITE_PROGRESS_INTERVAL = 1000;
|
|
2445
|
+
function tickWrite(
|
|
2446
|
+
phase: ImportWritePhase,
|
|
2447
|
+
processed: number,
|
|
2448
|
+
total: number,
|
|
2449
|
+
) {
|
|
2450
|
+
if (processed === total || processed % WRITE_PROGRESS_INTERVAL === 0) {
|
|
2451
|
+
onProgress({ kind: "write-progress", phase, processed, total });
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2258
2454
|
db.transaction(() => {
|
|
2259
2455
|
if (!selection) {
|
|
2260
2456
|
clearImportedData(db);
|
|
@@ -2339,6 +2535,15 @@ function importArchiveInternalEffect(
|
|
|
2339
2535
|
|
|
2340
2536
|
const writeProfile =
|
|
2341
2537
|
!selection || includeProfiles ? insertProfile : insertProfileIfMissing;
|
|
2538
|
+
const profilesTotal = profiles.size;
|
|
2539
|
+
if (profilesTotal > 0) {
|
|
2540
|
+
onProgress({
|
|
2541
|
+
kind: "write-start",
|
|
2542
|
+
phase: "profiles",
|
|
2543
|
+
total: profilesTotal,
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
let profileIndex = 0;
|
|
2342
2547
|
for (const profile of profiles.values()) {
|
|
2343
2548
|
writeProfile.run(
|
|
2344
2549
|
profile.id,
|
|
@@ -2357,8 +2562,18 @@ function importArchiveInternalEffect(
|
|
|
2357
2562
|
profile.rawJson,
|
|
2358
2563
|
profile.createdAt,
|
|
2359
2564
|
);
|
|
2565
|
+
profileIndex += 1;
|
|
2566
|
+
tickWrite("profiles", profileIndex, profilesTotal);
|
|
2360
2567
|
}
|
|
2361
2568
|
|
|
2569
|
+
if (tweetRows.length > 0) {
|
|
2570
|
+
onProgress({
|
|
2571
|
+
kind: "write-start",
|
|
2572
|
+
phase: "tweets",
|
|
2573
|
+
total: tweetRows.length,
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
let tweetWriteIndex = 0;
|
|
2362
2577
|
for (const tweet of tweetRows) {
|
|
2363
2578
|
const authorProfileId =
|
|
2364
2579
|
tweet.authorProfileId === "profile_me"
|
|
@@ -2406,9 +2621,19 @@ function importArchiveInternalEffect(
|
|
|
2406
2621
|
| { text: string }
|
|
2407
2622
|
| undefined;
|
|
2408
2623
|
insertTweetFts.run(tweet.id, storedTweet?.text ?? tweet.text);
|
|
2624
|
+
tweetWriteIndex += 1;
|
|
2625
|
+
tickWrite("tweets", tweetWriteIndex, tweetRows.length);
|
|
2409
2626
|
}
|
|
2410
2627
|
|
|
2411
2628
|
const importedAt = new Date().toISOString();
|
|
2629
|
+
if (collectionRows.length > 0) {
|
|
2630
|
+
onProgress({
|
|
2631
|
+
kind: "write-start",
|
|
2632
|
+
phase: "collections",
|
|
2633
|
+
total: collectionRows.length,
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
let collectionIndex = 0;
|
|
2412
2637
|
for (const collection of collectionRows) {
|
|
2413
2638
|
insertCollection.run(
|
|
2414
2639
|
"acct_primary",
|
|
@@ -2419,6 +2644,8 @@ function importArchiveInternalEffect(
|
|
|
2419
2644
|
collection.rawJson,
|
|
2420
2645
|
importedAt,
|
|
2421
2646
|
);
|
|
2647
|
+
collectionIndex += 1;
|
|
2648
|
+
tickWrite("collections", collectionIndex, collectionRows.length);
|
|
2422
2649
|
}
|
|
2423
2650
|
|
|
2424
2651
|
for (const conversation of conversations.values()) {
|
|
@@ -2433,6 +2660,14 @@ function importArchiveInternalEffect(
|
|
|
2433
2660
|
);
|
|
2434
2661
|
}
|
|
2435
2662
|
|
|
2663
|
+
if (dmMessages.length > 0) {
|
|
2664
|
+
onProgress({
|
|
2665
|
+
kind: "write-start",
|
|
2666
|
+
phase: "dmMessages",
|
|
2667
|
+
total: dmMessages.length,
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
let dmWriteIndex = 0;
|
|
2436
2671
|
for (const message of dmMessages) {
|
|
2437
2672
|
insertMessage.run(
|
|
2438
2673
|
message.id,
|
|
@@ -2445,6 +2680,8 @@ function importArchiveInternalEffect(
|
|
|
2445
2680
|
message.mediaCount,
|
|
2446
2681
|
);
|
|
2447
2682
|
insertDmFts.run(message.id, message.text);
|
|
2683
|
+
dmWriteIndex += 1;
|
|
2684
|
+
tickWrite("dmMessages", dmWriteIndex, dmMessages.length);
|
|
2448
2685
|
}
|
|
2449
2686
|
|
|
2450
2687
|
if (includeFollowers && followerEntries.length > 0) {
|
|
@@ -2468,6 +2705,7 @@ function importArchiveInternalEffect(
|
|
|
2468
2705
|
clearArchiveFollowRows("following");
|
|
2469
2706
|
}
|
|
2470
2707
|
})();
|
|
2708
|
+
onProgress({ kind: "done" });
|
|
2471
2709
|
|
|
2472
2710
|
return {
|
|
2473
2711
|
ok: true,
|
package/src/lib/authored-live.ts
CHANGED
|
@@ -1000,6 +1000,7 @@ export function syncAuthoredTweetsEffect({
|
|
|
1000
1000
|
userFields: AUTHORED_USER_FIELDS,
|
|
1001
1001
|
mediaFields: AUTHORED_MEDIA_FIELDS,
|
|
1002
1002
|
auth: "oauth2",
|
|
1003
|
+
username: identity.username,
|
|
1003
1004
|
}),
|
|
1004
1005
|
).pipe(
|
|
1005
1006
|
Effect.map((page) => ({ ok: true as const, page })),
|
package/src/lib/avatar-cache.ts
CHANGED
|
@@ -12,6 +12,7 @@ const AVATAR_SIZE_SUFFIX =
|
|
|
12
12
|
const MAX_AVATAR_BYTES = 2 * 1024 * 1024;
|
|
13
13
|
const MAX_AVATAR_DATA_URL_CHARS = MAX_AVATAR_BYTES * 4;
|
|
14
14
|
const REMOTE_AVATAR_TIMEOUT_MS = 10_000;
|
|
15
|
+
const DEFAULT_AVATAR_PREFETCH_CONCURRENCY = 8;
|
|
15
16
|
const ALLOWED_REMOTE_AVATAR_HOSTS = new Set(["pbs.twimg.com"]);
|
|
16
17
|
const RASTER_CONTENT_TYPES = new Set([
|
|
17
18
|
"image/jpeg",
|
|
@@ -289,6 +290,55 @@ export function readCachedAvatar(profileId: string) {
|
|
|
289
290
|
return runEffectPromise(readCachedAvatarEffect(profileId));
|
|
290
291
|
}
|
|
291
292
|
|
|
293
|
+
function normalizePrefetchConcurrency(value: number | undefined) {
|
|
294
|
+
if (!Number.isFinite(value) || value === undefined) {
|
|
295
|
+
return DEFAULT_AVATAR_PREFETCH_CONCURRENCY;
|
|
296
|
+
}
|
|
297
|
+
return Math.max(1, Math.min(Math.floor(value), 32));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function prefetchCachedAvatarsForProfileIdsEffect(
|
|
301
|
+
profileIds: string[],
|
|
302
|
+
options: { concurrency?: number } = {},
|
|
303
|
+
) {
|
|
304
|
+
const uniqueProfileIds = Array.from(
|
|
305
|
+
new Set(
|
|
306
|
+
profileIds
|
|
307
|
+
.map((profileId) => profileId.trim())
|
|
308
|
+
.filter((profileId) => profileId.length > 0),
|
|
309
|
+
),
|
|
310
|
+
);
|
|
311
|
+
const concurrency = normalizePrefetchConcurrency(options.concurrency);
|
|
312
|
+
|
|
313
|
+
return Effect.forEach(
|
|
314
|
+
uniqueProfileIds,
|
|
315
|
+
(profileId) =>
|
|
316
|
+
readCachedAvatarEffect(profileId).pipe(
|
|
317
|
+
Effect.map((avatar) =>
|
|
318
|
+
avatar ? ("available" as const) : ("missing" as const),
|
|
319
|
+
),
|
|
320
|
+
Effect.catchAll(() => Effect.succeed("failed" as const)),
|
|
321
|
+
),
|
|
322
|
+
{ concurrency },
|
|
323
|
+
).pipe(
|
|
324
|
+
Effect.map((statuses) => ({
|
|
325
|
+
requested: uniqueProfileIds.length,
|
|
326
|
+
available: statuses.filter((status) => status === "available").length,
|
|
327
|
+
missing: statuses.filter((status) => status === "missing").length,
|
|
328
|
+
failed: statuses.filter((status) => status === "failed").length,
|
|
329
|
+
})),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function prefetchCachedAvatarsForProfileIds(
|
|
334
|
+
profileIds: string[],
|
|
335
|
+
options: { concurrency?: number } = {},
|
|
336
|
+
) {
|
|
337
|
+
return runEffectPromise(
|
|
338
|
+
prefetchCachedAvatarsForProfileIdsEffect(profileIds, options),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
292
342
|
export const __test__ = {
|
|
293
343
|
decodeDataUrl,
|
|
294
344
|
detectRasterContentType,
|
package/src/lib/backup.ts
CHANGED
|
@@ -537,7 +537,8 @@ function buildShards(db: Database) {
|
|
|
537
537
|
const kind =
|
|
538
538
|
row.kind === "home" ||
|
|
539
539
|
row.kind === "mention" ||
|
|
540
|
-
row.kind === "authored"
|
|
540
|
+
row.kind === "authored" ||
|
|
541
|
+
row.kind === "search"
|
|
541
542
|
? row.kind
|
|
542
543
|
: "unknown";
|
|
543
544
|
addRows(shards, `data/timeline_edges/${kind}.jsonl`, [row]);
|
|
@@ -1853,8 +1854,8 @@ export function importBackupEffect({
|
|
|
1853
1854
|
account_id = coalesce(nullif(excluded.account_id, ''), tweets.account_id),
|
|
1854
1855
|
author_profile_id = coalesce(nullif(excluded.author_profile_id, ''), tweets.author_profile_id),
|
|
1855
1856
|
kind = case
|
|
1856
|
-
when tweets.kind in ('home', 'mention', 'authored') then tweets.kind
|
|
1857
|
-
when excluded.kind in ('home', 'mention', 'authored') then excluded.kind
|
|
1857
|
+
when tweets.kind in ('home', 'mention', 'authored', 'search') then tweets.kind
|
|
1858
|
+
when excluded.kind in ('home', 'mention', 'authored', 'search') then excluded.kind
|
|
1858
1859
|
else coalesce(nullif(excluded.kind, ''), tweets.kind)
|
|
1859
1860
|
end,
|
|
1860
1861
|
text = coalesce(nullif(excluded.text, ''), tweets.text),
|
package/src/lib/bird.ts
CHANGED
|
@@ -566,6 +566,39 @@ export function listBookmarkedTweetsViaBird(options: {
|
|
|
566
566
|
return runEffectPromise(listBookmarkedTweetsViaBirdEffect(options));
|
|
567
567
|
}
|
|
568
568
|
|
|
569
|
+
export function searchTweetsViaBirdEffect(
|
|
570
|
+
query: string,
|
|
571
|
+
options: {
|
|
572
|
+
maxResults: number;
|
|
573
|
+
all?: boolean;
|
|
574
|
+
maxPages?: number;
|
|
575
|
+
},
|
|
576
|
+
): Effect.Effect<XurlMentionsResponse, unknown> {
|
|
577
|
+
return Effect.gen(function* () {
|
|
578
|
+
const args = ["search", query, "-n", String(options.maxResults), "--json"];
|
|
579
|
+
if (options.all) {
|
|
580
|
+
args.push("--all");
|
|
581
|
+
}
|
|
582
|
+
if (options.all && options.maxPages !== undefined) {
|
|
583
|
+
args.push("--max-pages", String(options.maxPages));
|
|
584
|
+
}
|
|
585
|
+
const stdout = yield* runBirdJsonCommandEffect(args);
|
|
586
|
+
const payload = yield* parseBirdJsonEffect(stdout);
|
|
587
|
+
return yield* normalizeBirdTweetsPayloadEffect(payload, "search");
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export function searchTweetsViaBird(
|
|
592
|
+
query: string,
|
|
593
|
+
options: {
|
|
594
|
+
maxResults: number;
|
|
595
|
+
all?: boolean;
|
|
596
|
+
maxPages?: number;
|
|
597
|
+
},
|
|
598
|
+
): Promise<XurlMentionsResponse> {
|
|
599
|
+
return runEffectPromise(searchTweetsViaBirdEffect(query, options));
|
|
600
|
+
}
|
|
601
|
+
|
|
569
602
|
export function lookupTweetsByIdsViaBirdEffect(
|
|
570
603
|
ids: string[],
|
|
571
604
|
): Effect.Effect<XurlTweetsResponse, unknown> {
|
package/src/lib/config.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
existsSync,
|
|
5
5
|
mkdirSync,
|
|
6
6
|
readFileSync,
|
|
7
|
+
writeFileSync,
|
|
7
8
|
} from "node:fs";
|
|
8
9
|
import os from "node:os";
|
|
9
10
|
import path from "node:path";
|
|
@@ -16,7 +17,7 @@ export interface BirdclawPaths {
|
|
|
16
17
|
configPath: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
export type MentionsDataSource = "birdclaw" | "xurl" | "bird";
|
|
20
|
+
export type MentionsDataSource = "birdclaw" | "auto" | "xurl" | "bird";
|
|
20
21
|
export type ActionsTransport = "auto" | "bird" | "xurl";
|
|
21
22
|
|
|
22
23
|
export interface BirdclawConfig {
|
|
@@ -82,11 +83,37 @@ export function getBirdclawConfig(): BirdclawConfig {
|
|
|
82
83
|
return cachedConfig;
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
function getConfigPath() {
|
|
87
|
+
return process.env.BIRDCLAW_CONFIG?.trim() || getBirdclawPaths().configPath;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function writeBirdclawConfig(config: BirdclawConfig) {
|
|
91
|
+
const configPath = getConfigPath();
|
|
92
|
+
mkdirSync(path.dirname(configPath), { recursive: true });
|
|
93
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, "\t")}\n`, "utf8");
|
|
94
|
+
cachedConfig = config;
|
|
95
|
+
return configPath;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function setActionsTransport(transport: ActionsTransport) {
|
|
99
|
+
const config = getBirdclawConfig();
|
|
100
|
+
const nextConfig: BirdclawConfig = {
|
|
101
|
+
...config,
|
|
102
|
+
actions: {
|
|
103
|
+
...config.actions,
|
|
104
|
+
transport,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const configPath = writeBirdclawConfig(nextConfig);
|
|
108
|
+
return { configPath, transport };
|
|
109
|
+
}
|
|
110
|
+
|
|
85
111
|
export function resolveMentionsDataSource(
|
|
86
112
|
requestedMode?: string,
|
|
87
113
|
): MentionsDataSource {
|
|
88
114
|
if (
|
|
89
115
|
requestedMode === "birdclaw" ||
|
|
116
|
+
requestedMode === "auto" ||
|
|
90
117
|
requestedMode === "xurl" ||
|
|
91
118
|
requestedMode === "bird"
|
|
92
119
|
) {
|
|
@@ -94,13 +121,19 @@ export function resolveMentionsDataSource(
|
|
|
94
121
|
}
|
|
95
122
|
|
|
96
123
|
const envMode = process.env.BIRDCLAW_MENTIONS_DATA_SOURCE?.trim();
|
|
97
|
-
if (
|
|
124
|
+
if (
|
|
125
|
+
envMode === "birdclaw" ||
|
|
126
|
+
envMode === "auto" ||
|
|
127
|
+
envMode === "xurl" ||
|
|
128
|
+
envMode === "bird"
|
|
129
|
+
) {
|
|
98
130
|
return envMode;
|
|
99
131
|
}
|
|
100
132
|
|
|
101
133
|
const configMode = getBirdclawConfig().mentions?.dataSource;
|
|
102
134
|
if (
|
|
103
135
|
configMode === "birdclaw" ||
|
|
136
|
+
configMode === "auto" ||
|
|
104
137
|
configMode === "xurl" ||
|
|
105
138
|
configMode === "bird"
|
|
106
139
|
) {
|