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