birdclaw 0.8.4 → 0.9.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 +31 -0
- package/README.md +22 -2
- package/dist/cli/birdclaw.js +1233 -192
- package/dist/client/assets/AvatarChip-C8zELEVt.js +1 -0
- package/dist/client/assets/ProfilePreview-CsiLpFrb.js +1 -0
- package/dist/client/assets/SavedTimelineView-DE4CXK-h.js +1 -0
- package/dist/client/assets/SmartTimestamp-BypN4KPq.js +1 -0
- package/dist/client/assets/TimelineRouteFrame-D6djyMAT.js +1 -0
- package/dist/client/assets/blocks-h27nF3TL.js +1 -0
- package/dist/client/assets/{bookmarks-CbBkMvXk.js → bookmarks-Cprrli-V.js} +1 -1
- package/dist/client/assets/circle-check-BCK_qvXt.js +1 -0
- package/dist/client/assets/client-stream-contracts-D1QUdWfh.js +5 -0
- package/dist/client/assets/createLucideIcon-CMLcIxOz.js +1 -0
- package/dist/client/assets/data-sources-Beb3JZkl.js +1 -0
- package/dist/client/assets/discuss-ciwVz7Fo.js +1 -0
- package/dist/client/assets/dms-BZ3GUPey.js +1 -0
- package/dist/client/assets/inbox-Dajdut79.js +1 -0
- package/dist/client/assets/index-a1ZAcWSa.js +31 -0
- package/dist/client/assets/likes-CxZZqiZC.js +1 -0
- package/dist/client/assets/links-CsWS9uKL.js +1 -0
- package/dist/client/assets/mapbox-dx7h2mOm.js +2 -0
- package/dist/client/assets/{mapbox-gl-dgf_hX3a.js → mapbox-gl-WUhsQcSS.js} +1 -1
- package/dist/client/assets/{mentions-CxKCgzIf.js → mentions-C3JYXV2K.js} +1 -1
- package/dist/client/assets/network-map-D4XA7TTO.js +2 -0
- package/dist/client/assets/profile-analyze-QAznTMjZ.js +1 -0
- package/dist/client/assets/{profile-hydration-client-YEtJsNTv.js → profile-hydration-client-BQyC5--e.js} +1 -1
- package/dist/client/assets/profiles._handle-qntfbCE1.js +1 -0
- package/dist/client/assets/query-client-DZ5C7UNo.js +17 -0
- package/dist/client/assets/rate-limits-eHs_5dDB.js +1 -0
- package/dist/client/assets/react-BwGzW7mM.js +1 -0
- package/dist/client/assets/react-dom-DEMAB80t.js +1 -0
- package/dist/client/assets/{repeat-2-xCwHM9hD.js → repeat-2-CQ0lek01.js} +1 -1
- package/dist/client/assets/rolldown-runtime-B-9mzPXz.js +1 -0
- package/dist/client/assets/{routes-q7ZaW4En.js → routes-CeIv5bkV.js} +1 -1
- package/dist/client/assets/schemas-CntsNLmN.js +64 -0
- package/dist/client/assets/search-C_LqodNa.js +1 -0
- package/dist/client/assets/{sparkles-CQVlvHsZ.js → sparkles-Gx_BHkDX.js} +1 -1
- package/dist/client/assets/styles-DTMajAmk.css +2 -0
- package/dist/client/assets/today-DxppbQGF.js +1 -0
- package/dist/client/assets/{triangle-alert-CBL7s2pf.js → triangle-alert-DJPCW6jQ.js} +1 -1
- package/dist/client/assets/useTimelineRouteData-CWvBmHLE.js +1 -0
- package/dist/client/assets/{users-CHbKlqUr.js → users-DI3w8Mol.js} +1 -1
- package/dist/server/assets/{AvatarChip-zUDAEmjL.js → AvatarChip-DWag3IzT.js} +1 -1
- package/dist/server/assets/{FeedState-ntW0Q2GB.js → FeedState-CwzcvP-p.js} +1 -1
- package/dist/server/assets/{ProfileAnalysisStream-BFvhVdhn.js → ProfileAnalysisStream-CLjrYzEx.js} +2 -2
- package/dist/server/assets/{ProfilePreview-C6gAor_j.js → ProfilePreview-Bq-DSUM2.js} +7 -8
- package/dist/server/assets/{SavedTimelineView-BG1jnBp1.js → SavedTimelineView-Bk9nfnb-.js} +2 -2
- package/dist/server/assets/{SmartTimestamp-DfwE1NGf.js → SmartTimestamp-C06w_f9J.js} +1 -1
- package/dist/server/assets/SyncNowButton-CKuXKYcd.js +259 -0
- package/dist/server/assets/{TimelineRouteFrame-CU7ilM7U.js → TimelineRouteFrame-Bg3e7X_N.js} +5 -3
- package/dist/server/assets/{TweetRichText-DhCVv1-6.js → TweetRichText-wzUAtbPS.js} +2 -2
- package/dist/server/assets/{_tanstack-start-manifest_v-C-ZRnG_n.js → _tanstack-start-manifest_v-CjYYXQ8Z.js} +35 -35
- package/dist/server/assets/{blocks-DSgQghqP.js → blocks-CaP_u3mY.js} +3 -3
- package/dist/server/assets/{blocks-C1mgGTB3.js → blocks-DKy4yTF-.js} +3 -3
- package/dist/server/assets/{bookmarks-BJN8tykY.js → bookmarks-CPFq3JVX.js} +1 -1
- package/dist/server/assets/{client-stream-contracts-C2xbWVjm.js → client-stream-contracts-gtXEreFY.js} +72 -8
- package/dist/server/assets/{data-sources-BnA1Q94p.js → data-sources-DWwmQqcO.js} +1 -1
- package/dist/server/assets/{discuss-CJ1dau1j.js → discuss-BB8wFy4L.js} +3 -3
- package/dist/server/assets/{discuss-QTo08NCN.js → discuss-CVGB4_I0.js} +3 -3
- package/dist/server/assets/{dms-DAyQrVM2.js → dms-7NzdyQZQ.js} +6 -6
- package/dist/server/assets/{dms-BuLj4bp8.js → dms-vbEumuuO.js} +4 -4
- package/dist/server/assets/{empty-plugin-adapters-DzrEkdBF.js → empty-plugin-adapters-D9UWiqvJ.js} +1 -1
- package/dist/server/assets/{inbox-CkGOaW1q.js → inbox-CBeZctFd.js} +2 -2
- package/dist/server/assets/{inbox-DpWd1EzC.js → inbox-UpHhXODd.js} +4 -4
- package/dist/server/assets/{likes-DBOAtsJU.js → likes-CDAjYyTm.js} +1 -1
- package/dist/server/assets/{links-DAhSjynu.js → links-DU-TIHtg.js} +6 -6
- package/dist/server/assets/{links-DmH5lxHO.js → links-sQ3q2jvO.js} +6 -6
- package/dist/server/assets/{mentions-BH2MzKtB.js → mentions-DdndGSRw.js} +1 -1
- package/dist/server/assets/{network-map-D3CzRcC4.js → network-map-B9iOapuB.js} +2 -2
- package/dist/server/assets/{network-map-D5DoLOwR.js → network-map-CcOz-T9O.js} +2 -2
- package/dist/server/assets/{profile-analyze-C1NoM0tV.js → profile-analyze-BmDeTif5.js} +1 -1
- package/dist/server/assets/{profile-analyze-BP65PED0.js → profile-analyze-CUfXmXEH.js} +3 -3
- package/dist/server/assets/{profiles._handle-DsLNj2Ca.js → profiles._handle-8snagiR1.js} +4 -4
- package/dist/server/assets/{profiles._handle-DjwlN_ac.js → profiles._handle-BAqnghwo.js} +5 -5
- package/dist/server/assets/{rate-limits-VAr2qzYD.js → rate-limits-BY2-aQPT.js} +1 -1
- package/dist/server/assets/{router-DqkwSPiM.js → router-DP7iZD-q.js} +424 -128
- package/dist/server/assets/{routes-A9c9ZhwN.js → routes-D9226vKz.js} +1 -1
- package/dist/server/assets/start-5Z2QO8AU.js +4 -0
- package/dist/server/assets/{today-B5hVM57Z.js → today-BmvIMJzI.js} +3 -3
- package/dist/server/assets/{today-fvGiZ7ud.js → today-DKWphGrC.js} +112 -57
- package/dist/server/assets/{ui-qbmcMSSX.js → ui-rDHeS-cc.js} +2 -1
- package/dist/server/assets/{useTimelineRouteData-27cxynxa.js → useTimelineRouteData-BmDbGvQ8.js} +7 -7
- package/dist/server/server.js +50 -3413
- package/package.json +13 -13
- package/dist/client/assets/AvatarChip-DLoqM5xD.js +0 -1
- package/dist/client/assets/ProfilePreview-BejAuYsJ.js +0 -1
- package/dist/client/assets/SavedTimelineView-BIS7HKpp.js +0 -1
- package/dist/client/assets/SmartTimestamp-BR0-9se2.js +0 -1
- package/dist/client/assets/TimelineRouteFrame-CyGPj0eM.js +0 -1
- package/dist/client/assets/blocks-7UDKQp8H.js +0 -1
- package/dist/client/assets/chunk-C0phanGV.js +0 -1
- package/dist/client/assets/circle-check-C_Vq3ElD.js +0 -1
- package/dist/client/assets/client-stream-contracts-ys3XhWG4.js +0 -5
- package/dist/client/assets/createLucideIcon-JV8isFFR.js +0 -1
- package/dist/client/assets/data-sources-Cm2bpNvg.js +0 -1
- package/dist/client/assets/discuss-DTjaOugw.js +0 -1
- package/dist/client/assets/dms-CGd6Eogq.js +0 -1
- package/dist/client/assets/inbox-BHlGuOy2.js +0 -1
- package/dist/client/assets/index-CEIG7S6s.js +0 -31
- package/dist/client/assets/likes-cLrcAQem.js +0 -1
- package/dist/client/assets/links-CM9QO1LC.js +0 -1
- package/dist/client/assets/mapbox-BxTlqXt9.js +0 -2
- package/dist/client/assets/network-map-B5a3whqj.js +0 -2
- package/dist/client/assets/profile-analyze-CRtNEwlb.js +0 -1
- package/dist/client/assets/profiles._handle-C82_HoKX.js +0 -1
- package/dist/client/assets/query-client-fYu2rzRC.js +0 -17
- package/dist/client/assets/rate-limits-ScaEwdVK.js +0 -1
- package/dist/client/assets/react-dom-DHl6Ug7F.js +0 -1
- package/dist/client/assets/react-nBylq1U5.js +0 -1
- package/dist/client/assets/schemas-I9r6F6ya.js +0 -64
- package/dist/client/assets/search-Dq5kYsxA.js +0 -1
- package/dist/client/assets/styles-B8s3tt3r.css +0 -2
- package/dist/client/assets/today-CeG1O7Py.js +0 -1
- package/dist/client/assets/useTimelineRouteData-1HsqgvUi.js +0 -1
- package/dist/server/assets/SyncNowButton-BOn_IBXS.js +0 -76
- package/dist/server/assets/start-FaDOTmto.js +0 -4
package/dist/cli/birdclaw.js
CHANGED
|
@@ -487,6 +487,107 @@ var definitions = {
|
|
|
487
487
|
]
|
|
488
488
|
}
|
|
489
489
|
},
|
|
490
|
+
x_lists: {
|
|
491
|
+
exportSql: `
|
|
492
|
+
select account_id, list_id, name, description, owner_profile_id,
|
|
493
|
+
owner_external_user_id, is_private, member_count, follower_count,
|
|
494
|
+
source, membership_status, lists_synced_at, members_synced_at,
|
|
495
|
+
member_page_count, member_result_count, rate_limit_json, raw_json,
|
|
496
|
+
updated_at
|
|
497
|
+
from x_lists
|
|
498
|
+
order by account_id, name collate nocase, list_id
|
|
499
|
+
`,
|
|
500
|
+
...fixedShard("data/lists/lists.jsonl", "x_lists"),
|
|
501
|
+
merge: {
|
|
502
|
+
order: 9,
|
|
503
|
+
sql: `
|
|
504
|
+
insert into x_lists (
|
|
505
|
+
account_id, list_id, name, description, owner_profile_id,
|
|
506
|
+
owner_external_user_id, is_private, member_count, follower_count,
|
|
507
|
+
source, membership_status, lists_synced_at, members_synced_at,
|
|
508
|
+
member_page_count, member_result_count, rate_limit_json, raw_json,
|
|
509
|
+
updated_at
|
|
510
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
511
|
+
on conflict(account_id, list_id) do update set
|
|
512
|
+
name = excluded.name,
|
|
513
|
+
description = excluded.description,
|
|
514
|
+
owner_profile_id = coalesce(excluded.owner_profile_id, x_lists.owner_profile_id),
|
|
515
|
+
owner_external_user_id = coalesce(excluded.owner_external_user_id, x_lists.owner_external_user_id),
|
|
516
|
+
is_private = case when excluded.updated_at >= x_lists.updated_at then excluded.is_private else x_lists.is_private end,
|
|
517
|
+
member_count = coalesce(excluded.member_count, x_lists.member_count),
|
|
518
|
+
follower_count = coalesce(excluded.follower_count, x_lists.follower_count),
|
|
519
|
+
source = case when excluded.updated_at >= x_lists.updated_at then excluded.source else x_lists.source end,
|
|
520
|
+
membership_status = case when excluded.updated_at >= x_lists.updated_at then excluded.membership_status else x_lists.membership_status end,
|
|
521
|
+
lists_synced_at = max(x_lists.lists_synced_at, excluded.lists_synced_at),
|
|
522
|
+
members_synced_at = nullif(max(coalesce(x_lists.members_synced_at, ''), coalesce(excluded.members_synced_at, '')), ''),
|
|
523
|
+
member_page_count = case when excluded.updated_at >= x_lists.updated_at then excluded.member_page_count else x_lists.member_page_count end,
|
|
524
|
+
member_result_count = case when excluded.updated_at >= x_lists.updated_at then excluded.member_result_count else x_lists.member_result_count end,
|
|
525
|
+
rate_limit_json = case when excluded.updated_at >= x_lists.updated_at then excluded.rate_limit_json else x_lists.rate_limit_json end,
|
|
526
|
+
raw_json = case when excluded.updated_at >= x_lists.updated_at then excluded.raw_json else x_lists.raw_json end,
|
|
527
|
+
updated_at = max(x_lists.updated_at, excluded.updated_at)
|
|
528
|
+
`,
|
|
529
|
+
columns: [
|
|
530
|
+
"account_id",
|
|
531
|
+
"list_id",
|
|
532
|
+
"name",
|
|
533
|
+
"description",
|
|
534
|
+
"owner_profile_id",
|
|
535
|
+
"owner_external_user_id",
|
|
536
|
+
"is_private",
|
|
537
|
+
"member_count",
|
|
538
|
+
"follower_count",
|
|
539
|
+
"source",
|
|
540
|
+
"membership_status",
|
|
541
|
+
"lists_synced_at",
|
|
542
|
+
"members_synced_at",
|
|
543
|
+
"member_page_count",
|
|
544
|
+
"member_result_count",
|
|
545
|
+
"rate_limit_json",
|
|
546
|
+
"raw_json",
|
|
547
|
+
"updated_at"
|
|
548
|
+
]
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
x_list_members: {
|
|
552
|
+
exportSql: `
|
|
553
|
+
select account_id, list_id, profile_id, external_user_id, source,
|
|
554
|
+
current, first_seen_at, last_seen_at, ended_at, raw_json, updated_at
|
|
555
|
+
from x_list_members
|
|
556
|
+
order by account_id, list_id, profile_id
|
|
557
|
+
`,
|
|
558
|
+
...fixedShard("data/lists/members.jsonl", "x_list_members"),
|
|
559
|
+
merge: {
|
|
560
|
+
order: 10,
|
|
561
|
+
sql: `
|
|
562
|
+
insert into x_list_members (
|
|
563
|
+
account_id, list_id, profile_id, external_user_id, source, current,
|
|
564
|
+
first_seen_at, last_seen_at, ended_at, raw_json, updated_at
|
|
565
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
566
|
+
on conflict(account_id, list_id, profile_id) do update set
|
|
567
|
+
external_user_id = coalesce(nullif(excluded.external_user_id, ''), x_list_members.external_user_id),
|
|
568
|
+
source = case when excluded.updated_at >= x_list_members.updated_at then excluded.source else x_list_members.source end,
|
|
569
|
+
current = case when excluded.updated_at >= x_list_members.updated_at then excluded.current else x_list_members.current end,
|
|
570
|
+
first_seen_at = min(x_list_members.first_seen_at, excluded.first_seen_at),
|
|
571
|
+
last_seen_at = max(x_list_members.last_seen_at, excluded.last_seen_at),
|
|
572
|
+
ended_at = case when excluded.updated_at >= x_list_members.updated_at then excluded.ended_at else x_list_members.ended_at end,
|
|
573
|
+
raw_json = case when excluded.updated_at >= x_list_members.updated_at then excluded.raw_json else x_list_members.raw_json end,
|
|
574
|
+
updated_at = max(x_list_members.updated_at, excluded.updated_at)
|
|
575
|
+
`,
|
|
576
|
+
columns: [
|
|
577
|
+
"account_id",
|
|
578
|
+
"list_id",
|
|
579
|
+
"profile_id",
|
|
580
|
+
"external_user_id",
|
|
581
|
+
"source",
|
|
582
|
+
"current",
|
|
583
|
+
"first_seen_at",
|
|
584
|
+
"last_seen_at",
|
|
585
|
+
"ended_at",
|
|
586
|
+
"raw_json",
|
|
587
|
+
"updated_at"
|
|
588
|
+
]
|
|
589
|
+
}
|
|
590
|
+
},
|
|
490
591
|
tweets: {
|
|
491
592
|
exportSql: `
|
|
492
593
|
select id, author_profile_id, text, created_at, is_replied, reply_to_id,
|
|
@@ -498,7 +599,7 @@ var definitions = {
|
|
|
498
599
|
matchesPath: (candidate) => candidate.startsWith("data/tweets/"),
|
|
499
600
|
countKey: () => "tweets",
|
|
500
601
|
merge: {
|
|
501
|
-
order:
|
|
602
|
+
order: 11,
|
|
502
603
|
transform: sanitizeImportedTweets,
|
|
503
604
|
sql: `
|
|
504
605
|
insert into tweets (
|
|
@@ -556,7 +657,7 @@ var definitions = {
|
|
|
556
657
|
matchesPath: (candidate) => candidate.startsWith("data/collections/"),
|
|
557
658
|
countKey: (candidate) => `collections_${pathLeaf(candidate).replace(/\.jsonl$/, "") || "unknown"}`,
|
|
558
659
|
merge: {
|
|
559
|
-
order:
|
|
660
|
+
order: 12,
|
|
560
661
|
sql: `
|
|
561
662
|
insert into tweet_collections (
|
|
562
663
|
account_id, tweet_id, kind, collected_at, source, raw_json, updated_at
|
|
@@ -595,7 +696,7 @@ var definitions = {
|
|
|
595
696
|
matchesPath: (candidate) => candidate.startsWith("data/timeline_edges/"),
|
|
596
697
|
countKey: (candidate) => `timeline_edges_${pathLeaf(candidate).replace(/\.jsonl$/, "") || "unknown"}`,
|
|
597
698
|
merge: {
|
|
598
|
-
order:
|
|
699
|
+
order: 13,
|
|
599
700
|
sql: `
|
|
600
701
|
insert into tweet_account_edges (
|
|
601
702
|
account_id, tweet_id, kind, first_seen_at, last_seen_at, seen_count,
|
|
@@ -634,7 +735,7 @@ var definitions = {
|
|
|
634
735
|
`,
|
|
635
736
|
...fixedShard("data/dms/conversations.jsonl", "dm_conversations"),
|
|
636
737
|
merge: {
|
|
637
|
-
order:
|
|
738
|
+
order: 14,
|
|
638
739
|
sql: `
|
|
639
740
|
insert into dm_conversations (
|
|
640
741
|
id, account_id, participant_profile_id, title, inbox_kind, last_message_at, unread_count, needs_reply
|
|
@@ -675,7 +776,7 @@ var definitions = {
|
|
|
675
776
|
matchesPath: (candidate) => candidate.startsWith("data/dms/") && candidate !== "data/dms/conversations.jsonl",
|
|
676
777
|
countKey: () => "dm_messages",
|
|
677
778
|
merge: {
|
|
678
|
-
order:
|
|
779
|
+
order: 15,
|
|
679
780
|
sql: `
|
|
680
781
|
insert into dm_messages (
|
|
681
782
|
id, conversation_id, sender_profile_id, text, created_at, direction, is_replied, media_count
|
|
@@ -716,7 +817,7 @@ var definitions = {
|
|
|
716
817
|
`,
|
|
717
818
|
...fixedShard("data/links/url_expansions.jsonl", "url_expansions"),
|
|
718
819
|
merge: {
|
|
719
|
-
order:
|
|
820
|
+
order: 16,
|
|
720
821
|
transform: sanitizeImportedUrlExpansions,
|
|
721
822
|
sql: `
|
|
722
823
|
insert into url_expansions (
|
|
@@ -764,7 +865,7 @@ var definitions = {
|
|
|
764
865
|
`,
|
|
765
866
|
...fixedShard("data/links/occurrences.jsonl", "link_occurrences"),
|
|
766
867
|
merge: {
|
|
767
|
-
order:
|
|
868
|
+
order: 17,
|
|
768
869
|
sql: `
|
|
769
870
|
insert into link_occurrences (
|
|
770
871
|
source_kind, source_id, source_position, short_url, account_id,
|
|
@@ -796,7 +897,7 @@ var definitions = {
|
|
|
796
897
|
`,
|
|
797
898
|
...fixedShard("data/moderation/blocks.jsonl", "blocks"),
|
|
798
899
|
merge: {
|
|
799
|
-
order:
|
|
900
|
+
order: 18,
|
|
800
901
|
sql: `
|
|
801
902
|
insert into blocks (account_id, profile_id, source, created_at)
|
|
802
903
|
values (?, ?, ?, ?)
|
|
@@ -815,7 +916,7 @@ var definitions = {
|
|
|
815
916
|
`,
|
|
816
917
|
...fixedShard("data/moderation/mutes.jsonl", "mutes"),
|
|
817
918
|
merge: {
|
|
818
|
-
order:
|
|
919
|
+
order: 19,
|
|
819
920
|
sql: `
|
|
820
921
|
insert into mutes (account_id, profile_id, source, created_at)
|
|
821
922
|
values (?, ?, ?, ?)
|
|
@@ -834,7 +935,7 @@ var definitions = {
|
|
|
834
935
|
`,
|
|
835
936
|
...fixedShard("data/actions/tweet_actions.jsonl", "tweet_actions"),
|
|
836
937
|
merge: {
|
|
837
|
-
order:
|
|
938
|
+
order: 20,
|
|
838
939
|
sql: `
|
|
839
940
|
insert into tweet_actions (id, account_id, tweet_id, kind, body, created_at)
|
|
840
941
|
values (?, ?, ?, ?, ?, ?)
|
|
@@ -856,7 +957,7 @@ var definitions = {
|
|
|
856
957
|
`,
|
|
857
958
|
...fixedShard("data/ai_scores.jsonl", "ai_scores"),
|
|
858
959
|
merge: {
|
|
859
|
-
order:
|
|
960
|
+
order: 21,
|
|
860
961
|
sql: `
|
|
861
962
|
insert into ai_scores (
|
|
862
963
|
entity_kind, entity_id, model, score, summary, reasoning, updated_at
|
|
@@ -2402,6 +2503,43 @@ var BASE_SCHEMA_SQL = `
|
|
|
2402
2503
|
snapshot_id text not null
|
|
2403
2504
|
);
|
|
2404
2505
|
|
|
2506
|
+
create table if not exists x_lists (
|
|
2507
|
+
account_id text not null,
|
|
2508
|
+
list_id text not null,
|
|
2509
|
+
name text not null,
|
|
2510
|
+
description text not null default '',
|
|
2511
|
+
owner_profile_id text,
|
|
2512
|
+
owner_external_user_id text,
|
|
2513
|
+
is_private integer not null default 0,
|
|
2514
|
+
member_count integer,
|
|
2515
|
+
follower_count integer,
|
|
2516
|
+
source text not null,
|
|
2517
|
+
membership_status text not null default 'not_synced',
|
|
2518
|
+
lists_synced_at text not null,
|
|
2519
|
+
members_synced_at text,
|
|
2520
|
+
member_page_count integer not null default 0,
|
|
2521
|
+
member_result_count integer not null default 0,
|
|
2522
|
+
rate_limit_json text not null default '{}',
|
|
2523
|
+
raw_json text not null default '{}',
|
|
2524
|
+
updated_at text not null,
|
|
2525
|
+
primary key (account_id, list_id)
|
|
2526
|
+
);
|
|
2527
|
+
|
|
2528
|
+
create table if not exists x_list_members (
|
|
2529
|
+
account_id text not null,
|
|
2530
|
+
list_id text not null,
|
|
2531
|
+
profile_id text not null,
|
|
2532
|
+
external_user_id text not null,
|
|
2533
|
+
source text not null,
|
|
2534
|
+
current integer not null default 1,
|
|
2535
|
+
first_seen_at text not null,
|
|
2536
|
+
last_seen_at text not null,
|
|
2537
|
+
ended_at text,
|
|
2538
|
+
raw_json text not null default '{}',
|
|
2539
|
+
updated_at text not null,
|
|
2540
|
+
primary key (account_id, list_id, profile_id)
|
|
2541
|
+
);
|
|
2542
|
+
|
|
2405
2543
|
create table if not exists geocoded_locations (
|
|
2406
2544
|
normalized_key text primary key,
|
|
2407
2545
|
original text not null,
|
|
@@ -2474,6 +2612,9 @@ var INDEX_SQL = `
|
|
|
2474
2612
|
create index if not exists idx_follow_edges_profile on follow_edges(profile_id, current);
|
|
2475
2613
|
create index if not exists idx_follow_snapshots_account on follow_snapshots(account_id, direction, completed_at desc);
|
|
2476
2614
|
create index if not exists idx_follow_events_account on follow_events(account_id, direction, kind, event_at desc);
|
|
2615
|
+
create index if not exists idx_x_lists_account_name on x_lists(account_id, name collate nocase, list_id);
|
|
2616
|
+
create index if not exists idx_x_list_members_current on x_list_members(account_id, list_id, current, profile_id);
|
|
2617
|
+
create index if not exists idx_x_list_members_profile on x_list_members(profile_id, current, account_id, list_id);
|
|
2477
2618
|
create index if not exists idx_geocoded_locations_country on geocoded_locations(country_code);
|
|
2478
2619
|
create index if not exists idx_geocoded_locations_last_used on geocoded_locations(last_used_at desc);
|
|
2479
2620
|
create index if not exists idx_geocoded_unresolved_ttl on geocoded_locations_unresolved(ttl_until desc);
|
|
@@ -2735,6 +2876,50 @@ function ensureFollowGraphTables(db) {
|
|
|
2735
2876
|
);
|
|
2736
2877
|
`);
|
|
2737
2878
|
}
|
|
2879
|
+
function ensureXListTables(db) {
|
|
2880
|
+
db.exec(`
|
|
2881
|
+
create table if not exists x_lists (
|
|
2882
|
+
account_id text not null,
|
|
2883
|
+
list_id text not null,
|
|
2884
|
+
name text not null,
|
|
2885
|
+
description text not null default '',
|
|
2886
|
+
owner_profile_id text,
|
|
2887
|
+
owner_external_user_id text,
|
|
2888
|
+
is_private integer not null default 0,
|
|
2889
|
+
member_count integer,
|
|
2890
|
+
follower_count integer,
|
|
2891
|
+
source text not null,
|
|
2892
|
+
membership_status text not null default 'not_synced',
|
|
2893
|
+
lists_synced_at text not null,
|
|
2894
|
+
members_synced_at text,
|
|
2895
|
+
member_page_count integer not null default 0,
|
|
2896
|
+
member_result_count integer not null default 0,
|
|
2897
|
+
rate_limit_json text not null default '{}',
|
|
2898
|
+
raw_json text not null default '{}',
|
|
2899
|
+
updated_at text not null,
|
|
2900
|
+
primary key (account_id, list_id)
|
|
2901
|
+
);
|
|
2902
|
+
|
|
2903
|
+
create table if not exists x_list_members (
|
|
2904
|
+
account_id text not null,
|
|
2905
|
+
list_id text not null,
|
|
2906
|
+
profile_id text not null,
|
|
2907
|
+
external_user_id text not null,
|
|
2908
|
+
source text not null,
|
|
2909
|
+
current integer not null default 1,
|
|
2910
|
+
first_seen_at text not null,
|
|
2911
|
+
last_seen_at text not null,
|
|
2912
|
+
ended_at text,
|
|
2913
|
+
raw_json text not null default '{}',
|
|
2914
|
+
updated_at text not null,
|
|
2915
|
+
primary key (account_id, list_id, profile_id)
|
|
2916
|
+
);
|
|
2917
|
+
|
|
2918
|
+
create index if not exists idx_x_lists_account_name on x_lists(account_id, name collate nocase, list_id);
|
|
2919
|
+
create index if not exists idx_x_list_members_current on x_list_members(account_id, list_id, current, profile_id);
|
|
2920
|
+
create index if not exists idx_x_list_members_profile on x_list_members(profile_id, current, account_id, list_id);
|
|
2921
|
+
`);
|
|
2922
|
+
}
|
|
2738
2923
|
function backfillTweetCollections(db) {
|
|
2739
2924
|
const missingKinds = [
|
|
2740
2925
|
["likes", "liked"],
|
|
@@ -2880,6 +3065,13 @@ var DATABASE_MIGRATIONS = [
|
|
|
2880
3065
|
version: 2,
|
|
2881
3066
|
name: "normalize tweet account and collection state",
|
|
2882
3067
|
up: normalizeTweetState
|
|
3068
|
+
},
|
|
3069
|
+
{
|
|
3070
|
+
version: 3,
|
|
3071
|
+
name: "add durable X List metadata and membership",
|
|
3072
|
+
up: (db) => {
|
|
3073
|
+
ensureXListTables(db);
|
|
3074
|
+
}
|
|
2883
3075
|
}
|
|
2884
3076
|
];
|
|
2885
3077
|
function ensureDemoData(db) {
|
|
@@ -3115,6 +3307,8 @@ var ImportRepository = class {
|
|
|
3115
3307
|
}
|
|
3116
3308
|
clearBackupImport() {
|
|
3117
3309
|
this.db.exec(`
|
|
3310
|
+
delete from x_list_members;
|
|
3311
|
+
delete from x_lists;
|
|
3118
3312
|
delete from follow_events;
|
|
3119
3313
|
delete from follow_edges;
|
|
3120
3314
|
delete from follow_snapshot_members;
|
|
@@ -3522,10 +3716,11 @@ function runSubprocessEffect(options) {
|
|
|
3522
3716
|
}
|
|
3523
3717
|
|
|
3524
3718
|
// src/lib/backup.ts
|
|
3525
|
-
var BACKUP_SCHEMA_VERSION =
|
|
3719
|
+
var BACKUP_SCHEMA_VERSION = 3;
|
|
3526
3720
|
var MIN_SUPPORTED_BACKUP_SCHEMA_VERSION = 1;
|
|
3527
3721
|
var MANIFEST_PATH = "manifest.json";
|
|
3528
3722
|
var DATA_DIR = "data";
|
|
3723
|
+
var GITATTRIBUTES_PATH = ".gitattributes";
|
|
3529
3724
|
var AUTO_SYNC_CACHE_KEY = "backup:auto-sync";
|
|
3530
3725
|
var DEFAULT_STALE_AFTER_SECONDS = 15 * 60;
|
|
3531
3726
|
var autoUpdateInFlight = null;
|
|
@@ -3750,6 +3945,8 @@ data/moderation/mutes.jsonl
|
|
|
3750
3945
|
data/follow_snapshots.jsonl
|
|
3751
3946
|
data/follow_snapshot_members.jsonl
|
|
3752
3947
|
data/follow_edges.jsonl
|
|
3948
|
+
data/lists/lists.jsonl
|
|
3949
|
+
data/lists/members.jsonl
|
|
3753
3950
|
data/follow_events.jsonl
|
|
3754
3951
|
\`\`\`
|
|
3755
3952
|
|
|
@@ -3787,6 +3984,36 @@ function readPreviousManifestEffect(repoPath) {
|
|
|
3787
3984
|
Effect5.catchAll(() => Effect5.succeed(void 0))
|
|
3788
3985
|
);
|
|
3789
3986
|
}
|
|
3987
|
+
function ensureBackupGitattributesEffect(repoPath) {
|
|
3988
|
+
return Effect5.gen(function* () {
|
|
3989
|
+
const attributesPath = yield* trySync2(
|
|
3990
|
+
() => resolveBackupFilePath(repoPath, GITATTRIBUTES_PATH)
|
|
3991
|
+
);
|
|
3992
|
+
yield* assertNoSymlinkAncestorEffect(repoPath, attributesPath);
|
|
3993
|
+
const requiredLines = [
|
|
3994
|
+
"data/**/*.jsonl text eol=lf",
|
|
3995
|
+
`${MANIFEST_PATH} text eol=lf`
|
|
3996
|
+
];
|
|
3997
|
+
const generatedBlock = [
|
|
3998
|
+
"# BEGIN birdclaw backup attributes",
|
|
3999
|
+
"# Backup hashes use the raw LF-delimited bytes written by Birdclaw.",
|
|
4000
|
+
...requiredLines,
|
|
4001
|
+
"# END birdclaw backup attributes",
|
|
4002
|
+
""
|
|
4003
|
+
].join("\n");
|
|
4004
|
+
const current = yield* tryPromise(
|
|
4005
|
+
() => fs.readFile(attributesPath, "utf8")
|
|
4006
|
+
).pipe(Effect5.option);
|
|
4007
|
+
if (current._tag === "Some" && current.value.endsWith(generatedBlock)) {
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
const preserved = current._tag === "Some" ? current.value.replaceAll(generatedBlock, "").replace(/[\r\n]+$/u, "") : "";
|
|
4011
|
+
const content = preserved ? `${preserved}
|
|
4012
|
+
|
|
4013
|
+
${generatedBlock}` : generatedBlock;
|
|
4014
|
+
yield* tryPromise(() => fs.writeFile(attributesPath, content, "utf8"));
|
|
4015
|
+
});
|
|
4016
|
+
}
|
|
3790
4017
|
function maybeCommitAndPushEffect({
|
|
3791
4018
|
repoPath,
|
|
3792
4019
|
message,
|
|
@@ -3797,20 +4024,21 @@ function maybeCommitAndPushEffect({
|
|
|
3797
4024
|
return Effect5.succeed(void 0);
|
|
3798
4025
|
}
|
|
3799
4026
|
return Effect5.gen(function* () {
|
|
3800
|
-
yield*
|
|
3801
|
-
"-C",
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
)
|
|
3809
|
-
|
|
4027
|
+
if (!(yield* isGitRepoEffect(repoPath))) {
|
|
4028
|
+
yield* gitEffect(["-C", repoPath, "init"]);
|
|
4029
|
+
}
|
|
4030
|
+
if (!(yield* isGitRepoEffect(repoPath))) {
|
|
4031
|
+
return yield* Effect5.fail(
|
|
4032
|
+
new Error(
|
|
4033
|
+
"Backup Git operations must run at the configured repository root"
|
|
4034
|
+
)
|
|
4035
|
+
);
|
|
4036
|
+
}
|
|
3810
4037
|
yield* gitEffect([
|
|
3811
4038
|
"-C",
|
|
3812
4039
|
repoPath,
|
|
3813
4040
|
"add",
|
|
4041
|
+
GITATTRIBUTES_PATH,
|
|
3814
4042
|
"README.md",
|
|
3815
4043
|
MANIFEST_PATH,
|
|
3816
4044
|
DATA_DIR
|
|
@@ -3878,6 +4106,9 @@ function maybeCommitAndPushEffect({
|
|
|
3878
4106
|
});
|
|
3879
4107
|
}
|
|
3880
4108
|
function isGitRepoEffect(repoPath) {
|
|
4109
|
+
if (!existsSync2(path2.join(repoPath, ".git"))) {
|
|
4110
|
+
return Effect5.succeed(false);
|
|
4111
|
+
}
|
|
3881
4112
|
return gitEffect(["-C", repoPath, "rev-parse", "--is-inside-work-tree"]).pipe(
|
|
3882
4113
|
Effect5.as(true),
|
|
3883
4114
|
Effect5.catchAll(() => Effect5.succeed(false))
|
|
@@ -3934,7 +4165,7 @@ function ensureBackupGitRepoEffect({
|
|
|
3934
4165
|
repoPath,
|
|
3935
4166
|
"fetch",
|
|
3936
4167
|
"origin",
|
|
3937
|
-
"main"
|
|
4168
|
+
"main:refs/remotes/origin/main"
|
|
3938
4169
|
]).pipe(
|
|
3939
4170
|
Effect5.flatMap(
|
|
3940
4171
|
() => gitEffect(["-C", repoPath, "checkout", "-B", "main", "origin/main"])
|
|
@@ -3983,6 +4214,7 @@ function exportBackupEffect({
|
|
|
3983
4214
|
new Error("Backup repository path must be a real directory")
|
|
3984
4215
|
);
|
|
3985
4216
|
}
|
|
4217
|
+
yield* ensureBackupGitattributesEffect(resolvedRepoPath);
|
|
3986
4218
|
yield* ensureBackupReadmeEffect(resolvedRepoPath);
|
|
3987
4219
|
const shards = yield* trySync2(() => buildShards(database));
|
|
3988
4220
|
const shardEntries = yield* trySync2(
|
|
@@ -6133,6 +6365,16 @@ function runBirdJsonCommandAllowFailureEffect(args, timeoutMs) {
|
|
|
6133
6365
|
})
|
|
6134
6366
|
);
|
|
6135
6367
|
}
|
|
6368
|
+
function runBirdTweetJsonCommandEffect(args, timeoutMs) {
|
|
6369
|
+
return runBirdJsonCommandEffect([...args, "--json-full"], timeoutMs).pipe(
|
|
6370
|
+
Effect8.catchAll((error) => {
|
|
6371
|
+
if (!isUnsupportedBirdOptionError(error, "--json-full")) {
|
|
6372
|
+
return Effect8.fail(error);
|
|
6373
|
+
}
|
|
6374
|
+
return runBirdJsonCommandEffect([...args, "--json"], timeoutMs);
|
|
6375
|
+
})
|
|
6376
|
+
);
|
|
6377
|
+
}
|
|
6136
6378
|
function getBirdTweetItems(payload, command) {
|
|
6137
6379
|
if (Array.isArray(payload)) {
|
|
6138
6380
|
return payload;
|
|
@@ -6197,19 +6439,63 @@ function toReferencedTweets(item) {
|
|
|
6197
6439
|
}
|
|
6198
6440
|
return references.length > 0 ? references : void 0;
|
|
6199
6441
|
}
|
|
6442
|
+
function getRecord(value) {
|
|
6443
|
+
return value && typeof value === "object" ? value : void 0;
|
|
6444
|
+
}
|
|
6445
|
+
function findGraphqlAvatarUrl(value, authorId) {
|
|
6446
|
+
const pending = [value];
|
|
6447
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6448
|
+
while (pending.length > 0) {
|
|
6449
|
+
const current = pending.pop();
|
|
6450
|
+
const record3 = getRecord(current);
|
|
6451
|
+
if (!record3 || seen.has(record3)) continue;
|
|
6452
|
+
seen.add(record3);
|
|
6453
|
+
if (record3.rest_id === authorId) {
|
|
6454
|
+
const avatar = getRecord(record3.avatar);
|
|
6455
|
+
if (typeof avatar?.image_url === "string" && avatar.image_url) {
|
|
6456
|
+
return avatar.image_url;
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
for (const child of Object.values(record3)) {
|
|
6460
|
+
if (child && typeof child === "object") {
|
|
6461
|
+
pending.push(child);
|
|
6462
|
+
}
|
|
6463
|
+
}
|
|
6464
|
+
}
|
|
6465
|
+
return void 0;
|
|
6466
|
+
}
|
|
6467
|
+
function isHydratedBirdTweetItem(value) {
|
|
6468
|
+
const item = value;
|
|
6469
|
+
return typeof item?.id === "string" && typeof item.text === "string" && typeof item.createdAt === "string";
|
|
6470
|
+
}
|
|
6200
6471
|
function normalizeBirdTweets(items) {
|
|
6201
6472
|
const users = /* @__PURE__ */ new Map();
|
|
6202
|
-
const
|
|
6473
|
+
const includedTweets = /* @__PURE__ */ new Map();
|
|
6474
|
+
const normalizeItem = (item, preserveMissingMetrics = false) => {
|
|
6203
6475
|
const authorId = String(
|
|
6204
6476
|
item.authorId ?? item.author?.username ?? "unknown"
|
|
6205
6477
|
);
|
|
6206
|
-
|
|
6478
|
+
const profileImageUrl = item.author?.profileImageUrl ?? findGraphqlAvatarUrl(item._raw, authorId);
|
|
6479
|
+
const existingUser = users.get(authorId);
|
|
6480
|
+
if (!existingUser) {
|
|
6207
6481
|
users.set(authorId, {
|
|
6208
6482
|
id: authorId,
|
|
6209
6483
|
username: item.author?.username ?? `user_${authorId}`,
|
|
6210
|
-
name: item.author?.name ?? item.author?.username ?? `user_${authorId}
|
|
6484
|
+
name: item.author?.name ?? item.author?.username ?? `user_${authorId}`,
|
|
6485
|
+
...profileImageUrl ? { profile_image_url: profileImageUrl } : {}
|
|
6486
|
+
});
|
|
6487
|
+
} else if (profileImageUrl && !existingUser.profile_image_url) {
|
|
6488
|
+
users.set(authorId, {
|
|
6489
|
+
...existingUser,
|
|
6490
|
+
profile_image_url: profileImageUrl
|
|
6211
6491
|
});
|
|
6212
6492
|
}
|
|
6493
|
+
if (isHydratedBirdTweetItem(item.quotedTweet)) {
|
|
6494
|
+
const quotedTweet = normalizeItem(item.quotedTweet, true);
|
|
6495
|
+
if (quotedTweet.id !== item.id) {
|
|
6496
|
+
includedTweets.set(quotedTweet.id, quotedTweet);
|
|
6497
|
+
}
|
|
6498
|
+
}
|
|
6213
6499
|
return {
|
|
6214
6500
|
id: item.id,
|
|
6215
6501
|
author_id: authorId,
|
|
@@ -6218,17 +6504,29 @@ function normalizeBirdTweets(items) {
|
|
|
6218
6504
|
conversation_id: item.conversationId ?? item.id,
|
|
6219
6505
|
entities: toTweetEntities(item),
|
|
6220
6506
|
referenced_tweets: toReferencedTweets(item),
|
|
6221
|
-
public_metrics: {
|
|
6507
|
+
public_metrics: preserveMissingMetrics ? {
|
|
6508
|
+
...item.replyCount === void 0 ? {} : { reply_count: Number(item.replyCount) },
|
|
6509
|
+
...item.retweetCount === void 0 ? {} : { retweet_count: Number(item.retweetCount) },
|
|
6510
|
+
...item.likeCount === void 0 ? {} : { like_count: Number(item.likeCount) }
|
|
6511
|
+
} : {
|
|
6222
6512
|
reply_count: Number(item.replyCount ?? 0),
|
|
6223
6513
|
retweet_count: Number(item.retweetCount ?? 0),
|
|
6224
6514
|
like_count: Number(item.likeCount ?? 0)
|
|
6225
6515
|
},
|
|
6226
6516
|
edit_history_tweet_ids: [item.id]
|
|
6227
6517
|
};
|
|
6228
|
-
}
|
|
6518
|
+
};
|
|
6519
|
+
const data = items.map((item) => normalizeItem(item));
|
|
6520
|
+
const includes = {};
|
|
6521
|
+
if (users.size > 0) {
|
|
6522
|
+
includes.users = Array.from(users.values());
|
|
6523
|
+
}
|
|
6524
|
+
if (includedTweets.size > 0) {
|
|
6525
|
+
includes.tweets = Array.from(includedTweets.values());
|
|
6526
|
+
}
|
|
6229
6527
|
return {
|
|
6230
6528
|
data,
|
|
6231
|
-
includes: users.size > 0
|
|
6529
|
+
includes: users.size > 0 || includedTweets.size > 0 ? includes : void 0,
|
|
6232
6530
|
meta: {
|
|
6233
6531
|
result_count: data.length,
|
|
6234
6532
|
page_count: 1,
|
|
@@ -6260,11 +6558,10 @@ function listMentionsViaBirdEffect({
|
|
|
6260
6558
|
maxResults
|
|
6261
6559
|
}) {
|
|
6262
6560
|
return Effect8.gen(function* () {
|
|
6263
|
-
const stdout = yield*
|
|
6561
|
+
const stdout = yield* runBirdTweetJsonCommandEffect([
|
|
6264
6562
|
"mentions",
|
|
6265
6563
|
"-n",
|
|
6266
|
-
String(maxResults)
|
|
6267
|
-
"--json"
|
|
6564
|
+
String(maxResults)
|
|
6268
6565
|
]);
|
|
6269
6566
|
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6270
6567
|
return yield* normalizeBirdTweetsPayloadEffect(payload, "mentions");
|
|
@@ -6277,14 +6574,14 @@ function listTweetsViaBirdCommandEffect({
|
|
|
6277
6574
|
maxPages
|
|
6278
6575
|
}) {
|
|
6279
6576
|
return Effect8.gen(function* () {
|
|
6280
|
-
const args = [command, "-n", String(maxResults)
|
|
6577
|
+
const args = [command, "-n", String(maxResults)];
|
|
6281
6578
|
if (all) {
|
|
6282
6579
|
args.push("--all");
|
|
6580
|
+
if (maxPages !== void 0) {
|
|
6581
|
+
args.push("--max-pages", String(maxPages));
|
|
6582
|
+
}
|
|
6283
6583
|
}
|
|
6284
|
-
|
|
6285
|
-
args.push("--max-pages", String(maxPages));
|
|
6286
|
-
}
|
|
6287
|
-
const stdout = yield* runBirdJsonCommandEffect(args);
|
|
6584
|
+
const stdout = yield* runBirdTweetJsonCommandEffect(args);
|
|
6288
6585
|
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6289
6586
|
return yield* normalizeBirdTweetsPayloadEffect(payload, command);
|
|
6290
6587
|
});
|
|
@@ -6303,14 +6600,14 @@ function listBookmarkedTweetsViaBirdEffect(options) {
|
|
|
6303
6600
|
}
|
|
6304
6601
|
function searchTweetsViaBirdEffect(query, options) {
|
|
6305
6602
|
return Effect8.gen(function* () {
|
|
6306
|
-
const args = ["search", query, "-n", String(options.maxResults)
|
|
6603
|
+
const args = ["search", query, "-n", String(options.maxResults)];
|
|
6307
6604
|
if (options.all) {
|
|
6308
6605
|
args.push("--all");
|
|
6309
6606
|
}
|
|
6310
6607
|
if (options.all && options.maxPages !== void 0) {
|
|
6311
6608
|
args.push("--max-pages", String(options.maxPages));
|
|
6312
6609
|
}
|
|
6313
|
-
const stdout = yield*
|
|
6610
|
+
const stdout = yield* runBirdTweetJsonCommandEffect(args);
|
|
6314
6611
|
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6315
6612
|
return yield* normalizeBirdTweetsPayloadEffect(payload, "search");
|
|
6316
6613
|
});
|
|
@@ -6323,11 +6620,7 @@ function lookupTweetsByIdsViaBirdEffect(ids) {
|
|
|
6323
6620
|
const tweets = yield* Effect8.forEach(
|
|
6324
6621
|
ids,
|
|
6325
6622
|
(id) => Effect8.gen(function* () {
|
|
6326
|
-
const stdout = yield*
|
|
6327
|
-
"read",
|
|
6328
|
-
id,
|
|
6329
|
-
"--json"
|
|
6330
|
-
]);
|
|
6623
|
+
const stdout = yield* runBirdTweetJsonCommandEffect(["read", id]);
|
|
6331
6624
|
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6332
6625
|
return yield* normalizeBirdTweetItemEffect(payload, "read");
|
|
6333
6626
|
}),
|
|
@@ -6341,11 +6634,11 @@ function listHomeTimelineViaBirdEffect({
|
|
|
6341
6634
|
following = true
|
|
6342
6635
|
}) {
|
|
6343
6636
|
return Effect8.gen(function* () {
|
|
6344
|
-
const args = ["home", "-n", String(maxResults)
|
|
6637
|
+
const args = ["home", "-n", String(maxResults)];
|
|
6345
6638
|
if (following) {
|
|
6346
6639
|
args.push("--following");
|
|
6347
6640
|
}
|
|
6348
|
-
const stdout = yield*
|
|
6641
|
+
const stdout = yield* runBirdTweetJsonCommandEffect(args);
|
|
6349
6642
|
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6350
6643
|
return yield* normalizeBirdTweetsPayloadEffect(payload, "home");
|
|
6351
6644
|
});
|
|
@@ -6358,12 +6651,14 @@ function normalizeBirdFollowUsers(payload, command, maxResults) {
|
|
|
6358
6651
|
}
|
|
6359
6652
|
const data = users.map(toXurlMentionUser).filter((user) => Boolean(user));
|
|
6360
6653
|
const nextToken = !Array.isArray(rawPayload) && typeof rawPayload.nextCursor === "string" ? rawPayload.nextCursor : null;
|
|
6654
|
+
const paginationKnownComplete = Array.isArray(rawPayload) ? data.length < maxResults : nextToken === null;
|
|
6361
6655
|
return {
|
|
6362
6656
|
data,
|
|
6363
6657
|
meta: {
|
|
6364
6658
|
result_count: data.length,
|
|
6365
6659
|
page_count: data.length > 0 ? Math.max(1, Math.ceil(data.length / maxResults)) : 1,
|
|
6366
|
-
next_token: nextToken
|
|
6660
|
+
next_token: nextToken,
|
|
6661
|
+
pagination_known_complete: paginationKnownComplete
|
|
6367
6662
|
}
|
|
6368
6663
|
};
|
|
6369
6664
|
}
|
|
@@ -6400,6 +6695,73 @@ function listFollowUsersViaBirdEffect({
|
|
|
6400
6695
|
);
|
|
6401
6696
|
});
|
|
6402
6697
|
}
|
|
6698
|
+
function normalizeBirdLists(payload) {
|
|
6699
|
+
if (!Array.isArray(payload)) {
|
|
6700
|
+
throw new Error("bird lists returned unexpected JSON");
|
|
6701
|
+
}
|
|
6702
|
+
const data = payload.map((value) => {
|
|
6703
|
+
const item = value;
|
|
6704
|
+
if (!item.id || !item.name) return null;
|
|
6705
|
+
return {
|
|
6706
|
+
id: item.id,
|
|
6707
|
+
name: item.name,
|
|
6708
|
+
...item.description ? { description: item.description } : {},
|
|
6709
|
+
...typeof item.memberCount === "number" ? { memberCount: item.memberCount } : {},
|
|
6710
|
+
...typeof item.subscriberCount === "number" ? { followerCount: item.subscriberCount } : {},
|
|
6711
|
+
...typeof item.isPrivate === "boolean" ? { isPrivate: item.isPrivate } : {},
|
|
6712
|
+
...item.owner?.id ? { ownerId: item.owner.id } : {},
|
|
6713
|
+
...item.owner?.username ? { ownerUsername: item.owner.username } : {},
|
|
6714
|
+
...item.owner?.name ? { ownerName: item.owner.name } : {},
|
|
6715
|
+
...item.createdAt ? { createdAt: item.createdAt } : {},
|
|
6716
|
+
raw: value
|
|
6717
|
+
};
|
|
6718
|
+
}).filter((item) => Boolean(item));
|
|
6719
|
+
return { data, meta: { result_count: data.length, next_token: null } };
|
|
6720
|
+
}
|
|
6721
|
+
function listOwnedXListsViaBirdEffect({
|
|
6722
|
+
maxResults
|
|
6723
|
+
}) {
|
|
6724
|
+
return Effect8.gen(function* () {
|
|
6725
|
+
const stdout = yield* runBirdJsonCommandEffect([
|
|
6726
|
+
"lists",
|
|
6727
|
+
"-n",
|
|
6728
|
+
String(maxResults),
|
|
6729
|
+
"--json"
|
|
6730
|
+
]);
|
|
6731
|
+
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6732
|
+
return yield* Effect8.try({
|
|
6733
|
+
try: () => normalizeBirdLists(payload),
|
|
6734
|
+
catch: (error) => error
|
|
6735
|
+
});
|
|
6736
|
+
});
|
|
6737
|
+
}
|
|
6738
|
+
function listXListMembersViaBirdEffect({
|
|
6739
|
+
listId,
|
|
6740
|
+
maxResults,
|
|
6741
|
+
maxPages = 1
|
|
6742
|
+
}) {
|
|
6743
|
+
return Effect8.gen(function* () {
|
|
6744
|
+
const args = ["list-members", listId, "-n", String(maxResults), "--json"];
|
|
6745
|
+
if (maxPages > 1) {
|
|
6746
|
+
args.push("--all", "--max-pages", String(maxPages));
|
|
6747
|
+
}
|
|
6748
|
+
const stdout = yield* runBirdJsonCommandEffect(args);
|
|
6749
|
+
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6750
|
+
const normalized = yield* normalizeBirdFollowUsersEffect(
|
|
6751
|
+
payload,
|
|
6752
|
+
"list-members",
|
|
6753
|
+
maxResults
|
|
6754
|
+
);
|
|
6755
|
+
if (Array.isArray(payload)) {
|
|
6756
|
+
normalized.meta = {
|
|
6757
|
+
...normalized.meta,
|
|
6758
|
+
pagination_known_complete: false,
|
|
6759
|
+
pagination_inferred_complete: normalized.data.length < maxResults
|
|
6760
|
+
};
|
|
6761
|
+
}
|
|
6762
|
+
return normalized;
|
|
6763
|
+
});
|
|
6764
|
+
}
|
|
6403
6765
|
function listThreadViaBirdEffect({
|
|
6404
6766
|
tweetId,
|
|
6405
6767
|
all,
|
|
@@ -6407,14 +6769,14 @@ function listThreadViaBirdEffect({
|
|
|
6407
6769
|
timeoutMs
|
|
6408
6770
|
}) {
|
|
6409
6771
|
return Effect8.gen(function* () {
|
|
6410
|
-
const args = ["thread", tweetId
|
|
6772
|
+
const args = ["thread", tweetId];
|
|
6411
6773
|
if (all) {
|
|
6412
6774
|
args.push("--all");
|
|
6413
6775
|
}
|
|
6414
6776
|
if (maxPages !== void 0) {
|
|
6415
6777
|
args.push("--max-pages", String(maxPages));
|
|
6416
6778
|
}
|
|
6417
|
-
const stdout = yield*
|
|
6779
|
+
const stdout = yield* runBirdTweetJsonCommandEffect(args, timeoutMs);
|
|
6418
6780
|
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6419
6781
|
return yield* normalizeBirdTweetsPayloadEffect(payload, "thread");
|
|
6420
6782
|
});
|
|
@@ -7104,6 +7466,71 @@ function lookupUsersByIdsEffect(ids) {
|
|
|
7104
7466
|
function lookupUsersByIds(ids) {
|
|
7105
7467
|
return runEffectPromise(lookupUsersByIdsEffect(ids));
|
|
7106
7468
|
}
|
|
7469
|
+
function normalizeXListPage(payload) {
|
|
7470
|
+
const rawItems = Array.isArray(payload.data) ? payload.data : [];
|
|
7471
|
+
const data = rawItems.map((value) => {
|
|
7472
|
+
const item = value;
|
|
7473
|
+
if (typeof item.id !== "string" || typeof item.name !== "string") {
|
|
7474
|
+
return null;
|
|
7475
|
+
}
|
|
7476
|
+
return {
|
|
7477
|
+
id: item.id,
|
|
7478
|
+
name: item.name,
|
|
7479
|
+
...typeof item.description === "string" ? { description: item.description } : {},
|
|
7480
|
+
...typeof item.member_count === "number" ? { memberCount: item.member_count } : {},
|
|
7481
|
+
...typeof item.follower_count === "number" ? { followerCount: item.follower_count } : {},
|
|
7482
|
+
...typeof item.private === "boolean" ? { isPrivate: item.private } : {},
|
|
7483
|
+
...typeof item.owner_id === "string" ? { ownerId: item.owner_id } : {},
|
|
7484
|
+
...typeof item.created_at === "string" ? { createdAt: item.created_at } : {},
|
|
7485
|
+
raw: item
|
|
7486
|
+
};
|
|
7487
|
+
}).filter((item) => Boolean(item));
|
|
7488
|
+
const meta = payload.meta && typeof payload.meta === "object" ? payload.meta : {};
|
|
7489
|
+
return {
|
|
7490
|
+
data,
|
|
7491
|
+
meta: {
|
|
7492
|
+
result_count: data.length,
|
|
7493
|
+
...typeof meta.next_token === "string" ? { next_token: meta.next_token } : { next_token: null }
|
|
7494
|
+
}
|
|
7495
|
+
};
|
|
7496
|
+
}
|
|
7497
|
+
function listOwnedXListsViaXurlEffect({
|
|
7498
|
+
userId,
|
|
7499
|
+
username,
|
|
7500
|
+
maxResults,
|
|
7501
|
+
paginationToken
|
|
7502
|
+
}) {
|
|
7503
|
+
const query = new URLSearchParams({
|
|
7504
|
+
max_results: String(maxResults),
|
|
7505
|
+
"list.fields": "created_at,description,follower_count,member_count,name,owner_id,private"
|
|
7506
|
+
});
|
|
7507
|
+
if (paginationToken) query.set("pagination_token", paginationToken);
|
|
7508
|
+
return runOAuth2JsonCommandEffect({
|
|
7509
|
+
args: [`/2/users/${userId}/owned_lists?${query.toString()}`],
|
|
7510
|
+
username
|
|
7511
|
+
}).pipe(Effect9.map(normalizeXListPage));
|
|
7512
|
+
}
|
|
7513
|
+
function listXListMembersViaXurlEffect({
|
|
7514
|
+
listId,
|
|
7515
|
+
username,
|
|
7516
|
+
maxResults,
|
|
7517
|
+
paginationToken
|
|
7518
|
+
}) {
|
|
7519
|
+
const query = new URLSearchParams({
|
|
7520
|
+
max_results: String(maxResults),
|
|
7521
|
+
"user.fields": RICH_USER_FIELDS
|
|
7522
|
+
});
|
|
7523
|
+
if (paginationToken) query.set("pagination_token", paginationToken);
|
|
7524
|
+
return runOAuth2JsonCommandEffect({
|
|
7525
|
+
args: [`/2/lists/${listId}/members?${query.toString()}`],
|
|
7526
|
+
username
|
|
7527
|
+
}).pipe(
|
|
7528
|
+
Effect9.map((payload) => ({
|
|
7529
|
+
data: Array.isArray(payload.data) ? payload.data : [],
|
|
7530
|
+
meta: payload.meta && typeof payload.meta === "object" ? payload.meta : {}
|
|
7531
|
+
}))
|
|
7532
|
+
);
|
|
7533
|
+
}
|
|
7107
7534
|
function lookupUsersByHandlesEffect(handles, options = {}) {
|
|
7108
7535
|
if (handles.length === 0) {
|
|
7109
7536
|
return Effect9.succeed([]);
|
|
@@ -8218,16 +8645,15 @@ function enrichFallbackUrlEntities(text, entities, resolveExpansion) {
|
|
|
8218
8645
|
const existingUrls = entities.urls ?? [];
|
|
8219
8646
|
const enrichedExistingUrls = existingUrls.map((entry) => {
|
|
8220
8647
|
const expansion = resolveExpansion?.(entry.url);
|
|
8221
|
-
|
|
8222
|
-
const expandedUrl = expansion.expandedUrl || entry.expandedUrl;
|
|
8648
|
+
const expandedUrl = expansion?.expandedUrl || entry.expandedUrl || entry.url;
|
|
8223
8649
|
return {
|
|
8224
8650
|
...entry,
|
|
8225
8651
|
expandedUrl,
|
|
8226
|
-
displayUrl: expansion.displayUrl || displayUrlForLink(expandedUrl),
|
|
8227
|
-
...expansion
|
|
8652
|
+
displayUrl: expansion?.displayUrl || entry.displayUrl || displayUrlForLink(expandedUrl),
|
|
8653
|
+
...expansion?.title ? { title: expansion.title } : {},
|
|
8228
8654
|
...expansion && "description" in expansion ? { description: expansion.description ?? null } : {},
|
|
8229
|
-
...expansion
|
|
8230
|
-
...expansion
|
|
8655
|
+
...expansion?.imageUrl ? { imageUrl: expansion.imageUrl } : {},
|
|
8656
|
+
...expansion?.siteName ? { siteName: expansion.siteName } : {}
|
|
8231
8657
|
};
|
|
8232
8658
|
});
|
|
8233
8659
|
const fallbackUrls = [];
|
|
@@ -9661,6 +10087,16 @@ function ensureStubProfileForXUser(db, externalUserId) {
|
|
|
9661
10087
|
function getReferencedTweetId(tweet, type) {
|
|
9662
10088
|
return tweet.referenced_tweets?.find((item) => item.type === type)?.id ?? null;
|
|
9663
10089
|
}
|
|
10090
|
+
function toCanonicalTweets(payload) {
|
|
10091
|
+
const tweetsById = /* @__PURE__ */ new Map();
|
|
10092
|
+
for (const tweet of payload.includes?.tweets ?? []) {
|
|
10093
|
+
tweetsById.set(tweet.id, tweet);
|
|
10094
|
+
}
|
|
10095
|
+
for (const tweet of payload.data) {
|
|
10096
|
+
tweetsById.set(tweet.id, tweet);
|
|
10097
|
+
}
|
|
10098
|
+
return tweetsById.values();
|
|
10099
|
+
}
|
|
9664
10100
|
function replaceTweetFts(db, tweetId, text) {
|
|
9665
10101
|
db.prepare("delete from tweets_fts where tweet_id = ?").run(tweetId);
|
|
9666
10102
|
db.prepare("insert into tweets_fts (tweet_id, text) values (?, ?)").run(
|
|
@@ -9690,7 +10126,7 @@ function ingestTweetPayload(db, {
|
|
|
9690
10126
|
created_at = excluded.created_at,
|
|
9691
10127
|
is_replied = max(tweets.is_replied, excluded.is_replied),
|
|
9692
10128
|
reply_to_id = coalesce(tweets.reply_to_id, excluded.reply_to_id),
|
|
9693
|
-
like_count = excluded.like_count,
|
|
10129
|
+
like_count = case when ? then tweets.like_count else excluded.like_count end,
|
|
9694
10130
|
media_count = max(tweets.media_count, excluded.media_count),
|
|
9695
10131
|
entities_json = excluded.entities_json,
|
|
9696
10132
|
media_json = case
|
|
@@ -9711,25 +10147,29 @@ function ingestTweetPayload(db, {
|
|
|
9711
10147
|
const tweetIds = [];
|
|
9712
10148
|
db.transaction(() => {
|
|
9713
10149
|
const observedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9714
|
-
|
|
10150
|
+
const primaryTweetIds = new Set(payload.data.map((tweet) => tweet.id));
|
|
10151
|
+
for (const tweet of toCanonicalTweets(payload)) {
|
|
10152
|
+
const isPrimaryTweet = primaryTweetIds.has(tweet.id);
|
|
9715
10153
|
const author = usersById.get(tweet.author_id);
|
|
9716
10154
|
const profile = author ? upsertProfileFromXUser(db, author) : ensureStubProfileForXUser(db, tweet.author_id);
|
|
9717
10155
|
const replyToId = getReferencedTweetId(tweet, "replied_to");
|
|
9718
10156
|
const quotedTweetId = getReferencedTweetId(tweet, "quoted");
|
|
10157
|
+
const shouldMarkReplied = isPrimaryTweet && markRepliesAsReplied && Boolean(replyToId);
|
|
9719
10158
|
upsertTweet.run(
|
|
9720
10159
|
tweet.id,
|
|
9721
10160
|
profile.profile.id,
|
|
9722
10161
|
tweet.text,
|
|
9723
10162
|
tweet.created_at,
|
|
9724
|
-
|
|
10163
|
+
shouldMarkReplied ? 1 : 0,
|
|
9725
10164
|
replyToId,
|
|
9726
10165
|
Number(tweet.public_metrics?.like_count ?? 0),
|
|
9727
10166
|
countTweetMedia(tweet),
|
|
9728
10167
|
JSON.stringify(tweetEntitiesFromXurl(tweet.entities)),
|
|
9729
10168
|
buildMediaJsonFromIncludes(tweet, payload.includes?.media),
|
|
9730
|
-
quotedTweetId
|
|
10169
|
+
quotedTweetId,
|
|
10170
|
+
!isPrimaryTweet && tweet.public_metrics?.like_count === void 0 ? 1 : 0
|
|
9731
10171
|
);
|
|
9732
|
-
if (edgeKind) {
|
|
10172
|
+
if (edgeKind && isPrimaryTweet) {
|
|
9733
10173
|
upsertTweetAccountEdge(db, {
|
|
9734
10174
|
accountId,
|
|
9735
10175
|
tweetId: tweet.id,
|
|
@@ -9739,16 +10179,20 @@ function ingestTweetPayload(db, {
|
|
|
9739
10179
|
rawJson: JSON.stringify(tweet)
|
|
9740
10180
|
});
|
|
9741
10181
|
}
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
10182
|
+
if (isPrimaryTweet) {
|
|
10183
|
+
upsertCollection?.run(
|
|
10184
|
+
accountId,
|
|
10185
|
+
tweet.id,
|
|
10186
|
+
collectionKind2,
|
|
10187
|
+
source,
|
|
10188
|
+
JSON.stringify(tweet),
|
|
10189
|
+
observedAt
|
|
10190
|
+
);
|
|
10191
|
+
}
|
|
9750
10192
|
replaceTweetFts(db, tweet.id, tweet.text);
|
|
9751
|
-
|
|
10193
|
+
if (isPrimaryTweet) {
|
|
10194
|
+
tweetIds.push(tweet.id);
|
|
10195
|
+
}
|
|
9752
10196
|
}
|
|
9753
10197
|
})();
|
|
9754
10198
|
return tweetIds;
|
|
@@ -10720,6 +11164,8 @@ var RECENT_TIMELINE_EDGE_CANDIDATES = 5e3;
|
|
|
10720
11164
|
function listTimelineItems({
|
|
10721
11165
|
resource,
|
|
10722
11166
|
account,
|
|
11167
|
+
listAccountId,
|
|
11168
|
+
listId,
|
|
10723
11169
|
search,
|
|
10724
11170
|
replyFilter = "all",
|
|
10725
11171
|
since,
|
|
@@ -10750,14 +11196,14 @@ function listTimelineItems({
|
|
|
10750
11196
|
let join3 = "";
|
|
10751
11197
|
let where = "where e.kind = ?";
|
|
10752
11198
|
let searchSnippetSelect = "";
|
|
10753
|
-
const canUseRecentEdgeWindow = !likedOnly && !bookmarkedOnly && !account && !search?.trim() && replyFilter === "all" && !since?.trim() && !until?.trim() && includeReplies && qualityFilter === "all";
|
|
11199
|
+
const canUseRecentEdgeWindow = !likedOnly && !bookmarkedOnly && !listId && !account && !search?.trim() && replyFilter === "all" && !since?.trim() && !until?.trim() && includeReplies && qualityFilter === "all";
|
|
10754
11200
|
if (likedOnly || bookmarkedOnly) {
|
|
10755
11201
|
if (likedOnly && bookmarkedOnly) {
|
|
10756
11202
|
timelineEdgesCte = `
|
|
10757
11203
|
with timeline_edges as (
|
|
10758
11204
|
select likes.account_id, likes.tweet_id, 'home' as kind, likes.raw_json
|
|
10759
|
-
|
|
10760
|
-
join tweet_collections bookmarks
|
|
11205
|
+
from tweet_collections likes indexed by idx_tweet_collections_tweet
|
|
11206
|
+
join tweet_collections bookmarks indexed by idx_tweet_collections_tweet
|
|
10761
11207
|
on bookmarks.account_id = likes.account_id
|
|
10762
11208
|
and bookmarks.tweet_id = likes.tweet_id
|
|
10763
11209
|
and bookmarks.kind = 'bookmarks'
|
|
@@ -10769,7 +11215,7 @@ function listTimelineItems({
|
|
|
10769
11215
|
timelineEdgesCte = `
|
|
10770
11216
|
with timeline_edges as (
|
|
10771
11217
|
select account_id, tweet_id, 'home' as kind, raw_json
|
|
10772
|
-
from tweet_collections
|
|
11218
|
+
from tweet_collections indexed by idx_tweet_collections_tweet
|
|
10773
11219
|
where kind = ?
|
|
10774
11220
|
)
|
|
10775
11221
|
`;
|
|
@@ -10846,6 +11292,22 @@ function listTimelineItems({
|
|
|
10846
11292
|
params.push(until.trim());
|
|
10847
11293
|
}
|
|
10848
11294
|
}
|
|
11295
|
+
if (Boolean(listId) !== Boolean(listAccountId)) {
|
|
11296
|
+
throw new Error("List filtering requires both listId and listAccountId");
|
|
11297
|
+
}
|
|
11298
|
+
if (listId && listAccountId) {
|
|
11299
|
+
where += `
|
|
11300
|
+
and exists (
|
|
11301
|
+
select 1
|
|
11302
|
+
from x_list_members list_member
|
|
11303
|
+
where list_member.account_id = ?
|
|
11304
|
+
and list_member.list_id = ?
|
|
11305
|
+
and list_member.profile_id = t.author_profile_id
|
|
11306
|
+
and list_member.current = 1
|
|
11307
|
+
)
|
|
11308
|
+
`;
|
|
11309
|
+
params.push(listAccountId, listId);
|
|
11310
|
+
}
|
|
10849
11311
|
const ftsSearch = search?.trim() ? toFtsSearchQuery(search) : "";
|
|
10850
11312
|
if (ftsSearch) {
|
|
10851
11313
|
join3 += " join tweets_fts on tweets_fts.tweet_id = t.id ";
|
|
@@ -12519,6 +12981,7 @@ function reachedStartTimeBoundary(payload, startTimeMs) {
|
|
|
12519
12981
|
function mergeTimelinePayloads(payloads, limit) {
|
|
12520
12982
|
const data = [];
|
|
12521
12983
|
const usersById = /* @__PURE__ */ new Map();
|
|
12984
|
+
const tweetsById = /* @__PURE__ */ new Map();
|
|
12522
12985
|
const mediaByKey = /* @__PURE__ */ new Map();
|
|
12523
12986
|
let meta;
|
|
12524
12987
|
for (const payload of payloads) {
|
|
@@ -12531,6 +12994,9 @@ function mergeTimelinePayloads(payloads, limit) {
|
|
|
12531
12994
|
for (const user of payload.includes?.users ?? []) {
|
|
12532
12995
|
usersById.set(user.id, user);
|
|
12533
12996
|
}
|
|
12997
|
+
for (const tweet of payload.includes?.tweets ?? []) {
|
|
12998
|
+
tweetsById.set(tweet.id, tweet);
|
|
12999
|
+
}
|
|
12534
13000
|
for (const media of payload.includes?.media ?? []) {
|
|
12535
13001
|
mediaByKey.set(media.media_key, media);
|
|
12536
13002
|
}
|
|
@@ -12540,6 +13006,7 @@ function mergeTimelinePayloads(payloads, limit) {
|
|
|
12540
13006
|
data,
|
|
12541
13007
|
includes: {
|
|
12542
13008
|
users: [...usersById.values()],
|
|
13009
|
+
tweets: [...tweetsById.values()],
|
|
12543
13010
|
media: [...mediaByKey.values()]
|
|
12544
13011
|
},
|
|
12545
13012
|
meta
|
|
@@ -12848,6 +13315,7 @@ function compactTweet(source, item) {
|
|
|
12848
13315
|
createdAt: item.createdAt,
|
|
12849
13316
|
text: item.text,
|
|
12850
13317
|
entities: item.entities,
|
|
13318
|
+
media: item.media,
|
|
12851
13319
|
likeCount: item.likeCount,
|
|
12852
13320
|
liked: item.liked,
|
|
12853
13321
|
bookmarked: item.bookmarked,
|
|
@@ -12867,6 +13335,7 @@ function compactEmbeddedTweet(item) {
|
|
|
12867
13335
|
createdAt: item.createdAt,
|
|
12868
13336
|
text: item.text,
|
|
12869
13337
|
entities: item.entities,
|
|
13338
|
+
media: item.media,
|
|
12870
13339
|
likeCount: item.likeCount ?? 0,
|
|
12871
13340
|
liked: Boolean(item.liked),
|
|
12872
13341
|
bookmarked: Boolean(item.bookmarked),
|
|
@@ -15391,6 +15860,8 @@ function compactTweet2(source, item) {
|
|
|
15391
15860
|
authorProfile: item.author,
|
|
15392
15861
|
createdAt: item.createdAt,
|
|
15393
15862
|
text: item.text,
|
|
15863
|
+
entities: item.entities,
|
|
15864
|
+
media: item.media,
|
|
15394
15865
|
likeCount: item.likeCount,
|
|
15395
15866
|
liked: item.liked,
|
|
15396
15867
|
bookmarked: item.bookmarked,
|
|
@@ -15428,7 +15899,9 @@ function collectLiveSearchTweets(options) {
|
|
|
15428
15899
|
t.created_at,
|
|
15429
15900
|
t.is_replied,
|
|
15430
15901
|
t.like_count,
|
|
15431
|
-
|
|
15902
|
+
t.media_count,
|
|
15903
|
+
t.entities_json,
|
|
15904
|
+
t.media_json,
|
|
15432
15905
|
case
|
|
15433
15906
|
when exists (
|
|
15434
15907
|
select 1 from tweet_collections collection
|
|
@@ -15482,6 +15955,8 @@ function collectLiveSearchTweets(options) {
|
|
|
15482
15955
|
},
|
|
15483
15956
|
createdAt: String(row.created_at),
|
|
15484
15957
|
text: String(row.text),
|
|
15958
|
+
entities: parseJsonField2(row.entities_json, {}),
|
|
15959
|
+
media: parseJsonField2(row.media_json, []),
|
|
15485
15960
|
likeCount: Number(row.like_count),
|
|
15486
15961
|
liked: Boolean(row.liked),
|
|
15487
15962
|
bookmarked: Boolean(row.bookmarked),
|
|
@@ -23372,19 +23847,550 @@ function registerJobCommands({ program: program2, print: print2 }) {
|
|
|
23372
23847
|
});
|
|
23373
23848
|
}
|
|
23374
23849
|
|
|
23850
|
+
// src/lib/x-lists.ts
|
|
23851
|
+
import { Effect as Effect47 } from "effect";
|
|
23852
|
+
var DEFAULT_MAX_LISTS = 20;
|
|
23853
|
+
var DEFAULT_MEMBER_LIMIT = 20;
|
|
23854
|
+
var DEFAULT_MAX_MEMBER_PAGES = 1;
|
|
23855
|
+
var DEFAULT_DELAY_MS2 = 1e3;
|
|
23856
|
+
var MAX_LISTS = 100;
|
|
23857
|
+
var MAX_MEMBER_LIMIT = 100;
|
|
23858
|
+
var MAX_MEMBER_PAGES = 100;
|
|
23859
|
+
function positiveInteger(name, value, maximum) {
|
|
23860
|
+
if (!Number.isFinite(value) || value < 1 || value > maximum) {
|
|
23861
|
+
throw new Error(`${name} must be between 1 and ${String(maximum)}`);
|
|
23862
|
+
}
|
|
23863
|
+
return Math.floor(value);
|
|
23864
|
+
}
|
|
23865
|
+
function nonNegativeInteger(name, value) {
|
|
23866
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
23867
|
+
throw new Error(`${name} must be a non-negative integer`);
|
|
23868
|
+
}
|
|
23869
|
+
return Math.floor(value);
|
|
23870
|
+
}
|
|
23871
|
+
function normalizedMode(value) {
|
|
23872
|
+
if (value === void 0) return "auto";
|
|
23873
|
+
if (value === "auto" || value === "bird" || value === "xurl") return value;
|
|
23874
|
+
throw new Error("--mode must be auto, bird, or xurl");
|
|
23875
|
+
}
|
|
23876
|
+
function metaString(meta, key) {
|
|
23877
|
+
const value = meta?.[key];
|
|
23878
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
23879
|
+
}
|
|
23880
|
+
function metaNumber(meta, key) {
|
|
23881
|
+
const value = meta?.[key];
|
|
23882
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
23883
|
+
}
|
|
23884
|
+
function parseJsonRecord(value) {
|
|
23885
|
+
if (typeof value !== "string" || value.length === 0) return {};
|
|
23886
|
+
try {
|
|
23887
|
+
const parsed = JSON.parse(value);
|
|
23888
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
23889
|
+
} catch {
|
|
23890
|
+
return {};
|
|
23891
|
+
}
|
|
23892
|
+
}
|
|
23893
|
+
function fetchOwnedListsEffect({
|
|
23894
|
+
source,
|
|
23895
|
+
account,
|
|
23896
|
+
maxLists
|
|
23897
|
+
}) {
|
|
23898
|
+
if (source === "bird") {
|
|
23899
|
+
return Effect47.gen(function* () {
|
|
23900
|
+
const authenticated = yield* getAuthenticatedBirdAccountEffect();
|
|
23901
|
+
yield* trySync(
|
|
23902
|
+
() => assertLiveAccountMatches({
|
|
23903
|
+
source: "bird",
|
|
23904
|
+
account,
|
|
23905
|
+
liveUsername: authenticated.username,
|
|
23906
|
+
liveExternalUserId: authenticated.id
|
|
23907
|
+
})
|
|
23908
|
+
);
|
|
23909
|
+
return yield* listOwnedXListsViaBirdEffect({ maxResults: maxLists });
|
|
23910
|
+
});
|
|
23911
|
+
}
|
|
23912
|
+
return Effect47.gen(function* () {
|
|
23913
|
+
const authenticated = yield* lookupAuthenticatedOAuth2UserEffect(
|
|
23914
|
+
account.username
|
|
23915
|
+
);
|
|
23916
|
+
if (!authenticated) {
|
|
23917
|
+
return yield* Effect47.fail(
|
|
23918
|
+
new Error("xurl did not return an OAuth2 user")
|
|
23919
|
+
);
|
|
23920
|
+
}
|
|
23921
|
+
const liveUsername = String(authenticated.username ?? "");
|
|
23922
|
+
const liveExternalUserId = String(authenticated.id ?? "");
|
|
23923
|
+
yield* trySync(
|
|
23924
|
+
() => assertLiveAccountMatches({
|
|
23925
|
+
source: "xurl",
|
|
23926
|
+
account,
|
|
23927
|
+
liveUsername,
|
|
23928
|
+
liveExternalUserId
|
|
23929
|
+
})
|
|
23930
|
+
);
|
|
23931
|
+
const userId = account.externalUserId ?? liveExternalUserId;
|
|
23932
|
+
if (!userId) {
|
|
23933
|
+
return yield* Effect47.fail(
|
|
23934
|
+
new Error("xurl authenticated user is missing an id")
|
|
23935
|
+
);
|
|
23936
|
+
}
|
|
23937
|
+
return yield* listOwnedXListsViaXurlEffect({
|
|
23938
|
+
userId,
|
|
23939
|
+
username: account.username,
|
|
23940
|
+
maxResults: maxLists
|
|
23941
|
+
});
|
|
23942
|
+
});
|
|
23943
|
+
}
|
|
23944
|
+
function fetchListMembersEffect({
|
|
23945
|
+
source,
|
|
23946
|
+
list,
|
|
23947
|
+
username,
|
|
23948
|
+
memberLimit,
|
|
23949
|
+
maxMemberPages
|
|
23950
|
+
}) {
|
|
23951
|
+
if (source === "bird") {
|
|
23952
|
+
return listXListMembersViaBirdEffect({
|
|
23953
|
+
listId: list.id,
|
|
23954
|
+
maxResults: memberLimit,
|
|
23955
|
+
maxPages: maxMemberPages
|
|
23956
|
+
});
|
|
23957
|
+
}
|
|
23958
|
+
return Effect47.gen(function* () {
|
|
23959
|
+
const data = [];
|
|
23960
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23961
|
+
let paginationToken;
|
|
23962
|
+
let pageCount = 0;
|
|
23963
|
+
do {
|
|
23964
|
+
const page = yield* listXListMembersViaXurlEffect({
|
|
23965
|
+
listId: list.id,
|
|
23966
|
+
username,
|
|
23967
|
+
maxResults: memberLimit,
|
|
23968
|
+
paginationToken
|
|
23969
|
+
});
|
|
23970
|
+
pageCount += 1;
|
|
23971
|
+
for (const user of page.data) {
|
|
23972
|
+
if (seen.has(user.id)) continue;
|
|
23973
|
+
seen.add(user.id);
|
|
23974
|
+
data.push(user);
|
|
23975
|
+
}
|
|
23976
|
+
paginationToken = metaString(page.meta, "next_token");
|
|
23977
|
+
} while (paginationToken && pageCount < maxMemberPages);
|
|
23978
|
+
return {
|
|
23979
|
+
data,
|
|
23980
|
+
meta: {
|
|
23981
|
+
result_count: data.length,
|
|
23982
|
+
page_count: pageCount,
|
|
23983
|
+
next_token: paginationToken ?? null,
|
|
23984
|
+
pagination_known_complete: !paginationToken
|
|
23985
|
+
}
|
|
23986
|
+
};
|
|
23987
|
+
});
|
|
23988
|
+
}
|
|
23989
|
+
function membershipStatus({
|
|
23990
|
+
list,
|
|
23991
|
+
payload
|
|
23992
|
+
}) {
|
|
23993
|
+
if (payload.meta?.pagination_known_complete === true) return "complete";
|
|
23994
|
+
if (payload.meta?.pagination_inferred_complete === true) return "inferred";
|
|
23995
|
+
if (typeof list.memberCount === "number" && payload.data.length >= list.memberCount) {
|
|
23996
|
+
return "inferred";
|
|
23997
|
+
}
|
|
23998
|
+
return "partial";
|
|
23999
|
+
}
|
|
24000
|
+
function ownerProfileId(db, list) {
|
|
24001
|
+
if (!list.ownerId || !list.ownerUsername) return void 0;
|
|
24002
|
+
return upsertProfileFromXUser(db, {
|
|
24003
|
+
id: list.ownerId,
|
|
24004
|
+
username: list.ownerUsername,
|
|
24005
|
+
name: list.ownerName ?? list.ownerUsername
|
|
24006
|
+
}).profile.id;
|
|
24007
|
+
}
|
|
24008
|
+
function persistListResult({
|
|
24009
|
+
db,
|
|
24010
|
+
accountId,
|
|
24011
|
+
list,
|
|
24012
|
+
source,
|
|
24013
|
+
payload,
|
|
24014
|
+
status,
|
|
24015
|
+
now: now2,
|
|
24016
|
+
rateLimit
|
|
24017
|
+
}) {
|
|
24018
|
+
return db.transaction(() => {
|
|
24019
|
+
const listOwnerProfileId = ownerProfileId(db, list);
|
|
24020
|
+
const membersSyncedAt = payload ? now2 : null;
|
|
24021
|
+
const pageCount = payload ? metaNumber(payload.meta, "page_count") ?? 1 : 0;
|
|
24022
|
+
const resultCount = payload?.data.length ?? 0;
|
|
24023
|
+
db.prepare(
|
|
24024
|
+
`
|
|
24025
|
+
insert into x_lists (
|
|
24026
|
+
account_id, list_id, name, description, owner_profile_id,
|
|
24027
|
+
owner_external_user_id, is_private, member_count, follower_count,
|
|
24028
|
+
source, membership_status, lists_synced_at, members_synced_at,
|
|
24029
|
+
member_page_count, member_result_count, rate_limit_json, raw_json,
|
|
24030
|
+
updated_at
|
|
24031
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
24032
|
+
on conflict(account_id, list_id) do update set
|
|
24033
|
+
name = excluded.name,
|
|
24034
|
+
description = excluded.description,
|
|
24035
|
+
owner_profile_id = coalesce(excluded.owner_profile_id, x_lists.owner_profile_id),
|
|
24036
|
+
owner_external_user_id = coalesce(excluded.owner_external_user_id, x_lists.owner_external_user_id),
|
|
24037
|
+
is_private = excluded.is_private,
|
|
24038
|
+
member_count = coalesce(excluded.member_count, x_lists.member_count),
|
|
24039
|
+
follower_count = coalesce(excluded.follower_count, x_lists.follower_count),
|
|
24040
|
+
source = excluded.source,
|
|
24041
|
+
membership_status = excluded.membership_status,
|
|
24042
|
+
lists_synced_at = excluded.lists_synced_at,
|
|
24043
|
+
members_synced_at = coalesce(excluded.members_synced_at, x_lists.members_synced_at),
|
|
24044
|
+
member_page_count = excluded.member_page_count,
|
|
24045
|
+
member_result_count = excluded.member_result_count,
|
|
24046
|
+
rate_limit_json = excluded.rate_limit_json,
|
|
24047
|
+
raw_json = excluded.raw_json,
|
|
24048
|
+
updated_at = excluded.updated_at
|
|
24049
|
+
`
|
|
24050
|
+
).run(
|
|
24051
|
+
accountId,
|
|
24052
|
+
list.id,
|
|
24053
|
+
list.name,
|
|
24054
|
+
list.description ?? "",
|
|
24055
|
+
listOwnerProfileId ?? null,
|
|
24056
|
+
list.ownerId ?? null,
|
|
24057
|
+
list.isPrivate ? 1 : 0,
|
|
24058
|
+
list.memberCount ?? null,
|
|
24059
|
+
list.followerCount ?? null,
|
|
24060
|
+
source,
|
|
24061
|
+
status,
|
|
24062
|
+
now2,
|
|
24063
|
+
membersSyncedAt,
|
|
24064
|
+
pageCount,
|
|
24065
|
+
resultCount,
|
|
24066
|
+
JSON.stringify(rateLimit),
|
|
24067
|
+
JSON.stringify(list.raw ?? {}),
|
|
24068
|
+
now2
|
|
24069
|
+
);
|
|
24070
|
+
if (!payload) return;
|
|
24071
|
+
if (status === "complete") {
|
|
24072
|
+
db.prepare(
|
|
24073
|
+
`
|
|
24074
|
+
update x_list_members
|
|
24075
|
+
set current = 0, ended_at = ?, updated_at = ?
|
|
24076
|
+
where account_id = ? and list_id = ? and current = 1
|
|
24077
|
+
`
|
|
24078
|
+
).run(now2, now2, accountId, list.id);
|
|
24079
|
+
}
|
|
24080
|
+
const upsertMember = db.prepare(`
|
|
24081
|
+
insert into x_list_members (
|
|
24082
|
+
account_id, list_id, profile_id, external_user_id, source, current,
|
|
24083
|
+
first_seen_at, last_seen_at, ended_at, raw_json, updated_at
|
|
24084
|
+
) values (?, ?, ?, ?, ?, 1, ?, ?, null, ?, ?)
|
|
24085
|
+
on conflict(account_id, list_id, profile_id) do update set
|
|
24086
|
+
external_user_id = excluded.external_user_id,
|
|
24087
|
+
source = excluded.source,
|
|
24088
|
+
current = 1,
|
|
24089
|
+
last_seen_at = excluded.last_seen_at,
|
|
24090
|
+
ended_at = null,
|
|
24091
|
+
raw_json = excluded.raw_json,
|
|
24092
|
+
updated_at = excluded.updated_at
|
|
24093
|
+
`);
|
|
24094
|
+
for (const user of payload.data) {
|
|
24095
|
+
const resolved = upsertProfileFromXUser(db, user);
|
|
24096
|
+
upsertMember.run(
|
|
24097
|
+
accountId,
|
|
24098
|
+
list.id,
|
|
24099
|
+
resolved.profile.id,
|
|
24100
|
+
resolved.externalUserId,
|
|
24101
|
+
source,
|
|
24102
|
+
now2,
|
|
24103
|
+
now2,
|
|
24104
|
+
JSON.stringify(user),
|
|
24105
|
+
now2
|
|
24106
|
+
);
|
|
24107
|
+
}
|
|
24108
|
+
})();
|
|
24109
|
+
}
|
|
24110
|
+
function syncXListsEffect(options = {}) {
|
|
24111
|
+
return Effect47.suspend(() => {
|
|
24112
|
+
const mode = normalizedMode(options.mode);
|
|
24113
|
+
const maxLists = positiveInteger(
|
|
24114
|
+
"--max-lists",
|
|
24115
|
+
options.maxLists ?? DEFAULT_MAX_LISTS,
|
|
24116
|
+
MAX_LISTS
|
|
24117
|
+
);
|
|
24118
|
+
const memberLimit = positiveInteger(
|
|
24119
|
+
"--member-limit",
|
|
24120
|
+
options.memberLimit ?? DEFAULT_MEMBER_LIMIT,
|
|
24121
|
+
MAX_MEMBER_LIMIT
|
|
24122
|
+
);
|
|
24123
|
+
const maxMemberPages = positiveInteger(
|
|
24124
|
+
"--max-member-pages",
|
|
24125
|
+
options.maxMemberPages ?? DEFAULT_MAX_MEMBER_PAGES,
|
|
24126
|
+
MAX_MEMBER_PAGES
|
|
24127
|
+
);
|
|
24128
|
+
const delayMs = nonNegativeInteger(
|
|
24129
|
+
"--delay-ms",
|
|
24130
|
+
options.delayMs ?? DEFAULT_DELAY_MS2
|
|
24131
|
+
);
|
|
24132
|
+
const db = getNativeDb();
|
|
24133
|
+
const account = resolveLiveSyncAccount(db, options.account);
|
|
24134
|
+
const sources = mode === "auto" ? ["bird", "xurl"] : [mode];
|
|
24135
|
+
const transports = sources.map(
|
|
24136
|
+
(source) => createLiveTransportAdapter(
|
|
24137
|
+
source,
|
|
24138
|
+
fetchOwnedListsEffect({ source, account, maxLists })
|
|
24139
|
+
)
|
|
24140
|
+
);
|
|
24141
|
+
return Effect47.gen(function* () {
|
|
24142
|
+
const discovered = yield* fetchWithTransportFallbackEffect(transports);
|
|
24143
|
+
const source = discovered.source;
|
|
24144
|
+
const lists = discovered.payload.data.slice(0, maxLists);
|
|
24145
|
+
const results = [];
|
|
24146
|
+
for (const [index, list] of lists.entries()) {
|
|
24147
|
+
if (index > 0 && delayMs > 0) yield* Effect47.sleep(delayMs);
|
|
24148
|
+
const fetched = yield* fetchListMembersEffect({
|
|
24149
|
+
source,
|
|
24150
|
+
list,
|
|
24151
|
+
username: account.username,
|
|
24152
|
+
memberLimit,
|
|
24153
|
+
maxMemberPages
|
|
24154
|
+
}).pipe(
|
|
24155
|
+
Effect47.map((payload) => ({ ok: true, payload })),
|
|
24156
|
+
Effect47.catchAll(
|
|
24157
|
+
(error) => Effect47.succeed({ ok: false, error: toError(error) })
|
|
24158
|
+
)
|
|
24159
|
+
);
|
|
24160
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
24161
|
+
const rateLimit = {
|
|
24162
|
+
memberLimit,
|
|
24163
|
+
maxMemberPages,
|
|
24164
|
+
delayMs,
|
|
24165
|
+
nextCursorStored: fetched.ok ? Boolean(metaString(fetched.payload.meta, "next_token")) : false
|
|
24166
|
+
};
|
|
24167
|
+
if (!fetched.ok) {
|
|
24168
|
+
yield* databaseWriteEffect(
|
|
24169
|
+
(writeDb) => persistListResult({
|
|
24170
|
+
db: writeDb,
|
|
24171
|
+
accountId: account.accountId,
|
|
24172
|
+
list,
|
|
24173
|
+
source,
|
|
24174
|
+
status: "error",
|
|
24175
|
+
now: now2,
|
|
24176
|
+
rateLimit
|
|
24177
|
+
})
|
|
24178
|
+
);
|
|
24179
|
+
results.push({
|
|
24180
|
+
listId: list.id,
|
|
24181
|
+
name: list.name,
|
|
24182
|
+
status: "error",
|
|
24183
|
+
error: fetched.error.message
|
|
24184
|
+
});
|
|
24185
|
+
continue;
|
|
24186
|
+
}
|
|
24187
|
+
const status = membershipStatus({ list, payload: fetched.payload });
|
|
24188
|
+
yield* databaseWriteEffect(
|
|
24189
|
+
(writeDb) => persistListResult({
|
|
24190
|
+
db: writeDb,
|
|
24191
|
+
accountId: account.accountId,
|
|
24192
|
+
list,
|
|
24193
|
+
source,
|
|
24194
|
+
payload: fetched.payload,
|
|
24195
|
+
status,
|
|
24196
|
+
now: now2,
|
|
24197
|
+
rateLimit
|
|
24198
|
+
})
|
|
24199
|
+
);
|
|
24200
|
+
results.push({
|
|
24201
|
+
listId: list.id,
|
|
24202
|
+
name: list.name,
|
|
24203
|
+
status,
|
|
24204
|
+
members: fetched.payload.data.length,
|
|
24205
|
+
pages: metaNumber(fetched.payload.meta, "page_count") ?? 1
|
|
24206
|
+
});
|
|
24207
|
+
}
|
|
24208
|
+
const errors = results.filter((result) => result.status === "error");
|
|
24209
|
+
return {
|
|
24210
|
+
ok: errors.length === 0,
|
|
24211
|
+
accountId: account.accountId,
|
|
24212
|
+
mode,
|
|
24213
|
+
source,
|
|
24214
|
+
listCount: lists.length,
|
|
24215
|
+
membershipCompleteCount: results.filter(
|
|
24216
|
+
(result) => result.status === "complete"
|
|
24217
|
+
).length,
|
|
24218
|
+
membershipPartialCount: results.filter(
|
|
24219
|
+
(result) => result.status === "partial"
|
|
24220
|
+
).length,
|
|
24221
|
+
membershipInferredCount: results.filter(
|
|
24222
|
+
(result) => result.status === "inferred"
|
|
24223
|
+
).length,
|
|
24224
|
+
errorCount: errors.length,
|
|
24225
|
+
rateLimit: { maxLists, memberLimit, maxMemberPages, delayMs },
|
|
24226
|
+
lists: results
|
|
24227
|
+
};
|
|
24228
|
+
});
|
|
24229
|
+
});
|
|
24230
|
+
}
|
|
24231
|
+
function syncXLists(options = {}) {
|
|
24232
|
+
return runEffectPromise(syncXListsEffect(options));
|
|
24233
|
+
}
|
|
24234
|
+
function accountIdForRead(db, account) {
|
|
24235
|
+
return resolveLiveSyncAccount(db, account).accountId;
|
|
24236
|
+
}
|
|
24237
|
+
function storedListFromRow(row) {
|
|
24238
|
+
return {
|
|
24239
|
+
accountId: String(row.account_id),
|
|
24240
|
+
listId: String(row.list_id),
|
|
24241
|
+
name: String(row.name),
|
|
24242
|
+
description: String(row.description ?? ""),
|
|
24243
|
+
...row.owner_profile_id ? { ownerProfileId: String(row.owner_profile_id) } : {},
|
|
24244
|
+
...row.owner_external_user_id ? { ownerExternalUserId: String(row.owner_external_user_id) } : {},
|
|
24245
|
+
isPrivate: Number(row.is_private) === 1,
|
|
24246
|
+
...typeof row.member_count === "number" ? { memberCount: row.member_count } : {},
|
|
24247
|
+
...typeof row.follower_count === "number" ? { followerCount: row.follower_count } : {},
|
|
24248
|
+
source: String(row.source),
|
|
24249
|
+
membershipStatus: String(row.membership_status),
|
|
24250
|
+
listsSyncedAt: String(row.lists_synced_at),
|
|
24251
|
+
...row.members_synced_at ? { membersSyncedAt: String(row.members_synced_at) } : {},
|
|
24252
|
+
memberPageCount: Number(row.member_page_count),
|
|
24253
|
+
memberResultCount: Number(row.member_result_count),
|
|
24254
|
+
rateLimit: parseJsonRecord(row.rate_limit_json)
|
|
24255
|
+
};
|
|
24256
|
+
}
|
|
24257
|
+
function listStoredXLists(options = {}) {
|
|
24258
|
+
const db = getReadDb();
|
|
24259
|
+
const accountId = accountIdForRead(db, options.account);
|
|
24260
|
+
return db.prepare(
|
|
24261
|
+
`select * from x_lists where account_id = ? order by name collate nocase, list_id`
|
|
24262
|
+
).all(accountId).map(storedListFromRow);
|
|
24263
|
+
}
|
|
24264
|
+
function resolveStoredXListSelector({
|
|
24265
|
+
account,
|
|
24266
|
+
listId,
|
|
24267
|
+
name
|
|
24268
|
+
}) {
|
|
24269
|
+
if (Boolean(listId) === Boolean(name)) {
|
|
24270
|
+
throw new Error("Choose exactly one of --list or --list-id");
|
|
24271
|
+
}
|
|
24272
|
+
const db = getReadDb();
|
|
24273
|
+
const accountId = accountIdForRead(db, account);
|
|
24274
|
+
const rows = listId ? db.prepare("select * from x_lists where account_id = ? and list_id = ?").all(accountId, listId) : db.prepare(
|
|
24275
|
+
"select * from x_lists where account_id = ? and name = ? collate nocase order by list_id"
|
|
24276
|
+
).all(accountId, name);
|
|
24277
|
+
if (rows.length === 0) {
|
|
24278
|
+
throw new Error(
|
|
24279
|
+
`No cached List matches ${listId ? `id ${listId}` : `name ${name}`}; run birdclaw sync lists`
|
|
24280
|
+
);
|
|
24281
|
+
}
|
|
24282
|
+
if (rows.length > 1) {
|
|
24283
|
+
throw new Error(
|
|
24284
|
+
`List name ${name} is ambiguous; use --list-id (${rows.map((row) => String(row.list_id)).join(", ")})`
|
|
24285
|
+
);
|
|
24286
|
+
}
|
|
24287
|
+
return storedListFromRow(rows[0]);
|
|
24288
|
+
}
|
|
24289
|
+
function listStoredXListMembers({
|
|
24290
|
+
account,
|
|
24291
|
+
listId,
|
|
24292
|
+
name,
|
|
24293
|
+
includeEnded = false,
|
|
24294
|
+
limit = 100
|
|
24295
|
+
}) {
|
|
24296
|
+
const list = resolveStoredXListSelector({ account, listId, name });
|
|
24297
|
+
const db = getReadDb();
|
|
24298
|
+
const rows = db.prepare(
|
|
24299
|
+
`
|
|
24300
|
+
select
|
|
24301
|
+
member.account_id, member.list_id, member.external_user_id,
|
|
24302
|
+
member.current, member.first_seen_at, member.last_seen_at,
|
|
24303
|
+
member.ended_at, profile.id as profile_id, profile.handle,
|
|
24304
|
+
profile.display_name, profile.bio, profile.followers_count,
|
|
24305
|
+
profile.avatar_url
|
|
24306
|
+
from x_list_members member
|
|
24307
|
+
join profiles profile on profile.id = member.profile_id
|
|
24308
|
+
where member.account_id = ? and member.list_id = ?
|
|
24309
|
+
and (? = 1 or member.current = 1)
|
|
24310
|
+
order by profile.followers_count desc, profile.handle collate nocase
|
|
24311
|
+
limit ?
|
|
24312
|
+
`
|
|
24313
|
+
).all(
|
|
24314
|
+
list.accountId,
|
|
24315
|
+
list.listId,
|
|
24316
|
+
includeEnded ? 1 : 0,
|
|
24317
|
+
positiveInteger("--limit", limit, 1e4)
|
|
24318
|
+
);
|
|
24319
|
+
const items = rows.map((row) => ({
|
|
24320
|
+
accountId: String(row.account_id),
|
|
24321
|
+
listId: String(row.list_id),
|
|
24322
|
+
externalUserId: String(row.external_user_id),
|
|
24323
|
+
current: Number(row.current) === 1,
|
|
24324
|
+
firstSeenAt: String(row.first_seen_at),
|
|
24325
|
+
lastSeenAt: String(row.last_seen_at),
|
|
24326
|
+
...row.ended_at ? { endedAt: String(row.ended_at) } : {},
|
|
24327
|
+
profile: {
|
|
24328
|
+
id: String(row.profile_id),
|
|
24329
|
+
handle: String(row.handle),
|
|
24330
|
+
displayName: String(row.display_name),
|
|
24331
|
+
bio: String(row.bio),
|
|
24332
|
+
followersCount: Number(row.followers_count),
|
|
24333
|
+
...row.avatar_url ? { avatarUrl: String(row.avatar_url) } : {}
|
|
24334
|
+
}
|
|
24335
|
+
}));
|
|
24336
|
+
return { list, items };
|
|
24337
|
+
}
|
|
24338
|
+
|
|
24339
|
+
// src/cli/register-lists.ts
|
|
24340
|
+
function registerListCommands({
|
|
24341
|
+
program: program2,
|
|
24342
|
+
print: print2,
|
|
24343
|
+
asJson,
|
|
24344
|
+
autoUpdateBeforeRead: autoUpdateBeforeRead2
|
|
24345
|
+
}) {
|
|
24346
|
+
const listsCommand = program2.command("lists").description("Inspect cached X Lists and membership");
|
|
24347
|
+
listsCommand.command("list").description("List cached owned X Lists").option("--account <accountId>", "Account id").action(async (options) => {
|
|
24348
|
+
await autoUpdateBeforeRead2();
|
|
24349
|
+
const items = listStoredXLists({ account: options.account });
|
|
24350
|
+
if (asJson()) {
|
|
24351
|
+
print2(items, true);
|
|
24352
|
+
return;
|
|
24353
|
+
}
|
|
24354
|
+
console.log(
|
|
24355
|
+
items.map(
|
|
24356
|
+
(item) => `${item.name} ${item.listId} ${item.membershipStatus} ${String(item.memberResultCount)}/${item.memberCount === void 0 ? "?" : String(item.memberCount)}`
|
|
24357
|
+
).join("\n")
|
|
24358
|
+
);
|
|
24359
|
+
});
|
|
24360
|
+
listsCommand.command("members [name]").description("List cached members for one X List").option("--list-id <id>", "Select by List id").option("--account <accountId>", "Account id").option("--include-ended", "Include members absent from a complete sync").option("--limit <n>", "Limit results", "100").action(async (name, options) => {
|
|
24361
|
+
await autoUpdateBeforeRead2();
|
|
24362
|
+
const result = listStoredXListMembers({
|
|
24363
|
+
account: options.account,
|
|
24364
|
+
name,
|
|
24365
|
+
listId: options.listId,
|
|
24366
|
+
includeEnded: Boolean(options.includeEnded),
|
|
24367
|
+
limit: Number(options.limit)
|
|
24368
|
+
});
|
|
24369
|
+
if (asJson()) {
|
|
24370
|
+
print2(result, true);
|
|
24371
|
+
return;
|
|
24372
|
+
}
|
|
24373
|
+
console.log(
|
|
24374
|
+
result.items.map(
|
|
24375
|
+
(item) => `@${item.profile.handle} ${item.externalUserId} ${item.current ? "current" : "ended"}`
|
|
24376
|
+
).join("\n")
|
|
24377
|
+
);
|
|
24378
|
+
});
|
|
24379
|
+
}
|
|
24380
|
+
|
|
23375
24381
|
// src/lib/profile-replies.ts
|
|
23376
|
-
import { Effect as
|
|
24382
|
+
import { Effect as Effect51 } from "effect";
|
|
23377
24383
|
|
|
23378
24384
|
// src/lib/moderation-target.ts
|
|
23379
|
-
import { Effect as
|
|
24385
|
+
import { Effect as Effect50 } from "effect";
|
|
23380
24386
|
|
|
23381
24387
|
// src/lib/bird-actions.ts
|
|
23382
|
-
import { Effect as
|
|
24388
|
+
import { Effect as Effect49 } from "effect";
|
|
23383
24389
|
|
|
23384
24390
|
// src/lib/bird-command.ts
|
|
23385
24391
|
import { access } from "node:fs/promises";
|
|
23386
24392
|
import { constants as constants2 } from "node:fs";
|
|
23387
|
-
import { Data as Data3, Effect as
|
|
24393
|
+
import { Data as Data3, Effect as Effect48 } from "effect";
|
|
23388
24394
|
var BirdCommandUnavailableError = class extends Data3.TaggedError(
|
|
23389
24395
|
"BirdCommandUnavailableError"
|
|
23390
24396
|
) {
|
|
@@ -23437,25 +24443,25 @@ function execFailureFromCause(command, cause) {
|
|
|
23437
24443
|
}
|
|
23438
24444
|
function assertBirdCommandAvailableEffect(command) {
|
|
23439
24445
|
if (!isPathCommand(command)) {
|
|
23440
|
-
return
|
|
24446
|
+
return Effect48.void;
|
|
23441
24447
|
}
|
|
23442
|
-
return
|
|
24448
|
+
return Effect48.tryPromise({
|
|
23443
24449
|
try: () => Promise.resolve(access(command, constants2.X_OK)),
|
|
23444
24450
|
catch: (cause) => new BirdCommandUnavailableError({
|
|
23445
24451
|
message: formatBirdInstallHint(command),
|
|
23446
24452
|
command,
|
|
23447
24453
|
cause
|
|
23448
24454
|
})
|
|
23449
|
-
}).pipe(
|
|
24455
|
+
}).pipe(Effect48.asVoid);
|
|
23450
24456
|
}
|
|
23451
24457
|
function getBirdCommandEffect() {
|
|
23452
|
-
return
|
|
24458
|
+
return Effect48.try({
|
|
23453
24459
|
try: () => getBirdCommand(),
|
|
23454
24460
|
catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
|
|
23455
24461
|
});
|
|
23456
24462
|
}
|
|
23457
24463
|
function runBirdCommandEffect(args, options) {
|
|
23458
|
-
return
|
|
24464
|
+
return Effect48.gen(function* () {
|
|
23459
24465
|
const birdCommand = yield* getBirdCommandEffect();
|
|
23460
24466
|
yield* assertBirdCommandAvailableEffect(birdCommand);
|
|
23461
24467
|
return yield* runSubprocessEffect({
|
|
@@ -23468,8 +24474,8 @@ function runBirdCommandEffect(args, options) {
|
|
|
23468
24474
|
...options?.signal ? { signal: options.signal } : {},
|
|
23469
24475
|
...options?.killSignal ? { killSignal: options.killSignal } : {}
|
|
23470
24476
|
}).pipe(
|
|
23471
|
-
|
|
23472
|
-
|
|
24477
|
+
Effect48.map(({ stdout, stderr }) => ({ stdout, stderr })),
|
|
24478
|
+
Effect48.mapError((cause) => execFailureFromCause(birdCommand, cause))
|
|
23473
24479
|
);
|
|
23474
24480
|
});
|
|
23475
24481
|
}
|
|
@@ -23507,9 +24513,9 @@ function toError18(error) {
|
|
|
23507
24513
|
return error instanceof Error ? error : new Error(String(error));
|
|
23508
24514
|
}
|
|
23509
24515
|
function runBirdJsonCommandEffect2(args) {
|
|
23510
|
-
return
|
|
24516
|
+
return Effect49.gen(function* () {
|
|
23511
24517
|
const { stdout } = yield* runBirdCommandEffect(args);
|
|
23512
|
-
return yield*
|
|
24518
|
+
return yield* Effect49.try({
|
|
23513
24519
|
try: () => JSON.parse(stripAnsi2(stdout)),
|
|
23514
24520
|
catch: toError18
|
|
23515
24521
|
});
|
|
@@ -23523,13 +24529,13 @@ function safeBirdProfileArg(query) {
|
|
|
23523
24529
|
throw new Error("Invalid bird profile query");
|
|
23524
24530
|
}
|
|
23525
24531
|
function readBirdStatusViaBirdEffect(query) {
|
|
23526
|
-
return
|
|
23527
|
-
const safeQuery = yield*
|
|
24532
|
+
return Effect49.gen(function* () {
|
|
24533
|
+
const safeQuery = yield* Effect49.try({
|
|
23528
24534
|
try: () => safeBirdProfileArg(query),
|
|
23529
24535
|
catch: toError18
|
|
23530
24536
|
});
|
|
23531
24537
|
return yield* runBirdJsonCommandEffect2(["status", safeQuery, "--json"]);
|
|
23532
|
-
}).pipe(
|
|
24538
|
+
}).pipe(Effect49.catchAll(() => Effect49.succeed(null)));
|
|
23533
24539
|
}
|
|
23534
24540
|
function readBirdStatusViaBird(query) {
|
|
23535
24541
|
return runEffectPromise(readBirdStatusViaBirdEffect(query));
|
|
@@ -23566,8 +24572,8 @@ function toBirdLookupUser(payload) {
|
|
|
23566
24572
|
};
|
|
23567
24573
|
}
|
|
23568
24574
|
function lookupProfileViaBirdEffect2(query) {
|
|
23569
|
-
return
|
|
23570
|
-
const safeQuery = yield*
|
|
24575
|
+
return Effect49.gen(function* () {
|
|
24576
|
+
const safeQuery = yield* Effect49.try({
|
|
23571
24577
|
try: () => safeBirdProfileArg(query),
|
|
23572
24578
|
catch: toError18
|
|
23573
24579
|
});
|
|
@@ -23578,11 +24584,11 @@ function lookupProfileViaBirdEffect2(query) {
|
|
|
23578
24584
|
"1",
|
|
23579
24585
|
"--json"
|
|
23580
24586
|
]);
|
|
23581
|
-
return yield*
|
|
24587
|
+
return yield* Effect49.try({
|
|
23582
24588
|
try: () => toBirdLookupUser(payload),
|
|
23583
24589
|
catch: toError18
|
|
23584
24590
|
});
|
|
23585
|
-
}).pipe(
|
|
24591
|
+
}).pipe(Effect49.catchAll(() => Effect49.succeed(null)));
|
|
23586
24592
|
}
|
|
23587
24593
|
function lookupProfileViaBird(query) {
|
|
23588
24594
|
return runEffectPromise(lookupProfileViaBirdEffect2(query));
|
|
@@ -23593,8 +24599,8 @@ function runVerifiedBirdMutationEffect({
|
|
|
23593
24599
|
verifyField,
|
|
23594
24600
|
expectedValue
|
|
23595
24601
|
}) {
|
|
23596
|
-
return
|
|
23597
|
-
const safeQuery = yield*
|
|
24602
|
+
return Effect49.gen(function* () {
|
|
24603
|
+
const safeQuery = yield* Effect49.try({
|
|
23598
24604
|
try: () => safeBirdProfileArg(query),
|
|
23599
24605
|
catch: toError18
|
|
23600
24606
|
});
|
|
@@ -23608,12 +24614,12 @@ function runVerifiedBirdMutationEffect({
|
|
|
23608
24614
|
action,
|
|
23609
24615
|
safeQuery
|
|
23610
24616
|
]).pipe(
|
|
23611
|
-
|
|
24617
|
+
Effect49.map(({ stdout, stderr }) => ({
|
|
23612
24618
|
ok: true,
|
|
23613
24619
|
output: normalizeOutput(stdout, stderr)
|
|
23614
24620
|
})),
|
|
23615
|
-
|
|
23616
|
-
(error) =>
|
|
24621
|
+
Effect49.catchAll(
|
|
24622
|
+
(error) => Effect49.succeed({
|
|
23617
24623
|
ok: false,
|
|
23618
24624
|
output: formatExecError2(error, `bird ${action} failed`)
|
|
23619
24625
|
})
|
|
@@ -23692,7 +24698,7 @@ function toError19(error) {
|
|
|
23692
24698
|
return error instanceof Error ? error : new Error(String(error));
|
|
23693
24699
|
}
|
|
23694
24700
|
function trySync14(try_) {
|
|
23695
|
-
return
|
|
24701
|
+
return Effect50.try({
|
|
23696
24702
|
try: try_,
|
|
23697
24703
|
catch: toError19
|
|
23698
24704
|
});
|
|
@@ -23740,11 +24746,11 @@ function resolveLocalProfile(db, normalizedQuery) {
|
|
|
23740
24746
|
};
|
|
23741
24747
|
}
|
|
23742
24748
|
function resolveProfileEffect(query, providedDb) {
|
|
23743
|
-
return
|
|
24749
|
+
return Effect50.gen(function* () {
|
|
23744
24750
|
const db = providedDb ?? (yield* trySync14(() => getNativeDb()));
|
|
23745
24751
|
const normalizedQuery = normalizeProfileQuery(query);
|
|
23746
24752
|
if (!normalizedQuery) {
|
|
23747
|
-
return yield*
|
|
24753
|
+
return yield* Effect50.fail(new Error("Missing profile handle or id"));
|
|
23748
24754
|
}
|
|
23749
24755
|
const local = yield* trySync14(
|
|
23750
24756
|
() => resolveLocalProfile(db, normalizedQuery)
|
|
@@ -23753,7 +24759,7 @@ function resolveProfileEffect(query, providedDb) {
|
|
|
23753
24759
|
if (local) {
|
|
23754
24760
|
return local;
|
|
23755
24761
|
}
|
|
23756
|
-
return yield*
|
|
24762
|
+
return yield* Effect50.fail(
|
|
23757
24763
|
new Error(`Profile not found locally: ${query}`)
|
|
23758
24764
|
);
|
|
23759
24765
|
}
|
|
@@ -23765,8 +24771,8 @@ function resolveProfileEffect(query, providedDb) {
|
|
|
23765
24771
|
const birdResult = yield* tryPromise(
|
|
23766
24772
|
() => lookupProfileViaBird(local?.profile.handle ?? normalizedQuery)
|
|
23767
24773
|
).pipe(
|
|
23768
|
-
|
|
23769
|
-
|
|
24774
|
+
Effect50.map((value) => ({ ok: true, value })),
|
|
24775
|
+
Effect50.catchAll((error) => Effect50.succeed({ ok: false, error }))
|
|
23770
24776
|
);
|
|
23771
24777
|
if (birdResult.ok) {
|
|
23772
24778
|
user = birdResult.value ?? void 0;
|
|
@@ -23777,9 +24783,9 @@ function resolveProfileEffect(query, providedDb) {
|
|
|
23777
24783
|
const xurlResult = yield* tryPromise(
|
|
23778
24784
|
() => /^\d+$/.test(normalizedQuery) ? lookupUsersByIds([normalizedQuery]) : lookupUsersByHandles([local?.profile.handle ?? normalizedQuery])
|
|
23779
24785
|
).pipe(
|
|
23780
|
-
|
|
23781
|
-
|
|
23782
|
-
(error) =>
|
|
24786
|
+
Effect50.map((value) => ({ ok: true, value })),
|
|
24787
|
+
Effect50.catchAll(
|
|
24788
|
+
(error) => Effect50.succeed({ ok: false, error })
|
|
23783
24789
|
)
|
|
23784
24790
|
);
|
|
23785
24791
|
if (xurlResult.ok) {
|
|
@@ -23792,13 +24798,13 @@ function resolveProfileEffect(query, providedDb) {
|
|
|
23792
24798
|
if (local) {
|
|
23793
24799
|
return local;
|
|
23794
24800
|
}
|
|
23795
|
-
return yield*
|
|
24801
|
+
return yield* Effect50.fail(lastError);
|
|
23796
24802
|
}
|
|
23797
24803
|
if (!user) {
|
|
23798
24804
|
if (local) {
|
|
23799
24805
|
return local;
|
|
23800
24806
|
}
|
|
23801
|
-
return yield*
|
|
24807
|
+
return yield* Effect50.fail(new Error(`Profile not found: ${query}`));
|
|
23802
24808
|
}
|
|
23803
24809
|
if (local) {
|
|
23804
24810
|
return yield* databaseWriteEffect(
|
|
@@ -23839,11 +24845,11 @@ function inspectProfileReplies(query, { limit = 12 } = {}) {
|
|
|
23839
24845
|
return runEffectPromise(inspectProfileRepliesEffect(query, { limit }));
|
|
23840
24846
|
}
|
|
23841
24847
|
function inspectProfileRepliesEffect(query, { limit = 12 } = {}) {
|
|
23842
|
-
return
|
|
24848
|
+
return Effect51.gen(function* () {
|
|
23843
24849
|
const resolved = yield* tryPromise(() => resolveProfile(query));
|
|
23844
24850
|
const externalUserId = resolved.externalUserId;
|
|
23845
24851
|
if (!externalUserId) {
|
|
23846
|
-
return yield*
|
|
24852
|
+
return yield* Effect51.fail(
|
|
23847
24853
|
new Error(`Profile has no external Twitter user id: ${query}`)
|
|
23848
24854
|
);
|
|
23849
24855
|
}
|
|
@@ -23938,18 +24944,18 @@ function registerMentionCommands({
|
|
|
23938
24944
|
}
|
|
23939
24945
|
|
|
23940
24946
|
// src/lib/blocks.ts
|
|
23941
|
-
import { Effect as
|
|
24947
|
+
import { Effect as Effect54 } from "effect";
|
|
23942
24948
|
|
|
23943
24949
|
// src/lib/moderation-state.ts
|
|
23944
|
-
import { Effect as
|
|
24950
|
+
import { Effect as Effect53 } from "effect";
|
|
23945
24951
|
|
|
23946
24952
|
// src/lib/actions-transport.ts
|
|
23947
|
-
import { Effect as
|
|
24953
|
+
import { Effect as Effect52 } from "effect";
|
|
23948
24954
|
function toError20(error) {
|
|
23949
24955
|
return error instanceof Error ? error : new Error(String(error));
|
|
23950
24956
|
}
|
|
23951
24957
|
function trySync15(try_) {
|
|
23952
|
-
return
|
|
24958
|
+
return Effect52.try({
|
|
23953
24959
|
try: try_,
|
|
23954
24960
|
catch: toError20
|
|
23955
24961
|
});
|
|
@@ -23961,7 +24967,7 @@ function liveWritesDisabled4() {
|
|
|
23961
24967
|
return process.env.BIRDCLAW_DISABLE_LIVE_WRITES === "1";
|
|
23962
24968
|
}
|
|
23963
24969
|
function verifyExpectedAccountEffect(expectedAccount) {
|
|
23964
|
-
return
|
|
24970
|
+
return Effect52.gen(function* () {
|
|
23965
24971
|
if (!expectedAccount) return null;
|
|
23966
24972
|
if (liveWritesDisabled4()) return null;
|
|
23967
24973
|
const sourceUser = yield* tryPromise(() => lookupAuthenticatedUserFresh());
|
|
@@ -23971,7 +24977,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
|
|
|
23971
24977
|
const expectedHandle = profileHandleKey(expectedAccount.handle);
|
|
23972
24978
|
if (expectedExternalUserId) {
|
|
23973
24979
|
if (sourceUserId === expectedExternalUserId) return sourceUserId;
|
|
23974
|
-
return yield*
|
|
24980
|
+
return yield* Effect52.fail(
|
|
23975
24981
|
new Error(
|
|
23976
24982
|
`xurl is authenticated as user ${sourceUserId || "unknown"}, not account ${expectedAccount.id}`
|
|
23977
24983
|
)
|
|
@@ -23979,7 +24985,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
|
|
|
23979
24985
|
}
|
|
23980
24986
|
if (expectedHandle && sourceUsername === expectedHandle)
|
|
23981
24987
|
return sourceUserId;
|
|
23982
|
-
return yield*
|
|
24988
|
+
return yield* Effect52.fail(
|
|
23983
24989
|
new Error(
|
|
23984
24990
|
sourceUsername ? `xurl is authenticated as @${sourceUsername}, not @${expectedHandle}` : "xurl authenticated user unavailable"
|
|
23985
24991
|
)
|
|
@@ -23987,7 +24993,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
|
|
|
23987
24993
|
});
|
|
23988
24994
|
}
|
|
23989
24995
|
function runBirdActionEffect(action, query) {
|
|
23990
|
-
return
|
|
24996
|
+
return Effect52.gen(function* () {
|
|
23991
24997
|
const result = yield* tryPromise(
|
|
23992
24998
|
() => action === "block" ? blockUserViaBird(query) : action === "unblock" ? unblockUserViaBird(query) : action === "mute" ? muteUserViaBird(query) : unmuteUserViaBird(query)
|
|
23993
24999
|
);
|
|
@@ -24007,7 +25013,7 @@ function getVerifyExpectation(action) {
|
|
|
24007
25013
|
};
|
|
24008
25014
|
}
|
|
24009
25015
|
function runXurlActionEffect(action, query, targetUserId, verifiedSourceUserId) {
|
|
24010
|
-
return
|
|
25016
|
+
return Effect52.gen(function* () {
|
|
24011
25017
|
if (!targetUserId) {
|
|
24012
25018
|
return {
|
|
24013
25019
|
ok: false,
|
|
@@ -24072,13 +25078,13 @@ function runModerationActionEffect({
|
|
|
24072
25078
|
transport,
|
|
24073
25079
|
expectedAccount
|
|
24074
25080
|
}) {
|
|
24075
|
-
return
|
|
25081
|
+
return Effect52.gen(function* () {
|
|
24076
25082
|
const requestedTransport = yield* trySync15(
|
|
24077
25083
|
() => resolveActionsTransport(transport)
|
|
24078
25084
|
);
|
|
24079
25085
|
const verifyXurlAccount = () => verifyExpectedAccountEffect(expectedAccount).pipe(
|
|
24080
|
-
|
|
24081
|
-
(error) =>
|
|
25086
|
+
Effect52.catchAll(
|
|
25087
|
+
(error) => Effect52.succeed({
|
|
24082
25088
|
ok: false,
|
|
24083
25089
|
output: error instanceof Error ? error.message : String(error),
|
|
24084
25090
|
transport: "xurl"
|
|
@@ -24167,7 +25173,7 @@ function toError21(error) {
|
|
|
24167
25173
|
return error instanceof Error ? error : new Error(String(error));
|
|
24168
25174
|
}
|
|
24169
25175
|
function trySync16(try_) {
|
|
24170
|
-
return
|
|
25176
|
+
return Effect53.try({
|
|
24171
25177
|
try: try_,
|
|
24172
25178
|
catch: toError21
|
|
24173
25179
|
});
|
|
@@ -24286,20 +25292,20 @@ function resolveModerationTargetEffect({
|
|
|
24286
25292
|
query,
|
|
24287
25293
|
selfActionError
|
|
24288
25294
|
}) {
|
|
24289
|
-
return
|
|
25295
|
+
return Effect53.gen(function* () {
|
|
24290
25296
|
const db = yield* trySync16(() => getNativeDb());
|
|
24291
25297
|
const resolvedAccountId = accountId || getDefaultAccountId(db);
|
|
24292
25298
|
const accountHandle = yield* trySync16(
|
|
24293
25299
|
() => getAccountHandle(db, resolvedAccountId)
|
|
24294
25300
|
);
|
|
24295
25301
|
if (!accountHandle) {
|
|
24296
|
-
return yield*
|
|
25302
|
+
return yield* Effect53.fail(
|
|
24297
25303
|
new Error(`Unknown account: ${resolvedAccountId}`)
|
|
24298
25304
|
);
|
|
24299
25305
|
}
|
|
24300
25306
|
const normalizedQuery = normalizeProfileQuery(query);
|
|
24301
25307
|
if (normalizedQuery.toLowerCase() === accountHandle.toLowerCase()) {
|
|
24302
|
-
return yield*
|
|
25308
|
+
return yield* Effect53.fail(new Error(selfActionError));
|
|
24303
25309
|
}
|
|
24304
25310
|
const resolved = yield* resolveProfileEffect(query, db);
|
|
24305
25311
|
const account = yield* trySync16(
|
|
@@ -24310,7 +25316,7 @@ function resolveModerationTargetEffect({
|
|
|
24310
25316
|
);
|
|
24311
25317
|
const accountExternalUserId = account?.external_user_id ?? getExternalUserId(accountProfile?.id ?? "");
|
|
24312
25318
|
if (profileHandleKey(resolved.profile.handle) === profileHandleKey(accountHandle) || accountProfile?.id && resolved.profile.id === accountProfile.id || accountExternalUserId && resolved.externalUserId === accountExternalUserId) {
|
|
24313
|
-
return yield*
|
|
25319
|
+
return yield* Effect53.fail(new Error(selfActionError));
|
|
24314
25320
|
}
|
|
24315
25321
|
return {
|
|
24316
25322
|
db,
|
|
@@ -24327,7 +25333,7 @@ function resolveModerationTargetEffect({
|
|
|
24327
25333
|
}
|
|
24328
25334
|
function addModerationEffect(kind, accountId, query, options = {}) {
|
|
24329
25335
|
const descriptor = moderationDescriptors[kind];
|
|
24330
|
-
return
|
|
25336
|
+
return Effect53.gen(function* () {
|
|
24331
25337
|
const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } = yield* resolveModerationTargetEffect({
|
|
24332
25338
|
accountId,
|
|
24333
25339
|
query,
|
|
@@ -24377,7 +25383,7 @@ function addModeration(kind, accountId, query, options = {}) {
|
|
|
24377
25383
|
}
|
|
24378
25384
|
function recordModerationEffect(kind, accountId, query) {
|
|
24379
25385
|
const descriptor = moderationDescriptors[kind];
|
|
24380
|
-
return
|
|
25386
|
+
return Effect53.gen(function* () {
|
|
24381
25387
|
const { db, resolved, resolvedAccountId } = yield* resolveModerationTargetEffect({
|
|
24382
25388
|
accountId,
|
|
24383
25389
|
query,
|
|
@@ -24408,7 +25414,7 @@ function recordModeration(kind, accountId, query) {
|
|
|
24408
25414
|
}
|
|
24409
25415
|
function removeModerationEffect(kind, accountId, query, options = {}) {
|
|
24410
25416
|
const descriptor = moderationDescriptors[kind];
|
|
24411
|
-
return
|
|
25417
|
+
return Effect53.gen(function* () {
|
|
24412
25418
|
const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } = yield* resolveModerationTargetEffect({
|
|
24413
25419
|
accountId,
|
|
24414
25420
|
query,
|
|
@@ -24484,7 +25490,7 @@ function toError22(error) {
|
|
|
24484
25490
|
return error instanceof Error ? error : new Error(String(error));
|
|
24485
25491
|
}
|
|
24486
25492
|
function trySync17(try_) {
|
|
24487
|
-
return
|
|
25493
|
+
return Effect54.try({
|
|
24488
25494
|
try: try_,
|
|
24489
25495
|
catch: toError22
|
|
24490
25496
|
});
|
|
@@ -24497,12 +25503,12 @@ function listBlocks({
|
|
|
24497
25503
|
return listModerationState("block", { account, search, limit });
|
|
24498
25504
|
}
|
|
24499
25505
|
function syncBlocksEffect(accountId) {
|
|
24500
|
-
return
|
|
25506
|
+
return Effect54.gen(function* () {
|
|
24501
25507
|
const db = yield* trySync17(() => getNativeDb());
|
|
24502
25508
|
const resolvedAccountId = accountId || getDefaultAccountId(db);
|
|
24503
25509
|
const accountHandle = getAccountHandle(db, resolvedAccountId);
|
|
24504
25510
|
if (!accountHandle) {
|
|
24505
|
-
return yield*
|
|
25511
|
+
return yield* Effect54.fail(
|
|
24506
25512
|
new Error(`Unknown account: ${resolvedAccountId}`)
|
|
24507
25513
|
);
|
|
24508
25514
|
}
|
|
@@ -24523,9 +25529,9 @@ function syncBlocksEffect(accountId) {
|
|
|
24523
25529
|
}
|
|
24524
25530
|
};
|
|
24525
25531
|
}
|
|
24526
|
-
return yield*
|
|
25532
|
+
return yield* Effect54.gen(function* () {
|
|
24527
25533
|
const me = yield* tryPromise(() => lookupAuthenticatedUserFresh()).pipe(
|
|
24528
|
-
|
|
25534
|
+
Effect54.mapError(toError22)
|
|
24529
25535
|
);
|
|
24530
25536
|
const sourceUserId = typeof me?.id === "string" && me.id.length > 0 ? me.id : null;
|
|
24531
25537
|
const sourceUsername = typeof me?.username === "string" ? me.username.replace(/^@/, "") : "";
|
|
@@ -24572,7 +25578,7 @@ function syncBlocksEffect(accountId) {
|
|
|
24572
25578
|
do {
|
|
24573
25579
|
const page = yield* tryPromise(
|
|
24574
25580
|
() => listBlockedUsers(sourceUserId, nextToken ?? void 0)
|
|
24575
|
-
).pipe(
|
|
25581
|
+
).pipe(Effect54.mapError(toError22));
|
|
24576
25582
|
const pageProfileIds = yield* databaseWriteEffect((writeDb) => {
|
|
24577
25583
|
return page.items.map((user) => {
|
|
24578
25584
|
const resolved = upsertProfileFromXUser(writeDb, user);
|
|
@@ -24614,8 +25620,8 @@ function syncBlocksEffect(accountId) {
|
|
|
24614
25620
|
}
|
|
24615
25621
|
};
|
|
24616
25622
|
}).pipe(
|
|
24617
|
-
|
|
24618
|
-
(error) =>
|
|
25623
|
+
Effect54.catchAll(
|
|
25624
|
+
(error) => Effect54.succeed({
|
|
24619
25625
|
ok: false,
|
|
24620
25626
|
accountId: resolvedAccountId,
|
|
24621
25627
|
synced: remoteProfileIds.length > 0,
|
|
@@ -24766,7 +25772,7 @@ function registerModerationCommands({
|
|
|
24766
25772
|
|
|
24767
25773
|
// src/lib/blocklist.ts
|
|
24768
25774
|
import { readFile } from "node:fs/promises";
|
|
24769
|
-
import { Effect as
|
|
25775
|
+
import { Effect as Effect55 } from "effect";
|
|
24770
25776
|
function stripInlineCode(value) {
|
|
24771
25777
|
const match = value.match(/`([^`]+)`/);
|
|
24772
25778
|
return match?.[1] ?? value;
|
|
@@ -24803,15 +25809,15 @@ function parseBlocklistText(text) {
|
|
|
24803
25809
|
return items;
|
|
24804
25810
|
}
|
|
24805
25811
|
function importBlocklistEffect(accountId, filePath) {
|
|
24806
|
-
return
|
|
25812
|
+
return Effect55.gen(function* () {
|
|
24807
25813
|
const text = yield* tryPromise(() => readFile(filePath, "utf8"));
|
|
24808
25814
|
const queries = parseBlocklistText(text);
|
|
24809
25815
|
const items = [];
|
|
24810
25816
|
for (const query of queries) {
|
|
24811
25817
|
const blocked = yield* tryPromise(() => addBlock(accountId, query)).pipe(
|
|
24812
|
-
|
|
24813
|
-
|
|
24814
|
-
(error) =>
|
|
25818
|
+
Effect55.map((result2) => ({ ok: true, result: result2 })),
|
|
25819
|
+
Effect55.catchAll(
|
|
25820
|
+
(error) => Effect55.succeed({ ok: false, error })
|
|
24815
25821
|
)
|
|
24816
25822
|
);
|
|
24817
25823
|
if (!blocked.ok) {
|
|
@@ -24867,7 +25873,7 @@ function registerModerationCommands2(context) {
|
|
|
24867
25873
|
}
|
|
24868
25874
|
|
|
24869
25875
|
// src/lib/link-index.ts
|
|
24870
|
-
import { Effect as
|
|
25876
|
+
import { Effect as Effect56 } from "effect";
|
|
24871
25877
|
var DEFAULT_EXPAND_CONCURRENCY = 12;
|
|
24872
25878
|
function parseJsonField5(value, fallback) {
|
|
24873
25879
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -25029,7 +26035,7 @@ function expandWithConcurrencyEffect(db, urls, options) {
|
|
|
25029
26035
|
misses: 0,
|
|
25030
26036
|
errors: 0
|
|
25031
26037
|
};
|
|
25032
|
-
return
|
|
26038
|
+
return Effect56.forEach(
|
|
25033
26039
|
urls,
|
|
25034
26040
|
(url) => tryPromise(
|
|
25035
26041
|
() => expandUrls([url], {
|
|
@@ -25038,7 +26044,7 @@ function expandWithConcurrencyEffect(db, urls, options) {
|
|
|
25038
26044
|
timeoutMs: options.timeoutMs
|
|
25039
26045
|
})
|
|
25040
26046
|
).pipe(
|
|
25041
|
-
|
|
26047
|
+
Effect56.map((results) => {
|
|
25042
26048
|
const result = results[0];
|
|
25043
26049
|
if (result.source === "network") {
|
|
25044
26050
|
counts.networkExpansions++;
|
|
@@ -25055,10 +26061,10 @@ function expandWithConcurrencyEffect(db, urls, options) {
|
|
|
25055
26061
|
})
|
|
25056
26062
|
),
|
|
25057
26063
|
{ concurrency }
|
|
25058
|
-
).pipe(
|
|
26064
|
+
).pipe(Effect56.as(counts));
|
|
25059
26065
|
}
|
|
25060
26066
|
function backfillLinkIndexEffect(options = {}) {
|
|
25061
|
-
return
|
|
26067
|
+
return Effect56.gen(function* () {
|
|
25062
26068
|
const db = getNativeDb({ seedDemoData: false });
|
|
25063
26069
|
const { occurrences, entityExpansions } = rebuildOccurrences(
|
|
25064
26070
|
db,
|
|
@@ -25353,7 +26359,7 @@ import {
|
|
|
25353
26359
|
} from "node:fs";
|
|
25354
26360
|
import { appendFile, copyFile, rename, rm, writeFile } from "node:fs/promises";
|
|
25355
26361
|
import path11 from "node:path";
|
|
25356
|
-
import { Effect as
|
|
26362
|
+
import { Effect as Effect57 } from "effect";
|
|
25357
26363
|
var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
|
|
25358
26364
|
var PBS_PREFIXES = [
|
|
25359
26365
|
"/media/",
|
|
@@ -25372,13 +26378,13 @@ function toMediaError(error) {
|
|
|
25372
26378
|
return error instanceof Error ? error : new Error(String(error));
|
|
25373
26379
|
}
|
|
25374
26380
|
function tryMediaSync(try_) {
|
|
25375
|
-
return
|
|
26381
|
+
return Effect57.try({
|
|
25376
26382
|
try: try_,
|
|
25377
26383
|
catch: toMediaError
|
|
25378
26384
|
});
|
|
25379
26385
|
}
|
|
25380
26386
|
function tryMediaPromise(try_) {
|
|
25381
|
-
return
|
|
26387
|
+
return Effect57.tryPromise({
|
|
25382
26388
|
try: try_,
|
|
25383
26389
|
catch: toMediaError
|
|
25384
26390
|
});
|
|
@@ -25643,7 +26649,7 @@ function archivePathFor(item, mediaOriginalsDir) {
|
|
|
25643
26649
|
return archiveTweetDirs(mediaOriginalsDir, item.tweetId).map((tweetDir) => path11.join(tweetDir, fileName)).find((archivePath) => existsSync5(archivePath)) ?? null;
|
|
25644
26650
|
}
|
|
25645
26651
|
function reuseFromArchiveEffect(item, mediaOriginalsDir, maxBytes) {
|
|
25646
|
-
return
|
|
26652
|
+
return Effect57.gen(function* () {
|
|
25647
26653
|
const archivePath = archivePathFor(item, mediaOriginalsDir);
|
|
25648
26654
|
if (!archivePath || !existsSync5(archivePath)) return null;
|
|
25649
26655
|
const bytes = fileSize(archivePath);
|
|
@@ -25670,15 +26676,15 @@ function contentRangeTotal(response) {
|
|
|
25670
26676
|
return total ? Number(total) : null;
|
|
25671
26677
|
}
|
|
25672
26678
|
function writeResponseBodyEffect(response, tmpPath, append, maxBytes, initialBytes) {
|
|
25673
|
-
return
|
|
26679
|
+
return Effect57.gen(function* () {
|
|
25674
26680
|
if (!response.body)
|
|
25675
|
-
return yield*
|
|
26681
|
+
return yield* Effect57.fail(new Error("missing response body"));
|
|
25676
26682
|
let bytes = 0;
|
|
25677
26683
|
if (!append) {
|
|
25678
26684
|
yield* tryMediaPromise(() => writeFile(tmpPath, Buffer.alloc(0)));
|
|
25679
26685
|
}
|
|
25680
26686
|
const reader = response.body.getReader();
|
|
25681
|
-
const result = yield*
|
|
26687
|
+
const result = yield* Effect57.gen(function* () {
|
|
25682
26688
|
for (; ; ) {
|
|
25683
26689
|
const { done, value } = yield* tryMediaPromise(() => reader.read());
|
|
25684
26690
|
if (done) {
|
|
@@ -25688,31 +26694,31 @@ function writeResponseBodyEffect(response, tmpPath, append, maxBytes, initialByt
|
|
|
25688
26694
|
bytes += chunk.length;
|
|
25689
26695
|
if (initialBytes + bytes > maxBytes) {
|
|
25690
26696
|
yield* tryMediaPromise(() => rm(tmpPath, { force: true })).pipe(
|
|
25691
|
-
|
|
26697
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25692
26698
|
);
|
|
25693
|
-
return yield*
|
|
26699
|
+
return yield* Effect57.fail(new Error("max-bytes"));
|
|
25694
26700
|
}
|
|
25695
26701
|
yield* tryMediaPromise(() => appendFile(tmpPath, chunk));
|
|
25696
26702
|
}
|
|
25697
26703
|
return bytes;
|
|
25698
26704
|
}).pipe(
|
|
25699
|
-
|
|
25700
|
-
|
|
25701
|
-
(error) =>
|
|
26705
|
+
Effect57.map((writtenBytes) => ({ ok: true, writtenBytes })),
|
|
26706
|
+
Effect57.catchAll(
|
|
26707
|
+
(error) => Effect57.gen(function* () {
|
|
25702
26708
|
if (error.message === "max-bytes") {
|
|
25703
26709
|
yield* tryMediaPromise(() => rm(tmpPath, { force: true })).pipe(
|
|
25704
|
-
|
|
26710
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25705
26711
|
);
|
|
25706
26712
|
}
|
|
25707
26713
|
yield* tryMediaPromise(() => reader.cancel(error)).pipe(
|
|
25708
|
-
|
|
26714
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25709
26715
|
);
|
|
25710
26716
|
return { error, ok: false };
|
|
25711
26717
|
})
|
|
25712
26718
|
),
|
|
25713
|
-
|
|
26719
|
+
Effect57.ensuring(Effect57.sync(() => reader.releaseLock()))
|
|
25714
26720
|
);
|
|
25715
|
-
if (!result.ok) return yield*
|
|
26721
|
+
if (!result.ok) return yield* Effect57.fail(result.error);
|
|
25716
26722
|
return result.writtenBytes;
|
|
25717
26723
|
});
|
|
25718
26724
|
}
|
|
@@ -25724,13 +26730,13 @@ function fetchOneEffect({
|
|
|
25724
26730
|
userAgent,
|
|
25725
26731
|
maxBytes
|
|
25726
26732
|
}) {
|
|
25727
|
-
return
|
|
26733
|
+
return Effect57.gen(function* () {
|
|
25728
26734
|
let rateLimited = false;
|
|
25729
26735
|
for (let attempt = 0; attempt <= retryMax; attempt += 1) {
|
|
25730
26736
|
const partialBytes = item.kind === "image" ? 0 : fileSize(item.tmpPath);
|
|
25731
26737
|
if (partialBytes > maxBytes) {
|
|
25732
26738
|
yield* tryMediaPromise(() => rm(item.tmpPath, { force: true })).pipe(
|
|
25733
|
-
|
|
26739
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25734
26740
|
);
|
|
25735
26741
|
return fail(item, "max-bytes");
|
|
25736
26742
|
}
|
|
@@ -25742,9 +26748,9 @@ function fetchOneEffect({
|
|
|
25742
26748
|
}
|
|
25743
26749
|
})
|
|
25744
26750
|
).pipe(
|
|
25745
|
-
|
|
25746
|
-
|
|
25747
|
-
(error) =>
|
|
26751
|
+
Effect57.map((response2) => ({ ok: true, response: response2 })),
|
|
26752
|
+
Effect57.catchAll(
|
|
26753
|
+
(error) => Effect57.succeed({ error, ok: false })
|
|
25748
26754
|
)
|
|
25749
26755
|
);
|
|
25750
26756
|
if (!responseResult.ok) {
|
|
@@ -25755,7 +26761,7 @@ function fetchOneEffect({
|
|
|
25755
26761
|
rateLimited = true;
|
|
25756
26762
|
if (attempt < retryMax) {
|
|
25757
26763
|
yield* tryMediaPromise(() => sleep(1e3 * 2 ** attempt)).pipe(
|
|
25758
|
-
|
|
26764
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25759
26765
|
);
|
|
25760
26766
|
continue;
|
|
25761
26767
|
}
|
|
@@ -25767,7 +26773,7 @@ function fetchOneEffect({
|
|
|
25767
26773
|
const expectedTotal = contentRangeTotal(response) ?? (contentLength(response) ?? 0) + (response.status === 206 ? partialBytes : 0);
|
|
25768
26774
|
if (expectedTotal > maxBytes) {
|
|
25769
26775
|
yield* tryMediaPromise(() => rm(item.tmpPath, { force: true })).pipe(
|
|
25770
|
-
|
|
26776
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25771
26777
|
);
|
|
25772
26778
|
return fail(item, "max-bytes");
|
|
25773
26779
|
}
|
|
@@ -25779,9 +26785,9 @@ function fetchOneEffect({
|
|
|
25779
26785
|
maxBytes,
|
|
25780
26786
|
append ? partialBytes : 0
|
|
25781
26787
|
).pipe(
|
|
25782
|
-
|
|
25783
|
-
|
|
25784
|
-
(error) =>
|
|
26788
|
+
Effect57.map((bytes) => ({ bytes, ok: true })),
|
|
26789
|
+
Effect57.catchAll(
|
|
26790
|
+
(error) => Effect57.succeed({ error, ok: false })
|
|
25785
26791
|
)
|
|
25786
26792
|
);
|
|
25787
26793
|
if (!bytesResult.ok) {
|
|
@@ -25855,7 +26861,7 @@ function runGroupEffect(items, parallel, pacingMs, now2, sleep, worker) {
|
|
|
25855
26861
|
);
|
|
25856
26862
|
})
|
|
25857
26863
|
);
|
|
25858
|
-
return
|
|
26864
|
+
return Effect57.forEach(pacedItems, runPaced, {
|
|
25859
26865
|
concurrency: Math.min(parallel, items.length),
|
|
25860
26866
|
discard: true
|
|
25861
26867
|
});
|
|
@@ -25864,7 +26870,7 @@ function fetchTweetMedia(options = {}) {
|
|
|
25864
26870
|
return runEffectPromise(fetchTweetMediaEffect(options));
|
|
25865
26871
|
}
|
|
25866
26872
|
function fetchTweetMediaEffect(options = {}) {
|
|
25867
|
-
return
|
|
26873
|
+
return Effect57.gen(function* () {
|
|
25868
26874
|
const now2 = options.now ?? Date.now;
|
|
25869
26875
|
const startedAt = now2();
|
|
25870
26876
|
const sleep = options.sleep ?? defaultSleep;
|
|
@@ -25931,7 +26937,7 @@ function fetchTweetMediaEffect(options = {}) {
|
|
|
25931
26937
|
retryMax,
|
|
25932
26938
|
userAgent,
|
|
25933
26939
|
maxBytes
|
|
25934
|
-
}).pipe(
|
|
26940
|
+
}).pipe(Effect57.map((fetched) => applyFetched(result, fetched)))
|
|
25935
26941
|
);
|
|
25936
26942
|
yield* runGroupEffect(
|
|
25937
26943
|
httpCandidates.filter((item) => item.kind === "image"),
|
|
@@ -25979,9 +26985,9 @@ function formatMediaFetchResult(result) {
|
|
|
25979
26985
|
}
|
|
25980
26986
|
|
|
25981
26987
|
// src/lib/whois.ts
|
|
25982
|
-
import { Effect as
|
|
26988
|
+
import { Effect as Effect58 } from "effect";
|
|
25983
26989
|
function trySync18(try_) {
|
|
25984
|
-
return
|
|
26990
|
+
return Effect58.try({
|
|
25985
26991
|
try: try_,
|
|
25986
26992
|
catch: (cause) => cause
|
|
25987
26993
|
});
|
|
@@ -26512,7 +27518,7 @@ function loadWhoisConversations(query, options, includeDms, context, limit) {
|
|
|
26512
27518
|
return [...merged.values()];
|
|
26513
27519
|
}
|
|
26514
27520
|
function runWhoisEffect(query, options = {}) {
|
|
26515
|
-
return
|
|
27521
|
+
return Effect58.gen(function* () {
|
|
26516
27522
|
const includeDms = options.dms ?? true;
|
|
26517
27523
|
const includeTweets = options.tweets ?? false;
|
|
26518
27524
|
const limit = options.limit ?? 10;
|
|
@@ -26759,7 +27765,7 @@ function registerSearchCommands({
|
|
|
26759
27765
|
parsePositiveIntegerOption: parsePositiveIntegerOption2
|
|
26760
27766
|
}) {
|
|
26761
27767
|
const searchCommand = program2.command("search").description("Search local data");
|
|
26762
|
-
searchCommand.command("tweets [query]").option("--resource <resource>", "home, mentions, or authored", "home").option("--replied", "Only replied items").option("--unreplied", "Only unreplied items").option("--since <date>", "Include tweets created at or after this date").option("--until <date>", "Include tweets created before this date").option("--originals-only", "Exclude authored replies that start with @").option("--hide-low-quality", "Hide RTs, tiny replies, and link-only noise").option(
|
|
27768
|
+
searchCommand.command("tweets [query]").option("--resource <resource>", "home, mentions, or authored", "home").option("--account <accountId>", "Account id").option("--list <name>", "Only authors in a cached X List").option("--list-id <id>", "Only authors in a cached X List id").option("--replied", "Only replied items").option("--unreplied", "Only unreplied items").option("--since <date>", "Include tweets created at or after this date").option("--until <date>", "Include tweets created before this date").option("--originals-only", "Exclude authored replies that start with @").option("--hide-low-quality", "Hide RTs, tiny replies, and link-only noise").option(
|
|
26763
27769
|
"--min-likes <n>",
|
|
26764
27770
|
"Override the low-quality like threshold (default 50)"
|
|
26765
27771
|
).option("--quality-reason", "Include qualityReason on each row").option("--liked", "Only liked tweets").option("--bookmarked", "Only bookmarked tweets").option("--limit <n>", "Limit results", "20").action(async (query, options) => {
|
|
@@ -26769,10 +27775,18 @@ function registerSearchCommands({
|
|
|
26769
27775
|
);
|
|
26770
27776
|
if (options.minLikes !== void 0 && minLikes === void 0) return;
|
|
26771
27777
|
await autoUpdateBeforeRead2();
|
|
27778
|
+
const selectedList = options.list || options.listId ? resolveStoredXListSelector({
|
|
27779
|
+
account: options.account,
|
|
27780
|
+
name: options.list,
|
|
27781
|
+
listId: options.listId
|
|
27782
|
+
}) : void 0;
|
|
26772
27783
|
const replyFilter = options.replied ? "replied" : options.unreplied ? "unreplied" : "all";
|
|
26773
27784
|
print2(
|
|
26774
27785
|
listTimelineItems({
|
|
26775
27786
|
resource: options.resource === "mentions" ? "mentions" : options.resource === "authored" ? "authored" : "home",
|
|
27787
|
+
account: options.account,
|
|
27788
|
+
listAccountId: selectedList?.accountId,
|
|
27789
|
+
listId: selectedList?.listId,
|
|
26776
27790
|
search: query,
|
|
26777
27791
|
replyFilter,
|
|
26778
27792
|
since: options.since,
|
|
@@ -26955,7 +27969,7 @@ import { Readable as Readable2 } from "node:stream";
|
|
|
26955
27969
|
import { pathToFileURL } from "node:url";
|
|
26956
27970
|
|
|
26957
27971
|
// src/lib/http-effect.ts
|
|
26958
|
-
import { Effect as
|
|
27972
|
+
import { Effect as Effect59 } from "effect";
|
|
26959
27973
|
var LOCAL_WEB_PEER_HEADER = "x-birdclaw-local-peer";
|
|
26960
27974
|
|
|
26961
27975
|
// src/lib/production-server.ts
|
|
@@ -27229,7 +28243,7 @@ function registerStorageCommands({
|
|
|
27229
28243
|
}
|
|
27230
28244
|
|
|
27231
28245
|
// src/lib/authored-live.ts
|
|
27232
|
-
import { Effect as
|
|
28246
|
+
import { Effect as Effect60 } from "effect";
|
|
27233
28247
|
var AuthoredSyncError = class extends Error {
|
|
27234
28248
|
constructor(message, exitCode) {
|
|
27235
28249
|
super(message);
|
|
@@ -27471,10 +28485,10 @@ function resolveAuthoredIdentityEffect({
|
|
|
27471
28485
|
account,
|
|
27472
28486
|
db
|
|
27473
28487
|
}) {
|
|
27474
|
-
return
|
|
28488
|
+
return Effect60.gen(function* () {
|
|
27475
28489
|
const status = yield* liveTransportGateway.xurl.getTransportStatus();
|
|
27476
28490
|
if (status.availableTransport !== "xurl") {
|
|
27477
|
-
return yield*
|
|
28491
|
+
return yield* Effect60.fail(new AuthoredSyncError(status.statusText, 4));
|
|
27478
28492
|
}
|
|
27479
28493
|
const resolvedAccount = yield* trySync(
|
|
27480
28494
|
() => resolveLiveSyncAccount(db, account)
|
|
@@ -27489,7 +28503,7 @@ function resolveAuthoredIdentityEffect({
|
|
|
27489
28503
|
const authenticated = yield* liveTransportGateway.xurl.lookupAuthenticatedUser();
|
|
27490
28504
|
const authenticatedUser = userFromAuthenticatedPayload(authenticated);
|
|
27491
28505
|
if (!authenticatedUser?.id) {
|
|
27492
|
-
return yield*
|
|
28506
|
+
return yield* Effect60.fail(
|
|
27493
28507
|
new AuthoredSyncError(
|
|
27494
28508
|
"Could not resolve authenticated Twitter user id",
|
|
27495
28509
|
4
|
|
@@ -27497,7 +28511,7 @@ function resolveAuthoredIdentityEffect({
|
|
|
27497
28511
|
);
|
|
27498
28512
|
}
|
|
27499
28513
|
if (normalizeUsername(authenticatedUser.username) !== normalizeUsername(resolvedAccount.username)) {
|
|
27500
|
-
return yield*
|
|
28514
|
+
return yield* Effect60.fail(
|
|
27501
28515
|
new AuthoredSyncError(
|
|
27502
28516
|
`xurl is authenticated as @${authenticatedUser.username}, but selected account ${resolvedAccount.accountId} is @${resolvedAccount.username}. Link the account external_user_id or switch xurl login before syncing authored tweets.`,
|
|
27503
28517
|
4
|
|
@@ -27796,9 +28810,9 @@ function syncAuthoredTweetsEffect({
|
|
|
27796
28810
|
sinceId,
|
|
27797
28811
|
untilId
|
|
27798
28812
|
}) {
|
|
27799
|
-
return
|
|
28813
|
+
return Effect60.gen(function* () {
|
|
27800
28814
|
if (mode !== "xurl") {
|
|
27801
|
-
return yield*
|
|
28815
|
+
return yield* Effect60.fail(
|
|
27802
28816
|
new Error("authored sync only supports --mode xurl")
|
|
27803
28817
|
);
|
|
27804
28818
|
}
|
|
@@ -27861,7 +28875,7 @@ function syncAuthoredTweetsEffect({
|
|
|
27861
28875
|
sourceUser
|
|
27862
28876
|
})
|
|
27863
28877
|
).pipe(
|
|
27864
|
-
|
|
28878
|
+
Effect60.flatMap(
|
|
27865
28879
|
() => trySync(() => {
|
|
27866
28880
|
newestSeenId = maxTweetId(
|
|
27867
28881
|
newestSeenId,
|
|
@@ -27964,6 +28978,32 @@ function registerSyncCommands({
|
|
|
27964
28978
|
autoSyncAfterWrite: autoSyncAfterWrite2
|
|
27965
28979
|
}) {
|
|
27966
28980
|
const syncCommand = program2.command("sync").description("Refresh live Twitter collections into the local store");
|
|
28981
|
+
syncCommand.command("lists").description("Sync owned X Lists and rate-limited membership pages").option("--account <accountId>", "Account id").option("--mode <mode>", "auto, bird, or xurl", "auto").option("--max-lists <n>", "Maximum owned Lists to inspect", "20").option("--member-limit <n>", "Members per transport page", "20").option("--max-member-pages <n>", "Maximum membership pages per List", "1").option("--delay-ms <n>", "Delay between Lists", "1000").action(async (options) => {
|
|
28982
|
+
try {
|
|
28983
|
+
const result = await syncXLists({
|
|
28984
|
+
account: options.account,
|
|
28985
|
+
mode: options.mode,
|
|
28986
|
+
maxLists: Number(options.maxLists),
|
|
28987
|
+
memberLimit: Number(options.memberLimit),
|
|
28988
|
+
maxMemberPages: Number(options.maxMemberPages),
|
|
28989
|
+
delayMs: Number(options.delayMs)
|
|
28990
|
+
});
|
|
28991
|
+
await autoSyncAfterWrite2();
|
|
28992
|
+
print2(result, true);
|
|
28993
|
+
if (!result.ok) process.exitCode = 5;
|
|
28994
|
+
} catch (error) {
|
|
28995
|
+
print2(
|
|
28996
|
+
{
|
|
28997
|
+
ok: false,
|
|
28998
|
+
kind: "lists",
|
|
28999
|
+
mode: options.mode,
|
|
29000
|
+
error: errorMessage(error)
|
|
29001
|
+
},
|
|
29002
|
+
true
|
|
29003
|
+
);
|
|
29004
|
+
process.exitCode = 1;
|
|
29005
|
+
}
|
|
29006
|
+
});
|
|
27967
29007
|
syncCommand.command("timeline").description("Refresh live home timeline through xurl or bird").option("--account <accountId>", "Account id").option("--mode <mode>", "auto, xurl, or bird", "auto").option("--limit <n>", "Result limit", "100").option("--max-pages <n>", "Stop after N xurl pages", "3").option("--for-you", 'Fetch "For You" instead of chronological Following').option("--cache-ttl <seconds>", "Live-cache freshness window", "120").option("--refresh", "Bypass live-cache freshness window").action(async (options) => {
|
|
27968
29008
|
const result = await syncHomeTimeline({
|
|
27969
29009
|
account: options.account,
|
|
@@ -28138,6 +29178,7 @@ registerMentionCommands(commandContext);
|
|
|
28138
29178
|
registerDirectMessageCommands(commandContext);
|
|
28139
29179
|
registerSyncCommands(commandContext);
|
|
28140
29180
|
registerJobCommands(commandContext);
|
|
29181
|
+
registerListCommands(commandContext);
|
|
28141
29182
|
registerModerationCommands2(commandContext);
|
|
28142
29183
|
registerComposeCommands(commandContext);
|
|
28143
29184
|
registerInboxCommand(commandContext);
|