@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,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var debug = require('debug');
|
|
3
4
|
var toughCookie = require('tough-cookie');
|
|
4
5
|
var setCookie = require('set-cookie-parser');
|
|
5
6
|
var headersPolyfill = require('headers-polyfill');
|
|
@@ -29,24 +30,39 @@ function _interopNamespaceDefault(e) {
|
|
|
29
30
|
var OTPAuth__namespace = /*#__PURE__*/_interopNamespaceDefault(OTPAuth);
|
|
30
31
|
|
|
31
32
|
class ApiError extends Error {
|
|
32
|
-
constructor(response, data
|
|
33
|
-
super(
|
|
33
|
+
constructor(response, data) {
|
|
34
|
+
super(
|
|
35
|
+
`Response status: ${response.status} | headers: ${JSON.stringify(
|
|
36
|
+
headersToString(response.headers)
|
|
37
|
+
)} | data: ${typeof data === "string" ? data : JSON.stringify(data)}`
|
|
38
|
+
);
|
|
34
39
|
this.response = response;
|
|
35
40
|
this.data = data;
|
|
36
41
|
}
|
|
37
42
|
static async fromResponse(response) {
|
|
38
43
|
let data = void 0;
|
|
39
44
|
try {
|
|
40
|
-
|
|
45
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
46
|
+
data = await response.json();
|
|
47
|
+
} else {
|
|
48
|
+
data = await response.text();
|
|
49
|
+
}
|
|
41
50
|
} catch {
|
|
42
51
|
try {
|
|
43
52
|
data = await response.text();
|
|
44
53
|
} catch {
|
|
45
54
|
}
|
|
46
55
|
}
|
|
47
|
-
return new ApiError(response, data
|
|
56
|
+
return new ApiError(response, data);
|
|
48
57
|
}
|
|
49
58
|
}
|
|
59
|
+
function headersToString(headers) {
|
|
60
|
+
const result = [];
|
|
61
|
+
headers.forEach((value, key) => {
|
|
62
|
+
result.push(`${key}: ${value}`);
|
|
63
|
+
});
|
|
64
|
+
return result.join("\n");
|
|
65
|
+
}
|
|
50
66
|
class AuthenticationError extends Error {
|
|
51
67
|
constructor(message) {
|
|
52
68
|
super(message || "Authentication failed");
|
|
@@ -54,10 +70,15 @@ class AuthenticationError extends Error {
|
|
|
54
70
|
}
|
|
55
71
|
}
|
|
56
72
|
|
|
73
|
+
const log$2 = debug("twitter-scraper:rate-limit");
|
|
57
74
|
class WaitingRateLimitStrategy {
|
|
58
75
|
async onRateLimit({ response: res }) {
|
|
76
|
+
const xRateLimitLimit = res.headers.get("x-rate-limit-limit");
|
|
59
77
|
const xRateLimitRemaining = res.headers.get("x-rate-limit-remaining");
|
|
60
78
|
const xRateLimitReset = res.headers.get("x-rate-limit-reset");
|
|
79
|
+
log$2(
|
|
80
|
+
`Rate limit event: limit=${xRateLimitLimit}, remaining=${xRateLimitRemaining}, reset=${xRateLimitReset}`
|
|
81
|
+
);
|
|
61
82
|
if (xRateLimitRemaining == "0" && xRateLimitReset) {
|
|
62
83
|
const currentTime = (/* @__PURE__ */ new Date()).valueOf() / 1e3;
|
|
63
84
|
const timeDeltaMs = 1e3 * (parseInt(xRateLimitReset) - currentTime);
|
|
@@ -108,8 +129,14 @@ async function updateCookieJar(cookieJar, headers) {
|
|
|
108
129
|
}
|
|
109
130
|
}
|
|
110
131
|
|
|
132
|
+
const log$1 = debug("twitter-scraper:api");
|
|
111
133
|
const bearerToken = "AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF";
|
|
134
|
+
async function jitter(maxMs) {
|
|
135
|
+
const jitter2 = Math.random() * maxMs;
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, jitter2));
|
|
137
|
+
}
|
|
112
138
|
async function requestApi(url, auth, method = "GET", platform = new Platform()) {
|
|
139
|
+
log$1(`Making ${method} request to ${url}`);
|
|
113
140
|
const headers = new headersPolyfill.Headers();
|
|
114
141
|
await auth.installTo(headers, url);
|
|
115
142
|
await platform.randomizeCiphers();
|
|
@@ -136,6 +163,7 @@ async function requestApi(url, auth, method = "GET", platform = new Platform())
|
|
|
136
163
|
}
|
|
137
164
|
await updateCookieJar(auth.cookieJar(), res.headers);
|
|
138
165
|
if (res.status === 429) {
|
|
166
|
+
log$1("Rate limit hit, waiting for retry...");
|
|
139
167
|
await auth.onRateLimit({
|
|
140
168
|
fetchParameters,
|
|
141
169
|
response: res
|
|
@@ -294,11 +322,17 @@ class TwitterGuestAuth {
|
|
|
294
322
|
}
|
|
295
323
|
headers.set("cookie", await this.getCookieString());
|
|
296
324
|
}
|
|
297
|
-
getCookies() {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
325
|
+
async getCookies() {
|
|
326
|
+
const cookies = await Promise.all([
|
|
327
|
+
this.jar.getCookies(this.getCookieJarUrl()),
|
|
328
|
+
this.jar.getCookies("https://twitter.com"),
|
|
329
|
+
this.jar.getCookies("https://x.com")
|
|
330
|
+
]);
|
|
331
|
+
return cookies.flat();
|
|
332
|
+
}
|
|
333
|
+
async getCookieString() {
|
|
334
|
+
const cookies = await this.getCookies();
|
|
335
|
+
return cookies.map((cookie) => `${cookie.key}=${cookie.value}`).join("; ");
|
|
302
336
|
}
|
|
303
337
|
async removeCookie(key) {
|
|
304
338
|
const store = this.jar.store;
|
|
@@ -318,7 +352,7 @@ class TwitterGuestAuth {
|
|
|
318
352
|
* Updates the authentication state with a new guest token from the Twitter API.
|
|
319
353
|
*/
|
|
320
354
|
async updateGuestToken() {
|
|
321
|
-
const guestActivateUrl = "https://api.
|
|
355
|
+
const guestActivateUrl = "https://api.x.com/1.1/guest/activate.json";
|
|
322
356
|
const headers = new headersPolyfill.Headers({
|
|
323
357
|
Authorization: `Bearer ${this.bearerToken}`,
|
|
324
358
|
Cookie: await this.getCookieString()
|
|
@@ -352,6 +386,7 @@ class TwitterGuestAuth {
|
|
|
352
386
|
}
|
|
353
387
|
}
|
|
354
388
|
|
|
389
|
+
const log = debug("twitter-scraper:auth-user");
|
|
355
390
|
const TwitterUserAuthSubtask = typebox.Type.Object({
|
|
356
391
|
subtask_id: typebox.Type.String(),
|
|
357
392
|
enter_text: typebox.Type.Optional(typebox.Type.Object({}))
|
|
@@ -403,7 +438,7 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
403
438
|
}
|
|
404
439
|
async isLoggedIn() {
|
|
405
440
|
const res = await requestApi(
|
|
406
|
-
"https://api.
|
|
441
|
+
"https://api.x.com/1.1/account/verify_credentials.json",
|
|
407
442
|
this
|
|
408
443
|
);
|
|
409
444
|
if (!res.success) {
|
|
@@ -447,7 +482,7 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
447
482
|
}
|
|
448
483
|
try {
|
|
449
484
|
await requestApi(
|
|
450
|
-
"https://api.
|
|
485
|
+
"https://api.x.com/1.1/account/logout.json",
|
|
451
486
|
this,
|
|
452
487
|
"POST"
|
|
453
488
|
);
|
|
@@ -483,15 +518,59 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
483
518
|
this.removeCookie("external_referer=");
|
|
484
519
|
this.removeCookie("ct0=");
|
|
485
520
|
this.removeCookie("aa_u=");
|
|
521
|
+
this.removeCookie("__cf_bm=");
|
|
486
522
|
return await this.executeFlowTask({
|
|
487
523
|
flow_name: "login",
|
|
488
524
|
input_flow_data: {
|
|
489
525
|
flow_context: {
|
|
490
526
|
debug_overrides: {},
|
|
491
527
|
start_location: {
|
|
492
|
-
location: "
|
|
528
|
+
location: "unknown"
|
|
493
529
|
}
|
|
494
530
|
}
|
|
531
|
+
},
|
|
532
|
+
subtask_versions: {
|
|
533
|
+
action_list: 2,
|
|
534
|
+
alert_dialog: 1,
|
|
535
|
+
app_download_cta: 1,
|
|
536
|
+
check_logged_in_account: 1,
|
|
537
|
+
choice_selection: 3,
|
|
538
|
+
contacts_live_sync_permission_prompt: 0,
|
|
539
|
+
cta: 7,
|
|
540
|
+
email_verification: 2,
|
|
541
|
+
end_flow: 1,
|
|
542
|
+
enter_date: 1,
|
|
543
|
+
enter_email: 2,
|
|
544
|
+
enter_password: 5,
|
|
545
|
+
enter_phone: 2,
|
|
546
|
+
enter_recaptcha: 1,
|
|
547
|
+
enter_text: 5,
|
|
548
|
+
enter_username: 2,
|
|
549
|
+
generic_urt: 3,
|
|
550
|
+
in_app_notification: 1,
|
|
551
|
+
interest_picker: 3,
|
|
552
|
+
js_instrumentation: 1,
|
|
553
|
+
menu_dialog: 1,
|
|
554
|
+
notifications_permission_prompt: 2,
|
|
555
|
+
open_account: 2,
|
|
556
|
+
open_home_timeline: 1,
|
|
557
|
+
open_link: 1,
|
|
558
|
+
phone_verification: 4,
|
|
559
|
+
privacy_options: 1,
|
|
560
|
+
security_key: 3,
|
|
561
|
+
select_avatar: 4,
|
|
562
|
+
select_banner: 2,
|
|
563
|
+
settings_list: 7,
|
|
564
|
+
show_code: 1,
|
|
565
|
+
sign_up: 2,
|
|
566
|
+
sign_up_review: 4,
|
|
567
|
+
tweet_selection_urt: 1,
|
|
568
|
+
update_users: 1,
|
|
569
|
+
upload_media: 1,
|
|
570
|
+
user_recommendations_list: 4,
|
|
571
|
+
user_recommendations_urt: 1,
|
|
572
|
+
wait_spinner: 3,
|
|
573
|
+
web_modal: 1
|
|
495
574
|
}
|
|
496
575
|
});
|
|
497
576
|
}
|
|
@@ -624,7 +703,10 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
624
703
|
});
|
|
625
704
|
}
|
|
626
705
|
async executeFlowTask(data) {
|
|
627
|
-
|
|
706
|
+
let onboardingTaskUrl = "https://api.x.com/1.1/onboarding/task.json";
|
|
707
|
+
if ("flow_name" in data) {
|
|
708
|
+
onboardingTaskUrl = `https://api.x.com/1.1/onboarding/task.json?flow_name=${data.flow_name}`;
|
|
709
|
+
}
|
|
628
710
|
const token = this.guestToken;
|
|
629
711
|
if (token == null) {
|
|
630
712
|
throw new AuthenticationError(
|
|
@@ -642,15 +724,39 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
642
724
|
"x-twitter-client-language": "en"
|
|
643
725
|
});
|
|
644
726
|
await this.installCsrfToken(headers);
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
727
|
+
let res;
|
|
728
|
+
do {
|
|
729
|
+
const fetchParameters = [
|
|
730
|
+
onboardingTaskUrl,
|
|
731
|
+
{
|
|
732
|
+
credentials: "include",
|
|
733
|
+
method: "POST",
|
|
734
|
+
headers,
|
|
735
|
+
body: JSON.stringify(data)
|
|
736
|
+
}
|
|
737
|
+
];
|
|
738
|
+
try {
|
|
739
|
+
res = await this.fetch(...fetchParameters);
|
|
740
|
+
} catch (err) {
|
|
741
|
+
if (!(err instanceof Error)) {
|
|
742
|
+
throw err;
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
status: "error",
|
|
746
|
+
err: new Error("Failed to perform request.")
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
await updateCookieJar(this.jar, res.headers);
|
|
750
|
+
if (res.status === 429) {
|
|
751
|
+
log("Rate limit hit, waiting before retrying...");
|
|
752
|
+
await this.onRateLimit({
|
|
753
|
+
fetchParameters,
|
|
754
|
+
response: res
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
} while (res.status === 429);
|
|
652
758
|
if (!res.ok) {
|
|
653
|
-
return { status: "error", err:
|
|
759
|
+
return { status: "error", err: await ApiError.fromResponse(res) };
|
|
654
760
|
}
|
|
655
761
|
const flow = await res.json();
|
|
656
762
|
if (flow?.flow_token == null) {
|
|
@@ -688,71 +794,105 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
688
794
|
}
|
|
689
795
|
}
|
|
690
796
|
|
|
797
|
+
const endpoints = {
|
|
798
|
+
// TODO: Migrate other endpoint URLs here
|
|
799
|
+
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",
|
|
800
|
+
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",
|
|
801
|
+
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",
|
|
802
|
+
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",
|
|
803
|
+
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",
|
|
804
|
+
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",
|
|
805
|
+
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"
|
|
806
|
+
};
|
|
807
|
+
class ApiRequest {
|
|
808
|
+
constructor(info) {
|
|
809
|
+
this.url = info.url;
|
|
810
|
+
this.variables = info.variables;
|
|
811
|
+
this.features = info.features;
|
|
812
|
+
this.fieldToggles = info.fieldToggles;
|
|
813
|
+
}
|
|
814
|
+
toRequestUrl() {
|
|
815
|
+
const params = new URLSearchParams();
|
|
816
|
+
if (this.variables) {
|
|
817
|
+
params.set("variables", stringify(this.variables));
|
|
818
|
+
}
|
|
819
|
+
if (this.features) {
|
|
820
|
+
params.set("features", stringify(this.features));
|
|
821
|
+
}
|
|
822
|
+
if (this.fieldToggles) {
|
|
823
|
+
params.set("fieldToggles", stringify(this.fieldToggles));
|
|
824
|
+
}
|
|
825
|
+
return `${this.url}?${params.toString()}`;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function parseEndpointExample(example) {
|
|
829
|
+
const { protocol, host, pathname, searchParams: query } = new URL(example);
|
|
830
|
+
const base = `${protocol}//${host}${pathname}`;
|
|
831
|
+
const variables = query.get("variables");
|
|
832
|
+
const features = query.get("features");
|
|
833
|
+
const fieldToggles = query.get("fieldToggles");
|
|
834
|
+
return new ApiRequest({
|
|
835
|
+
url: base,
|
|
836
|
+
variables: variables ? JSON.parse(variables) : void 0,
|
|
837
|
+
features: features ? JSON.parse(features) : void 0,
|
|
838
|
+
fieldToggles: fieldToggles ? JSON.parse(fieldToggles) : void 0
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
function createApiRequestFactory(endpoints2) {
|
|
842
|
+
return Object.entries(endpoints2).map(([endpointName, endpointExample]) => {
|
|
843
|
+
return {
|
|
844
|
+
[`create${endpointName}Request`]: () => {
|
|
845
|
+
return parseEndpointExample(endpointExample);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
}).reduce((agg, next) => {
|
|
849
|
+
return Object.assign(agg, next);
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
const apiRequestFactory = createApiRequestFactory(endpoints);
|
|
853
|
+
|
|
691
854
|
function getAvatarOriginalSizeUrl(avatarUrl) {
|
|
692
855
|
return avatarUrl ? avatarUrl.replace("_normal", "") : void 0;
|
|
693
856
|
}
|
|
694
|
-
function parseProfile(
|
|
857
|
+
function parseProfile(legacy, isBlueVerified) {
|
|
695
858
|
const profile = {
|
|
696
|
-
avatar: getAvatarOriginalSizeUrl(
|
|
697
|
-
banner:
|
|
698
|
-
biography:
|
|
699
|
-
followersCount:
|
|
700
|
-
followingCount:
|
|
701
|
-
friendsCount:
|
|
702
|
-
mediaCount:
|
|
703
|
-
isPrivate:
|
|
704
|
-
isVerified:
|
|
705
|
-
likesCount:
|
|
706
|
-
listedCount:
|
|
707
|
-
location:
|
|
708
|
-
name:
|
|
709
|
-
pinnedTweetIds:
|
|
710
|
-
tweetsCount:
|
|
711
|
-
url: `https://twitter.com/${
|
|
712
|
-
userId:
|
|
713
|
-
username:
|
|
859
|
+
avatar: getAvatarOriginalSizeUrl(legacy.profile_image_url_https),
|
|
860
|
+
banner: legacy.profile_banner_url,
|
|
861
|
+
biography: legacy.description,
|
|
862
|
+
followersCount: legacy.followers_count,
|
|
863
|
+
followingCount: legacy.friends_count,
|
|
864
|
+
friendsCount: legacy.friends_count,
|
|
865
|
+
mediaCount: legacy.media_count,
|
|
866
|
+
isPrivate: legacy.protected ?? false,
|
|
867
|
+
isVerified: legacy.verified,
|
|
868
|
+
likesCount: legacy.favourites_count,
|
|
869
|
+
listedCount: legacy.listed_count,
|
|
870
|
+
location: legacy.location,
|
|
871
|
+
name: legacy.name,
|
|
872
|
+
pinnedTweetIds: legacy.pinned_tweet_ids_str,
|
|
873
|
+
tweetsCount: legacy.statuses_count,
|
|
874
|
+
url: `https://twitter.com/${legacy.screen_name}`,
|
|
875
|
+
userId: legacy.id_str,
|
|
876
|
+
username: legacy.screen_name,
|
|
714
877
|
isBlueVerified: isBlueVerified ?? false,
|
|
715
|
-
canDm:
|
|
878
|
+
canDm: legacy.can_dm
|
|
716
879
|
};
|
|
717
|
-
if (
|
|
718
|
-
profile.joined = new Date(Date.parse(
|
|
880
|
+
if (legacy.created_at != null) {
|
|
881
|
+
profile.joined = new Date(Date.parse(legacy.created_at));
|
|
719
882
|
}
|
|
720
|
-
const urls =
|
|
883
|
+
const urls = legacy.entities?.url?.urls;
|
|
721
884
|
if (urls?.length != null && urls?.length > 0) {
|
|
722
885
|
profile.website = urls[0].expanded_url;
|
|
723
886
|
}
|
|
724
887
|
return profile;
|
|
725
888
|
}
|
|
726
889
|
async function getProfile(username, auth) {
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
})
|
|
734
|
-
);
|
|
735
|
-
params.set(
|
|
736
|
-
"features",
|
|
737
|
-
stringify({
|
|
738
|
-
hidden_profile_likes_enabled: false,
|
|
739
|
-
hidden_profile_subscriptions_enabled: false,
|
|
740
|
-
// Auth-restricted
|
|
741
|
-
responsive_web_graphql_exclude_directive_enabled: true,
|
|
742
|
-
verified_phone_label_enabled: false,
|
|
743
|
-
subscriptions_verification_info_is_identity_verified_enabled: false,
|
|
744
|
-
subscriptions_verification_info_verified_since_enabled: true,
|
|
745
|
-
highlights_tweets_tab_ui_enabled: true,
|
|
746
|
-
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
747
|
-
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
748
|
-
responsive_web_graphql_timeline_navigation_enabled: true
|
|
749
|
-
})
|
|
750
|
-
);
|
|
751
|
-
params.set("fieldToggles", stringify({ withAuxiliaryUserLabels: false }));
|
|
752
|
-
const res = await requestApi(
|
|
753
|
-
`https://twitter.com/i/api/graphql/G3KGOASz96M-Qu0nwmGXNg/UserByScreenName?${params.toString()}`,
|
|
754
|
-
auth
|
|
755
|
-
);
|
|
890
|
+
const request = apiRequestFactory.createUserByScreenNameRequest();
|
|
891
|
+
request.variables.screen_name = username;
|
|
892
|
+
request.variables.withSafetyModeUserFields = true;
|
|
893
|
+
request.features.hidden_profile_subscriptions_enabled = false;
|
|
894
|
+
request.fieldToggles.withAuxiliaryUserLabels = false;
|
|
895
|
+
const res = await requestApi(request.toRequestUrl(), auth);
|
|
756
896
|
if (!res.success) {
|
|
757
897
|
return res;
|
|
758
898
|
}
|
|
@@ -779,15 +919,20 @@ async function getProfile(username, auth) {
|
|
|
779
919
|
};
|
|
780
920
|
}
|
|
781
921
|
legacy.id_str = user.rest_id;
|
|
922
|
+
legacy.screen_name ?? (legacy.screen_name = user.core?.screen_name);
|
|
923
|
+
legacy.profile_image_url_https ?? (legacy.profile_image_url_https = user.avatar?.image_url);
|
|
924
|
+
legacy.created_at ?? (legacy.created_at = user.core?.created_at);
|
|
925
|
+
legacy.location ?? (legacy.location = user.location?.location);
|
|
926
|
+
legacy.name ?? (legacy.name = user.core?.name);
|
|
782
927
|
if (legacy.screen_name == null || legacy.screen_name.length === 0) {
|
|
783
928
|
return {
|
|
784
929
|
success: false,
|
|
785
|
-
err: new Error(`
|
|
930
|
+
err: new Error(`User ${username} does not exist or is private.`)
|
|
786
931
|
};
|
|
787
932
|
}
|
|
788
933
|
return {
|
|
789
934
|
success: true,
|
|
790
|
-
value: parseProfile(
|
|
935
|
+
value: parseProfile(legacy, user.is_blue_verified)
|
|
791
936
|
};
|
|
792
937
|
}
|
|
793
938
|
const idCache = /* @__PURE__ */ new Map();
|
|
@@ -836,6 +981,7 @@ async function* getUserTimeline(query, maxProfiles, fetchFunc) {
|
|
|
836
981
|
nProfiles++;
|
|
837
982
|
}
|
|
838
983
|
if (!next) break;
|
|
984
|
+
await jitter(1e3);
|
|
839
985
|
}
|
|
840
986
|
}
|
|
841
987
|
async function* getTweetTimeline(query, maxTweets, fetchFunc) {
|
|
@@ -860,6 +1006,7 @@ async function* getTweetTimeline(query, maxTweets, fetchFunc) {
|
|
|
860
1006
|
}
|
|
861
1007
|
nTweets++;
|
|
862
1008
|
}
|
|
1009
|
+
await jitter(1e3);
|
|
863
1010
|
}
|
|
864
1011
|
}
|
|
865
1012
|
|
|
@@ -982,7 +1129,7 @@ function getLegacyTweetId(tweet) {
|
|
|
982
1129
|
}
|
|
983
1130
|
return tweet.conversation_id_str;
|
|
984
1131
|
}
|
|
985
|
-
function parseLegacyTweet(user, tweet, editControl) {
|
|
1132
|
+
function parseLegacyTweet(coreUser, user, tweet, editControl) {
|
|
986
1133
|
if (tweet == null) {
|
|
987
1134
|
return {
|
|
988
1135
|
success: false,
|
|
@@ -1012,6 +1159,8 @@ function parseLegacyTweet(user, tweet, editControl) {
|
|
|
1012
1159
|
const { photos, videos, sensitiveContent } = parseMediaGroups(media);
|
|
1013
1160
|
const tweetVersions = editControl?.edit_tweet_ids ?? [tweetId];
|
|
1014
1161
|
const editIds = tweetVersions.filter((id) => id !== tweetId);
|
|
1162
|
+
const name = user.name ?? coreUser?.name;
|
|
1163
|
+
const username = user.screen_name ?? coreUser?.screen_name;
|
|
1015
1164
|
const tw = {
|
|
1016
1165
|
__raw_UNSTABLE: tweet,
|
|
1017
1166
|
bookmarkCount: tweet.bookmark_count,
|
|
@@ -1024,8 +1173,8 @@ function parseLegacyTweet(user, tweet, editControl) {
|
|
|
1024
1173
|
username: mention.screen_name,
|
|
1025
1174
|
name: mention.name
|
|
1026
1175
|
})),
|
|
1027
|
-
name
|
|
1028
|
-
permanentUrl: `https://twitter.com/${
|
|
1176
|
+
name,
|
|
1177
|
+
permanentUrl: `https://twitter.com/${username}/status/${tweetId}`,
|
|
1029
1178
|
photos,
|
|
1030
1179
|
replies: tweet.reply_count,
|
|
1031
1180
|
retweets: tweet.retweet_count,
|
|
@@ -1033,7 +1182,7 @@ function parseLegacyTweet(user, tweet, editControl) {
|
|
|
1033
1182
|
thread: [],
|
|
1034
1183
|
urls: urls.filter(isFieldDefined("expanded_url")).map((url) => url.expanded_url),
|
|
1035
1184
|
userId: tweet.user_id_str,
|
|
1036
|
-
username
|
|
1185
|
+
username,
|
|
1037
1186
|
videos,
|
|
1038
1187
|
isQuoted: false,
|
|
1039
1188
|
isReply: false,
|
|
@@ -1067,6 +1216,7 @@ function parseLegacyTweet(user, tweet, editControl) {
|
|
|
1067
1216
|
tw.retweetedStatusId = retweetedStatusIdStr;
|
|
1068
1217
|
if (retweetedStatusResult) {
|
|
1069
1218
|
const parsedResult = parseLegacyTweet(
|
|
1219
|
+
retweetedStatusResult?.core?.user_results?.result?.core,
|
|
1070
1220
|
retweetedStatusResult?.core?.user_results?.result?.legacy,
|
|
1071
1221
|
retweetedStatusResult?.legacy
|
|
1072
1222
|
);
|
|
@@ -1094,6 +1244,7 @@ function parseResult(result) {
|
|
|
1094
1244
|
result.legacy.full_text = noteTweetResultText;
|
|
1095
1245
|
}
|
|
1096
1246
|
const tweetResult = parseLegacyTweet(
|
|
1247
|
+
result?.core?.user_results?.result?.core,
|
|
1097
1248
|
result?.core?.user_results?.result?.legacy,
|
|
1098
1249
|
result?.legacy
|
|
1099
1250
|
);
|
|
@@ -1244,6 +1395,7 @@ function parseSearchTimelineTweets(timeline) {
|
|
|
1244
1395
|
if (itemContent?.tweetDisplayType === "Tweet") {
|
|
1245
1396
|
const tweetResultRaw = itemContent.tweet_results?.result;
|
|
1246
1397
|
const tweetResult = parseLegacyTweet(
|
|
1398
|
+
tweetResultRaw?.core?.user_results?.result?.core,
|
|
1247
1399
|
tweetResultRaw?.core?.user_results?.result?.legacy,
|
|
1248
1400
|
tweetResultRaw?.legacy,
|
|
1249
1401
|
tweetResultRaw?.edit_control?.edit_control_initial
|
|
@@ -1585,62 +1737,6 @@ async function getTrends(auth) {
|
|
|
1585
1737
|
return trends;
|
|
1586
1738
|
}
|
|
1587
1739
|
|
|
1588
|
-
const endpoints = {
|
|
1589
|
-
// TODO: Migrate other endpoint URLs here
|
|
1590
|
-
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",
|
|
1591
|
-
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",
|
|
1592
|
-
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",
|
|
1593
|
-
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",
|
|
1594
|
-
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",
|
|
1595
|
-
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"
|
|
1596
|
-
};
|
|
1597
|
-
class ApiRequest {
|
|
1598
|
-
constructor(info) {
|
|
1599
|
-
this.url = info.url;
|
|
1600
|
-
this.variables = info.variables;
|
|
1601
|
-
this.features = info.features;
|
|
1602
|
-
this.fieldToggles = info.fieldToggles;
|
|
1603
|
-
}
|
|
1604
|
-
toRequestUrl() {
|
|
1605
|
-
const params = new URLSearchParams();
|
|
1606
|
-
if (this.variables) {
|
|
1607
|
-
params.set("variables", stringify(this.variables));
|
|
1608
|
-
}
|
|
1609
|
-
if (this.features) {
|
|
1610
|
-
params.set("features", stringify(this.features));
|
|
1611
|
-
}
|
|
1612
|
-
if (this.fieldToggles) {
|
|
1613
|
-
params.set("fieldToggles", stringify(this.fieldToggles));
|
|
1614
|
-
}
|
|
1615
|
-
return `${this.url}?${params.toString()}`;
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
function parseEndpointExample(example) {
|
|
1619
|
-
const { protocol, host, pathname, searchParams: query } = new URL(example);
|
|
1620
|
-
const base = `${protocol}//${host}${pathname}`;
|
|
1621
|
-
const variables = query.get("variables");
|
|
1622
|
-
const features = query.get("features");
|
|
1623
|
-
const fieldToggles = query.get("fieldToggles");
|
|
1624
|
-
return new ApiRequest({
|
|
1625
|
-
url: base,
|
|
1626
|
-
variables: variables ? JSON.parse(variables) : void 0,
|
|
1627
|
-
features: features ? JSON.parse(features) : void 0,
|
|
1628
|
-
fieldToggles: fieldToggles ? JSON.parse(fieldToggles) : void 0
|
|
1629
|
-
});
|
|
1630
|
-
}
|
|
1631
|
-
function createApiRequestFactory(endpoints2) {
|
|
1632
|
-
return Object.entries(endpoints2).map(([endpointName, endpointExample]) => {
|
|
1633
|
-
return {
|
|
1634
|
-
[`create${endpointName}Request`]: () => {
|
|
1635
|
-
return parseEndpointExample(endpointExample);
|
|
1636
|
-
}
|
|
1637
|
-
};
|
|
1638
|
-
}).reduce((agg, next) => {
|
|
1639
|
-
return Object.assign(agg, next);
|
|
1640
|
-
});
|
|
1641
|
-
}
|
|
1642
|
-
const apiRequestFactory = createApiRequestFactory(endpoints);
|
|
1643
|
-
|
|
1644
1740
|
function parseListTimelineTweets(timeline) {
|
|
1645
1741
|
let bottomCursor;
|
|
1646
1742
|
let topCursor;
|