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