birdclaw 0.8.5 → 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 +24 -0
- package/README.md +22 -2
- package/dist/cli/birdclaw.js +1228 -186
- 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-LbYOsoKf.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-DYNcLcll.js → mapbox-gl-WUhsQcSS.js} +1 -1
- package/dist/client/assets/{mentions-BoVVAIT7.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-CiLuYpZr.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-L7bs6IHH.js → ProfileAnalysisStream-CLjrYzEx.js} +2 -2
- package/dist/server/assets/{ProfilePreview-Cg-AOHBE.js → ProfilePreview-Bq-DSUM2.js} +2 -2
- package/dist/server/assets/{SavedTimelineView-BtCq1dHk.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-CjMIh63G.js → TimelineRouteFrame-Bg3e7X_N.js} +5 -3
- package/dist/server/assets/{TweetRichText-DDywESRF.js → TweetRichText-wzUAtbPS.js} +2 -2
- package/dist/server/assets/{_tanstack-start-manifest_v-aLWUdbAc.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-D_YkmCK3.js → bookmarks-CPFq3JVX.js} +1 -1
- package/dist/server/assets/{client-stream-contracts-Bq6kMmhh.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-DAq6ehL9.js → discuss-BB8wFy4L.js} +3 -3
- package/dist/server/assets/{discuss-C1QMBBSk.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-BMm2JWYI.js → likes-CDAjYyTm.js} +1 -1
- package/dist/server/assets/{links-dqXb35YM.js → links-DU-TIHtg.js} +6 -6
- package/dist/server/assets/{links-Da4tjWtI.js → links-sQ3q2jvO.js} +6 -6
- package/dist/server/assets/{mentions-CEyEcOFw.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-DH1TSBkT.js → profile-analyze-BmDeTif5.js} +1 -1
- package/dist/server/assets/{profile-analyze-CGCj4VFd.js → profile-analyze-CUfXmXEH.js} +3 -3
- package/dist/server/assets/{profiles._handle-B5V-huoo.js → profiles._handle-8snagiR1.js} +4 -4
- package/dist/server/assets/{profiles._handle-CJ6rgzt8.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-C4cz9ypd.js → router-DP7iZD-q.js} +421 -125
- package/dist/server/assets/{routes-B9aikOdB.js → routes-D9226vKz.js} +1 -1
- package/dist/server/assets/start-5Z2QO8AU.js +4 -0
- package/dist/server/assets/{today-JkxZXdBt.js → today-BmvIMJzI.js} +3 -3
- package/dist/server/assets/{today-B_TA16QG.js → today-DKWphGrC.js} +112 -57
- package/dist/server/assets/{ui-qbmcMSSX.js → ui-rDHeS-cc.js} +2 -1
- package/dist/server/assets/{useTimelineRouteData-BLXvMGb2.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-DQX6_xc3.js +0 -1
- package/dist/client/assets/SavedTimelineView-BhSiVyR1.js +0 -1
- package/dist/client/assets/SmartTimestamp-BR0-9se2.js +0 -1
- package/dist/client/assets/TimelineRouteFrame-9NDLnXzH.js +0 -1
- package/dist/client/assets/blocks-CFp_njvG.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-sR4xMQxY.js +0 -5
- package/dist/client/assets/createLucideIcon-JV8isFFR.js +0 -1
- package/dist/client/assets/data-sources-euTr6T_L.js +0 -1
- package/dist/client/assets/discuss-BWKu_p7B.js +0 -1
- package/dist/client/assets/dms-SLXMtC4v.js +0 -1
- package/dist/client/assets/inbox-BybSI5Vf.js +0 -1
- package/dist/client/assets/index-DZVisyna.js +0 -31
- package/dist/client/assets/likes-dyh9p8y_.js +0 -1
- package/dist/client/assets/links-CIwqK_dX.js +0 -1
- package/dist/client/assets/mapbox-B4zqX7r0.js +0 -2
- package/dist/client/assets/network-map-zRysIY_b.js +0 -2
- package/dist/client/assets/profile-analyze-C_yDeX6-.js +0 -1
- package/dist/client/assets/profiles._handle-DdIVPqU-.js +0 -1
- package/dist/client/assets/query-client-fYu2rzRC.js +0 -17
- package/dist/client/assets/rate-limits-BWPmPOSh.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-BoO64YoY.js +0 -1
- package/dist/client/assets/useTimelineRouteData-BaO3Rna1.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([]);
|
|
@@ -9660,6 +10087,16 @@ function ensureStubProfileForXUser(db, externalUserId) {
|
|
|
9660
10087
|
function getReferencedTweetId(tweet, type) {
|
|
9661
10088
|
return tweet.referenced_tweets?.find((item) => item.type === type)?.id ?? null;
|
|
9662
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
|
+
}
|
|
9663
10100
|
function replaceTweetFts(db, tweetId, text) {
|
|
9664
10101
|
db.prepare("delete from tweets_fts where tweet_id = ?").run(tweetId);
|
|
9665
10102
|
db.prepare("insert into tweets_fts (tweet_id, text) values (?, ?)").run(
|
|
@@ -9689,7 +10126,7 @@ function ingestTweetPayload(db, {
|
|
|
9689
10126
|
created_at = excluded.created_at,
|
|
9690
10127
|
is_replied = max(tweets.is_replied, excluded.is_replied),
|
|
9691
10128
|
reply_to_id = coalesce(tweets.reply_to_id, excluded.reply_to_id),
|
|
9692
|
-
like_count = excluded.like_count,
|
|
10129
|
+
like_count = case when ? then tweets.like_count else excluded.like_count end,
|
|
9693
10130
|
media_count = max(tweets.media_count, excluded.media_count),
|
|
9694
10131
|
entities_json = excluded.entities_json,
|
|
9695
10132
|
media_json = case
|
|
@@ -9710,25 +10147,29 @@ function ingestTweetPayload(db, {
|
|
|
9710
10147
|
const tweetIds = [];
|
|
9711
10148
|
db.transaction(() => {
|
|
9712
10149
|
const observedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9713
|
-
|
|
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);
|
|
9714
10153
|
const author = usersById.get(tweet.author_id);
|
|
9715
10154
|
const profile = author ? upsertProfileFromXUser(db, author) : ensureStubProfileForXUser(db, tweet.author_id);
|
|
9716
10155
|
const replyToId = getReferencedTweetId(tweet, "replied_to");
|
|
9717
10156
|
const quotedTweetId = getReferencedTweetId(tweet, "quoted");
|
|
10157
|
+
const shouldMarkReplied = isPrimaryTweet && markRepliesAsReplied && Boolean(replyToId);
|
|
9718
10158
|
upsertTweet.run(
|
|
9719
10159
|
tweet.id,
|
|
9720
10160
|
profile.profile.id,
|
|
9721
10161
|
tweet.text,
|
|
9722
10162
|
tweet.created_at,
|
|
9723
|
-
|
|
10163
|
+
shouldMarkReplied ? 1 : 0,
|
|
9724
10164
|
replyToId,
|
|
9725
10165
|
Number(tweet.public_metrics?.like_count ?? 0),
|
|
9726
10166
|
countTweetMedia(tweet),
|
|
9727
10167
|
JSON.stringify(tweetEntitiesFromXurl(tweet.entities)),
|
|
9728
10168
|
buildMediaJsonFromIncludes(tweet, payload.includes?.media),
|
|
9729
|
-
quotedTweetId
|
|
10169
|
+
quotedTweetId,
|
|
10170
|
+
!isPrimaryTweet && tweet.public_metrics?.like_count === void 0 ? 1 : 0
|
|
9730
10171
|
);
|
|
9731
|
-
if (edgeKind) {
|
|
10172
|
+
if (edgeKind && isPrimaryTweet) {
|
|
9732
10173
|
upsertTweetAccountEdge(db, {
|
|
9733
10174
|
accountId,
|
|
9734
10175
|
tweetId: tweet.id,
|
|
@@ -9738,18 +10179,22 @@ function ingestTweetPayload(db, {
|
|
|
9738
10179
|
rawJson: JSON.stringify(tweet)
|
|
9739
10180
|
});
|
|
9740
10181
|
}
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
10182
|
+
if (isPrimaryTweet) {
|
|
10183
|
+
upsertCollection?.run(
|
|
10184
|
+
accountId,
|
|
10185
|
+
tweet.id,
|
|
10186
|
+
collectionKind2,
|
|
10187
|
+
source,
|
|
10188
|
+
JSON.stringify(tweet),
|
|
10189
|
+
observedAt
|
|
10190
|
+
);
|
|
10191
|
+
}
|
|
10192
|
+
replaceTweetFts(db, tweet.id, tweet.text);
|
|
10193
|
+
if (isPrimaryTweet) {
|
|
10194
|
+
tweetIds.push(tweet.id);
|
|
10195
|
+
}
|
|
10196
|
+
}
|
|
10197
|
+
})();
|
|
9753
10198
|
return tweetIds;
|
|
9754
10199
|
}
|
|
9755
10200
|
|
|
@@ -10719,6 +11164,8 @@ var RECENT_TIMELINE_EDGE_CANDIDATES = 5e3;
|
|
|
10719
11164
|
function listTimelineItems({
|
|
10720
11165
|
resource,
|
|
10721
11166
|
account,
|
|
11167
|
+
listAccountId,
|
|
11168
|
+
listId,
|
|
10722
11169
|
search,
|
|
10723
11170
|
replyFilter = "all",
|
|
10724
11171
|
since,
|
|
@@ -10749,7 +11196,7 @@ function listTimelineItems({
|
|
|
10749
11196
|
let join3 = "";
|
|
10750
11197
|
let where = "where e.kind = ?";
|
|
10751
11198
|
let searchSnippetSelect = "";
|
|
10752
|
-
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";
|
|
10753
11200
|
if (likedOnly || bookmarkedOnly) {
|
|
10754
11201
|
if (likedOnly && bookmarkedOnly) {
|
|
10755
11202
|
timelineEdgesCte = `
|
|
@@ -10845,6 +11292,22 @@ function listTimelineItems({
|
|
|
10845
11292
|
params.push(until.trim());
|
|
10846
11293
|
}
|
|
10847
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
|
+
}
|
|
10848
11311
|
const ftsSearch = search?.trim() ? toFtsSearchQuery(search) : "";
|
|
10849
11312
|
if (ftsSearch) {
|
|
10850
11313
|
join3 += " join tweets_fts on tweets_fts.tweet_id = t.id ";
|
|
@@ -12518,6 +12981,7 @@ function reachedStartTimeBoundary(payload, startTimeMs) {
|
|
|
12518
12981
|
function mergeTimelinePayloads(payloads, limit) {
|
|
12519
12982
|
const data = [];
|
|
12520
12983
|
const usersById = /* @__PURE__ */ new Map();
|
|
12984
|
+
const tweetsById = /* @__PURE__ */ new Map();
|
|
12521
12985
|
const mediaByKey = /* @__PURE__ */ new Map();
|
|
12522
12986
|
let meta;
|
|
12523
12987
|
for (const payload of payloads) {
|
|
@@ -12530,6 +12994,9 @@ function mergeTimelinePayloads(payloads, limit) {
|
|
|
12530
12994
|
for (const user of payload.includes?.users ?? []) {
|
|
12531
12995
|
usersById.set(user.id, user);
|
|
12532
12996
|
}
|
|
12997
|
+
for (const tweet of payload.includes?.tweets ?? []) {
|
|
12998
|
+
tweetsById.set(tweet.id, tweet);
|
|
12999
|
+
}
|
|
12533
13000
|
for (const media of payload.includes?.media ?? []) {
|
|
12534
13001
|
mediaByKey.set(media.media_key, media);
|
|
12535
13002
|
}
|
|
@@ -12539,6 +13006,7 @@ function mergeTimelinePayloads(payloads, limit) {
|
|
|
12539
13006
|
data,
|
|
12540
13007
|
includes: {
|
|
12541
13008
|
users: [...usersById.values()],
|
|
13009
|
+
tweets: [...tweetsById.values()],
|
|
12542
13010
|
media: [...mediaByKey.values()]
|
|
12543
13011
|
},
|
|
12544
13012
|
meta
|
|
@@ -12847,6 +13315,7 @@ function compactTweet(source, item) {
|
|
|
12847
13315
|
createdAt: item.createdAt,
|
|
12848
13316
|
text: item.text,
|
|
12849
13317
|
entities: item.entities,
|
|
13318
|
+
media: item.media,
|
|
12850
13319
|
likeCount: item.likeCount,
|
|
12851
13320
|
liked: item.liked,
|
|
12852
13321
|
bookmarked: item.bookmarked,
|
|
@@ -12866,6 +13335,7 @@ function compactEmbeddedTweet(item) {
|
|
|
12866
13335
|
createdAt: item.createdAt,
|
|
12867
13336
|
text: item.text,
|
|
12868
13337
|
entities: item.entities,
|
|
13338
|
+
media: item.media,
|
|
12869
13339
|
likeCount: item.likeCount ?? 0,
|
|
12870
13340
|
liked: Boolean(item.liked),
|
|
12871
13341
|
bookmarked: Boolean(item.bookmarked),
|
|
@@ -15390,6 +15860,8 @@ function compactTweet2(source, item) {
|
|
|
15390
15860
|
authorProfile: item.author,
|
|
15391
15861
|
createdAt: item.createdAt,
|
|
15392
15862
|
text: item.text,
|
|
15863
|
+
entities: item.entities,
|
|
15864
|
+
media: item.media,
|
|
15393
15865
|
likeCount: item.likeCount,
|
|
15394
15866
|
liked: item.liked,
|
|
15395
15867
|
bookmarked: item.bookmarked,
|
|
@@ -15427,7 +15899,9 @@ function collectLiveSearchTweets(options) {
|
|
|
15427
15899
|
t.created_at,
|
|
15428
15900
|
t.is_replied,
|
|
15429
15901
|
t.like_count,
|
|
15430
|
-
|
|
15902
|
+
t.media_count,
|
|
15903
|
+
t.entities_json,
|
|
15904
|
+
t.media_json,
|
|
15431
15905
|
case
|
|
15432
15906
|
when exists (
|
|
15433
15907
|
select 1 from tweet_collections collection
|
|
@@ -15481,6 +15955,8 @@ function collectLiveSearchTweets(options) {
|
|
|
15481
15955
|
},
|
|
15482
15956
|
createdAt: String(row.created_at),
|
|
15483
15957
|
text: String(row.text),
|
|
15958
|
+
entities: parseJsonField2(row.entities_json, {}),
|
|
15959
|
+
media: parseJsonField2(row.media_json, []),
|
|
15484
15960
|
likeCount: Number(row.like_count),
|
|
15485
15961
|
liked: Boolean(row.liked),
|
|
15486
15962
|
bookmarked: Boolean(row.bookmarked),
|
|
@@ -23371,19 +23847,550 @@ function registerJobCommands({ program: program2, print: print2 }) {
|
|
|
23371
23847
|
});
|
|
23372
23848
|
}
|
|
23373
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
|
+
|
|
23374
24381
|
// src/lib/profile-replies.ts
|
|
23375
|
-
import { Effect as
|
|
24382
|
+
import { Effect as Effect51 } from "effect";
|
|
23376
24383
|
|
|
23377
24384
|
// src/lib/moderation-target.ts
|
|
23378
|
-
import { Effect as
|
|
24385
|
+
import { Effect as Effect50 } from "effect";
|
|
23379
24386
|
|
|
23380
24387
|
// src/lib/bird-actions.ts
|
|
23381
|
-
import { Effect as
|
|
24388
|
+
import { Effect as Effect49 } from "effect";
|
|
23382
24389
|
|
|
23383
24390
|
// src/lib/bird-command.ts
|
|
23384
24391
|
import { access } from "node:fs/promises";
|
|
23385
24392
|
import { constants as constants2 } from "node:fs";
|
|
23386
|
-
import { Data as Data3, Effect as
|
|
24393
|
+
import { Data as Data3, Effect as Effect48 } from "effect";
|
|
23387
24394
|
var BirdCommandUnavailableError = class extends Data3.TaggedError(
|
|
23388
24395
|
"BirdCommandUnavailableError"
|
|
23389
24396
|
) {
|
|
@@ -23436,25 +24443,25 @@ function execFailureFromCause(command, cause) {
|
|
|
23436
24443
|
}
|
|
23437
24444
|
function assertBirdCommandAvailableEffect(command) {
|
|
23438
24445
|
if (!isPathCommand(command)) {
|
|
23439
|
-
return
|
|
24446
|
+
return Effect48.void;
|
|
23440
24447
|
}
|
|
23441
|
-
return
|
|
24448
|
+
return Effect48.tryPromise({
|
|
23442
24449
|
try: () => Promise.resolve(access(command, constants2.X_OK)),
|
|
23443
24450
|
catch: (cause) => new BirdCommandUnavailableError({
|
|
23444
24451
|
message: formatBirdInstallHint(command),
|
|
23445
24452
|
command,
|
|
23446
24453
|
cause
|
|
23447
24454
|
})
|
|
23448
|
-
}).pipe(
|
|
24455
|
+
}).pipe(Effect48.asVoid);
|
|
23449
24456
|
}
|
|
23450
24457
|
function getBirdCommandEffect() {
|
|
23451
|
-
return
|
|
24458
|
+
return Effect48.try({
|
|
23452
24459
|
try: () => getBirdCommand(),
|
|
23453
24460
|
catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
|
|
23454
24461
|
});
|
|
23455
24462
|
}
|
|
23456
24463
|
function runBirdCommandEffect(args, options) {
|
|
23457
|
-
return
|
|
24464
|
+
return Effect48.gen(function* () {
|
|
23458
24465
|
const birdCommand = yield* getBirdCommandEffect();
|
|
23459
24466
|
yield* assertBirdCommandAvailableEffect(birdCommand);
|
|
23460
24467
|
return yield* runSubprocessEffect({
|
|
@@ -23467,8 +24474,8 @@ function runBirdCommandEffect(args, options) {
|
|
|
23467
24474
|
...options?.signal ? { signal: options.signal } : {},
|
|
23468
24475
|
...options?.killSignal ? { killSignal: options.killSignal } : {}
|
|
23469
24476
|
}).pipe(
|
|
23470
|
-
|
|
23471
|
-
|
|
24477
|
+
Effect48.map(({ stdout, stderr }) => ({ stdout, stderr })),
|
|
24478
|
+
Effect48.mapError((cause) => execFailureFromCause(birdCommand, cause))
|
|
23472
24479
|
);
|
|
23473
24480
|
});
|
|
23474
24481
|
}
|
|
@@ -23506,9 +24513,9 @@ function toError18(error) {
|
|
|
23506
24513
|
return error instanceof Error ? error : new Error(String(error));
|
|
23507
24514
|
}
|
|
23508
24515
|
function runBirdJsonCommandEffect2(args) {
|
|
23509
|
-
return
|
|
24516
|
+
return Effect49.gen(function* () {
|
|
23510
24517
|
const { stdout } = yield* runBirdCommandEffect(args);
|
|
23511
|
-
return yield*
|
|
24518
|
+
return yield* Effect49.try({
|
|
23512
24519
|
try: () => JSON.parse(stripAnsi2(stdout)),
|
|
23513
24520
|
catch: toError18
|
|
23514
24521
|
});
|
|
@@ -23522,13 +24529,13 @@ function safeBirdProfileArg(query) {
|
|
|
23522
24529
|
throw new Error("Invalid bird profile query");
|
|
23523
24530
|
}
|
|
23524
24531
|
function readBirdStatusViaBirdEffect(query) {
|
|
23525
|
-
return
|
|
23526
|
-
const safeQuery = yield*
|
|
24532
|
+
return Effect49.gen(function* () {
|
|
24533
|
+
const safeQuery = yield* Effect49.try({
|
|
23527
24534
|
try: () => safeBirdProfileArg(query),
|
|
23528
24535
|
catch: toError18
|
|
23529
24536
|
});
|
|
23530
24537
|
return yield* runBirdJsonCommandEffect2(["status", safeQuery, "--json"]);
|
|
23531
|
-
}).pipe(
|
|
24538
|
+
}).pipe(Effect49.catchAll(() => Effect49.succeed(null)));
|
|
23532
24539
|
}
|
|
23533
24540
|
function readBirdStatusViaBird(query) {
|
|
23534
24541
|
return runEffectPromise(readBirdStatusViaBirdEffect(query));
|
|
@@ -23565,8 +24572,8 @@ function toBirdLookupUser(payload) {
|
|
|
23565
24572
|
};
|
|
23566
24573
|
}
|
|
23567
24574
|
function lookupProfileViaBirdEffect2(query) {
|
|
23568
|
-
return
|
|
23569
|
-
const safeQuery = yield*
|
|
24575
|
+
return Effect49.gen(function* () {
|
|
24576
|
+
const safeQuery = yield* Effect49.try({
|
|
23570
24577
|
try: () => safeBirdProfileArg(query),
|
|
23571
24578
|
catch: toError18
|
|
23572
24579
|
});
|
|
@@ -23577,11 +24584,11 @@ function lookupProfileViaBirdEffect2(query) {
|
|
|
23577
24584
|
"1",
|
|
23578
24585
|
"--json"
|
|
23579
24586
|
]);
|
|
23580
|
-
return yield*
|
|
24587
|
+
return yield* Effect49.try({
|
|
23581
24588
|
try: () => toBirdLookupUser(payload),
|
|
23582
24589
|
catch: toError18
|
|
23583
24590
|
});
|
|
23584
|
-
}).pipe(
|
|
24591
|
+
}).pipe(Effect49.catchAll(() => Effect49.succeed(null)));
|
|
23585
24592
|
}
|
|
23586
24593
|
function lookupProfileViaBird(query) {
|
|
23587
24594
|
return runEffectPromise(lookupProfileViaBirdEffect2(query));
|
|
@@ -23592,8 +24599,8 @@ function runVerifiedBirdMutationEffect({
|
|
|
23592
24599
|
verifyField,
|
|
23593
24600
|
expectedValue
|
|
23594
24601
|
}) {
|
|
23595
|
-
return
|
|
23596
|
-
const safeQuery = yield*
|
|
24602
|
+
return Effect49.gen(function* () {
|
|
24603
|
+
const safeQuery = yield* Effect49.try({
|
|
23597
24604
|
try: () => safeBirdProfileArg(query),
|
|
23598
24605
|
catch: toError18
|
|
23599
24606
|
});
|
|
@@ -23607,12 +24614,12 @@ function runVerifiedBirdMutationEffect({
|
|
|
23607
24614
|
action,
|
|
23608
24615
|
safeQuery
|
|
23609
24616
|
]).pipe(
|
|
23610
|
-
|
|
24617
|
+
Effect49.map(({ stdout, stderr }) => ({
|
|
23611
24618
|
ok: true,
|
|
23612
24619
|
output: normalizeOutput(stdout, stderr)
|
|
23613
24620
|
})),
|
|
23614
|
-
|
|
23615
|
-
(error) =>
|
|
24621
|
+
Effect49.catchAll(
|
|
24622
|
+
(error) => Effect49.succeed({
|
|
23616
24623
|
ok: false,
|
|
23617
24624
|
output: formatExecError2(error, `bird ${action} failed`)
|
|
23618
24625
|
})
|
|
@@ -23691,7 +24698,7 @@ function toError19(error) {
|
|
|
23691
24698
|
return error instanceof Error ? error : new Error(String(error));
|
|
23692
24699
|
}
|
|
23693
24700
|
function trySync14(try_) {
|
|
23694
|
-
return
|
|
24701
|
+
return Effect50.try({
|
|
23695
24702
|
try: try_,
|
|
23696
24703
|
catch: toError19
|
|
23697
24704
|
});
|
|
@@ -23739,11 +24746,11 @@ function resolveLocalProfile(db, normalizedQuery) {
|
|
|
23739
24746
|
};
|
|
23740
24747
|
}
|
|
23741
24748
|
function resolveProfileEffect(query, providedDb) {
|
|
23742
|
-
return
|
|
24749
|
+
return Effect50.gen(function* () {
|
|
23743
24750
|
const db = providedDb ?? (yield* trySync14(() => getNativeDb()));
|
|
23744
24751
|
const normalizedQuery = normalizeProfileQuery(query);
|
|
23745
24752
|
if (!normalizedQuery) {
|
|
23746
|
-
return yield*
|
|
24753
|
+
return yield* Effect50.fail(new Error("Missing profile handle or id"));
|
|
23747
24754
|
}
|
|
23748
24755
|
const local = yield* trySync14(
|
|
23749
24756
|
() => resolveLocalProfile(db, normalizedQuery)
|
|
@@ -23752,7 +24759,7 @@ function resolveProfileEffect(query, providedDb) {
|
|
|
23752
24759
|
if (local) {
|
|
23753
24760
|
return local;
|
|
23754
24761
|
}
|
|
23755
|
-
return yield*
|
|
24762
|
+
return yield* Effect50.fail(
|
|
23756
24763
|
new Error(`Profile not found locally: ${query}`)
|
|
23757
24764
|
);
|
|
23758
24765
|
}
|
|
@@ -23764,8 +24771,8 @@ function resolveProfileEffect(query, providedDb) {
|
|
|
23764
24771
|
const birdResult = yield* tryPromise(
|
|
23765
24772
|
() => lookupProfileViaBird(local?.profile.handle ?? normalizedQuery)
|
|
23766
24773
|
).pipe(
|
|
23767
|
-
|
|
23768
|
-
|
|
24774
|
+
Effect50.map((value) => ({ ok: true, value })),
|
|
24775
|
+
Effect50.catchAll((error) => Effect50.succeed({ ok: false, error }))
|
|
23769
24776
|
);
|
|
23770
24777
|
if (birdResult.ok) {
|
|
23771
24778
|
user = birdResult.value ?? void 0;
|
|
@@ -23776,9 +24783,9 @@ function resolveProfileEffect(query, providedDb) {
|
|
|
23776
24783
|
const xurlResult = yield* tryPromise(
|
|
23777
24784
|
() => /^\d+$/.test(normalizedQuery) ? lookupUsersByIds([normalizedQuery]) : lookupUsersByHandles([local?.profile.handle ?? normalizedQuery])
|
|
23778
24785
|
).pipe(
|
|
23779
|
-
|
|
23780
|
-
|
|
23781
|
-
(error) =>
|
|
24786
|
+
Effect50.map((value) => ({ ok: true, value })),
|
|
24787
|
+
Effect50.catchAll(
|
|
24788
|
+
(error) => Effect50.succeed({ ok: false, error })
|
|
23782
24789
|
)
|
|
23783
24790
|
);
|
|
23784
24791
|
if (xurlResult.ok) {
|
|
@@ -23791,13 +24798,13 @@ function resolveProfileEffect(query, providedDb) {
|
|
|
23791
24798
|
if (local) {
|
|
23792
24799
|
return local;
|
|
23793
24800
|
}
|
|
23794
|
-
return yield*
|
|
24801
|
+
return yield* Effect50.fail(lastError);
|
|
23795
24802
|
}
|
|
23796
24803
|
if (!user) {
|
|
23797
24804
|
if (local) {
|
|
23798
24805
|
return local;
|
|
23799
24806
|
}
|
|
23800
|
-
return yield*
|
|
24807
|
+
return yield* Effect50.fail(new Error(`Profile not found: ${query}`));
|
|
23801
24808
|
}
|
|
23802
24809
|
if (local) {
|
|
23803
24810
|
return yield* databaseWriteEffect(
|
|
@@ -23838,11 +24845,11 @@ function inspectProfileReplies(query, { limit = 12 } = {}) {
|
|
|
23838
24845
|
return runEffectPromise(inspectProfileRepliesEffect(query, { limit }));
|
|
23839
24846
|
}
|
|
23840
24847
|
function inspectProfileRepliesEffect(query, { limit = 12 } = {}) {
|
|
23841
|
-
return
|
|
24848
|
+
return Effect51.gen(function* () {
|
|
23842
24849
|
const resolved = yield* tryPromise(() => resolveProfile(query));
|
|
23843
24850
|
const externalUserId = resolved.externalUserId;
|
|
23844
24851
|
if (!externalUserId) {
|
|
23845
|
-
return yield*
|
|
24852
|
+
return yield* Effect51.fail(
|
|
23846
24853
|
new Error(`Profile has no external Twitter user id: ${query}`)
|
|
23847
24854
|
);
|
|
23848
24855
|
}
|
|
@@ -23937,18 +24944,18 @@ function registerMentionCommands({
|
|
|
23937
24944
|
}
|
|
23938
24945
|
|
|
23939
24946
|
// src/lib/blocks.ts
|
|
23940
|
-
import { Effect as
|
|
24947
|
+
import { Effect as Effect54 } from "effect";
|
|
23941
24948
|
|
|
23942
24949
|
// src/lib/moderation-state.ts
|
|
23943
|
-
import { Effect as
|
|
24950
|
+
import { Effect as Effect53 } from "effect";
|
|
23944
24951
|
|
|
23945
24952
|
// src/lib/actions-transport.ts
|
|
23946
|
-
import { Effect as
|
|
24953
|
+
import { Effect as Effect52 } from "effect";
|
|
23947
24954
|
function toError20(error) {
|
|
23948
24955
|
return error instanceof Error ? error : new Error(String(error));
|
|
23949
24956
|
}
|
|
23950
24957
|
function trySync15(try_) {
|
|
23951
|
-
return
|
|
24958
|
+
return Effect52.try({
|
|
23952
24959
|
try: try_,
|
|
23953
24960
|
catch: toError20
|
|
23954
24961
|
});
|
|
@@ -23960,7 +24967,7 @@ function liveWritesDisabled4() {
|
|
|
23960
24967
|
return process.env.BIRDCLAW_DISABLE_LIVE_WRITES === "1";
|
|
23961
24968
|
}
|
|
23962
24969
|
function verifyExpectedAccountEffect(expectedAccount) {
|
|
23963
|
-
return
|
|
24970
|
+
return Effect52.gen(function* () {
|
|
23964
24971
|
if (!expectedAccount) return null;
|
|
23965
24972
|
if (liveWritesDisabled4()) return null;
|
|
23966
24973
|
const sourceUser = yield* tryPromise(() => lookupAuthenticatedUserFresh());
|
|
@@ -23970,7 +24977,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
|
|
|
23970
24977
|
const expectedHandle = profileHandleKey(expectedAccount.handle);
|
|
23971
24978
|
if (expectedExternalUserId) {
|
|
23972
24979
|
if (sourceUserId === expectedExternalUserId) return sourceUserId;
|
|
23973
|
-
return yield*
|
|
24980
|
+
return yield* Effect52.fail(
|
|
23974
24981
|
new Error(
|
|
23975
24982
|
`xurl is authenticated as user ${sourceUserId || "unknown"}, not account ${expectedAccount.id}`
|
|
23976
24983
|
)
|
|
@@ -23978,7 +24985,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
|
|
|
23978
24985
|
}
|
|
23979
24986
|
if (expectedHandle && sourceUsername === expectedHandle)
|
|
23980
24987
|
return sourceUserId;
|
|
23981
|
-
return yield*
|
|
24988
|
+
return yield* Effect52.fail(
|
|
23982
24989
|
new Error(
|
|
23983
24990
|
sourceUsername ? `xurl is authenticated as @${sourceUsername}, not @${expectedHandle}` : "xurl authenticated user unavailable"
|
|
23984
24991
|
)
|
|
@@ -23986,7 +24993,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
|
|
|
23986
24993
|
});
|
|
23987
24994
|
}
|
|
23988
24995
|
function runBirdActionEffect(action, query) {
|
|
23989
|
-
return
|
|
24996
|
+
return Effect52.gen(function* () {
|
|
23990
24997
|
const result = yield* tryPromise(
|
|
23991
24998
|
() => action === "block" ? blockUserViaBird(query) : action === "unblock" ? unblockUserViaBird(query) : action === "mute" ? muteUserViaBird(query) : unmuteUserViaBird(query)
|
|
23992
24999
|
);
|
|
@@ -24006,7 +25013,7 @@ function getVerifyExpectation(action) {
|
|
|
24006
25013
|
};
|
|
24007
25014
|
}
|
|
24008
25015
|
function runXurlActionEffect(action, query, targetUserId, verifiedSourceUserId) {
|
|
24009
|
-
return
|
|
25016
|
+
return Effect52.gen(function* () {
|
|
24010
25017
|
if (!targetUserId) {
|
|
24011
25018
|
return {
|
|
24012
25019
|
ok: false,
|
|
@@ -24071,13 +25078,13 @@ function runModerationActionEffect({
|
|
|
24071
25078
|
transport,
|
|
24072
25079
|
expectedAccount
|
|
24073
25080
|
}) {
|
|
24074
|
-
return
|
|
25081
|
+
return Effect52.gen(function* () {
|
|
24075
25082
|
const requestedTransport = yield* trySync15(
|
|
24076
25083
|
() => resolveActionsTransport(transport)
|
|
24077
25084
|
);
|
|
24078
25085
|
const verifyXurlAccount = () => verifyExpectedAccountEffect(expectedAccount).pipe(
|
|
24079
|
-
|
|
24080
|
-
(error) =>
|
|
25086
|
+
Effect52.catchAll(
|
|
25087
|
+
(error) => Effect52.succeed({
|
|
24081
25088
|
ok: false,
|
|
24082
25089
|
output: error instanceof Error ? error.message : String(error),
|
|
24083
25090
|
transport: "xurl"
|
|
@@ -24166,7 +25173,7 @@ function toError21(error) {
|
|
|
24166
25173
|
return error instanceof Error ? error : new Error(String(error));
|
|
24167
25174
|
}
|
|
24168
25175
|
function trySync16(try_) {
|
|
24169
|
-
return
|
|
25176
|
+
return Effect53.try({
|
|
24170
25177
|
try: try_,
|
|
24171
25178
|
catch: toError21
|
|
24172
25179
|
});
|
|
@@ -24285,20 +25292,20 @@ function resolveModerationTargetEffect({
|
|
|
24285
25292
|
query,
|
|
24286
25293
|
selfActionError
|
|
24287
25294
|
}) {
|
|
24288
|
-
return
|
|
25295
|
+
return Effect53.gen(function* () {
|
|
24289
25296
|
const db = yield* trySync16(() => getNativeDb());
|
|
24290
25297
|
const resolvedAccountId = accountId || getDefaultAccountId(db);
|
|
24291
25298
|
const accountHandle = yield* trySync16(
|
|
24292
25299
|
() => getAccountHandle(db, resolvedAccountId)
|
|
24293
25300
|
);
|
|
24294
25301
|
if (!accountHandle) {
|
|
24295
|
-
return yield*
|
|
25302
|
+
return yield* Effect53.fail(
|
|
24296
25303
|
new Error(`Unknown account: ${resolvedAccountId}`)
|
|
24297
25304
|
);
|
|
24298
25305
|
}
|
|
24299
25306
|
const normalizedQuery = normalizeProfileQuery(query);
|
|
24300
25307
|
if (normalizedQuery.toLowerCase() === accountHandle.toLowerCase()) {
|
|
24301
|
-
return yield*
|
|
25308
|
+
return yield* Effect53.fail(new Error(selfActionError));
|
|
24302
25309
|
}
|
|
24303
25310
|
const resolved = yield* resolveProfileEffect(query, db);
|
|
24304
25311
|
const account = yield* trySync16(
|
|
@@ -24309,7 +25316,7 @@ function resolveModerationTargetEffect({
|
|
|
24309
25316
|
);
|
|
24310
25317
|
const accountExternalUserId = account?.external_user_id ?? getExternalUserId(accountProfile?.id ?? "");
|
|
24311
25318
|
if (profileHandleKey(resolved.profile.handle) === profileHandleKey(accountHandle) || accountProfile?.id && resolved.profile.id === accountProfile.id || accountExternalUserId && resolved.externalUserId === accountExternalUserId) {
|
|
24312
|
-
return yield*
|
|
25319
|
+
return yield* Effect53.fail(new Error(selfActionError));
|
|
24313
25320
|
}
|
|
24314
25321
|
return {
|
|
24315
25322
|
db,
|
|
@@ -24326,7 +25333,7 @@ function resolveModerationTargetEffect({
|
|
|
24326
25333
|
}
|
|
24327
25334
|
function addModerationEffect(kind, accountId, query, options = {}) {
|
|
24328
25335
|
const descriptor = moderationDescriptors[kind];
|
|
24329
|
-
return
|
|
25336
|
+
return Effect53.gen(function* () {
|
|
24330
25337
|
const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } = yield* resolveModerationTargetEffect({
|
|
24331
25338
|
accountId,
|
|
24332
25339
|
query,
|
|
@@ -24376,7 +25383,7 @@ function addModeration(kind, accountId, query, options = {}) {
|
|
|
24376
25383
|
}
|
|
24377
25384
|
function recordModerationEffect(kind, accountId, query) {
|
|
24378
25385
|
const descriptor = moderationDescriptors[kind];
|
|
24379
|
-
return
|
|
25386
|
+
return Effect53.gen(function* () {
|
|
24380
25387
|
const { db, resolved, resolvedAccountId } = yield* resolveModerationTargetEffect({
|
|
24381
25388
|
accountId,
|
|
24382
25389
|
query,
|
|
@@ -24407,7 +25414,7 @@ function recordModeration(kind, accountId, query) {
|
|
|
24407
25414
|
}
|
|
24408
25415
|
function removeModerationEffect(kind, accountId, query, options = {}) {
|
|
24409
25416
|
const descriptor = moderationDescriptors[kind];
|
|
24410
|
-
return
|
|
25417
|
+
return Effect53.gen(function* () {
|
|
24411
25418
|
const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } = yield* resolveModerationTargetEffect({
|
|
24412
25419
|
accountId,
|
|
24413
25420
|
query,
|
|
@@ -24483,7 +25490,7 @@ function toError22(error) {
|
|
|
24483
25490
|
return error instanceof Error ? error : new Error(String(error));
|
|
24484
25491
|
}
|
|
24485
25492
|
function trySync17(try_) {
|
|
24486
|
-
return
|
|
25493
|
+
return Effect54.try({
|
|
24487
25494
|
try: try_,
|
|
24488
25495
|
catch: toError22
|
|
24489
25496
|
});
|
|
@@ -24496,12 +25503,12 @@ function listBlocks({
|
|
|
24496
25503
|
return listModerationState("block", { account, search, limit });
|
|
24497
25504
|
}
|
|
24498
25505
|
function syncBlocksEffect(accountId) {
|
|
24499
|
-
return
|
|
25506
|
+
return Effect54.gen(function* () {
|
|
24500
25507
|
const db = yield* trySync17(() => getNativeDb());
|
|
24501
25508
|
const resolvedAccountId = accountId || getDefaultAccountId(db);
|
|
24502
25509
|
const accountHandle = getAccountHandle(db, resolvedAccountId);
|
|
24503
25510
|
if (!accountHandle) {
|
|
24504
|
-
return yield*
|
|
25511
|
+
return yield* Effect54.fail(
|
|
24505
25512
|
new Error(`Unknown account: ${resolvedAccountId}`)
|
|
24506
25513
|
);
|
|
24507
25514
|
}
|
|
@@ -24522,9 +25529,9 @@ function syncBlocksEffect(accountId) {
|
|
|
24522
25529
|
}
|
|
24523
25530
|
};
|
|
24524
25531
|
}
|
|
24525
|
-
return yield*
|
|
25532
|
+
return yield* Effect54.gen(function* () {
|
|
24526
25533
|
const me = yield* tryPromise(() => lookupAuthenticatedUserFresh()).pipe(
|
|
24527
|
-
|
|
25534
|
+
Effect54.mapError(toError22)
|
|
24528
25535
|
);
|
|
24529
25536
|
const sourceUserId = typeof me?.id === "string" && me.id.length > 0 ? me.id : null;
|
|
24530
25537
|
const sourceUsername = typeof me?.username === "string" ? me.username.replace(/^@/, "") : "";
|
|
@@ -24571,7 +25578,7 @@ function syncBlocksEffect(accountId) {
|
|
|
24571
25578
|
do {
|
|
24572
25579
|
const page = yield* tryPromise(
|
|
24573
25580
|
() => listBlockedUsers(sourceUserId, nextToken ?? void 0)
|
|
24574
|
-
).pipe(
|
|
25581
|
+
).pipe(Effect54.mapError(toError22));
|
|
24575
25582
|
const pageProfileIds = yield* databaseWriteEffect((writeDb) => {
|
|
24576
25583
|
return page.items.map((user) => {
|
|
24577
25584
|
const resolved = upsertProfileFromXUser(writeDb, user);
|
|
@@ -24613,8 +25620,8 @@ function syncBlocksEffect(accountId) {
|
|
|
24613
25620
|
}
|
|
24614
25621
|
};
|
|
24615
25622
|
}).pipe(
|
|
24616
|
-
|
|
24617
|
-
(error) =>
|
|
25623
|
+
Effect54.catchAll(
|
|
25624
|
+
(error) => Effect54.succeed({
|
|
24618
25625
|
ok: false,
|
|
24619
25626
|
accountId: resolvedAccountId,
|
|
24620
25627
|
synced: remoteProfileIds.length > 0,
|
|
@@ -24765,7 +25772,7 @@ function registerModerationCommands({
|
|
|
24765
25772
|
|
|
24766
25773
|
// src/lib/blocklist.ts
|
|
24767
25774
|
import { readFile } from "node:fs/promises";
|
|
24768
|
-
import { Effect as
|
|
25775
|
+
import { Effect as Effect55 } from "effect";
|
|
24769
25776
|
function stripInlineCode(value) {
|
|
24770
25777
|
const match = value.match(/`([^`]+)`/);
|
|
24771
25778
|
return match?.[1] ?? value;
|
|
@@ -24802,15 +25809,15 @@ function parseBlocklistText(text) {
|
|
|
24802
25809
|
return items;
|
|
24803
25810
|
}
|
|
24804
25811
|
function importBlocklistEffect(accountId, filePath) {
|
|
24805
|
-
return
|
|
25812
|
+
return Effect55.gen(function* () {
|
|
24806
25813
|
const text = yield* tryPromise(() => readFile(filePath, "utf8"));
|
|
24807
25814
|
const queries = parseBlocklistText(text);
|
|
24808
25815
|
const items = [];
|
|
24809
25816
|
for (const query of queries) {
|
|
24810
25817
|
const blocked = yield* tryPromise(() => addBlock(accountId, query)).pipe(
|
|
24811
|
-
|
|
24812
|
-
|
|
24813
|
-
(error) =>
|
|
25818
|
+
Effect55.map((result2) => ({ ok: true, result: result2 })),
|
|
25819
|
+
Effect55.catchAll(
|
|
25820
|
+
(error) => Effect55.succeed({ ok: false, error })
|
|
24814
25821
|
)
|
|
24815
25822
|
);
|
|
24816
25823
|
if (!blocked.ok) {
|
|
@@ -24866,7 +25873,7 @@ function registerModerationCommands2(context) {
|
|
|
24866
25873
|
}
|
|
24867
25874
|
|
|
24868
25875
|
// src/lib/link-index.ts
|
|
24869
|
-
import { Effect as
|
|
25876
|
+
import { Effect as Effect56 } from "effect";
|
|
24870
25877
|
var DEFAULT_EXPAND_CONCURRENCY = 12;
|
|
24871
25878
|
function parseJsonField5(value, fallback) {
|
|
24872
25879
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -25028,7 +26035,7 @@ function expandWithConcurrencyEffect(db, urls, options) {
|
|
|
25028
26035
|
misses: 0,
|
|
25029
26036
|
errors: 0
|
|
25030
26037
|
};
|
|
25031
|
-
return
|
|
26038
|
+
return Effect56.forEach(
|
|
25032
26039
|
urls,
|
|
25033
26040
|
(url) => tryPromise(
|
|
25034
26041
|
() => expandUrls([url], {
|
|
@@ -25037,7 +26044,7 @@ function expandWithConcurrencyEffect(db, urls, options) {
|
|
|
25037
26044
|
timeoutMs: options.timeoutMs
|
|
25038
26045
|
})
|
|
25039
26046
|
).pipe(
|
|
25040
|
-
|
|
26047
|
+
Effect56.map((results) => {
|
|
25041
26048
|
const result = results[0];
|
|
25042
26049
|
if (result.source === "network") {
|
|
25043
26050
|
counts.networkExpansions++;
|
|
@@ -25054,10 +26061,10 @@ function expandWithConcurrencyEffect(db, urls, options) {
|
|
|
25054
26061
|
})
|
|
25055
26062
|
),
|
|
25056
26063
|
{ concurrency }
|
|
25057
|
-
).pipe(
|
|
26064
|
+
).pipe(Effect56.as(counts));
|
|
25058
26065
|
}
|
|
25059
26066
|
function backfillLinkIndexEffect(options = {}) {
|
|
25060
|
-
return
|
|
26067
|
+
return Effect56.gen(function* () {
|
|
25061
26068
|
const db = getNativeDb({ seedDemoData: false });
|
|
25062
26069
|
const { occurrences, entityExpansions } = rebuildOccurrences(
|
|
25063
26070
|
db,
|
|
@@ -25352,7 +26359,7 @@ import {
|
|
|
25352
26359
|
} from "node:fs";
|
|
25353
26360
|
import { appendFile, copyFile, rename, rm, writeFile } from "node:fs/promises";
|
|
25354
26361
|
import path11 from "node:path";
|
|
25355
|
-
import { Effect as
|
|
26362
|
+
import { Effect as Effect57 } from "effect";
|
|
25356
26363
|
var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
|
|
25357
26364
|
var PBS_PREFIXES = [
|
|
25358
26365
|
"/media/",
|
|
@@ -25371,13 +26378,13 @@ function toMediaError(error) {
|
|
|
25371
26378
|
return error instanceof Error ? error : new Error(String(error));
|
|
25372
26379
|
}
|
|
25373
26380
|
function tryMediaSync(try_) {
|
|
25374
|
-
return
|
|
26381
|
+
return Effect57.try({
|
|
25375
26382
|
try: try_,
|
|
25376
26383
|
catch: toMediaError
|
|
25377
26384
|
});
|
|
25378
26385
|
}
|
|
25379
26386
|
function tryMediaPromise(try_) {
|
|
25380
|
-
return
|
|
26387
|
+
return Effect57.tryPromise({
|
|
25381
26388
|
try: try_,
|
|
25382
26389
|
catch: toMediaError
|
|
25383
26390
|
});
|
|
@@ -25642,7 +26649,7 @@ function archivePathFor(item, mediaOriginalsDir) {
|
|
|
25642
26649
|
return archiveTweetDirs(mediaOriginalsDir, item.tweetId).map((tweetDir) => path11.join(tweetDir, fileName)).find((archivePath) => existsSync5(archivePath)) ?? null;
|
|
25643
26650
|
}
|
|
25644
26651
|
function reuseFromArchiveEffect(item, mediaOriginalsDir, maxBytes) {
|
|
25645
|
-
return
|
|
26652
|
+
return Effect57.gen(function* () {
|
|
25646
26653
|
const archivePath = archivePathFor(item, mediaOriginalsDir);
|
|
25647
26654
|
if (!archivePath || !existsSync5(archivePath)) return null;
|
|
25648
26655
|
const bytes = fileSize(archivePath);
|
|
@@ -25669,15 +26676,15 @@ function contentRangeTotal(response) {
|
|
|
25669
26676
|
return total ? Number(total) : null;
|
|
25670
26677
|
}
|
|
25671
26678
|
function writeResponseBodyEffect(response, tmpPath, append, maxBytes, initialBytes) {
|
|
25672
|
-
return
|
|
26679
|
+
return Effect57.gen(function* () {
|
|
25673
26680
|
if (!response.body)
|
|
25674
|
-
return yield*
|
|
26681
|
+
return yield* Effect57.fail(new Error("missing response body"));
|
|
25675
26682
|
let bytes = 0;
|
|
25676
26683
|
if (!append) {
|
|
25677
26684
|
yield* tryMediaPromise(() => writeFile(tmpPath, Buffer.alloc(0)));
|
|
25678
26685
|
}
|
|
25679
26686
|
const reader = response.body.getReader();
|
|
25680
|
-
const result = yield*
|
|
26687
|
+
const result = yield* Effect57.gen(function* () {
|
|
25681
26688
|
for (; ; ) {
|
|
25682
26689
|
const { done, value } = yield* tryMediaPromise(() => reader.read());
|
|
25683
26690
|
if (done) {
|
|
@@ -25687,31 +26694,31 @@ function writeResponseBodyEffect(response, tmpPath, append, maxBytes, initialByt
|
|
|
25687
26694
|
bytes += chunk.length;
|
|
25688
26695
|
if (initialBytes + bytes > maxBytes) {
|
|
25689
26696
|
yield* tryMediaPromise(() => rm(tmpPath, { force: true })).pipe(
|
|
25690
|
-
|
|
26697
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25691
26698
|
);
|
|
25692
|
-
return yield*
|
|
26699
|
+
return yield* Effect57.fail(new Error("max-bytes"));
|
|
25693
26700
|
}
|
|
25694
26701
|
yield* tryMediaPromise(() => appendFile(tmpPath, chunk));
|
|
25695
26702
|
}
|
|
25696
26703
|
return bytes;
|
|
25697
26704
|
}).pipe(
|
|
25698
|
-
|
|
25699
|
-
|
|
25700
|
-
(error) =>
|
|
26705
|
+
Effect57.map((writtenBytes) => ({ ok: true, writtenBytes })),
|
|
26706
|
+
Effect57.catchAll(
|
|
26707
|
+
(error) => Effect57.gen(function* () {
|
|
25701
26708
|
if (error.message === "max-bytes") {
|
|
25702
26709
|
yield* tryMediaPromise(() => rm(tmpPath, { force: true })).pipe(
|
|
25703
|
-
|
|
26710
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25704
26711
|
);
|
|
25705
26712
|
}
|
|
25706
26713
|
yield* tryMediaPromise(() => reader.cancel(error)).pipe(
|
|
25707
|
-
|
|
26714
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25708
26715
|
);
|
|
25709
26716
|
return { error, ok: false };
|
|
25710
26717
|
})
|
|
25711
26718
|
),
|
|
25712
|
-
|
|
26719
|
+
Effect57.ensuring(Effect57.sync(() => reader.releaseLock()))
|
|
25713
26720
|
);
|
|
25714
|
-
if (!result.ok) return yield*
|
|
26721
|
+
if (!result.ok) return yield* Effect57.fail(result.error);
|
|
25715
26722
|
return result.writtenBytes;
|
|
25716
26723
|
});
|
|
25717
26724
|
}
|
|
@@ -25723,13 +26730,13 @@ function fetchOneEffect({
|
|
|
25723
26730
|
userAgent,
|
|
25724
26731
|
maxBytes
|
|
25725
26732
|
}) {
|
|
25726
|
-
return
|
|
26733
|
+
return Effect57.gen(function* () {
|
|
25727
26734
|
let rateLimited = false;
|
|
25728
26735
|
for (let attempt = 0; attempt <= retryMax; attempt += 1) {
|
|
25729
26736
|
const partialBytes = item.kind === "image" ? 0 : fileSize(item.tmpPath);
|
|
25730
26737
|
if (partialBytes > maxBytes) {
|
|
25731
26738
|
yield* tryMediaPromise(() => rm(item.tmpPath, { force: true })).pipe(
|
|
25732
|
-
|
|
26739
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25733
26740
|
);
|
|
25734
26741
|
return fail(item, "max-bytes");
|
|
25735
26742
|
}
|
|
@@ -25741,9 +26748,9 @@ function fetchOneEffect({
|
|
|
25741
26748
|
}
|
|
25742
26749
|
})
|
|
25743
26750
|
).pipe(
|
|
25744
|
-
|
|
25745
|
-
|
|
25746
|
-
(error) =>
|
|
26751
|
+
Effect57.map((response2) => ({ ok: true, response: response2 })),
|
|
26752
|
+
Effect57.catchAll(
|
|
26753
|
+
(error) => Effect57.succeed({ error, ok: false })
|
|
25747
26754
|
)
|
|
25748
26755
|
);
|
|
25749
26756
|
if (!responseResult.ok) {
|
|
@@ -25754,7 +26761,7 @@ function fetchOneEffect({
|
|
|
25754
26761
|
rateLimited = true;
|
|
25755
26762
|
if (attempt < retryMax) {
|
|
25756
26763
|
yield* tryMediaPromise(() => sleep(1e3 * 2 ** attempt)).pipe(
|
|
25757
|
-
|
|
26764
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25758
26765
|
);
|
|
25759
26766
|
continue;
|
|
25760
26767
|
}
|
|
@@ -25766,7 +26773,7 @@ function fetchOneEffect({
|
|
|
25766
26773
|
const expectedTotal = contentRangeTotal(response) ?? (contentLength(response) ?? 0) + (response.status === 206 ? partialBytes : 0);
|
|
25767
26774
|
if (expectedTotal > maxBytes) {
|
|
25768
26775
|
yield* tryMediaPromise(() => rm(item.tmpPath, { force: true })).pipe(
|
|
25769
|
-
|
|
26776
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25770
26777
|
);
|
|
25771
26778
|
return fail(item, "max-bytes");
|
|
25772
26779
|
}
|
|
@@ -25778,9 +26785,9 @@ function fetchOneEffect({
|
|
|
25778
26785
|
maxBytes,
|
|
25779
26786
|
append ? partialBytes : 0
|
|
25780
26787
|
).pipe(
|
|
25781
|
-
|
|
25782
|
-
|
|
25783
|
-
(error) =>
|
|
26788
|
+
Effect57.map((bytes) => ({ bytes, ok: true })),
|
|
26789
|
+
Effect57.catchAll(
|
|
26790
|
+
(error) => Effect57.succeed({ error, ok: false })
|
|
25784
26791
|
)
|
|
25785
26792
|
);
|
|
25786
26793
|
if (!bytesResult.ok) {
|
|
@@ -25854,7 +26861,7 @@ function runGroupEffect(items, parallel, pacingMs, now2, sleep, worker) {
|
|
|
25854
26861
|
);
|
|
25855
26862
|
})
|
|
25856
26863
|
);
|
|
25857
|
-
return
|
|
26864
|
+
return Effect57.forEach(pacedItems, runPaced, {
|
|
25858
26865
|
concurrency: Math.min(parallel, items.length),
|
|
25859
26866
|
discard: true
|
|
25860
26867
|
});
|
|
@@ -25863,7 +26870,7 @@ function fetchTweetMedia(options = {}) {
|
|
|
25863
26870
|
return runEffectPromise(fetchTweetMediaEffect(options));
|
|
25864
26871
|
}
|
|
25865
26872
|
function fetchTweetMediaEffect(options = {}) {
|
|
25866
|
-
return
|
|
26873
|
+
return Effect57.gen(function* () {
|
|
25867
26874
|
const now2 = options.now ?? Date.now;
|
|
25868
26875
|
const startedAt = now2();
|
|
25869
26876
|
const sleep = options.sleep ?? defaultSleep;
|
|
@@ -25930,7 +26937,7 @@ function fetchTweetMediaEffect(options = {}) {
|
|
|
25930
26937
|
retryMax,
|
|
25931
26938
|
userAgent,
|
|
25932
26939
|
maxBytes
|
|
25933
|
-
}).pipe(
|
|
26940
|
+
}).pipe(Effect57.map((fetched) => applyFetched(result, fetched)))
|
|
25934
26941
|
);
|
|
25935
26942
|
yield* runGroupEffect(
|
|
25936
26943
|
httpCandidates.filter((item) => item.kind === "image"),
|
|
@@ -25978,9 +26985,9 @@ function formatMediaFetchResult(result) {
|
|
|
25978
26985
|
}
|
|
25979
26986
|
|
|
25980
26987
|
// src/lib/whois.ts
|
|
25981
|
-
import { Effect as
|
|
26988
|
+
import { Effect as Effect58 } from "effect";
|
|
25982
26989
|
function trySync18(try_) {
|
|
25983
|
-
return
|
|
26990
|
+
return Effect58.try({
|
|
25984
26991
|
try: try_,
|
|
25985
26992
|
catch: (cause) => cause
|
|
25986
26993
|
});
|
|
@@ -26511,7 +27518,7 @@ function loadWhoisConversations(query, options, includeDms, context, limit) {
|
|
|
26511
27518
|
return [...merged.values()];
|
|
26512
27519
|
}
|
|
26513
27520
|
function runWhoisEffect(query, options = {}) {
|
|
26514
|
-
return
|
|
27521
|
+
return Effect58.gen(function* () {
|
|
26515
27522
|
const includeDms = options.dms ?? true;
|
|
26516
27523
|
const includeTweets = options.tweets ?? false;
|
|
26517
27524
|
const limit = options.limit ?? 10;
|
|
@@ -26758,7 +27765,7 @@ function registerSearchCommands({
|
|
|
26758
27765
|
parsePositiveIntegerOption: parsePositiveIntegerOption2
|
|
26759
27766
|
}) {
|
|
26760
27767
|
const searchCommand = program2.command("search").description("Search local data");
|
|
26761
|
-
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(
|
|
26762
27769
|
"--min-likes <n>",
|
|
26763
27770
|
"Override the low-quality like threshold (default 50)"
|
|
26764
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) => {
|
|
@@ -26768,10 +27775,18 @@ function registerSearchCommands({
|
|
|
26768
27775
|
);
|
|
26769
27776
|
if (options.minLikes !== void 0 && minLikes === void 0) return;
|
|
26770
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;
|
|
26771
27783
|
const replyFilter = options.replied ? "replied" : options.unreplied ? "unreplied" : "all";
|
|
26772
27784
|
print2(
|
|
26773
27785
|
listTimelineItems({
|
|
26774
27786
|
resource: options.resource === "mentions" ? "mentions" : options.resource === "authored" ? "authored" : "home",
|
|
27787
|
+
account: options.account,
|
|
27788
|
+
listAccountId: selectedList?.accountId,
|
|
27789
|
+
listId: selectedList?.listId,
|
|
26775
27790
|
search: query,
|
|
26776
27791
|
replyFilter,
|
|
26777
27792
|
since: options.since,
|
|
@@ -26954,7 +27969,7 @@ import { Readable as Readable2 } from "node:stream";
|
|
|
26954
27969
|
import { pathToFileURL } from "node:url";
|
|
26955
27970
|
|
|
26956
27971
|
// src/lib/http-effect.ts
|
|
26957
|
-
import { Effect as
|
|
27972
|
+
import { Effect as Effect59 } from "effect";
|
|
26958
27973
|
var LOCAL_WEB_PEER_HEADER = "x-birdclaw-local-peer";
|
|
26959
27974
|
|
|
26960
27975
|
// src/lib/production-server.ts
|
|
@@ -27228,7 +28243,7 @@ function registerStorageCommands({
|
|
|
27228
28243
|
}
|
|
27229
28244
|
|
|
27230
28245
|
// src/lib/authored-live.ts
|
|
27231
|
-
import { Effect as
|
|
28246
|
+
import { Effect as Effect60 } from "effect";
|
|
27232
28247
|
var AuthoredSyncError = class extends Error {
|
|
27233
28248
|
constructor(message, exitCode) {
|
|
27234
28249
|
super(message);
|
|
@@ -27470,10 +28485,10 @@ function resolveAuthoredIdentityEffect({
|
|
|
27470
28485
|
account,
|
|
27471
28486
|
db
|
|
27472
28487
|
}) {
|
|
27473
|
-
return
|
|
28488
|
+
return Effect60.gen(function* () {
|
|
27474
28489
|
const status = yield* liveTransportGateway.xurl.getTransportStatus();
|
|
27475
28490
|
if (status.availableTransport !== "xurl") {
|
|
27476
|
-
return yield*
|
|
28491
|
+
return yield* Effect60.fail(new AuthoredSyncError(status.statusText, 4));
|
|
27477
28492
|
}
|
|
27478
28493
|
const resolvedAccount = yield* trySync(
|
|
27479
28494
|
() => resolveLiveSyncAccount(db, account)
|
|
@@ -27488,7 +28503,7 @@ function resolveAuthoredIdentityEffect({
|
|
|
27488
28503
|
const authenticated = yield* liveTransportGateway.xurl.lookupAuthenticatedUser();
|
|
27489
28504
|
const authenticatedUser = userFromAuthenticatedPayload(authenticated);
|
|
27490
28505
|
if (!authenticatedUser?.id) {
|
|
27491
|
-
return yield*
|
|
28506
|
+
return yield* Effect60.fail(
|
|
27492
28507
|
new AuthoredSyncError(
|
|
27493
28508
|
"Could not resolve authenticated Twitter user id",
|
|
27494
28509
|
4
|
|
@@ -27496,7 +28511,7 @@ function resolveAuthoredIdentityEffect({
|
|
|
27496
28511
|
);
|
|
27497
28512
|
}
|
|
27498
28513
|
if (normalizeUsername(authenticatedUser.username) !== normalizeUsername(resolvedAccount.username)) {
|
|
27499
|
-
return yield*
|
|
28514
|
+
return yield* Effect60.fail(
|
|
27500
28515
|
new AuthoredSyncError(
|
|
27501
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.`,
|
|
27502
28517
|
4
|
|
@@ -27795,9 +28810,9 @@ function syncAuthoredTweetsEffect({
|
|
|
27795
28810
|
sinceId,
|
|
27796
28811
|
untilId
|
|
27797
28812
|
}) {
|
|
27798
|
-
return
|
|
28813
|
+
return Effect60.gen(function* () {
|
|
27799
28814
|
if (mode !== "xurl") {
|
|
27800
|
-
return yield*
|
|
28815
|
+
return yield* Effect60.fail(
|
|
27801
28816
|
new Error("authored sync only supports --mode xurl")
|
|
27802
28817
|
);
|
|
27803
28818
|
}
|
|
@@ -27860,7 +28875,7 @@ function syncAuthoredTweetsEffect({
|
|
|
27860
28875
|
sourceUser
|
|
27861
28876
|
})
|
|
27862
28877
|
).pipe(
|
|
27863
|
-
|
|
28878
|
+
Effect60.flatMap(
|
|
27864
28879
|
() => trySync(() => {
|
|
27865
28880
|
newestSeenId = maxTweetId(
|
|
27866
28881
|
newestSeenId,
|
|
@@ -27963,6 +28978,32 @@ function registerSyncCommands({
|
|
|
27963
28978
|
autoSyncAfterWrite: autoSyncAfterWrite2
|
|
27964
28979
|
}) {
|
|
27965
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
|
+
});
|
|
27966
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) => {
|
|
27967
29008
|
const result = await syncHomeTimeline({
|
|
27968
29009
|
account: options.account,
|
|
@@ -28137,6 +29178,7 @@ registerMentionCommands(commandContext);
|
|
|
28137
29178
|
registerDirectMessageCommands(commandContext);
|
|
28138
29179
|
registerSyncCommands(commandContext);
|
|
28139
29180
|
registerJobCommands(commandContext);
|
|
29181
|
+
registerListCommands(commandContext);
|
|
28140
29182
|
registerModerationCommands2(commandContext);
|
|
28141
29183
|
registerComposeCommands(commandContext);
|
|
28142
29184
|
registerInboxCommand(commandContext);
|