@the-convocation/twitter-scraper 0.16.0 → 0.16.2
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/dist/default/cjs/index.js +232 -136
- package/dist/default/cjs/index.js.map +1 -1
- package/dist/default/esm/index.mjs +232 -136
- package/dist/default/esm/index.mjs.map +1 -1
- package/dist/node/cjs/index.cjs +232 -136
- package/dist/node/cjs/index.cjs.map +1 -1
- package/dist/node/esm/index.mjs +232 -136
- package/dist/node/esm/index.mjs.map +1 -1
- package/dist/types/index.d.ts +10 -3
- package/package.json +3 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
1
2
|
import { Cookie, CookieJar } from 'tough-cookie';
|
|
2
3
|
import setCookie from 'set-cookie-parser';
|
|
3
4
|
import { Headers } from 'headers-polyfill';
|
|
@@ -8,24 +9,39 @@ import * as OTPAuth from 'otpauth';
|
|
|
8
9
|
import stringify from 'json-stable-stringify';
|
|
9
10
|
|
|
10
11
|
class ApiError extends Error {
|
|
11
|
-
constructor(response, data
|
|
12
|
-
super(
|
|
12
|
+
constructor(response, data) {
|
|
13
|
+
super(
|
|
14
|
+
`Response status: ${response.status} | headers: ${JSON.stringify(
|
|
15
|
+
headersToString(response.headers)
|
|
16
|
+
)} | data: ${typeof data === "string" ? data : JSON.stringify(data)}`
|
|
17
|
+
);
|
|
13
18
|
this.response = response;
|
|
14
19
|
this.data = data;
|
|
15
20
|
}
|
|
16
21
|
static async fromResponse(response) {
|
|
17
22
|
let data = void 0;
|
|
18
23
|
try {
|
|
19
|
-
|
|
24
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
25
|
+
data = await response.json();
|
|
26
|
+
} else {
|
|
27
|
+
data = await response.text();
|
|
28
|
+
}
|
|
20
29
|
} catch {
|
|
21
30
|
try {
|
|
22
31
|
data = await response.text();
|
|
23
32
|
} catch {
|
|
24
33
|
}
|
|
25
34
|
}
|
|
26
|
-
return new ApiError(response, data
|
|
35
|
+
return new ApiError(response, data);
|
|
27
36
|
}
|
|
28
37
|
}
|
|
38
|
+
function headersToString(headers) {
|
|
39
|
+
const result = [];
|
|
40
|
+
headers.forEach((value, key) => {
|
|
41
|
+
result.push(`${key}: ${value}`);
|
|
42
|
+
});
|
|
43
|
+
return result.join("\n");
|
|
44
|
+
}
|
|
29
45
|
class AuthenticationError extends Error {
|
|
30
46
|
constructor(message) {
|
|
31
47
|
super(message || "Authentication failed");
|
|
@@ -33,10 +49,15 @@ class AuthenticationError extends Error {
|
|
|
33
49
|
}
|
|
34
50
|
}
|
|
35
51
|
|
|
52
|
+
const log$2 = debug("twitter-scraper:rate-limit");
|
|
36
53
|
class WaitingRateLimitStrategy {
|
|
37
54
|
async onRateLimit({ response: res }) {
|
|
55
|
+
const xRateLimitLimit = res.headers.get("x-rate-limit-limit");
|
|
38
56
|
const xRateLimitRemaining = res.headers.get("x-rate-limit-remaining");
|
|
39
57
|
const xRateLimitReset = res.headers.get("x-rate-limit-reset");
|
|
58
|
+
log$2(
|
|
59
|
+
`Rate limit event: limit=${xRateLimitLimit}, remaining=${xRateLimitRemaining}, reset=${xRateLimitReset}`
|
|
60
|
+
);
|
|
40
61
|
if (xRateLimitRemaining == "0" && xRateLimitReset) {
|
|
41
62
|
const currentTime = (/* @__PURE__ */ new Date()).valueOf() / 1e3;
|
|
42
63
|
const timeDeltaMs = 1e3 * (parseInt(xRateLimitReset) - currentTime);
|
|
@@ -87,8 +108,14 @@ async function updateCookieJar(cookieJar, headers) {
|
|
|
87
108
|
}
|
|
88
109
|
}
|
|
89
110
|
|
|
111
|
+
const log$1 = debug("twitter-scraper:api");
|
|
90
112
|
const bearerToken = "AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF";
|
|
113
|
+
async function jitter(maxMs) {
|
|
114
|
+
const jitter2 = Math.random() * maxMs;
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, jitter2));
|
|
116
|
+
}
|
|
91
117
|
async function requestApi(url, auth, method = "GET", platform = new Platform()) {
|
|
118
|
+
log$1(`Making ${method} request to ${url}`);
|
|
92
119
|
const headers = new Headers();
|
|
93
120
|
await auth.installTo(headers, url);
|
|
94
121
|
await platform.randomizeCiphers();
|
|
@@ -115,6 +142,7 @@ async function requestApi(url, auth, method = "GET", platform = new Platform())
|
|
|
115
142
|
}
|
|
116
143
|
await updateCookieJar(auth.cookieJar(), res.headers);
|
|
117
144
|
if (res.status === 429) {
|
|
145
|
+
log$1("Rate limit hit, waiting for retry...");
|
|
118
146
|
await auth.onRateLimit({
|
|
119
147
|
fetchParameters,
|
|
120
148
|
response: res
|
|
@@ -273,11 +301,17 @@ class TwitterGuestAuth {
|
|
|
273
301
|
}
|
|
274
302
|
headers.set("cookie", await this.getCookieString());
|
|
275
303
|
}
|
|
276
|
-
getCookies() {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
304
|
+
async getCookies() {
|
|
305
|
+
const cookies = await Promise.all([
|
|
306
|
+
this.jar.getCookies(this.getCookieJarUrl()),
|
|
307
|
+
this.jar.getCookies("https://twitter.com"),
|
|
308
|
+
this.jar.getCookies("https://x.com")
|
|
309
|
+
]);
|
|
310
|
+
return cookies.flat();
|
|
311
|
+
}
|
|
312
|
+
async getCookieString() {
|
|
313
|
+
const cookies = await this.getCookies();
|
|
314
|
+
return cookies.map((cookie) => `${cookie.key}=${cookie.value}`).join("; ");
|
|
281
315
|
}
|
|
282
316
|
async removeCookie(key) {
|
|
283
317
|
const store = this.jar.store;
|
|
@@ -297,7 +331,7 @@ class TwitterGuestAuth {
|
|
|
297
331
|
* Updates the authentication state with a new guest token from the Twitter API.
|
|
298
332
|
*/
|
|
299
333
|
async updateGuestToken() {
|
|
300
|
-
const guestActivateUrl = "https://api.
|
|
334
|
+
const guestActivateUrl = "https://api.x.com/1.1/guest/activate.json";
|
|
301
335
|
const headers = new Headers({
|
|
302
336
|
Authorization: `Bearer ${this.bearerToken}`,
|
|
303
337
|
Cookie: await this.getCookieString()
|
|
@@ -331,6 +365,7 @@ class TwitterGuestAuth {
|
|
|
331
365
|
}
|
|
332
366
|
}
|
|
333
367
|
|
|
368
|
+
const log = debug("twitter-scraper:auth-user");
|
|
334
369
|
const TwitterUserAuthSubtask = Type.Object({
|
|
335
370
|
subtask_id: Type.String(),
|
|
336
371
|
enter_text: Type.Optional(Type.Object({}))
|
|
@@ -382,7 +417,7 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
382
417
|
}
|
|
383
418
|
async isLoggedIn() {
|
|
384
419
|
const res = await requestApi(
|
|
385
|
-
"https://api.
|
|
420
|
+
"https://api.x.com/1.1/account/verify_credentials.json",
|
|
386
421
|
this
|
|
387
422
|
);
|
|
388
423
|
if (!res.success) {
|
|
@@ -426,7 +461,7 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
426
461
|
}
|
|
427
462
|
try {
|
|
428
463
|
await requestApi(
|
|
429
|
-
"https://api.
|
|
464
|
+
"https://api.x.com/1.1/account/logout.json",
|
|
430
465
|
this,
|
|
431
466
|
"POST"
|
|
432
467
|
);
|
|
@@ -462,15 +497,59 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
462
497
|
this.removeCookie("external_referer=");
|
|
463
498
|
this.removeCookie("ct0=");
|
|
464
499
|
this.removeCookie("aa_u=");
|
|
500
|
+
this.removeCookie("__cf_bm=");
|
|
465
501
|
return await this.executeFlowTask({
|
|
466
502
|
flow_name: "login",
|
|
467
503
|
input_flow_data: {
|
|
468
504
|
flow_context: {
|
|
469
505
|
debug_overrides: {},
|
|
470
506
|
start_location: {
|
|
471
|
-
location: "
|
|
507
|
+
location: "unknown"
|
|
472
508
|
}
|
|
473
509
|
}
|
|
510
|
+
},
|
|
511
|
+
subtask_versions: {
|
|
512
|
+
action_list: 2,
|
|
513
|
+
alert_dialog: 1,
|
|
514
|
+
app_download_cta: 1,
|
|
515
|
+
check_logged_in_account: 1,
|
|
516
|
+
choice_selection: 3,
|
|
517
|
+
contacts_live_sync_permission_prompt: 0,
|
|
518
|
+
cta: 7,
|
|
519
|
+
email_verification: 2,
|
|
520
|
+
end_flow: 1,
|
|
521
|
+
enter_date: 1,
|
|
522
|
+
enter_email: 2,
|
|
523
|
+
enter_password: 5,
|
|
524
|
+
enter_phone: 2,
|
|
525
|
+
enter_recaptcha: 1,
|
|
526
|
+
enter_text: 5,
|
|
527
|
+
enter_username: 2,
|
|
528
|
+
generic_urt: 3,
|
|
529
|
+
in_app_notification: 1,
|
|
530
|
+
interest_picker: 3,
|
|
531
|
+
js_instrumentation: 1,
|
|
532
|
+
menu_dialog: 1,
|
|
533
|
+
notifications_permission_prompt: 2,
|
|
534
|
+
open_account: 2,
|
|
535
|
+
open_home_timeline: 1,
|
|
536
|
+
open_link: 1,
|
|
537
|
+
phone_verification: 4,
|
|
538
|
+
privacy_options: 1,
|
|
539
|
+
security_key: 3,
|
|
540
|
+
select_avatar: 4,
|
|
541
|
+
select_banner: 2,
|
|
542
|
+
settings_list: 7,
|
|
543
|
+
show_code: 1,
|
|
544
|
+
sign_up: 2,
|
|
545
|
+
sign_up_review: 4,
|
|
546
|
+
tweet_selection_urt: 1,
|
|
547
|
+
update_users: 1,
|
|
548
|
+
upload_media: 1,
|
|
549
|
+
user_recommendations_list: 4,
|
|
550
|
+
user_recommendations_urt: 1,
|
|
551
|
+
wait_spinner: 3,
|
|
552
|
+
web_modal: 1
|
|
474
553
|
}
|
|
475
554
|
});
|
|
476
555
|
}
|
|
@@ -603,7 +682,10 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
603
682
|
});
|
|
604
683
|
}
|
|
605
684
|
async executeFlowTask(data) {
|
|
606
|
-
|
|
685
|
+
let onboardingTaskUrl = "https://api.x.com/1.1/onboarding/task.json";
|
|
686
|
+
if ("flow_name" in data) {
|
|
687
|
+
onboardingTaskUrl = `https://api.x.com/1.1/onboarding/task.json?flow_name=${data.flow_name}`;
|
|
688
|
+
}
|
|
607
689
|
const token = this.guestToken;
|
|
608
690
|
if (token == null) {
|
|
609
691
|
throw new AuthenticationError(
|
|
@@ -621,15 +703,39 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
621
703
|
"x-twitter-client-language": "en"
|
|
622
704
|
});
|
|
623
705
|
await this.installCsrfToken(headers);
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
706
|
+
let res;
|
|
707
|
+
do {
|
|
708
|
+
const fetchParameters = [
|
|
709
|
+
onboardingTaskUrl,
|
|
710
|
+
{
|
|
711
|
+
credentials: "include",
|
|
712
|
+
method: "POST",
|
|
713
|
+
headers,
|
|
714
|
+
body: JSON.stringify(data)
|
|
715
|
+
}
|
|
716
|
+
];
|
|
717
|
+
try {
|
|
718
|
+
res = await this.fetch(...fetchParameters);
|
|
719
|
+
} catch (err) {
|
|
720
|
+
if (!(err instanceof Error)) {
|
|
721
|
+
throw err;
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
status: "error",
|
|
725
|
+
err: new Error("Failed to perform request.")
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
await updateCookieJar(this.jar, res.headers);
|
|
729
|
+
if (res.status === 429) {
|
|
730
|
+
log("Rate limit hit, waiting before retrying...");
|
|
731
|
+
await this.onRateLimit({
|
|
732
|
+
fetchParameters,
|
|
733
|
+
response: res
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
} while (res.status === 429);
|
|
631
737
|
if (!res.ok) {
|
|
632
|
-
return { status: "error", err:
|
|
738
|
+
return { status: "error", err: await ApiError.fromResponse(res) };
|
|
633
739
|
}
|
|
634
740
|
const flow = await res.json();
|
|
635
741
|
if (flow?.flow_token == null) {
|
|
@@ -667,71 +773,105 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
667
773
|
}
|
|
668
774
|
}
|
|
669
775
|
|
|
776
|
+
const endpoints = {
|
|
777
|
+
// TODO: Migrate other endpoint URLs here
|
|
778
|
+
UserTweets: "https://x.com/i/api/graphql/Li2XXGESVev94TzFtntrgA/UserTweets?variables=%7B%22userId%22%3A%221806359170830172162%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Atrue%2C%22withQuickPromoteEligibilityTweetFields%22%3Atrue%2C%22withVoice%22%3Atrue%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Afalse%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Afalse%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticlePlainText%22%3Afalse%7D",
|
|
779
|
+
UserTweetsAndReplies: "https://x.com/i/api/graphql/Hk4KlJ-ONjlJsucqR55P7g/UserTweetsAndReplies?variables=%7B%22userId%22%3A%221806359170830172162%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Atrue%2C%22withCommunity%22%3Atrue%2C%22withVoice%22%3Atrue%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Afalse%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Afalse%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticlePlainText%22%3Afalse%7D",
|
|
780
|
+
UserLikedTweets: "https://x.com/i/api/graphql/XHTMjDbiTGLQ9cP1em-aqQ/Likes?variables=%7B%22userId%22%3A%222244196397%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Afalse%2C%22withClientEventToken%22%3Afalse%2C%22withBirdwatchNotes%22%3Afalse%2C%22withVoice%22%3Atrue%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Afalse%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Afalse%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticlePlainText%22%3Afalse%7D",
|
|
781
|
+
UserByScreenName: "https://x.com/i/api/graphql/xWw45l6nX7DP2FKRyePXSw/UserByScreenName?variables=%7B%22screen_name%22%3A%22geminiapp%22%7D&features=%7B%22hidden_profile_subscriptions_enabled%22%3Atrue%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22subscriptions_verification_info_is_identity_verified_enabled%22%3Atrue%2C%22subscriptions_verification_info_verified_since_enabled%22%3Atrue%2C%22highlights_tweets_tab_ui_enabled%22%3Atrue%2C%22responsive_web_twitter_article_notes_tab_enabled%22%3Atrue%2C%22subscriptions_feature_can_gift_premium%22%3Atrue%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%7D&fieldToggles=%7B%22withAuxiliaryUserLabels%22%3Atrue%7D",
|
|
782
|
+
TweetDetail: "https://x.com/i/api/graphql/u5Tij6ERlSH2LZvCUqallw/TweetDetail?variables=%7B%22focalTweetId%22%3A%221924893675529900467%22%2C%22referrer%22%3A%22profile%22%2C%22with_rux_injections%22%3Afalse%2C%22rankingMode%22%3A%22Relevance%22%2C%22includePromotedContent%22%3Atrue%2C%22withCommunity%22%3Atrue%2C%22withQuickPromoteEligibilityTweetFields%22%3Atrue%2C%22withBirdwatchNotes%22%3Atrue%2C%22withVoice%22%3Atrue%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Afalse%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Afalse%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticleRichContentState%22%3Atrue%2C%22withArticlePlainText%22%3Afalse%2C%22withGrokAnalyze%22%3Afalse%2C%22withDisallowedReplyControls%22%3Afalse%7D",
|
|
783
|
+
TweetResultByRestId: "https://api.x.com/graphql/Opujkru5iJSDWj4DuJISOw/TweetResultByRestId?variables=%7B%22tweetId%22%3A%221924893675529900467%22%2C%22withCommunity%22%3Afalse%2C%22includePromotedContent%22%3Afalse%2C%22withVoice%22%3Afalse%7D&features=%7B%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Afalse%2C%22responsive_web_jetfuel_frame%22%3Afalse%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Afalse%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticleRichContentState%22%3Atrue%2C%22withArticlePlainText%22%3Afalse%2C%22withGrokAnalyze%22%3Afalse%2C%22withDisallowedReplyControls%22%3Afalse%7D",
|
|
784
|
+
ListTweets: "https://x.com/i/api/graphql/S1Sm3_mNJwa-fnY9htcaAQ/ListLatestTweetsTimeline?variables=%7B%22listId%22%3A%221736495155002106192%22%2C%22count%22%3A20%7D&features=%7B%22rweb_video_screen_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22premium_content_api_read_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22responsive_web_grok_analyze_button_fetch_trends_enabled%22%3Afalse%2C%22responsive_web_grok_analyze_post_followups_enabled%22%3Atrue%2C%22responsive_web_jetfuel_frame%22%3Afalse%2C%22responsive_web_grok_share_attachment_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22responsive_web_grok_show_grok_translated_post%22%3Afalse%2C%22responsive_web_grok_analysis_button_from_backend%22%3Atrue%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D"
|
|
785
|
+
};
|
|
786
|
+
class ApiRequest {
|
|
787
|
+
constructor(info) {
|
|
788
|
+
this.url = info.url;
|
|
789
|
+
this.variables = info.variables;
|
|
790
|
+
this.features = info.features;
|
|
791
|
+
this.fieldToggles = info.fieldToggles;
|
|
792
|
+
}
|
|
793
|
+
toRequestUrl() {
|
|
794
|
+
const params = new URLSearchParams();
|
|
795
|
+
if (this.variables) {
|
|
796
|
+
params.set("variables", stringify(this.variables));
|
|
797
|
+
}
|
|
798
|
+
if (this.features) {
|
|
799
|
+
params.set("features", stringify(this.features));
|
|
800
|
+
}
|
|
801
|
+
if (this.fieldToggles) {
|
|
802
|
+
params.set("fieldToggles", stringify(this.fieldToggles));
|
|
803
|
+
}
|
|
804
|
+
return `${this.url}?${params.toString()}`;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function parseEndpointExample(example) {
|
|
808
|
+
const { protocol, host, pathname, searchParams: query } = new URL(example);
|
|
809
|
+
const base = `${protocol}//${host}${pathname}`;
|
|
810
|
+
const variables = query.get("variables");
|
|
811
|
+
const features = query.get("features");
|
|
812
|
+
const fieldToggles = query.get("fieldToggles");
|
|
813
|
+
return new ApiRequest({
|
|
814
|
+
url: base,
|
|
815
|
+
variables: variables ? JSON.parse(variables) : void 0,
|
|
816
|
+
features: features ? JSON.parse(features) : void 0,
|
|
817
|
+
fieldToggles: fieldToggles ? JSON.parse(fieldToggles) : void 0
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
function createApiRequestFactory(endpoints2) {
|
|
821
|
+
return Object.entries(endpoints2).map(([endpointName, endpointExample]) => {
|
|
822
|
+
return {
|
|
823
|
+
[`create${endpointName}Request`]: () => {
|
|
824
|
+
return parseEndpointExample(endpointExample);
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
}).reduce((agg, next) => {
|
|
828
|
+
return Object.assign(agg, next);
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
const apiRequestFactory = createApiRequestFactory(endpoints);
|
|
832
|
+
|
|
670
833
|
function getAvatarOriginalSizeUrl(avatarUrl) {
|
|
671
834
|
return avatarUrl ? avatarUrl.replace("_normal", "") : void 0;
|
|
672
835
|
}
|
|
673
|
-
function parseProfile(
|
|
836
|
+
function parseProfile(legacy, isBlueVerified) {
|
|
674
837
|
const profile = {
|
|
675
|
-
avatar: getAvatarOriginalSizeUrl(
|
|
676
|
-
banner:
|
|
677
|
-
biography:
|
|
678
|
-
followersCount:
|
|
679
|
-
followingCount:
|
|
680
|
-
friendsCount:
|
|
681
|
-
mediaCount:
|
|
682
|
-
isPrivate:
|
|
683
|
-
isVerified:
|
|
684
|
-
likesCount:
|
|
685
|
-
listedCount:
|
|
686
|
-
location:
|
|
687
|
-
name:
|
|
688
|
-
pinnedTweetIds:
|
|
689
|
-
tweetsCount:
|
|
690
|
-
url: `https://twitter.com/${
|
|
691
|
-
userId:
|
|
692
|
-
username:
|
|
838
|
+
avatar: getAvatarOriginalSizeUrl(legacy.profile_image_url_https),
|
|
839
|
+
banner: legacy.profile_banner_url,
|
|
840
|
+
biography: legacy.description,
|
|
841
|
+
followersCount: legacy.followers_count,
|
|
842
|
+
followingCount: legacy.friends_count,
|
|
843
|
+
friendsCount: legacy.friends_count,
|
|
844
|
+
mediaCount: legacy.media_count,
|
|
845
|
+
isPrivate: legacy.protected ?? false,
|
|
846
|
+
isVerified: legacy.verified,
|
|
847
|
+
likesCount: legacy.favourites_count,
|
|
848
|
+
listedCount: legacy.listed_count,
|
|
849
|
+
location: legacy.location,
|
|
850
|
+
name: legacy.name,
|
|
851
|
+
pinnedTweetIds: legacy.pinned_tweet_ids_str,
|
|
852
|
+
tweetsCount: legacy.statuses_count,
|
|
853
|
+
url: `https://twitter.com/${legacy.screen_name}`,
|
|
854
|
+
userId: legacy.id_str,
|
|
855
|
+
username: legacy.screen_name,
|
|
693
856
|
isBlueVerified: isBlueVerified ?? false,
|
|
694
|
-
canDm:
|
|
857
|
+
canDm: legacy.can_dm
|
|
695
858
|
};
|
|
696
|
-
if (
|
|
697
|
-
profile.joined = new Date(Date.parse(
|
|
859
|
+
if (legacy.created_at != null) {
|
|
860
|
+
profile.joined = new Date(Date.parse(legacy.created_at));
|
|
698
861
|
}
|
|
699
|
-
const urls =
|
|
862
|
+
const urls = legacy.entities?.url?.urls;
|
|
700
863
|
if (urls?.length != null && urls?.length > 0) {
|
|
701
864
|
profile.website = urls[0].expanded_url;
|
|
702
865
|
}
|
|
703
866
|
return profile;
|
|
704
867
|
}
|
|
705
868
|
async function getProfile(username, auth) {
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
})
|
|
713
|
-
);
|
|
714
|
-
params.set(
|
|
715
|
-
"features",
|
|
716
|
-
stringify({
|
|
717
|
-
hidden_profile_likes_enabled: false,
|
|
718
|
-
hidden_profile_subscriptions_enabled: false,
|
|
719
|
-
// Auth-restricted
|
|
720
|
-
responsive_web_graphql_exclude_directive_enabled: true,
|
|
721
|
-
verified_phone_label_enabled: false,
|
|
722
|
-
subscriptions_verification_info_is_identity_verified_enabled: false,
|
|
723
|
-
subscriptions_verification_info_verified_since_enabled: true,
|
|
724
|
-
highlights_tweets_tab_ui_enabled: true,
|
|
725
|
-
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
726
|
-
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
727
|
-
responsive_web_graphql_timeline_navigation_enabled: true
|
|
728
|
-
})
|
|
729
|
-
);
|
|
730
|
-
params.set("fieldToggles", stringify({ withAuxiliaryUserLabels: false }));
|
|
731
|
-
const res = await requestApi(
|
|
732
|
-
`https://twitter.com/i/api/graphql/G3KGOASz96M-Qu0nwmGXNg/UserByScreenName?${params.toString()}`,
|
|
733
|
-
auth
|
|
734
|
-
);
|
|
869
|
+
const request = apiRequestFactory.createUserByScreenNameRequest();
|
|
870
|
+
request.variables.screen_name = username;
|
|
871
|
+
request.variables.withSafetyModeUserFields = true;
|
|
872
|
+
request.features.hidden_profile_subscriptions_enabled = false;
|
|
873
|
+
request.fieldToggles.withAuxiliaryUserLabels = false;
|
|
874
|
+
const res = await requestApi(request.toRequestUrl(), auth);
|
|
735
875
|
if (!res.success) {
|
|
736
876
|
return res;
|
|
737
877
|
}
|
|
@@ -758,15 +898,20 @@ async function getProfile(username, auth) {
|
|
|
758
898
|
};
|
|
759
899
|
}
|
|
760
900
|
legacy.id_str = user.rest_id;
|
|
901
|
+
legacy.screen_name ?? (legacy.screen_name = user.core?.screen_name);
|
|
902
|
+
legacy.profile_image_url_https ?? (legacy.profile_image_url_https = user.avatar?.image_url);
|
|
903
|
+
legacy.created_at ?? (legacy.created_at = user.core?.created_at);
|
|
904
|
+
legacy.location ?? (legacy.location = user.location?.location);
|
|
905
|
+
legacy.name ?? (legacy.name = user.core?.name);
|
|
761
906
|
if (legacy.screen_name == null || legacy.screen_name.length === 0) {
|
|
762
907
|
return {
|
|
763
908
|
success: false,
|
|
764
|
-
err: new Error(`
|
|
909
|
+
err: new Error(`User ${username} does not exist or is private.`)
|
|
765
910
|
};
|
|
766
911
|
}
|
|
767
912
|
return {
|
|
768
913
|
success: true,
|
|
769
|
-
value: parseProfile(
|
|
914
|
+
value: parseProfile(legacy, user.is_blue_verified)
|
|
770
915
|
};
|
|
771
916
|
}
|
|
772
917
|
const idCache = /* @__PURE__ */ new Map();
|
|
@@ -815,6 +960,7 @@ async function* getUserTimeline(query, maxProfiles, fetchFunc) {
|
|
|
815
960
|
nProfiles++;
|
|
816
961
|
}
|
|
817
962
|
if (!next) break;
|
|
963
|
+
await jitter(1e3);
|
|
818
964
|
}
|
|
819
965
|
}
|
|
820
966
|
async function* getTweetTimeline(query, maxTweets, fetchFunc) {
|
|
@@ -839,6 +985,7 @@ async function* getTweetTimeline(query, maxTweets, fetchFunc) {
|
|
|
839
985
|
}
|
|
840
986
|
nTweets++;
|
|
841
987
|
}
|
|
988
|
+
await jitter(1e3);
|
|
842
989
|
}
|
|
843
990
|
}
|
|
844
991
|
|
|
@@ -961,7 +1108,7 @@ function getLegacyTweetId(tweet) {
|
|
|
961
1108
|
}
|
|
962
1109
|
return tweet.conversation_id_str;
|
|
963
1110
|
}
|
|
964
|
-
function parseLegacyTweet(user, tweet, editControl) {
|
|
1111
|
+
function parseLegacyTweet(coreUser, user, tweet, editControl) {
|
|
965
1112
|
if (tweet == null) {
|
|
966
1113
|
return {
|
|
967
1114
|
success: false,
|
|
@@ -991,6 +1138,8 @@ function parseLegacyTweet(user, tweet, editControl) {
|
|
|
991
1138
|
const { photos, videos, sensitiveContent } = parseMediaGroups(media);
|
|
992
1139
|
const tweetVersions = editControl?.edit_tweet_ids ?? [tweetId];
|
|
993
1140
|
const editIds = tweetVersions.filter((id) => id !== tweetId);
|
|
1141
|
+
const name = user.name ?? coreUser?.name;
|
|
1142
|
+
const username = user.screen_name ?? coreUser?.screen_name;
|
|
994
1143
|
const tw = {
|
|
995
1144
|
__raw_UNSTABLE: tweet,
|
|
996
1145
|
bookmarkCount: tweet.bookmark_count,
|
|
@@ -1003,8 +1152,8 @@ function parseLegacyTweet(user, tweet, editControl) {
|
|
|
1003
1152
|
username: mention.screen_name,
|
|
1004
1153
|
name: mention.name
|
|
1005
1154
|
})),
|
|
1006
|
-
name
|
|
1007
|
-
permanentUrl: `https://twitter.com/${
|
|
1155
|
+
name,
|
|
1156
|
+
permanentUrl: `https://twitter.com/${username}/status/${tweetId}`,
|
|
1008
1157
|
photos,
|
|
1009
1158
|
replies: tweet.reply_count,
|
|
1010
1159
|
retweets: tweet.retweet_count,
|
|
@@ -1012,7 +1161,7 @@ function parseLegacyTweet(user, tweet, editControl) {
|
|
|
1012
1161
|
thread: [],
|
|
1013
1162
|
urls: urls.filter(isFieldDefined("expanded_url")).map((url) => url.expanded_url),
|
|
1014
1163
|
userId: tweet.user_id_str,
|
|
1015
|
-
username
|
|
1164
|
+
username,
|
|
1016
1165
|
videos,
|
|
1017
1166
|
isQuoted: false,
|
|
1018
1167
|
isReply: false,
|
|
@@ -1046,6 +1195,7 @@ function parseLegacyTweet(user, tweet, editControl) {
|
|
|
1046
1195
|
tw.retweetedStatusId = retweetedStatusIdStr;
|
|
1047
1196
|
if (retweetedStatusResult) {
|
|
1048
1197
|
const parsedResult = parseLegacyTweet(
|
|
1198
|
+
retweetedStatusResult?.core?.user_results?.result?.core,
|
|
1049
1199
|
retweetedStatusResult?.core?.user_results?.result?.legacy,
|
|
1050
1200
|
retweetedStatusResult?.legacy
|
|
1051
1201
|
);
|
|
@@ -1073,6 +1223,7 @@ function parseResult(result) {
|
|
|
1073
1223
|
result.legacy.full_text = noteTweetResultText;
|
|
1074
1224
|
}
|
|
1075
1225
|
const tweetResult = parseLegacyTweet(
|
|
1226
|
+
result?.core?.user_results?.result?.core,
|
|
1076
1227
|
result?.core?.user_results?.result?.legacy,
|
|
1077
1228
|
result?.legacy
|
|
1078
1229
|
);
|
|
@@ -1223,6 +1374,7 @@ function parseSearchTimelineTweets(timeline) {
|
|
|
1223
1374
|
if (itemContent?.tweetDisplayType === "Tweet") {
|
|
1224
1375
|
const tweetResultRaw = itemContent.tweet_results?.result;
|
|
1225
1376
|
const tweetResult = parseLegacyTweet(
|
|
1377
|
+
tweetResultRaw?.core?.user_results?.result?.core,
|
|
1226
1378
|
tweetResultRaw?.core?.user_results?.result?.legacy,
|
|
1227
1379
|
tweetResultRaw?.legacy,
|
|
1228
1380
|
tweetResultRaw?.edit_control?.edit_control_initial
|
|
@@ -1564,62 +1716,6 @@ async function getTrends(auth) {
|
|
|
1564
1716
|
return trends;
|
|
1565
1717
|
}
|
|
1566
1718
|
|
|
1567
|
-
const endpoints = {
|
|
1568
|
-
// TODO: Migrate other endpoint URLs here
|
|
1569
|
-
UserTweets: "https://twitter.com/i/api/graphql/V7H0Ap3_Hh2FyS75OCDO3Q/UserTweets?variables=%7B%22userId%22%3A%224020276615%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Atrue%2C%22withQuickPromoteEligibilityTweetFields%22%3Atrue%2C%22withVoice%22%3Atrue%2C%22withV2Timeline%22%3Atrue%7D&features=%7B%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22responsive_web_graphql_exclude_directive_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22rweb_video_timestamps_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticlePlainText%22%3Afalse%7D",
|
|
1570
|
-
UserTweetsAndReplies: "https://twitter.com/i/api/graphql/E4wA5vo2sjVyvpliUffSCw/UserTweetsAndReplies?variables=%7B%22userId%22%3A%224020276615%22%2C%22count%22%3A40%2C%22cursor%22%3A%22DAABCgABGPWl-F-ATiIKAAIY9YfiF1rRAggAAwAAAAEAAA%22%2C%22includePromotedContent%22%3Atrue%2C%22withCommunity%22%3Atrue%2C%22withVoice%22%3Atrue%2C%22withV2Timeline%22%3Atrue%7D&features=%7B%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22responsive_web_graphql_exclude_directive_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22rweb_video_timestamps_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticlePlainText%22%3Afalse%7D",
|
|
1571
|
-
UserLikedTweets: "https://twitter.com/i/api/graphql/eSSNbhECHHWWALkkQq-YTA/Likes?variables=%7B%22userId%22%3A%222244196397%22%2C%22count%22%3A20%2C%22includePromotedContent%22%3Afalse%2C%22withClientEventToken%22%3Afalse%2C%22withBirdwatchNotes%22%3Afalse%2C%22withVoice%22%3Atrue%2C%22withV2Timeline%22%3Atrue%7D&features=%7B%22responsive_web_graphql_exclude_directive_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22rweb_video_timestamps_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D",
|
|
1572
|
-
TweetDetail: "https://twitter.com/i/api/graphql/xOhkmRac04YFZmOzU9PJHg/TweetDetail?variables=%7B%22focalTweetId%22%3A%221237110546383724547%22%2C%22with_rux_injections%22%3Afalse%2C%22includePromotedContent%22%3Atrue%2C%22withCommunity%22%3Atrue%2C%22withQuickPromoteEligibilityTweetFields%22%3Atrue%2C%22withBirdwatchNotes%22%3Atrue%2C%22withVoice%22%3Atrue%2C%22withV2Timeline%22%3Atrue%7D&features=%7B%22responsive_web_graphql_exclude_directive_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Afalse%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_media_download_video_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticleRichContentState%22%3Afalse%7D",
|
|
1573
|
-
TweetResultByRestId: "https://twitter.com/i/api/graphql/DJS3BdhUhcaEpZ7B7irJDg/TweetResultByRestId?variables=%7B%22tweetId%22%3A%221237110546383724547%22%2C%22withCommunity%22%3Afalse%2C%22includePromotedContent%22%3Afalse%2C%22withVoice%22%3Afalse%7D&features=%7B%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Afalse%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_graphql_exclude_directive_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22responsive_web_media_download_video_enabled%22%3Afalse%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D",
|
|
1574
|
-
ListTweets: "https://twitter.com/i/api/graphql/whF0_KH1fCkdLLoyNPMoEw/ListLatestTweetsTimeline?variables=%7B%22listId%22%3A%221736495155002106192%22%2C%22count%22%3A20%7D&features=%7B%22responsive_web_graphql_exclude_directive_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Afalse%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22rweb_video_timestamps_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_media_download_video_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D"
|
|
1575
|
-
};
|
|
1576
|
-
class ApiRequest {
|
|
1577
|
-
constructor(info) {
|
|
1578
|
-
this.url = info.url;
|
|
1579
|
-
this.variables = info.variables;
|
|
1580
|
-
this.features = info.features;
|
|
1581
|
-
this.fieldToggles = info.fieldToggles;
|
|
1582
|
-
}
|
|
1583
|
-
toRequestUrl() {
|
|
1584
|
-
const params = new URLSearchParams();
|
|
1585
|
-
if (this.variables) {
|
|
1586
|
-
params.set("variables", stringify(this.variables));
|
|
1587
|
-
}
|
|
1588
|
-
if (this.features) {
|
|
1589
|
-
params.set("features", stringify(this.features));
|
|
1590
|
-
}
|
|
1591
|
-
if (this.fieldToggles) {
|
|
1592
|
-
params.set("fieldToggles", stringify(this.fieldToggles));
|
|
1593
|
-
}
|
|
1594
|
-
return `${this.url}?${params.toString()}`;
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
function parseEndpointExample(example) {
|
|
1598
|
-
const { protocol, host, pathname, searchParams: query } = new URL(example);
|
|
1599
|
-
const base = `${protocol}//${host}${pathname}`;
|
|
1600
|
-
const variables = query.get("variables");
|
|
1601
|
-
const features = query.get("features");
|
|
1602
|
-
const fieldToggles = query.get("fieldToggles");
|
|
1603
|
-
return new ApiRequest({
|
|
1604
|
-
url: base,
|
|
1605
|
-
variables: variables ? JSON.parse(variables) : void 0,
|
|
1606
|
-
features: features ? JSON.parse(features) : void 0,
|
|
1607
|
-
fieldToggles: fieldToggles ? JSON.parse(fieldToggles) : void 0
|
|
1608
|
-
});
|
|
1609
|
-
}
|
|
1610
|
-
function createApiRequestFactory(endpoints2) {
|
|
1611
|
-
return Object.entries(endpoints2).map(([endpointName, endpointExample]) => {
|
|
1612
|
-
return {
|
|
1613
|
-
[`create${endpointName}Request`]: () => {
|
|
1614
|
-
return parseEndpointExample(endpointExample);
|
|
1615
|
-
}
|
|
1616
|
-
};
|
|
1617
|
-
}).reduce((agg, next) => {
|
|
1618
|
-
return Object.assign(agg, next);
|
|
1619
|
-
});
|
|
1620
|
-
}
|
|
1621
|
-
const apiRequestFactory = createApiRequestFactory(endpoints);
|
|
1622
|
-
|
|
1623
1719
|
function parseListTimelineTweets(timeline) {
|
|
1624
1720
|
let bottomCursor;
|
|
1625
1721
|
let topCursor;
|