birdclaw 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -1
- package/README.md +75 -5
- package/package.json +8 -2
- package/scripts/browser-perf.mjs +1 -0
- package/scripts/start-test-server.mjs +16 -3
- package/src/cli.ts +812 -37
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +37 -7
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/DmWorkspace.tsx +18 -8
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +818 -0
- package/src/components/ProfileAnalysisStream.tsx +428 -0
- package/src/components/ProfilePreview.tsx +120 -9
- package/src/components/SavedTimelineView.tsx +30 -8
- package/src/components/SyncNowButton.tsx +60 -25
- package/src/components/ThemeSlider.tsx +55 -50
- package/src/components/TimelineCard.tsx +240 -92
- package/src/components/TimelineRouteFrame.tsx +38 -8
- package/src/components/TweetMediaGrid.tsx +87 -38
- package/src/components/TweetRichText.tsx +45 -17
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +97 -13
- package/src/lib/account-sync-job.ts +666 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +128 -53
- package/src/lib/archive-finder.ts +78 -63
- package/src/lib/archive-import.ts +1593 -1291
- package/src/lib/authored-live.ts +262 -204
- package/src/lib/avatar-cache.ts +208 -43
- package/src/lib/backup.ts +1536 -954
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +582 -194
- package/src/lib/blocklist.ts +40 -23
- package/src/lib/blocks-write.ts +129 -80
- package/src/lib/blocks.ts +165 -97
- package/src/lib/bookmark-sync-job.ts +250 -160
- package/src/lib/config.ts +35 -2
- package/src/lib/conversation-surface.ts +79 -48
- package/src/lib/data-sources.ts +219 -0
- package/src/lib/db.ts +95 -4
- package/src/lib/dms-live.ts +720 -66
- package/src/lib/effect-runtime.ts +45 -0
- package/src/lib/follow-graph.ts +224 -180
- package/src/lib/geocoding.ts +296 -0
- package/src/lib/http-effect.ts +222 -0
- package/src/lib/inbox.ts +74 -43
- package/src/lib/link-index.ts +88 -76
- package/src/lib/link-insights.ts +24 -0
- package/src/lib/link-preview-metadata.ts +472 -52
- package/src/lib/location.ts +137 -0
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +445 -288
- package/src/lib/mentions-live.ts +549 -354
- package/src/lib/moderation-target.ts +102 -65
- package/src/lib/moderation-write.ts +77 -18
- package/src/lib/mutes-write.ts +129 -80
- package/src/lib/mutes.ts +8 -1
- package/src/lib/network-map.ts +382 -0
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +1317 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-analysis.ts +1272 -0
- package/src/lib/profile-bio-entities.ts +1 -1
- package/src/lib/profile-hydration.ts +124 -72
- package/src/lib/profile-replies.ts +60 -43
- package/src/lib/profile-resolver.ts +402 -294
- package/src/lib/queries.ts +983 -203
- package/src/lib/research.ts +165 -120
- package/src/lib/search-discussion.ts +1016 -0
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +325 -51
- package/src/lib/tweet-account-edges.ts +2 -0
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/tweet-render.ts +141 -1
- package/src/lib/tweet-search-live.ts +565 -0
- package/src/lib/types.ts +75 -3
- package/src/lib/ui.ts +31 -8
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +222 -149
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl-rate-limits.ts +267 -0
- package/src/lib/xurl.ts +1185 -405
- package/src/routeTree.gen.ts +273 -0
- package/src/routes/__root.tsx +24 -5
- package/src/routes/api/action.tsx +127 -78
- package/src/routes/api/avatar.tsx +39 -30
- package/src/routes/api/blocks.tsx +26 -23
- package/src/routes/api/conversation.tsx +25 -14
- package/src/routes/api/data-sources.tsx +24 -0
- package/src/routes/api/inbox.tsx +27 -21
- package/src/routes/api/link-insights.tsx +31 -25
- package/src/routes/api/link-preview.tsx +25 -21
- package/src/routes/api/network-map.tsx +55 -0
- package/src/routes/api/period-digest.tsx +133 -0
- package/src/routes/api/profile-analysis.tsx +152 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +80 -55
- package/src/routes/api/search-discussion.tsx +169 -0
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +75 -29
- package/src/routes/api/xurl-rate-limits.tsx +24 -0
- package/src/routes/data-sources.tsx +255 -0
- package/src/routes/discuss.tsx +419 -0
- package/src/routes/dms.tsx +95 -28
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/network-map.tsx +1035 -0
- package/src/routes/profile-analyze.tsx +112 -0
- package/src/routes/profiles.$handle.tsx +228 -0
- package/src/routes/rate-limits.tsx +309 -0
- package/src/routes/today.tsx +455 -0
- package/src/styles.css +22 -0
package/src/lib/whois.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
1
2
|
import { getNativeDb } from "./db";
|
|
3
|
+
import { runEffectPromise } from "./effect-runtime";
|
|
2
4
|
import {
|
|
3
5
|
ensureIdentitySearchIndexForDmProfiles,
|
|
4
6
|
syncIdentitySearchIndexForProfileIds,
|
|
5
7
|
} from "./identity-search-index";
|
|
6
8
|
import { fetchProfileBioEntities } from "./profile-bio-entities";
|
|
7
9
|
import { fetchProfileSnapshots } from "./profile-history";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
+
import { resolveProfilesForIdsEffect } from "./profile-resolver";
|
|
11
|
+
import type { ProfileResolveResult } from "./profile-resolver";
|
|
10
12
|
import { listDmConversations, listTimelineItems } from "./queries";
|
|
13
|
+
import { expandUrlsFromTextsEffect } from "./url-expansion";
|
|
11
14
|
import type {
|
|
12
15
|
DmConversationItem,
|
|
13
16
|
ProfileAffiliation,
|
|
@@ -61,7 +64,7 @@ export interface WhoisResult {
|
|
|
61
64
|
candidates: WhoisCandidate[];
|
|
62
65
|
relatedTweets: TimelineItem[];
|
|
63
66
|
urlExpansions: UrlExpansionItem[];
|
|
64
|
-
profileResolution?:
|
|
67
|
+
profileResolution?: ProfileResolveResult[];
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
export interface WhoisEvidenceSignal {
|
|
@@ -84,6 +87,13 @@ export interface WhoisEvidenceSignal {
|
|
|
84
87
|
source: "profile" | "affiliation" | "bio_entity" | "history" | "dm" | "url";
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
function trySync<T>(try_: () => T) {
|
|
91
|
+
return Effect.try({
|
|
92
|
+
try: try_,
|
|
93
|
+
catch: (cause) => cause,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
interface WhoisQueryIntent {
|
|
88
98
|
raw: string;
|
|
89
99
|
normalized: string;
|
|
@@ -771,160 +781,169 @@ function loadWhoisConversations(
|
|
|
771
781
|
return [...merged.values()];
|
|
772
782
|
}
|
|
773
783
|
|
|
774
|
-
export
|
|
784
|
+
export function runWhoisEffect(
|
|
775
785
|
query: string,
|
|
776
786
|
options: WhoisOptions = {},
|
|
777
|
-
):
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
includeDms,
|
|
786
|
-
context,
|
|
787
|
-
limit,
|
|
788
|
-
);
|
|
789
|
-
let profileResolution: WhoisResult["profileResolution"];
|
|
790
|
-
|
|
791
|
-
if (options.resolveProfiles ?? true) {
|
|
792
|
-
profileResolution = await resolveProfilesForIds(
|
|
793
|
-
conversations.map((item) => item.participant.id),
|
|
794
|
-
{
|
|
795
|
-
refresh: options.refreshProfileCache,
|
|
796
|
-
xurlFallback: options.xurlFallback ?? true,
|
|
797
|
-
},
|
|
798
|
-
);
|
|
799
|
-
conversations = loadWhoisConversations(
|
|
800
|
-
query,
|
|
801
|
-
options,
|
|
802
|
-
includeDms,
|
|
803
|
-
context,
|
|
804
|
-
limit,
|
|
787
|
+
): Effect.Effect<WhoisResult, unknown> {
|
|
788
|
+
return Effect.gen(function* () {
|
|
789
|
+
const includeDms = options.dms ?? true;
|
|
790
|
+
const includeTweets = options.tweets ?? false;
|
|
791
|
+
const limit = options.limit ?? 10;
|
|
792
|
+
const context = options.context ?? 4;
|
|
793
|
+
let conversations = yield* trySync(() =>
|
|
794
|
+
loadWhoisConversations(query, options, includeDms, context, limit),
|
|
805
795
|
);
|
|
806
|
-
|
|
807
|
-
syncIdentitySearchIndexForProfileIds(
|
|
808
|
-
getNativeDb(),
|
|
809
|
-
conversations.map((item) => item.participant.id),
|
|
810
|
-
);
|
|
796
|
+
let profileResolution: WhoisResult["profileResolution"];
|
|
811
797
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
798
|
+
if (options.resolveProfiles ?? true) {
|
|
799
|
+
profileResolution = yield* resolveProfilesForIdsEffect(
|
|
800
|
+
conversations.map((item) => item.participant.id),
|
|
801
|
+
{
|
|
802
|
+
refresh: options.refreshProfileCache,
|
|
803
|
+
xurlFallback: options.xurlFallback ?? true,
|
|
804
|
+
},
|
|
805
|
+
);
|
|
806
|
+
conversations = yield* trySync(() =>
|
|
807
|
+
loadWhoisConversations(query, options, includeDms, context, limit),
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
yield* trySync(() =>
|
|
811
|
+
syncIdentitySearchIndexForProfileIds(
|
|
812
|
+
getNativeDb(),
|
|
813
|
+
conversations.map((item) => item.participant.id),
|
|
814
|
+
),
|
|
815
|
+
);
|
|
828
816
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
817
|
+
const relatedTweets = includeTweets
|
|
818
|
+
? yield* trySync(() => [
|
|
819
|
+
...listTimelineItems({
|
|
820
|
+
resource: "home",
|
|
821
|
+
account: options.account,
|
|
822
|
+
search: query,
|
|
823
|
+
limit,
|
|
824
|
+
}),
|
|
825
|
+
...listTimelineItems({
|
|
826
|
+
resource: "mentions",
|
|
827
|
+
account: options.account,
|
|
828
|
+
search: query,
|
|
829
|
+
limit,
|
|
830
|
+
}),
|
|
831
|
+
])
|
|
843
832
|
: [];
|
|
844
|
-
for (const conversation of conversations) {
|
|
845
|
-
attachExpansionsToMatches(conversation, urlExpansions);
|
|
846
|
-
}
|
|
847
833
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
)
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
);
|
|
869
|
-
if (
|
|
870
|
-
!hasCurrentAffiliationMatch(
|
|
871
|
-
conversation.participant,
|
|
872
|
-
options.currentAffiliation,
|
|
873
|
-
)
|
|
874
|
-
) {
|
|
875
|
-
return null;
|
|
876
|
-
}
|
|
877
|
-
if (
|
|
878
|
-
!hasAffiliationEvidenceMatch(
|
|
879
|
-
conversation.participant,
|
|
880
|
-
score.profileEvidence,
|
|
881
|
-
options.affiliation,
|
|
882
|
-
)
|
|
883
|
-
) {
|
|
884
|
-
return null;
|
|
885
|
-
}
|
|
886
|
-
if (
|
|
887
|
-
options.excludeDomainOnly &&
|
|
888
|
-
!hasNonDomainEvidence(score.profileEvidence)
|
|
889
|
-
) {
|
|
890
|
-
return null;
|
|
891
|
-
}
|
|
892
|
-
return {
|
|
893
|
-
conversation,
|
|
894
|
-
confidence: score.confidence,
|
|
895
|
-
category: score.category,
|
|
896
|
-
reasons: score.reasons,
|
|
897
|
-
profileEvidence: score.profileEvidence,
|
|
898
|
-
evidence: (conversation.matches ?? []).map((match) => ({
|
|
899
|
-
messageId: match.message.id,
|
|
900
|
-
createdAt: match.message.createdAt,
|
|
901
|
-
direction: match.message.direction,
|
|
902
|
-
text: match.message.text,
|
|
903
|
-
...(match.urlExpansions
|
|
904
|
-
? { urlExpansions: match.urlExpansions }
|
|
905
|
-
: {}),
|
|
906
|
-
})),
|
|
907
|
-
};
|
|
908
|
-
})
|
|
909
|
-
.filter((candidate): candidate is WhoisCandidate => candidate !== null)
|
|
910
|
-
.sort((left, right) => {
|
|
911
|
-
if (right.confidence !== left.confidence) {
|
|
912
|
-
return right.confidence - left.confidence;
|
|
834
|
+
const texts = yield* trySync(() => [
|
|
835
|
+
...conversations.flatMap(getMessageTexts),
|
|
836
|
+
...conversations.flatMap((conversation) =>
|
|
837
|
+
[
|
|
838
|
+
conversation.participant.bio,
|
|
839
|
+
conversation.participant.url ?? "",
|
|
840
|
+
...getProfileBioUrls(conversation.participant),
|
|
841
|
+
].filter((text) => text.includes("https://t.co/")),
|
|
842
|
+
),
|
|
843
|
+
...relatedTweets.map((tweet) => tweet.text),
|
|
844
|
+
]);
|
|
845
|
+
const urlExpansions =
|
|
846
|
+
(options.expandUrls ?? true)
|
|
847
|
+
? yield* expandUrlsFromTextsEffect(texts, {
|
|
848
|
+
refresh: options.refreshUrlCache,
|
|
849
|
+
})
|
|
850
|
+
: [];
|
|
851
|
+
yield* trySync(() => {
|
|
852
|
+
for (const conversation of conversations) {
|
|
853
|
+
attachExpansionsToMatches(conversation, urlExpansions);
|
|
913
854
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
const candidates = yield* trySync(() => {
|
|
858
|
+
const profileIds = conversations.map(
|
|
859
|
+
(conversation) => conversation.participant.id,
|
|
917
860
|
);
|
|
918
|
-
|
|
919
|
-
|
|
861
|
+
const db = getNativeDb();
|
|
862
|
+
const bioEntitiesByProfile = fetchProfileBioEntities(db, profileIds);
|
|
863
|
+
const snapshotsByProfile = fetchProfileSnapshots(db, profileIds);
|
|
864
|
+
return conversations
|
|
865
|
+
.map((conversation): WhoisCandidate | null => {
|
|
866
|
+
const conversationExpansions = urlExpansions.filter((item) =>
|
|
867
|
+
getMessageTexts(conversation).some((text) =>
|
|
868
|
+
text.includes(item.url),
|
|
869
|
+
),
|
|
870
|
+
);
|
|
871
|
+
const profileId = conversation.participant.id;
|
|
872
|
+
const score = scoreCandidate(
|
|
873
|
+
query,
|
|
874
|
+
conversation,
|
|
875
|
+
conversationExpansions,
|
|
876
|
+
bioEntitiesByProfile.get(profileId) ?? [],
|
|
877
|
+
snapshotsByProfile.get(profileId) ?? [],
|
|
878
|
+
);
|
|
879
|
+
if (
|
|
880
|
+
!hasCurrentAffiliationMatch(
|
|
881
|
+
conversation.participant,
|
|
882
|
+
options.currentAffiliation,
|
|
883
|
+
)
|
|
884
|
+
) {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
if (
|
|
888
|
+
!hasAffiliationEvidenceMatch(
|
|
889
|
+
conversation.participant,
|
|
890
|
+
score.profileEvidence,
|
|
891
|
+
options.affiliation,
|
|
892
|
+
)
|
|
893
|
+
) {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
if (
|
|
897
|
+
options.excludeDomainOnly &&
|
|
898
|
+
!hasNonDomainEvidence(score.profileEvidence)
|
|
899
|
+
) {
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
return {
|
|
903
|
+
conversation,
|
|
904
|
+
confidence: score.confidence,
|
|
905
|
+
category: score.category,
|
|
906
|
+
reasons: score.reasons,
|
|
907
|
+
profileEvidence: score.profileEvidence,
|
|
908
|
+
evidence: (conversation.matches ?? []).map((match) => ({
|
|
909
|
+
messageId: match.message.id,
|
|
910
|
+
createdAt: match.message.createdAt,
|
|
911
|
+
direction: match.message.direction,
|
|
912
|
+
text: match.message.text,
|
|
913
|
+
...(match.urlExpansions
|
|
914
|
+
? { urlExpansions: match.urlExpansions }
|
|
915
|
+
: {}),
|
|
916
|
+
})),
|
|
917
|
+
};
|
|
918
|
+
})
|
|
919
|
+
.filter((candidate): candidate is WhoisCandidate => candidate !== null)
|
|
920
|
+
.sort((left, right) => {
|
|
921
|
+
if (right.confidence !== left.confidence) {
|
|
922
|
+
return right.confidence - left.confidence;
|
|
923
|
+
}
|
|
924
|
+
return (
|
|
925
|
+
new Date(right.conversation.lastMessageAt).getTime() -
|
|
926
|
+
new Date(left.conversation.lastMessageAt).getTime()
|
|
927
|
+
);
|
|
928
|
+
})
|
|
929
|
+
.slice(0, limit);
|
|
930
|
+
});
|
|
920
931
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
932
|
+
return {
|
|
933
|
+
query,
|
|
934
|
+
candidates,
|
|
935
|
+
relatedTweets,
|
|
936
|
+
urlExpansions,
|
|
937
|
+
...(profileResolution ? { profileResolution } : {}),
|
|
938
|
+
};
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
export function runWhois(
|
|
943
|
+
query: string,
|
|
944
|
+
options: WhoisOptions = {},
|
|
945
|
+
): Promise<WhoisResult> {
|
|
946
|
+
return runEffectPromise(runWhoisEffect(query, options));
|
|
928
947
|
}
|
|
929
948
|
|
|
930
949
|
const CATEGORY_LABELS: Record<WhoisCandidateCategory, string> = {
|
package/src/lib/x-web.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { getCookies } from "@steipete/sweet-cookie";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
3
5
|
|
|
4
6
|
const X_WEB_BEARER_TOKEN =
|
|
5
7
|
"AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA";
|
|
@@ -48,53 +50,70 @@ function pickCookieValue(
|
|
|
48
50
|
return matches[0]?.value ?? null;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
function resolveXWebCookiesEffect() {
|
|
54
|
+
return Effect.gen(function* () {
|
|
55
|
+
const envAuthToken = normalizeCookieValue(
|
|
56
|
+
process.env.AUTH_TOKEN ?? process.env.TWITTER_AUTH_TOKEN,
|
|
57
|
+
);
|
|
58
|
+
const envCt0 = normalizeCookieValue(
|
|
59
|
+
process.env.CT0 ?? process.env.TWITTER_CT0,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (envAuthToken && envCt0) {
|
|
63
|
+
return {
|
|
64
|
+
authToken: envAuthToken,
|
|
65
|
+
ct0: envCt0,
|
|
66
|
+
cookieHeader: buildCookieHeader(envAuthToken, envCt0),
|
|
67
|
+
source: "env",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const cookieResult = yield* tryPromise(() =>
|
|
72
|
+
getCookies({
|
|
73
|
+
url: "https://x.com/",
|
|
74
|
+
origins: [...X_WEB_ORIGINS],
|
|
75
|
+
names: [...X_WEB_COOKIE_NAMES],
|
|
76
|
+
browsers: ["safari", "chrome", "firefox"] satisfies CookieSource[],
|
|
77
|
+
mode: "merge",
|
|
78
|
+
timeoutMs: process.platform === "darwin" ? 30_000 : undefined,
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const authToken = pickCookieValue(cookieResult.cookies, "auth_token");
|
|
83
|
+
const ct0 = pickCookieValue(cookieResult.cookies, "ct0");
|
|
84
|
+
if (!authToken || !ct0) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
58
87
|
|
|
59
|
-
if (envAuthToken && envCt0) {
|
|
60
88
|
return {
|
|
61
|
-
authToken
|
|
62
|
-
ct0
|
|
63
|
-
cookieHeader: buildCookieHeader(
|
|
64
|
-
source: "
|
|
89
|
+
authToken,
|
|
90
|
+
ct0,
|
|
91
|
+
cookieHeader: buildCookieHeader(authToken, ct0),
|
|
92
|
+
source: "browser",
|
|
65
93
|
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const cookieResult = await getCookies({
|
|
69
|
-
url: "https://x.com/",
|
|
70
|
-
origins: [...X_WEB_ORIGINS],
|
|
71
|
-
names: [...X_WEB_COOKIE_NAMES],
|
|
72
|
-
browsers: ["safari", "chrome", "firefox"] satisfies CookieSource[],
|
|
73
|
-
mode: "merge",
|
|
74
|
-
timeoutMs: process.platform === "darwin" ? 30_000 : undefined,
|
|
75
94
|
});
|
|
76
|
-
|
|
77
|
-
const authToken = pickCookieValue(cookieResult.cookies, "auth_token");
|
|
78
|
-
const ct0 = pickCookieValue(cookieResult.cookies, "ct0");
|
|
79
|
-
if (!authToken || !ct0) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
authToken,
|
|
85
|
-
ct0,
|
|
86
|
-
cookieHeader: buildCookieHeader(authToken, ct0),
|
|
87
|
-
source: "browser",
|
|
88
|
-
};
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
|
|
97
|
+
function runXWebBlockMutationEffect(
|
|
92
98
|
path: string,
|
|
93
99
|
params: URLSearchParams,
|
|
94
100
|
action: string,
|
|
95
101
|
) {
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
return Effect.gen(function* () {
|
|
103
|
+
if (process.env.BIRDCLAW_DISABLE_LIVE_WRITES === "1") {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
output: `x-web ${action} unavailable: live writes disabled`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (process.env.BIRDCLAW_ALLOW_X_WEB_WRITES !== "1") {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
output: `x-web ${action} unavailable: unverified account`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const cookies = yield* resolveXWebCookiesEffect();
|
|
98
117
|
if (!cookies) {
|
|
99
118
|
return {
|
|
100
119
|
ok: false,
|
|
@@ -102,27 +121,29 @@ async function runXWebBlockMutation(
|
|
|
102
121
|
};
|
|
103
122
|
}
|
|
104
123
|
|
|
105
|
-
const response =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
const response = yield* tryPromise(() =>
|
|
125
|
+
fetch(`https://x.com/i/api/1.1/${path}`, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: {
|
|
128
|
+
accept: "*/*",
|
|
129
|
+
"accept-language": "en-US,en;q=0.9",
|
|
130
|
+
authorization: `Bearer ${X_WEB_BEARER_TOKEN}`,
|
|
131
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
132
|
+
cookie: cookies.cookieHeader,
|
|
133
|
+
origin: "https://x.com",
|
|
134
|
+
referer: "https://x.com/",
|
|
135
|
+
"user-agent": X_WEB_USER_AGENT,
|
|
136
|
+
"x-client-transaction-id": randomUUID().replaceAll("-", ""),
|
|
137
|
+
"x-csrf-token": cookies.ct0,
|
|
138
|
+
"x-twitter-active-user": "yes",
|
|
139
|
+
"x-twitter-auth-type": "OAuth2Session",
|
|
140
|
+
"x-twitter-client-language": "en",
|
|
141
|
+
},
|
|
142
|
+
body: params,
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const text = yield* tryPromise(() => response.text());
|
|
126
147
|
if (!response.ok) {
|
|
127
148
|
return {
|
|
128
149
|
ok: false,
|
|
@@ -134,19 +155,21 @@ async function runXWebBlockMutation(
|
|
|
134
155
|
ok: true,
|
|
135
156
|
output: `x-web ${action} ok via ${cookies.source}`,
|
|
136
157
|
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
}).pipe(
|
|
159
|
+
Effect.catchAll((error) =>
|
|
160
|
+
Effect.succeed({
|
|
161
|
+
ok: false,
|
|
162
|
+
output:
|
|
163
|
+
error instanceof Error
|
|
164
|
+
? `x-web ${action} failed: ${error.message}`
|
|
165
|
+
: `x-web ${action} failed`,
|
|
166
|
+
}),
|
|
167
|
+
),
|
|
168
|
+
);
|
|
146
169
|
}
|
|
147
170
|
|
|
148
|
-
export
|
|
149
|
-
return
|
|
171
|
+
export function blockUserViaXWebEffect(targetUserId: string) {
|
|
172
|
+
return runXWebBlockMutationEffect(
|
|
150
173
|
"blocks/create.json",
|
|
151
174
|
new URLSearchParams({
|
|
152
175
|
user_id: targetUserId,
|
|
@@ -156,8 +179,8 @@ export async function blockUserViaXWeb(targetUserId: string) {
|
|
|
156
179
|
);
|
|
157
180
|
}
|
|
158
181
|
|
|
159
|
-
export
|
|
160
|
-
return
|
|
182
|
+
export function unblockUserViaXWebEffect(targetUserId: string) {
|
|
183
|
+
return runXWebBlockMutationEffect(
|
|
161
184
|
"blocks/destroy.json",
|
|
162
185
|
new URLSearchParams({
|
|
163
186
|
user_id: targetUserId,
|
|
@@ -166,3 +189,11 @@ export async function unblockUserViaXWeb(targetUserId: string) {
|
|
|
166
189
|
"unblock",
|
|
167
190
|
);
|
|
168
191
|
}
|
|
192
|
+
|
|
193
|
+
export function blockUserViaXWeb(targetUserId: string) {
|
|
194
|
+
return runEffectPromise(blockUserViaXWebEffect(targetUserId));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function unblockUserViaXWeb(targetUserId: string) {
|
|
198
|
+
return runEffectPromise(unblockUserViaXWebEffect(targetUserId));
|
|
199
|
+
}
|