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