birdclaw 0.8.2 → 0.8.4

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 (247) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +10 -8
  3. package/bin/birdclaw.mjs +4 -66
  4. package/dist/cli/birdclaw.js +28165 -0
  5. package/dist/client/assets/AvatarChip-DLoqM5xD.js +1 -0
  6. package/dist/client/assets/ProfilePreview-BejAuYsJ.js +1 -0
  7. package/dist/client/assets/SavedTimelineView-BIS7HKpp.js +1 -0
  8. package/dist/client/assets/SmartTimestamp-BR0-9se2.js +1 -0
  9. package/dist/client/assets/TimelineRouteFrame-CyGPj0eM.js +1 -0
  10. package/dist/client/assets/blocks-7UDKQp8H.js +1 -0
  11. package/dist/client/assets/bookmarks-CbBkMvXk.js +1 -0
  12. package/dist/client/assets/chunk-C0phanGV.js +1 -0
  13. package/dist/client/assets/circle-check-C_Vq3ElD.js +1 -0
  14. package/dist/client/assets/client-stream-contracts-ys3XhWG4.js +5 -0
  15. package/dist/client/assets/createLucideIcon-JV8isFFR.js +1 -0
  16. package/dist/client/assets/data-sources-Cm2bpNvg.js +1 -0
  17. package/dist/client/assets/discuss-DTjaOugw.js +1 -0
  18. package/dist/client/assets/dms-CGd6Eogq.js +1 -0
  19. package/dist/client/assets/inbox-BHlGuOy2.js +1 -0
  20. package/dist/client/assets/index-CEIG7S6s.js +31 -0
  21. package/dist/client/assets/index-bpFrQ5yG.css +1 -0
  22. package/dist/client/assets/likes-cLrcAQem.js +1 -0
  23. package/dist/client/assets/links-CM9QO1LC.js +1 -0
  24. package/dist/client/assets/mapbox-BxTlqXt9.js +2 -0
  25. package/dist/client/assets/mapbox-gl-dgf_hX3a.js +3215 -0
  26. package/dist/client/assets/mentions-CxKCgzIf.js +1 -0
  27. package/dist/client/assets/network-map-B5a3whqj.js +2 -0
  28. package/dist/client/assets/profile-analyze-CRtNEwlb.js +1 -0
  29. package/dist/client/assets/profile-hydration-client-YEtJsNTv.js +1 -0
  30. package/dist/client/assets/profiles._handle-C82_HoKX.js +1 -0
  31. package/dist/client/assets/query-client-fYu2rzRC.js +17 -0
  32. package/dist/client/assets/rate-limits-ScaEwdVK.js +1 -0
  33. package/dist/client/assets/react-dom-DHl6Ug7F.js +1 -0
  34. package/dist/client/assets/react-nBylq1U5.js +1 -0
  35. package/dist/client/assets/repeat-2-xCwHM9hD.js +1 -0
  36. package/dist/client/assets/routes-q7ZaW4En.js +1 -0
  37. package/dist/client/assets/schemas-I9r6F6ya.js +64 -0
  38. package/dist/client/assets/search-Dq5kYsxA.js +1 -0
  39. package/dist/client/assets/sparkles-CQVlvHsZ.js +1 -0
  40. package/dist/client/assets/styles-B8s3tt3r.css +2 -0
  41. package/dist/client/assets/today-CeG1O7Py.js +1 -0
  42. package/dist/client/assets/triangle-alert-CBL7s2pf.js +1 -0
  43. package/dist/client/assets/useTimelineRouteData-1HsqgvUi.js +1 -0
  44. package/dist/client/assets/users-CHbKlqUr.js +1 -0
  45. package/dist/server/assets/AvatarChip-zUDAEmjL.js +105 -0
  46. package/dist/server/assets/FeedState-ntW0Q2GB.js +214 -0
  47. package/dist/server/assets/ProfileAnalysisStream-BFvhVdhn.js +198 -0
  48. package/dist/server/assets/ProfilePreview-C6gAor_j.js +752 -0
  49. package/dist/server/assets/SavedTimelineView-BG1jnBp1.js +64 -0
  50. package/dist/server/assets/SmartTimestamp-DfwE1NGf.js +45 -0
  51. package/dist/server/assets/SyncNowButton-BOn_IBXS.js +76 -0
  52. package/dist/server/assets/TimelineRouteFrame-CU7ilM7U.js +85 -0
  53. package/dist/server/assets/TweetRichText-DhCVv1-6.js +74 -0
  54. package/dist/server/assets/_tanstack-start-manifest_v-C-ZRnG_n.js +148 -0
  55. package/dist/server/assets/account-selection-CTdFGWRk.js +44 -0
  56. package/dist/server/assets/blocks-C1mgGTB3.js +299 -0
  57. package/dist/server/assets/blocks-DSgQghqP.js +17 -0
  58. package/dist/server/assets/bookmarks-BJN8tykY.js +14 -0
  59. package/dist/server/assets/client-stream-contracts-C2xbWVjm.js +769 -0
  60. package/dist/server/assets/data-sources-BnA1Q94p.js +180 -0
  61. package/dist/server/assets/discuss-CJ1dau1j.js +15 -0
  62. package/dist/server/assets/discuss-QTo08NCN.js +338 -0
  63. package/dist/server/assets/dms-BuLj4bp8.js +368 -0
  64. package/dist/server/assets/dms-DAyQrVM2.js +240 -0
  65. package/dist/server/assets/empty-plugin-adapters-DzrEkdBF.js +5 -0
  66. package/dist/server/assets/inbox-CkGOaW1q.js +214 -0
  67. package/dist/server/assets/inbox-DpWd1EzC.js +146 -0
  68. package/dist/server/assets/likes-DBOAtsJU.js +14 -0
  69. package/dist/server/assets/links-DAhSjynu.js +240 -0
  70. package/dist/server/assets/links-DmH5lxHO.js +468 -0
  71. package/dist/server/assets/mentions-BH2MzKtB.js +26 -0
  72. package/dist/server/assets/network-map-D3CzRcC4.js +224 -0
  73. package/dist/server/assets/network-map-D5DoLOwR.js +582 -0
  74. package/dist/server/assets/profile-analyze-BP65PED0.js +90 -0
  75. package/dist/server/assets/profile-analyze-C1NoM0tV.js +9 -0
  76. package/dist/server/assets/profile-hydration-client-GJ4kwJS8.js +133 -0
  77. package/dist/server/assets/profiles._handle-DjwlN_ac.js +184 -0
  78. package/dist/server/assets/profiles._handle-DsLNj2Ca.js +12 -0
  79. package/dist/server/assets/query-client-BdauHBgd.js +923 -0
  80. package/dist/server/assets/rate-limits-VAr2qzYD.js +262 -0
  81. package/dist/server/assets/route-search-BUlQ5lGM.js +117 -0
  82. package/dist/server/assets/router-DqkwSPiM.js +17026 -0
  83. package/dist/server/assets/routes-A9c9ZhwN.js +26 -0
  84. package/dist/server/assets/start-FaDOTmto.js +4 -0
  85. package/dist/server/assets/today-B5hVM57Z.js +17 -0
  86. package/dist/server/assets/today-fvGiZ7ud.js +303 -0
  87. package/dist/server/assets/ui-qbmcMSSX.js +137 -0
  88. package/{src/components/useDebouncedValue.ts → dist/server/assets/useDebouncedValue-DOnA6Xcy.js} +4 -4
  89. package/dist/server/assets/useTimelineRouteData-27cxynxa.js +1101 -0
  90. package/dist/server/server.js +5520 -0
  91. package/package.json +27 -34
  92. package/playwright.config.ts +0 -35
  93. package/scripts/browser-perf.mjs +0 -427
  94. package/scripts/build-docs-site.mjs +0 -967
  95. package/scripts/docs-site-assets.mjs +0 -311
  96. package/scripts/run-vitest.mjs +0 -21
  97. package/scripts/sanitize-node-options.mjs +0 -23
  98. package/scripts/start-test-server.mjs +0 -42
  99. package/src/cli-moderation.ts +0 -210
  100. package/src/cli.ts +0 -2336
  101. package/src/components/AccountSwitcher.tsx +0 -186
  102. package/src/components/AppNav.tsx +0 -118
  103. package/src/components/AvatarChip.tsx +0 -58
  104. package/src/components/BrandMark.tsx +0 -67
  105. package/src/components/ConversationThread.tsx +0 -131
  106. package/src/components/DmWorkspace.tsx +0 -286
  107. package/src/components/EmbeddedTweetCard.tsx +0 -55
  108. package/src/components/FeedState.tsx +0 -147
  109. package/src/components/FloatingPreview.tsx +0 -382
  110. package/src/components/InboxCard.tsx +0 -152
  111. package/src/components/LinkPreviewCard.tsx +0 -292
  112. package/src/components/MarkdownViewer.tsx +0 -777
  113. package/src/components/ProfileAnalysisStream.tsx +0 -428
  114. package/src/components/ProfilePreview.tsx +0 -129
  115. package/src/components/SavedTimelineView.tsx +0 -155
  116. package/src/components/SmartTimestamp.tsx +0 -72
  117. package/src/components/SyncNowButton.tsx +0 -140
  118. package/src/components/ThemeSlider.tsx +0 -80
  119. package/src/components/TimelineCard.tsx +0 -533
  120. package/src/components/TimelineRouteFrame.tsx +0 -186
  121. package/src/components/TweetArticleCard.tsx +0 -66
  122. package/src/components/TweetMediaGrid.tsx +0 -185
  123. package/src/components/TweetRichText.tsx +0 -159
  124. package/src/components/account-selection.ts +0 -64
  125. package/src/components/useTimelineRouteData.ts +0 -323
  126. package/src/lib/account-sync-job.ts +0 -666
  127. package/src/lib/actions-transport.ts +0 -300
  128. package/src/lib/api-client.ts +0 -419
  129. package/src/lib/archive-finder.ts +0 -185
  130. package/src/lib/archive-import.ts +0 -2764
  131. package/src/lib/authored-live.ts +0 -1072
  132. package/src/lib/avatar-cache.ts +0 -349
  133. package/src/lib/backup.ts +0 -2741
  134. package/src/lib/bird-actions.ts +0 -294
  135. package/src/lib/bird-command.ts +0 -130
  136. package/src/lib/bird.ts +0 -1140
  137. package/src/lib/blocklist.ts +0 -136
  138. package/src/lib/blocks-write.ts +0 -167
  139. package/src/lib/blocks.ts +0 -389
  140. package/src/lib/bookmark-sync-job.ts +0 -536
  141. package/src/lib/client-cache.ts +0 -109
  142. package/src/lib/config.ts +0 -224
  143. package/src/lib/conversation-surface.ts +0 -205
  144. package/src/lib/data-sources.ts +0 -219
  145. package/src/lib/db.ts +0 -1135
  146. package/src/lib/dms-live.ts +0 -933
  147. package/src/lib/effect-runtime.ts +0 -45
  148. package/src/lib/follow-graph.ts +0 -1097
  149. package/src/lib/geocoding.ts +0 -296
  150. package/src/lib/http-effect.ts +0 -222
  151. package/src/lib/identity-search-index.ts +0 -369
  152. package/src/lib/inbox.ts +0 -241
  153. package/src/lib/link-index.ts +0 -641
  154. package/src/lib/link-insights.ts +0 -860
  155. package/src/lib/link-preview-metadata.ts +0 -754
  156. package/src/lib/location.ts +0 -137
  157. package/src/lib/media-fetch.ts +0 -860
  158. package/src/lib/media-includes.ts +0 -165
  159. package/src/lib/mention-threads-live.ts +0 -917
  160. package/src/lib/mentions-export.ts +0 -147
  161. package/src/lib/mentions-live.ts +0 -1238
  162. package/src/lib/moderation-target.ts +0 -216
  163. package/src/lib/moderation-write.ts +0 -131
  164. package/src/lib/mutes-write.ts +0 -167
  165. package/src/lib/mutes.ts +0 -84
  166. package/src/lib/ndjson-stream.ts +0 -106
  167. package/src/lib/network-map.ts +0 -382
  168. package/src/lib/openai.ts +0 -117
  169. package/src/lib/period-digest.ts +0 -1552
  170. package/src/lib/present.ts +0 -118
  171. package/src/lib/profile-affiliation-hydration.ts +0 -228
  172. package/src/lib/profile-affiliations.ts +0 -262
  173. package/src/lib/profile-analysis.ts +0 -1272
  174. package/src/lib/profile-bio-entities.ts +0 -318
  175. package/src/lib/profile-history.ts +0 -164
  176. package/src/lib/profile-hydration.ts +0 -180
  177. package/src/lib/profile-replies.ts +0 -77
  178. package/src/lib/profile-resolver.ts +0 -650
  179. package/src/lib/queries.ts +0 -2381
  180. package/src/lib/research.ts +0 -652
  181. package/src/lib/search-discussion.ts +0 -1016
  182. package/src/lib/seed.ts +0 -613
  183. package/src/lib/sqlite.ts +0 -147
  184. package/src/lib/sync-cache.ts +0 -65
  185. package/src/lib/theme-transition.ts +0 -113
  186. package/src/lib/theme.tsx +0 -165
  187. package/src/lib/timeline-collections-live.ts +0 -581
  188. package/src/lib/timeline-live.ts +0 -452
  189. package/src/lib/tweet-account-edges.ts +0 -48
  190. package/src/lib/tweet-lookup.ts +0 -46
  191. package/src/lib/tweet-render.ts +0 -430
  192. package/src/lib/tweet-search-live.ts +0 -566
  193. package/src/lib/types.ts +0 -723
  194. package/src/lib/ui.ts +0 -529
  195. package/src/lib/url-expansion-store.ts +0 -110
  196. package/src/lib/url-expansion.ts +0 -343
  197. package/src/lib/url-safety.ts +0 -220
  198. package/src/lib/web-sync.ts +0 -516
  199. package/src/lib/whois.ts +0 -1079
  200. package/src/lib/x-profile.ts +0 -365
  201. package/src/lib/x-web.ts +0 -199
  202. package/src/lib/xurl-rate-limits.ts +0 -267
  203. package/src/lib/xurl.ts +0 -1677
  204. package/src/routeTree.gen.ts +0 -723
  205. package/src/router.tsx +0 -20
  206. package/src/routes/__root.tsx +0 -79
  207. package/src/routes/api/action.tsx +0 -147
  208. package/src/routes/api/avatar.tsx +0 -50
  209. package/src/routes/api/blocks.tsx +0 -35
  210. package/src/routes/api/conversation.tsx +0 -45
  211. package/src/routes/api/data-sources.tsx +0 -24
  212. package/src/routes/api/inbox.tsx +0 -43
  213. package/src/routes/api/link-insights.tsx +0 -85
  214. package/src/routes/api/link-preview.tsx +0 -47
  215. package/src/routes/api/network-map.tsx +0 -55
  216. package/src/routes/api/period-digest.tsx +0 -101
  217. package/src/routes/api/profile-analysis.tsx +0 -97
  218. package/src/routes/api/profile-hydrate.tsx +0 -54
  219. package/src/routes/api/query.tsx +0 -115
  220. package/src/routes/api/search-discussion.tsx +0 -113
  221. package/src/routes/api/status.tsx +0 -28
  222. package/src/routes/api/sync.tsx +0 -105
  223. package/src/routes/api/xurl-rate-limits.tsx +0 -24
  224. package/src/routes/blocks.tsx +0 -377
  225. package/src/routes/bookmarks.tsx +0 -18
  226. package/src/routes/data-sources.tsx +0 -257
  227. package/src/routes/discuss.tsx +0 -419
  228. package/src/routes/dms.tsx +0 -432
  229. package/src/routes/inbox.tsx +0 -226
  230. package/src/routes/index.tsx +0 -32
  231. package/src/routes/likes.tsx +0 -18
  232. package/src/routes/links.tsx +0 -994
  233. package/src/routes/mentions.tsx +0 -32
  234. package/src/routes/network-map.tsx +0 -1035
  235. package/src/routes/profile-analyze.tsx +0 -112
  236. package/src/routes/profiles.$handle.tsx +0 -228
  237. package/src/routes/rate-limits.tsx +0 -309
  238. package/src/routes/today.tsx +0 -511
  239. package/src/styles.css +0 -257
  240. package/tsconfig.json +0 -29
  241. package/vite.config.ts +0 -29
  242. /package/{public → dist/client}/birdclaw-mark.png +0 -0
  243. /package/{public → dist/client}/favicon.ico +0 -0
  244. /package/{public → dist/client}/logo192.png +0 -0
  245. /package/{public → dist/client}/logo512.png +0 -0
  246. /package/{public → dist/client}/manifest.json +0 -0
  247. /package/{public → dist/client}/robots.txt +0 -0
@@ -1,365 +0,0 @@
1
- import type { Database } from "./sqlite";
2
- import { normalizeAvatarUrl } from "./avatar-cache";
3
- import { syncIdentitySearchIndexForProfileIds } from "./identity-search-index";
4
- import { syncProfileBioEntitiesForProfileId } from "./profile-bio-entities";
5
- import { syncProfileAffiliationsFromUser } from "./profile-affiliations";
6
- import { recordProfileSnapshot } from "./profile-history";
7
- import type { ProfileRecord, XurlMentionUser } from "./types";
8
-
9
- export interface ResolvedXProfile {
10
- profile: ProfileRecord;
11
- externalUserId: string;
12
- }
13
-
14
- export function buildExternalProfileId(externalUserId: string) {
15
- return `profile_user_${externalUserId}`;
16
- }
17
-
18
- export function getExternalUserId(profileId: string) {
19
- if (profileId.startsWith("profile_user_")) {
20
- return profileId.replace(/^profile_user_/, "");
21
- }
22
- return null;
23
- }
24
-
25
- export function randomAvatarHue(input: string) {
26
- return (
27
- input
28
- .split("")
29
- .reduce((sum, character) => sum + character.charCodeAt(0), 0) % 360
30
- );
31
- }
32
-
33
- function toProfile(row: Record<string, unknown>): ProfileRecord {
34
- const followingCount = Number(row.following_count ?? 0);
35
- const entities = parseJsonRecord(row.entities_json);
36
- return {
37
- id: String(row.id),
38
- handle: String(row.handle),
39
- displayName: String(row.display_name),
40
- bio: String(row.bio),
41
- followersCount: Number(row.followers_count),
42
- ...(Number.isFinite(followingCount) ? { followingCount } : {}),
43
- avatarHue: Number(row.avatar_hue),
44
- avatarUrl:
45
- typeof row.avatar_url === "string" ? String(row.avatar_url) : undefined,
46
- ...(typeof row.location === "string" && row.location.length > 0
47
- ? { location: row.location }
48
- : {}),
49
- ...(typeof row.url === "string" && row.url.length > 0
50
- ? { url: row.url }
51
- : {}),
52
- ...(typeof row.verified_type === "string" && row.verified_type.length > 0
53
- ? { verifiedType: row.verified_type }
54
- : {}),
55
- ...(entities ? { entities } : {}),
56
- createdAt: String(row.created_at),
57
- };
58
- }
59
-
60
- function parseJsonRecord(value: unknown) {
61
- if (typeof value !== "string" || value.length === 0) {
62
- return undefined;
63
- }
64
- try {
65
- const parsed = JSON.parse(value) as unknown;
66
- return parsed && typeof parsed === "object" && !Array.isArray(parsed)
67
- ? (parsed as Record<string, unknown>)
68
- : undefined;
69
- } catch {
70
- return undefined;
71
- }
72
- }
73
-
74
- function getString(value: unknown) {
75
- return typeof value === "string" && value.trim().length > 0
76
- ? value.trim()
77
- : undefined;
78
- }
79
-
80
- function getExpandedUrlFromEntities(
81
- entities: Record<string, unknown> | undefined,
82
- key: "url" | "description",
83
- ) {
84
- const block = entities?.[key];
85
- if (!block || typeof block !== "object") {
86
- return undefined;
87
- }
88
- const urls = (block as { urls?: unknown }).urls;
89
- if (!Array.isArray(urls)) {
90
- return undefined;
91
- }
92
- for (const entry of urls) {
93
- if (!entry || typeof entry !== "object") {
94
- continue;
95
- }
96
- const record = entry as Record<string, unknown>;
97
- const expanded =
98
- getString(record.expandedUrl) ?? getString(record.expanded_url);
99
- if (expanded) {
100
- return expanded;
101
- }
102
- }
103
- return undefined;
104
- }
105
-
106
- function normalizeVerifiedType(user: XurlMentionUser) {
107
- const verifiedType = getString(user.verified_type);
108
- if (verifiedType) {
109
- return verifiedType.toLowerCase();
110
- }
111
- return user.verified ? "verified" : null;
112
- }
113
-
114
- function buildProfileMetadata(user: XurlMentionUser) {
115
- const entities = user.entities;
116
- const url =
117
- getExpandedUrlFromEntities(entities, "url") ?? getString(user.url);
118
- return {
119
- location: getString(user.location) ?? null,
120
- url: url ?? null,
121
- verifiedType: normalizeVerifiedType(user),
122
- entitiesJson: JSON.stringify(entities ?? {}),
123
- rawJson: JSON.stringify(user),
124
- };
125
- }
126
-
127
- function updateExistingProfileFromUser(
128
- db: Database,
129
- profileId: string,
130
- user: XurlMentionUser,
131
- ): ResolvedXProfile {
132
- const username = String(user.username ?? "").replace(/^@/, "");
133
- const displayName = String(user.name ?? "").trim() || username;
134
- const followersCount = Number(user.public_metrics?.followers_count ?? 0);
135
- const hasFollowingCount =
136
- typeof user.public_metrics?.following_count === "number";
137
- const followingCount = hasFollowingCount
138
- ? (user.public_metrics?.following_count ?? null)
139
- : null;
140
- const bio = String(user.description ?? "");
141
- const avatarUrl = normalizeAvatarUrl(user.profile_image_url);
142
- const metadata = buildProfileMetadata(user);
143
-
144
- recordProfileSnapshot(db, profileId, "pre_update");
145
- db.prepare(
146
- `
147
- update profiles
148
- set handle = ?,
149
- display_name = ?,
150
- bio = ?,
151
- followers_count = ?,
152
- following_count = coalesce(?, following_count),
153
- public_metrics_json = ?,
154
- avatar_url = coalesce(?, avatar_url),
155
- location = coalesce(?, location),
156
- url = coalesce(?, url),
157
- verified_type = coalesce(?, verified_type),
158
- entities_json = case
159
- when ? not in ('', '{}', 'null') then ?
160
- else profiles.entities_json
161
- end,
162
- raw_json = ?
163
- where id = ?
164
- `,
165
- ).run(
166
- username,
167
- displayName,
168
- bio,
169
- followersCount,
170
- followingCount,
171
- JSON.stringify(user.public_metrics ?? {}),
172
- avatarUrl,
173
- metadata.location,
174
- metadata.url,
175
- metadata.verifiedType,
176
- metadata.entitiesJson,
177
- metadata.entitiesJson,
178
- metadata.rawJson,
179
- profileId,
180
- );
181
- syncProfileAffiliationsFromUser(db, profileId, user);
182
- recordProfileSnapshot(db, profileId, "x_profile");
183
- syncProfileBioEntitiesForProfileId(db, profileId);
184
- syncIdentitySearchIndexForProfileIds(db, [profileId]);
185
-
186
- const row = db
187
- .prepare(
188
- `
189
- select id, handle, display_name, bio, followers_count, following_count, avatar_hue, avatar_url,
190
- location, url, verified_type, entities_json, created_at
191
- from profiles
192
- where id = ?
193
- `,
194
- )
195
- .get(profileId) as Record<string, unknown>;
196
-
197
- return {
198
- profile: toProfile(row),
199
- externalUserId: String(user.id),
200
- };
201
- }
202
-
203
- export function upsertProfileFromXUser(
204
- db: Database,
205
- user: XurlMentionUser,
206
- ): ResolvedXProfile {
207
- const externalUserId = String(user.id ?? "");
208
- if (!externalUserId) {
209
- throw new Error("Resolved user is missing an id");
210
- }
211
-
212
- const username = String(user.username ?? "").replace(/^@/, "");
213
- if (!username) {
214
- throw new Error("Resolved user is missing a username");
215
- }
216
-
217
- const profileId = buildExternalProfileId(externalUserId);
218
- const existingRow = db
219
- .prepare(
220
- `
221
- select id
222
- from profiles
223
- where id = ? or handle = ?
224
- limit 1
225
- `,
226
- )
227
- .get(profileId, username) as { id: string } | undefined;
228
-
229
- if (existingRow) {
230
- return updateExistingProfileFromUser(db, existingRow.id, user);
231
- }
232
-
233
- const displayName = String(user.name ?? "").trim() || username;
234
- const followersCount = Number(user.public_metrics?.followers_count ?? 0);
235
- const hasFollowingCount =
236
- typeof user.public_metrics?.following_count === "number";
237
- const followingCount = hasFollowingCount
238
- ? (user.public_metrics?.following_count ?? null)
239
- : null;
240
- const bio = String(user.description ?? "");
241
- const avatarUrl = normalizeAvatarUrl(user.profile_image_url);
242
- const metadata = buildProfileMetadata(user);
243
- const createdAt = new Date().toISOString();
244
- const avatarHue = randomAvatarHue(username);
245
-
246
- db.prepare(
247
- `
248
- insert into profiles (
249
- id, handle, display_name, bio, followers_count, following_count, avatar_hue,
250
- public_metrics_json, avatar_url, location, url, verified_type, entities_json,
251
- raw_json, created_at
252
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
253
- on conflict(id) do update set
254
- handle = excluded.handle,
255
- display_name = excluded.display_name,
256
- bio = excluded.bio,
257
- followers_count = excluded.followers_count,
258
- public_metrics_json = excluded.public_metrics_json,
259
- following_count = case
260
- when ? then excluded.following_count
261
- else profiles.following_count
262
- end,
263
- avatar_url = coalesce(excluded.avatar_url, profiles.avatar_url),
264
- location = coalesce(excluded.location, profiles.location),
265
- url = coalesce(excluded.url, profiles.url),
266
- verified_type = coalesce(excluded.verified_type, profiles.verified_type),
267
- entities_json = case
268
- when excluded.entities_json not in ('', '{}', 'null') then excluded.entities_json
269
- else profiles.entities_json
270
- end,
271
- raw_json = excluded.raw_json
272
- `,
273
- ).run(
274
- profileId,
275
- username,
276
- displayName,
277
- bio,
278
- followersCount,
279
- followingCount ?? 0,
280
- avatarHue,
281
- JSON.stringify(user.public_metrics ?? {}),
282
- avatarUrl,
283
- metadata.location,
284
- metadata.url,
285
- metadata.verifiedType,
286
- metadata.entitiesJson,
287
- metadata.rawJson,
288
- createdAt,
289
- hasFollowingCount ? 1 : 0,
290
- );
291
- syncProfileAffiliationsFromUser(db, profileId, user);
292
- recordProfileSnapshot(db, profileId, "x_profile");
293
- syncProfileBioEntitiesForProfileId(db, profileId);
294
- syncIdentitySearchIndexForProfileIds(db, [profileId]);
295
-
296
- return {
297
- profile: {
298
- id: profileId,
299
- handle: username,
300
- displayName,
301
- bio,
302
- followersCount,
303
- followingCount: followingCount ?? 0,
304
- avatarHue,
305
- avatarUrl: avatarUrl ?? undefined,
306
- ...(metadata.location ? { location: metadata.location } : {}),
307
- ...(metadata.url ? { url: metadata.url } : {}),
308
- ...(metadata.verifiedType ? { verifiedType: metadata.verifiedType } : {}),
309
- ...(user.entities ? { entities: user.entities } : {}),
310
- createdAt,
311
- },
312
- externalUserId,
313
- };
314
- }
315
-
316
- export function ensureStubProfileForXUser(
317
- db: Database,
318
- externalUserId: string,
319
- ): ResolvedXProfile {
320
- const profileId = buildExternalProfileId(externalUserId);
321
- const existingRow = db
322
- .prepare(
323
- `
324
- select id, handle, display_name, bio, followers_count, following_count, avatar_hue, avatar_url,
325
- location, url, verified_type, entities_json, created_at
326
- from profiles
327
- where id = ?
328
- limit 1
329
- `,
330
- )
331
- .get(profileId) as Record<string, unknown> | undefined;
332
-
333
- if (existingRow) {
334
- return {
335
- profile: toProfile(existingRow),
336
- externalUserId,
337
- };
338
- }
339
-
340
- const handle = `user_${externalUserId}`;
341
- const createdAt = new Date().toISOString();
342
- const avatarHue = randomAvatarHue(handle);
343
- db.prepare(
344
- `
345
- insert into profiles (
346
- id, handle, display_name, bio, followers_count, following_count, avatar_hue,
347
- avatar_url, location, url, verified_type, entities_json, raw_json, created_at
348
- ) values (?, ?, ?, '', 0, 0, ?, null, null, null, null, '{}', '{}', ?)
349
- `,
350
- ).run(profileId, handle, handle, avatarHue, createdAt);
351
-
352
- return {
353
- profile: {
354
- id: profileId,
355
- handle,
356
- displayName: handle,
357
- bio: "",
358
- followersCount: 0,
359
- followingCount: 0,
360
- avatarHue,
361
- createdAt,
362
- },
363
- externalUserId,
364
- };
365
- }
package/src/lib/x-web.ts DELETED
@@ -1,199 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { getCookies } from "@steipete/sweet-cookie";
3
- import { Effect } from "effect";
4
- import { runEffectPromise, tryPromise } from "./effect-runtime";
5
-
6
- const X_WEB_BEARER_TOKEN =
7
- "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA";
8
- const X_WEB_USER_AGENT =
9
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
10
- const X_WEB_COOKIE_NAMES = ["auth_token", "ct0"] as const;
11
- const X_WEB_ORIGINS = ["https://x.com/", "https://twitter.com/"];
12
-
13
- type CookieSource = "safari" | "chrome" | "firefox";
14
-
15
- function normalizeCookieValue(value: unknown) {
16
- return typeof value === "string" && value.trim().length > 0
17
- ? value.trim()
18
- : null;
19
- }
20
-
21
- function buildCookieHeader(authToken: string, ct0: string) {
22
- return `auth_token=${authToken}; ct0=${ct0}`;
23
- }
24
-
25
- function pickCookieValue(
26
- cookies: Array<{ name?: string; value?: string; domain?: string }>,
27
- name: (typeof X_WEB_COOKIE_NAMES)[number],
28
- ) {
29
- const matches = cookies.filter(
30
- (cookie) => cookie?.name === name && typeof cookie.value === "string",
31
- );
32
- if (matches.length === 0) {
33
- return null;
34
- }
35
-
36
- const xDomain = matches.find((cookie) =>
37
- (cookie.domain ?? "").endsWith("x.com"),
38
- );
39
- if (xDomain?.value) {
40
- return xDomain.value;
41
- }
42
-
43
- const twitterDomain = matches.find((cookie) =>
44
- (cookie.domain ?? "").endsWith("twitter.com"),
45
- );
46
- if (twitterDomain?.value) {
47
- return twitterDomain.value;
48
- }
49
-
50
- return matches[0]?.value ?? null;
51
- }
52
-
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
- }
87
-
88
- return {
89
- authToken,
90
- ct0,
91
- cookieHeader: buildCookieHeader(authToken, ct0),
92
- source: "browser",
93
- };
94
- });
95
- }
96
-
97
- function runXWebBlockMutationEffect(
98
- path: string,
99
- params: URLSearchParams,
100
- action: string,
101
- ) {
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();
117
- if (!cookies) {
118
- return {
119
- ok: false,
120
- output: `x-web ${action} unavailable: missing auth_token/ct0 cookies`,
121
- };
122
- }
123
-
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());
147
- if (!response.ok) {
148
- return {
149
- ok: false,
150
- output: `x-web ${action} failed (${response.status}): ${text.slice(0, 240)}`,
151
- };
152
- }
153
-
154
- return {
155
- ok: true,
156
- output: `x-web ${action} ok via ${cookies.source}`,
157
- };
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
- );
169
- }
170
-
171
- export function blockUserViaXWebEffect(targetUserId: string) {
172
- return runXWebBlockMutationEffect(
173
- "blocks/create.json",
174
- new URLSearchParams({
175
- user_id: targetUserId,
176
- skip_status: "true",
177
- }),
178
- "block",
179
- );
180
- }
181
-
182
- export function unblockUserViaXWebEffect(targetUserId: string) {
183
- return runXWebBlockMutationEffect(
184
- "blocks/destroy.json",
185
- new URLSearchParams({
186
- user_id: targetUserId,
187
- skip_status: "true",
188
- }),
189
- "unblock",
190
- );
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
- }