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