@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.
- package/README.md +3 -3
- package/dist/default/cjs/index.js +419 -177
- package/dist/default/cjs/index.js.map +1 -1
- package/dist/default/esm/index.mjs +419 -177
- package/dist/default/esm/index.mjs.map +1 -1
- package/dist/node/cjs/index.cjs +416 -174
- package/dist/node/cjs/index.cjs.map +1 -1
- package/dist/node/esm/index.mjs +416 -174
- package/dist/node/esm/index.mjs.map +1 -1
- package/dist/types/index.d.ts +13 -0
- package/examples/cycletls/README.md +4 -10
- package/examples/cycletls/package.json +1 -0
- package/examples/node-integration/package.json +2 -1
- package/package.json +6 -4
package/dist/node/cjs/index.cjs
CHANGED
|
@@ -72,13 +72,13 @@ class AuthenticationError extends Error {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const log$
|
|
75
|
+
const log$6 = debug("twitter-scraper:rate-limit");
|
|
76
76
|
class WaitingRateLimitStrategy {
|
|
77
77
|
async onRateLimit({ response: res }) {
|
|
78
78
|
const xRateLimitLimit = res.headers.get("x-rate-limit-limit");
|
|
79
79
|
const xRateLimitRemaining = res.headers.get("x-rate-limit-remaining");
|
|
80
80
|
const xRateLimitReset = res.headers.get("x-rate-limit-reset");
|
|
81
|
-
log$
|
|
81
|
+
log$6(
|
|
82
82
|
`Rate limit event: limit=${xRateLimitLimit}, remaining=${xRateLimitRemaining}, reset=${xRateLimitReset}`
|
|
83
83
|
);
|
|
84
84
|
if (xRateLimitRemaining == "0" && xRateLimitReset) {
|
|
@@ -94,20 +94,7 @@ class ErrorRateLimitStrategy {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
async randomizeCiphers() {
|
|
99
|
-
const platform = await Platform.importPlatform();
|
|
100
|
-
await platform?.randomizeCiphers();
|
|
101
|
-
}
|
|
102
|
-
static async importPlatform() {
|
|
103
|
-
{
|
|
104
|
-
const { platform } = await Promise.resolve().then(function () { return index; });
|
|
105
|
-
return platform;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const log$3 = debug("twitter-scraper:requests");
|
|
97
|
+
const log$5 = debug("twitter-scraper:requests");
|
|
111
98
|
async function updateCookieJar(cookieJar, headers) {
|
|
112
99
|
let setCookieHeaders = [];
|
|
113
100
|
if (typeof headers.getSetCookie === "function") {
|
|
@@ -122,12 +109,12 @@ async function updateCookieJar(cookieJar, headers) {
|
|
|
122
109
|
for (const cookieStr of setCookieHeaders) {
|
|
123
110
|
const cookie = toughCookie.Cookie.parse(cookieStr);
|
|
124
111
|
if (!cookie) {
|
|
125
|
-
log$
|
|
112
|
+
log$5(`Failed to parse cookie: ${cookieStr.substring(0, 100)}`);
|
|
126
113
|
continue;
|
|
127
114
|
}
|
|
128
115
|
if (cookie.maxAge === 0 || cookie.expires && cookie.expires < /* @__PURE__ */ new Date()) {
|
|
129
116
|
if (cookie.key === "ct0") {
|
|
130
|
-
log$
|
|
117
|
+
log$5(`Skipping deletion of ct0 cookie (Max-Age=0)`);
|
|
131
118
|
}
|
|
132
119
|
continue;
|
|
133
120
|
}
|
|
@@ -135,7 +122,7 @@ async function updateCookieJar(cookieJar, headers) {
|
|
|
135
122
|
const url = `${cookie.secure ? "https" : "http"}://${cookie.domain}${cookie.path}`;
|
|
136
123
|
await cookieJar.setCookie(cookie, url);
|
|
137
124
|
if (cookie.key === "ct0") {
|
|
138
|
-
log$
|
|
125
|
+
log$5(
|
|
139
126
|
`Successfully set ct0 cookie with value: ${cookie.value.substring(
|
|
140
127
|
0,
|
|
141
128
|
20
|
|
@@ -143,9 +130,9 @@ async function updateCookieJar(cookieJar, headers) {
|
|
|
143
130
|
);
|
|
144
131
|
}
|
|
145
132
|
} catch (err) {
|
|
146
|
-
log$
|
|
133
|
+
log$5(`Failed to set cookie ${cookie.key}: ${err}`);
|
|
147
134
|
if (cookie.key === "ct0") {
|
|
148
|
-
log$
|
|
135
|
+
log$5(`FAILED to set ct0 cookie! Error: ${err}`);
|
|
149
136
|
}
|
|
150
137
|
}
|
|
151
138
|
}
|
|
@@ -159,131 +146,84 @@ async function updateCookieJar(cookieJar, headers) {
|
|
|
159
146
|
}
|
|
160
147
|
}
|
|
161
148
|
|
|
162
|
-
const log$
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
149
|
+
const log$4 = debug("twitter-scraper:xpff");
|
|
150
|
+
let isoCrypto = null;
|
|
151
|
+
function getCrypto() {
|
|
152
|
+
if (isoCrypto != null) {
|
|
153
|
+
return isoCrypto;
|
|
154
|
+
}
|
|
155
|
+
if (typeof crypto === "undefined") {
|
|
156
|
+
log$4("Global crypto is undefined, importing from crypto module...");
|
|
157
|
+
const { webcrypto } = require("crypto");
|
|
158
|
+
isoCrypto = webcrypto;
|
|
159
|
+
return webcrypto;
|
|
160
|
+
}
|
|
161
|
+
isoCrypto = crypto;
|
|
162
|
+
return crypto;
|
|
163
|
+
}
|
|
164
|
+
async function sha256(message) {
|
|
165
|
+
const msgBuffer = new TextEncoder().encode(message);
|
|
166
|
+
const hashBuffer = await getCrypto().subtle.digest("SHA-256", msgBuffer);
|
|
167
|
+
return new Uint8Array(hashBuffer);
|
|
168
|
+
}
|
|
169
|
+
function buf2hex(buffer) {
|
|
170
|
+
return [...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
171
|
+
}
|
|
172
|
+
class XPFFHeaderGenerator {
|
|
173
|
+
constructor(seed) {
|
|
174
|
+
this.seed = seed;
|
|
175
|
+
}
|
|
176
|
+
async deriveKey(guestId) {
|
|
177
|
+
const combined = `${this.seed}${guestId}`;
|
|
178
|
+
const result = await sha256(combined);
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
async generateHeader(plaintext, guestId) {
|
|
182
|
+
log$4(`Generating XPFF key for guest ID: ${guestId}`);
|
|
183
|
+
const key = await this.deriveKey(guestId);
|
|
184
|
+
const nonce = getCrypto().getRandomValues(new Uint8Array(12));
|
|
185
|
+
const cipher = await getCrypto().subtle.importKey(
|
|
186
|
+
"raw",
|
|
187
|
+
key,
|
|
188
|
+
{ name: "AES-GCM" },
|
|
189
|
+
false,
|
|
190
|
+
["encrypt"]
|
|
191
|
+
);
|
|
192
|
+
const encrypted = await getCrypto().subtle.encrypt(
|
|
176
193
|
{
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
if (!res.ok) {
|
|
203
|
-
return {
|
|
204
|
-
success: false,
|
|
205
|
-
err: await ApiError.fromResponse(res)
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
const value = await res.json();
|
|
209
|
-
if (res.headers.get("x-rate-limit-incoming") == "0") {
|
|
210
|
-
auth.deleteToken();
|
|
211
|
-
return { success: true, value };
|
|
212
|
-
} else {
|
|
213
|
-
return { success: true, value };
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
function addApiFeatures(o) {
|
|
217
|
-
return {
|
|
218
|
-
...o,
|
|
219
|
-
rweb_lists_timeline_redesign_enabled: true,
|
|
220
|
-
responsive_web_graphql_exclude_directive_enabled: true,
|
|
221
|
-
verified_phone_label_enabled: false,
|
|
222
|
-
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
223
|
-
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
224
|
-
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
225
|
-
tweetypie_unmention_optimization_enabled: true,
|
|
226
|
-
responsive_web_edit_tweet_api_enabled: true,
|
|
227
|
-
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
|
228
|
-
view_counts_everywhere_api_enabled: true,
|
|
229
|
-
longform_notetweets_consumption_enabled: true,
|
|
230
|
-
tweet_awards_web_tipping_enabled: false,
|
|
231
|
-
freedom_of_speech_not_reach_fetch_enabled: true,
|
|
232
|
-
standardized_nudges_misinfo: true,
|
|
233
|
-
longform_notetweets_rich_text_read_enabled: true,
|
|
234
|
-
responsive_web_enhance_cards_enabled: false,
|
|
235
|
-
subscriptions_verification_info_enabled: true,
|
|
236
|
-
subscriptions_verification_info_reason_enabled: true,
|
|
237
|
-
subscriptions_verification_info_verified_since_enabled: true,
|
|
238
|
-
super_follow_badge_privacy_enabled: false,
|
|
239
|
-
super_follow_exclusive_tweet_notifications_enabled: false,
|
|
240
|
-
super_follow_tweet_api_enabled: false,
|
|
241
|
-
super_follow_user_api_enabled: false,
|
|
242
|
-
android_graphql_skip_api_media_color_palette: false,
|
|
243
|
-
creator_subscriptions_subscription_count_enabled: false,
|
|
244
|
-
blue_business_profile_image_shape_enabled: false,
|
|
245
|
-
unified_cards_ad_metadata_container_dynamic_card_content_query_enabled: false
|
|
246
|
-
};
|
|
194
|
+
name: "AES-GCM",
|
|
195
|
+
iv: nonce
|
|
196
|
+
},
|
|
197
|
+
cipher,
|
|
198
|
+
new TextEncoder().encode(plaintext)
|
|
199
|
+
);
|
|
200
|
+
const combined = new Uint8Array(nonce.length + encrypted.byteLength);
|
|
201
|
+
combined.set(nonce);
|
|
202
|
+
combined.set(new Uint8Array(encrypted), nonce.length);
|
|
203
|
+
const result = buf2hex(combined);
|
|
204
|
+
log$4(`XPFF header generated for guest ID ${guestId}: ${result}`);
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const xpffBaseKey = "0e6be1f1e21ffc33590b888fd4dc81b19713e570e805d4e5df80a493c9571a05";
|
|
209
|
+
function xpffPlain() {
|
|
210
|
+
const timestamp = Date.now();
|
|
211
|
+
return JSON.stringify({
|
|
212
|
+
navigator_properties: {
|
|
213
|
+
hasBeenActive: "true",
|
|
214
|
+
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
|
215
|
+
webdriver: "false"
|
|
216
|
+
},
|
|
217
|
+
created_at: timestamp
|
|
218
|
+
});
|
|
247
219
|
}
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
params.set("include_followed_by", "1");
|
|
253
|
-
params.set("include_want_retweets", "1");
|
|
254
|
-
params.set("include_mute_edge", "1");
|
|
255
|
-
params.set("include_can_dm", "1");
|
|
256
|
-
params.set("include_can_media_tag", "1");
|
|
257
|
-
params.set("include_ext_has_nft_avatar", "1");
|
|
258
|
-
params.set("include_ext_is_blue_verified", "1");
|
|
259
|
-
params.set("include_ext_verified_type", "1");
|
|
260
|
-
params.set("skip_status", "1");
|
|
261
|
-
params.set("cards_platform", "Web-12");
|
|
262
|
-
params.set("include_cards", "1");
|
|
263
|
-
params.set("include_ext_alt_text", "true");
|
|
264
|
-
params.set("include_ext_limited_action_results", "false");
|
|
265
|
-
params.set("include_quote_count", "true");
|
|
266
|
-
params.set("include_reply_count", "1");
|
|
267
|
-
params.set("tweet_mode", "extended");
|
|
268
|
-
params.set("include_ext_collab_control", "true");
|
|
269
|
-
params.set("include_ext_views", "true");
|
|
270
|
-
params.set("include_entities", "true");
|
|
271
|
-
params.set("include_user_entities", "true");
|
|
272
|
-
params.set("include_ext_media_color", "true");
|
|
273
|
-
params.set("include_ext_media_availability", "true");
|
|
274
|
-
params.set("include_ext_sensitive_media_warning", "true");
|
|
275
|
-
params.set("include_ext_trusted_friends_metadata", "true");
|
|
276
|
-
params.set("send_error_codes", "true");
|
|
277
|
-
params.set("simple_quoted_tweet", "true");
|
|
278
|
-
params.set("include_tweet_replies", `${includeTweetReplies}`);
|
|
279
|
-
params.set(
|
|
280
|
-
"ext",
|
|
281
|
-
"mediaStats,highlightedLabel,hasNftAvatar,voiceInfo,birdwatchPivot,enrichments,superFollowMetadata,unmentionInfo,editControl,collab_control,vibe"
|
|
282
|
-
);
|
|
283
|
-
return params;
|
|
220
|
+
async function generateXPFFHeader(guestId) {
|
|
221
|
+
const generator = new XPFFHeaderGenerator(xpffBaseKey);
|
|
222
|
+
const plaintext = xpffPlain();
|
|
223
|
+
return generator.generateHeader(plaintext, guestId);
|
|
284
224
|
}
|
|
285
225
|
|
|
286
|
-
const log$
|
|
226
|
+
const log$3 = debug("twitter-scraper:auth");
|
|
287
227
|
function withTransform(fetchFn, transform) {
|
|
288
228
|
return async (input, init) => {
|
|
289
229
|
const fetchArgs = await transform?.request?.(input, init) ?? [
|
|
@@ -337,20 +277,30 @@ class TwitterGuestAuth {
|
|
|
337
277
|
if (this.shouldUpdate()) {
|
|
338
278
|
await this.updateGuestToken();
|
|
339
279
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
throw new AuthenticationError(
|
|
343
|
-
"Authentication token is null or undefined."
|
|
344
|
-
);
|
|
280
|
+
if (this.guestToken) {
|
|
281
|
+
headers.set("x-guest-token", this.guestToken);
|
|
345
282
|
}
|
|
346
283
|
headers.set("authorization", `Bearer ${this.bearerToken}`);
|
|
347
|
-
headers.set(
|
|
284
|
+
headers.set(
|
|
285
|
+
"user-agent",
|
|
286
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
|
287
|
+
);
|
|
288
|
+
await this.installCsrfToken(headers);
|
|
289
|
+
if (this.options?.experimental?.xpff) {
|
|
290
|
+
const guestId = await this.guestId();
|
|
291
|
+
if (guestId != null) {
|
|
292
|
+
const xpffHeader = await generateXPFFHeader(guestId);
|
|
293
|
+
headers.set("x-xp-forwarded-for", xpffHeader);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
headers.set("cookie", await this.getCookieString());
|
|
297
|
+
}
|
|
298
|
+
async installCsrfToken(headers) {
|
|
348
299
|
const cookies = await this.getCookies();
|
|
349
300
|
const xCsrfToken = cookies.find((cookie) => cookie.key === "ct0");
|
|
350
301
|
if (xCsrfToken) {
|
|
351
302
|
headers.set("x-csrf-token", xCsrfToken.value);
|
|
352
303
|
}
|
|
353
|
-
headers.set("cookie", await this.getCookieString());
|
|
354
304
|
}
|
|
355
305
|
async setCookie(key, value) {
|
|
356
306
|
const cookie = toughCookie.Cookie.parse(`${key}=${value}`);
|
|
@@ -383,16 +333,28 @@ class TwitterGuestAuth {
|
|
|
383
333
|
getCookieJarUrl() {
|
|
384
334
|
return typeof document !== "undefined" ? document.location.toString() : "https://x.com";
|
|
385
335
|
}
|
|
336
|
+
async guestId() {
|
|
337
|
+
const cookies = await this.getCookies();
|
|
338
|
+
const guestIdCookie = cookies.find((cookie) => cookie.key === "guest_id");
|
|
339
|
+
return guestIdCookie ? guestIdCookie.value : null;
|
|
340
|
+
}
|
|
386
341
|
/**
|
|
387
342
|
* Updates the authentication state with a new guest token from the Twitter API.
|
|
388
343
|
*/
|
|
389
344
|
async updateGuestToken() {
|
|
345
|
+
try {
|
|
346
|
+
await this.updateGuestTokenCore();
|
|
347
|
+
} catch (err) {
|
|
348
|
+
log$3("Failed to update guest token; this may cause issues:", err);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async updateGuestTokenCore() {
|
|
390
352
|
const guestActivateUrl = "https://api.x.com/1.1/guest/activate.json";
|
|
391
353
|
const headers = new headersPolyfill.Headers({
|
|
392
354
|
Authorization: `Bearer ${this.bearerToken}`,
|
|
393
355
|
Cookie: await this.getCookieString()
|
|
394
356
|
});
|
|
395
|
-
log$
|
|
357
|
+
log$3(`Making POST request to ${guestActivateUrl}`);
|
|
396
358
|
const res = await this.fetch(guestActivateUrl, {
|
|
397
359
|
method: "POST",
|
|
398
360
|
headers,
|
|
@@ -402,7 +364,7 @@ class TwitterGuestAuth {
|
|
|
402
364
|
if (!res.ok) {
|
|
403
365
|
throw new AuthenticationError(await res.text());
|
|
404
366
|
}
|
|
405
|
-
const o = await res
|
|
367
|
+
const o = await flexParseJson(res);
|
|
406
368
|
if (o == null || o["guest_token"] == null) {
|
|
407
369
|
throw new AuthenticationError("guest_token not found.");
|
|
408
370
|
}
|
|
@@ -413,7 +375,7 @@ class TwitterGuestAuth {
|
|
|
413
375
|
this.guestToken = newGuestToken;
|
|
414
376
|
this.guestCreatedAt = /* @__PURE__ */ new Date();
|
|
415
377
|
await this.setCookie("gt", newGuestToken);
|
|
416
|
-
log$
|
|
378
|
+
log$3(`Updated guest token: ${newGuestToken}`);
|
|
417
379
|
}
|
|
418
380
|
/**
|
|
419
381
|
* Returns if the authentication token needs to be updated or not.
|
|
@@ -424,6 +386,277 @@ class TwitterGuestAuth {
|
|
|
424
386
|
}
|
|
425
387
|
}
|
|
426
388
|
|
|
389
|
+
class Platform {
|
|
390
|
+
async randomizeCiphers() {
|
|
391
|
+
const platform = await Platform.importPlatform();
|
|
392
|
+
await platform?.randomizeCiphers();
|
|
393
|
+
}
|
|
394
|
+
static async importPlatform() {
|
|
395
|
+
{
|
|
396
|
+
const { platform } = await Promise.resolve().then(function () { return index; });
|
|
397
|
+
return platform;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const log$2 = debug("twitter-scraper:xctxid");
|
|
403
|
+
let linkedom = null;
|
|
404
|
+
function linkedomImport() {
|
|
405
|
+
if (!linkedom) {
|
|
406
|
+
const mod = require("linkedom");
|
|
407
|
+
linkedom = mod;
|
|
408
|
+
return mod;
|
|
409
|
+
}
|
|
410
|
+
return linkedom;
|
|
411
|
+
}
|
|
412
|
+
async function parseHTML(html) {
|
|
413
|
+
if (typeof window !== "undefined") {
|
|
414
|
+
const { defaultView } = new DOMParser().parseFromString(html, "text/html");
|
|
415
|
+
if (!defaultView) {
|
|
416
|
+
throw new Error("Failed to get defaultView from parsed HTML.");
|
|
417
|
+
}
|
|
418
|
+
return defaultView;
|
|
419
|
+
} else {
|
|
420
|
+
const { DOMParser: DOMParser2 } = linkedomImport();
|
|
421
|
+
return new DOMParser2().parseFromString(html, "text/html").defaultView;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async function handleXMigration(fetchFn) {
|
|
425
|
+
const headers = {
|
|
426
|
+
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",
|
|
427
|
+
"accept-language": "ja",
|
|
428
|
+
"cache-control": "no-cache",
|
|
429
|
+
pragma: "no-cache",
|
|
430
|
+
priority: "u=0, i",
|
|
431
|
+
"sec-ch-ua": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
|
|
432
|
+
"sec-ch-ua-mobile": "?0",
|
|
433
|
+
"sec-ch-ua-platform": '"Windows"',
|
|
434
|
+
"sec-fetch-dest": "document",
|
|
435
|
+
"sec-fetch-mode": "navigate",
|
|
436
|
+
"sec-fetch-site": "none",
|
|
437
|
+
"sec-fetch-user": "?1",
|
|
438
|
+
"upgrade-insecure-requests": "1",
|
|
439
|
+
"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"
|
|
440
|
+
};
|
|
441
|
+
const response = await fetchFn("https://x.com", {
|
|
442
|
+
headers
|
|
443
|
+
});
|
|
444
|
+
if (!response.ok) {
|
|
445
|
+
throw new Error(`Failed to fetch X homepage: ${response.statusText}`);
|
|
446
|
+
}
|
|
447
|
+
const htmlText = await response.text();
|
|
448
|
+
let dom = await parseHTML(htmlText);
|
|
449
|
+
let document = dom.window.document;
|
|
450
|
+
const migrationRedirectionRegex = new RegExp(
|
|
451
|
+
"(http(?:s)?://(?:www\\.)?(twitter|x){1}\\.com(/x)?/migrate([/?])?tok=[a-zA-Z0-9%\\-_]+)+",
|
|
452
|
+
"i"
|
|
453
|
+
);
|
|
454
|
+
const metaRefresh = document.querySelector("meta[http-equiv='refresh']");
|
|
455
|
+
const metaContent = metaRefresh ? metaRefresh.getAttribute("content") || "" : "";
|
|
456
|
+
const migrationRedirectionUrl = migrationRedirectionRegex.exec(metaContent) || migrationRedirectionRegex.exec(htmlText);
|
|
457
|
+
if (migrationRedirectionUrl) {
|
|
458
|
+
const redirectResponse = await fetch(migrationRedirectionUrl[0]);
|
|
459
|
+
if (!redirectResponse.ok) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
`Failed to follow migration redirection: ${redirectResponse.statusText}`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
const redirectHtml = await redirectResponse.text();
|
|
465
|
+
dom = await parseHTML(redirectHtml);
|
|
466
|
+
document = dom.window.document;
|
|
467
|
+
}
|
|
468
|
+
const migrationForm = document.querySelector("form[name='f']") || document.querySelector("form[action='https://x.com/x/migrate']");
|
|
469
|
+
if (migrationForm) {
|
|
470
|
+
const url = migrationForm.getAttribute("action") || "https://x.com/x/migrate";
|
|
471
|
+
const method = migrationForm.getAttribute("method") || "POST";
|
|
472
|
+
const requestPayload = new FormData();
|
|
473
|
+
const inputFields = migrationForm.querySelectorAll("input");
|
|
474
|
+
for (const element of Array.from(inputFields)) {
|
|
475
|
+
const name = element.getAttribute("name");
|
|
476
|
+
const value = element.getAttribute("value");
|
|
477
|
+
if (name && value) {
|
|
478
|
+
requestPayload.append(name, value);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const formResponse = await fetch(url, {
|
|
482
|
+
method,
|
|
483
|
+
body: requestPayload,
|
|
484
|
+
headers
|
|
485
|
+
});
|
|
486
|
+
if (!formResponse.ok) {
|
|
487
|
+
throw new Error(
|
|
488
|
+
`Failed to submit migration form: ${formResponse.statusText}`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
const formHtml = await formResponse.text();
|
|
492
|
+
dom = await parseHTML(formHtml);
|
|
493
|
+
document = dom.window.document;
|
|
494
|
+
}
|
|
495
|
+
return document;
|
|
496
|
+
}
|
|
497
|
+
let ClientTransaction = null;
|
|
498
|
+
function clientTransaction() {
|
|
499
|
+
if (!ClientTransaction) {
|
|
500
|
+
const mod = require("x-client-transaction-id");
|
|
501
|
+
const ctx = mod.ClientTransaction;
|
|
502
|
+
ClientTransaction = ctx;
|
|
503
|
+
return ctx;
|
|
504
|
+
}
|
|
505
|
+
return ClientTransaction;
|
|
506
|
+
}
|
|
507
|
+
async function generateTransactionId(url, fetchFn, method) {
|
|
508
|
+
const parsedUrl = new URL(url);
|
|
509
|
+
const path = parsedUrl.pathname;
|
|
510
|
+
log$2(`Generating transaction ID for ${method} ${path}`);
|
|
511
|
+
const document = await handleXMigration(fetchFn);
|
|
512
|
+
const transaction = await clientTransaction().create(document);
|
|
513
|
+
const transactionId = await transaction.generateTransactionId(method, path);
|
|
514
|
+
log$2(`Transaction ID: ${transactionId}`);
|
|
515
|
+
return transactionId;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const log$1 = debug("twitter-scraper:api");
|
|
519
|
+
const bearerToken = "AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF";
|
|
520
|
+
async function jitter(maxMs) {
|
|
521
|
+
const jitter2 = Math.random() * maxMs;
|
|
522
|
+
await new Promise((resolve) => setTimeout(resolve, jitter2));
|
|
523
|
+
}
|
|
524
|
+
async function requestApi(url, auth, method = "GET", platform = new Platform(), headers = new headersPolyfill.Headers()) {
|
|
525
|
+
log$1(`Making ${method} request to ${url}`);
|
|
526
|
+
await auth.installTo(headers, url);
|
|
527
|
+
await platform.randomizeCiphers();
|
|
528
|
+
if (auth instanceof TwitterGuestAuth && auth.options?.experimental?.xClientTransactionId) {
|
|
529
|
+
const transactionId = await generateTransactionId(
|
|
530
|
+
url,
|
|
531
|
+
auth.fetch.bind(auth),
|
|
532
|
+
method
|
|
533
|
+
);
|
|
534
|
+
headers.set("x-client-transaction-id", transactionId);
|
|
535
|
+
}
|
|
536
|
+
let res;
|
|
537
|
+
do {
|
|
538
|
+
const fetchParameters = [
|
|
539
|
+
url,
|
|
540
|
+
{
|
|
541
|
+
method,
|
|
542
|
+
headers,
|
|
543
|
+
credentials: "include"
|
|
544
|
+
}
|
|
545
|
+
];
|
|
546
|
+
try {
|
|
547
|
+
res = await auth.fetch(...fetchParameters);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
if (!(err instanceof Error)) {
|
|
550
|
+
throw err;
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
success: false,
|
|
554
|
+
err: new Error("Failed to perform request.")
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
await updateCookieJar(auth.cookieJar(), res.headers);
|
|
558
|
+
if (res.status === 429) {
|
|
559
|
+
log$1("Rate limit hit, waiting for retry...");
|
|
560
|
+
await auth.onRateLimit({
|
|
561
|
+
fetchParameters,
|
|
562
|
+
response: res
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
} while (res.status === 429);
|
|
566
|
+
if (!res.ok) {
|
|
567
|
+
return {
|
|
568
|
+
success: false,
|
|
569
|
+
err: await ApiError.fromResponse(res)
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
const value = await flexParseJson(res);
|
|
573
|
+
if (res.headers.get("x-rate-limit-incoming") == "0") {
|
|
574
|
+
auth.deleteToken();
|
|
575
|
+
return { success: true, value };
|
|
576
|
+
} else {
|
|
577
|
+
return { success: true, value };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function flexParseJson(res) {
|
|
581
|
+
try {
|
|
582
|
+
return await res.json();
|
|
583
|
+
} catch {
|
|
584
|
+
log$1("Failed to parse response as JSON, trying text parse...");
|
|
585
|
+
const text = await res.text();
|
|
586
|
+
log$1("Response text:", text);
|
|
587
|
+
return JSON.parse(text);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function addApiFeatures(o) {
|
|
591
|
+
return {
|
|
592
|
+
...o,
|
|
593
|
+
rweb_lists_timeline_redesign_enabled: true,
|
|
594
|
+
responsive_web_graphql_exclude_directive_enabled: true,
|
|
595
|
+
verified_phone_label_enabled: false,
|
|
596
|
+
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
597
|
+
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
598
|
+
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
599
|
+
tweetypie_unmention_optimization_enabled: true,
|
|
600
|
+
responsive_web_edit_tweet_api_enabled: true,
|
|
601
|
+
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
|
602
|
+
view_counts_everywhere_api_enabled: true,
|
|
603
|
+
longform_notetweets_consumption_enabled: true,
|
|
604
|
+
tweet_awards_web_tipping_enabled: false,
|
|
605
|
+
freedom_of_speech_not_reach_fetch_enabled: true,
|
|
606
|
+
standardized_nudges_misinfo: true,
|
|
607
|
+
longform_notetweets_rich_text_read_enabled: true,
|
|
608
|
+
responsive_web_enhance_cards_enabled: false,
|
|
609
|
+
subscriptions_verification_info_enabled: true,
|
|
610
|
+
subscriptions_verification_info_reason_enabled: true,
|
|
611
|
+
subscriptions_verification_info_verified_since_enabled: true,
|
|
612
|
+
super_follow_badge_privacy_enabled: false,
|
|
613
|
+
super_follow_exclusive_tweet_notifications_enabled: false,
|
|
614
|
+
super_follow_tweet_api_enabled: false,
|
|
615
|
+
super_follow_user_api_enabled: false,
|
|
616
|
+
android_graphql_skip_api_media_color_palette: false,
|
|
617
|
+
creator_subscriptions_subscription_count_enabled: false,
|
|
618
|
+
blue_business_profile_image_shape_enabled: false,
|
|
619
|
+
unified_cards_ad_metadata_container_dynamic_card_content_query_enabled: false
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
function addApiParams(params, includeTweetReplies) {
|
|
623
|
+
params.set("include_profile_interstitial_type", "1");
|
|
624
|
+
params.set("include_blocking", "1");
|
|
625
|
+
params.set("include_blocked_by", "1");
|
|
626
|
+
params.set("include_followed_by", "1");
|
|
627
|
+
params.set("include_want_retweets", "1");
|
|
628
|
+
params.set("include_mute_edge", "1");
|
|
629
|
+
params.set("include_can_dm", "1");
|
|
630
|
+
params.set("include_can_media_tag", "1");
|
|
631
|
+
params.set("include_ext_has_nft_avatar", "1");
|
|
632
|
+
params.set("include_ext_is_blue_verified", "1");
|
|
633
|
+
params.set("include_ext_verified_type", "1");
|
|
634
|
+
params.set("skip_status", "1");
|
|
635
|
+
params.set("cards_platform", "Web-12");
|
|
636
|
+
params.set("include_cards", "1");
|
|
637
|
+
params.set("include_ext_alt_text", "true");
|
|
638
|
+
params.set("include_ext_limited_action_results", "false");
|
|
639
|
+
params.set("include_quote_count", "true");
|
|
640
|
+
params.set("include_reply_count", "1");
|
|
641
|
+
params.set("tweet_mode", "extended");
|
|
642
|
+
params.set("include_ext_collab_control", "true");
|
|
643
|
+
params.set("include_ext_views", "true");
|
|
644
|
+
params.set("include_entities", "true");
|
|
645
|
+
params.set("include_user_entities", "true");
|
|
646
|
+
params.set("include_ext_media_color", "true");
|
|
647
|
+
params.set("include_ext_media_availability", "true");
|
|
648
|
+
params.set("include_ext_sensitive_media_warning", "true");
|
|
649
|
+
params.set("include_ext_trusted_friends_metadata", "true");
|
|
650
|
+
params.set("send_error_codes", "true");
|
|
651
|
+
params.set("simple_quoted_tweet", "true");
|
|
652
|
+
params.set("include_tweet_replies", `${includeTweetReplies}`);
|
|
653
|
+
params.set(
|
|
654
|
+
"ext",
|
|
655
|
+
"mediaStats,highlightedLabel,hasNftAvatar,voiceInfo,birdwatchPivot,enrichments,superFollowMetadata,unmentionInfo,editControl,collab_control,vibe"
|
|
656
|
+
);
|
|
657
|
+
return params;
|
|
658
|
+
}
|
|
659
|
+
|
|
427
660
|
const log = debug("twitter-scraper:auth-user");
|
|
428
661
|
const TwitterUserAuthSubtask = typebox.Type.Object({
|
|
429
662
|
subtask_id: typebox.Type.String(),
|
|
@@ -531,21 +764,25 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
531
764
|
this.jar = new toughCookie.CookieJar();
|
|
532
765
|
}
|
|
533
766
|
}
|
|
534
|
-
async installCsrfToken(headers) {
|
|
535
|
-
const cookies = await this.getCookies();
|
|
536
|
-
const xCsrfToken = cookies.find((cookie) => cookie.key === "ct0");
|
|
537
|
-
if (xCsrfToken) {
|
|
538
|
-
headers.set("x-csrf-token", xCsrfToken.value);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
767
|
async installTo(headers) {
|
|
542
768
|
headers.set("authorization", `Bearer ${this.bearerToken}`);
|
|
543
|
-
|
|
544
|
-
|
|
769
|
+
headers.set(
|
|
770
|
+
"user-agent",
|
|
771
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
|
772
|
+
);
|
|
545
773
|
if (this.guestToken) {
|
|
546
774
|
headers.set("x-guest-token", this.guestToken);
|
|
547
775
|
}
|
|
548
776
|
await this.installCsrfToken(headers);
|
|
777
|
+
if (this.options?.experimental?.xpff) {
|
|
778
|
+
const guestId = await this.guestId();
|
|
779
|
+
if (guestId != null) {
|
|
780
|
+
const xpffHeader = await generateXPFFHeader(guestId);
|
|
781
|
+
headers.set("x-xp-forwarded-for", xpffHeader);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const cookie = await this.getCookieString();
|
|
785
|
+
headers.set("cookie", cookie);
|
|
549
786
|
}
|
|
550
787
|
async initLogin() {
|
|
551
788
|
this.removeCookie("twitter_ads_id=");
|
|
@@ -750,12 +987,6 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
750
987
|
onboardingTaskUrl = `https://api.x.com/1.1/onboarding/task.json?flow_name=${data.flow_name}`;
|
|
751
988
|
}
|
|
752
989
|
log(`Making POST request to ${onboardingTaskUrl}`);
|
|
753
|
-
const token = this.guestToken;
|
|
754
|
-
if (token == null) {
|
|
755
|
-
throw new AuthenticationError(
|
|
756
|
-
"Authentication token is null or undefined."
|
|
757
|
-
);
|
|
758
|
-
}
|
|
759
990
|
const headers = new headersPolyfill.Headers({
|
|
760
991
|
accept: "*/*",
|
|
761
992
|
"accept-language": "en-US,en;q=0.9",
|
|
@@ -772,12 +1003,19 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
772
1003
|
"sec-fetch-mode": "cors",
|
|
773
1004
|
"sec-fetch-site": "same-origin",
|
|
774
1005
|
"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",
|
|
775
|
-
"x-guest-token": token,
|
|
776
1006
|
"x-twitter-auth-type": "OAuth2Client",
|
|
777
1007
|
"x-twitter-active-user": "yes",
|
|
778
1008
|
"x-twitter-client-language": "en"
|
|
779
1009
|
});
|
|
780
1010
|
await this.installTo(headers);
|
|
1011
|
+
if (this.options?.experimental?.xClientTransactionId) {
|
|
1012
|
+
const transactionId = await generateTransactionId(
|
|
1013
|
+
onboardingTaskUrl,
|
|
1014
|
+
this.fetch.bind(this),
|
|
1015
|
+
"POST"
|
|
1016
|
+
);
|
|
1017
|
+
headers.set("x-client-transaction-id", transactionId);
|
|
1018
|
+
}
|
|
781
1019
|
let res;
|
|
782
1020
|
do {
|
|
783
1021
|
const fetchParameters = [
|
|
@@ -812,7 +1050,7 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
812
1050
|
if (!res.ok) {
|
|
813
1051
|
return { status: "error", err: await ApiError.fromResponse(res) };
|
|
814
1052
|
}
|
|
815
|
-
const flow = await res
|
|
1053
|
+
const flow = await flexParseJson(res);
|
|
816
1054
|
if (flow?.flow_token == null) {
|
|
817
1055
|
return {
|
|
818
1056
|
status: "error",
|
|
@@ -850,12 +1088,12 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
850
1088
|
|
|
851
1089
|
const endpoints = {
|
|
852
1090
|
// TODO: Migrate other endpoint URLs here
|
|
853
|
-
UserTweets: "https://
|
|
1091
|
+
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",
|
|
854
1092
|
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",
|
|
855
1093
|
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",
|
|
856
|
-
UserByScreenName: "https://
|
|
857
|
-
TweetDetail: "https://x.com/i/api/graphql/
|
|
858
|
-
TweetResultByRestId: "https://api.x.com/graphql/
|
|
1094
|
+
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",
|
|
1095
|
+
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",
|
|
1096
|
+
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",
|
|
859
1097
|
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"
|
|
860
1098
|
};
|
|
861
1099
|
class ApiRequest {
|
|
@@ -2602,7 +2840,11 @@ class Scraper {
|
|
|
2602
2840
|
return {
|
|
2603
2841
|
fetch: this.options?.fetch,
|
|
2604
2842
|
transform: this.options?.transform,
|
|
2605
|
-
rateLimitStrategy: this.options?.rateLimitStrategy
|
|
2843
|
+
rateLimitStrategy: this.options?.rateLimitStrategy,
|
|
2844
|
+
experimental: {
|
|
2845
|
+
xClientTransactionId: this.options?.experimental?.xClientTransactionId,
|
|
2846
|
+
xpff: this.options?.experimental?.xpff
|
|
2847
|
+
}
|
|
2606
2848
|
};
|
|
2607
2849
|
}
|
|
2608
2850
|
handleResponse(res) {
|