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