birdclaw 0.5.0 → 0.6.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +55 -5
  3. package/bin/birdclaw.mjs +50 -11
  4. package/package.json +9 -7
  5. package/public/birdclaw-mark.png +0 -0
  6. package/scripts/browser-perf.mjs +400 -0
  7. package/scripts/build-docs-site.mjs +940 -0
  8. package/scripts/docs-site-assets.mjs +311 -0
  9. package/scripts/run-vitest.mjs +21 -0
  10. package/scripts/sanitize-node-options.mjs +23 -0
  11. package/scripts/start-test-server.mjs +42 -0
  12. package/src/cli.ts +422 -14
  13. package/src/components/AccountSwitcher.tsx +186 -0
  14. package/src/components/AppNav.tsx +29 -9
  15. package/src/components/AvatarChip.tsx +9 -3
  16. package/src/components/BrandMark.tsx +67 -0
  17. package/src/components/ConversationThread.tsx +11 -10
  18. package/src/components/DmWorkspace.tsx +39 -14
  19. package/src/components/FeedState.tsx +147 -0
  20. package/src/components/InboxCard.tsx +14 -2
  21. package/src/components/LinkPreviewCard.tsx +40 -18
  22. package/src/components/MarkdownViewer.tsx +452 -0
  23. package/src/components/SavedTimelineView.tsx +64 -56
  24. package/src/components/SyncNowButton.tsx +137 -0
  25. package/src/components/ThemeSlider.tsx +49 -93
  26. package/src/components/TimelineCard.tsx +364 -136
  27. package/src/components/TimelineRouteFrame.tsx +170 -0
  28. package/src/components/TweetMediaGrid.tsx +170 -24
  29. package/src/components/TweetRichText.tsx +28 -13
  30. package/src/components/account-selection.ts +64 -0
  31. package/src/components/useTimelineRouteData.ts +153 -0
  32. package/src/lib/account-sync-job.ts +654 -0
  33. package/src/lib/actions-transport.ts +216 -146
  34. package/src/lib/api-client.ts +304 -0
  35. package/src/lib/archive-finder.ts +72 -53
  36. package/src/lib/archive-import.ts +1377 -1298
  37. package/src/lib/authored-live.ts +261 -204
  38. package/src/lib/avatar-cache.ts +159 -44
  39. package/src/lib/backup.ts +1532 -951
  40. package/src/lib/bird-actions.ts +139 -57
  41. package/src/lib/bird-command.ts +101 -28
  42. package/src/lib/bird.ts +549 -194
  43. package/src/lib/blocklist.ts +40 -23
  44. package/src/lib/blocks-write.ts +129 -80
  45. package/src/lib/blocks.ts +165 -97
  46. package/src/lib/bookmark-sync-job.ts +250 -160
  47. package/src/lib/conversation-surface.ts +205 -0
  48. package/src/lib/db.ts +35 -3
  49. package/src/lib/dms-live.ts +720 -66
  50. package/src/lib/effect-runtime.ts +45 -0
  51. package/src/lib/follow-graph.ts +224 -180
  52. package/src/lib/http-effect.ts +222 -0
  53. package/src/lib/inbox.ts +74 -43
  54. package/src/lib/link-index.ts +88 -76
  55. package/src/lib/link-insights.ts +24 -0
  56. package/src/lib/link-preview-metadata.ts +472 -52
  57. package/src/lib/media-fetch.ts +286 -213
  58. package/src/lib/mention-threads-live.ts +352 -288
  59. package/src/lib/mentions-live.ts +390 -342
  60. package/src/lib/moderation-target.ts +102 -65
  61. package/src/lib/moderation-write.ts +77 -18
  62. package/src/lib/mutes-write.ts +129 -80
  63. package/src/lib/mutes.ts +8 -1
  64. package/src/lib/openai.ts +84 -53
  65. package/src/lib/period-digest.ts +953 -0
  66. package/src/lib/profile-affiliation-hydration.ts +93 -54
  67. package/src/lib/profile-hydration.ts +124 -72
  68. package/src/lib/profile-replies.ts +60 -43
  69. package/src/lib/profile-resolver.ts +402 -294
  70. package/src/lib/queries.ts +1024 -189
  71. package/src/lib/research.ts +165 -120
  72. package/src/lib/sqlite.ts +1 -0
  73. package/src/lib/timeline-collections-live.ts +204 -167
  74. package/src/lib/timeline-live.ts +60 -39
  75. package/src/lib/tweet-lookup.ts +30 -19
  76. package/src/lib/types.ts +38 -1
  77. package/src/lib/ui.ts +41 -10
  78. package/src/lib/url-expansion.ts +226 -55
  79. package/src/lib/url-safety.ts +220 -0
  80. package/src/lib/web-sync.ts +511 -0
  81. package/src/lib/whois.ts +166 -147
  82. package/src/lib/x-web.ts +102 -71
  83. package/src/lib/xurl.ts +681 -411
  84. package/src/routeTree.gen.ts +63 -0
  85. package/src/routes/__root.tsx +25 -5
  86. package/src/routes/api/action.tsx +127 -78
  87. package/src/routes/api/avatar.tsx +39 -30
  88. package/src/routes/api/blocks.tsx +26 -23
  89. package/src/routes/api/conversation.tsx +25 -14
  90. package/src/routes/api/inbox.tsx +27 -21
  91. package/src/routes/api/link-insights.tsx +31 -25
  92. package/src/routes/api/link-preview.tsx +25 -21
  93. package/src/routes/api/period-digest.tsx +123 -0
  94. package/src/routes/api/profile-hydrate.tsx +31 -28
  95. package/src/routes/api/query.tsx +79 -55
  96. package/src/routes/api/status.tsx +18 -10
  97. package/src/routes/api/sync.tsx +105 -0
  98. package/src/routes/dms.tsx +195 -55
  99. package/src/routes/inbox.tsx +32 -19
  100. package/src/routes/index.tsx +21 -127
  101. package/src/routes/links.tsx +50 -5
  102. package/src/routes/mentions.tsx +21 -127
  103. package/src/routes/today.tsx +441 -0
  104. package/src/styles.css +74 -11
  105. package/vite.config.ts +8 -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 { expandUrlsFromTexts } from "./url-expansion";
9
- import { resolveProfilesForIds } from "./profile-resolver";
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?: Awaited<ReturnType<typeof resolveProfilesForIds>>;
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 async function runWhois(
784
+ export function runWhoisEffect(
775
785
  query: string,
776
786
  options: WhoisOptions = {},
777
- ): Promise<WhoisResult> {
778
- const includeDms = options.dms ?? true;
779
- const includeTweets = options.tweets ?? false;
780
- const limit = options.limit ?? 10;
781
- const context = options.context ?? 4;
782
- let conversations = loadWhoisConversations(
783
- query,
784
- options,
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
- const relatedTweets = includeTweets
813
- ? [
814
- ...listTimelineItems({
815
- resource: "home",
816
- account: options.account,
817
- search: query,
818
- limit,
819
- }),
820
- ...listTimelineItems({
821
- resource: "mentions",
822
- account: options.account,
823
- search: query,
824
- limit,
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
- const texts = [
830
- ...conversations.flatMap(getMessageTexts),
831
- ...conversations.flatMap((conversation) =>
832
- [
833
- conversation.participant.bio,
834
- conversation.participant.url ?? "",
835
- ...getProfileBioUrls(conversation.participant),
836
- ].filter((text) => text.includes("https://t.co/")),
837
- ),
838
- ...relatedTweets.map((tweet) => tweet.text),
839
- ];
840
- const urlExpansions =
841
- (options.expandUrls ?? true)
842
- ? await expandUrlsFromTexts(texts, { refresh: options.refreshUrlCache })
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
- const profileIds = conversations.map(
849
- (conversation) => conversation.participant.id,
850
- );
851
- const bioEntitiesByProfile = fetchProfileBioEntities(
852
- getNativeDb(),
853
- profileIds,
854
- );
855
- const snapshotsByProfile = fetchProfileSnapshots(getNativeDb(), profileIds);
856
- const candidates = conversations
857
- .map((conversation): WhoisCandidate | null => {
858
- const conversationExpansions = urlExpansions.filter((item) =>
859
- getMessageTexts(conversation).some((text) => text.includes(item.url)),
860
- );
861
- const profileId = conversation.participant.id;
862
- const score = scoreCandidate(
863
- query,
864
- conversation,
865
- conversationExpansions,
866
- bioEntitiesByProfile.get(profileId) ?? [],
867
- snapshotsByProfile.get(profileId) ?? [],
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
- return (
915
- new Date(right.conversation.lastMessageAt).getTime() -
916
- new Date(left.conversation.lastMessageAt).getTime()
855
+ });
856
+
857
+ const candidates = yield* trySync(() => {
858
+ const profileIds = conversations.map(
859
+ (conversation) => conversation.participant.id,
917
860
  );
918
- })
919
- .slice(0, limit);
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
- return {
922
- query,
923
- candidates,
924
- relatedTweets,
925
- urlExpansions,
926
- ...(profileResolution ? { profileResolution } : {}),
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
- async function resolveXWebCookies() {
52
- const envAuthToken = normalizeCookieValue(
53
- process.env.AUTH_TOKEN ?? process.env.TWITTER_AUTH_TOKEN,
54
- );
55
- const envCt0 = normalizeCookieValue(
56
- process.env.CT0 ?? process.env.TWITTER_CT0,
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: envAuthToken,
62
- ct0: envCt0,
63
- cookieHeader: buildCookieHeader(envAuthToken, envCt0),
64
- source: "env",
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
- async function runXWebBlockMutation(
97
+ function runXWebBlockMutationEffect(
92
98
  path: string,
93
99
  params: URLSearchParams,
94
100
  action: string,
95
101
  ) {
96
- try {
97
- const cookies = await resolveXWebCookies();
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 = await fetch(`https://x.com/i/api/1.1/${path}`, {
106
- method: "POST",
107
- headers: {
108
- accept: "*/*",
109
- "accept-language": "en-US,en;q=0.9",
110
- authorization: `Bearer ${X_WEB_BEARER_TOKEN}`,
111
- "content-type": "application/x-www-form-urlencoded",
112
- cookie: cookies.cookieHeader,
113
- origin: "https://x.com",
114
- referer: "https://x.com/",
115
- "user-agent": X_WEB_USER_AGENT,
116
- "x-client-transaction-id": randomUUID().replaceAll("-", ""),
117
- "x-csrf-token": cookies.ct0,
118
- "x-twitter-active-user": "yes",
119
- "x-twitter-auth-type": "OAuth2Session",
120
- "x-twitter-client-language": "en",
121
- },
122
- body: params,
123
- });
124
-
125
- const text = await response.text();
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
- } catch (error) {
138
- return {
139
- ok: false,
140
- output:
141
- error instanceof Error
142
- ? `x-web ${action} failed: ${error.message}`
143
- : `x-web ${action} failed`,
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 async function blockUserViaXWeb(targetUserId: string) {
149
- return runXWebBlockMutation(
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 async function unblockUserViaXWeb(targetUserId: string) {
160
- return runXWebBlockMutation(
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
+ }