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