birdclaw 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/README.md +55 -5
- package/bin/birdclaw.mjs +50 -11
- package/package.json +9 -7
- package/public/birdclaw-mark.png +0 -0
- package/scripts/browser-perf.mjs +400 -0
- package/scripts/build-docs-site.mjs +940 -0
- package/scripts/docs-site-assets.mjs +311 -0
- package/scripts/run-vitest.mjs +21 -0
- package/scripts/sanitize-node-options.mjs +23 -0
- package/scripts/start-test-server.mjs +42 -0
- package/src/cli.ts +422 -14
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +29 -9
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/BrandMark.tsx +67 -0
- package/src/components/ConversationThread.tsx +11 -10
- package/src/components/DmWorkspace.tsx +39 -14
- package/src/components/FeedState.tsx +147 -0
- package/src/components/InboxCard.tsx +14 -2
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +452 -0
- package/src/components/SavedTimelineView.tsx +64 -56
- package/src/components/SyncNowButton.tsx +137 -0
- package/src/components/ThemeSlider.tsx +49 -93
- package/src/components/TimelineCard.tsx +364 -136
- package/src/components/TimelineRouteFrame.tsx +170 -0
- package/src/components/TweetMediaGrid.tsx +170 -24
- package/src/components/TweetRichText.tsx +28 -13
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +153 -0
- package/src/lib/account-sync-job.ts +654 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +304 -0
- package/src/lib/archive-finder.ts +72 -53
- package/src/lib/archive-import.ts +1377 -1298
- package/src/lib/authored-live.ts +261 -204
- package/src/lib/avatar-cache.ts +159 -44
- package/src/lib/backup.ts +1532 -951
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +549 -194
- package/src/lib/blocklist.ts +40 -23
- package/src/lib/blocks-write.ts +129 -80
- package/src/lib/blocks.ts +165 -97
- package/src/lib/bookmark-sync-job.ts +250 -160
- package/src/lib/conversation-surface.ts +205 -0
- package/src/lib/db.ts +35 -3
- package/src/lib/dms-live.ts +720 -66
- package/src/lib/effect-runtime.ts +45 -0
- package/src/lib/follow-graph.ts +224 -180
- package/src/lib/http-effect.ts +222 -0
- package/src/lib/inbox.ts +74 -43
- package/src/lib/link-index.ts +88 -76
- package/src/lib/link-insights.ts +24 -0
- package/src/lib/link-preview-metadata.ts +472 -52
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +352 -288
- package/src/lib/mentions-live.ts +390 -342
- package/src/lib/moderation-target.ts +102 -65
- package/src/lib/moderation-write.ts +77 -18
- package/src/lib/mutes-write.ts +129 -80
- package/src/lib/mutes.ts +8 -1
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +953 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-hydration.ts +124 -72
- package/src/lib/profile-replies.ts +60 -43
- package/src/lib/profile-resolver.ts +402 -294
- package/src/lib/queries.ts +1024 -189
- package/src/lib/research.ts +165 -120
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +60 -39
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/types.ts +38 -1
- package/src/lib/ui.ts +41 -10
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +511 -0
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl.ts +681 -411
- package/src/routeTree.gen.ts +63 -0
- package/src/routes/__root.tsx +25 -5
- package/src/routes/api/action.tsx +127 -78
- package/src/routes/api/avatar.tsx +39 -30
- package/src/routes/api/blocks.tsx +26 -23
- package/src/routes/api/conversation.tsx +25 -14
- package/src/routes/api/inbox.tsx +27 -21
- package/src/routes/api/link-insights.tsx +31 -25
- package/src/routes/api/link-preview.tsx +25 -21
- package/src/routes/api/period-digest.tsx +123 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +79 -55
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +105 -0
- package/src/routes/dms.tsx +195 -55
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/index.tsx +21 -127
- package/src/routes/links.tsx +50 -5
- package/src/routes/mentions.tsx +21 -127
- package/src/routes/today.tsx +441 -0
- package/src/styles.css +74 -11
- package/vite.config.ts +8 -0
package/src/lib/bird.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { Effect } from "effect";
|
|
6
7
|
import { getBirdCommand } from "./config";
|
|
8
|
+
import { runEffectPromise } from "./effect-runtime";
|
|
7
9
|
import type {
|
|
8
10
|
XurlMentionData,
|
|
9
11
|
XurlFollowUsersResponse,
|
|
@@ -15,6 +17,7 @@ import type {
|
|
|
15
17
|
|
|
16
18
|
const execFileAsync = promisify(execFile);
|
|
17
19
|
const BIRD_JSON_MAX_BUFFER_BYTES = 512 * 1024 * 1024;
|
|
20
|
+
const BIRD_STDOUT_REDIRECT_SCRIPT = 'out="$1"; shift; exec "$@" > "$out"';
|
|
18
21
|
|
|
19
22
|
interface BirdTweetMedia {
|
|
20
23
|
type?: string;
|
|
@@ -36,7 +39,9 @@ interface BirdTweetItem {
|
|
|
36
39
|
conversationId?: string;
|
|
37
40
|
inReplyToStatusId?: string | null;
|
|
38
41
|
quotedStatusId?: string | null;
|
|
42
|
+
retweetedStatusId?: string | null;
|
|
39
43
|
quotedTweet?: { id?: string | null } | null;
|
|
44
|
+
retweetedTweet?: { id?: string | null } | null;
|
|
40
45
|
author?: BirdTweetAuthor;
|
|
41
46
|
authorId?: string;
|
|
42
47
|
media?: BirdTweetMedia[];
|
|
@@ -58,6 +63,8 @@ export interface BirdDmEvent {
|
|
|
58
63
|
recipientId?: string;
|
|
59
64
|
sender?: BirdDmUser;
|
|
60
65
|
recipient?: BirdDmUser;
|
|
66
|
+
inboxKind?: "accepted" | "request";
|
|
67
|
+
isMessageRequest?: boolean;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
export interface BirdDmConversation {
|
|
@@ -66,6 +73,8 @@ export interface BirdDmConversation {
|
|
|
66
73
|
messages: BirdDmEvent[];
|
|
67
74
|
lastMessageAt?: string;
|
|
68
75
|
lastMessagePreview?: string;
|
|
76
|
+
inboxKind?: "accepted" | "request";
|
|
77
|
+
isMessageRequest?: boolean;
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
export interface BirdDmsResponse {
|
|
@@ -74,6 +83,27 @@ export interface BirdDmsResponse {
|
|
|
74
83
|
events: BirdDmEvent[];
|
|
75
84
|
}
|
|
76
85
|
|
|
86
|
+
export interface BirdAuthenticatedAccount {
|
|
87
|
+
id?: string;
|
|
88
|
+
username: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type BirdDmRequestAction = "accept" | "reject" | "block";
|
|
92
|
+
|
|
93
|
+
export type BirdDmMutationResponse =
|
|
94
|
+
| {
|
|
95
|
+
success: true;
|
|
96
|
+
conversationId?: string;
|
|
97
|
+
userId?: string;
|
|
98
|
+
username?: string;
|
|
99
|
+
blockedUserId?: string;
|
|
100
|
+
blockedUsername?: string;
|
|
101
|
+
}
|
|
102
|
+
| {
|
|
103
|
+
success: false;
|
|
104
|
+
error: string;
|
|
105
|
+
};
|
|
106
|
+
|
|
77
107
|
interface BirdUserOverviewPayload {
|
|
78
108
|
user?: {
|
|
79
109
|
id?: string;
|
|
@@ -223,23 +253,90 @@ function isUnsupportedBirdOptionError(error: unknown, option: string) {
|
|
|
223
253
|
return text.includes(option) && /unknown option|error:/i.test(text);
|
|
224
254
|
}
|
|
225
255
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
256
|
+
function makeBirdStdoutTempEffect() {
|
|
257
|
+
return Effect.acquireRelease(
|
|
258
|
+
Effect.sync(() => {
|
|
259
|
+
const tempDir = mkdtempSync(join(tmpdir(), "birdclaw-bird-"));
|
|
260
|
+
return { tempDir, stdoutPath: join(tempDir, "stdout.json") };
|
|
261
|
+
}),
|
|
262
|
+
({ tempDir }) =>
|
|
263
|
+
Effect.sync(() => rmSync(tempDir, { recursive: true, force: true })),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function runBirdJsonCommandEffect(args: string[], timeoutMs?: number) {
|
|
268
|
+
return Effect.scoped(
|
|
269
|
+
Effect.gen(function* () {
|
|
270
|
+
const birdCommand = yield* Effect.try({
|
|
271
|
+
try: () => getBirdCommand(),
|
|
272
|
+
catch: (error) =>
|
|
273
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
274
|
+
});
|
|
275
|
+
const { stdoutPath } = yield* makeBirdStdoutTempEffect();
|
|
276
|
+
yield* Effect.tryPromise({
|
|
277
|
+
try: () =>
|
|
278
|
+
execFileAsync(
|
|
279
|
+
"/bin/bash",
|
|
280
|
+
[
|
|
281
|
+
"-c",
|
|
282
|
+
BIRD_STDOUT_REDIRECT_SCRIPT,
|
|
283
|
+
"birdclaw-bird",
|
|
284
|
+
stdoutPath,
|
|
285
|
+
birdCommand,
|
|
286
|
+
...args,
|
|
287
|
+
],
|
|
288
|
+
{ maxBuffer: BIRD_JSON_MAX_BUFFER_BYTES, timeout: timeoutMs },
|
|
289
|
+
),
|
|
290
|
+
catch: (error) => formatBirdCommandError(error, birdCommand),
|
|
291
|
+
});
|
|
292
|
+
return yield* Effect.try({
|
|
293
|
+
try: () => readFileSync(stdoutPath, "utf8"),
|
|
294
|
+
catch: (error) => error,
|
|
295
|
+
});
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function runBirdJsonCommandAllowFailureEffect(
|
|
301
|
+
args: string[],
|
|
302
|
+
timeoutMs?: number,
|
|
303
|
+
) {
|
|
304
|
+
return Effect.scoped(
|
|
305
|
+
Effect.gen(function* () {
|
|
306
|
+
const birdCommand = yield* Effect.try({
|
|
307
|
+
try: () => getBirdCommand(),
|
|
308
|
+
catch: (error) =>
|
|
309
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
310
|
+
});
|
|
311
|
+
const { stdoutPath } = yield* makeBirdStdoutTempEffect();
|
|
312
|
+
yield* Effect.tryPromise({
|
|
313
|
+
try: () =>
|
|
314
|
+
execFileAsync(
|
|
315
|
+
"/bin/bash",
|
|
316
|
+
[
|
|
317
|
+
"-c",
|
|
318
|
+
BIRD_STDOUT_REDIRECT_SCRIPT,
|
|
319
|
+
"birdclaw-bird",
|
|
320
|
+
stdoutPath,
|
|
321
|
+
birdCommand,
|
|
322
|
+
...args,
|
|
323
|
+
],
|
|
324
|
+
{ maxBuffer: BIRD_JSON_MAX_BUFFER_BYTES, timeout: timeoutMs },
|
|
325
|
+
).catch((error: unknown) => {
|
|
326
|
+
const stdout = readFileSync(stdoutPath, "utf8");
|
|
327
|
+
if (stdout.trim().length > 0) {
|
|
328
|
+
return { stdout: "", stderr: "" };
|
|
329
|
+
}
|
|
330
|
+
throw formatBirdCommandError(error, birdCommand);
|
|
331
|
+
}),
|
|
332
|
+
catch: (error) => error,
|
|
333
|
+
});
|
|
334
|
+
return yield* Effect.try({
|
|
335
|
+
try: () => readFileSync(stdoutPath, "utf8"),
|
|
336
|
+
catch: (error) => error,
|
|
337
|
+
});
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
243
340
|
}
|
|
244
341
|
|
|
245
342
|
function getBirdTweetItems(payload: unknown, command: string) {
|
|
@@ -304,6 +401,16 @@ function toReferencedTweets(item: BirdTweetItem) {
|
|
|
304
401
|
references.push({ type: "quoted", id: quotedTweetId });
|
|
305
402
|
}
|
|
306
403
|
|
|
404
|
+
const retweetedTweetId =
|
|
405
|
+
typeof item.retweetedStatusId === "string" && item.retweetedStatusId
|
|
406
|
+
? item.retweetedStatusId
|
|
407
|
+
: typeof item.retweetedTweet?.id === "string" && item.retweetedTweet.id
|
|
408
|
+
? item.retweetedTweet.id
|
|
409
|
+
: null;
|
|
410
|
+
if (retweetedTweetId) {
|
|
411
|
+
references.push({ type: "retweeted", id: retweetedTweetId });
|
|
412
|
+
}
|
|
413
|
+
|
|
307
414
|
return references.length > 0 ? references : undefined;
|
|
308
415
|
}
|
|
309
416
|
|
|
@@ -352,23 +459,51 @@ function normalizeBirdTweets(items: BirdTweetItem[]): XurlMentionsResponse {
|
|
|
352
459
|
};
|
|
353
460
|
}
|
|
354
461
|
|
|
355
|
-
|
|
462
|
+
function parseBirdJsonEffect(stdout: string) {
|
|
463
|
+
return Effect.try({
|
|
464
|
+
try: () => parseBirdJson(stdout),
|
|
465
|
+
catch: (error) => error,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function normalizeBirdTweetsPayloadEffect(payload: unknown, command: string) {
|
|
470
|
+
return Effect.try({
|
|
471
|
+
try: () => normalizeBirdTweets(getBirdTweetItems(payload, command)),
|
|
472
|
+
catch: (error) => error,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function normalizeBirdTweetItemEffect(payload: unknown, command: string) {
|
|
477
|
+
return Effect.try({
|
|
478
|
+
try: () => getBirdTweetItem(payload, command),
|
|
479
|
+
catch: (error) => error,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function listMentionsViaBirdEffect({
|
|
356
484
|
maxResults,
|
|
357
485
|
}: {
|
|
358
486
|
maxResults: number;
|
|
359
|
-
}):
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
487
|
+
}): Effect.Effect<XurlMentionsResponse, unknown> {
|
|
488
|
+
return Effect.gen(function* () {
|
|
489
|
+
const stdout = yield* runBirdJsonCommandEffect([
|
|
490
|
+
"mentions",
|
|
491
|
+
"-n",
|
|
492
|
+
String(maxResults),
|
|
493
|
+
"--json",
|
|
494
|
+
]);
|
|
495
|
+
const payload = yield* parseBirdJsonEffect(stdout);
|
|
496
|
+
return yield* normalizeBirdTweetsPayloadEffect(payload, "mentions");
|
|
497
|
+
});
|
|
498
|
+
}
|
|
367
499
|
|
|
368
|
-
|
|
500
|
+
export function listMentionsViaBird(options: {
|
|
501
|
+
maxResults: number;
|
|
502
|
+
}): Promise<XurlMentionsResponse> {
|
|
503
|
+
return runEffectPromise(listMentionsViaBirdEffect(options));
|
|
369
504
|
}
|
|
370
505
|
|
|
371
|
-
|
|
506
|
+
function listTweetsViaBirdCommandEffect({
|
|
372
507
|
command,
|
|
373
508
|
maxResults,
|
|
374
509
|
all,
|
|
@@ -378,86 +513,114 @@ async function listTweetsViaBirdCommand({
|
|
|
378
513
|
maxResults: number;
|
|
379
514
|
all?: boolean;
|
|
380
515
|
maxPages?: number;
|
|
381
|
-
}):
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
516
|
+
}): Effect.Effect<XurlMentionsResponse, unknown> {
|
|
517
|
+
return Effect.gen(function* () {
|
|
518
|
+
const args = [command, "-n", String(maxResults), "--json"];
|
|
519
|
+
if (all) {
|
|
520
|
+
args.push("--all");
|
|
521
|
+
}
|
|
522
|
+
if (maxPages !== undefined) {
|
|
523
|
+
args.push("--max-pages", String(maxPages));
|
|
524
|
+
}
|
|
525
|
+
const stdout = yield* runBirdJsonCommandEffect(args);
|
|
526
|
+
const payload = yield* parseBirdJsonEffect(stdout);
|
|
527
|
+
return yield* normalizeBirdTweetsPayloadEffect(payload, command);
|
|
528
|
+
});
|
|
393
529
|
}
|
|
394
530
|
|
|
395
|
-
export
|
|
396
|
-
maxResults,
|
|
397
|
-
all,
|
|
398
|
-
maxPages,
|
|
399
|
-
}: {
|
|
531
|
+
export function listLikedTweetsViaBirdEffect(options: {
|
|
400
532
|
maxResults: number;
|
|
401
533
|
all?: boolean;
|
|
402
534
|
maxPages?: number;
|
|
403
|
-
}):
|
|
404
|
-
return
|
|
535
|
+
}): Effect.Effect<XurlMentionsResponse, unknown> {
|
|
536
|
+
return listTweetsViaBirdCommandEffect({
|
|
405
537
|
command: "likes",
|
|
406
|
-
|
|
407
|
-
all,
|
|
408
|
-
maxPages,
|
|
538
|
+
...options,
|
|
409
539
|
});
|
|
410
540
|
}
|
|
411
541
|
|
|
412
|
-
export
|
|
413
|
-
maxResults,
|
|
414
|
-
all,
|
|
415
|
-
maxPages,
|
|
416
|
-
}: {
|
|
542
|
+
export function listLikedTweetsViaBird(options: {
|
|
417
543
|
maxResults: number;
|
|
418
544
|
all?: boolean;
|
|
419
545
|
maxPages?: number;
|
|
420
546
|
}): Promise<XurlMentionsResponse> {
|
|
421
|
-
return
|
|
547
|
+
return runEffectPromise(listLikedTweetsViaBirdEffect(options));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function listBookmarkedTweetsViaBirdEffect(options: {
|
|
551
|
+
maxResults: number;
|
|
552
|
+
all?: boolean;
|
|
553
|
+
maxPages?: number;
|
|
554
|
+
}): Effect.Effect<XurlMentionsResponse, unknown> {
|
|
555
|
+
return listTweetsViaBirdCommandEffect({
|
|
422
556
|
command: "bookmarks",
|
|
423
|
-
|
|
424
|
-
all,
|
|
425
|
-
maxPages,
|
|
557
|
+
...options,
|
|
426
558
|
});
|
|
427
559
|
}
|
|
428
560
|
|
|
429
|
-
export
|
|
561
|
+
export function listBookmarkedTweetsViaBird(options: {
|
|
562
|
+
maxResults: number;
|
|
563
|
+
all?: boolean;
|
|
564
|
+
maxPages?: number;
|
|
565
|
+
}): Promise<XurlMentionsResponse> {
|
|
566
|
+
return runEffectPromise(listBookmarkedTweetsViaBirdEffect(options));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function lookupTweetsByIdsViaBirdEffect(
|
|
430
570
|
ids: string[],
|
|
431
|
-
):
|
|
571
|
+
): Effect.Effect<XurlTweetsResponse, unknown> {
|
|
432
572
|
if (ids.length === 0) {
|
|
433
|
-
return { data: [] };
|
|
573
|
+
return Effect.succeed({ data: [] });
|
|
434
574
|
}
|
|
435
575
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
576
|
+
return Effect.gen(function* () {
|
|
577
|
+
const tweets = yield* Effect.forEach(
|
|
578
|
+
ids,
|
|
579
|
+
(id) =>
|
|
580
|
+
Effect.gen(function* () {
|
|
581
|
+
const stdout = yield* runBirdJsonCommandEffect([
|
|
582
|
+
"read",
|
|
583
|
+
id,
|
|
584
|
+
"--json",
|
|
585
|
+
]);
|
|
586
|
+
const payload = yield* parseBirdJsonEffect(stdout);
|
|
587
|
+
return yield* normalizeBirdTweetItemEffect(payload, "read");
|
|
588
|
+
}),
|
|
589
|
+
{ concurrency: "unbounded" },
|
|
590
|
+
);
|
|
591
|
+
return normalizeBirdTweets(tweets);
|
|
592
|
+
});
|
|
593
|
+
}
|
|
442
594
|
|
|
443
|
-
|
|
595
|
+
export function lookupTweetsByIdsViaBird(
|
|
596
|
+
ids: string[],
|
|
597
|
+
): Promise<XurlTweetsResponse> {
|
|
598
|
+
return runEffectPromise(lookupTweetsByIdsViaBirdEffect(ids));
|
|
444
599
|
}
|
|
445
600
|
|
|
446
|
-
export
|
|
601
|
+
export function listHomeTimelineViaBirdEffect({
|
|
447
602
|
maxResults,
|
|
448
603
|
following = true,
|
|
449
604
|
}: {
|
|
450
605
|
maxResults: number;
|
|
451
606
|
following?: boolean;
|
|
452
|
-
}):
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
607
|
+
}): Effect.Effect<XurlMentionsResponse, unknown> {
|
|
608
|
+
return Effect.gen(function* () {
|
|
609
|
+
const args = ["home", "-n", String(maxResults), "--json"];
|
|
610
|
+
if (following) {
|
|
611
|
+
args.push("--following");
|
|
612
|
+
}
|
|
613
|
+
const stdout = yield* runBirdJsonCommandEffect(args);
|
|
614
|
+
const payload = yield* parseBirdJsonEffect(stdout);
|
|
615
|
+
return yield* normalizeBirdTweetsPayloadEffect(payload, "home");
|
|
616
|
+
});
|
|
617
|
+
}
|
|
459
618
|
|
|
460
|
-
|
|
619
|
+
export function listHomeTimelineViaBird(options: {
|
|
620
|
+
maxResults: number;
|
|
621
|
+
following?: boolean;
|
|
622
|
+
}): Promise<XurlMentionsResponse> {
|
|
623
|
+
return runEffectPromise(listHomeTimelineViaBirdEffect(options));
|
|
461
624
|
}
|
|
462
625
|
|
|
463
626
|
function normalizeBirdFollowUsers(
|
|
@@ -490,7 +653,18 @@ function normalizeBirdFollowUsers(
|
|
|
490
653
|
};
|
|
491
654
|
}
|
|
492
655
|
|
|
493
|
-
|
|
656
|
+
function normalizeBirdFollowUsersEffect(
|
|
657
|
+
payload: unknown,
|
|
658
|
+
command: "followers" | "following",
|
|
659
|
+
maxResults: number,
|
|
660
|
+
) {
|
|
661
|
+
return Effect.try({
|
|
662
|
+
try: () => normalizeBirdFollowUsers(payload, command, maxResults),
|
|
663
|
+
catch: (error) => error,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function listFollowUsersViaBirdEffect({
|
|
494
668
|
direction,
|
|
495
669
|
userId,
|
|
496
670
|
maxResults,
|
|
@@ -502,24 +676,39 @@ export async function listFollowUsersViaBird({
|
|
|
502
676
|
maxResults: number;
|
|
503
677
|
all?: boolean;
|
|
504
678
|
maxPages?: number;
|
|
505
|
-
}):
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
679
|
+
}): Effect.Effect<XurlFollowUsersResponse, unknown> {
|
|
680
|
+
return Effect.gen(function* () {
|
|
681
|
+
const args = [direction, "-n", String(maxResults), "--json"];
|
|
682
|
+
if (userId) {
|
|
683
|
+
args.push("--user", userId);
|
|
684
|
+
}
|
|
685
|
+
if (all) {
|
|
686
|
+
args.push("--all");
|
|
687
|
+
}
|
|
688
|
+
if (maxPages !== undefined) {
|
|
689
|
+
args.push("--max-pages", String(maxPages));
|
|
690
|
+
}
|
|
691
|
+
const stdout = yield* runBirdJsonCommandEffect(args);
|
|
692
|
+
const payload = yield* parseBirdJsonEffect(stdout);
|
|
693
|
+
return yield* normalizeBirdFollowUsersEffect(
|
|
694
|
+
payload,
|
|
695
|
+
direction,
|
|
696
|
+
maxResults,
|
|
697
|
+
);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
518
700
|
|
|
519
|
-
|
|
701
|
+
export function listFollowUsersViaBird(options: {
|
|
702
|
+
direction: "followers" | "following";
|
|
703
|
+
userId?: string;
|
|
704
|
+
maxResults: number;
|
|
705
|
+
all?: boolean;
|
|
706
|
+
maxPages?: number;
|
|
707
|
+
}): Promise<XurlFollowUsersResponse> {
|
|
708
|
+
return runEffectPromise(listFollowUsersViaBirdEffect(options));
|
|
520
709
|
}
|
|
521
710
|
|
|
522
|
-
export
|
|
711
|
+
export function listThreadViaBirdEffect({
|
|
523
712
|
tweetId,
|
|
524
713
|
all,
|
|
525
714
|
maxPages,
|
|
@@ -529,75 +718,224 @@ export async function listThreadViaBird({
|
|
|
529
718
|
all?: boolean;
|
|
530
719
|
maxPages?: number;
|
|
531
720
|
timeoutMs?: number;
|
|
721
|
+
}): Effect.Effect<XurlMentionsResponse, unknown> {
|
|
722
|
+
return Effect.gen(function* () {
|
|
723
|
+
const args = ["thread", tweetId, "--json"];
|
|
724
|
+
if (all) {
|
|
725
|
+
args.push("--all");
|
|
726
|
+
}
|
|
727
|
+
if (maxPages !== undefined) {
|
|
728
|
+
args.push("--max-pages", String(maxPages));
|
|
729
|
+
}
|
|
730
|
+
const stdout = yield* runBirdJsonCommandEffect(args, timeoutMs);
|
|
731
|
+
const payload = yield* parseBirdJsonEffect(stdout);
|
|
732
|
+
return yield* normalizeBirdTweetsPayloadEffect(payload, "thread");
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export function listThreadViaBird(options: {
|
|
737
|
+
tweetId: string;
|
|
738
|
+
all?: boolean;
|
|
739
|
+
maxPages?: number;
|
|
740
|
+
timeoutMs?: number;
|
|
532
741
|
}): Promise<XurlMentionsResponse> {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
742
|
+
return runEffectPromise(listThreadViaBirdEffect(options));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function normalizeBirdDmsPayloadEffect(payload: unknown) {
|
|
746
|
+
return Effect.try({
|
|
747
|
+
try: () => {
|
|
748
|
+
if (
|
|
749
|
+
!payload ||
|
|
750
|
+
typeof payload !== "object" ||
|
|
751
|
+
(payload as { success?: unknown }).success !== true ||
|
|
752
|
+
!Array.isArray(
|
|
753
|
+
(payload as { conversations?: unknown }).conversations,
|
|
754
|
+
) ||
|
|
755
|
+
!Array.isArray((payload as { events?: unknown }).events)
|
|
756
|
+
) {
|
|
757
|
+
throw new Error("bird dms returned unexpected JSON");
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return payload as BirdDmsResponse;
|
|
761
|
+
},
|
|
762
|
+
catch: (error) => error,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function parseBirdWhoami(stdout: string): BirdAuthenticatedAccount {
|
|
767
|
+
const usernameMatch = stdout.match(/@([A-Za-z0-9_]{1,15})\b/);
|
|
768
|
+
if (!usernameMatch?.[1]) {
|
|
769
|
+
throw new Error("bird whoami did not report an authenticated username");
|
|
539
770
|
}
|
|
540
|
-
const
|
|
541
|
-
|
|
771
|
+
const id = stdout
|
|
772
|
+
.split(/\r?\n/)
|
|
773
|
+
.map((line) => line.trim())
|
|
774
|
+
.map((line) => {
|
|
775
|
+
const labeled = line.match(/(?:🪪|user_?id:?)[^\d]*(\d{2,})/i);
|
|
776
|
+
if (labeled?.[1]) {
|
|
777
|
+
return labeled[1];
|
|
778
|
+
}
|
|
779
|
+
if (/[A-Za-z@]/.test(line)) {
|
|
780
|
+
return undefined;
|
|
781
|
+
}
|
|
782
|
+
return line.match(/^\D*(\d{2,})\D*$/)?.[1];
|
|
783
|
+
})
|
|
784
|
+
.find((value): value is string => Boolean(value));
|
|
785
|
+
return {
|
|
786
|
+
username: usernameMatch[1],
|
|
787
|
+
...(id ? { id } : {}),
|
|
788
|
+
};
|
|
789
|
+
}
|
|
542
790
|
|
|
543
|
-
|
|
791
|
+
export function getAuthenticatedBirdAccountEffect(): Effect.Effect<
|
|
792
|
+
BirdAuthenticatedAccount,
|
|
793
|
+
unknown
|
|
794
|
+
> {
|
|
795
|
+
return Effect.gen(function* () {
|
|
796
|
+
const stdout = yield* runBirdJsonCommandEffect(["whoami"]);
|
|
797
|
+
return yield* Effect.try({
|
|
798
|
+
try: () => parseBirdWhoami(stdout),
|
|
799
|
+
catch: (error) => error,
|
|
800
|
+
});
|
|
801
|
+
});
|
|
544
802
|
}
|
|
545
803
|
|
|
546
|
-
export
|
|
804
|
+
export function getAuthenticatedBirdAccount(): Promise<BirdAuthenticatedAccount> {
|
|
805
|
+
return runEffectPromise(getAuthenticatedBirdAccountEffect());
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export function listDirectMessagesViaBirdEffect({
|
|
547
809
|
maxResults,
|
|
810
|
+
inbox = "all",
|
|
811
|
+
maxPages,
|
|
812
|
+
allPages = false,
|
|
813
|
+
pageDelayMs,
|
|
548
814
|
}: {
|
|
549
815
|
maxResults: number;
|
|
816
|
+
inbox?: "all" | "accepted" | "requests";
|
|
817
|
+
maxPages?: number;
|
|
818
|
+
allPages?: boolean;
|
|
819
|
+
pageDelayMs?: number;
|
|
820
|
+
}): Effect.Effect<BirdDmsResponse, unknown> {
|
|
821
|
+
return Effect.gen(function* () {
|
|
822
|
+
const args = ["dms", "-n", String(maxResults), "--json"];
|
|
823
|
+
if (inbox !== "all") {
|
|
824
|
+
args.push("--inbox", inbox);
|
|
825
|
+
}
|
|
826
|
+
if (allPages) {
|
|
827
|
+
args.push("--all-pages");
|
|
828
|
+
} else if (typeof maxPages === "number") {
|
|
829
|
+
args.push("--max-pages", String(maxPages));
|
|
830
|
+
}
|
|
831
|
+
if (typeof pageDelayMs === "number" && pageDelayMs > 0) {
|
|
832
|
+
args.push("--page-delay-ms", String(pageDelayMs));
|
|
833
|
+
}
|
|
834
|
+
const stdout = yield* runBirdJsonCommandEffect(args);
|
|
835
|
+
const payload = yield* parseBirdJsonEffect(stdout);
|
|
836
|
+
return yield* normalizeBirdDmsPayloadEffect(payload);
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export function listDirectMessagesViaBird(options: {
|
|
841
|
+
maxResults: number;
|
|
842
|
+
inbox?: "all" | "accepted" | "requests";
|
|
843
|
+
maxPages?: number;
|
|
844
|
+
allPages?: boolean;
|
|
845
|
+
pageDelayMs?: number;
|
|
550
846
|
}): Promise<BirdDmsResponse> {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
"-n",
|
|
554
|
-
String(maxResults),
|
|
555
|
-
"--json",
|
|
556
|
-
]);
|
|
557
|
-
const payload = parseBirdJson(stdout);
|
|
558
|
-
if (
|
|
559
|
-
!payload ||
|
|
560
|
-
typeof payload !== "object" ||
|
|
561
|
-
(payload as { success?: unknown }).success !== true ||
|
|
562
|
-
!Array.isArray((payload as { conversations?: unknown }).conversations) ||
|
|
563
|
-
!Array.isArray((payload as { events?: unknown }).events)
|
|
564
|
-
) {
|
|
565
|
-
throw new Error("bird dms returned unexpected JSON");
|
|
566
|
-
}
|
|
847
|
+
return runEffectPromise(listDirectMessagesViaBirdEffect(options));
|
|
848
|
+
}
|
|
567
849
|
|
|
568
|
-
|
|
850
|
+
export function runDirectMessageRequestMutationViaBirdEffect({
|
|
851
|
+
action,
|
|
852
|
+
conversationId,
|
|
853
|
+
maxPages,
|
|
854
|
+
allPages = false,
|
|
855
|
+
}: {
|
|
856
|
+
action: BirdDmRequestAction;
|
|
857
|
+
conversationId: string;
|
|
858
|
+
maxPages?: number;
|
|
859
|
+
allPages?: boolean;
|
|
860
|
+
}): Effect.Effect<BirdDmMutationResponse, unknown> {
|
|
861
|
+
return Effect.gen(function* () {
|
|
862
|
+
const command =
|
|
863
|
+
action === "accept"
|
|
864
|
+
? "dm-accept"
|
|
865
|
+
: action === "reject"
|
|
866
|
+
? "dm-reject"
|
|
867
|
+
: "dm-block";
|
|
868
|
+
const args = [command, conversationId, "--json"];
|
|
869
|
+
if (action === "block") {
|
|
870
|
+
if (allPages) {
|
|
871
|
+
args.push("--all-pages");
|
|
872
|
+
} else if (typeof maxPages === "number") {
|
|
873
|
+
args.push("--max-pages", String(maxPages));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const stdout = yield* runBirdJsonCommandAllowFailureEffect(args);
|
|
877
|
+
const payload = yield* parseBirdJsonEffect(stdout);
|
|
878
|
+
if (
|
|
879
|
+
payload &&
|
|
880
|
+
typeof payload === "object" &&
|
|
881
|
+
typeof (payload as { success?: unknown }).success === "boolean"
|
|
882
|
+
) {
|
|
883
|
+
return payload as BirdDmMutationResponse;
|
|
884
|
+
}
|
|
885
|
+
throw new Error(`bird ${command} returned unexpected JSON`);
|
|
886
|
+
});
|
|
569
887
|
}
|
|
570
888
|
|
|
571
|
-
export
|
|
889
|
+
export function runDirectMessageRequestMutationViaBird(options: {
|
|
890
|
+
action: BirdDmRequestAction;
|
|
891
|
+
conversationId: string;
|
|
892
|
+
maxPages?: number;
|
|
893
|
+
allPages?: boolean;
|
|
894
|
+
}): Promise<BirdDmMutationResponse> {
|
|
895
|
+
return runEffectPromise(
|
|
896
|
+
runDirectMessageRequestMutationViaBirdEffect(options),
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
export function lookupProfileViaBirdEffect(
|
|
572
901
|
usernameOrId: string,
|
|
573
|
-
):
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
902
|
+
): Effect.Effect<XurlMentionUser | null, unknown> {
|
|
903
|
+
return Effect.gen(function* () {
|
|
904
|
+
const target = usernameOrId.trim().replace(/^@/, "");
|
|
905
|
+
if (!target) {
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
578
908
|
|
|
579
|
-
|
|
580
|
-
try {
|
|
581
|
-
stdout = await runBirdJsonCommand([
|
|
909
|
+
const stdout = yield* runBirdJsonCommandEffect([
|
|
582
910
|
"user",
|
|
583
911
|
target,
|
|
584
912
|
"--json",
|
|
585
913
|
"--profile-only",
|
|
586
|
-
])
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
914
|
+
]).pipe(
|
|
915
|
+
Effect.catchAll((error) => {
|
|
916
|
+
if (!isUnsupportedBirdOptionError(error, "--profile-only")) {
|
|
917
|
+
return Effect.fail(error);
|
|
918
|
+
}
|
|
919
|
+
return runBirdJsonCommandEffect([
|
|
920
|
+
"user",
|
|
921
|
+
target,
|
|
922
|
+
"--json",
|
|
923
|
+
"--count",
|
|
924
|
+
"1",
|
|
925
|
+
]);
|
|
926
|
+
}),
|
|
927
|
+
);
|
|
928
|
+
const payload = (yield* parseBirdJsonEffect(
|
|
929
|
+
stdout,
|
|
930
|
+
)) as BirdUserOverviewPayload;
|
|
931
|
+
return toXurlMentionUser(payload.user);
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
export function lookupProfileViaBird(
|
|
936
|
+
usernameOrId: string,
|
|
937
|
+
): Promise<XurlMentionUser | null> {
|
|
938
|
+
return runEffectPromise(lookupProfileViaBirdEffect(usernameOrId));
|
|
601
939
|
}
|
|
602
940
|
|
|
603
941
|
function toXurlMentionUser(
|
|
@@ -642,10 +980,11 @@ function toXurlMentionUser(
|
|
|
642
980
|
};
|
|
643
981
|
}
|
|
644
982
|
|
|
645
|
-
export
|
|
983
|
+
export function lookupProfilesViaBirdEffect(
|
|
646
984
|
usernameOrIds: string[],
|
|
647
|
-
):
|
|
648
|
-
Array<{ target: string; user: XurlMentionUser | null; error?: string }
|
|
985
|
+
): Effect.Effect<
|
|
986
|
+
Array<{ target: string; user: XurlMentionUser | null; error?: string }>,
|
|
987
|
+
unknown
|
|
649
988
|
> {
|
|
650
989
|
const targets = Array.from(
|
|
651
990
|
new Set(
|
|
@@ -655,56 +994,72 @@ export async function lookupProfilesViaBird(
|
|
|
655
994
|
),
|
|
656
995
|
);
|
|
657
996
|
if (targets.length === 0) {
|
|
658
|
-
return [];
|
|
997
|
+
return Effect.succeed([]);
|
|
659
998
|
}
|
|
660
999
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
String(item.target ?? "")
|
|
675
|
-
.replace(/^@/, "")
|
|
676
|
-
.toLowerCase(),
|
|
677
|
-
item.error ?? "Unknown error",
|
|
678
|
-
]),
|
|
679
|
-
);
|
|
680
|
-
return targets.map((target) => ({
|
|
681
|
-
target,
|
|
682
|
-
user: byTarget.get(target.toLowerCase()) ?? null,
|
|
683
|
-
...(errors.has(target.toLowerCase())
|
|
684
|
-
? { error: errors.get(target.toLowerCase()) }
|
|
685
|
-
: {}),
|
|
686
|
-
}));
|
|
687
|
-
} catch (error) {
|
|
688
|
-
if (!isUnsupportedBirdOptionError(error, "profiles")) {
|
|
689
|
-
throw error;
|
|
690
|
-
}
|
|
691
|
-
return Promise.all(
|
|
692
|
-
targets.map(async (target) => {
|
|
693
|
-
try {
|
|
694
|
-
return { target, user: await lookupProfileViaBird(target) };
|
|
695
|
-
} catch (lookupError) {
|
|
696
|
-
return {
|
|
697
|
-
target,
|
|
698
|
-
user: null,
|
|
699
|
-
error:
|
|
700
|
-
lookupError instanceof Error
|
|
701
|
-
? lookupError.message
|
|
702
|
-
: String(lookupError),
|
|
703
|
-
};
|
|
1000
|
+
return runBirdJsonCommandEffect(["profiles", ...targets, "--json"]).pipe(
|
|
1001
|
+
Effect.flatMap((stdout) =>
|
|
1002
|
+
Effect.gen(function* () {
|
|
1003
|
+
const payload = (yield* parseBirdJsonEffect(
|
|
1004
|
+
stdout,
|
|
1005
|
+
)) as BirdProfilesPayload;
|
|
1006
|
+
const users = (payload.users ?? [])
|
|
1007
|
+
.map(toXurlMentionUser)
|
|
1008
|
+
.filter((user): user is XurlMentionUser => Boolean(user));
|
|
1009
|
+
const byTarget = new Map<string, XurlMentionUser>();
|
|
1010
|
+
for (const user of users) {
|
|
1011
|
+
byTarget.set(String(user.id), user);
|
|
1012
|
+
byTarget.set(user.username.toLowerCase(), user);
|
|
704
1013
|
}
|
|
1014
|
+
const errors = new Map(
|
|
1015
|
+
(payload.errors ?? []).map((item) => [
|
|
1016
|
+
String(item.target ?? "")
|
|
1017
|
+
.replace(/^@/, "")
|
|
1018
|
+
.toLowerCase(),
|
|
1019
|
+
item.error ?? "Unknown error",
|
|
1020
|
+
]),
|
|
1021
|
+
);
|
|
1022
|
+
return targets.map((target) => ({
|
|
1023
|
+
target,
|
|
1024
|
+
user: byTarget.get(target.toLowerCase()) ?? null,
|
|
1025
|
+
...(errors.has(target.toLowerCase())
|
|
1026
|
+
? { error: errors.get(target.toLowerCase()) }
|
|
1027
|
+
: {}),
|
|
1028
|
+
}));
|
|
705
1029
|
}),
|
|
706
|
-
)
|
|
707
|
-
|
|
1030
|
+
),
|
|
1031
|
+
Effect.catchAll((error) => {
|
|
1032
|
+
if (!isUnsupportedBirdOptionError(error, "profiles")) {
|
|
1033
|
+
return Effect.fail(error);
|
|
1034
|
+
}
|
|
1035
|
+
return Effect.forEach(
|
|
1036
|
+
targets,
|
|
1037
|
+
(target) =>
|
|
1038
|
+
lookupProfileViaBirdEffect(target).pipe(
|
|
1039
|
+
Effect.map((user) => ({ target, user })),
|
|
1040
|
+
Effect.catchAll((lookupError) =>
|
|
1041
|
+
Effect.succeed({
|
|
1042
|
+
target,
|
|
1043
|
+
user: null,
|
|
1044
|
+
error:
|
|
1045
|
+
lookupError instanceof Error
|
|
1046
|
+
? lookupError.message
|
|
1047
|
+
: String(lookupError),
|
|
1048
|
+
}),
|
|
1049
|
+
),
|
|
1050
|
+
),
|
|
1051
|
+
{ concurrency: "unbounded" },
|
|
1052
|
+
);
|
|
1053
|
+
}),
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
export function lookupProfilesViaBird(
|
|
1058
|
+
usernameOrIds: string[],
|
|
1059
|
+
): Promise<
|
|
1060
|
+
Array<{ target: string; user: XurlMentionUser | null; error?: string }>
|
|
1061
|
+
> {
|
|
1062
|
+
return runEffectPromise(lookupProfilesViaBirdEffect(usernameOrIds));
|
|
708
1063
|
}
|
|
709
1064
|
|
|
710
1065
|
export const __test__ = {
|