birdclaw 0.4.0 → 0.5.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 +39 -1
- package/README.md +108 -7
- package/package.json +24 -23
- package/playwright.config.ts +1 -0
- package/public/favicon.ico +0 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +2 -2
- package/src/cli.ts +563 -18
- package/src/components/AppNav.tsx +66 -29
- package/src/components/AvatarChip.tsx +10 -5
- package/src/components/ConversationThread.tsx +125 -0
- package/src/components/DmWorkspace.tsx +96 -98
- package/src/components/EmbeddedTweetCard.tsx +20 -14
- package/src/components/InboxCard.tsx +92 -90
- package/src/components/LinkPreviewCard.tsx +270 -0
- package/src/components/ProfilePreview.tsx +8 -3
- package/src/components/SavedTimelineView.tsx +48 -38
- package/src/components/TimelineCard.tsx +228 -84
- package/src/components/TweetRichText.tsx +6 -2
- package/src/lib/archive-import.ts +1567 -65
- package/src/lib/authored-live.ts +1074 -0
- package/src/lib/backup.ts +318 -14
- package/src/lib/bird-actions.ts +1 -10
- package/src/lib/bird-command.ts +57 -0
- package/src/lib/bird.ts +89 -5
- package/src/lib/config.ts +1 -1
- package/src/lib/db.ts +282 -4
- package/src/lib/follow-graph.ts +1053 -0
- package/src/lib/link-index.ts +629 -0
- package/src/lib/link-insights.ts +834 -0
- package/src/lib/link-preview-metadata.ts +334 -0
- package/src/lib/media-fetch.ts +787 -0
- package/src/lib/media-includes.ts +165 -0
- package/src/lib/mention-threads-live.ts +535 -43
- package/src/lib/mentions-live.ts +623 -19
- package/src/lib/moderation-target.ts +6 -0
- package/src/lib/profile-hydration.ts +1 -1
- package/src/lib/profile-resolver.ts +115 -1
- package/src/lib/queries.ts +233 -7
- package/src/lib/seed.ts +127 -8
- package/src/lib/timeline-collections-live.ts +145 -22
- package/src/lib/timeline-live.ts +10 -15
- package/src/lib/tweet-account-edges.ts +9 -2
- package/src/lib/tweet-render.ts +97 -0
- package/src/lib/types.ts +219 -2
- package/src/lib/ui.ts +375 -147
- package/src/lib/url-expansion-store.ts +110 -0
- package/src/lib/url-expansion.ts +33 -8
- package/src/lib/x-profile.ts +7 -2
- package/src/lib/xurl.ts +296 -12
- package/src/routeTree.gen.ts +105 -0
- package/src/routes/__root.tsx +7 -3
- package/src/routes/api/conversation.tsx +34 -0
- package/src/routes/api/link-insights.tsx +79 -0
- package/src/routes/api/link-preview.tsx +43 -0
- package/src/routes/api/profile-hydrate.tsx +51 -0
- package/src/routes/blocks.tsx +111 -86
- package/src/routes/dms.tsx +90 -78
- package/src/routes/inbox.tsx +98 -86
- package/src/routes/index.tsx +63 -50
- package/src/routes/links.tsx +883 -0
- package/src/routes/mentions.tsx +63 -50
- package/src/styles.css +106 -43
package/src/lib/xurl.ts
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import type {
|
|
4
|
+
FollowDirection,
|
|
4
5
|
TransportStatus,
|
|
6
|
+
XurlFollowUsersResponse,
|
|
5
7
|
XurlMentionsResponse,
|
|
6
8
|
XurlMentionUser,
|
|
7
9
|
XurlTweetsResponse,
|
|
8
10
|
XurlUserTweet,
|
|
11
|
+
XurlUserTweetsResponse,
|
|
9
12
|
} from "./types";
|
|
10
13
|
|
|
11
14
|
const execFileAsync = promisify(execFile);
|
|
12
15
|
const TRANSPORT_STATUS_TTL_MS = 5 * 60_000;
|
|
13
16
|
const AUTHENTICATED_USER_TTL_MS = 60_000;
|
|
14
17
|
const JSON_RETRY_LIMIT = 6;
|
|
18
|
+
const MEDIA_EXPANSION = "attachments.media_keys";
|
|
19
|
+
const AUTHOR_MEDIA_EXPANSIONS = `author_id,${MEDIA_EXPANSION}`;
|
|
20
|
+
const MEDIA_FIELDS =
|
|
21
|
+
"variants,preview_image_url,url,duration_ms,alt_text,type,width,height,public_metrics";
|
|
22
|
+
const RICH_USER_FIELDS =
|
|
23
|
+
"description,entities,location,public_metrics,profile_image_url,url,created_at,verified,verified_type";
|
|
24
|
+
const THREAD_TWEET_FIELDS =
|
|
25
|
+
"created_at,conversation_id,entities,public_metrics,referenced_tweets,in_reply_to_user_id,attachments";
|
|
26
|
+
// X bookmarks pagination truncates above 90 until this bug is fixed:
|
|
27
|
+
// https://devcommunity.x.com/t/bookmarks-api-v2-stops-paginating-after-3-pages-no-next-token-returned/257339
|
|
28
|
+
const BOOKMARKS_MAX_RESULTS_CAP = 90;
|
|
29
|
+
|
|
30
|
+
type TimelineCollectionEndpoint = "liked_tweets" | "bookmarks";
|
|
31
|
+
type JsonCommandOptions = {
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
deadlineMs?: number;
|
|
34
|
+
};
|
|
15
35
|
|
|
16
36
|
let transportStatusCache:
|
|
17
37
|
| {
|
|
@@ -66,6 +86,10 @@ function formatExecError(error: unknown, fallback: string) {
|
|
|
66
86
|
return parts.join("\n");
|
|
67
87
|
}
|
|
68
88
|
|
|
89
|
+
function formatXurlCommandError(error: unknown, args: string[]) {
|
|
90
|
+
return new Error(formatExecError(error, `xurl ${args.join(" ")} failed`));
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
function parseErrorPayload(error: unknown) {
|
|
70
94
|
const stdout =
|
|
71
95
|
typeof error === "object" &&
|
|
@@ -99,6 +123,16 @@ function getRetryDelayMs(error: unknown, attempt: number) {
|
|
|
99
123
|
return Math.min(baseDelay * 2 ** attempt, 30_000);
|
|
100
124
|
}
|
|
101
125
|
|
|
126
|
+
function capTimelineCollectionMaxResults(
|
|
127
|
+
collection: TimelineCollectionEndpoint,
|
|
128
|
+
maxResults: number,
|
|
129
|
+
isPaginatedWalk: boolean,
|
|
130
|
+
) {
|
|
131
|
+
return collection === "bookmarks" && isPaginatedWalk
|
|
132
|
+
? Math.min(maxResults, BOOKMARKS_MAX_RESULTS_CAP)
|
|
133
|
+
: maxResults;
|
|
134
|
+
}
|
|
135
|
+
|
|
102
136
|
async function sleep(ms: number) {
|
|
103
137
|
if (ms <= 0) {
|
|
104
138
|
return;
|
|
@@ -123,6 +157,12 @@ async function hasXurl(): Promise<boolean> {
|
|
|
123
157
|
}
|
|
124
158
|
}
|
|
125
159
|
|
|
160
|
+
function isUnauthenticatedXurlStatus(status: string) {
|
|
161
|
+
return /no apps registered|no authenticated user|not authenticated|not logged in/i.test(
|
|
162
|
+
status,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
126
166
|
export async function getTransportStatus(): Promise<TransportStatus> {
|
|
127
167
|
const now = Date.now();
|
|
128
168
|
if (transportStatusCache?.value && transportStatusCache.expiresAt > now) {
|
|
@@ -145,11 +185,23 @@ export async function getTransportStatus(): Promise<TransportStatus> {
|
|
|
145
185
|
|
|
146
186
|
try {
|
|
147
187
|
const { stdout } = await execFileAsync("xurl", ["auth", "status"]);
|
|
188
|
+
const rawStatus = stdout.trim();
|
|
189
|
+
|
|
190
|
+
if (isUnauthenticatedXurlStatus(rawStatus)) {
|
|
191
|
+
return {
|
|
192
|
+
installed: true,
|
|
193
|
+
availableTransport: "local",
|
|
194
|
+
statusText:
|
|
195
|
+
"xurl installed but not authenticated. local (bird) mode active.",
|
|
196
|
+
rawStatus,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
148
200
|
return {
|
|
149
201
|
installed: true,
|
|
150
202
|
availableTransport: "xurl",
|
|
151
203
|
statusText: "xurl available",
|
|
152
|
-
rawStatus
|
|
204
|
+
rawStatus,
|
|
153
205
|
};
|
|
154
206
|
} catch (error) {
|
|
155
207
|
return {
|
|
@@ -198,18 +250,52 @@ async function runShortcut(
|
|
|
198
250
|
}
|
|
199
251
|
}
|
|
200
252
|
|
|
201
|
-
async function runJsonCommand(
|
|
253
|
+
async function runJsonCommand(
|
|
254
|
+
args: string[],
|
|
255
|
+
options: JsonCommandOptions = {},
|
|
256
|
+
attempt = 0,
|
|
257
|
+
) {
|
|
258
|
+
const deadlineMs =
|
|
259
|
+
options.deadlineMs ??
|
|
260
|
+
(typeof options.timeoutMs === "number" &&
|
|
261
|
+
Number.isFinite(options.timeoutMs) &&
|
|
262
|
+
options.timeoutMs > 0
|
|
263
|
+
? Date.now() + options.timeoutMs
|
|
264
|
+
: undefined);
|
|
265
|
+
const timeoutMs = deadlineMs
|
|
266
|
+
? Math.max(0, deadlineMs - Date.now())
|
|
267
|
+
: undefined;
|
|
268
|
+
const controller =
|
|
269
|
+
typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
270
|
+
? new AbortController()
|
|
271
|
+
: undefined;
|
|
272
|
+
const timeout = controller
|
|
273
|
+
? setTimeout(() => controller.abort(), timeoutMs)
|
|
274
|
+
: undefined;
|
|
275
|
+
|
|
202
276
|
try {
|
|
203
|
-
const { stdout } =
|
|
277
|
+
const { stdout } = controller
|
|
278
|
+
? await execFileAsync("xurl", args, { signal: controller.signal })
|
|
279
|
+
: await execFileAsync("xurl", args);
|
|
204
280
|
return JSON.parse(stdout) as Record<string, unknown>;
|
|
205
281
|
} catch (error) {
|
|
206
282
|
const retryDelayMs = getRetryDelayMs(error, attempt);
|
|
207
283
|
if (retryDelayMs === null || attempt >= JSON_RETRY_LIMIT - 1) {
|
|
208
|
-
throw error;
|
|
284
|
+
throw formatXurlCommandError(error, args);
|
|
285
|
+
}
|
|
286
|
+
const remainingMs = deadlineMs
|
|
287
|
+
? Math.max(0, deadlineMs - Date.now())
|
|
288
|
+
: undefined;
|
|
289
|
+
if (remainingMs !== undefined && retryDelayMs >= remainingMs) {
|
|
290
|
+
throw formatXurlCommandError(error, args);
|
|
209
291
|
}
|
|
210
292
|
|
|
211
293
|
await sleep(retryDelayMs);
|
|
212
|
-
return runJsonCommand(args, attempt + 1);
|
|
294
|
+
return runJsonCommand(args, { ...options, deadlineMs }, attempt + 1);
|
|
295
|
+
} finally {
|
|
296
|
+
if (timeout) {
|
|
297
|
+
clearTimeout(timeout);
|
|
298
|
+
}
|
|
213
299
|
}
|
|
214
300
|
}
|
|
215
301
|
|
|
@@ -307,11 +393,15 @@ export async function listMentionsViaXurl({
|
|
|
307
393
|
username,
|
|
308
394
|
userId,
|
|
309
395
|
paginationToken,
|
|
396
|
+
sinceId,
|
|
397
|
+
startTime,
|
|
310
398
|
}: {
|
|
311
399
|
maxResults: number;
|
|
312
400
|
username?: string;
|
|
313
401
|
userId?: string;
|
|
314
402
|
paginationToken?: string;
|
|
403
|
+
sinceId?: string;
|
|
404
|
+
startTime?: string;
|
|
315
405
|
}): Promise<XurlMentionsResponse> {
|
|
316
406
|
let resolvedUserId = userId;
|
|
317
407
|
if (!resolvedUserId) {
|
|
@@ -332,14 +422,21 @@ export async function listMentionsViaXurl({
|
|
|
332
422
|
|
|
333
423
|
const query = new URLSearchParams({
|
|
334
424
|
max_results: String(maxResults),
|
|
335
|
-
expansions:
|
|
425
|
+
expansions: AUTHOR_MEDIA_EXPANSIONS,
|
|
336
426
|
"tweet.fields": "created_at,conversation_id,entities,public_metrics",
|
|
427
|
+
"media.fields": MEDIA_FIELDS,
|
|
337
428
|
"user.fields":
|
|
338
429
|
"description,entities,location,public_metrics,profile_image_url,url,created_at,verified,verified_type",
|
|
339
430
|
});
|
|
340
431
|
if (paginationToken) {
|
|
341
432
|
query.set("pagination_token", paginationToken);
|
|
342
433
|
}
|
|
434
|
+
if (sinceId) {
|
|
435
|
+
query.set("since_id", sinceId);
|
|
436
|
+
}
|
|
437
|
+
if (startTime) {
|
|
438
|
+
query.set("start_time", startTime);
|
|
439
|
+
}
|
|
343
440
|
|
|
344
441
|
const payload = await runJsonCommand([
|
|
345
442
|
`/2/users/${resolvedUserId}/mentions?${query.toString()}`,
|
|
@@ -364,12 +461,14 @@ async function listTimelineCollectionViaXurl({
|
|
|
364
461
|
maxResults,
|
|
365
462
|
username,
|
|
366
463
|
userId,
|
|
464
|
+
isPaginatedWalk = false,
|
|
367
465
|
paginationToken,
|
|
368
466
|
}: {
|
|
369
|
-
collection:
|
|
467
|
+
collection: TimelineCollectionEndpoint;
|
|
370
468
|
maxResults: number;
|
|
371
469
|
username?: string;
|
|
372
470
|
userId?: string;
|
|
471
|
+
isPaginatedWalk?: boolean;
|
|
373
472
|
paginationToken?: string;
|
|
374
473
|
}): Promise<XurlMentionsResponse> {
|
|
375
474
|
let resolvedUserId = userId;
|
|
@@ -389,11 +488,17 @@ async function listTimelineCollectionViaXurl({
|
|
|
389
488
|
}
|
|
390
489
|
}
|
|
391
490
|
|
|
491
|
+
const requestMaxResults = capTimelineCollectionMaxResults(
|
|
492
|
+
collection,
|
|
493
|
+
maxResults,
|
|
494
|
+
isPaginatedWalk,
|
|
495
|
+
);
|
|
392
496
|
const query = new URLSearchParams({
|
|
393
|
-
max_results: String(
|
|
394
|
-
expansions:
|
|
497
|
+
max_results: String(requestMaxResults),
|
|
498
|
+
expansions: AUTHOR_MEDIA_EXPANSIONS,
|
|
395
499
|
"tweet.fields":
|
|
396
500
|
"created_at,conversation_id,entities,public_metrics,referenced_tweets",
|
|
501
|
+
"media.fields": MEDIA_FIELDS,
|
|
397
502
|
"user.fields":
|
|
398
503
|
"description,entities,location,public_metrics,profile_image_url,url,created_at,verified,verified_type",
|
|
399
504
|
});
|
|
@@ -437,6 +542,7 @@ export async function listBookmarkedTweetsViaXurl(options: {
|
|
|
437
542
|
maxResults: number;
|
|
438
543
|
username?: string;
|
|
439
544
|
userId?: string;
|
|
545
|
+
isPaginatedWalk?: boolean;
|
|
440
546
|
paginationToken?: string;
|
|
441
547
|
}): Promise<XurlMentionsResponse> {
|
|
442
548
|
return listTimelineCollectionViaXurl({
|
|
@@ -445,6 +551,61 @@ export async function listBookmarkedTweetsViaXurl(options: {
|
|
|
445
551
|
});
|
|
446
552
|
}
|
|
447
553
|
|
|
554
|
+
export async function listFollowUsersViaXurl({
|
|
555
|
+
direction,
|
|
556
|
+
maxResults,
|
|
557
|
+
username,
|
|
558
|
+
userId,
|
|
559
|
+
paginationToken,
|
|
560
|
+
}: {
|
|
561
|
+
direction: FollowDirection;
|
|
562
|
+
maxResults: number;
|
|
563
|
+
username?: string;
|
|
564
|
+
userId?: string;
|
|
565
|
+
paginationToken?: string;
|
|
566
|
+
}): Promise<XurlFollowUsersResponse> {
|
|
567
|
+
let resolvedUserId = userId;
|
|
568
|
+
if (!resolvedUserId) {
|
|
569
|
+
if (username) {
|
|
570
|
+
const [user] = await lookupUsersByHandles([username]);
|
|
571
|
+
if (!user?.id) {
|
|
572
|
+
throw new Error(`Could not resolve Twitter user id for @${username}`);
|
|
573
|
+
}
|
|
574
|
+
resolvedUserId = String(user.id);
|
|
575
|
+
} else {
|
|
576
|
+
const user = await lookupAuthenticatedUser();
|
|
577
|
+
if (!user?.id) {
|
|
578
|
+
throw new Error("Could not resolve authenticated Twitter user id");
|
|
579
|
+
}
|
|
580
|
+
resolvedUserId = String(user.id);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const query = new URLSearchParams({
|
|
585
|
+
max_results: String(maxResults),
|
|
586
|
+
"user.fields":
|
|
587
|
+
"id,username,name,description,verified,protected,public_metrics,profile_image_url,created_at",
|
|
588
|
+
});
|
|
589
|
+
if (paginationToken) {
|
|
590
|
+
query.set("pagination_token", paginationToken);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const payload = await runJsonCommand([
|
|
594
|
+
"--auth",
|
|
595
|
+
"oauth2",
|
|
596
|
+
`/2/users/${resolvedUserId}/${direction}?${query.toString()}`,
|
|
597
|
+
]);
|
|
598
|
+
return {
|
|
599
|
+
data: Array.isArray(payload.data)
|
|
600
|
+
? (payload.data as XurlMentionUser[])
|
|
601
|
+
: [],
|
|
602
|
+
meta:
|
|
603
|
+
payload.meta && typeof payload.meta === "object"
|
|
604
|
+
? (payload.meta as Record<string, unknown>)
|
|
605
|
+
: undefined,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
448
609
|
export async function listBlockedUsers(
|
|
449
610
|
userId: string,
|
|
450
611
|
paginationToken?: string,
|
|
@@ -482,17 +643,49 @@ export async function listUserTweets(
|
|
|
482
643
|
maxResults,
|
|
483
644
|
paginationToken,
|
|
484
645
|
excludeRetweets = true,
|
|
646
|
+
sinceId,
|
|
647
|
+
untilId,
|
|
648
|
+
tweetFields,
|
|
649
|
+
expansions,
|
|
650
|
+
userFields,
|
|
651
|
+
mediaFields,
|
|
652
|
+
auth,
|
|
485
653
|
}: {
|
|
486
654
|
maxResults: number;
|
|
487
655
|
paginationToken?: string;
|
|
488
656
|
excludeRetweets?: boolean;
|
|
657
|
+
sinceId?: string;
|
|
658
|
+
untilId?: string;
|
|
659
|
+
tweetFields?: string[];
|
|
660
|
+
expansions?: string[];
|
|
661
|
+
userFields?: string[];
|
|
662
|
+
mediaFields?: string[];
|
|
663
|
+
auth?: "oauth2";
|
|
489
664
|
},
|
|
490
|
-
) {
|
|
665
|
+
): Promise<XurlUserTweetsResponse> {
|
|
491
666
|
const query = new URLSearchParams({
|
|
492
667
|
max_results: String(maxResults),
|
|
668
|
+
expansions: MEDIA_EXPANSION,
|
|
493
669
|
"tweet.fields":
|
|
670
|
+
tweetFields?.join(",") ??
|
|
494
671
|
"created_at,conversation_id,public_metrics,referenced_tweets",
|
|
672
|
+
"media.fields": MEDIA_FIELDS,
|
|
495
673
|
});
|
|
674
|
+
if (expansions && expansions.length > 0) {
|
|
675
|
+
query.set("expansions", expansions.join(","));
|
|
676
|
+
}
|
|
677
|
+
if (userFields && userFields.length > 0) {
|
|
678
|
+
query.set("user.fields", userFields.join(","));
|
|
679
|
+
}
|
|
680
|
+
if (mediaFields && mediaFields.length > 0) {
|
|
681
|
+
query.set("media.fields", mediaFields.join(","));
|
|
682
|
+
}
|
|
683
|
+
if (sinceId) {
|
|
684
|
+
query.set("since_id", sinceId);
|
|
685
|
+
}
|
|
686
|
+
if (untilId) {
|
|
687
|
+
query.set("until_id", untilId);
|
|
688
|
+
}
|
|
496
689
|
if (excludeRetweets) {
|
|
497
690
|
query.set("exclude", "retweets");
|
|
498
691
|
}
|
|
@@ -500,7 +693,10 @@ export async function listUserTweets(
|
|
|
500
693
|
query.set("pagination_token", paginationToken);
|
|
501
694
|
}
|
|
502
695
|
|
|
503
|
-
const
|
|
696
|
+
const endpoint = `/2/users/${userId}/tweets?${query}`;
|
|
697
|
+
const payload = await runJsonCommand(
|
|
698
|
+
auth === "oauth2" ? ["--auth", "oauth2", endpoint] : [endpoint],
|
|
699
|
+
);
|
|
504
700
|
const data = Array.isArray(payload.data)
|
|
505
701
|
? (payload.data as XurlUserTweet[])
|
|
506
702
|
: [];
|
|
@@ -508,11 +704,16 @@ export async function listUserTweets(
|
|
|
508
704
|
payload.meta && typeof payload.meta === "object"
|
|
509
705
|
? (payload.meta as Record<string, unknown>)
|
|
510
706
|
: null;
|
|
707
|
+
const includes =
|
|
708
|
+
payload.includes && typeof payload.includes === "object"
|
|
709
|
+
? (payload.includes as XurlUserTweetsResponse["includes"])
|
|
710
|
+
: undefined;
|
|
511
711
|
|
|
512
712
|
return {
|
|
513
713
|
items: data,
|
|
514
714
|
nextToken:
|
|
515
715
|
typeof meta?.next_token === "string" ? String(meta.next_token) : null,
|
|
716
|
+
...(includes ? { includes } : {}),
|
|
516
717
|
};
|
|
517
718
|
}
|
|
518
719
|
|
|
@@ -525,9 +726,10 @@ export async function lookupTweetsByIds(
|
|
|
525
726
|
|
|
526
727
|
const query = new URLSearchParams({
|
|
527
728
|
ids: ids.join(","),
|
|
528
|
-
expansions:
|
|
729
|
+
expansions: AUTHOR_MEDIA_EXPANSIONS,
|
|
529
730
|
"tweet.fields":
|
|
530
731
|
"created_at,conversation_id,entities,public_metrics,referenced_tweets",
|
|
732
|
+
"media.fields": MEDIA_FIELDS,
|
|
531
733
|
"user.fields":
|
|
532
734
|
"description,entities,location,public_metrics,profile_image_url,url,created_at,verified,verified_type",
|
|
533
735
|
});
|
|
@@ -548,6 +750,88 @@ export async function lookupTweetsByIds(
|
|
|
548
750
|
};
|
|
549
751
|
}
|
|
550
752
|
|
|
753
|
+
export async function searchRecentByConversationId(
|
|
754
|
+
conversationId: string,
|
|
755
|
+
{
|
|
756
|
+
maxResults,
|
|
757
|
+
paginationToken,
|
|
758
|
+
timeoutMs,
|
|
759
|
+
}: {
|
|
760
|
+
maxResults: number;
|
|
761
|
+
paginationToken?: string;
|
|
762
|
+
timeoutMs?: number;
|
|
763
|
+
},
|
|
764
|
+
): Promise<XurlTweetsResponse> {
|
|
765
|
+
const query = new URLSearchParams({
|
|
766
|
+
query: `conversation_id:${conversationId}`,
|
|
767
|
+
max_results: String(maxResults),
|
|
768
|
+
expansions: AUTHOR_MEDIA_EXPANSIONS,
|
|
769
|
+
"tweet.fields": THREAD_TWEET_FIELDS,
|
|
770
|
+
"media.fields": MEDIA_FIELDS,
|
|
771
|
+
"user.fields": RICH_USER_FIELDS,
|
|
772
|
+
});
|
|
773
|
+
if (paginationToken) {
|
|
774
|
+
query.set("pagination_token", paginationToken);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const payload = await runJsonCommand(
|
|
778
|
+
[`/2/tweets/search/recent?${query.toString()}`],
|
|
779
|
+
{ timeoutMs },
|
|
780
|
+
);
|
|
781
|
+
return {
|
|
782
|
+
data: Array.isArray(payload.data)
|
|
783
|
+
? (payload.data as XurlTweetsResponse["data"])
|
|
784
|
+
: [],
|
|
785
|
+
includes:
|
|
786
|
+
payload.includes && typeof payload.includes === "object"
|
|
787
|
+
? (payload.includes as XurlTweetsResponse["includes"])
|
|
788
|
+
: undefined,
|
|
789
|
+
meta:
|
|
790
|
+
payload.meta && typeof payload.meta === "object"
|
|
791
|
+
? (payload.meta as XurlTweetsResponse["meta"])
|
|
792
|
+
: undefined,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export async function getTweetById(
|
|
797
|
+
id: string,
|
|
798
|
+
{ timeoutMs }: { timeoutMs?: number } = {},
|
|
799
|
+
): Promise<XurlTweetsResponse> {
|
|
800
|
+
const query = new URLSearchParams({
|
|
801
|
+
expansions: AUTHOR_MEDIA_EXPANSIONS,
|
|
802
|
+
"tweet.fields": THREAD_TWEET_FIELDS,
|
|
803
|
+
"media.fields": MEDIA_FIELDS,
|
|
804
|
+
"user.fields": RICH_USER_FIELDS,
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
const payload = await runJsonCommand(
|
|
808
|
+
[`/2/tweets/${id}?${query.toString()}`],
|
|
809
|
+
{
|
|
810
|
+
timeoutMs,
|
|
811
|
+
},
|
|
812
|
+
);
|
|
813
|
+
const data =
|
|
814
|
+
payload.data &&
|
|
815
|
+
typeof payload.data === "object" &&
|
|
816
|
+
!Array.isArray(payload.data)
|
|
817
|
+
? [payload.data as XurlTweetsResponse["data"][number]]
|
|
818
|
+
: Array.isArray(payload.data)
|
|
819
|
+
? (payload.data as XurlTweetsResponse["data"])
|
|
820
|
+
: [];
|
|
821
|
+
|
|
822
|
+
return {
|
|
823
|
+
data,
|
|
824
|
+
includes:
|
|
825
|
+
payload.includes && typeof payload.includes === "object"
|
|
826
|
+
? (payload.includes as XurlTweetsResponse["includes"])
|
|
827
|
+
: undefined,
|
|
828
|
+
meta:
|
|
829
|
+
payload.meta && typeof payload.meta === "object"
|
|
830
|
+
? (payload.meta as XurlTweetsResponse["meta"])
|
|
831
|
+
: undefined,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
551
835
|
export async function postViaXurl(text: string) {
|
|
552
836
|
return runShortcut(["post", text]);
|
|
553
837
|
}
|