birdclaw 0.8.3 → 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.
- package/CHANGELOG.md +17 -0
- package/README.md +10 -8
- package/bin/birdclaw.mjs +4 -66
- package/dist/cli/birdclaw.js +28165 -0
- package/dist/client/assets/AvatarChip-DLoqM5xD.js +1 -0
- package/dist/client/assets/ProfilePreview-BejAuYsJ.js +1 -0
- package/dist/client/assets/SavedTimelineView-BIS7HKpp.js +1 -0
- package/dist/client/assets/SmartTimestamp-BR0-9se2.js +1 -0
- package/dist/client/assets/TimelineRouteFrame-CyGPj0eM.js +1 -0
- package/dist/client/assets/blocks-7UDKQp8H.js +1 -0
- package/dist/client/assets/bookmarks-CbBkMvXk.js +1 -0
- package/dist/client/assets/chunk-C0phanGV.js +1 -0
- package/dist/client/assets/circle-check-C_Vq3ElD.js +1 -0
- package/dist/client/assets/client-stream-contracts-ys3XhWG4.js +5 -0
- package/dist/client/assets/createLucideIcon-JV8isFFR.js +1 -0
- package/dist/client/assets/data-sources-Cm2bpNvg.js +1 -0
- package/dist/client/assets/discuss-DTjaOugw.js +1 -0
- package/dist/client/assets/dms-CGd6Eogq.js +1 -0
- package/dist/client/assets/inbox-BHlGuOy2.js +1 -0
- package/dist/client/assets/index-CEIG7S6s.js +31 -0
- package/dist/client/assets/index-bpFrQ5yG.css +1 -0
- package/dist/client/assets/likes-cLrcAQem.js +1 -0
- package/dist/client/assets/links-CM9QO1LC.js +1 -0
- package/dist/client/assets/mapbox-BxTlqXt9.js +2 -0
- package/dist/client/assets/mapbox-gl-dgf_hX3a.js +3215 -0
- package/dist/client/assets/mentions-CxKCgzIf.js +1 -0
- package/dist/client/assets/network-map-B5a3whqj.js +2 -0
- package/dist/client/assets/profile-analyze-CRtNEwlb.js +1 -0
- package/dist/client/assets/profile-hydration-client-YEtJsNTv.js +1 -0
- package/dist/client/assets/profiles._handle-C82_HoKX.js +1 -0
- package/dist/client/assets/query-client-fYu2rzRC.js +17 -0
- package/dist/client/assets/rate-limits-ScaEwdVK.js +1 -0
- package/dist/client/assets/react-dom-DHl6Ug7F.js +1 -0
- package/dist/client/assets/react-nBylq1U5.js +1 -0
- package/dist/client/assets/repeat-2-xCwHM9hD.js +1 -0
- package/dist/client/assets/routes-q7ZaW4En.js +1 -0
- package/dist/client/assets/schemas-I9r6F6ya.js +64 -0
- package/dist/client/assets/search-Dq5kYsxA.js +1 -0
- package/dist/client/assets/sparkles-CQVlvHsZ.js +1 -0
- package/dist/client/assets/styles-B8s3tt3r.css +2 -0
- package/dist/client/assets/today-CeG1O7Py.js +1 -0
- package/dist/client/assets/triangle-alert-CBL7s2pf.js +1 -0
- package/dist/client/assets/useTimelineRouteData-1HsqgvUi.js +1 -0
- package/dist/client/assets/users-CHbKlqUr.js +1 -0
- package/dist/server/assets/AvatarChip-zUDAEmjL.js +105 -0
- package/dist/server/assets/FeedState-ntW0Q2GB.js +214 -0
- package/dist/server/assets/ProfileAnalysisStream-BFvhVdhn.js +198 -0
- package/dist/server/assets/ProfilePreview-C6gAor_j.js +752 -0
- package/dist/server/assets/SavedTimelineView-BG1jnBp1.js +64 -0
- package/dist/server/assets/SmartTimestamp-DfwE1NGf.js +45 -0
- package/dist/server/assets/SyncNowButton-BOn_IBXS.js +76 -0
- package/dist/server/assets/TimelineRouteFrame-CU7ilM7U.js +85 -0
- package/dist/server/assets/TweetRichText-DhCVv1-6.js +74 -0
- package/dist/server/assets/_tanstack-start-manifest_v-C-ZRnG_n.js +148 -0
- package/dist/server/assets/account-selection-CTdFGWRk.js +44 -0
- package/dist/server/assets/blocks-C1mgGTB3.js +299 -0
- package/dist/server/assets/blocks-DSgQghqP.js +17 -0
- package/dist/server/assets/bookmarks-BJN8tykY.js +14 -0
- package/dist/server/assets/client-stream-contracts-C2xbWVjm.js +769 -0
- package/dist/server/assets/data-sources-BnA1Q94p.js +180 -0
- package/dist/server/assets/discuss-CJ1dau1j.js +15 -0
- package/dist/server/assets/discuss-QTo08NCN.js +338 -0
- package/dist/server/assets/dms-BuLj4bp8.js +368 -0
- package/dist/server/assets/dms-DAyQrVM2.js +240 -0
- package/dist/server/assets/empty-plugin-adapters-DzrEkdBF.js +5 -0
- package/dist/server/assets/inbox-CkGOaW1q.js +214 -0
- package/dist/server/assets/inbox-DpWd1EzC.js +146 -0
- package/dist/server/assets/likes-DBOAtsJU.js +14 -0
- package/dist/server/assets/links-DAhSjynu.js +240 -0
- package/dist/server/assets/links-DmH5lxHO.js +468 -0
- package/dist/server/assets/mentions-BH2MzKtB.js +26 -0
- package/dist/server/assets/network-map-D3CzRcC4.js +224 -0
- package/dist/server/assets/network-map-D5DoLOwR.js +582 -0
- package/dist/server/assets/profile-analyze-BP65PED0.js +90 -0
- package/dist/server/assets/profile-analyze-C1NoM0tV.js +9 -0
- package/dist/server/assets/profile-hydration-client-GJ4kwJS8.js +133 -0
- package/dist/server/assets/profiles._handle-DjwlN_ac.js +184 -0
- package/dist/server/assets/profiles._handle-DsLNj2Ca.js +12 -0
- package/dist/server/assets/query-client-BdauHBgd.js +923 -0
- package/dist/server/assets/rate-limits-VAr2qzYD.js +262 -0
- package/dist/server/assets/route-search-BUlQ5lGM.js +117 -0
- package/dist/server/assets/router-DqkwSPiM.js +17026 -0
- package/dist/server/assets/routes-A9c9ZhwN.js +26 -0
- package/dist/server/assets/start-FaDOTmto.js +4 -0
- package/dist/server/assets/today-B5hVM57Z.js +17 -0
- package/dist/server/assets/today-fvGiZ7ud.js +303 -0
- package/dist/server/assets/ui-qbmcMSSX.js +137 -0
- package/{src/components/useDebouncedValue.ts → dist/server/assets/useDebouncedValue-DOnA6Xcy.js} +4 -4
- package/dist/server/assets/useTimelineRouteData-27cxynxa.js +1101 -0
- package/dist/server/server.js +5520 -0
- package/package.json +26 -34
- package/playwright.config.ts +0 -35
- package/scripts/browser-perf.mjs +0 -427
- package/scripts/build-docs-site.mjs +0 -967
- package/scripts/docs-site-assets.mjs +0 -311
- package/scripts/run-vitest.mjs +0 -21
- package/scripts/sanitize-node-options.mjs +0 -23
- package/scripts/start-test-server.mjs +0 -42
- package/src/cli/command-context.ts +0 -17
- package/src/cli/register-analysis.ts +0 -500
- package/src/cli/register-compose.ts +0 -40
- package/src/cli/register-graph.ts +0 -132
- package/src/cli/register-inbox.ts +0 -41
- package/src/cli/register-storage.ts +0 -106
- package/src/cli-moderation.ts +0 -210
- package/src/cli.ts +0 -1616
- package/src/components/AccountSwitcher.tsx +0 -178
- package/src/components/AppNav.tsx +0 -118
- package/src/components/AvatarChip.tsx +0 -58
- package/src/components/AvatarPreload.ts +0 -149
- package/src/components/BrandMark.tsx +0 -67
- package/src/components/ConversationThread.tsx +0 -131
- package/src/components/DmWorkspace.tsx +0 -286
- package/src/components/EmbeddedTweetCard.tsx +0 -55
- package/src/components/FeedState.tsx +0 -147
- package/src/components/FloatingPreview.tsx +0 -382
- package/src/components/InboxCard.tsx +0 -152
- package/src/components/LinkPreviewCard.tsx +0 -292
- package/src/components/MarkdownCitations.tsx +0 -680
- package/src/components/MarkdownViewer.tsx +0 -111
- package/src/components/ProfileAnalysisClient.ts +0 -191
- package/src/components/ProfileAnalysisStream.tsx +0 -259
- package/src/components/ProfilePreview.tsx +0 -131
- package/src/components/SavedTimelineView.tsx +0 -155
- package/src/components/SmartTimestamp.tsx +0 -72
- package/src/components/SyncNowButton.tsx +0 -140
- package/src/components/ThemeSlider.tsx +0 -80
- package/src/components/TimelineCard.tsx +0 -533
- package/src/components/TimelineRouteFrame.tsx +0 -186
- package/src/components/TweetArticleCard.tsx +0 -66
- package/src/components/TweetMediaGrid.tsx +0 -185
- package/src/components/TweetRichText.tsx +0 -159
- package/src/components/account-selection.ts +0 -64
- package/src/components/links-controller.ts +0 -162
- package/src/components/links-model.ts +0 -198
- package/src/components/network-map-controller.ts +0 -84
- package/src/components/network-map-model.ts +0 -255
- package/src/components/useTimelineRouteData.ts +0 -193
- package/src/lib/account-sync-job.ts +0 -666
- package/src/lib/actions-transport.ts +0 -300
- package/src/lib/analysis-runtime.ts +0 -238
- package/src/lib/api-client.ts +0 -220
- package/src/lib/api-contracts.ts +0 -328
- package/src/lib/archive-finder.ts +0 -185
- package/src/lib/archive-import-plan.ts +0 -102
- package/src/lib/archive-import.ts +0 -2695
- package/src/lib/authored-live.ts +0 -1027
- package/src/lib/avatar-cache.ts +0 -349
- package/src/lib/backup.ts +0 -2652
- package/src/lib/bird-actions.ts +0 -294
- package/src/lib/bird-command.ts +0 -130
- package/src/lib/bird.ts +0 -1140
- package/src/lib/blocklist.ts +0 -136
- package/src/lib/blocks-write.ts +0 -171
- package/src/lib/blocks.ts +0 -387
- package/src/lib/bookmark-sync-job.ts +0 -536
- package/src/lib/config.ts +0 -224
- package/src/lib/conversation-surface.ts +0 -205
- package/src/lib/data-sources.ts +0 -219
- package/src/lib/database-metrics.ts +0 -88
- package/src/lib/database-migrations.ts +0 -34
- package/src/lib/database-schema.ts +0 -312
- package/src/lib/database-writer.ts +0 -69
- package/src/lib/db.ts +0 -889
- package/src/lib/dm-read-model.ts +0 -533
- package/src/lib/dms-live.ts +0 -870
- package/src/lib/effect-runtime.ts +0 -45
- package/src/lib/follow-graph.ts +0 -1087
- package/src/lib/geocoding.ts +0 -296
- package/src/lib/http-effect.ts +0 -222
- package/src/lib/identity-search-index.ts +0 -369
- package/src/lib/import-repository.ts +0 -138
- package/src/lib/inbox.ts +0 -242
- package/src/lib/link-index.ts +0 -641
- package/src/lib/link-insights.ts +0 -860
- package/src/lib/link-preview-metadata.ts +0 -754
- package/src/lib/live-sync-engine.ts +0 -209
- package/src/lib/live-transport-gateway.ts +0 -128
- package/src/lib/location.ts +0 -137
- package/src/lib/media-fetch.ts +0 -860
- package/src/lib/media-includes.ts +0 -165
- package/src/lib/mention-threads-live.ts +0 -830
- package/src/lib/mentions-export.ts +0 -147
- package/src/lib/mentions-live.ts +0 -1114
- package/src/lib/moderation-target.ts +0 -227
- package/src/lib/moderation-write.ts +0 -131
- package/src/lib/mutes-write.ts +0 -171
- package/src/lib/mutes.ts +0 -84
- package/src/lib/ndjson-stream.ts +0 -106
- package/src/lib/network-map.ts +0 -382
- package/src/lib/openai-response-runtime.ts +0 -251
- package/src/lib/openai.ts +0 -117
- package/src/lib/paginated-sync.ts +0 -93
- package/src/lib/period-digest.ts +0 -1364
- package/src/lib/present.ts +0 -118
- package/src/lib/profile-affiliation-hydration.ts +0 -228
- package/src/lib/profile-affiliations.ts +0 -262
- package/src/lib/profile-analysis.ts +0 -1198
- package/src/lib/profile-bio-entities.ts +0 -318
- package/src/lib/profile-history.ts +0 -164
- package/src/lib/profile-hydration.ts +0 -180
- package/src/lib/profile-replies.ts +0 -77
- package/src/lib/profile-resolver.ts +0 -650
- package/src/lib/queries.ts +0 -6
- package/src/lib/query-actions.ts +0 -437
- package/src/lib/query-client.tsx +0 -47
- package/src/lib/query-read-model-shared.ts +0 -52
- package/src/lib/query-read-models.ts +0 -5
- package/src/lib/query-resource.ts +0 -41
- package/src/lib/query-status.ts +0 -164
- package/src/lib/research.ts +0 -652
- package/src/lib/runtime-services.ts +0 -20
- package/src/lib/search-discussion.ts +0 -812
- package/src/lib/seed.ts +0 -613
- package/src/lib/server-runtime-services.ts +0 -30
- package/src/lib/sqlite.ts +0 -183
- package/src/lib/streaming-ingestion.ts +0 -240
- package/src/lib/sync-cache.ts +0 -70
- package/src/lib/sync-plan.ts +0 -175
- package/src/lib/theme-transition.ts +0 -113
- package/src/lib/theme.tsx +0 -165
- package/src/lib/timeline-collections-live.ts +0 -407
- package/src/lib/timeline-live.ts +0 -302
- package/src/lib/timeline-read-model.ts +0 -1191
- package/src/lib/tweet-account-edges.ts +0 -48
- package/src/lib/tweet-lookup.ts +0 -46
- package/src/lib/tweet-render.ts +0 -430
- package/src/lib/tweet-repository.ts +0 -156
- package/src/lib/tweet-search-live.ts +0 -462
- package/src/lib/types.ts +0 -723
- package/src/lib/ui.ts +0 -529
- package/src/lib/url-expansion-store.ts +0 -110
- package/src/lib/url-expansion.ts +0 -343
- package/src/lib/url-safety.ts +0 -220
- package/src/lib/web-sync.ts +0 -533
- package/src/lib/whois.ts +0 -1080
- package/src/lib/x-profile.ts +0 -365
- package/src/lib/x-web.ts +0 -199
- package/src/lib/xurl-rate-limits.ts +0 -267
- package/src/lib/xurl.ts +0 -1677
- package/src/routeTree.gen.ts +0 -723
- package/src/router.tsx +0 -20
- package/src/routes/__root.tsx +0 -82
- package/src/routes/api/action.tsx +0 -147
- package/src/routes/api/avatar.tsx +0 -50
- package/src/routes/api/blocks.tsx +0 -35
- package/src/routes/api/conversation.tsx +0 -45
- package/src/routes/api/data-sources.tsx +0 -24
- package/src/routes/api/inbox.tsx +0 -43
- package/src/routes/api/link-insights.tsx +0 -85
- package/src/routes/api/link-preview.tsx +0 -47
- package/src/routes/api/network-map.tsx +0 -55
- package/src/routes/api/period-digest.tsx +0 -101
- package/src/routes/api/profile-analysis.tsx +0 -97
- package/src/routes/api/profile-hydrate.tsx +0 -54
- package/src/routes/api/query.tsx +0 -121
- package/src/routes/api/search-discussion.tsx +0 -113
- package/src/routes/api/status.tsx +0 -30
- package/src/routes/api/sync.tsx +0 -108
- package/src/routes/api/xurl-rate-limits.tsx +0 -24
- package/src/routes/blocks.tsx +0 -343
- package/src/routes/bookmarks.tsx +0 -18
- package/src/routes/data-sources.tsx +0 -249
- package/src/routes/discuss.tsx +0 -419
- package/src/routes/dms.tsx +0 -415
- package/src/routes/inbox.tsx +0 -232
- package/src/routes/index.tsx +0 -32
- package/src/routes/likes.tsx +0 -18
- package/src/routes/links.tsx +0 -631
- package/src/routes/mentions.tsx +0 -32
- package/src/routes/network-map.tsx +0 -732
- package/src/routes/profile-analyze.tsx +0 -112
- package/src/routes/profiles.$handle.tsx +0 -228
- package/src/routes/rate-limits.tsx +0 -305
- package/src/routes/today.tsx +0 -511
- package/src/styles.css +0 -257
- package/tsconfig.json +0 -29
- package/vite.config.ts +0 -29
- /package/{public → dist/client}/birdclaw-mark.png +0 -0
- /package/{public → dist/client}/favicon.ico +0 -0
- /package/{public → dist/client}/logo192.png +0 -0
- /package/{public → dist/client}/logo512.png +0 -0
- /package/{public → dist/client}/manifest.json +0 -0
- /package/{public → dist/client}/robots.txt +0 -0
package/src/lib/bird.ts
DELETED
|
@@ -1,1140 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
import { Effect } from "effect";
|
|
7
|
-
import { getBirdCommand } from "./config";
|
|
8
|
-
import { runEffectPromise } from "./effect-runtime";
|
|
9
|
-
import type {
|
|
10
|
-
XurlMentionData,
|
|
11
|
-
XurlFollowUsersResponse,
|
|
12
|
-
XurlMentionsResponse,
|
|
13
|
-
XurlMentionUser,
|
|
14
|
-
XurlReferencedTweet,
|
|
15
|
-
XurlTweetsResponse,
|
|
16
|
-
} from "./types";
|
|
17
|
-
|
|
18
|
-
const execFileAsync = promisify(execFile);
|
|
19
|
-
const BIRD_JSON_MAX_BUFFER_BYTES = 512 * 1024 * 1024;
|
|
20
|
-
const BIRD_STDOUT_REDIRECT_SCRIPT = 'out="$1"; shift; exec "$@" > "$out"';
|
|
21
|
-
|
|
22
|
-
interface BirdTweetMedia {
|
|
23
|
-
type?: string;
|
|
24
|
-
url?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface BirdTweetAuthor {
|
|
28
|
-
username?: string;
|
|
29
|
-
name?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface BirdTweetArticle {
|
|
33
|
-
title?: string;
|
|
34
|
-
previewText?: string;
|
|
35
|
-
coverImageUrl?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface BirdTweetItem {
|
|
39
|
-
id: string;
|
|
40
|
-
text: string;
|
|
41
|
-
createdAt: string;
|
|
42
|
-
replyCount?: number;
|
|
43
|
-
retweetCount?: number;
|
|
44
|
-
likeCount?: number;
|
|
45
|
-
conversationId?: string;
|
|
46
|
-
inReplyToStatusId?: string | null;
|
|
47
|
-
quotedStatusId?: string | null;
|
|
48
|
-
retweetedStatusId?: string | null;
|
|
49
|
-
quotedTweet?: { id?: string | null } | null;
|
|
50
|
-
retweetedTweet?: { id?: string | null } | null;
|
|
51
|
-
author?: BirdTweetAuthor;
|
|
52
|
-
authorId?: string;
|
|
53
|
-
media?: BirdTweetMedia[];
|
|
54
|
-
article?: BirdTweetArticle | null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface BirdDmUser {
|
|
58
|
-
id: string;
|
|
59
|
-
username?: string;
|
|
60
|
-
name?: string;
|
|
61
|
-
profileImageUrl?: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface BirdDmEvent {
|
|
65
|
-
id: string;
|
|
66
|
-
conversationId?: string;
|
|
67
|
-
text: string;
|
|
68
|
-
createdAt?: string;
|
|
69
|
-
senderId?: string;
|
|
70
|
-
recipientId?: string;
|
|
71
|
-
sender?: BirdDmUser;
|
|
72
|
-
recipient?: BirdDmUser;
|
|
73
|
-
inboxKind?: "accepted" | "request";
|
|
74
|
-
isMessageRequest?: boolean;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface BirdDmConversation {
|
|
78
|
-
id: string;
|
|
79
|
-
participants: BirdDmUser[];
|
|
80
|
-
messages: BirdDmEvent[];
|
|
81
|
-
lastMessageAt?: string;
|
|
82
|
-
lastMessagePreview?: string;
|
|
83
|
-
inboxKind?: "accepted" | "request";
|
|
84
|
-
isMessageRequest?: boolean;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface BirdDmsResponse {
|
|
88
|
-
success: true;
|
|
89
|
-
conversations: BirdDmConversation[];
|
|
90
|
-
events: BirdDmEvent[];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface BirdAuthenticatedAccount {
|
|
94
|
-
id?: string;
|
|
95
|
-
username: string;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export type BirdDmRequestAction = "accept" | "reject" | "block";
|
|
99
|
-
|
|
100
|
-
export type BirdDmMutationResponse =
|
|
101
|
-
| {
|
|
102
|
-
success: true;
|
|
103
|
-
conversationId?: string;
|
|
104
|
-
userId?: string;
|
|
105
|
-
username?: string;
|
|
106
|
-
blockedUserId?: string;
|
|
107
|
-
blockedUsername?: string;
|
|
108
|
-
}
|
|
109
|
-
| {
|
|
110
|
-
success: false;
|
|
111
|
-
error: string;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
interface BirdUserOverviewPayload {
|
|
115
|
-
user?: {
|
|
116
|
-
id?: string;
|
|
117
|
-
username?: string;
|
|
118
|
-
name?: string;
|
|
119
|
-
description?: string;
|
|
120
|
-
location?: string;
|
|
121
|
-
url?: string;
|
|
122
|
-
verified?: boolean;
|
|
123
|
-
verifiedType?: string;
|
|
124
|
-
verified_type?: string;
|
|
125
|
-
followersCount?: number;
|
|
126
|
-
followingCount?: number;
|
|
127
|
-
profileImageUrl?: string;
|
|
128
|
-
createdAt?: string;
|
|
129
|
-
entities?: Record<string, unknown>;
|
|
130
|
-
affiliation?: Record<string, unknown>;
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
interface BirdProfilesPayload {
|
|
135
|
-
users?: NonNullable<BirdUserOverviewPayload["user"]>[];
|
|
136
|
-
errors?: Array<{ target?: string; error?: string }>;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
type BirdFollowUsersPayload =
|
|
140
|
-
| NonNullable<BirdUserOverviewPayload["user"]>[]
|
|
141
|
-
| {
|
|
142
|
-
users?: NonNullable<BirdUserOverviewPayload["user"]>[];
|
|
143
|
-
nextCursor?: string | null;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
function toIsoTimestamp(value: string) {
|
|
147
|
-
const parsed = new Date(value);
|
|
148
|
-
if (Number.isNaN(parsed.getTime())) {
|
|
149
|
-
return value;
|
|
150
|
-
}
|
|
151
|
-
return parsed.toISOString();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function escapeJsonStringControlChars(value: string) {
|
|
155
|
-
let output = "";
|
|
156
|
-
let inString = false;
|
|
157
|
-
let escaped = false;
|
|
158
|
-
|
|
159
|
-
for (const character of value) {
|
|
160
|
-
if (!inString) {
|
|
161
|
-
output += character;
|
|
162
|
-
if (character === '"') {
|
|
163
|
-
inString = true;
|
|
164
|
-
}
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (escaped) {
|
|
169
|
-
output += character;
|
|
170
|
-
escaped = false;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (character === "\\") {
|
|
175
|
-
output += character;
|
|
176
|
-
escaped = true;
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (character === '"') {
|
|
181
|
-
output += character;
|
|
182
|
-
inString = false;
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (character === "\n") {
|
|
187
|
-
output += "\\n";
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
if (character === "\r") {
|
|
191
|
-
output += "\\r";
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
if (character === "\t") {
|
|
195
|
-
output += "\\t";
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
if (character.charCodeAt(0) < 0x20) {
|
|
199
|
-
output += `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
output += character;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return output;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function parseBirdJson(stdout: string) {
|
|
210
|
-
try {
|
|
211
|
-
return JSON.parse(stdout) as unknown;
|
|
212
|
-
} catch (error) {
|
|
213
|
-
if (!(error instanceof SyntaxError)) {
|
|
214
|
-
throw error;
|
|
215
|
-
}
|
|
216
|
-
return JSON.parse(escapeJsonStringControlChars(stdout)) as unknown;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function formatBirdCommandError(error: unknown, birdCommand: string) {
|
|
221
|
-
const text = [
|
|
222
|
-
error instanceof Error ? error.message : "",
|
|
223
|
-
error &&
|
|
224
|
-
typeof error === "object" &&
|
|
225
|
-
"stderr" in error &&
|
|
226
|
-
typeof error.stderr === "string"
|
|
227
|
-
? error.stderr
|
|
228
|
-
: "",
|
|
229
|
-
error &&
|
|
230
|
-
typeof error === "object" &&
|
|
231
|
-
"stdout" in error &&
|
|
232
|
-
typeof error.stdout === "string"
|
|
233
|
-
? error.stdout
|
|
234
|
-
: "",
|
|
235
|
-
].join("\n");
|
|
236
|
-
if (
|
|
237
|
-
(error instanceof Error &&
|
|
238
|
-
"code" in error &&
|
|
239
|
-
(error as { code?: unknown }).code === "ENOENT") ||
|
|
240
|
-
(/No such file or directory|command not found|cannot execute/i.test(text) &&
|
|
241
|
-
text.includes(birdCommand))
|
|
242
|
-
) {
|
|
243
|
-
return new Error(
|
|
244
|
-
`bird command unavailable: ${birdCommand}\nInstall bird on PATH, set BIRDCLAW_BIRD_COMMAND, or update ~/.birdclaw/config.json mentions.birdCommand.`,
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return error;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function isUnsupportedBirdOptionError(error: unknown, option: string) {
|
|
252
|
-
if (!error || typeof error !== "object") {
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
const text = [
|
|
256
|
-
error instanceof Error ? error.message : "",
|
|
257
|
-
"stderr" in error && typeof error.stderr === "string" ? error.stderr : "",
|
|
258
|
-
"stdout" in error && typeof error.stdout === "string" ? error.stdout : "",
|
|
259
|
-
].join("\n");
|
|
260
|
-
return text.includes(option) && /unknown option|error:/i.test(text);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function makeBirdStdoutTempEffect() {
|
|
264
|
-
return Effect.acquireRelease(
|
|
265
|
-
Effect.sync(() => {
|
|
266
|
-
const tempDir = mkdtempSync(join(tmpdir(), "birdclaw-bird-"));
|
|
267
|
-
return { tempDir, stdoutPath: join(tempDir, "stdout.json") };
|
|
268
|
-
}),
|
|
269
|
-
({ tempDir }) =>
|
|
270
|
-
Effect.sync(() => rmSync(tempDir, { recursive: true, force: true })),
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export function runBirdJsonCommandEffect(args: string[], timeoutMs?: number) {
|
|
275
|
-
return Effect.scoped(
|
|
276
|
-
Effect.gen(function* () {
|
|
277
|
-
const birdCommand = yield* Effect.try({
|
|
278
|
-
try: () => getBirdCommand(),
|
|
279
|
-
catch: (error) =>
|
|
280
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
281
|
-
});
|
|
282
|
-
const { stdoutPath } = yield* makeBirdStdoutTempEffect();
|
|
283
|
-
yield* Effect.tryPromise({
|
|
284
|
-
try: () =>
|
|
285
|
-
execFileAsync(
|
|
286
|
-
"/bin/bash",
|
|
287
|
-
[
|
|
288
|
-
"-c",
|
|
289
|
-
BIRD_STDOUT_REDIRECT_SCRIPT,
|
|
290
|
-
"birdclaw-bird",
|
|
291
|
-
stdoutPath,
|
|
292
|
-
birdCommand,
|
|
293
|
-
...args,
|
|
294
|
-
],
|
|
295
|
-
{ maxBuffer: BIRD_JSON_MAX_BUFFER_BYTES, timeout: timeoutMs },
|
|
296
|
-
),
|
|
297
|
-
catch: (error) => formatBirdCommandError(error, birdCommand),
|
|
298
|
-
});
|
|
299
|
-
return yield* Effect.try({
|
|
300
|
-
try: () => readFileSync(stdoutPath, "utf8"),
|
|
301
|
-
catch: (error) => error,
|
|
302
|
-
});
|
|
303
|
-
}),
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function runBirdJsonCommandAllowFailureEffect(
|
|
308
|
-
args: string[],
|
|
309
|
-
timeoutMs?: number,
|
|
310
|
-
) {
|
|
311
|
-
return Effect.scoped(
|
|
312
|
-
Effect.gen(function* () {
|
|
313
|
-
const birdCommand = yield* Effect.try({
|
|
314
|
-
try: () => getBirdCommand(),
|
|
315
|
-
catch: (error) =>
|
|
316
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
317
|
-
});
|
|
318
|
-
const { stdoutPath } = yield* makeBirdStdoutTempEffect();
|
|
319
|
-
yield* Effect.tryPromise({
|
|
320
|
-
try: () =>
|
|
321
|
-
execFileAsync(
|
|
322
|
-
"/bin/bash",
|
|
323
|
-
[
|
|
324
|
-
"-c",
|
|
325
|
-
BIRD_STDOUT_REDIRECT_SCRIPT,
|
|
326
|
-
"birdclaw-bird",
|
|
327
|
-
stdoutPath,
|
|
328
|
-
birdCommand,
|
|
329
|
-
...args,
|
|
330
|
-
],
|
|
331
|
-
{ maxBuffer: BIRD_JSON_MAX_BUFFER_BYTES, timeout: timeoutMs },
|
|
332
|
-
).catch((error: unknown) => {
|
|
333
|
-
const stdout = readFileSync(stdoutPath, "utf8");
|
|
334
|
-
if (stdout.trim().length > 0) {
|
|
335
|
-
return { stdout: "", stderr: "" };
|
|
336
|
-
}
|
|
337
|
-
throw formatBirdCommandError(error, birdCommand);
|
|
338
|
-
}),
|
|
339
|
-
catch: (error) => error,
|
|
340
|
-
});
|
|
341
|
-
return yield* Effect.try({
|
|
342
|
-
try: () => readFileSync(stdoutPath, "utf8"),
|
|
343
|
-
catch: (error) => error,
|
|
344
|
-
});
|
|
345
|
-
}),
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function getBirdTweetItems(payload: unknown, command: string) {
|
|
350
|
-
if (Array.isArray(payload)) {
|
|
351
|
-
return payload as BirdTweetItem[];
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (
|
|
355
|
-
payload &&
|
|
356
|
-
typeof payload === "object" &&
|
|
357
|
-
Array.isArray((payload as { tweets?: unknown }).tweets)
|
|
358
|
-
) {
|
|
359
|
-
return (payload as { tweets: BirdTweetItem[] }).tweets;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
throw new Error(`bird ${command} returned unexpected JSON`);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function getBirdTweetItem(payload: unknown, command: string) {
|
|
366
|
-
if (payload && typeof payload === "object") {
|
|
367
|
-
const record = payload as { id?: unknown };
|
|
368
|
-
if (typeof record.id === "string" && record.id.length > 0) {
|
|
369
|
-
return payload as BirdTweetItem;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
throw new Error(`bird ${command} returned unexpected JSON`);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function toMediaEntities(media: BirdTweetMedia[] | undefined) {
|
|
377
|
-
if (!Array.isArray(media) || media.length === 0) {
|
|
378
|
-
return undefined;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return {
|
|
382
|
-
urls: media
|
|
383
|
-
.filter((item) => typeof item?.url === "string" && item.url.length > 0)
|
|
384
|
-
.map((item, index) => ({
|
|
385
|
-
start: index,
|
|
386
|
-
end: index,
|
|
387
|
-
url: item.url as string,
|
|
388
|
-
expanded_url: item.url as string,
|
|
389
|
-
display_url: item.url as string,
|
|
390
|
-
media_key: `bird_media_${index}`,
|
|
391
|
-
})),
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function toTweetEntities(item: BirdTweetItem) {
|
|
396
|
-
const mediaEntities = toMediaEntities(item.media);
|
|
397
|
-
const title = item.article?.title?.trim();
|
|
398
|
-
if (!title) return mediaEntities;
|
|
399
|
-
const handle = item.author?.username?.replace(/^@/, "");
|
|
400
|
-
const url = handle
|
|
401
|
-
? `https://x.com/${handle}/status/${item.id}`
|
|
402
|
-
: `https://x.com/i/status/${item.id}`;
|
|
403
|
-
return {
|
|
404
|
-
...mediaEntities,
|
|
405
|
-
article: {
|
|
406
|
-
title,
|
|
407
|
-
url,
|
|
408
|
-
...(item.article?.previewText?.trim()
|
|
409
|
-
? { previewText: item.article.previewText.trim() }
|
|
410
|
-
: {}),
|
|
411
|
-
...(item.article?.coverImageUrl?.trim()
|
|
412
|
-
? { coverImageUrl: item.article.coverImageUrl.trim() }
|
|
413
|
-
: {}),
|
|
414
|
-
},
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function toReferencedTweets(item: BirdTweetItem) {
|
|
419
|
-
const references: XurlReferencedTweet[] = [];
|
|
420
|
-
if (typeof item.inReplyToStatusId === "string" && item.inReplyToStatusId) {
|
|
421
|
-
references.push({ type: "replied_to", id: item.inReplyToStatusId });
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const quotedTweetId =
|
|
425
|
-
typeof item.quotedStatusId === "string" && item.quotedStatusId
|
|
426
|
-
? item.quotedStatusId
|
|
427
|
-
: typeof item.quotedTweet?.id === "string" && item.quotedTweet.id
|
|
428
|
-
? item.quotedTweet.id
|
|
429
|
-
: null;
|
|
430
|
-
if (quotedTweetId) {
|
|
431
|
-
references.push({ type: "quoted", id: quotedTweetId });
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const retweetedTweetId =
|
|
435
|
-
typeof item.retweetedStatusId === "string" && item.retweetedStatusId
|
|
436
|
-
? item.retweetedStatusId
|
|
437
|
-
: typeof item.retweetedTweet?.id === "string" && item.retweetedTweet.id
|
|
438
|
-
? item.retweetedTweet.id
|
|
439
|
-
: null;
|
|
440
|
-
if (retweetedTweetId) {
|
|
441
|
-
references.push({ type: "retweeted", id: retweetedTweetId });
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return references.length > 0 ? references : undefined;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function normalizeBirdTweets(items: BirdTweetItem[]): XurlMentionsResponse {
|
|
448
|
-
const users = new Map<string, XurlMentionUser>();
|
|
449
|
-
const data = items.map((item): XurlMentionData => {
|
|
450
|
-
const authorId = String(
|
|
451
|
-
item.authorId ?? item.author?.username ?? "unknown",
|
|
452
|
-
);
|
|
453
|
-
if (!users.has(authorId)) {
|
|
454
|
-
users.set(authorId, {
|
|
455
|
-
id: authorId,
|
|
456
|
-
username: item.author?.username ?? `user_${authorId}`,
|
|
457
|
-
name: item.author?.name ?? item.author?.username ?? `user_${authorId}`,
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
return {
|
|
462
|
-
id: item.id,
|
|
463
|
-
author_id: authorId,
|
|
464
|
-
text: item.text,
|
|
465
|
-
created_at: toIsoTimestamp(item.createdAt),
|
|
466
|
-
conversation_id: item.conversationId ?? item.id,
|
|
467
|
-
entities: toTweetEntities(item),
|
|
468
|
-
referenced_tweets: toReferencedTweets(item),
|
|
469
|
-
public_metrics: {
|
|
470
|
-
reply_count: Number(item.replyCount ?? 0),
|
|
471
|
-
retweet_count: Number(item.retweetCount ?? 0),
|
|
472
|
-
like_count: Number(item.likeCount ?? 0),
|
|
473
|
-
},
|
|
474
|
-
edit_history_tweet_ids: [item.id],
|
|
475
|
-
};
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
return {
|
|
479
|
-
data,
|
|
480
|
-
includes:
|
|
481
|
-
users.size > 0 ? { users: Array.from(users.values()) } : undefined,
|
|
482
|
-
meta: {
|
|
483
|
-
result_count: data.length,
|
|
484
|
-
page_count: 1,
|
|
485
|
-
next_token: null,
|
|
486
|
-
...(data[0] ? { newest_id: data[0].id } : {}),
|
|
487
|
-
...(data.at(-1) ? { oldest_id: data.at(-1)?.id } : {}),
|
|
488
|
-
},
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function parseBirdJsonEffect(stdout: string) {
|
|
493
|
-
return Effect.try({
|
|
494
|
-
try: () => parseBirdJson(stdout),
|
|
495
|
-
catch: (error) => error,
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function normalizeBirdTweetsPayloadEffect(payload: unknown, command: string) {
|
|
500
|
-
return Effect.try({
|
|
501
|
-
try: () => normalizeBirdTweets(getBirdTweetItems(payload, command)),
|
|
502
|
-
catch: (error) => error,
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function normalizeBirdTweetItemEffect(payload: unknown, command: string) {
|
|
507
|
-
return Effect.try({
|
|
508
|
-
try: () => getBirdTweetItem(payload, command),
|
|
509
|
-
catch: (error) => error,
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
export function listMentionsViaBirdEffect({
|
|
514
|
-
maxResults,
|
|
515
|
-
}: {
|
|
516
|
-
maxResults: number;
|
|
517
|
-
}): Effect.Effect<XurlMentionsResponse, unknown> {
|
|
518
|
-
return Effect.gen(function* () {
|
|
519
|
-
const stdout = yield* runBirdJsonCommandEffect([
|
|
520
|
-
"mentions",
|
|
521
|
-
"-n",
|
|
522
|
-
String(maxResults),
|
|
523
|
-
"--json",
|
|
524
|
-
]);
|
|
525
|
-
const payload = yield* parseBirdJsonEffect(stdout);
|
|
526
|
-
return yield* normalizeBirdTweetsPayloadEffect(payload, "mentions");
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
export function listMentionsViaBird(options: {
|
|
531
|
-
maxResults: number;
|
|
532
|
-
}): Promise<XurlMentionsResponse> {
|
|
533
|
-
return runEffectPromise(listMentionsViaBirdEffect(options));
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function listTweetsViaBirdCommandEffect({
|
|
537
|
-
command,
|
|
538
|
-
maxResults,
|
|
539
|
-
all,
|
|
540
|
-
maxPages,
|
|
541
|
-
}: {
|
|
542
|
-
command: "likes" | "bookmarks";
|
|
543
|
-
maxResults: number;
|
|
544
|
-
all?: boolean;
|
|
545
|
-
maxPages?: number;
|
|
546
|
-
}): Effect.Effect<XurlMentionsResponse, unknown> {
|
|
547
|
-
return Effect.gen(function* () {
|
|
548
|
-
const args = [command, "-n", String(maxResults), "--json"];
|
|
549
|
-
if (all) {
|
|
550
|
-
args.push("--all");
|
|
551
|
-
}
|
|
552
|
-
if (maxPages !== undefined) {
|
|
553
|
-
args.push("--max-pages", String(maxPages));
|
|
554
|
-
}
|
|
555
|
-
const stdout = yield* runBirdJsonCommandEffect(args);
|
|
556
|
-
const payload = yield* parseBirdJsonEffect(stdout);
|
|
557
|
-
return yield* normalizeBirdTweetsPayloadEffect(payload, command);
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
export function listLikedTweetsViaBirdEffect(options: {
|
|
562
|
-
maxResults: number;
|
|
563
|
-
all?: boolean;
|
|
564
|
-
maxPages?: number;
|
|
565
|
-
}): Effect.Effect<XurlMentionsResponse, unknown> {
|
|
566
|
-
return listTweetsViaBirdCommandEffect({
|
|
567
|
-
command: "likes",
|
|
568
|
-
...options,
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
export function listLikedTweetsViaBird(options: {
|
|
573
|
-
maxResults: number;
|
|
574
|
-
all?: boolean;
|
|
575
|
-
maxPages?: number;
|
|
576
|
-
}): Promise<XurlMentionsResponse> {
|
|
577
|
-
return runEffectPromise(listLikedTweetsViaBirdEffect(options));
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
export function listBookmarkedTweetsViaBirdEffect(options: {
|
|
581
|
-
maxResults: number;
|
|
582
|
-
all?: boolean;
|
|
583
|
-
maxPages?: number;
|
|
584
|
-
}): Effect.Effect<XurlMentionsResponse, unknown> {
|
|
585
|
-
return listTweetsViaBirdCommandEffect({
|
|
586
|
-
command: "bookmarks",
|
|
587
|
-
...options,
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
export function listBookmarkedTweetsViaBird(options: {
|
|
592
|
-
maxResults: number;
|
|
593
|
-
all?: boolean;
|
|
594
|
-
maxPages?: number;
|
|
595
|
-
}): Promise<XurlMentionsResponse> {
|
|
596
|
-
return runEffectPromise(listBookmarkedTweetsViaBirdEffect(options));
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
export function searchTweetsViaBirdEffect(
|
|
600
|
-
query: string,
|
|
601
|
-
options: {
|
|
602
|
-
maxResults: number;
|
|
603
|
-
all?: boolean;
|
|
604
|
-
maxPages?: number;
|
|
605
|
-
},
|
|
606
|
-
): Effect.Effect<XurlMentionsResponse, unknown> {
|
|
607
|
-
return Effect.gen(function* () {
|
|
608
|
-
const args = ["search", query, "-n", String(options.maxResults), "--json"];
|
|
609
|
-
if (options.all) {
|
|
610
|
-
args.push("--all");
|
|
611
|
-
}
|
|
612
|
-
if (options.all && options.maxPages !== undefined) {
|
|
613
|
-
args.push("--max-pages", String(options.maxPages));
|
|
614
|
-
}
|
|
615
|
-
const stdout = yield* runBirdJsonCommandEffect(args);
|
|
616
|
-
const payload = yield* parseBirdJsonEffect(stdout);
|
|
617
|
-
return yield* normalizeBirdTweetsPayloadEffect(payload, "search");
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
export function searchTweetsViaBird(
|
|
622
|
-
query: string,
|
|
623
|
-
options: {
|
|
624
|
-
maxResults: number;
|
|
625
|
-
all?: boolean;
|
|
626
|
-
maxPages?: number;
|
|
627
|
-
},
|
|
628
|
-
): Promise<XurlMentionsResponse> {
|
|
629
|
-
return runEffectPromise(searchTweetsViaBirdEffect(query, options));
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
export function lookupTweetsByIdsViaBirdEffect(
|
|
633
|
-
ids: string[],
|
|
634
|
-
): Effect.Effect<XurlTweetsResponse, unknown> {
|
|
635
|
-
if (ids.length === 0) {
|
|
636
|
-
return Effect.succeed({ data: [] });
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
return Effect.gen(function* () {
|
|
640
|
-
const tweets = yield* Effect.forEach(
|
|
641
|
-
ids,
|
|
642
|
-
(id) =>
|
|
643
|
-
Effect.gen(function* () {
|
|
644
|
-
const stdout = yield* runBirdJsonCommandEffect([
|
|
645
|
-
"read",
|
|
646
|
-
id,
|
|
647
|
-
"--json",
|
|
648
|
-
]);
|
|
649
|
-
const payload = yield* parseBirdJsonEffect(stdout);
|
|
650
|
-
return yield* normalizeBirdTweetItemEffect(payload, "read");
|
|
651
|
-
}),
|
|
652
|
-
{ concurrency: "unbounded" },
|
|
653
|
-
);
|
|
654
|
-
return normalizeBirdTweets(tweets);
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
export function lookupTweetsByIdsViaBird(
|
|
659
|
-
ids: string[],
|
|
660
|
-
): Promise<XurlTweetsResponse> {
|
|
661
|
-
return runEffectPromise(lookupTweetsByIdsViaBirdEffect(ids));
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
export function listHomeTimelineViaBirdEffect({
|
|
665
|
-
maxResults,
|
|
666
|
-
following = true,
|
|
667
|
-
}: {
|
|
668
|
-
maxResults: number;
|
|
669
|
-
following?: boolean;
|
|
670
|
-
}): Effect.Effect<XurlMentionsResponse, unknown> {
|
|
671
|
-
return Effect.gen(function* () {
|
|
672
|
-
const args = ["home", "-n", String(maxResults), "--json"];
|
|
673
|
-
if (following) {
|
|
674
|
-
args.push("--following");
|
|
675
|
-
}
|
|
676
|
-
const stdout = yield* runBirdJsonCommandEffect(args);
|
|
677
|
-
const payload = yield* parseBirdJsonEffect(stdout);
|
|
678
|
-
return yield* normalizeBirdTweetsPayloadEffect(payload, "home");
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
export function listHomeTimelineViaBird(options: {
|
|
683
|
-
maxResults: number;
|
|
684
|
-
following?: boolean;
|
|
685
|
-
}): Promise<XurlMentionsResponse> {
|
|
686
|
-
return runEffectPromise(listHomeTimelineViaBirdEffect(options));
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
function normalizeBirdFollowUsers(
|
|
690
|
-
payload: unknown,
|
|
691
|
-
command: "followers" | "following",
|
|
692
|
-
maxResults: number,
|
|
693
|
-
): XurlFollowUsersResponse {
|
|
694
|
-
const rawPayload = payload as BirdFollowUsersPayload;
|
|
695
|
-
const users = Array.isArray(rawPayload) ? rawPayload : rawPayload.users;
|
|
696
|
-
if (!Array.isArray(users)) {
|
|
697
|
-
throw new Error(`bird ${command} returned unexpected JSON`);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const data = users
|
|
701
|
-
.map(toXurlMentionUser)
|
|
702
|
-
.filter((user): user is XurlMentionUser => Boolean(user));
|
|
703
|
-
const nextToken =
|
|
704
|
-
!Array.isArray(rawPayload) && typeof rawPayload.nextCursor === "string"
|
|
705
|
-
? rawPayload.nextCursor
|
|
706
|
-
: null;
|
|
707
|
-
|
|
708
|
-
return {
|
|
709
|
-
data,
|
|
710
|
-
meta: {
|
|
711
|
-
result_count: data.length,
|
|
712
|
-
page_count:
|
|
713
|
-
data.length > 0 ? Math.max(1, Math.ceil(data.length / maxResults)) : 1,
|
|
714
|
-
next_token: nextToken,
|
|
715
|
-
},
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function normalizeBirdFollowUsersEffect(
|
|
720
|
-
payload: unknown,
|
|
721
|
-
command: "followers" | "following",
|
|
722
|
-
maxResults: number,
|
|
723
|
-
) {
|
|
724
|
-
return Effect.try({
|
|
725
|
-
try: () => normalizeBirdFollowUsers(payload, command, maxResults),
|
|
726
|
-
catch: (error) => error,
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
export function listFollowUsersViaBirdEffect({
|
|
731
|
-
direction,
|
|
732
|
-
userId,
|
|
733
|
-
maxResults,
|
|
734
|
-
all,
|
|
735
|
-
maxPages,
|
|
736
|
-
}: {
|
|
737
|
-
direction: "followers" | "following";
|
|
738
|
-
userId?: string;
|
|
739
|
-
maxResults: number;
|
|
740
|
-
all?: boolean;
|
|
741
|
-
maxPages?: number;
|
|
742
|
-
}): Effect.Effect<XurlFollowUsersResponse, unknown> {
|
|
743
|
-
return Effect.gen(function* () {
|
|
744
|
-
const args = [direction, "-n", String(maxResults), "--json"];
|
|
745
|
-
if (userId) {
|
|
746
|
-
args.push("--user", userId);
|
|
747
|
-
}
|
|
748
|
-
if (all) {
|
|
749
|
-
args.push("--all");
|
|
750
|
-
}
|
|
751
|
-
if (maxPages !== undefined) {
|
|
752
|
-
args.push("--max-pages", String(maxPages));
|
|
753
|
-
}
|
|
754
|
-
const stdout = yield* runBirdJsonCommandEffect(args);
|
|
755
|
-
const payload = yield* parseBirdJsonEffect(stdout);
|
|
756
|
-
return yield* normalizeBirdFollowUsersEffect(
|
|
757
|
-
payload,
|
|
758
|
-
direction,
|
|
759
|
-
maxResults,
|
|
760
|
-
);
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
export function listFollowUsersViaBird(options: {
|
|
765
|
-
direction: "followers" | "following";
|
|
766
|
-
userId?: string;
|
|
767
|
-
maxResults: number;
|
|
768
|
-
all?: boolean;
|
|
769
|
-
maxPages?: number;
|
|
770
|
-
}): Promise<XurlFollowUsersResponse> {
|
|
771
|
-
return runEffectPromise(listFollowUsersViaBirdEffect(options));
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
export function listThreadViaBirdEffect({
|
|
775
|
-
tweetId,
|
|
776
|
-
all,
|
|
777
|
-
maxPages,
|
|
778
|
-
timeoutMs,
|
|
779
|
-
}: {
|
|
780
|
-
tweetId: string;
|
|
781
|
-
all?: boolean;
|
|
782
|
-
maxPages?: number;
|
|
783
|
-
timeoutMs?: number;
|
|
784
|
-
}): Effect.Effect<XurlMentionsResponse, unknown> {
|
|
785
|
-
return Effect.gen(function* () {
|
|
786
|
-
const args = ["thread", tweetId, "--json"];
|
|
787
|
-
if (all) {
|
|
788
|
-
args.push("--all");
|
|
789
|
-
}
|
|
790
|
-
if (maxPages !== undefined) {
|
|
791
|
-
args.push("--max-pages", String(maxPages));
|
|
792
|
-
}
|
|
793
|
-
const stdout = yield* runBirdJsonCommandEffect(args, timeoutMs);
|
|
794
|
-
const payload = yield* parseBirdJsonEffect(stdout);
|
|
795
|
-
return yield* normalizeBirdTweetsPayloadEffect(payload, "thread");
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
export function listThreadViaBird(options: {
|
|
800
|
-
tweetId: string;
|
|
801
|
-
all?: boolean;
|
|
802
|
-
maxPages?: number;
|
|
803
|
-
timeoutMs?: number;
|
|
804
|
-
}): Promise<XurlMentionsResponse> {
|
|
805
|
-
return runEffectPromise(listThreadViaBirdEffect(options));
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
function normalizeBirdDmsPayloadEffect(payload: unknown) {
|
|
809
|
-
return Effect.try({
|
|
810
|
-
try: () => {
|
|
811
|
-
if (
|
|
812
|
-
!payload ||
|
|
813
|
-
typeof payload !== "object" ||
|
|
814
|
-
(payload as { success?: unknown }).success !== true ||
|
|
815
|
-
!Array.isArray(
|
|
816
|
-
(payload as { conversations?: unknown }).conversations,
|
|
817
|
-
) ||
|
|
818
|
-
!Array.isArray((payload as { events?: unknown }).events)
|
|
819
|
-
) {
|
|
820
|
-
throw new Error("bird dms returned unexpected JSON");
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
return payload as BirdDmsResponse;
|
|
824
|
-
},
|
|
825
|
-
catch: (error) => error,
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function parseBirdWhoami(stdout: string): BirdAuthenticatedAccount {
|
|
830
|
-
const usernameMatch = stdout.match(/@([A-Za-z0-9_]{1,15})\b/);
|
|
831
|
-
if (!usernameMatch?.[1]) {
|
|
832
|
-
throw new Error("bird whoami did not report an authenticated username");
|
|
833
|
-
}
|
|
834
|
-
const id = stdout
|
|
835
|
-
.split(/\r?\n/)
|
|
836
|
-
.map((line) => line.trim())
|
|
837
|
-
.map((line) => {
|
|
838
|
-
const labeled = line.match(/(?:🪪|user_?id:?)[^\d]*(\d{2,})/i);
|
|
839
|
-
if (labeled?.[1]) {
|
|
840
|
-
return labeled[1];
|
|
841
|
-
}
|
|
842
|
-
if (/[A-Za-z@]/.test(line)) {
|
|
843
|
-
return undefined;
|
|
844
|
-
}
|
|
845
|
-
return line.match(/^\D*(\d{2,})\D*$/)?.[1];
|
|
846
|
-
})
|
|
847
|
-
.find((value): value is string => Boolean(value));
|
|
848
|
-
return {
|
|
849
|
-
username: usernameMatch[1],
|
|
850
|
-
...(id ? { id } : {}),
|
|
851
|
-
};
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
export function getAuthenticatedBirdAccountEffect(): Effect.Effect<
|
|
855
|
-
BirdAuthenticatedAccount,
|
|
856
|
-
unknown
|
|
857
|
-
> {
|
|
858
|
-
return Effect.gen(function* () {
|
|
859
|
-
const stdout = yield* runBirdJsonCommandEffect(["whoami"]);
|
|
860
|
-
return yield* Effect.try({
|
|
861
|
-
try: () => parseBirdWhoami(stdout),
|
|
862
|
-
catch: (error) => error,
|
|
863
|
-
});
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
export function getAuthenticatedBirdAccount(): Promise<BirdAuthenticatedAccount> {
|
|
868
|
-
return runEffectPromise(getAuthenticatedBirdAccountEffect());
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
export function listDirectMessagesViaBirdEffect({
|
|
872
|
-
maxResults,
|
|
873
|
-
inbox = "all",
|
|
874
|
-
maxPages,
|
|
875
|
-
allPages = false,
|
|
876
|
-
pageDelayMs,
|
|
877
|
-
}: {
|
|
878
|
-
maxResults: number;
|
|
879
|
-
inbox?: "all" | "accepted" | "requests";
|
|
880
|
-
maxPages?: number;
|
|
881
|
-
allPages?: boolean;
|
|
882
|
-
pageDelayMs?: number;
|
|
883
|
-
}): Effect.Effect<BirdDmsResponse, unknown> {
|
|
884
|
-
return Effect.gen(function* () {
|
|
885
|
-
const args = ["dms", "-n", String(maxResults), "--json"];
|
|
886
|
-
if (inbox !== "all") {
|
|
887
|
-
args.push("--inbox", inbox);
|
|
888
|
-
}
|
|
889
|
-
if (allPages) {
|
|
890
|
-
args.push("--all-pages");
|
|
891
|
-
} else if (typeof maxPages === "number") {
|
|
892
|
-
args.push("--max-pages", String(maxPages));
|
|
893
|
-
}
|
|
894
|
-
if (typeof pageDelayMs === "number" && pageDelayMs > 0) {
|
|
895
|
-
args.push("--page-delay-ms", String(pageDelayMs));
|
|
896
|
-
}
|
|
897
|
-
const stdout = yield* runBirdJsonCommandEffect(args);
|
|
898
|
-
const payload = yield* parseBirdJsonEffect(stdout);
|
|
899
|
-
return yield* normalizeBirdDmsPayloadEffect(payload);
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
export function listDirectMessagesViaBird(options: {
|
|
904
|
-
maxResults: number;
|
|
905
|
-
inbox?: "all" | "accepted" | "requests";
|
|
906
|
-
maxPages?: number;
|
|
907
|
-
allPages?: boolean;
|
|
908
|
-
pageDelayMs?: number;
|
|
909
|
-
}): Promise<BirdDmsResponse> {
|
|
910
|
-
return runEffectPromise(listDirectMessagesViaBirdEffect(options));
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
export function runDirectMessageRequestMutationViaBirdEffect({
|
|
914
|
-
action,
|
|
915
|
-
conversationId,
|
|
916
|
-
maxPages,
|
|
917
|
-
allPages = false,
|
|
918
|
-
}: {
|
|
919
|
-
action: BirdDmRequestAction;
|
|
920
|
-
conversationId: string;
|
|
921
|
-
maxPages?: number;
|
|
922
|
-
allPages?: boolean;
|
|
923
|
-
}): Effect.Effect<BirdDmMutationResponse, unknown> {
|
|
924
|
-
return Effect.gen(function* () {
|
|
925
|
-
const command =
|
|
926
|
-
action === "accept"
|
|
927
|
-
? "dm-accept"
|
|
928
|
-
: action === "reject"
|
|
929
|
-
? "dm-reject"
|
|
930
|
-
: "dm-block";
|
|
931
|
-
const args = [command, conversationId, "--json"];
|
|
932
|
-
if (action === "block") {
|
|
933
|
-
if (allPages) {
|
|
934
|
-
args.push("--all-pages");
|
|
935
|
-
} else if (typeof maxPages === "number") {
|
|
936
|
-
args.push("--max-pages", String(maxPages));
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
const stdout = yield* runBirdJsonCommandAllowFailureEffect(args);
|
|
940
|
-
const payload = yield* parseBirdJsonEffect(stdout);
|
|
941
|
-
if (
|
|
942
|
-
payload &&
|
|
943
|
-
typeof payload === "object" &&
|
|
944
|
-
typeof (payload as { success?: unknown }).success === "boolean"
|
|
945
|
-
) {
|
|
946
|
-
return payload as BirdDmMutationResponse;
|
|
947
|
-
}
|
|
948
|
-
throw new Error(`bird ${command} returned unexpected JSON`);
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
export function runDirectMessageRequestMutationViaBird(options: {
|
|
953
|
-
action: BirdDmRequestAction;
|
|
954
|
-
conversationId: string;
|
|
955
|
-
maxPages?: number;
|
|
956
|
-
allPages?: boolean;
|
|
957
|
-
}): Promise<BirdDmMutationResponse> {
|
|
958
|
-
return runEffectPromise(
|
|
959
|
-
runDirectMessageRequestMutationViaBirdEffect(options),
|
|
960
|
-
);
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
export function lookupProfileViaBirdEffect(
|
|
964
|
-
usernameOrId: string,
|
|
965
|
-
): Effect.Effect<XurlMentionUser | null, unknown> {
|
|
966
|
-
return Effect.gen(function* () {
|
|
967
|
-
const target = usernameOrId.trim().replace(/^@/, "");
|
|
968
|
-
if (!target) {
|
|
969
|
-
return null;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
const stdout = yield* runBirdJsonCommandEffect([
|
|
973
|
-
"user",
|
|
974
|
-
target,
|
|
975
|
-
"--json",
|
|
976
|
-
"--profile-only",
|
|
977
|
-
]).pipe(
|
|
978
|
-
Effect.catchAll((error) => {
|
|
979
|
-
if (!isUnsupportedBirdOptionError(error, "--profile-only")) {
|
|
980
|
-
return Effect.fail(error);
|
|
981
|
-
}
|
|
982
|
-
return runBirdJsonCommandEffect([
|
|
983
|
-
"user",
|
|
984
|
-
target,
|
|
985
|
-
"--json",
|
|
986
|
-
"--count",
|
|
987
|
-
"1",
|
|
988
|
-
]);
|
|
989
|
-
}),
|
|
990
|
-
);
|
|
991
|
-
const payload = (yield* parseBirdJsonEffect(
|
|
992
|
-
stdout,
|
|
993
|
-
)) as BirdUserOverviewPayload;
|
|
994
|
-
return toXurlMentionUser(payload.user);
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
export function lookupProfileViaBird(
|
|
999
|
-
usernameOrId: string,
|
|
1000
|
-
): Promise<XurlMentionUser | null> {
|
|
1001
|
-
return runEffectPromise(lookupProfileViaBirdEffect(usernameOrId));
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
function toXurlMentionUser(
|
|
1005
|
-
user: BirdUserOverviewPayload["user"],
|
|
1006
|
-
): XurlMentionUser | null {
|
|
1007
|
-
if (!user?.id || !user.username) {
|
|
1008
|
-
return null;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
return {
|
|
1012
|
-
id: String(user.id),
|
|
1013
|
-
username: String(user.username).replace(/^@/, ""),
|
|
1014
|
-
name: String(user.name ?? user.username),
|
|
1015
|
-
description:
|
|
1016
|
-
typeof user.description === "string" ? user.description : undefined,
|
|
1017
|
-
location: typeof user.location === "string" ? user.location : undefined,
|
|
1018
|
-
url: typeof user.url === "string" ? user.url : undefined,
|
|
1019
|
-
verified: typeof user.verified === "boolean" ? user.verified : undefined,
|
|
1020
|
-
verified_type:
|
|
1021
|
-
typeof user.verifiedType === "string"
|
|
1022
|
-
? user.verifiedType
|
|
1023
|
-
: typeof user.verified_type === "string"
|
|
1024
|
-
? user.verified_type
|
|
1025
|
-
: undefined,
|
|
1026
|
-
profile_image_url:
|
|
1027
|
-
typeof user.profileImageUrl === "string"
|
|
1028
|
-
? user.profileImageUrl
|
|
1029
|
-
: undefined,
|
|
1030
|
-
entities:
|
|
1031
|
-
user.entities && typeof user.entities === "object"
|
|
1032
|
-
? user.entities
|
|
1033
|
-
: undefined,
|
|
1034
|
-
affiliation:
|
|
1035
|
-
user.affiliation && typeof user.affiliation === "object"
|
|
1036
|
-
? user.affiliation
|
|
1037
|
-
: undefined,
|
|
1038
|
-
created_at: typeof user.createdAt === "string" ? user.createdAt : undefined,
|
|
1039
|
-
public_metrics: {
|
|
1040
|
-
followers_count: Number(user.followersCount ?? 0),
|
|
1041
|
-
following_count: Number(user.followingCount ?? 0),
|
|
1042
|
-
},
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
export function lookupProfilesViaBirdEffect(
|
|
1047
|
-
usernameOrIds: string[],
|
|
1048
|
-
): Effect.Effect<
|
|
1049
|
-
Array<{ target: string; user: XurlMentionUser | null; error?: string }>,
|
|
1050
|
-
unknown
|
|
1051
|
-
> {
|
|
1052
|
-
const targets = Array.from(
|
|
1053
|
-
new Set(
|
|
1054
|
-
usernameOrIds
|
|
1055
|
-
.map((target) => target.trim().replace(/^@/, ""))
|
|
1056
|
-
.filter((target) => target.length > 0),
|
|
1057
|
-
),
|
|
1058
|
-
);
|
|
1059
|
-
if (targets.length === 0) {
|
|
1060
|
-
return Effect.succeed([]);
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
return runBirdJsonCommandEffect(["profiles", ...targets, "--json"]).pipe(
|
|
1064
|
-
Effect.flatMap((stdout) =>
|
|
1065
|
-
Effect.gen(function* () {
|
|
1066
|
-
const payload = (yield* parseBirdJsonEffect(
|
|
1067
|
-
stdout,
|
|
1068
|
-
)) as BirdProfilesPayload;
|
|
1069
|
-
const users = (payload.users ?? [])
|
|
1070
|
-
.map(toXurlMentionUser)
|
|
1071
|
-
.filter((user): user is XurlMentionUser => Boolean(user));
|
|
1072
|
-
const byTarget = new Map<string, XurlMentionUser>();
|
|
1073
|
-
for (const user of users) {
|
|
1074
|
-
byTarget.set(String(user.id), user);
|
|
1075
|
-
byTarget.set(user.username.toLowerCase(), user);
|
|
1076
|
-
}
|
|
1077
|
-
const errors = new Map(
|
|
1078
|
-
(payload.errors ?? []).map((item) => [
|
|
1079
|
-
String(item.target ?? "")
|
|
1080
|
-
.replace(/^@/, "")
|
|
1081
|
-
.toLowerCase(),
|
|
1082
|
-
item.error ?? "Unknown error",
|
|
1083
|
-
]),
|
|
1084
|
-
);
|
|
1085
|
-
return targets.map((target) => ({
|
|
1086
|
-
target,
|
|
1087
|
-
user: byTarget.get(target.toLowerCase()) ?? null,
|
|
1088
|
-
...(errors.has(target.toLowerCase())
|
|
1089
|
-
? { error: errors.get(target.toLowerCase()) }
|
|
1090
|
-
: {}),
|
|
1091
|
-
}));
|
|
1092
|
-
}),
|
|
1093
|
-
),
|
|
1094
|
-
Effect.catchAll((error) => {
|
|
1095
|
-
if (!isUnsupportedBirdOptionError(error, "profiles")) {
|
|
1096
|
-
return Effect.fail(error);
|
|
1097
|
-
}
|
|
1098
|
-
return Effect.forEach(
|
|
1099
|
-
targets,
|
|
1100
|
-
(target) =>
|
|
1101
|
-
lookupProfileViaBirdEffect(target).pipe(
|
|
1102
|
-
Effect.map((user) => ({ target, user })),
|
|
1103
|
-
Effect.catchAll((lookupError) =>
|
|
1104
|
-
Effect.succeed({
|
|
1105
|
-
target,
|
|
1106
|
-
user: null,
|
|
1107
|
-
error:
|
|
1108
|
-
lookupError instanceof Error
|
|
1109
|
-
? lookupError.message
|
|
1110
|
-
: String(lookupError),
|
|
1111
|
-
}),
|
|
1112
|
-
),
|
|
1113
|
-
),
|
|
1114
|
-
{ concurrency: "unbounded" },
|
|
1115
|
-
);
|
|
1116
|
-
}),
|
|
1117
|
-
);
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
export function lookupProfilesViaBird(
|
|
1121
|
-
usernameOrIds: string[],
|
|
1122
|
-
): Promise<
|
|
1123
|
-
Array<{ target: string; user: XurlMentionUser | null; error?: string }>
|
|
1124
|
-
> {
|
|
1125
|
-
return runEffectPromise(lookupProfilesViaBirdEffect(usernameOrIds));
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
export const __test__ = {
|
|
1129
|
-
toIsoTimestamp,
|
|
1130
|
-
escapeJsonStringControlChars,
|
|
1131
|
-
parseBirdJson,
|
|
1132
|
-
formatBirdCommandError,
|
|
1133
|
-
isUnsupportedBirdOptionError,
|
|
1134
|
-
getBirdTweetItems,
|
|
1135
|
-
getBirdTweetItem,
|
|
1136
|
-
toMediaEntities,
|
|
1137
|
-
toTweetEntities,
|
|
1138
|
-
toReferencedTweets,
|
|
1139
|
-
normalizeBirdTweets,
|
|
1140
|
-
};
|