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