birdclaw 0.8.5 → 0.9.1
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 +34 -0
- package/README.md +23 -3
- package/dist/cli/birdclaw.js +1344 -190
- package/dist/client/assets/{AvatarChip-DLoqM5xD.js → AvatarChip-BN3b4MhM.js} +1 -1
- package/dist/client/assets/{ProfilePreview-DQX6_xc3.js → ProfilePreview-CHbT0U9a.js} +1 -1
- package/dist/client/assets/{SavedTimelineView-BhSiVyR1.js → SavedTimelineView-mGXE9EYJ.js} +1 -1
- package/dist/client/assets/{SmartTimestamp-BR0-9se2.js → SmartTimestamp-DG8XJKUz.js} +1 -1
- package/dist/client/assets/TimelineRouteFrame-BmMC7qLj.js +1 -0
- package/dist/client/assets/blocks-CawnB0mN.js +1 -0
- package/dist/client/assets/{bookmarks-LbYOsoKf.js → bookmarks-DZIMTKEp.js} +1 -1
- package/dist/client/assets/client-stream-contracts-DcKcyZhg.js +5 -0
- package/dist/client/assets/{data-sources-euTr6T_L.js → data-sources-BJUT2qaO.js} +1 -1
- package/dist/client/assets/{discuss-BWKu_p7B.js → discuss-DMPJVaRk.js} +1 -1
- package/dist/client/assets/dms-BfPFG4Qr.js +1 -0
- package/dist/client/assets/{inbox-BybSI5Vf.js → inbox-DmeYJ7tt.js} +1 -1
- package/dist/client/assets/{index-DZVisyna.js → index-Bqmo4MbK.js} +6 -6
- package/dist/client/assets/likes-BxVL-j_C.js +1 -0
- package/dist/client/assets/{links-CIwqK_dX.js → links-BeA1Wt8F.js} +1 -1
- package/dist/client/assets/{mapbox-B4zqX7r0.js → mapbox-BTSMNSry.js} +2 -2
- package/dist/client/assets/{mapbox-gl-DYNcLcll.js → mapbox-gl-CPemBIlO.js} +1 -1
- package/dist/client/assets/{mentions-BoVVAIT7.js → mentions-DBrY-UPg.js} +1 -1
- package/dist/client/assets/{network-map-zRysIY_b.js → network-map-MqbE164r.js} +2 -2
- package/dist/client/assets/{profile-analyze-C_yDeX6-.js → profile-analyze-DEA75Reh.js} +1 -1
- package/dist/client/assets/{profile-hydration-client-YEtJsNTv.js → profile-hydration-client-BNe0sUmK.js} +1 -1
- package/dist/client/assets/{profiles._handle-DdIVPqU-.js → profiles._handle-K34ofz9U.js} +1 -1
- package/dist/client/assets/{query-client-fYu2rzRC.js → query-client-DUP1ZzP7.js} +1 -1
- package/dist/client/assets/{rate-limits-BWPmPOSh.js → rate-limits-B8Wmpt9r.js} +1 -1
- package/dist/client/assets/{routes-CiLuYpZr.js → routes-Bl-Lw2hN.js} +1 -1
- package/dist/client/assets/schemas-CaALBBpc.js +64 -0
- package/dist/client/assets/styles-zihNNCGg.css +2 -0
- package/dist/client/assets/today-DFvZ0Ys5.js +1 -0
- package/dist/client/assets/{useTimelineRouteData-BaO3Rna1.js → useTimelineRouteData-Cpf-Jqsx.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-CVfs07C8.js} +24 -24
- 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/{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-CAvZOdfA.js} +437 -108
- package/dist/server/assets/{routes-B9aikOdB.js → routes-D9226vKz.js} +1 -1
- 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 +2 -2
- package/package.json +13 -13
- package/dist/client/assets/TimelineRouteFrame-9NDLnXzH.js +0 -1
- package/dist/client/assets/blocks-CFp_njvG.js +0 -1
- package/dist/client/assets/client-stream-contracts-sR4xMQxY.js +0 -5
- package/dist/client/assets/dms-SLXMtC4v.js +0 -1
- package/dist/client/assets/likes-dyh9p8y_.js +0 -1
- package/dist/client/assets/schemas-I9r6F6ya.js +0 -64
- package/dist/client/assets/styles-B8s3tt3r.css +0 -2
- package/dist/client/assets/today-BoO64YoY.js +0 -1
- package/dist/server/assets/SyncNowButton-BOn_IBXS.js +0 -76
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(
|
|
@@ -4811,6 +5043,18 @@ var DEFAULT_DELIMITER_HOLD = 8;
|
|
|
4811
5043
|
function toError3(error) {
|
|
4812
5044
|
return error instanceof Error ? error : new Error(String(error));
|
|
4813
5045
|
}
|
|
5046
|
+
var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
5047
|
+
function resolveOpenAIBaseUrl(getEnv2) {
|
|
5048
|
+
const configured = getEnv2("BIRDCLAW_OPENAI_BASE_URL") || getEnv2("OPENAI_BASE_URL");
|
|
5049
|
+
const base = configured?.trim() || DEFAULT_OPENAI_BASE_URL;
|
|
5050
|
+
return base.replace(/\/+$/, "");
|
|
5051
|
+
}
|
|
5052
|
+
function debugLog(getEnv2, message) {
|
|
5053
|
+
if (!getEnv2("BIRDCLAW_DEBUG")) return;
|
|
5054
|
+
if (typeof process === "undefined") return;
|
|
5055
|
+
process.stderr.write(`[birdclaw:openai] ${message}
|
|
5056
|
+
`);
|
|
5057
|
+
}
|
|
4814
5058
|
function createOpenAIStreamState() {
|
|
4815
5059
|
return {
|
|
4816
5060
|
eventBuffer: "",
|
|
@@ -4949,8 +5193,11 @@ function requestOpenAIResponseEffect({
|
|
|
4949
5193
|
if (!apiKey) {
|
|
4950
5194
|
return yield* Effect6.fail(new Error("OPENAI_API_KEY is not set"));
|
|
4951
5195
|
}
|
|
5196
|
+
const baseUrl = resolveOpenAIBaseUrl(runtime.env);
|
|
5197
|
+
const url = `${baseUrl}/responses`;
|
|
5198
|
+
debugLog(runtime.env, `POST ${url}`);
|
|
4952
5199
|
const response = yield* tryPromise(
|
|
4953
|
-
() => runtime.fetch(
|
|
5200
|
+
() => runtime.fetch(url, {
|
|
4954
5201
|
method: "POST",
|
|
4955
5202
|
signal,
|
|
4956
5203
|
headers: {
|
|
@@ -4959,17 +5206,29 @@ function requestOpenAIResponseEffect({
|
|
|
4959
5206
|
},
|
|
4960
5207
|
body: JSON.stringify(body)
|
|
4961
5208
|
})
|
|
4962
|
-
).pipe(
|
|
5209
|
+
).pipe(
|
|
5210
|
+
Effect6.mapError(toError3),
|
|
5211
|
+
Effect6.tapError(
|
|
5212
|
+
(error) => Effect6.sync(
|
|
5213
|
+
() => debugLog(runtime.env, `network error for ${url}: ${error.message}`)
|
|
5214
|
+
)
|
|
5215
|
+
)
|
|
5216
|
+
);
|
|
4963
5217
|
if (!response.ok) {
|
|
4964
5218
|
const text = yield* tryPromise(() => response.text()).pipe(
|
|
4965
5219
|
Effect6.mapError(toError3)
|
|
4966
5220
|
);
|
|
5221
|
+
debugLog(
|
|
5222
|
+
runtime.env,
|
|
5223
|
+
`${url} -> ${String(response.status)} ${text.slice(0, 400)}`
|
|
5224
|
+
);
|
|
4967
5225
|
return yield* Effect6.fail(
|
|
4968
5226
|
new Error(
|
|
4969
5227
|
`OpenAI request failed: ${String(response.status)} ${text.slice(0, 400)}`
|
|
4970
5228
|
)
|
|
4971
5229
|
);
|
|
4972
5230
|
}
|
|
5231
|
+
debugLog(runtime.env, `${url} -> ${String(response.status)} OK`);
|
|
4973
5232
|
return response;
|
|
4974
5233
|
});
|
|
4975
5234
|
}
|
|
@@ -6133,6 +6392,16 @@ function runBirdJsonCommandAllowFailureEffect(args, timeoutMs) {
|
|
|
6133
6392
|
})
|
|
6134
6393
|
);
|
|
6135
6394
|
}
|
|
6395
|
+
function runBirdTweetJsonCommandEffect(args, timeoutMs) {
|
|
6396
|
+
return runBirdJsonCommandEffect([...args, "--json-full"], timeoutMs).pipe(
|
|
6397
|
+
Effect8.catchAll((error) => {
|
|
6398
|
+
if (!isUnsupportedBirdOptionError(error, "--json-full")) {
|
|
6399
|
+
return Effect8.fail(error);
|
|
6400
|
+
}
|
|
6401
|
+
return runBirdJsonCommandEffect([...args, "--json"], timeoutMs);
|
|
6402
|
+
})
|
|
6403
|
+
);
|
|
6404
|
+
}
|
|
6136
6405
|
function getBirdTweetItems(payload, command) {
|
|
6137
6406
|
if (Array.isArray(payload)) {
|
|
6138
6407
|
return payload;
|
|
@@ -6197,19 +6466,63 @@ function toReferencedTweets(item) {
|
|
|
6197
6466
|
}
|
|
6198
6467
|
return references.length > 0 ? references : void 0;
|
|
6199
6468
|
}
|
|
6469
|
+
function getRecord(value) {
|
|
6470
|
+
return value && typeof value === "object" ? value : void 0;
|
|
6471
|
+
}
|
|
6472
|
+
function findGraphqlAvatarUrl(value, authorId) {
|
|
6473
|
+
const pending = [value];
|
|
6474
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6475
|
+
while (pending.length > 0) {
|
|
6476
|
+
const current = pending.pop();
|
|
6477
|
+
const record3 = getRecord(current);
|
|
6478
|
+
if (!record3 || seen.has(record3)) continue;
|
|
6479
|
+
seen.add(record3);
|
|
6480
|
+
if (record3.rest_id === authorId) {
|
|
6481
|
+
const avatar = getRecord(record3.avatar);
|
|
6482
|
+
if (typeof avatar?.image_url === "string" && avatar.image_url) {
|
|
6483
|
+
return avatar.image_url;
|
|
6484
|
+
}
|
|
6485
|
+
}
|
|
6486
|
+
for (const child of Object.values(record3)) {
|
|
6487
|
+
if (child && typeof child === "object") {
|
|
6488
|
+
pending.push(child);
|
|
6489
|
+
}
|
|
6490
|
+
}
|
|
6491
|
+
}
|
|
6492
|
+
return void 0;
|
|
6493
|
+
}
|
|
6494
|
+
function isHydratedBirdTweetItem(value) {
|
|
6495
|
+
const item = value;
|
|
6496
|
+
return typeof item?.id === "string" && typeof item.text === "string" && typeof item.createdAt === "string";
|
|
6497
|
+
}
|
|
6200
6498
|
function normalizeBirdTweets(items) {
|
|
6201
6499
|
const users = /* @__PURE__ */ new Map();
|
|
6202
|
-
const
|
|
6500
|
+
const includedTweets = /* @__PURE__ */ new Map();
|
|
6501
|
+
const normalizeItem = (item, preserveMissingMetrics = false) => {
|
|
6203
6502
|
const authorId = String(
|
|
6204
6503
|
item.authorId ?? item.author?.username ?? "unknown"
|
|
6205
6504
|
);
|
|
6206
|
-
|
|
6505
|
+
const profileImageUrl = item.author?.profileImageUrl ?? findGraphqlAvatarUrl(item._raw, authorId);
|
|
6506
|
+
const existingUser = users.get(authorId);
|
|
6507
|
+
if (!existingUser) {
|
|
6207
6508
|
users.set(authorId, {
|
|
6208
6509
|
id: authorId,
|
|
6209
6510
|
username: item.author?.username ?? `user_${authorId}`,
|
|
6210
|
-
name: item.author?.name ?? item.author?.username ?? `user_${authorId}
|
|
6511
|
+
name: item.author?.name ?? item.author?.username ?? `user_${authorId}`,
|
|
6512
|
+
...profileImageUrl ? { profile_image_url: profileImageUrl } : {}
|
|
6513
|
+
});
|
|
6514
|
+
} else if (profileImageUrl && !existingUser.profile_image_url) {
|
|
6515
|
+
users.set(authorId, {
|
|
6516
|
+
...existingUser,
|
|
6517
|
+
profile_image_url: profileImageUrl
|
|
6211
6518
|
});
|
|
6212
6519
|
}
|
|
6520
|
+
if (isHydratedBirdTweetItem(item.quotedTweet)) {
|
|
6521
|
+
const quotedTweet = normalizeItem(item.quotedTweet, true);
|
|
6522
|
+
if (quotedTweet.id !== item.id) {
|
|
6523
|
+
includedTweets.set(quotedTweet.id, quotedTweet);
|
|
6524
|
+
}
|
|
6525
|
+
}
|
|
6213
6526
|
return {
|
|
6214
6527
|
id: item.id,
|
|
6215
6528
|
author_id: authorId,
|
|
@@ -6218,17 +6531,29 @@ function normalizeBirdTweets(items) {
|
|
|
6218
6531
|
conversation_id: item.conversationId ?? item.id,
|
|
6219
6532
|
entities: toTweetEntities(item),
|
|
6220
6533
|
referenced_tweets: toReferencedTweets(item),
|
|
6221
|
-
public_metrics: {
|
|
6534
|
+
public_metrics: preserveMissingMetrics ? {
|
|
6535
|
+
...item.replyCount === void 0 ? {} : { reply_count: Number(item.replyCount) },
|
|
6536
|
+
...item.retweetCount === void 0 ? {} : { retweet_count: Number(item.retweetCount) },
|
|
6537
|
+
...item.likeCount === void 0 ? {} : { like_count: Number(item.likeCount) }
|
|
6538
|
+
} : {
|
|
6222
6539
|
reply_count: Number(item.replyCount ?? 0),
|
|
6223
6540
|
retweet_count: Number(item.retweetCount ?? 0),
|
|
6224
6541
|
like_count: Number(item.likeCount ?? 0)
|
|
6225
6542
|
},
|
|
6226
6543
|
edit_history_tweet_ids: [item.id]
|
|
6227
6544
|
};
|
|
6228
|
-
}
|
|
6545
|
+
};
|
|
6546
|
+
const data = items.map((item) => normalizeItem(item));
|
|
6547
|
+
const includes = {};
|
|
6548
|
+
if (users.size > 0) {
|
|
6549
|
+
includes.users = Array.from(users.values());
|
|
6550
|
+
}
|
|
6551
|
+
if (includedTweets.size > 0) {
|
|
6552
|
+
includes.tweets = Array.from(includedTweets.values());
|
|
6553
|
+
}
|
|
6229
6554
|
return {
|
|
6230
6555
|
data,
|
|
6231
|
-
includes: users.size > 0
|
|
6556
|
+
includes: users.size > 0 || includedTweets.size > 0 ? includes : void 0,
|
|
6232
6557
|
meta: {
|
|
6233
6558
|
result_count: data.length,
|
|
6234
6559
|
page_count: 1,
|
|
@@ -6260,11 +6585,10 @@ function listMentionsViaBirdEffect({
|
|
|
6260
6585
|
maxResults
|
|
6261
6586
|
}) {
|
|
6262
6587
|
return Effect8.gen(function* () {
|
|
6263
|
-
const stdout = yield*
|
|
6588
|
+
const stdout = yield* runBirdTweetJsonCommandEffect([
|
|
6264
6589
|
"mentions",
|
|
6265
6590
|
"-n",
|
|
6266
|
-
String(maxResults)
|
|
6267
|
-
"--json"
|
|
6591
|
+
String(maxResults)
|
|
6268
6592
|
]);
|
|
6269
6593
|
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6270
6594
|
return yield* normalizeBirdTweetsPayloadEffect(payload, "mentions");
|
|
@@ -6277,14 +6601,14 @@ function listTweetsViaBirdCommandEffect({
|
|
|
6277
6601
|
maxPages
|
|
6278
6602
|
}) {
|
|
6279
6603
|
return Effect8.gen(function* () {
|
|
6280
|
-
const args = [command, "-n", String(maxResults)
|
|
6604
|
+
const args = [command, "-n", String(maxResults)];
|
|
6281
6605
|
if (all) {
|
|
6282
6606
|
args.push("--all");
|
|
6607
|
+
if (maxPages !== void 0) {
|
|
6608
|
+
args.push("--max-pages", String(maxPages));
|
|
6609
|
+
}
|
|
6283
6610
|
}
|
|
6284
|
-
|
|
6285
|
-
args.push("--max-pages", String(maxPages));
|
|
6286
|
-
}
|
|
6287
|
-
const stdout = yield* runBirdJsonCommandEffect(args);
|
|
6611
|
+
const stdout = yield* runBirdTweetJsonCommandEffect(args);
|
|
6288
6612
|
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6289
6613
|
return yield* normalizeBirdTweetsPayloadEffect(payload, command);
|
|
6290
6614
|
});
|
|
@@ -6303,14 +6627,14 @@ function listBookmarkedTweetsViaBirdEffect(options) {
|
|
|
6303
6627
|
}
|
|
6304
6628
|
function searchTweetsViaBirdEffect(query, options) {
|
|
6305
6629
|
return Effect8.gen(function* () {
|
|
6306
|
-
const args = ["search", query, "-n", String(options.maxResults)
|
|
6630
|
+
const args = ["search", query, "-n", String(options.maxResults)];
|
|
6307
6631
|
if (options.all) {
|
|
6308
6632
|
args.push("--all");
|
|
6309
6633
|
}
|
|
6310
6634
|
if (options.all && options.maxPages !== void 0) {
|
|
6311
6635
|
args.push("--max-pages", String(options.maxPages));
|
|
6312
6636
|
}
|
|
6313
|
-
const stdout = yield*
|
|
6637
|
+
const stdout = yield* runBirdTweetJsonCommandEffect(args);
|
|
6314
6638
|
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6315
6639
|
return yield* normalizeBirdTweetsPayloadEffect(payload, "search");
|
|
6316
6640
|
});
|
|
@@ -6323,11 +6647,7 @@ function lookupTweetsByIdsViaBirdEffect(ids) {
|
|
|
6323
6647
|
const tweets = yield* Effect8.forEach(
|
|
6324
6648
|
ids,
|
|
6325
6649
|
(id) => Effect8.gen(function* () {
|
|
6326
|
-
const stdout = yield*
|
|
6327
|
-
"read",
|
|
6328
|
-
id,
|
|
6329
|
-
"--json"
|
|
6330
|
-
]);
|
|
6650
|
+
const stdout = yield* runBirdTweetJsonCommandEffect(["read", id]);
|
|
6331
6651
|
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6332
6652
|
return yield* normalizeBirdTweetItemEffect(payload, "read");
|
|
6333
6653
|
}),
|
|
@@ -6341,11 +6661,11 @@ function listHomeTimelineViaBirdEffect({
|
|
|
6341
6661
|
following = true
|
|
6342
6662
|
}) {
|
|
6343
6663
|
return Effect8.gen(function* () {
|
|
6344
|
-
const args = ["home", "-n", String(maxResults)
|
|
6664
|
+
const args = ["home", "-n", String(maxResults)];
|
|
6345
6665
|
if (following) {
|
|
6346
6666
|
args.push("--following");
|
|
6347
6667
|
}
|
|
6348
|
-
const stdout = yield*
|
|
6668
|
+
const stdout = yield* runBirdTweetJsonCommandEffect(args);
|
|
6349
6669
|
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6350
6670
|
return yield* normalizeBirdTweetsPayloadEffect(payload, "home");
|
|
6351
6671
|
});
|
|
@@ -6358,12 +6678,14 @@ function normalizeBirdFollowUsers(payload, command, maxResults) {
|
|
|
6358
6678
|
}
|
|
6359
6679
|
const data = users.map(toXurlMentionUser).filter((user) => Boolean(user));
|
|
6360
6680
|
const nextToken = !Array.isArray(rawPayload) && typeof rawPayload.nextCursor === "string" ? rawPayload.nextCursor : null;
|
|
6681
|
+
const paginationKnownComplete = Array.isArray(rawPayload) ? data.length < maxResults : nextToken === null;
|
|
6361
6682
|
return {
|
|
6362
6683
|
data,
|
|
6363
6684
|
meta: {
|
|
6364
6685
|
result_count: data.length,
|
|
6365
6686
|
page_count: data.length > 0 ? Math.max(1, Math.ceil(data.length / maxResults)) : 1,
|
|
6366
|
-
next_token: nextToken
|
|
6687
|
+
next_token: nextToken,
|
|
6688
|
+
pagination_known_complete: paginationKnownComplete
|
|
6367
6689
|
}
|
|
6368
6690
|
};
|
|
6369
6691
|
}
|
|
@@ -6400,6 +6722,73 @@ function listFollowUsersViaBirdEffect({
|
|
|
6400
6722
|
);
|
|
6401
6723
|
});
|
|
6402
6724
|
}
|
|
6725
|
+
function normalizeBirdLists(payload) {
|
|
6726
|
+
if (!Array.isArray(payload)) {
|
|
6727
|
+
throw new Error("bird lists returned unexpected JSON");
|
|
6728
|
+
}
|
|
6729
|
+
const data = payload.map((value) => {
|
|
6730
|
+
const item = value;
|
|
6731
|
+
if (!item.id || !item.name) return null;
|
|
6732
|
+
return {
|
|
6733
|
+
id: item.id,
|
|
6734
|
+
name: item.name,
|
|
6735
|
+
...item.description ? { description: item.description } : {},
|
|
6736
|
+
...typeof item.memberCount === "number" ? { memberCount: item.memberCount } : {},
|
|
6737
|
+
...typeof item.subscriberCount === "number" ? { followerCount: item.subscriberCount } : {},
|
|
6738
|
+
...typeof item.isPrivate === "boolean" ? { isPrivate: item.isPrivate } : {},
|
|
6739
|
+
...item.owner?.id ? { ownerId: item.owner.id } : {},
|
|
6740
|
+
...item.owner?.username ? { ownerUsername: item.owner.username } : {},
|
|
6741
|
+
...item.owner?.name ? { ownerName: item.owner.name } : {},
|
|
6742
|
+
...item.createdAt ? { createdAt: item.createdAt } : {},
|
|
6743
|
+
raw: value
|
|
6744
|
+
};
|
|
6745
|
+
}).filter((item) => Boolean(item));
|
|
6746
|
+
return { data, meta: { result_count: data.length, next_token: null } };
|
|
6747
|
+
}
|
|
6748
|
+
function listOwnedXListsViaBirdEffect({
|
|
6749
|
+
maxResults
|
|
6750
|
+
}) {
|
|
6751
|
+
return Effect8.gen(function* () {
|
|
6752
|
+
const stdout = yield* runBirdJsonCommandEffect([
|
|
6753
|
+
"lists",
|
|
6754
|
+
"-n",
|
|
6755
|
+
String(maxResults),
|
|
6756
|
+
"--json"
|
|
6757
|
+
]);
|
|
6758
|
+
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6759
|
+
return yield* Effect8.try({
|
|
6760
|
+
try: () => normalizeBirdLists(payload),
|
|
6761
|
+
catch: (error) => error
|
|
6762
|
+
});
|
|
6763
|
+
});
|
|
6764
|
+
}
|
|
6765
|
+
function listXListMembersViaBirdEffect({
|
|
6766
|
+
listId,
|
|
6767
|
+
maxResults,
|
|
6768
|
+
maxPages = 1
|
|
6769
|
+
}) {
|
|
6770
|
+
return Effect8.gen(function* () {
|
|
6771
|
+
const args = ["list-members", listId, "-n", String(maxResults), "--json"];
|
|
6772
|
+
if (maxPages > 1) {
|
|
6773
|
+
args.push("--all", "--max-pages", String(maxPages));
|
|
6774
|
+
}
|
|
6775
|
+
const stdout = yield* runBirdJsonCommandEffect(args);
|
|
6776
|
+
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6777
|
+
const normalized = yield* normalizeBirdFollowUsersEffect(
|
|
6778
|
+
payload,
|
|
6779
|
+
"list-members",
|
|
6780
|
+
maxResults
|
|
6781
|
+
);
|
|
6782
|
+
if (Array.isArray(payload)) {
|
|
6783
|
+
normalized.meta = {
|
|
6784
|
+
...normalized.meta,
|
|
6785
|
+
pagination_known_complete: false,
|
|
6786
|
+
pagination_inferred_complete: normalized.data.length < maxResults
|
|
6787
|
+
};
|
|
6788
|
+
}
|
|
6789
|
+
return normalized;
|
|
6790
|
+
});
|
|
6791
|
+
}
|
|
6403
6792
|
function listThreadViaBirdEffect({
|
|
6404
6793
|
tweetId,
|
|
6405
6794
|
all,
|
|
@@ -6407,14 +6796,14 @@ function listThreadViaBirdEffect({
|
|
|
6407
6796
|
timeoutMs
|
|
6408
6797
|
}) {
|
|
6409
6798
|
return Effect8.gen(function* () {
|
|
6410
|
-
const args = ["thread", tweetId
|
|
6799
|
+
const args = ["thread", tweetId];
|
|
6411
6800
|
if (all) {
|
|
6412
6801
|
args.push("--all");
|
|
6413
6802
|
}
|
|
6414
6803
|
if (maxPages !== void 0) {
|
|
6415
6804
|
args.push("--max-pages", String(maxPages));
|
|
6416
6805
|
}
|
|
6417
|
-
const stdout = yield*
|
|
6806
|
+
const stdout = yield* runBirdTweetJsonCommandEffect(args, timeoutMs);
|
|
6418
6807
|
const payload = yield* parseBirdJsonEffect(stdout);
|
|
6419
6808
|
return yield* normalizeBirdTweetsPayloadEffect(payload, "thread");
|
|
6420
6809
|
});
|
|
@@ -6447,9 +6836,11 @@ function parseBirdWhoami(stdout) {
|
|
|
6447
6836
|
}
|
|
6448
6837
|
return line.match(/^\D*(\d{2,})\D*$/)?.[1];
|
|
6449
6838
|
}).find((value) => Boolean(value));
|
|
6839
|
+
const name = stdout.match(/@[A-Za-z0-9_]{1,15}\s*\(([^)]+)\)/)?.[1]?.trim();
|
|
6450
6840
|
return {
|
|
6451
6841
|
username: usernameMatch[1],
|
|
6452
|
-
...id ? { id } : {}
|
|
6842
|
+
...id ? { id } : {},
|
|
6843
|
+
...name ? { name } : {}
|
|
6453
6844
|
};
|
|
6454
6845
|
}
|
|
6455
6846
|
function getAuthenticatedBirdAccountEffect() {
|
|
@@ -6461,6 +6852,9 @@ function getAuthenticatedBirdAccountEffect() {
|
|
|
6461
6852
|
});
|
|
6462
6853
|
});
|
|
6463
6854
|
}
|
|
6855
|
+
function getAuthenticatedBirdAccount() {
|
|
6856
|
+
return runEffectPromise(getAuthenticatedBirdAccountEffect());
|
|
6857
|
+
}
|
|
6464
6858
|
function listDirectMessagesViaBirdEffect({
|
|
6465
6859
|
maxResults,
|
|
6466
6860
|
inbox = "all",
|
|
@@ -7104,6 +7498,71 @@ function lookupUsersByIdsEffect(ids) {
|
|
|
7104
7498
|
function lookupUsersByIds(ids) {
|
|
7105
7499
|
return runEffectPromise(lookupUsersByIdsEffect(ids));
|
|
7106
7500
|
}
|
|
7501
|
+
function normalizeXListPage(payload) {
|
|
7502
|
+
const rawItems = Array.isArray(payload.data) ? payload.data : [];
|
|
7503
|
+
const data = rawItems.map((value) => {
|
|
7504
|
+
const item = value;
|
|
7505
|
+
if (typeof item.id !== "string" || typeof item.name !== "string") {
|
|
7506
|
+
return null;
|
|
7507
|
+
}
|
|
7508
|
+
return {
|
|
7509
|
+
id: item.id,
|
|
7510
|
+
name: item.name,
|
|
7511
|
+
...typeof item.description === "string" ? { description: item.description } : {},
|
|
7512
|
+
...typeof item.member_count === "number" ? { memberCount: item.member_count } : {},
|
|
7513
|
+
...typeof item.follower_count === "number" ? { followerCount: item.follower_count } : {},
|
|
7514
|
+
...typeof item.private === "boolean" ? { isPrivate: item.private } : {},
|
|
7515
|
+
...typeof item.owner_id === "string" ? { ownerId: item.owner_id } : {},
|
|
7516
|
+
...typeof item.created_at === "string" ? { createdAt: item.created_at } : {},
|
|
7517
|
+
raw: item
|
|
7518
|
+
};
|
|
7519
|
+
}).filter((item) => Boolean(item));
|
|
7520
|
+
const meta = payload.meta && typeof payload.meta === "object" ? payload.meta : {};
|
|
7521
|
+
return {
|
|
7522
|
+
data,
|
|
7523
|
+
meta: {
|
|
7524
|
+
result_count: data.length,
|
|
7525
|
+
...typeof meta.next_token === "string" ? { next_token: meta.next_token } : { next_token: null }
|
|
7526
|
+
}
|
|
7527
|
+
};
|
|
7528
|
+
}
|
|
7529
|
+
function listOwnedXListsViaXurlEffect({
|
|
7530
|
+
userId,
|
|
7531
|
+
username,
|
|
7532
|
+
maxResults,
|
|
7533
|
+
paginationToken
|
|
7534
|
+
}) {
|
|
7535
|
+
const query = new URLSearchParams({
|
|
7536
|
+
max_results: String(maxResults),
|
|
7537
|
+
"list.fields": "created_at,description,follower_count,member_count,name,owner_id,private"
|
|
7538
|
+
});
|
|
7539
|
+
if (paginationToken) query.set("pagination_token", paginationToken);
|
|
7540
|
+
return runOAuth2JsonCommandEffect({
|
|
7541
|
+
args: [`/2/users/${userId}/owned_lists?${query.toString()}`],
|
|
7542
|
+
username
|
|
7543
|
+
}).pipe(Effect9.map(normalizeXListPage));
|
|
7544
|
+
}
|
|
7545
|
+
function listXListMembersViaXurlEffect({
|
|
7546
|
+
listId,
|
|
7547
|
+
username,
|
|
7548
|
+
maxResults,
|
|
7549
|
+
paginationToken
|
|
7550
|
+
}) {
|
|
7551
|
+
const query = new URLSearchParams({
|
|
7552
|
+
max_results: String(maxResults),
|
|
7553
|
+
"user.fields": RICH_USER_FIELDS
|
|
7554
|
+
});
|
|
7555
|
+
if (paginationToken) query.set("pagination_token", paginationToken);
|
|
7556
|
+
return runOAuth2JsonCommandEffect({
|
|
7557
|
+
args: [`/2/lists/${listId}/members?${query.toString()}`],
|
|
7558
|
+
username
|
|
7559
|
+
}).pipe(
|
|
7560
|
+
Effect9.map((payload) => ({
|
|
7561
|
+
data: Array.isArray(payload.data) ? payload.data : [],
|
|
7562
|
+
meta: payload.meta && typeof payload.meta === "object" ? payload.meta : {}
|
|
7563
|
+
}))
|
|
7564
|
+
);
|
|
7565
|
+
}
|
|
7107
7566
|
function lookupUsersByHandlesEffect(handles, options = {}) {
|
|
7108
7567
|
if (handles.length === 0) {
|
|
7109
7568
|
return Effect9.succeed([]);
|
|
@@ -9660,6 +10119,16 @@ function ensureStubProfileForXUser(db, externalUserId) {
|
|
|
9660
10119
|
function getReferencedTweetId(tweet, type) {
|
|
9661
10120
|
return tweet.referenced_tweets?.find((item) => item.type === type)?.id ?? null;
|
|
9662
10121
|
}
|
|
10122
|
+
function toCanonicalTweets(payload) {
|
|
10123
|
+
const tweetsById = /* @__PURE__ */ new Map();
|
|
10124
|
+
for (const tweet of payload.includes?.tweets ?? []) {
|
|
10125
|
+
tweetsById.set(tweet.id, tweet);
|
|
10126
|
+
}
|
|
10127
|
+
for (const tweet of payload.data) {
|
|
10128
|
+
tweetsById.set(tweet.id, tweet);
|
|
10129
|
+
}
|
|
10130
|
+
return tweetsById.values();
|
|
10131
|
+
}
|
|
9663
10132
|
function replaceTweetFts(db, tweetId, text) {
|
|
9664
10133
|
db.prepare("delete from tweets_fts where tweet_id = ?").run(tweetId);
|
|
9665
10134
|
db.prepare("insert into tweets_fts (tweet_id, text) values (?, ?)").run(
|
|
@@ -9689,7 +10158,7 @@ function ingestTweetPayload(db, {
|
|
|
9689
10158
|
created_at = excluded.created_at,
|
|
9690
10159
|
is_replied = max(tweets.is_replied, excluded.is_replied),
|
|
9691
10160
|
reply_to_id = coalesce(tweets.reply_to_id, excluded.reply_to_id),
|
|
9692
|
-
like_count = excluded.like_count,
|
|
10161
|
+
like_count = case when ? then tweets.like_count else excluded.like_count end,
|
|
9693
10162
|
media_count = max(tweets.media_count, excluded.media_count),
|
|
9694
10163
|
entities_json = excluded.entities_json,
|
|
9695
10164
|
media_json = case
|
|
@@ -9710,25 +10179,29 @@ function ingestTweetPayload(db, {
|
|
|
9710
10179
|
const tweetIds = [];
|
|
9711
10180
|
db.transaction(() => {
|
|
9712
10181
|
const observedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9713
|
-
|
|
10182
|
+
const primaryTweetIds = new Set(payload.data.map((tweet) => tweet.id));
|
|
10183
|
+
for (const tweet of toCanonicalTweets(payload)) {
|
|
10184
|
+
const isPrimaryTweet = primaryTweetIds.has(tweet.id);
|
|
9714
10185
|
const author = usersById.get(tweet.author_id);
|
|
9715
10186
|
const profile = author ? upsertProfileFromXUser(db, author) : ensureStubProfileForXUser(db, tweet.author_id);
|
|
9716
10187
|
const replyToId = getReferencedTweetId(tweet, "replied_to");
|
|
9717
10188
|
const quotedTweetId = getReferencedTweetId(tweet, "quoted");
|
|
10189
|
+
const shouldMarkReplied = isPrimaryTweet && markRepliesAsReplied && Boolean(replyToId);
|
|
9718
10190
|
upsertTweet.run(
|
|
9719
10191
|
tweet.id,
|
|
9720
10192
|
profile.profile.id,
|
|
9721
10193
|
tweet.text,
|
|
9722
10194
|
tweet.created_at,
|
|
9723
|
-
|
|
10195
|
+
shouldMarkReplied ? 1 : 0,
|
|
9724
10196
|
replyToId,
|
|
9725
10197
|
Number(tweet.public_metrics?.like_count ?? 0),
|
|
9726
10198
|
countTweetMedia(tweet),
|
|
9727
10199
|
JSON.stringify(tweetEntitiesFromXurl(tweet.entities)),
|
|
9728
10200
|
buildMediaJsonFromIncludes(tweet, payload.includes?.media),
|
|
9729
|
-
quotedTweetId
|
|
10201
|
+
quotedTweetId,
|
|
10202
|
+
!isPrimaryTweet && tweet.public_metrics?.like_count === void 0 ? 1 : 0
|
|
9730
10203
|
);
|
|
9731
|
-
if (edgeKind) {
|
|
10204
|
+
if (edgeKind && isPrimaryTweet) {
|
|
9732
10205
|
upsertTweetAccountEdge(db, {
|
|
9733
10206
|
accountId,
|
|
9734
10207
|
tweetId: tweet.id,
|
|
@@ -9738,16 +10211,20 @@ function ingestTweetPayload(db, {
|
|
|
9738
10211
|
rawJson: JSON.stringify(tweet)
|
|
9739
10212
|
});
|
|
9740
10213
|
}
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
10214
|
+
if (isPrimaryTweet) {
|
|
10215
|
+
upsertCollection?.run(
|
|
10216
|
+
accountId,
|
|
10217
|
+
tweet.id,
|
|
10218
|
+
collectionKind2,
|
|
10219
|
+
source,
|
|
10220
|
+
JSON.stringify(tweet),
|
|
10221
|
+
observedAt
|
|
10222
|
+
);
|
|
10223
|
+
}
|
|
9749
10224
|
replaceTweetFts(db, tweet.id, tweet.text);
|
|
9750
|
-
|
|
10225
|
+
if (isPrimaryTweet) {
|
|
10226
|
+
tweetIds.push(tweet.id);
|
|
10227
|
+
}
|
|
9751
10228
|
}
|
|
9752
10229
|
})();
|
|
9753
10230
|
return tweetIds;
|
|
@@ -10719,6 +11196,8 @@ var RECENT_TIMELINE_EDGE_CANDIDATES = 5e3;
|
|
|
10719
11196
|
function listTimelineItems({
|
|
10720
11197
|
resource,
|
|
10721
11198
|
account,
|
|
11199
|
+
listAccountId,
|
|
11200
|
+
listId,
|
|
10722
11201
|
search,
|
|
10723
11202
|
replyFilter = "all",
|
|
10724
11203
|
since,
|
|
@@ -10749,7 +11228,7 @@ function listTimelineItems({
|
|
|
10749
11228
|
let join3 = "";
|
|
10750
11229
|
let where = "where e.kind = ?";
|
|
10751
11230
|
let searchSnippetSelect = "";
|
|
10752
|
-
const canUseRecentEdgeWindow = !likedOnly && !bookmarkedOnly && !account && !search?.trim() && replyFilter === "all" && !since?.trim() && !until?.trim() && includeReplies && qualityFilter === "all";
|
|
11231
|
+
const canUseRecentEdgeWindow = !likedOnly && !bookmarkedOnly && !listId && !account && !search?.trim() && replyFilter === "all" && !since?.trim() && !until?.trim() && includeReplies && qualityFilter === "all";
|
|
10753
11232
|
if (likedOnly || bookmarkedOnly) {
|
|
10754
11233
|
if (likedOnly && bookmarkedOnly) {
|
|
10755
11234
|
timelineEdgesCte = `
|
|
@@ -10845,6 +11324,22 @@ function listTimelineItems({
|
|
|
10845
11324
|
params.push(until.trim());
|
|
10846
11325
|
}
|
|
10847
11326
|
}
|
|
11327
|
+
if (Boolean(listId) !== Boolean(listAccountId)) {
|
|
11328
|
+
throw new Error("List filtering requires both listId and listAccountId");
|
|
11329
|
+
}
|
|
11330
|
+
if (listId && listAccountId) {
|
|
11331
|
+
where += `
|
|
11332
|
+
and exists (
|
|
11333
|
+
select 1
|
|
11334
|
+
from x_list_members list_member
|
|
11335
|
+
where list_member.account_id = ?
|
|
11336
|
+
and list_member.list_id = ?
|
|
11337
|
+
and list_member.profile_id = t.author_profile_id
|
|
11338
|
+
and list_member.current = 1
|
|
11339
|
+
)
|
|
11340
|
+
`;
|
|
11341
|
+
params.push(listAccountId, listId);
|
|
11342
|
+
}
|
|
10848
11343
|
const ftsSearch = search?.trim() ? toFtsSearchQuery(search) : "";
|
|
10849
11344
|
if (ftsSearch) {
|
|
10850
11345
|
join3 += " join tweets_fts on tweets_fts.tweet_id = t.id ";
|
|
@@ -12518,6 +13013,7 @@ function reachedStartTimeBoundary(payload, startTimeMs) {
|
|
|
12518
13013
|
function mergeTimelinePayloads(payloads, limit) {
|
|
12519
13014
|
const data = [];
|
|
12520
13015
|
const usersById = /* @__PURE__ */ new Map();
|
|
13016
|
+
const tweetsById = /* @__PURE__ */ new Map();
|
|
12521
13017
|
const mediaByKey = /* @__PURE__ */ new Map();
|
|
12522
13018
|
let meta;
|
|
12523
13019
|
for (const payload of payloads) {
|
|
@@ -12530,6 +13026,9 @@ function mergeTimelinePayloads(payloads, limit) {
|
|
|
12530
13026
|
for (const user of payload.includes?.users ?? []) {
|
|
12531
13027
|
usersById.set(user.id, user);
|
|
12532
13028
|
}
|
|
13029
|
+
for (const tweet of payload.includes?.tweets ?? []) {
|
|
13030
|
+
tweetsById.set(tweet.id, tweet);
|
|
13031
|
+
}
|
|
12533
13032
|
for (const media of payload.includes?.media ?? []) {
|
|
12534
13033
|
mediaByKey.set(media.media_key, media);
|
|
12535
13034
|
}
|
|
@@ -12539,6 +13038,7 @@ function mergeTimelinePayloads(payloads, limit) {
|
|
|
12539
13038
|
data,
|
|
12540
13039
|
includes: {
|
|
12541
13040
|
users: [...usersById.values()],
|
|
13041
|
+
tweets: [...tweetsById.values()],
|
|
12542
13042
|
media: [...mediaByKey.values()]
|
|
12543
13043
|
},
|
|
12544
13044
|
meta
|
|
@@ -12847,6 +13347,7 @@ function compactTweet(source, item) {
|
|
|
12847
13347
|
createdAt: item.createdAt,
|
|
12848
13348
|
text: item.text,
|
|
12849
13349
|
entities: item.entities,
|
|
13350
|
+
media: item.media,
|
|
12850
13351
|
likeCount: item.likeCount,
|
|
12851
13352
|
liked: item.liked,
|
|
12852
13353
|
bookmarked: item.bookmarked,
|
|
@@ -12866,6 +13367,7 @@ function compactEmbeddedTweet(item) {
|
|
|
12866
13367
|
createdAt: item.createdAt,
|
|
12867
13368
|
text: item.text,
|
|
12868
13369
|
entities: item.entities,
|
|
13370
|
+
media: item.media,
|
|
12869
13371
|
likeCount: item.likeCount ?? 0,
|
|
12870
13372
|
liked: Boolean(item.liked),
|
|
12871
13373
|
bookmarked: Boolean(item.bookmarked),
|
|
@@ -15390,6 +15892,8 @@ function compactTweet2(source, item) {
|
|
|
15390
15892
|
authorProfile: item.author,
|
|
15391
15893
|
createdAt: item.createdAt,
|
|
15392
15894
|
text: item.text,
|
|
15895
|
+
entities: item.entities,
|
|
15896
|
+
media: item.media,
|
|
15393
15897
|
likeCount: item.likeCount,
|
|
15394
15898
|
liked: item.liked,
|
|
15395
15899
|
bookmarked: item.bookmarked,
|
|
@@ -15427,7 +15931,9 @@ function collectLiveSearchTweets(options) {
|
|
|
15427
15931
|
t.created_at,
|
|
15428
15932
|
t.is_replied,
|
|
15429
15933
|
t.like_count,
|
|
15430
|
-
|
|
15934
|
+
t.media_count,
|
|
15935
|
+
t.entities_json,
|
|
15936
|
+
t.media_json,
|
|
15431
15937
|
case
|
|
15432
15938
|
when exists (
|
|
15433
15939
|
select 1 from tweet_collections collection
|
|
@@ -15481,6 +15987,8 @@ function collectLiveSearchTweets(options) {
|
|
|
15481
15987
|
},
|
|
15482
15988
|
createdAt: String(row.created_at),
|
|
15483
15989
|
text: String(row.text),
|
|
15990
|
+
entities: parseJsonField2(row.entities_json, {}),
|
|
15991
|
+
media: parseJsonField2(row.media_json, []),
|
|
15484
15992
|
likeCount: Number(row.like_count),
|
|
15485
15993
|
liked: Boolean(row.liked),
|
|
15486
15994
|
bookmarked: Boolean(row.bookmarked),
|
|
@@ -18837,14 +19345,77 @@ function trySync7(try_) {
|
|
|
18837
19345
|
catch: toError11
|
|
18838
19346
|
});
|
|
18839
19347
|
}
|
|
19348
|
+
var SEEDED_ACCOUNT_HANDLE = "steipete";
|
|
19349
|
+
var SEEDED_ACCOUNT_EXTERNAL_USER_ID = "25401953";
|
|
19350
|
+
function hydrateAccountFromBirdEffect() {
|
|
19351
|
+
return Effect34.gen(function* () {
|
|
19352
|
+
const account = yield* tryPromise(() => getAuthenticatedBirdAccount()).pipe(
|
|
19353
|
+
Effect34.catchAll(() => Effect34.succeed(null))
|
|
19354
|
+
);
|
|
19355
|
+
if (!account?.username) return false;
|
|
19356
|
+
const handle = account.username.replace(/^@/, "");
|
|
19357
|
+
const name = account.name?.trim() || null;
|
|
19358
|
+
const externalUserId = account.id ?? null;
|
|
19359
|
+
const hydrated = yield* trySync7(() => {
|
|
19360
|
+
const db = getNativeDb();
|
|
19361
|
+
return db.transaction(() => {
|
|
19362
|
+
const current = db.prepare(
|
|
19363
|
+
`select handle, external_user_id, transport from accounts where id = 'acct_primary'`
|
|
19364
|
+
).get();
|
|
19365
|
+
if (!current) return false;
|
|
19366
|
+
const storedHandle = current?.handle?.replace(/^@/, "") ?? null;
|
|
19367
|
+
const handleMatches = storedHandle?.toLowerCase() === handle.toLowerCase();
|
|
19368
|
+
const identityMatches = externalUserId !== null && current.external_user_id !== null ? externalUserId === current.external_user_id : handleMatches;
|
|
19369
|
+
const isSeededPlaceholder = storedHandle?.toLowerCase() === SEEDED_ACCOUNT_HANDLE && current.external_user_id === SEEDED_ACCOUNT_EXTERNAL_USER_ID && current.transport === "xurl";
|
|
19370
|
+
if (!identityMatches && !isSeededPlaceholder) return false;
|
|
19371
|
+
const identityChanged = !identityMatches;
|
|
19372
|
+
if (identityChanged) {
|
|
19373
|
+
db.prepare(
|
|
19374
|
+
`update profiles
|
|
19375
|
+
set handle = ?,
|
|
19376
|
+
display_name = coalesce(?, display_name),
|
|
19377
|
+
avatar_url = null
|
|
19378
|
+
where id = 'profile_me'`
|
|
19379
|
+
).run(handle, name);
|
|
19380
|
+
db.prepare(
|
|
19381
|
+
`update accounts
|
|
19382
|
+
set handle = ?,
|
|
19383
|
+
name = coalesce(?, name),
|
|
19384
|
+
transport = 'bird',
|
|
19385
|
+
external_user_id = ?
|
|
19386
|
+
where id = 'acct_primary'`
|
|
19387
|
+
).run(`@${handle}`, name, externalUserId);
|
|
19388
|
+
} else {
|
|
19389
|
+
db.prepare(
|
|
19390
|
+
`update profiles
|
|
19391
|
+
set handle = ?,
|
|
19392
|
+
display_name = coalesce(?, display_name)
|
|
19393
|
+
where id = 'profile_me'`
|
|
19394
|
+
).run(handle, name);
|
|
19395
|
+
db.prepare(
|
|
19396
|
+
`update accounts
|
|
19397
|
+
set handle = ?,
|
|
19398
|
+
name = coalesce(?, name),
|
|
19399
|
+
transport = 'bird',
|
|
19400
|
+
external_user_id = coalesce(?, external_user_id)
|
|
19401
|
+
where id = 'acct_primary'`
|
|
19402
|
+
).run(`@${handle}`, name, externalUserId);
|
|
19403
|
+
}
|
|
19404
|
+
return true;
|
|
19405
|
+
})();
|
|
19406
|
+
});
|
|
19407
|
+
return hydrated;
|
|
19408
|
+
});
|
|
19409
|
+
}
|
|
18840
19410
|
function hydrateProfilesFromXEffect() {
|
|
18841
19411
|
return Effect34.gen(function* () {
|
|
18842
19412
|
const transport = yield* tryPromise(() => getTransportStatus());
|
|
18843
19413
|
if (transport.availableTransport !== "xurl") {
|
|
19414
|
+
const hydratedAccount2 = yield* hydrateAccountFromBirdEffect();
|
|
18844
19415
|
return {
|
|
18845
19416
|
ok: true,
|
|
18846
19417
|
hydratedProfiles: 0,
|
|
18847
|
-
hydratedAccount:
|
|
19418
|
+
hydratedAccount: hydratedAccount2,
|
|
18848
19419
|
reason: transport.statusText
|
|
18849
19420
|
};
|
|
18850
19421
|
}
|
|
@@ -21918,6 +22489,7 @@ import { Effect as Effect42 } from "effect";
|
|
|
21918
22489
|
|
|
21919
22490
|
// src/lib/openai.ts
|
|
21920
22491
|
import { Effect as Effect41 } from "effect";
|
|
22492
|
+
var getEnv = (name) => process.env[name];
|
|
21921
22493
|
function clampScore(value) {
|
|
21922
22494
|
return Math.max(0, Math.min(100, Math.round(value)));
|
|
21923
22495
|
}
|
|
@@ -21937,8 +22509,11 @@ function scoreInboxItemWithOpenAIEffect(input) {
|
|
|
21937
22509
|
return yield* Effect41.fail(new Error("OPENAI_API_KEY is not set"));
|
|
21938
22510
|
}
|
|
21939
22511
|
const model = process.env.BIRDCLAW_OPENAI_MODEL || "gpt-5.2";
|
|
22512
|
+
const baseUrl = resolveOpenAIBaseUrl(getEnv);
|
|
22513
|
+
const url = `${baseUrl}/chat/completions`;
|
|
22514
|
+
debugLog(getEnv, `POST ${url} (model=${model})`);
|
|
21940
22515
|
const response = yield* tryPromise(
|
|
21941
|
-
() => fetch(
|
|
22516
|
+
() => fetch(url, {
|
|
21942
22517
|
method: "POST",
|
|
21943
22518
|
headers: {
|
|
21944
22519
|
authorization: `Bearer ${apiKey}`,
|
|
@@ -21959,10 +22534,23 @@ function scoreInboxItemWithOpenAIEffect(input) {
|
|
|
21959
22534
|
]
|
|
21960
22535
|
})
|
|
21961
22536
|
})
|
|
21962
|
-
).pipe(
|
|
22537
|
+
).pipe(
|
|
22538
|
+
Effect41.mapError(toError15),
|
|
22539
|
+
Effect41.tapError(
|
|
22540
|
+
(error) => Effect41.sync(
|
|
22541
|
+
() => debugLog(getEnv, `network error for ${url}: ${error.message}`)
|
|
22542
|
+
)
|
|
22543
|
+
)
|
|
22544
|
+
);
|
|
21963
22545
|
if (!response.ok) {
|
|
22546
|
+
const text = yield* tryPromise(() => response.text()).pipe(
|
|
22547
|
+
Effect41.mapError(toError15)
|
|
22548
|
+
);
|
|
22549
|
+
debugLog(getEnv, `${url} -> ${response.status} ${text.slice(0, 400)}`);
|
|
21964
22550
|
return yield* Effect41.fail(
|
|
21965
|
-
new Error(
|
|
22551
|
+
new Error(
|
|
22552
|
+
`OpenAI request failed: ${response.status} ${text.slice(0, 400)}`
|
|
22553
|
+
)
|
|
21966
22554
|
);
|
|
21967
22555
|
}
|
|
21968
22556
|
const payload = yield* tryPromise(() => response.json()).pipe(
|
|
@@ -23371,19 +23959,550 @@ function registerJobCommands({ program: program2, print: print2 }) {
|
|
|
23371
23959
|
});
|
|
23372
23960
|
}
|
|
23373
23961
|
|
|
23962
|
+
// src/lib/x-lists.ts
|
|
23963
|
+
import { Effect as Effect47 } from "effect";
|
|
23964
|
+
var DEFAULT_MAX_LISTS = 20;
|
|
23965
|
+
var DEFAULT_MEMBER_LIMIT = 20;
|
|
23966
|
+
var DEFAULT_MAX_MEMBER_PAGES = 1;
|
|
23967
|
+
var DEFAULT_DELAY_MS2 = 1e3;
|
|
23968
|
+
var MAX_LISTS = 100;
|
|
23969
|
+
var MAX_MEMBER_LIMIT = 100;
|
|
23970
|
+
var MAX_MEMBER_PAGES = 100;
|
|
23971
|
+
function positiveInteger(name, value, maximum) {
|
|
23972
|
+
if (!Number.isFinite(value) || value < 1 || value > maximum) {
|
|
23973
|
+
throw new Error(`${name} must be between 1 and ${String(maximum)}`);
|
|
23974
|
+
}
|
|
23975
|
+
return Math.floor(value);
|
|
23976
|
+
}
|
|
23977
|
+
function nonNegativeInteger(name, value) {
|
|
23978
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
23979
|
+
throw new Error(`${name} must be a non-negative integer`);
|
|
23980
|
+
}
|
|
23981
|
+
return Math.floor(value);
|
|
23982
|
+
}
|
|
23983
|
+
function normalizedMode(value) {
|
|
23984
|
+
if (value === void 0) return "auto";
|
|
23985
|
+
if (value === "auto" || value === "bird" || value === "xurl") return value;
|
|
23986
|
+
throw new Error("--mode must be auto, bird, or xurl");
|
|
23987
|
+
}
|
|
23988
|
+
function metaString(meta, key) {
|
|
23989
|
+
const value = meta?.[key];
|
|
23990
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
23991
|
+
}
|
|
23992
|
+
function metaNumber(meta, key) {
|
|
23993
|
+
const value = meta?.[key];
|
|
23994
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
23995
|
+
}
|
|
23996
|
+
function parseJsonRecord(value) {
|
|
23997
|
+
if (typeof value !== "string" || value.length === 0) return {};
|
|
23998
|
+
try {
|
|
23999
|
+
const parsed = JSON.parse(value);
|
|
24000
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
24001
|
+
} catch {
|
|
24002
|
+
return {};
|
|
24003
|
+
}
|
|
24004
|
+
}
|
|
24005
|
+
function fetchOwnedListsEffect({
|
|
24006
|
+
source,
|
|
24007
|
+
account,
|
|
24008
|
+
maxLists
|
|
24009
|
+
}) {
|
|
24010
|
+
if (source === "bird") {
|
|
24011
|
+
return Effect47.gen(function* () {
|
|
24012
|
+
const authenticated = yield* getAuthenticatedBirdAccountEffect();
|
|
24013
|
+
yield* trySync(
|
|
24014
|
+
() => assertLiveAccountMatches({
|
|
24015
|
+
source: "bird",
|
|
24016
|
+
account,
|
|
24017
|
+
liveUsername: authenticated.username,
|
|
24018
|
+
liveExternalUserId: authenticated.id
|
|
24019
|
+
})
|
|
24020
|
+
);
|
|
24021
|
+
return yield* listOwnedXListsViaBirdEffect({ maxResults: maxLists });
|
|
24022
|
+
});
|
|
24023
|
+
}
|
|
24024
|
+
return Effect47.gen(function* () {
|
|
24025
|
+
const authenticated = yield* lookupAuthenticatedOAuth2UserEffect(
|
|
24026
|
+
account.username
|
|
24027
|
+
);
|
|
24028
|
+
if (!authenticated) {
|
|
24029
|
+
return yield* Effect47.fail(
|
|
24030
|
+
new Error("xurl did not return an OAuth2 user")
|
|
24031
|
+
);
|
|
24032
|
+
}
|
|
24033
|
+
const liveUsername = String(authenticated.username ?? "");
|
|
24034
|
+
const liveExternalUserId = String(authenticated.id ?? "");
|
|
24035
|
+
yield* trySync(
|
|
24036
|
+
() => assertLiveAccountMatches({
|
|
24037
|
+
source: "xurl",
|
|
24038
|
+
account,
|
|
24039
|
+
liveUsername,
|
|
24040
|
+
liveExternalUserId
|
|
24041
|
+
})
|
|
24042
|
+
);
|
|
24043
|
+
const userId = account.externalUserId ?? liveExternalUserId;
|
|
24044
|
+
if (!userId) {
|
|
24045
|
+
return yield* Effect47.fail(
|
|
24046
|
+
new Error("xurl authenticated user is missing an id")
|
|
24047
|
+
);
|
|
24048
|
+
}
|
|
24049
|
+
return yield* listOwnedXListsViaXurlEffect({
|
|
24050
|
+
userId,
|
|
24051
|
+
username: account.username,
|
|
24052
|
+
maxResults: maxLists
|
|
24053
|
+
});
|
|
24054
|
+
});
|
|
24055
|
+
}
|
|
24056
|
+
function fetchListMembersEffect({
|
|
24057
|
+
source,
|
|
24058
|
+
list,
|
|
24059
|
+
username,
|
|
24060
|
+
memberLimit,
|
|
24061
|
+
maxMemberPages
|
|
24062
|
+
}) {
|
|
24063
|
+
if (source === "bird") {
|
|
24064
|
+
return listXListMembersViaBirdEffect({
|
|
24065
|
+
listId: list.id,
|
|
24066
|
+
maxResults: memberLimit,
|
|
24067
|
+
maxPages: maxMemberPages
|
|
24068
|
+
});
|
|
24069
|
+
}
|
|
24070
|
+
return Effect47.gen(function* () {
|
|
24071
|
+
const data = [];
|
|
24072
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24073
|
+
let paginationToken;
|
|
24074
|
+
let pageCount = 0;
|
|
24075
|
+
do {
|
|
24076
|
+
const page = yield* listXListMembersViaXurlEffect({
|
|
24077
|
+
listId: list.id,
|
|
24078
|
+
username,
|
|
24079
|
+
maxResults: memberLimit,
|
|
24080
|
+
paginationToken
|
|
24081
|
+
});
|
|
24082
|
+
pageCount += 1;
|
|
24083
|
+
for (const user of page.data) {
|
|
24084
|
+
if (seen.has(user.id)) continue;
|
|
24085
|
+
seen.add(user.id);
|
|
24086
|
+
data.push(user);
|
|
24087
|
+
}
|
|
24088
|
+
paginationToken = metaString(page.meta, "next_token");
|
|
24089
|
+
} while (paginationToken && pageCount < maxMemberPages);
|
|
24090
|
+
return {
|
|
24091
|
+
data,
|
|
24092
|
+
meta: {
|
|
24093
|
+
result_count: data.length,
|
|
24094
|
+
page_count: pageCount,
|
|
24095
|
+
next_token: paginationToken ?? null,
|
|
24096
|
+
pagination_known_complete: !paginationToken
|
|
24097
|
+
}
|
|
24098
|
+
};
|
|
24099
|
+
});
|
|
24100
|
+
}
|
|
24101
|
+
function membershipStatus({
|
|
24102
|
+
list,
|
|
24103
|
+
payload
|
|
24104
|
+
}) {
|
|
24105
|
+
if (payload.meta?.pagination_known_complete === true) return "complete";
|
|
24106
|
+
if (payload.meta?.pagination_inferred_complete === true) return "inferred";
|
|
24107
|
+
if (typeof list.memberCount === "number" && payload.data.length >= list.memberCount) {
|
|
24108
|
+
return "inferred";
|
|
24109
|
+
}
|
|
24110
|
+
return "partial";
|
|
24111
|
+
}
|
|
24112
|
+
function ownerProfileId(db, list) {
|
|
24113
|
+
if (!list.ownerId || !list.ownerUsername) return void 0;
|
|
24114
|
+
return upsertProfileFromXUser(db, {
|
|
24115
|
+
id: list.ownerId,
|
|
24116
|
+
username: list.ownerUsername,
|
|
24117
|
+
name: list.ownerName ?? list.ownerUsername
|
|
24118
|
+
}).profile.id;
|
|
24119
|
+
}
|
|
24120
|
+
function persistListResult({
|
|
24121
|
+
db,
|
|
24122
|
+
accountId,
|
|
24123
|
+
list,
|
|
24124
|
+
source,
|
|
24125
|
+
payload,
|
|
24126
|
+
status,
|
|
24127
|
+
now: now2,
|
|
24128
|
+
rateLimit
|
|
24129
|
+
}) {
|
|
24130
|
+
return db.transaction(() => {
|
|
24131
|
+
const listOwnerProfileId = ownerProfileId(db, list);
|
|
24132
|
+
const membersSyncedAt = payload ? now2 : null;
|
|
24133
|
+
const pageCount = payload ? metaNumber(payload.meta, "page_count") ?? 1 : 0;
|
|
24134
|
+
const resultCount = payload?.data.length ?? 0;
|
|
24135
|
+
db.prepare(
|
|
24136
|
+
`
|
|
24137
|
+
insert into x_lists (
|
|
24138
|
+
account_id, list_id, name, description, owner_profile_id,
|
|
24139
|
+
owner_external_user_id, is_private, member_count, follower_count,
|
|
24140
|
+
source, membership_status, lists_synced_at, members_synced_at,
|
|
24141
|
+
member_page_count, member_result_count, rate_limit_json, raw_json,
|
|
24142
|
+
updated_at
|
|
24143
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
24144
|
+
on conflict(account_id, list_id) do update set
|
|
24145
|
+
name = excluded.name,
|
|
24146
|
+
description = excluded.description,
|
|
24147
|
+
owner_profile_id = coalesce(excluded.owner_profile_id, x_lists.owner_profile_id),
|
|
24148
|
+
owner_external_user_id = coalesce(excluded.owner_external_user_id, x_lists.owner_external_user_id),
|
|
24149
|
+
is_private = excluded.is_private,
|
|
24150
|
+
member_count = coalesce(excluded.member_count, x_lists.member_count),
|
|
24151
|
+
follower_count = coalesce(excluded.follower_count, x_lists.follower_count),
|
|
24152
|
+
source = excluded.source,
|
|
24153
|
+
membership_status = excluded.membership_status,
|
|
24154
|
+
lists_synced_at = excluded.lists_synced_at,
|
|
24155
|
+
members_synced_at = coalesce(excluded.members_synced_at, x_lists.members_synced_at),
|
|
24156
|
+
member_page_count = excluded.member_page_count,
|
|
24157
|
+
member_result_count = excluded.member_result_count,
|
|
24158
|
+
rate_limit_json = excluded.rate_limit_json,
|
|
24159
|
+
raw_json = excluded.raw_json,
|
|
24160
|
+
updated_at = excluded.updated_at
|
|
24161
|
+
`
|
|
24162
|
+
).run(
|
|
24163
|
+
accountId,
|
|
24164
|
+
list.id,
|
|
24165
|
+
list.name,
|
|
24166
|
+
list.description ?? "",
|
|
24167
|
+
listOwnerProfileId ?? null,
|
|
24168
|
+
list.ownerId ?? null,
|
|
24169
|
+
list.isPrivate ? 1 : 0,
|
|
24170
|
+
list.memberCount ?? null,
|
|
24171
|
+
list.followerCount ?? null,
|
|
24172
|
+
source,
|
|
24173
|
+
status,
|
|
24174
|
+
now2,
|
|
24175
|
+
membersSyncedAt,
|
|
24176
|
+
pageCount,
|
|
24177
|
+
resultCount,
|
|
24178
|
+
JSON.stringify(rateLimit),
|
|
24179
|
+
JSON.stringify(list.raw ?? {}),
|
|
24180
|
+
now2
|
|
24181
|
+
);
|
|
24182
|
+
if (!payload) return;
|
|
24183
|
+
if (status === "complete") {
|
|
24184
|
+
db.prepare(
|
|
24185
|
+
`
|
|
24186
|
+
update x_list_members
|
|
24187
|
+
set current = 0, ended_at = ?, updated_at = ?
|
|
24188
|
+
where account_id = ? and list_id = ? and current = 1
|
|
24189
|
+
`
|
|
24190
|
+
).run(now2, now2, accountId, list.id);
|
|
24191
|
+
}
|
|
24192
|
+
const upsertMember = db.prepare(`
|
|
24193
|
+
insert into x_list_members (
|
|
24194
|
+
account_id, list_id, profile_id, external_user_id, source, current,
|
|
24195
|
+
first_seen_at, last_seen_at, ended_at, raw_json, updated_at
|
|
24196
|
+
) values (?, ?, ?, ?, ?, 1, ?, ?, null, ?, ?)
|
|
24197
|
+
on conflict(account_id, list_id, profile_id) do update set
|
|
24198
|
+
external_user_id = excluded.external_user_id,
|
|
24199
|
+
source = excluded.source,
|
|
24200
|
+
current = 1,
|
|
24201
|
+
last_seen_at = excluded.last_seen_at,
|
|
24202
|
+
ended_at = null,
|
|
24203
|
+
raw_json = excluded.raw_json,
|
|
24204
|
+
updated_at = excluded.updated_at
|
|
24205
|
+
`);
|
|
24206
|
+
for (const user of payload.data) {
|
|
24207
|
+
const resolved = upsertProfileFromXUser(db, user);
|
|
24208
|
+
upsertMember.run(
|
|
24209
|
+
accountId,
|
|
24210
|
+
list.id,
|
|
24211
|
+
resolved.profile.id,
|
|
24212
|
+
resolved.externalUserId,
|
|
24213
|
+
source,
|
|
24214
|
+
now2,
|
|
24215
|
+
now2,
|
|
24216
|
+
JSON.stringify(user),
|
|
24217
|
+
now2
|
|
24218
|
+
);
|
|
24219
|
+
}
|
|
24220
|
+
})();
|
|
24221
|
+
}
|
|
24222
|
+
function syncXListsEffect(options = {}) {
|
|
24223
|
+
return Effect47.suspend(() => {
|
|
24224
|
+
const mode = normalizedMode(options.mode);
|
|
24225
|
+
const maxLists = positiveInteger(
|
|
24226
|
+
"--max-lists",
|
|
24227
|
+
options.maxLists ?? DEFAULT_MAX_LISTS,
|
|
24228
|
+
MAX_LISTS
|
|
24229
|
+
);
|
|
24230
|
+
const memberLimit = positiveInteger(
|
|
24231
|
+
"--member-limit",
|
|
24232
|
+
options.memberLimit ?? DEFAULT_MEMBER_LIMIT,
|
|
24233
|
+
MAX_MEMBER_LIMIT
|
|
24234
|
+
);
|
|
24235
|
+
const maxMemberPages = positiveInteger(
|
|
24236
|
+
"--max-member-pages",
|
|
24237
|
+
options.maxMemberPages ?? DEFAULT_MAX_MEMBER_PAGES,
|
|
24238
|
+
MAX_MEMBER_PAGES
|
|
24239
|
+
);
|
|
24240
|
+
const delayMs = nonNegativeInteger(
|
|
24241
|
+
"--delay-ms",
|
|
24242
|
+
options.delayMs ?? DEFAULT_DELAY_MS2
|
|
24243
|
+
);
|
|
24244
|
+
const db = getNativeDb();
|
|
24245
|
+
const account = resolveLiveSyncAccount(db, options.account);
|
|
24246
|
+
const sources = mode === "auto" ? ["bird", "xurl"] : [mode];
|
|
24247
|
+
const transports = sources.map(
|
|
24248
|
+
(source) => createLiveTransportAdapter(
|
|
24249
|
+
source,
|
|
24250
|
+
fetchOwnedListsEffect({ source, account, maxLists })
|
|
24251
|
+
)
|
|
24252
|
+
);
|
|
24253
|
+
return Effect47.gen(function* () {
|
|
24254
|
+
const discovered = yield* fetchWithTransportFallbackEffect(transports);
|
|
24255
|
+
const source = discovered.source;
|
|
24256
|
+
const lists = discovered.payload.data.slice(0, maxLists);
|
|
24257
|
+
const results = [];
|
|
24258
|
+
for (const [index, list] of lists.entries()) {
|
|
24259
|
+
if (index > 0 && delayMs > 0) yield* Effect47.sleep(delayMs);
|
|
24260
|
+
const fetched = yield* fetchListMembersEffect({
|
|
24261
|
+
source,
|
|
24262
|
+
list,
|
|
24263
|
+
username: account.username,
|
|
24264
|
+
memberLimit,
|
|
24265
|
+
maxMemberPages
|
|
24266
|
+
}).pipe(
|
|
24267
|
+
Effect47.map((payload) => ({ ok: true, payload })),
|
|
24268
|
+
Effect47.catchAll(
|
|
24269
|
+
(error) => Effect47.succeed({ ok: false, error: toError(error) })
|
|
24270
|
+
)
|
|
24271
|
+
);
|
|
24272
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
24273
|
+
const rateLimit = {
|
|
24274
|
+
memberLimit,
|
|
24275
|
+
maxMemberPages,
|
|
24276
|
+
delayMs,
|
|
24277
|
+
nextCursorStored: fetched.ok ? Boolean(metaString(fetched.payload.meta, "next_token")) : false
|
|
24278
|
+
};
|
|
24279
|
+
if (!fetched.ok) {
|
|
24280
|
+
yield* databaseWriteEffect(
|
|
24281
|
+
(writeDb) => persistListResult({
|
|
24282
|
+
db: writeDb,
|
|
24283
|
+
accountId: account.accountId,
|
|
24284
|
+
list,
|
|
24285
|
+
source,
|
|
24286
|
+
status: "error",
|
|
24287
|
+
now: now2,
|
|
24288
|
+
rateLimit
|
|
24289
|
+
})
|
|
24290
|
+
);
|
|
24291
|
+
results.push({
|
|
24292
|
+
listId: list.id,
|
|
24293
|
+
name: list.name,
|
|
24294
|
+
status: "error",
|
|
24295
|
+
error: fetched.error.message
|
|
24296
|
+
});
|
|
24297
|
+
continue;
|
|
24298
|
+
}
|
|
24299
|
+
const status = membershipStatus({ list, payload: fetched.payload });
|
|
24300
|
+
yield* databaseWriteEffect(
|
|
24301
|
+
(writeDb) => persistListResult({
|
|
24302
|
+
db: writeDb,
|
|
24303
|
+
accountId: account.accountId,
|
|
24304
|
+
list,
|
|
24305
|
+
source,
|
|
24306
|
+
payload: fetched.payload,
|
|
24307
|
+
status,
|
|
24308
|
+
now: now2,
|
|
24309
|
+
rateLimit
|
|
24310
|
+
})
|
|
24311
|
+
);
|
|
24312
|
+
results.push({
|
|
24313
|
+
listId: list.id,
|
|
24314
|
+
name: list.name,
|
|
24315
|
+
status,
|
|
24316
|
+
members: fetched.payload.data.length,
|
|
24317
|
+
pages: metaNumber(fetched.payload.meta, "page_count") ?? 1
|
|
24318
|
+
});
|
|
24319
|
+
}
|
|
24320
|
+
const errors = results.filter((result) => result.status === "error");
|
|
24321
|
+
return {
|
|
24322
|
+
ok: errors.length === 0,
|
|
24323
|
+
accountId: account.accountId,
|
|
24324
|
+
mode,
|
|
24325
|
+
source,
|
|
24326
|
+
listCount: lists.length,
|
|
24327
|
+
membershipCompleteCount: results.filter(
|
|
24328
|
+
(result) => result.status === "complete"
|
|
24329
|
+
).length,
|
|
24330
|
+
membershipPartialCount: results.filter(
|
|
24331
|
+
(result) => result.status === "partial"
|
|
24332
|
+
).length,
|
|
24333
|
+
membershipInferredCount: results.filter(
|
|
24334
|
+
(result) => result.status === "inferred"
|
|
24335
|
+
).length,
|
|
24336
|
+
errorCount: errors.length,
|
|
24337
|
+
rateLimit: { maxLists, memberLimit, maxMemberPages, delayMs },
|
|
24338
|
+
lists: results
|
|
24339
|
+
};
|
|
24340
|
+
});
|
|
24341
|
+
});
|
|
24342
|
+
}
|
|
24343
|
+
function syncXLists(options = {}) {
|
|
24344
|
+
return runEffectPromise(syncXListsEffect(options));
|
|
24345
|
+
}
|
|
24346
|
+
function accountIdForRead(db, account) {
|
|
24347
|
+
return resolveLiveSyncAccount(db, account).accountId;
|
|
24348
|
+
}
|
|
24349
|
+
function storedListFromRow(row) {
|
|
24350
|
+
return {
|
|
24351
|
+
accountId: String(row.account_id),
|
|
24352
|
+
listId: String(row.list_id),
|
|
24353
|
+
name: String(row.name),
|
|
24354
|
+
description: String(row.description ?? ""),
|
|
24355
|
+
...row.owner_profile_id ? { ownerProfileId: String(row.owner_profile_id) } : {},
|
|
24356
|
+
...row.owner_external_user_id ? { ownerExternalUserId: String(row.owner_external_user_id) } : {},
|
|
24357
|
+
isPrivate: Number(row.is_private) === 1,
|
|
24358
|
+
...typeof row.member_count === "number" ? { memberCount: row.member_count } : {},
|
|
24359
|
+
...typeof row.follower_count === "number" ? { followerCount: row.follower_count } : {},
|
|
24360
|
+
source: String(row.source),
|
|
24361
|
+
membershipStatus: String(row.membership_status),
|
|
24362
|
+
listsSyncedAt: String(row.lists_synced_at),
|
|
24363
|
+
...row.members_synced_at ? { membersSyncedAt: String(row.members_synced_at) } : {},
|
|
24364
|
+
memberPageCount: Number(row.member_page_count),
|
|
24365
|
+
memberResultCount: Number(row.member_result_count),
|
|
24366
|
+
rateLimit: parseJsonRecord(row.rate_limit_json)
|
|
24367
|
+
};
|
|
24368
|
+
}
|
|
24369
|
+
function listStoredXLists(options = {}) {
|
|
24370
|
+
const db = getReadDb();
|
|
24371
|
+
const accountId = accountIdForRead(db, options.account);
|
|
24372
|
+
return db.prepare(
|
|
24373
|
+
`select * from x_lists where account_id = ? order by name collate nocase, list_id`
|
|
24374
|
+
).all(accountId).map(storedListFromRow);
|
|
24375
|
+
}
|
|
24376
|
+
function resolveStoredXListSelector({
|
|
24377
|
+
account,
|
|
24378
|
+
listId,
|
|
24379
|
+
name
|
|
24380
|
+
}) {
|
|
24381
|
+
if (Boolean(listId) === Boolean(name)) {
|
|
24382
|
+
throw new Error("Choose exactly one of --list or --list-id");
|
|
24383
|
+
}
|
|
24384
|
+
const db = getReadDb();
|
|
24385
|
+
const accountId = accountIdForRead(db, account);
|
|
24386
|
+
const rows = listId ? db.prepare("select * from x_lists where account_id = ? and list_id = ?").all(accountId, listId) : db.prepare(
|
|
24387
|
+
"select * from x_lists where account_id = ? and name = ? collate nocase order by list_id"
|
|
24388
|
+
).all(accountId, name);
|
|
24389
|
+
if (rows.length === 0) {
|
|
24390
|
+
throw new Error(
|
|
24391
|
+
`No cached List matches ${listId ? `id ${listId}` : `name ${name}`}; run birdclaw sync lists`
|
|
24392
|
+
);
|
|
24393
|
+
}
|
|
24394
|
+
if (rows.length > 1) {
|
|
24395
|
+
throw new Error(
|
|
24396
|
+
`List name ${name} is ambiguous; use --list-id (${rows.map((row) => String(row.list_id)).join(", ")})`
|
|
24397
|
+
);
|
|
24398
|
+
}
|
|
24399
|
+
return storedListFromRow(rows[0]);
|
|
24400
|
+
}
|
|
24401
|
+
function listStoredXListMembers({
|
|
24402
|
+
account,
|
|
24403
|
+
listId,
|
|
24404
|
+
name,
|
|
24405
|
+
includeEnded = false,
|
|
24406
|
+
limit = 100
|
|
24407
|
+
}) {
|
|
24408
|
+
const list = resolveStoredXListSelector({ account, listId, name });
|
|
24409
|
+
const db = getReadDb();
|
|
24410
|
+
const rows = db.prepare(
|
|
24411
|
+
`
|
|
24412
|
+
select
|
|
24413
|
+
member.account_id, member.list_id, member.external_user_id,
|
|
24414
|
+
member.current, member.first_seen_at, member.last_seen_at,
|
|
24415
|
+
member.ended_at, profile.id as profile_id, profile.handle,
|
|
24416
|
+
profile.display_name, profile.bio, profile.followers_count,
|
|
24417
|
+
profile.avatar_url
|
|
24418
|
+
from x_list_members member
|
|
24419
|
+
join profiles profile on profile.id = member.profile_id
|
|
24420
|
+
where member.account_id = ? and member.list_id = ?
|
|
24421
|
+
and (? = 1 or member.current = 1)
|
|
24422
|
+
order by profile.followers_count desc, profile.handle collate nocase
|
|
24423
|
+
limit ?
|
|
24424
|
+
`
|
|
24425
|
+
).all(
|
|
24426
|
+
list.accountId,
|
|
24427
|
+
list.listId,
|
|
24428
|
+
includeEnded ? 1 : 0,
|
|
24429
|
+
positiveInteger("--limit", limit, 1e4)
|
|
24430
|
+
);
|
|
24431
|
+
const items = rows.map((row) => ({
|
|
24432
|
+
accountId: String(row.account_id),
|
|
24433
|
+
listId: String(row.list_id),
|
|
24434
|
+
externalUserId: String(row.external_user_id),
|
|
24435
|
+
current: Number(row.current) === 1,
|
|
24436
|
+
firstSeenAt: String(row.first_seen_at),
|
|
24437
|
+
lastSeenAt: String(row.last_seen_at),
|
|
24438
|
+
...row.ended_at ? { endedAt: String(row.ended_at) } : {},
|
|
24439
|
+
profile: {
|
|
24440
|
+
id: String(row.profile_id),
|
|
24441
|
+
handle: String(row.handle),
|
|
24442
|
+
displayName: String(row.display_name),
|
|
24443
|
+
bio: String(row.bio),
|
|
24444
|
+
followersCount: Number(row.followers_count),
|
|
24445
|
+
...row.avatar_url ? { avatarUrl: String(row.avatar_url) } : {}
|
|
24446
|
+
}
|
|
24447
|
+
}));
|
|
24448
|
+
return { list, items };
|
|
24449
|
+
}
|
|
24450
|
+
|
|
24451
|
+
// src/cli/register-lists.ts
|
|
24452
|
+
function registerListCommands({
|
|
24453
|
+
program: program2,
|
|
24454
|
+
print: print2,
|
|
24455
|
+
asJson,
|
|
24456
|
+
autoUpdateBeforeRead: autoUpdateBeforeRead2
|
|
24457
|
+
}) {
|
|
24458
|
+
const listsCommand = program2.command("lists").description("Inspect cached X Lists and membership");
|
|
24459
|
+
listsCommand.command("list").description("List cached owned X Lists").option("--account <accountId>", "Account id").action(async (options) => {
|
|
24460
|
+
await autoUpdateBeforeRead2();
|
|
24461
|
+
const items = listStoredXLists({ account: options.account });
|
|
24462
|
+
if (asJson()) {
|
|
24463
|
+
print2(items, true);
|
|
24464
|
+
return;
|
|
24465
|
+
}
|
|
24466
|
+
console.log(
|
|
24467
|
+
items.map(
|
|
24468
|
+
(item) => `${item.name} ${item.listId} ${item.membershipStatus} ${String(item.memberResultCount)}/${item.memberCount === void 0 ? "?" : String(item.memberCount)}`
|
|
24469
|
+
).join("\n")
|
|
24470
|
+
);
|
|
24471
|
+
});
|
|
24472
|
+
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) => {
|
|
24473
|
+
await autoUpdateBeforeRead2();
|
|
24474
|
+
const result = listStoredXListMembers({
|
|
24475
|
+
account: options.account,
|
|
24476
|
+
name,
|
|
24477
|
+
listId: options.listId,
|
|
24478
|
+
includeEnded: Boolean(options.includeEnded),
|
|
24479
|
+
limit: Number(options.limit)
|
|
24480
|
+
});
|
|
24481
|
+
if (asJson()) {
|
|
24482
|
+
print2(result, true);
|
|
24483
|
+
return;
|
|
24484
|
+
}
|
|
24485
|
+
console.log(
|
|
24486
|
+
result.items.map(
|
|
24487
|
+
(item) => `@${item.profile.handle} ${item.externalUserId} ${item.current ? "current" : "ended"}`
|
|
24488
|
+
).join("\n")
|
|
24489
|
+
);
|
|
24490
|
+
});
|
|
24491
|
+
}
|
|
24492
|
+
|
|
23374
24493
|
// src/lib/profile-replies.ts
|
|
23375
|
-
import { Effect as
|
|
24494
|
+
import { Effect as Effect51 } from "effect";
|
|
23376
24495
|
|
|
23377
24496
|
// src/lib/moderation-target.ts
|
|
23378
|
-
import { Effect as
|
|
24497
|
+
import { Effect as Effect50 } from "effect";
|
|
23379
24498
|
|
|
23380
24499
|
// src/lib/bird-actions.ts
|
|
23381
|
-
import { Effect as
|
|
24500
|
+
import { Effect as Effect49 } from "effect";
|
|
23382
24501
|
|
|
23383
24502
|
// src/lib/bird-command.ts
|
|
23384
24503
|
import { access } from "node:fs/promises";
|
|
23385
24504
|
import { constants as constants2 } from "node:fs";
|
|
23386
|
-
import { Data as Data3, Effect as
|
|
24505
|
+
import { Data as Data3, Effect as Effect48 } from "effect";
|
|
23387
24506
|
var BirdCommandUnavailableError = class extends Data3.TaggedError(
|
|
23388
24507
|
"BirdCommandUnavailableError"
|
|
23389
24508
|
) {
|
|
@@ -23436,25 +24555,25 @@ function execFailureFromCause(command, cause) {
|
|
|
23436
24555
|
}
|
|
23437
24556
|
function assertBirdCommandAvailableEffect(command) {
|
|
23438
24557
|
if (!isPathCommand(command)) {
|
|
23439
|
-
return
|
|
24558
|
+
return Effect48.void;
|
|
23440
24559
|
}
|
|
23441
|
-
return
|
|
24560
|
+
return Effect48.tryPromise({
|
|
23442
24561
|
try: () => Promise.resolve(access(command, constants2.X_OK)),
|
|
23443
24562
|
catch: (cause) => new BirdCommandUnavailableError({
|
|
23444
24563
|
message: formatBirdInstallHint(command),
|
|
23445
24564
|
command,
|
|
23446
24565
|
cause
|
|
23447
24566
|
})
|
|
23448
|
-
}).pipe(
|
|
24567
|
+
}).pipe(Effect48.asVoid);
|
|
23449
24568
|
}
|
|
23450
24569
|
function getBirdCommandEffect() {
|
|
23451
|
-
return
|
|
24570
|
+
return Effect48.try({
|
|
23452
24571
|
try: () => getBirdCommand(),
|
|
23453
24572
|
catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
|
|
23454
24573
|
});
|
|
23455
24574
|
}
|
|
23456
24575
|
function runBirdCommandEffect(args, options) {
|
|
23457
|
-
return
|
|
24576
|
+
return Effect48.gen(function* () {
|
|
23458
24577
|
const birdCommand = yield* getBirdCommandEffect();
|
|
23459
24578
|
yield* assertBirdCommandAvailableEffect(birdCommand);
|
|
23460
24579
|
return yield* runSubprocessEffect({
|
|
@@ -23467,8 +24586,8 @@ function runBirdCommandEffect(args, options) {
|
|
|
23467
24586
|
...options?.signal ? { signal: options.signal } : {},
|
|
23468
24587
|
...options?.killSignal ? { killSignal: options.killSignal } : {}
|
|
23469
24588
|
}).pipe(
|
|
23470
|
-
|
|
23471
|
-
|
|
24589
|
+
Effect48.map(({ stdout, stderr }) => ({ stdout, stderr })),
|
|
24590
|
+
Effect48.mapError((cause) => execFailureFromCause(birdCommand, cause))
|
|
23472
24591
|
);
|
|
23473
24592
|
});
|
|
23474
24593
|
}
|
|
@@ -23506,9 +24625,9 @@ function toError18(error) {
|
|
|
23506
24625
|
return error instanceof Error ? error : new Error(String(error));
|
|
23507
24626
|
}
|
|
23508
24627
|
function runBirdJsonCommandEffect2(args) {
|
|
23509
|
-
return
|
|
24628
|
+
return Effect49.gen(function* () {
|
|
23510
24629
|
const { stdout } = yield* runBirdCommandEffect(args);
|
|
23511
|
-
return yield*
|
|
24630
|
+
return yield* Effect49.try({
|
|
23512
24631
|
try: () => JSON.parse(stripAnsi2(stdout)),
|
|
23513
24632
|
catch: toError18
|
|
23514
24633
|
});
|
|
@@ -23522,13 +24641,13 @@ function safeBirdProfileArg(query) {
|
|
|
23522
24641
|
throw new Error("Invalid bird profile query");
|
|
23523
24642
|
}
|
|
23524
24643
|
function readBirdStatusViaBirdEffect(query) {
|
|
23525
|
-
return
|
|
23526
|
-
const safeQuery = yield*
|
|
24644
|
+
return Effect49.gen(function* () {
|
|
24645
|
+
const safeQuery = yield* Effect49.try({
|
|
23527
24646
|
try: () => safeBirdProfileArg(query),
|
|
23528
24647
|
catch: toError18
|
|
23529
24648
|
});
|
|
23530
24649
|
return yield* runBirdJsonCommandEffect2(["status", safeQuery, "--json"]);
|
|
23531
|
-
}).pipe(
|
|
24650
|
+
}).pipe(Effect49.catchAll(() => Effect49.succeed(null)));
|
|
23532
24651
|
}
|
|
23533
24652
|
function readBirdStatusViaBird(query) {
|
|
23534
24653
|
return runEffectPromise(readBirdStatusViaBirdEffect(query));
|
|
@@ -23565,8 +24684,8 @@ function toBirdLookupUser(payload) {
|
|
|
23565
24684
|
};
|
|
23566
24685
|
}
|
|
23567
24686
|
function lookupProfileViaBirdEffect2(query) {
|
|
23568
|
-
return
|
|
23569
|
-
const safeQuery = yield*
|
|
24687
|
+
return Effect49.gen(function* () {
|
|
24688
|
+
const safeQuery = yield* Effect49.try({
|
|
23570
24689
|
try: () => safeBirdProfileArg(query),
|
|
23571
24690
|
catch: toError18
|
|
23572
24691
|
});
|
|
@@ -23577,11 +24696,11 @@ function lookupProfileViaBirdEffect2(query) {
|
|
|
23577
24696
|
"1",
|
|
23578
24697
|
"--json"
|
|
23579
24698
|
]);
|
|
23580
|
-
return yield*
|
|
24699
|
+
return yield* Effect49.try({
|
|
23581
24700
|
try: () => toBirdLookupUser(payload),
|
|
23582
24701
|
catch: toError18
|
|
23583
24702
|
});
|
|
23584
|
-
}).pipe(
|
|
24703
|
+
}).pipe(Effect49.catchAll(() => Effect49.succeed(null)));
|
|
23585
24704
|
}
|
|
23586
24705
|
function lookupProfileViaBird(query) {
|
|
23587
24706
|
return runEffectPromise(lookupProfileViaBirdEffect2(query));
|
|
@@ -23592,8 +24711,8 @@ function runVerifiedBirdMutationEffect({
|
|
|
23592
24711
|
verifyField,
|
|
23593
24712
|
expectedValue
|
|
23594
24713
|
}) {
|
|
23595
|
-
return
|
|
23596
|
-
const safeQuery = yield*
|
|
24714
|
+
return Effect49.gen(function* () {
|
|
24715
|
+
const safeQuery = yield* Effect49.try({
|
|
23597
24716
|
try: () => safeBirdProfileArg(query),
|
|
23598
24717
|
catch: toError18
|
|
23599
24718
|
});
|
|
@@ -23607,12 +24726,12 @@ function runVerifiedBirdMutationEffect({
|
|
|
23607
24726
|
action,
|
|
23608
24727
|
safeQuery
|
|
23609
24728
|
]).pipe(
|
|
23610
|
-
|
|
24729
|
+
Effect49.map(({ stdout, stderr }) => ({
|
|
23611
24730
|
ok: true,
|
|
23612
24731
|
output: normalizeOutput(stdout, stderr)
|
|
23613
24732
|
})),
|
|
23614
|
-
|
|
23615
|
-
(error) =>
|
|
24733
|
+
Effect49.catchAll(
|
|
24734
|
+
(error) => Effect49.succeed({
|
|
23616
24735
|
ok: false,
|
|
23617
24736
|
output: formatExecError2(error, `bird ${action} failed`)
|
|
23618
24737
|
})
|
|
@@ -23691,7 +24810,7 @@ function toError19(error) {
|
|
|
23691
24810
|
return error instanceof Error ? error : new Error(String(error));
|
|
23692
24811
|
}
|
|
23693
24812
|
function trySync14(try_) {
|
|
23694
|
-
return
|
|
24813
|
+
return Effect50.try({
|
|
23695
24814
|
try: try_,
|
|
23696
24815
|
catch: toError19
|
|
23697
24816
|
});
|
|
@@ -23739,11 +24858,11 @@ function resolveLocalProfile(db, normalizedQuery) {
|
|
|
23739
24858
|
};
|
|
23740
24859
|
}
|
|
23741
24860
|
function resolveProfileEffect(query, providedDb) {
|
|
23742
|
-
return
|
|
24861
|
+
return Effect50.gen(function* () {
|
|
23743
24862
|
const db = providedDb ?? (yield* trySync14(() => getNativeDb()));
|
|
23744
24863
|
const normalizedQuery = normalizeProfileQuery(query);
|
|
23745
24864
|
if (!normalizedQuery) {
|
|
23746
|
-
return yield*
|
|
24865
|
+
return yield* Effect50.fail(new Error("Missing profile handle or id"));
|
|
23747
24866
|
}
|
|
23748
24867
|
const local = yield* trySync14(
|
|
23749
24868
|
() => resolveLocalProfile(db, normalizedQuery)
|
|
@@ -23752,7 +24871,7 @@ function resolveProfileEffect(query, providedDb) {
|
|
|
23752
24871
|
if (local) {
|
|
23753
24872
|
return local;
|
|
23754
24873
|
}
|
|
23755
|
-
return yield*
|
|
24874
|
+
return yield* Effect50.fail(
|
|
23756
24875
|
new Error(`Profile not found locally: ${query}`)
|
|
23757
24876
|
);
|
|
23758
24877
|
}
|
|
@@ -23764,8 +24883,8 @@ function resolveProfileEffect(query, providedDb) {
|
|
|
23764
24883
|
const birdResult = yield* tryPromise(
|
|
23765
24884
|
() => lookupProfileViaBird(local?.profile.handle ?? normalizedQuery)
|
|
23766
24885
|
).pipe(
|
|
23767
|
-
|
|
23768
|
-
|
|
24886
|
+
Effect50.map((value) => ({ ok: true, value })),
|
|
24887
|
+
Effect50.catchAll((error) => Effect50.succeed({ ok: false, error }))
|
|
23769
24888
|
);
|
|
23770
24889
|
if (birdResult.ok) {
|
|
23771
24890
|
user = birdResult.value ?? void 0;
|
|
@@ -23776,9 +24895,9 @@ function resolveProfileEffect(query, providedDb) {
|
|
|
23776
24895
|
const xurlResult = yield* tryPromise(
|
|
23777
24896
|
() => /^\d+$/.test(normalizedQuery) ? lookupUsersByIds([normalizedQuery]) : lookupUsersByHandles([local?.profile.handle ?? normalizedQuery])
|
|
23778
24897
|
).pipe(
|
|
23779
|
-
|
|
23780
|
-
|
|
23781
|
-
(error) =>
|
|
24898
|
+
Effect50.map((value) => ({ ok: true, value })),
|
|
24899
|
+
Effect50.catchAll(
|
|
24900
|
+
(error) => Effect50.succeed({ ok: false, error })
|
|
23782
24901
|
)
|
|
23783
24902
|
);
|
|
23784
24903
|
if (xurlResult.ok) {
|
|
@@ -23791,13 +24910,13 @@ function resolveProfileEffect(query, providedDb) {
|
|
|
23791
24910
|
if (local) {
|
|
23792
24911
|
return local;
|
|
23793
24912
|
}
|
|
23794
|
-
return yield*
|
|
24913
|
+
return yield* Effect50.fail(lastError);
|
|
23795
24914
|
}
|
|
23796
24915
|
if (!user) {
|
|
23797
24916
|
if (local) {
|
|
23798
24917
|
return local;
|
|
23799
24918
|
}
|
|
23800
|
-
return yield*
|
|
24919
|
+
return yield* Effect50.fail(new Error(`Profile not found: ${query}`));
|
|
23801
24920
|
}
|
|
23802
24921
|
if (local) {
|
|
23803
24922
|
return yield* databaseWriteEffect(
|
|
@@ -23838,11 +24957,11 @@ function inspectProfileReplies(query, { limit = 12 } = {}) {
|
|
|
23838
24957
|
return runEffectPromise(inspectProfileRepliesEffect(query, { limit }));
|
|
23839
24958
|
}
|
|
23840
24959
|
function inspectProfileRepliesEffect(query, { limit = 12 } = {}) {
|
|
23841
|
-
return
|
|
24960
|
+
return Effect51.gen(function* () {
|
|
23842
24961
|
const resolved = yield* tryPromise(() => resolveProfile(query));
|
|
23843
24962
|
const externalUserId = resolved.externalUserId;
|
|
23844
24963
|
if (!externalUserId) {
|
|
23845
|
-
return yield*
|
|
24964
|
+
return yield* Effect51.fail(
|
|
23846
24965
|
new Error(`Profile has no external Twitter user id: ${query}`)
|
|
23847
24966
|
);
|
|
23848
24967
|
}
|
|
@@ -23937,18 +25056,18 @@ function registerMentionCommands({
|
|
|
23937
25056
|
}
|
|
23938
25057
|
|
|
23939
25058
|
// src/lib/blocks.ts
|
|
23940
|
-
import { Effect as
|
|
25059
|
+
import { Effect as Effect54 } from "effect";
|
|
23941
25060
|
|
|
23942
25061
|
// src/lib/moderation-state.ts
|
|
23943
|
-
import { Effect as
|
|
25062
|
+
import { Effect as Effect53 } from "effect";
|
|
23944
25063
|
|
|
23945
25064
|
// src/lib/actions-transport.ts
|
|
23946
|
-
import { Effect as
|
|
25065
|
+
import { Effect as Effect52 } from "effect";
|
|
23947
25066
|
function toError20(error) {
|
|
23948
25067
|
return error instanceof Error ? error : new Error(String(error));
|
|
23949
25068
|
}
|
|
23950
25069
|
function trySync15(try_) {
|
|
23951
|
-
return
|
|
25070
|
+
return Effect52.try({
|
|
23952
25071
|
try: try_,
|
|
23953
25072
|
catch: toError20
|
|
23954
25073
|
});
|
|
@@ -23960,7 +25079,7 @@ function liveWritesDisabled4() {
|
|
|
23960
25079
|
return process.env.BIRDCLAW_DISABLE_LIVE_WRITES === "1";
|
|
23961
25080
|
}
|
|
23962
25081
|
function verifyExpectedAccountEffect(expectedAccount) {
|
|
23963
|
-
return
|
|
25082
|
+
return Effect52.gen(function* () {
|
|
23964
25083
|
if (!expectedAccount) return null;
|
|
23965
25084
|
if (liveWritesDisabled4()) return null;
|
|
23966
25085
|
const sourceUser = yield* tryPromise(() => lookupAuthenticatedUserFresh());
|
|
@@ -23970,7 +25089,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
|
|
|
23970
25089
|
const expectedHandle = profileHandleKey(expectedAccount.handle);
|
|
23971
25090
|
if (expectedExternalUserId) {
|
|
23972
25091
|
if (sourceUserId === expectedExternalUserId) return sourceUserId;
|
|
23973
|
-
return yield*
|
|
25092
|
+
return yield* Effect52.fail(
|
|
23974
25093
|
new Error(
|
|
23975
25094
|
`xurl is authenticated as user ${sourceUserId || "unknown"}, not account ${expectedAccount.id}`
|
|
23976
25095
|
)
|
|
@@ -23978,7 +25097,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
|
|
|
23978
25097
|
}
|
|
23979
25098
|
if (expectedHandle && sourceUsername === expectedHandle)
|
|
23980
25099
|
return sourceUserId;
|
|
23981
|
-
return yield*
|
|
25100
|
+
return yield* Effect52.fail(
|
|
23982
25101
|
new Error(
|
|
23983
25102
|
sourceUsername ? `xurl is authenticated as @${sourceUsername}, not @${expectedHandle}` : "xurl authenticated user unavailable"
|
|
23984
25103
|
)
|
|
@@ -23986,7 +25105,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
|
|
|
23986
25105
|
});
|
|
23987
25106
|
}
|
|
23988
25107
|
function runBirdActionEffect(action, query) {
|
|
23989
|
-
return
|
|
25108
|
+
return Effect52.gen(function* () {
|
|
23990
25109
|
const result = yield* tryPromise(
|
|
23991
25110
|
() => action === "block" ? blockUserViaBird(query) : action === "unblock" ? unblockUserViaBird(query) : action === "mute" ? muteUserViaBird(query) : unmuteUserViaBird(query)
|
|
23992
25111
|
);
|
|
@@ -24006,7 +25125,7 @@ function getVerifyExpectation(action) {
|
|
|
24006
25125
|
};
|
|
24007
25126
|
}
|
|
24008
25127
|
function runXurlActionEffect(action, query, targetUserId, verifiedSourceUserId) {
|
|
24009
|
-
return
|
|
25128
|
+
return Effect52.gen(function* () {
|
|
24010
25129
|
if (!targetUserId) {
|
|
24011
25130
|
return {
|
|
24012
25131
|
ok: false,
|
|
@@ -24071,13 +25190,13 @@ function runModerationActionEffect({
|
|
|
24071
25190
|
transport,
|
|
24072
25191
|
expectedAccount
|
|
24073
25192
|
}) {
|
|
24074
|
-
return
|
|
25193
|
+
return Effect52.gen(function* () {
|
|
24075
25194
|
const requestedTransport = yield* trySync15(
|
|
24076
25195
|
() => resolveActionsTransport(transport)
|
|
24077
25196
|
);
|
|
24078
25197
|
const verifyXurlAccount = () => verifyExpectedAccountEffect(expectedAccount).pipe(
|
|
24079
|
-
|
|
24080
|
-
(error) =>
|
|
25198
|
+
Effect52.catchAll(
|
|
25199
|
+
(error) => Effect52.succeed({
|
|
24081
25200
|
ok: false,
|
|
24082
25201
|
output: error instanceof Error ? error.message : String(error),
|
|
24083
25202
|
transport: "xurl"
|
|
@@ -24166,7 +25285,7 @@ function toError21(error) {
|
|
|
24166
25285
|
return error instanceof Error ? error : new Error(String(error));
|
|
24167
25286
|
}
|
|
24168
25287
|
function trySync16(try_) {
|
|
24169
|
-
return
|
|
25288
|
+
return Effect53.try({
|
|
24170
25289
|
try: try_,
|
|
24171
25290
|
catch: toError21
|
|
24172
25291
|
});
|
|
@@ -24285,20 +25404,20 @@ function resolveModerationTargetEffect({
|
|
|
24285
25404
|
query,
|
|
24286
25405
|
selfActionError
|
|
24287
25406
|
}) {
|
|
24288
|
-
return
|
|
25407
|
+
return Effect53.gen(function* () {
|
|
24289
25408
|
const db = yield* trySync16(() => getNativeDb());
|
|
24290
25409
|
const resolvedAccountId = accountId || getDefaultAccountId(db);
|
|
24291
25410
|
const accountHandle = yield* trySync16(
|
|
24292
25411
|
() => getAccountHandle(db, resolvedAccountId)
|
|
24293
25412
|
);
|
|
24294
25413
|
if (!accountHandle) {
|
|
24295
|
-
return yield*
|
|
25414
|
+
return yield* Effect53.fail(
|
|
24296
25415
|
new Error(`Unknown account: ${resolvedAccountId}`)
|
|
24297
25416
|
);
|
|
24298
25417
|
}
|
|
24299
25418
|
const normalizedQuery = normalizeProfileQuery(query);
|
|
24300
25419
|
if (normalizedQuery.toLowerCase() === accountHandle.toLowerCase()) {
|
|
24301
|
-
return yield*
|
|
25420
|
+
return yield* Effect53.fail(new Error(selfActionError));
|
|
24302
25421
|
}
|
|
24303
25422
|
const resolved = yield* resolveProfileEffect(query, db);
|
|
24304
25423
|
const account = yield* trySync16(
|
|
@@ -24309,7 +25428,7 @@ function resolveModerationTargetEffect({
|
|
|
24309
25428
|
);
|
|
24310
25429
|
const accountExternalUserId = account?.external_user_id ?? getExternalUserId(accountProfile?.id ?? "");
|
|
24311
25430
|
if (profileHandleKey(resolved.profile.handle) === profileHandleKey(accountHandle) || accountProfile?.id && resolved.profile.id === accountProfile.id || accountExternalUserId && resolved.externalUserId === accountExternalUserId) {
|
|
24312
|
-
return yield*
|
|
25431
|
+
return yield* Effect53.fail(new Error(selfActionError));
|
|
24313
25432
|
}
|
|
24314
25433
|
return {
|
|
24315
25434
|
db,
|
|
@@ -24326,7 +25445,7 @@ function resolveModerationTargetEffect({
|
|
|
24326
25445
|
}
|
|
24327
25446
|
function addModerationEffect(kind, accountId, query, options = {}) {
|
|
24328
25447
|
const descriptor = moderationDescriptors[kind];
|
|
24329
|
-
return
|
|
25448
|
+
return Effect53.gen(function* () {
|
|
24330
25449
|
const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } = yield* resolveModerationTargetEffect({
|
|
24331
25450
|
accountId,
|
|
24332
25451
|
query,
|
|
@@ -24376,7 +25495,7 @@ function addModeration(kind, accountId, query, options = {}) {
|
|
|
24376
25495
|
}
|
|
24377
25496
|
function recordModerationEffect(kind, accountId, query) {
|
|
24378
25497
|
const descriptor = moderationDescriptors[kind];
|
|
24379
|
-
return
|
|
25498
|
+
return Effect53.gen(function* () {
|
|
24380
25499
|
const { db, resolved, resolvedAccountId } = yield* resolveModerationTargetEffect({
|
|
24381
25500
|
accountId,
|
|
24382
25501
|
query,
|
|
@@ -24407,7 +25526,7 @@ function recordModeration(kind, accountId, query) {
|
|
|
24407
25526
|
}
|
|
24408
25527
|
function removeModerationEffect(kind, accountId, query, options = {}) {
|
|
24409
25528
|
const descriptor = moderationDescriptors[kind];
|
|
24410
|
-
return
|
|
25529
|
+
return Effect53.gen(function* () {
|
|
24411
25530
|
const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } = yield* resolveModerationTargetEffect({
|
|
24412
25531
|
accountId,
|
|
24413
25532
|
query,
|
|
@@ -24483,7 +25602,7 @@ function toError22(error) {
|
|
|
24483
25602
|
return error instanceof Error ? error : new Error(String(error));
|
|
24484
25603
|
}
|
|
24485
25604
|
function trySync17(try_) {
|
|
24486
|
-
return
|
|
25605
|
+
return Effect54.try({
|
|
24487
25606
|
try: try_,
|
|
24488
25607
|
catch: toError22
|
|
24489
25608
|
});
|
|
@@ -24496,12 +25615,12 @@ function listBlocks({
|
|
|
24496
25615
|
return listModerationState("block", { account, search, limit });
|
|
24497
25616
|
}
|
|
24498
25617
|
function syncBlocksEffect(accountId) {
|
|
24499
|
-
return
|
|
25618
|
+
return Effect54.gen(function* () {
|
|
24500
25619
|
const db = yield* trySync17(() => getNativeDb());
|
|
24501
25620
|
const resolvedAccountId = accountId || getDefaultAccountId(db);
|
|
24502
25621
|
const accountHandle = getAccountHandle(db, resolvedAccountId);
|
|
24503
25622
|
if (!accountHandle) {
|
|
24504
|
-
return yield*
|
|
25623
|
+
return yield* Effect54.fail(
|
|
24505
25624
|
new Error(`Unknown account: ${resolvedAccountId}`)
|
|
24506
25625
|
);
|
|
24507
25626
|
}
|
|
@@ -24522,9 +25641,9 @@ function syncBlocksEffect(accountId) {
|
|
|
24522
25641
|
}
|
|
24523
25642
|
};
|
|
24524
25643
|
}
|
|
24525
|
-
return yield*
|
|
25644
|
+
return yield* Effect54.gen(function* () {
|
|
24526
25645
|
const me = yield* tryPromise(() => lookupAuthenticatedUserFresh()).pipe(
|
|
24527
|
-
|
|
25646
|
+
Effect54.mapError(toError22)
|
|
24528
25647
|
);
|
|
24529
25648
|
const sourceUserId = typeof me?.id === "string" && me.id.length > 0 ? me.id : null;
|
|
24530
25649
|
const sourceUsername = typeof me?.username === "string" ? me.username.replace(/^@/, "") : "";
|
|
@@ -24571,7 +25690,7 @@ function syncBlocksEffect(accountId) {
|
|
|
24571
25690
|
do {
|
|
24572
25691
|
const page = yield* tryPromise(
|
|
24573
25692
|
() => listBlockedUsers(sourceUserId, nextToken ?? void 0)
|
|
24574
|
-
).pipe(
|
|
25693
|
+
).pipe(Effect54.mapError(toError22));
|
|
24575
25694
|
const pageProfileIds = yield* databaseWriteEffect((writeDb) => {
|
|
24576
25695
|
return page.items.map((user) => {
|
|
24577
25696
|
const resolved = upsertProfileFromXUser(writeDb, user);
|
|
@@ -24613,8 +25732,8 @@ function syncBlocksEffect(accountId) {
|
|
|
24613
25732
|
}
|
|
24614
25733
|
};
|
|
24615
25734
|
}).pipe(
|
|
24616
|
-
|
|
24617
|
-
(error) =>
|
|
25735
|
+
Effect54.catchAll(
|
|
25736
|
+
(error) => Effect54.succeed({
|
|
24618
25737
|
ok: false,
|
|
24619
25738
|
accountId: resolvedAccountId,
|
|
24620
25739
|
synced: remoteProfileIds.length > 0,
|
|
@@ -24765,7 +25884,7 @@ function registerModerationCommands({
|
|
|
24765
25884
|
|
|
24766
25885
|
// src/lib/blocklist.ts
|
|
24767
25886
|
import { readFile } from "node:fs/promises";
|
|
24768
|
-
import { Effect as
|
|
25887
|
+
import { Effect as Effect55 } from "effect";
|
|
24769
25888
|
function stripInlineCode(value) {
|
|
24770
25889
|
const match = value.match(/`([^`]+)`/);
|
|
24771
25890
|
return match?.[1] ?? value;
|
|
@@ -24802,15 +25921,15 @@ function parseBlocklistText(text) {
|
|
|
24802
25921
|
return items;
|
|
24803
25922
|
}
|
|
24804
25923
|
function importBlocklistEffect(accountId, filePath) {
|
|
24805
|
-
return
|
|
25924
|
+
return Effect55.gen(function* () {
|
|
24806
25925
|
const text = yield* tryPromise(() => readFile(filePath, "utf8"));
|
|
24807
25926
|
const queries = parseBlocklistText(text);
|
|
24808
25927
|
const items = [];
|
|
24809
25928
|
for (const query of queries) {
|
|
24810
25929
|
const blocked = yield* tryPromise(() => addBlock(accountId, query)).pipe(
|
|
24811
|
-
|
|
24812
|
-
|
|
24813
|
-
(error) =>
|
|
25930
|
+
Effect55.map((result2) => ({ ok: true, result: result2 })),
|
|
25931
|
+
Effect55.catchAll(
|
|
25932
|
+
(error) => Effect55.succeed({ ok: false, error })
|
|
24814
25933
|
)
|
|
24815
25934
|
);
|
|
24816
25935
|
if (!blocked.ok) {
|
|
@@ -24866,7 +25985,7 @@ function registerModerationCommands2(context) {
|
|
|
24866
25985
|
}
|
|
24867
25986
|
|
|
24868
25987
|
// src/lib/link-index.ts
|
|
24869
|
-
import { Effect as
|
|
25988
|
+
import { Effect as Effect56 } from "effect";
|
|
24870
25989
|
var DEFAULT_EXPAND_CONCURRENCY = 12;
|
|
24871
25990
|
function parseJsonField5(value, fallback) {
|
|
24872
25991
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -25028,7 +26147,7 @@ function expandWithConcurrencyEffect(db, urls, options) {
|
|
|
25028
26147
|
misses: 0,
|
|
25029
26148
|
errors: 0
|
|
25030
26149
|
};
|
|
25031
|
-
return
|
|
26150
|
+
return Effect56.forEach(
|
|
25032
26151
|
urls,
|
|
25033
26152
|
(url) => tryPromise(
|
|
25034
26153
|
() => expandUrls([url], {
|
|
@@ -25037,7 +26156,7 @@ function expandWithConcurrencyEffect(db, urls, options) {
|
|
|
25037
26156
|
timeoutMs: options.timeoutMs
|
|
25038
26157
|
})
|
|
25039
26158
|
).pipe(
|
|
25040
|
-
|
|
26159
|
+
Effect56.map((results) => {
|
|
25041
26160
|
const result = results[0];
|
|
25042
26161
|
if (result.source === "network") {
|
|
25043
26162
|
counts.networkExpansions++;
|
|
@@ -25054,10 +26173,10 @@ function expandWithConcurrencyEffect(db, urls, options) {
|
|
|
25054
26173
|
})
|
|
25055
26174
|
),
|
|
25056
26175
|
{ concurrency }
|
|
25057
|
-
).pipe(
|
|
26176
|
+
).pipe(Effect56.as(counts));
|
|
25058
26177
|
}
|
|
25059
26178
|
function backfillLinkIndexEffect(options = {}) {
|
|
25060
|
-
return
|
|
26179
|
+
return Effect56.gen(function* () {
|
|
25061
26180
|
const db = getNativeDb({ seedDemoData: false });
|
|
25062
26181
|
const { occurrences, entityExpansions } = rebuildOccurrences(
|
|
25063
26182
|
db,
|
|
@@ -25352,7 +26471,7 @@ import {
|
|
|
25352
26471
|
} from "node:fs";
|
|
25353
26472
|
import { appendFile, copyFile, rename, rm, writeFile } from "node:fs/promises";
|
|
25354
26473
|
import path11 from "node:path";
|
|
25355
|
-
import { Effect as
|
|
26474
|
+
import { Effect as Effect57 } from "effect";
|
|
25356
26475
|
var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
|
|
25357
26476
|
var PBS_PREFIXES = [
|
|
25358
26477
|
"/media/",
|
|
@@ -25371,13 +26490,13 @@ function toMediaError(error) {
|
|
|
25371
26490
|
return error instanceof Error ? error : new Error(String(error));
|
|
25372
26491
|
}
|
|
25373
26492
|
function tryMediaSync(try_) {
|
|
25374
|
-
return
|
|
26493
|
+
return Effect57.try({
|
|
25375
26494
|
try: try_,
|
|
25376
26495
|
catch: toMediaError
|
|
25377
26496
|
});
|
|
25378
26497
|
}
|
|
25379
26498
|
function tryMediaPromise(try_) {
|
|
25380
|
-
return
|
|
26499
|
+
return Effect57.tryPromise({
|
|
25381
26500
|
try: try_,
|
|
25382
26501
|
catch: toMediaError
|
|
25383
26502
|
});
|
|
@@ -25642,7 +26761,7 @@ function archivePathFor(item, mediaOriginalsDir) {
|
|
|
25642
26761
|
return archiveTweetDirs(mediaOriginalsDir, item.tweetId).map((tweetDir) => path11.join(tweetDir, fileName)).find((archivePath) => existsSync5(archivePath)) ?? null;
|
|
25643
26762
|
}
|
|
25644
26763
|
function reuseFromArchiveEffect(item, mediaOriginalsDir, maxBytes) {
|
|
25645
|
-
return
|
|
26764
|
+
return Effect57.gen(function* () {
|
|
25646
26765
|
const archivePath = archivePathFor(item, mediaOriginalsDir);
|
|
25647
26766
|
if (!archivePath || !existsSync5(archivePath)) return null;
|
|
25648
26767
|
const bytes = fileSize(archivePath);
|
|
@@ -25669,15 +26788,15 @@ function contentRangeTotal(response) {
|
|
|
25669
26788
|
return total ? Number(total) : null;
|
|
25670
26789
|
}
|
|
25671
26790
|
function writeResponseBodyEffect(response, tmpPath, append, maxBytes, initialBytes) {
|
|
25672
|
-
return
|
|
26791
|
+
return Effect57.gen(function* () {
|
|
25673
26792
|
if (!response.body)
|
|
25674
|
-
return yield*
|
|
26793
|
+
return yield* Effect57.fail(new Error("missing response body"));
|
|
25675
26794
|
let bytes = 0;
|
|
25676
26795
|
if (!append) {
|
|
25677
26796
|
yield* tryMediaPromise(() => writeFile(tmpPath, Buffer.alloc(0)));
|
|
25678
26797
|
}
|
|
25679
26798
|
const reader = response.body.getReader();
|
|
25680
|
-
const result = yield*
|
|
26799
|
+
const result = yield* Effect57.gen(function* () {
|
|
25681
26800
|
for (; ; ) {
|
|
25682
26801
|
const { done, value } = yield* tryMediaPromise(() => reader.read());
|
|
25683
26802
|
if (done) {
|
|
@@ -25687,31 +26806,31 @@ function writeResponseBodyEffect(response, tmpPath, append, maxBytes, initialByt
|
|
|
25687
26806
|
bytes += chunk.length;
|
|
25688
26807
|
if (initialBytes + bytes > maxBytes) {
|
|
25689
26808
|
yield* tryMediaPromise(() => rm(tmpPath, { force: true })).pipe(
|
|
25690
|
-
|
|
26809
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25691
26810
|
);
|
|
25692
|
-
return yield*
|
|
26811
|
+
return yield* Effect57.fail(new Error("max-bytes"));
|
|
25693
26812
|
}
|
|
25694
26813
|
yield* tryMediaPromise(() => appendFile(tmpPath, chunk));
|
|
25695
26814
|
}
|
|
25696
26815
|
return bytes;
|
|
25697
26816
|
}).pipe(
|
|
25698
|
-
|
|
25699
|
-
|
|
25700
|
-
(error) =>
|
|
26817
|
+
Effect57.map((writtenBytes) => ({ ok: true, writtenBytes })),
|
|
26818
|
+
Effect57.catchAll(
|
|
26819
|
+
(error) => Effect57.gen(function* () {
|
|
25701
26820
|
if (error.message === "max-bytes") {
|
|
25702
26821
|
yield* tryMediaPromise(() => rm(tmpPath, { force: true })).pipe(
|
|
25703
|
-
|
|
26822
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25704
26823
|
);
|
|
25705
26824
|
}
|
|
25706
26825
|
yield* tryMediaPromise(() => reader.cancel(error)).pipe(
|
|
25707
|
-
|
|
26826
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25708
26827
|
);
|
|
25709
26828
|
return { error, ok: false };
|
|
25710
26829
|
})
|
|
25711
26830
|
),
|
|
25712
|
-
|
|
26831
|
+
Effect57.ensuring(Effect57.sync(() => reader.releaseLock()))
|
|
25713
26832
|
);
|
|
25714
|
-
if (!result.ok) return yield*
|
|
26833
|
+
if (!result.ok) return yield* Effect57.fail(result.error);
|
|
25715
26834
|
return result.writtenBytes;
|
|
25716
26835
|
});
|
|
25717
26836
|
}
|
|
@@ -25723,13 +26842,13 @@ function fetchOneEffect({
|
|
|
25723
26842
|
userAgent,
|
|
25724
26843
|
maxBytes
|
|
25725
26844
|
}) {
|
|
25726
|
-
return
|
|
26845
|
+
return Effect57.gen(function* () {
|
|
25727
26846
|
let rateLimited = false;
|
|
25728
26847
|
for (let attempt = 0; attempt <= retryMax; attempt += 1) {
|
|
25729
26848
|
const partialBytes = item.kind === "image" ? 0 : fileSize(item.tmpPath);
|
|
25730
26849
|
if (partialBytes > maxBytes) {
|
|
25731
26850
|
yield* tryMediaPromise(() => rm(item.tmpPath, { force: true })).pipe(
|
|
25732
|
-
|
|
26851
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25733
26852
|
);
|
|
25734
26853
|
return fail(item, "max-bytes");
|
|
25735
26854
|
}
|
|
@@ -25741,9 +26860,9 @@ function fetchOneEffect({
|
|
|
25741
26860
|
}
|
|
25742
26861
|
})
|
|
25743
26862
|
).pipe(
|
|
25744
|
-
|
|
25745
|
-
|
|
25746
|
-
(error) =>
|
|
26863
|
+
Effect57.map((response2) => ({ ok: true, response: response2 })),
|
|
26864
|
+
Effect57.catchAll(
|
|
26865
|
+
(error) => Effect57.succeed({ error, ok: false })
|
|
25747
26866
|
)
|
|
25748
26867
|
);
|
|
25749
26868
|
if (!responseResult.ok) {
|
|
@@ -25754,7 +26873,7 @@ function fetchOneEffect({
|
|
|
25754
26873
|
rateLimited = true;
|
|
25755
26874
|
if (attempt < retryMax) {
|
|
25756
26875
|
yield* tryMediaPromise(() => sleep(1e3 * 2 ** attempt)).pipe(
|
|
25757
|
-
|
|
26876
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25758
26877
|
);
|
|
25759
26878
|
continue;
|
|
25760
26879
|
}
|
|
@@ -25766,7 +26885,7 @@ function fetchOneEffect({
|
|
|
25766
26885
|
const expectedTotal = contentRangeTotal(response) ?? (contentLength(response) ?? 0) + (response.status === 206 ? partialBytes : 0);
|
|
25767
26886
|
if (expectedTotal > maxBytes) {
|
|
25768
26887
|
yield* tryMediaPromise(() => rm(item.tmpPath, { force: true })).pipe(
|
|
25769
|
-
|
|
26888
|
+
Effect57.catchAll(() => Effect57.void)
|
|
25770
26889
|
);
|
|
25771
26890
|
return fail(item, "max-bytes");
|
|
25772
26891
|
}
|
|
@@ -25778,9 +26897,9 @@ function fetchOneEffect({
|
|
|
25778
26897
|
maxBytes,
|
|
25779
26898
|
append ? partialBytes : 0
|
|
25780
26899
|
).pipe(
|
|
25781
|
-
|
|
25782
|
-
|
|
25783
|
-
(error) =>
|
|
26900
|
+
Effect57.map((bytes) => ({ bytes, ok: true })),
|
|
26901
|
+
Effect57.catchAll(
|
|
26902
|
+
(error) => Effect57.succeed({ error, ok: false })
|
|
25784
26903
|
)
|
|
25785
26904
|
);
|
|
25786
26905
|
if (!bytesResult.ok) {
|
|
@@ -25854,7 +26973,7 @@ function runGroupEffect(items, parallel, pacingMs, now2, sleep, worker) {
|
|
|
25854
26973
|
);
|
|
25855
26974
|
})
|
|
25856
26975
|
);
|
|
25857
|
-
return
|
|
26976
|
+
return Effect57.forEach(pacedItems, runPaced, {
|
|
25858
26977
|
concurrency: Math.min(parallel, items.length),
|
|
25859
26978
|
discard: true
|
|
25860
26979
|
});
|
|
@@ -25863,7 +26982,7 @@ function fetchTweetMedia(options = {}) {
|
|
|
25863
26982
|
return runEffectPromise(fetchTweetMediaEffect(options));
|
|
25864
26983
|
}
|
|
25865
26984
|
function fetchTweetMediaEffect(options = {}) {
|
|
25866
|
-
return
|
|
26985
|
+
return Effect57.gen(function* () {
|
|
25867
26986
|
const now2 = options.now ?? Date.now;
|
|
25868
26987
|
const startedAt = now2();
|
|
25869
26988
|
const sleep = options.sleep ?? defaultSleep;
|
|
@@ -25930,7 +27049,7 @@ function fetchTweetMediaEffect(options = {}) {
|
|
|
25930
27049
|
retryMax,
|
|
25931
27050
|
userAgent,
|
|
25932
27051
|
maxBytes
|
|
25933
|
-
}).pipe(
|
|
27052
|
+
}).pipe(Effect57.map((fetched) => applyFetched(result, fetched)))
|
|
25934
27053
|
);
|
|
25935
27054
|
yield* runGroupEffect(
|
|
25936
27055
|
httpCandidates.filter((item) => item.kind === "image"),
|
|
@@ -25978,9 +27097,9 @@ function formatMediaFetchResult(result) {
|
|
|
25978
27097
|
}
|
|
25979
27098
|
|
|
25980
27099
|
// src/lib/whois.ts
|
|
25981
|
-
import { Effect as
|
|
27100
|
+
import { Effect as Effect58 } from "effect";
|
|
25982
27101
|
function trySync18(try_) {
|
|
25983
|
-
return
|
|
27102
|
+
return Effect58.try({
|
|
25984
27103
|
try: try_,
|
|
25985
27104
|
catch: (cause) => cause
|
|
25986
27105
|
});
|
|
@@ -26511,7 +27630,7 @@ function loadWhoisConversations(query, options, includeDms, context, limit) {
|
|
|
26511
27630
|
return [...merged.values()];
|
|
26512
27631
|
}
|
|
26513
27632
|
function runWhoisEffect(query, options = {}) {
|
|
26514
|
-
return
|
|
27633
|
+
return Effect58.gen(function* () {
|
|
26515
27634
|
const includeDms = options.dms ?? true;
|
|
26516
27635
|
const includeTweets = options.tweets ?? false;
|
|
26517
27636
|
const limit = options.limit ?? 10;
|
|
@@ -26758,7 +27877,7 @@ function registerSearchCommands({
|
|
|
26758
27877
|
parsePositiveIntegerOption: parsePositiveIntegerOption2
|
|
26759
27878
|
}) {
|
|
26760
27879
|
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(
|
|
27880
|
+
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
27881
|
"--min-likes <n>",
|
|
26763
27882
|
"Override the low-quality like threshold (default 50)"
|
|
26764
27883
|
).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 +27887,18 @@ function registerSearchCommands({
|
|
|
26768
27887
|
);
|
|
26769
27888
|
if (options.minLikes !== void 0 && minLikes === void 0) return;
|
|
26770
27889
|
await autoUpdateBeforeRead2();
|
|
27890
|
+
const selectedList = options.list || options.listId ? resolveStoredXListSelector({
|
|
27891
|
+
account: options.account,
|
|
27892
|
+
name: options.list,
|
|
27893
|
+
listId: options.listId
|
|
27894
|
+
}) : void 0;
|
|
26771
27895
|
const replyFilter = options.replied ? "replied" : options.unreplied ? "unreplied" : "all";
|
|
26772
27896
|
print2(
|
|
26773
27897
|
listTimelineItems({
|
|
26774
27898
|
resource: options.resource === "mentions" ? "mentions" : options.resource === "authored" ? "authored" : "home",
|
|
27899
|
+
account: options.account,
|
|
27900
|
+
listAccountId: selectedList?.accountId,
|
|
27901
|
+
listId: selectedList?.listId,
|
|
26775
27902
|
search: query,
|
|
26776
27903
|
replyFilter,
|
|
26777
27904
|
since: options.since,
|
|
@@ -26954,7 +28081,7 @@ import { Readable as Readable2 } from "node:stream";
|
|
|
26954
28081
|
import { pathToFileURL } from "node:url";
|
|
26955
28082
|
|
|
26956
28083
|
// src/lib/http-effect.ts
|
|
26957
|
-
import { Effect as
|
|
28084
|
+
import { Effect as Effect59 } from "effect";
|
|
26958
28085
|
var LOCAL_WEB_PEER_HEADER = "x-birdclaw-local-peer";
|
|
26959
28086
|
|
|
26960
28087
|
// src/lib/production-server.ts
|
|
@@ -27228,7 +28355,7 @@ function registerStorageCommands({
|
|
|
27228
28355
|
}
|
|
27229
28356
|
|
|
27230
28357
|
// src/lib/authored-live.ts
|
|
27231
|
-
import { Effect as
|
|
28358
|
+
import { Effect as Effect60 } from "effect";
|
|
27232
28359
|
var AuthoredSyncError = class extends Error {
|
|
27233
28360
|
constructor(message, exitCode) {
|
|
27234
28361
|
super(message);
|
|
@@ -27470,10 +28597,10 @@ function resolveAuthoredIdentityEffect({
|
|
|
27470
28597
|
account,
|
|
27471
28598
|
db
|
|
27472
28599
|
}) {
|
|
27473
|
-
return
|
|
28600
|
+
return Effect60.gen(function* () {
|
|
27474
28601
|
const status = yield* liveTransportGateway.xurl.getTransportStatus();
|
|
27475
28602
|
if (status.availableTransport !== "xurl") {
|
|
27476
|
-
return yield*
|
|
28603
|
+
return yield* Effect60.fail(new AuthoredSyncError(status.statusText, 4));
|
|
27477
28604
|
}
|
|
27478
28605
|
const resolvedAccount = yield* trySync(
|
|
27479
28606
|
() => resolveLiveSyncAccount(db, account)
|
|
@@ -27488,7 +28615,7 @@ function resolveAuthoredIdentityEffect({
|
|
|
27488
28615
|
const authenticated = yield* liveTransportGateway.xurl.lookupAuthenticatedUser();
|
|
27489
28616
|
const authenticatedUser = userFromAuthenticatedPayload(authenticated);
|
|
27490
28617
|
if (!authenticatedUser?.id) {
|
|
27491
|
-
return yield*
|
|
28618
|
+
return yield* Effect60.fail(
|
|
27492
28619
|
new AuthoredSyncError(
|
|
27493
28620
|
"Could not resolve authenticated Twitter user id",
|
|
27494
28621
|
4
|
|
@@ -27496,7 +28623,7 @@ function resolveAuthoredIdentityEffect({
|
|
|
27496
28623
|
);
|
|
27497
28624
|
}
|
|
27498
28625
|
if (normalizeUsername(authenticatedUser.username) !== normalizeUsername(resolvedAccount.username)) {
|
|
27499
|
-
return yield*
|
|
28626
|
+
return yield* Effect60.fail(
|
|
27500
28627
|
new AuthoredSyncError(
|
|
27501
28628
|
`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
28629
|
4
|
|
@@ -27795,9 +28922,9 @@ function syncAuthoredTweetsEffect({
|
|
|
27795
28922
|
sinceId,
|
|
27796
28923
|
untilId
|
|
27797
28924
|
}) {
|
|
27798
|
-
return
|
|
28925
|
+
return Effect60.gen(function* () {
|
|
27799
28926
|
if (mode !== "xurl") {
|
|
27800
|
-
return yield*
|
|
28927
|
+
return yield* Effect60.fail(
|
|
27801
28928
|
new Error("authored sync only supports --mode xurl")
|
|
27802
28929
|
);
|
|
27803
28930
|
}
|
|
@@ -27860,7 +28987,7 @@ function syncAuthoredTweetsEffect({
|
|
|
27860
28987
|
sourceUser
|
|
27861
28988
|
})
|
|
27862
28989
|
).pipe(
|
|
27863
|
-
|
|
28990
|
+
Effect60.flatMap(
|
|
27864
28991
|
() => trySync(() => {
|
|
27865
28992
|
newestSeenId = maxTweetId(
|
|
27866
28993
|
newestSeenId,
|
|
@@ -27963,6 +29090,32 @@ function registerSyncCommands({
|
|
|
27963
29090
|
autoSyncAfterWrite: autoSyncAfterWrite2
|
|
27964
29091
|
}) {
|
|
27965
29092
|
const syncCommand = program2.command("sync").description("Refresh live Twitter collections into the local store");
|
|
29093
|
+
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) => {
|
|
29094
|
+
try {
|
|
29095
|
+
const result = await syncXLists({
|
|
29096
|
+
account: options.account,
|
|
29097
|
+
mode: options.mode,
|
|
29098
|
+
maxLists: Number(options.maxLists),
|
|
29099
|
+
memberLimit: Number(options.memberLimit),
|
|
29100
|
+
maxMemberPages: Number(options.maxMemberPages),
|
|
29101
|
+
delayMs: Number(options.delayMs)
|
|
29102
|
+
});
|
|
29103
|
+
await autoSyncAfterWrite2();
|
|
29104
|
+
print2(result, true);
|
|
29105
|
+
if (!result.ok) process.exitCode = 5;
|
|
29106
|
+
} catch (error) {
|
|
29107
|
+
print2(
|
|
29108
|
+
{
|
|
29109
|
+
ok: false,
|
|
29110
|
+
kind: "lists",
|
|
29111
|
+
mode: options.mode,
|
|
29112
|
+
error: errorMessage(error)
|
|
29113
|
+
},
|
|
29114
|
+
true
|
|
29115
|
+
);
|
|
29116
|
+
process.exitCode = 1;
|
|
29117
|
+
}
|
|
29118
|
+
});
|
|
27966
29119
|
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
29120
|
const result = await syncHomeTimeline({
|
|
27968
29121
|
account: options.account,
|
|
@@ -28137,6 +29290,7 @@ registerMentionCommands(commandContext);
|
|
|
28137
29290
|
registerDirectMessageCommands(commandContext);
|
|
28138
29291
|
registerSyncCommands(commandContext);
|
|
28139
29292
|
registerJobCommands(commandContext);
|
|
29293
|
+
registerListCommands(commandContext);
|
|
28140
29294
|
registerModerationCommands2(commandContext);
|
|
28141
29295
|
registerComposeCommands(commandContext);
|
|
28142
29296
|
registerInboxCommand(commandContext);
|