birdclaw 0.5.1 → 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 (89) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/README.md +50 -5
  3. package/package.json +3 -2
  4. package/scripts/browser-perf.mjs +1 -0
  5. package/scripts/start-test-server.mjs +16 -3
  6. package/src/cli.ts +376 -13
  7. package/src/components/AccountSwitcher.tsx +186 -0
  8. package/src/components/AppNav.tsx +27 -7
  9. package/src/components/AvatarChip.tsx +9 -3
  10. package/src/components/DmWorkspace.tsx +18 -8
  11. package/src/components/LinkPreviewCard.tsx +40 -18
  12. package/src/components/MarkdownViewer.tsx +452 -0
  13. package/src/components/SyncNowButton.tsx +57 -25
  14. package/src/components/ThemeSlider.tsx +55 -50
  15. package/src/components/TimelineCard.tsx +225 -93
  16. package/src/components/TimelineRouteFrame.tsx +22 -8
  17. package/src/components/TweetMediaGrid.tsx +87 -38
  18. package/src/components/TweetRichText.tsx +15 -11
  19. package/src/components/account-selection.ts +64 -0
  20. package/src/components/useTimelineRouteData.ts +23 -7
  21. package/src/lib/account-sync-job.ts +654 -0
  22. package/src/lib/actions-transport.ts +216 -146
  23. package/src/lib/api-client.ts +128 -53
  24. package/src/lib/archive-finder.ts +78 -63
  25. package/src/lib/archive-import.ts +1364 -1300
  26. package/src/lib/authored-live.ts +261 -204
  27. package/src/lib/avatar-cache.ts +159 -44
  28. package/src/lib/backup.ts +1532 -951
  29. package/src/lib/bird-actions.ts +139 -57
  30. package/src/lib/bird-command.ts +101 -28
  31. package/src/lib/bird.ts +549 -194
  32. package/src/lib/blocklist.ts +40 -23
  33. package/src/lib/blocks-write.ts +129 -80
  34. package/src/lib/blocks.ts +165 -97
  35. package/src/lib/bookmark-sync-job.ts +250 -160
  36. package/src/lib/conversation-surface.ts +79 -48
  37. package/src/lib/db.ts +33 -3
  38. package/src/lib/dms-live.ts +720 -66
  39. package/src/lib/effect-runtime.ts +45 -0
  40. package/src/lib/follow-graph.ts +224 -180
  41. package/src/lib/http-effect.ts +222 -0
  42. package/src/lib/inbox.ts +74 -43
  43. package/src/lib/link-index.ts +88 -76
  44. package/src/lib/link-insights.ts +24 -0
  45. package/src/lib/link-preview-metadata.ts +472 -52
  46. package/src/lib/media-fetch.ts +286 -213
  47. package/src/lib/mention-threads-live.ts +352 -288
  48. package/src/lib/mentions-live.ts +390 -342
  49. package/src/lib/moderation-target.ts +102 -65
  50. package/src/lib/moderation-write.ts +77 -18
  51. package/src/lib/mutes-write.ts +129 -80
  52. package/src/lib/mutes.ts +8 -1
  53. package/src/lib/openai.ts +84 -53
  54. package/src/lib/period-digest.ts +953 -0
  55. package/src/lib/profile-affiliation-hydration.ts +93 -54
  56. package/src/lib/profile-hydration.ts +124 -72
  57. package/src/lib/profile-replies.ts +60 -43
  58. package/src/lib/profile-resolver.ts +402 -294
  59. package/src/lib/queries.ts +969 -199
  60. package/src/lib/research.ts +165 -120
  61. package/src/lib/sqlite.ts +1 -0
  62. package/src/lib/timeline-collections-live.ts +204 -167
  63. package/src/lib/timeline-live.ts +60 -39
  64. package/src/lib/tweet-lookup.ts +30 -19
  65. package/src/lib/types.ts +38 -1
  66. package/src/lib/ui.ts +30 -7
  67. package/src/lib/url-expansion.ts +226 -55
  68. package/src/lib/url-safety.ts +220 -0
  69. package/src/lib/web-sync.ts +216 -148
  70. package/src/lib/whois.ts +166 -147
  71. package/src/lib/x-web.ts +102 -71
  72. package/src/lib/xurl.ts +681 -411
  73. package/src/routeTree.gen.ts +42 -0
  74. package/src/routes/__root.tsx +25 -5
  75. package/src/routes/api/action.tsx +127 -78
  76. package/src/routes/api/avatar.tsx +39 -30
  77. package/src/routes/api/blocks.tsx +26 -23
  78. package/src/routes/api/conversation.tsx +25 -14
  79. package/src/routes/api/inbox.tsx +27 -21
  80. package/src/routes/api/link-insights.tsx +31 -25
  81. package/src/routes/api/link-preview.tsx +25 -21
  82. package/src/routes/api/period-digest.tsx +123 -0
  83. package/src/routes/api/profile-hydrate.tsx +31 -28
  84. package/src/routes/api/query.tsx +79 -55
  85. package/src/routes/api/status.tsx +18 -10
  86. package/src/routes/api/sync.tsx +75 -29
  87. package/src/routes/dms.tsx +95 -28
  88. package/src/routes/inbox.tsx +32 -19
  89. package/src/routes/today.tsx +441 -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
+ }