@the-convocation/twitter-scraper 0.19.0 → 0.20.0

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.
@@ -51,13 +51,13 @@ class AuthenticationError extends Error {
51
51
  }
52
52
  }
53
53
 
54
- const log$4 = debug("twitter-scraper:rate-limit");
54
+ const log$6 = debug("twitter-scraper:rate-limit");
55
55
  class WaitingRateLimitStrategy {
56
56
  async onRateLimit({ response: res }) {
57
57
  const xRateLimitLimit = res.headers.get("x-rate-limit-limit");
58
58
  const xRateLimitRemaining = res.headers.get("x-rate-limit-remaining");
59
59
  const xRateLimitReset = res.headers.get("x-rate-limit-reset");
60
- log$4(
60
+ log$6(
61
61
  `Rate limit event: limit=${xRateLimitLimit}, remaining=${xRateLimitRemaining}, reset=${xRateLimitReset}`
62
62
  );
63
63
  if (xRateLimitRemaining == "0" && xRateLimitReset) {
@@ -73,20 +73,7 @@ class ErrorRateLimitStrategy {
73
73
  }
74
74
  }
75
75
 
76
- class Platform {
77
- async randomizeCiphers() {
78
- const platform = await Platform.importPlatform();
79
- await platform?.randomizeCiphers();
80
- }
81
- static async importPlatform() {
82
- {
83
- const { platform } = await Promise.resolve().then(function () { return index; });
84
- return platform;
85
- }
86
- }
87
- }
88
-
89
- const log$3 = debug("twitter-scraper:requests");
76
+ const log$5 = debug("twitter-scraper:requests");
90
77
  async function updateCookieJar(cookieJar, headers) {
91
78
  let setCookieHeaders = [];
92
79
  if (typeof headers.getSetCookie === "function") {
@@ -101,12 +88,12 @@ async function updateCookieJar(cookieJar, headers) {
101
88
  for (const cookieStr of setCookieHeaders) {
102
89
  const cookie = Cookie.parse(cookieStr);
103
90
  if (!cookie) {
104
- log$3(`Failed to parse cookie: ${cookieStr.substring(0, 100)}`);
91
+ log$5(`Failed to parse cookie: ${cookieStr.substring(0, 100)}`);
105
92
  continue;
106
93
  }
107
94
  if (cookie.maxAge === 0 || cookie.expires && cookie.expires < /* @__PURE__ */ new Date()) {
108
95
  if (cookie.key === "ct0") {
109
- log$3(`Skipping deletion of ct0 cookie (Max-Age=0)`);
96
+ log$5(`Skipping deletion of ct0 cookie (Max-Age=0)`);
110
97
  }
111
98
  continue;
112
99
  }
@@ -114,7 +101,7 @@ async function updateCookieJar(cookieJar, headers) {
114
101
  const url = `${cookie.secure ? "https" : "http"}://${cookie.domain}${cookie.path}`;
115
102
  await cookieJar.setCookie(cookie, url);
116
103
  if (cookie.key === "ct0") {
117
- log$3(
104
+ log$5(
118
105
  `Successfully set ct0 cookie with value: ${cookie.value.substring(
119
106
  0,
120
107
  20
@@ -122,9 +109,9 @@ async function updateCookieJar(cookieJar, headers) {
122
109
  );
123
110
  }
124
111
  } catch (err) {
125
- log$3(`Failed to set cookie ${cookie.key}: ${err}`);
112
+ log$5(`Failed to set cookie ${cookie.key}: ${err}`);
126
113
  if (cookie.key === "ct0") {
127
- log$3(`FAILED to set ct0 cookie! Error: ${err}`);
114
+ log$5(`FAILED to set ct0 cookie! Error: ${err}`);
128
115
  }
129
116
  }
130
117
  }
@@ -138,131 +125,84 @@ async function updateCookieJar(cookieJar, headers) {
138
125
  }
139
126
  }
140
127
 
141
- const log$2 = debug("twitter-scraper:api");
142
- const bearerToken = "AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF";
143
- async function jitter(maxMs) {
144
- const jitter2 = Math.random() * maxMs;
145
- await new Promise((resolve) => setTimeout(resolve, jitter2));
146
- }
147
- async function requestApi(url, auth, method = "GET", platform = new Platform(), headers = new Headers()) {
148
- log$2(`Making ${method} request to ${url}`);
149
- await auth.installTo(headers, url);
150
- await platform.randomizeCiphers();
151
- let res;
152
- do {
153
- const fetchParameters = [
154
- url,
128
+ const log$4 = debug("twitter-scraper:xpff");
129
+ let isoCrypto = null;
130
+ function getCrypto() {
131
+ if (isoCrypto != null) {
132
+ return isoCrypto;
133
+ }
134
+ if (typeof crypto === "undefined") {
135
+ log$4("Global crypto is undefined, importing from crypto module...");
136
+ const { webcrypto } = require("crypto");
137
+ isoCrypto = webcrypto;
138
+ return webcrypto;
139
+ }
140
+ isoCrypto = crypto;
141
+ return crypto;
142
+ }
143
+ async function sha256(message) {
144
+ const msgBuffer = new TextEncoder().encode(message);
145
+ const hashBuffer = await getCrypto().subtle.digest("SHA-256", msgBuffer);
146
+ return new Uint8Array(hashBuffer);
147
+ }
148
+ function buf2hex(buffer) {
149
+ return [...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, "0")).join("");
150
+ }
151
+ class XPFFHeaderGenerator {
152
+ constructor(seed) {
153
+ this.seed = seed;
154
+ }
155
+ async deriveKey(guestId) {
156
+ const combined = `${this.seed}${guestId}`;
157
+ const result = await sha256(combined);
158
+ return result;
159
+ }
160
+ async generateHeader(plaintext, guestId) {
161
+ log$4(`Generating XPFF key for guest ID: ${guestId}`);
162
+ const key = await this.deriveKey(guestId);
163
+ const nonce = getCrypto().getRandomValues(new Uint8Array(12));
164
+ const cipher = await getCrypto().subtle.importKey(
165
+ "raw",
166
+ key,
167
+ { name: "AES-GCM" },
168
+ false,
169
+ ["encrypt"]
170
+ );
171
+ const encrypted = await getCrypto().subtle.encrypt(
155
172
  {
156
- method,
157
- headers,
158
- credentials: "include"
159
- }
160
- ];
161
- try {
162
- res = await auth.fetch(...fetchParameters);
163
- } catch (err) {
164
- if (!(err instanceof Error)) {
165
- throw err;
166
- }
167
- return {
168
- success: false,
169
- err: new Error("Failed to perform request.")
170
- };
171
- }
172
- await updateCookieJar(auth.cookieJar(), res.headers);
173
- if (res.status === 429) {
174
- log$2("Rate limit hit, waiting for retry...");
175
- await auth.onRateLimit({
176
- fetchParameters,
177
- response: res
178
- });
179
- }
180
- } while (res.status === 429);
181
- if (!res.ok) {
182
- return {
183
- success: false,
184
- err: await ApiError.fromResponse(res)
185
- };
186
- }
187
- const value = await res.json();
188
- if (res.headers.get("x-rate-limit-incoming") == "0") {
189
- auth.deleteToken();
190
- return { success: true, value };
191
- } else {
192
- return { success: true, value };
193
- }
194
- }
195
- function addApiFeatures(o) {
196
- return {
197
- ...o,
198
- rweb_lists_timeline_redesign_enabled: true,
199
- responsive_web_graphql_exclude_directive_enabled: true,
200
- verified_phone_label_enabled: false,
201
- creator_subscriptions_tweet_preview_api_enabled: true,
202
- responsive_web_graphql_timeline_navigation_enabled: true,
203
- responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
204
- tweetypie_unmention_optimization_enabled: true,
205
- responsive_web_edit_tweet_api_enabled: true,
206
- graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
207
- view_counts_everywhere_api_enabled: true,
208
- longform_notetweets_consumption_enabled: true,
209
- tweet_awards_web_tipping_enabled: false,
210
- freedom_of_speech_not_reach_fetch_enabled: true,
211
- standardized_nudges_misinfo: true,
212
- longform_notetweets_rich_text_read_enabled: true,
213
- responsive_web_enhance_cards_enabled: false,
214
- subscriptions_verification_info_enabled: true,
215
- subscriptions_verification_info_reason_enabled: true,
216
- subscriptions_verification_info_verified_since_enabled: true,
217
- super_follow_badge_privacy_enabled: false,
218
- super_follow_exclusive_tweet_notifications_enabled: false,
219
- super_follow_tweet_api_enabled: false,
220
- super_follow_user_api_enabled: false,
221
- android_graphql_skip_api_media_color_palette: false,
222
- creator_subscriptions_subscription_count_enabled: false,
223
- blue_business_profile_image_shape_enabled: false,
224
- unified_cards_ad_metadata_container_dynamic_card_content_query_enabled: false
225
- };
173
+ name: "AES-GCM",
174
+ iv: nonce
175
+ },
176
+ cipher,
177
+ new TextEncoder().encode(plaintext)
178
+ );
179
+ const combined = new Uint8Array(nonce.length + encrypted.byteLength);
180
+ combined.set(nonce);
181
+ combined.set(new Uint8Array(encrypted), nonce.length);
182
+ const result = buf2hex(combined);
183
+ log$4(`XPFF header generated for guest ID ${guestId}: ${result}`);
184
+ return result;
185
+ }
186
+ }
187
+ const xpffBaseKey = "0e6be1f1e21ffc33590b888fd4dc81b19713e570e805d4e5df80a493c9571a05";
188
+ function xpffPlain() {
189
+ const timestamp = Date.now();
190
+ return JSON.stringify({
191
+ navigator_properties: {
192
+ hasBeenActive: "true",
193
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
194
+ webdriver: "false"
195
+ },
196
+ created_at: timestamp
197
+ });
226
198
  }
227
- function addApiParams(params, includeTweetReplies) {
228
- params.set("include_profile_interstitial_type", "1");
229
- params.set("include_blocking", "1");
230
- params.set("include_blocked_by", "1");
231
- params.set("include_followed_by", "1");
232
- params.set("include_want_retweets", "1");
233
- params.set("include_mute_edge", "1");
234
- params.set("include_can_dm", "1");
235
- params.set("include_can_media_tag", "1");
236
- params.set("include_ext_has_nft_avatar", "1");
237
- params.set("include_ext_is_blue_verified", "1");
238
- params.set("include_ext_verified_type", "1");
239
- params.set("skip_status", "1");
240
- params.set("cards_platform", "Web-12");
241
- params.set("include_cards", "1");
242
- params.set("include_ext_alt_text", "true");
243
- params.set("include_ext_limited_action_results", "false");
244
- params.set("include_quote_count", "true");
245
- params.set("include_reply_count", "1");
246
- params.set("tweet_mode", "extended");
247
- params.set("include_ext_collab_control", "true");
248
- params.set("include_ext_views", "true");
249
- params.set("include_entities", "true");
250
- params.set("include_user_entities", "true");
251
- params.set("include_ext_media_color", "true");
252
- params.set("include_ext_media_availability", "true");
253
- params.set("include_ext_sensitive_media_warning", "true");
254
- params.set("include_ext_trusted_friends_metadata", "true");
255
- params.set("send_error_codes", "true");
256
- params.set("simple_quoted_tweet", "true");
257
- params.set("include_tweet_replies", `${includeTweetReplies}`);
258
- params.set(
259
- "ext",
260
- "mediaStats,highlightedLabel,hasNftAvatar,voiceInfo,birdwatchPivot,enrichments,superFollowMetadata,unmentionInfo,editControl,collab_control,vibe"
261
- );
262
- return params;
199
+ async function generateXPFFHeader(guestId) {
200
+ const generator = new XPFFHeaderGenerator(xpffBaseKey);
201
+ const plaintext = xpffPlain();
202
+ return generator.generateHeader(plaintext, guestId);
263
203
  }
264
204
 
265
- const log$1 = debug("twitter-scraper:auth");
205
+ const log$3 = debug("twitter-scraper:auth");
266
206
  function withTransform(fetchFn, transform) {
267
207
  return async (input, init) => {
268
208
  const fetchArgs = await transform?.request?.(input, init) ?? [
@@ -316,20 +256,30 @@ class TwitterGuestAuth {
316
256
  if (this.shouldUpdate()) {
317
257
  await this.updateGuestToken();
318
258
  }
319
- const token = this.guestToken;
320
- if (token == null) {
321
- throw new AuthenticationError(
322
- "Authentication token is null or undefined."
323
- );
259
+ if (this.guestToken) {
260
+ headers.set("x-guest-token", this.guestToken);
324
261
  }
325
262
  headers.set("authorization", `Bearer ${this.bearerToken}`);
326
- headers.set("x-guest-token", token);
263
+ headers.set(
264
+ "user-agent",
265
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
266
+ );
267
+ await this.installCsrfToken(headers);
268
+ if (this.options?.experimental?.xpff) {
269
+ const guestId = await this.guestId();
270
+ if (guestId != null) {
271
+ const xpffHeader = await generateXPFFHeader(guestId);
272
+ headers.set("x-xp-forwarded-for", xpffHeader);
273
+ }
274
+ }
275
+ headers.set("cookie", await this.getCookieString());
276
+ }
277
+ async installCsrfToken(headers) {
327
278
  const cookies = await this.getCookies();
328
279
  const xCsrfToken = cookies.find((cookie) => cookie.key === "ct0");
329
280
  if (xCsrfToken) {
330
281
  headers.set("x-csrf-token", xCsrfToken.value);
331
282
  }
332
- headers.set("cookie", await this.getCookieString());
333
283
  }
334
284
  async setCookie(key, value) {
335
285
  const cookie = Cookie.parse(`${key}=${value}`);
@@ -362,16 +312,28 @@ class TwitterGuestAuth {
362
312
  getCookieJarUrl() {
363
313
  return typeof document !== "undefined" ? document.location.toString() : "https://x.com";
364
314
  }
315
+ async guestId() {
316
+ const cookies = await this.getCookies();
317
+ const guestIdCookie = cookies.find((cookie) => cookie.key === "guest_id");
318
+ return guestIdCookie ? guestIdCookie.value : null;
319
+ }
365
320
  /**
366
321
  * Updates the authentication state with a new guest token from the Twitter API.
367
322
  */
368
323
  async updateGuestToken() {
324
+ try {
325
+ await this.updateGuestTokenCore();
326
+ } catch (err) {
327
+ log$3("Failed to update guest token; this may cause issues:", err);
328
+ }
329
+ }
330
+ async updateGuestTokenCore() {
369
331
  const guestActivateUrl = "https://api.x.com/1.1/guest/activate.json";
370
332
  const headers = new Headers({
371
333
  Authorization: `Bearer ${this.bearerToken}`,
372
334
  Cookie: await this.getCookieString()
373
335
  });
374
- log$1(`Making POST request to ${guestActivateUrl}`);
336
+ log$3(`Making POST request to ${guestActivateUrl}`);
375
337
  const res = await this.fetch(guestActivateUrl, {
376
338
  method: "POST",
377
339
  headers,
@@ -381,7 +343,7 @@ class TwitterGuestAuth {
381
343
  if (!res.ok) {
382
344
  throw new AuthenticationError(await res.text());
383
345
  }
384
- const o = await res.json();
346
+ const o = await flexParseJson(res);
385
347
  if (o == null || o["guest_token"] == null) {
386
348
  throw new AuthenticationError("guest_token not found.");
387
349
  }
@@ -392,7 +354,7 @@ class TwitterGuestAuth {
392
354
  this.guestToken = newGuestToken;
393
355
  this.guestCreatedAt = /* @__PURE__ */ new Date();
394
356
  await this.setCookie("gt", newGuestToken);
395
- log$1(`Updated guest token: ${newGuestToken}`);
357
+ log$3(`Updated guest token: ${newGuestToken}`);
396
358
  }
397
359
  /**
398
360
  * Returns if the authentication token needs to be updated or not.
@@ -403,6 +365,277 @@ class TwitterGuestAuth {
403
365
  }
404
366
  }
405
367
 
368
+ class Platform {
369
+ async randomizeCiphers() {
370
+ const platform = await Platform.importPlatform();
371
+ await platform?.randomizeCiphers();
372
+ }
373
+ static async importPlatform() {
374
+ {
375
+ const { platform } = await Promise.resolve().then(function () { return index; });
376
+ return platform;
377
+ }
378
+ }
379
+ }
380
+
381
+ const log$2 = debug("twitter-scraper:xctxid");
382
+ let linkedom = null;
383
+ function linkedomImport() {
384
+ if (!linkedom) {
385
+ const mod = require("linkedom");
386
+ linkedom = mod;
387
+ return mod;
388
+ }
389
+ return linkedom;
390
+ }
391
+ async function parseHTML(html) {
392
+ if (typeof window !== "undefined") {
393
+ const { defaultView } = new DOMParser().parseFromString(html, "text/html");
394
+ if (!defaultView) {
395
+ throw new Error("Failed to get defaultView from parsed HTML.");
396
+ }
397
+ return defaultView;
398
+ } else {
399
+ const { DOMParser: DOMParser2 } = linkedomImport();
400
+ return new DOMParser2().parseFromString(html, "text/html").defaultView;
401
+ }
402
+ }
403
+ async function handleXMigration(fetchFn) {
404
+ const headers = {
405
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
406
+ "accept-language": "ja",
407
+ "cache-control": "no-cache",
408
+ pragma: "no-cache",
409
+ priority: "u=0, i",
410
+ "sec-ch-ua": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
411
+ "sec-ch-ua-mobile": "?0",
412
+ "sec-ch-ua-platform": '"Windows"',
413
+ "sec-fetch-dest": "document",
414
+ "sec-fetch-mode": "navigate",
415
+ "sec-fetch-site": "none",
416
+ "sec-fetch-user": "?1",
417
+ "upgrade-insecure-requests": "1",
418
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
419
+ };
420
+ const response = await fetchFn("https://x.com", {
421
+ headers
422
+ });
423
+ if (!response.ok) {
424
+ throw new Error(`Failed to fetch X homepage: ${response.statusText}`);
425
+ }
426
+ const htmlText = await response.text();
427
+ let dom = await parseHTML(htmlText);
428
+ let document = dom.window.document;
429
+ const migrationRedirectionRegex = new RegExp(
430
+ "(http(?:s)?://(?:www\\.)?(twitter|x){1}\\.com(/x)?/migrate([/?])?tok=[a-zA-Z0-9%\\-_]+)+",
431
+ "i"
432
+ );
433
+ const metaRefresh = document.querySelector("meta[http-equiv='refresh']");
434
+ const metaContent = metaRefresh ? metaRefresh.getAttribute("content") || "" : "";
435
+ const migrationRedirectionUrl = migrationRedirectionRegex.exec(metaContent) || migrationRedirectionRegex.exec(htmlText);
436
+ if (migrationRedirectionUrl) {
437
+ const redirectResponse = await fetch(migrationRedirectionUrl[0]);
438
+ if (!redirectResponse.ok) {
439
+ throw new Error(
440
+ `Failed to follow migration redirection: ${redirectResponse.statusText}`
441
+ );
442
+ }
443
+ const redirectHtml = await redirectResponse.text();
444
+ dom = await parseHTML(redirectHtml);
445
+ document = dom.window.document;
446
+ }
447
+ const migrationForm = document.querySelector("form[name='f']") || document.querySelector("form[action='https://x.com/x/migrate']");
448
+ if (migrationForm) {
449
+ const url = migrationForm.getAttribute("action") || "https://x.com/x/migrate";
450
+ const method = migrationForm.getAttribute("method") || "POST";
451
+ const requestPayload = new FormData();
452
+ const inputFields = migrationForm.querySelectorAll("input");
453
+ for (const element of Array.from(inputFields)) {
454
+ const name = element.getAttribute("name");
455
+ const value = element.getAttribute("value");
456
+ if (name && value) {
457
+ requestPayload.append(name, value);
458
+ }
459
+ }
460
+ const formResponse = await fetch(url, {
461
+ method,
462
+ body: requestPayload,
463
+ headers
464
+ });
465
+ if (!formResponse.ok) {
466
+ throw new Error(
467
+ `Failed to submit migration form: ${formResponse.statusText}`
468
+ );
469
+ }
470
+ const formHtml = await formResponse.text();
471
+ dom = await parseHTML(formHtml);
472
+ document = dom.window.document;
473
+ }
474
+ return document;
475
+ }
476
+ let ClientTransaction = null;
477
+ function clientTransaction() {
478
+ if (!ClientTransaction) {
479
+ const mod = require("x-client-transaction-id");
480
+ const ctx = mod.ClientTransaction;
481
+ ClientTransaction = ctx;
482
+ return ctx;
483
+ }
484
+ return ClientTransaction;
485
+ }
486
+ async function generateTransactionId(url, fetchFn, method) {
487
+ const parsedUrl = new URL(url);
488
+ const path = parsedUrl.pathname;
489
+ log$2(`Generating transaction ID for ${method} ${path}`);
490
+ const document = await handleXMigration(fetchFn);
491
+ const transaction = await clientTransaction().create(document);
492
+ const transactionId = await transaction.generateTransactionId(method, path);
493
+ log$2(`Transaction ID: ${transactionId}`);
494
+ return transactionId;
495
+ }
496
+
497
+ const log$1 = debug("twitter-scraper:api");
498
+ const bearerToken = "AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF";
499
+ async function jitter(maxMs) {
500
+ const jitter2 = Math.random() * maxMs;
501
+ await new Promise((resolve) => setTimeout(resolve, jitter2));
502
+ }
503
+ async function requestApi(url, auth, method = "GET", platform = new Platform(), headers = new Headers()) {
504
+ log$1(`Making ${method} request to ${url}`);
505
+ await auth.installTo(headers, url);
506
+ await platform.randomizeCiphers();
507
+ if (auth instanceof TwitterGuestAuth && auth.options?.experimental?.xClientTransactionId) {
508
+ const transactionId = await generateTransactionId(
509
+ url,
510
+ auth.fetch.bind(auth),
511
+ method
512
+ );
513
+ headers.set("x-client-transaction-id", transactionId);
514
+ }
515
+ let res;
516
+ do {
517
+ const fetchParameters = [
518
+ url,
519
+ {
520
+ method,
521
+ headers,
522
+ credentials: "include"
523
+ }
524
+ ];
525
+ try {
526
+ res = await auth.fetch(...fetchParameters);
527
+ } catch (err) {
528
+ if (!(err instanceof Error)) {
529
+ throw err;
530
+ }
531
+ return {
532
+ success: false,
533
+ err: new Error("Failed to perform request.")
534
+ };
535
+ }
536
+ await updateCookieJar(auth.cookieJar(), res.headers);
537
+ if (res.status === 429) {
538
+ log$1("Rate limit hit, waiting for retry...");
539
+ await auth.onRateLimit({
540
+ fetchParameters,
541
+ response: res
542
+ });
543
+ }
544
+ } while (res.status === 429);
545
+ if (!res.ok) {
546
+ return {
547
+ success: false,
548
+ err: await ApiError.fromResponse(res)
549
+ };
550
+ }
551
+ const value = await flexParseJson(res);
552
+ if (res.headers.get("x-rate-limit-incoming") == "0") {
553
+ auth.deleteToken();
554
+ return { success: true, value };
555
+ } else {
556
+ return { success: true, value };
557
+ }
558
+ }
559
+ async function flexParseJson(res) {
560
+ try {
561
+ return await res.json();
562
+ } catch {
563
+ log$1("Failed to parse response as JSON, trying text parse...");
564
+ const text = await res.text();
565
+ log$1("Response text:", text);
566
+ return JSON.parse(text);
567
+ }
568
+ }
569
+ function addApiFeatures(o) {
570
+ return {
571
+ ...o,
572
+ rweb_lists_timeline_redesign_enabled: true,
573
+ responsive_web_graphql_exclude_directive_enabled: true,
574
+ verified_phone_label_enabled: false,
575
+ creator_subscriptions_tweet_preview_api_enabled: true,
576
+ responsive_web_graphql_timeline_navigation_enabled: true,
577
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
578
+ tweetypie_unmention_optimization_enabled: true,
579
+ responsive_web_edit_tweet_api_enabled: true,
580
+ graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
581
+ view_counts_everywhere_api_enabled: true,
582
+ longform_notetweets_consumption_enabled: true,
583
+ tweet_awards_web_tipping_enabled: false,
584
+ freedom_of_speech_not_reach_fetch_enabled: true,
585
+ standardized_nudges_misinfo: true,
586
+ longform_notetweets_rich_text_read_enabled: true,
587
+ responsive_web_enhance_cards_enabled: false,
588
+ subscriptions_verification_info_enabled: true,
589
+ subscriptions_verification_info_reason_enabled: true,
590
+ subscriptions_verification_info_verified_since_enabled: true,
591
+ super_follow_badge_privacy_enabled: false,
592
+ super_follow_exclusive_tweet_notifications_enabled: false,
593
+ super_follow_tweet_api_enabled: false,
594
+ super_follow_user_api_enabled: false,
595
+ android_graphql_skip_api_media_color_palette: false,
596
+ creator_subscriptions_subscription_count_enabled: false,
597
+ blue_business_profile_image_shape_enabled: false,
598
+ unified_cards_ad_metadata_container_dynamic_card_content_query_enabled: false
599
+ };
600
+ }
601
+ function addApiParams(params, includeTweetReplies) {
602
+ params.set("include_profile_interstitial_type", "1");
603
+ params.set("include_blocking", "1");
604
+ params.set("include_blocked_by", "1");
605
+ params.set("include_followed_by", "1");
606
+ params.set("include_want_retweets", "1");
607
+ params.set("include_mute_edge", "1");
608
+ params.set("include_can_dm", "1");
609
+ params.set("include_can_media_tag", "1");
610
+ params.set("include_ext_has_nft_avatar", "1");
611
+ params.set("include_ext_is_blue_verified", "1");
612
+ params.set("include_ext_verified_type", "1");
613
+ params.set("skip_status", "1");
614
+ params.set("cards_platform", "Web-12");
615
+ params.set("include_cards", "1");
616
+ params.set("include_ext_alt_text", "true");
617
+ params.set("include_ext_limited_action_results", "false");
618
+ params.set("include_quote_count", "true");
619
+ params.set("include_reply_count", "1");
620
+ params.set("tweet_mode", "extended");
621
+ params.set("include_ext_collab_control", "true");
622
+ params.set("include_ext_views", "true");
623
+ params.set("include_entities", "true");
624
+ params.set("include_user_entities", "true");
625
+ params.set("include_ext_media_color", "true");
626
+ params.set("include_ext_media_availability", "true");
627
+ params.set("include_ext_sensitive_media_warning", "true");
628
+ params.set("include_ext_trusted_friends_metadata", "true");
629
+ params.set("send_error_codes", "true");
630
+ params.set("simple_quoted_tweet", "true");
631
+ params.set("include_tweet_replies", `${includeTweetReplies}`);
632
+ params.set(
633
+ "ext",
634
+ "mediaStats,highlightedLabel,hasNftAvatar,voiceInfo,birdwatchPivot,enrichments,superFollowMetadata,unmentionInfo,editControl,collab_control,vibe"
635
+ );
636
+ return params;
637
+ }
638
+
406
639
  const log = debug("twitter-scraper:auth-user");
407
640
  const TwitterUserAuthSubtask = Type.Object({
408
641
  subtask_id: Type.String(),
@@ -510,21 +743,25 @@ class TwitterUserAuth extends TwitterGuestAuth {
510
743
  this.jar = new CookieJar();
511
744
  }
512
745
  }
513
- async installCsrfToken(headers) {
514
- const cookies = await this.getCookies();
515
- const xCsrfToken = cookies.find((cookie) => cookie.key === "ct0");
516
- if (xCsrfToken) {
517
- headers.set("x-csrf-token", xCsrfToken.value);
518
- }
519
- }
520
746
  async installTo(headers) {
521
747
  headers.set("authorization", `Bearer ${this.bearerToken}`);
522
- const cookie = await this.getCookieString();
523
- headers.set("cookie", cookie);
748
+ headers.set(
749
+ "user-agent",
750
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
751
+ );
524
752
  if (this.guestToken) {
525
753
  headers.set("x-guest-token", this.guestToken);
526
754
  }
527
755
  await this.installCsrfToken(headers);
756
+ if (this.options?.experimental?.xpff) {
757
+ const guestId = await this.guestId();
758
+ if (guestId != null) {
759
+ const xpffHeader = await generateXPFFHeader(guestId);
760
+ headers.set("x-xp-forwarded-for", xpffHeader);
761
+ }
762
+ }
763
+ const cookie = await this.getCookieString();
764
+ headers.set("cookie", cookie);
528
765
  }
529
766
  async initLogin() {
530
767
  this.removeCookie("twitter_ads_id=");
@@ -729,12 +966,6 @@ class TwitterUserAuth extends TwitterGuestAuth {
729
966
  onboardingTaskUrl = `https://api.x.com/1.1/onboarding/task.json?flow_name=${data.flow_name}`;
730
967
  }
731
968
  log(`Making POST request to ${onboardingTaskUrl}`);
732
- const token = this.guestToken;
733
- if (token == null) {
734
- throw new AuthenticationError(
735
- "Authentication token is null or undefined."
736
- );
737
- }
738
969
  const headers = new Headers({
739
970
  accept: "*/*",
740
971
  "accept-language": "en-US,en;q=0.9",
@@ -751,12 +982,19 @@ class TwitterUserAuth extends TwitterGuestAuth {
751
982
  "sec-fetch-mode": "cors",
752
983
  "sec-fetch-site": "same-origin",
753
984
  "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
754
- "x-guest-token": token,
755
985
  "x-twitter-auth-type": "OAuth2Client",
756
986
  "x-twitter-active-user": "yes",
757
987
  "x-twitter-client-language": "en"
758
988
  });
759
989
  await this.installTo(headers);
990
+ if (this.options?.experimental?.xClientTransactionId) {
991
+ const transactionId = await generateTransactionId(
992
+ onboardingTaskUrl,
993
+ this.fetch.bind(this),
994
+ "POST"
995
+ );
996
+ headers.set("x-client-transaction-id", transactionId);
997
+ }
760
998
  let res;
761
999
  do {
762
1000
  const fetchParameters = [
@@ -791,7 +1029,7 @@ class TwitterUserAuth extends TwitterGuestAuth {
791
1029
  if (!res.ok) {
792
1030
  return { status: "error", err: await ApiError.fromResponse(res) };
793
1031
  }
794
- const flow = await res.json();
1032
+ const flow = await flexParseJson(res);
795
1033
  if (flow?.flow_token == null) {
796
1034
  return {
797
1035
  status: "error",
@@ -829,12 +1067,12 @@ class TwitterUserAuth extends TwitterGuestAuth {
829
1067
 
830
1068
  const endpoints = {
831
1069
  // TODO: Migrate other endpoint URLs here
832
- UserTweets: "https://api.x.com/graphql/VfoNveT-zJPGVZMPydZUfQ/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%22payments_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%3Afalse%2C%22responsive_web_jetfuel_frame%22%3Atrue%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%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%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_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticlePlainText%22%3Afalse%7D",
1070
+ UserTweets: "https://x.com/i/api/graphql/oRJs8SLCRNRbQzuZG93_oA/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%22payments_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%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%3Atrue%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%3Atrue%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_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticlePlainText%22%3Afalse%7D",
833
1071
  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",
834
1072
  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",
835
- UserByScreenName: "https://api.x.com/graphql/96tVxbPqMZDoYB5pmzezKA/UserByScreenName?variables=%7B%22screen_name%22%3A%22geminiapp%22%2C%22withGrokTranslatedBio%22%3Afalse%7D&features=%7B%22hidden_profile_subscriptions_enabled%22%3Atrue%2C%22payments_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%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",
836
- 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",
837
- TweetResultByRestId: "https://api.x.com/graphql/wqi5M7wZ7tW-X9S2t-Mqcg/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%3Atrue%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%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%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22payments_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%22responsive_web_grok_image_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_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&fieldToggles=%7B%22withArticleRichContentState%22%3Atrue%2C%22withArticlePlainText%22%3Afalse%2C%22withGrokAnalyze%22%3Afalse%2C%22withDisallowedReplyControls%22%3Afalse%7D",
1073
+ UserByScreenName: "https://x.com/i/api/graphql/ZHSN3WlvahPKVvUxVQbg1A/UserByScreenName?variables=%7B%22screen_name%22%3A%22geminiapp%22%2C%22withGrokTranslatedBio%22%3Atrue%7D&features=%7B%22hidden_profile_subscriptions_enabled%22%3Atrue%2C%22payments_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%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",
1074
+ TweetDetail: "https://x.com/i/api/graphql/YVyS4SfwYW7Uw5qwy0mQCA/TweetDetail?variables=%7B%22focalTweetId%22%3A%221985465713096794294%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%22payments_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%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%3Atrue%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%3Atrue%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_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_enabled%22%3Afalse%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",
1075
+ TweetResultByRestId: "https://api.x.com/graphql/tCVRZ3WCvoj0BVO7BKnL-Q/TweetResultByRestId?variables=%7B%22tweetId%22%3A%221985465713096794294%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%3Atrue%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%22payments_enabled%22%3Afalse%2C%22profile_label_improvements_pcf_label_in_post_enabled%22%3Atrue%2C%22responsive_web_profile_redirect_enabled%22%3Afalse%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_grok_imagine_annotation_enabled%22%3Atrue%2C%22responsive_web_grok_community_note_auto_translation_is_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&fieldToggles=%7B%22withArticleRichContentState%22%3Atrue%2C%22withArticlePlainText%22%3Afalse%2C%22withGrokAnalyze%22%3Afalse%2C%22withDisallowedReplyControls%22%3Afalse%7D",
838
1076
  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"
839
1077
  };
840
1078
  class ApiRequest {
@@ -2581,7 +2819,11 @@ class Scraper {
2581
2819
  return {
2582
2820
  fetch: this.options?.fetch,
2583
2821
  transform: this.options?.transform,
2584
- rateLimitStrategy: this.options?.rateLimitStrategy
2822
+ rateLimitStrategy: this.options?.rateLimitStrategy,
2823
+ experimental: {
2824
+ xClientTransactionId: this.options?.experimental?.xClientTransactionId,
2825
+ xpff: this.options?.experimental?.xpff
2826
+ }
2585
2827
  };
2586
2828
  }
2587
2829
  handleResponse(res) {