@the-convocation/twitter-scraper 0.18.3 → 0.19.1
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 +35 -0
- package/dist/cycletls/cjs/index.cjs +99 -0
- package/dist/cycletls/cjs/index.cjs.map +1 -0
- package/dist/cycletls/esm/index.mjs +96 -0
- package/dist/cycletls/esm/index.mjs.map +1 -0
- package/dist/cycletls/index.d.ts +11 -0
- package/dist/default/cjs/index.js +100 -25
- package/dist/default/cjs/index.js.map +1 -1
- package/dist/default/esm/index.mjs +100 -25
- package/dist/default/esm/index.mjs.map +1 -1
- package/dist/node/cjs/index.cjs +100 -25
- package/dist/node/cjs/index.cjs.map +1 -1
- package/dist/node/esm/index.mjs +100 -25
- package/dist/node/esm/index.mjs.map +1 -1
- package/examples/cycletls/README.md +48 -0
- package/examples/cycletls/package.json +13 -0
- package/package.json +25 -9
- package/rollup.config.mjs +34 -0
package/README.md
CHANGED
|
@@ -168,6 +168,41 @@ const scraper = new Scraper({
|
|
|
168
168
|
});
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
+
### Bypassing Cloudflare bot detection
|
|
172
|
+
|
|
173
|
+
In some cases, Twitter's authentication endpoints may be protected by Cloudflare's advanced bot detection, resulting in `403 Forbidden` errors during login. This typically happens because standard Node.js TLS fingerprints are detected as non-browser clients.
|
|
174
|
+
|
|
175
|
+
To bypass this protection, you can use the optional CycleTLS `fetch` integration to mimic Chrome browser TLS fingerprints:
|
|
176
|
+
|
|
177
|
+
**Installation:**
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
npm install cycletls
|
|
181
|
+
# or
|
|
182
|
+
yarn add cycletls
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Usage:**
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { Scraper } from '@the-convocation/twitter-scraper';
|
|
189
|
+
import { cycleTLSFetch, cycleTLSExit } from '@the-convocation/twitter-scraper/cycletls';
|
|
190
|
+
|
|
191
|
+
const scraper = new Scraper({
|
|
192
|
+
fetch: cycleTLSFetch,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Use the scraper normally
|
|
196
|
+
await scraper.login(username, password, email);
|
|
197
|
+
|
|
198
|
+
// Important: cleanup CycleTLS resources when done
|
|
199
|
+
cycleTLSExit();
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Note:** The `/cycletls` entrypoint is Node.js only and will not work in browser environments. It's provided as a separate optional entrypoint to avoid bundling binaries in environments where they cannot run.
|
|
203
|
+
|
|
204
|
+
See the [cycletls example](./examples/cycletls/) for a complete working example.
|
|
205
|
+
|
|
171
206
|
### Rate limiting
|
|
172
207
|
The Twitter API heavily rate-limits clients, requiring that the scraper has its own
|
|
173
208
|
rate-limit handling to behave predictably when rate-limiting occurs. By default, the
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var initCycleTLS = require('cycletls');
|
|
4
|
+
var headersPolyfill = require('headers-polyfill');
|
|
5
|
+
var debug = require('debug');
|
|
6
|
+
|
|
7
|
+
const log = debug("twitter-scraper:cycletls");
|
|
8
|
+
let cycleTLSInstance = null;
|
|
9
|
+
async function initCycleTLSFetch() {
|
|
10
|
+
if (!cycleTLSInstance) {
|
|
11
|
+
log("Initializing CycleTLS...");
|
|
12
|
+
cycleTLSInstance = await initCycleTLS();
|
|
13
|
+
log("CycleTLS initialized successfully");
|
|
14
|
+
}
|
|
15
|
+
return cycleTLSInstance;
|
|
16
|
+
}
|
|
17
|
+
function cycleTLSExit() {
|
|
18
|
+
if (cycleTLSInstance) {
|
|
19
|
+
log("Exiting CycleTLS...");
|
|
20
|
+
cycleTLSInstance.exit();
|
|
21
|
+
cycleTLSInstance = null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function cycleTLSFetch(input, init) {
|
|
25
|
+
const instance = await initCycleTLSFetch();
|
|
26
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
27
|
+
const method = (init?.method || "GET").toUpperCase();
|
|
28
|
+
log(`Making ${method} request to ${url}`);
|
|
29
|
+
const headers = {};
|
|
30
|
+
if (init?.headers) {
|
|
31
|
+
if (init.headers instanceof headersPolyfill.Headers) {
|
|
32
|
+
init.headers.forEach((value, key) => {
|
|
33
|
+
headers[key] = value;
|
|
34
|
+
});
|
|
35
|
+
} else if (Array.isArray(init.headers)) {
|
|
36
|
+
init.headers.forEach(([key, value]) => {
|
|
37
|
+
headers[key] = value;
|
|
38
|
+
});
|
|
39
|
+
} else {
|
|
40
|
+
Object.assign(headers, init.headers);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
let body;
|
|
44
|
+
if (init?.body) {
|
|
45
|
+
if (typeof init.body === "string") {
|
|
46
|
+
body = init.body;
|
|
47
|
+
} else if (init.body instanceof URLSearchParams) {
|
|
48
|
+
body = init.body.toString();
|
|
49
|
+
} else {
|
|
50
|
+
body = init.body.toString();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const options = {
|
|
54
|
+
body,
|
|
55
|
+
headers,
|
|
56
|
+
// Chrome 120 on Windows 10
|
|
57
|
+
ja3: "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0",
|
|
58
|
+
userAgent: headers["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"
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
const response = await instance(
|
|
62
|
+
url,
|
|
63
|
+
options,
|
|
64
|
+
method.toLowerCase()
|
|
65
|
+
);
|
|
66
|
+
const responseHeaders = new headersPolyfill.Headers();
|
|
67
|
+
if (response.headers) {
|
|
68
|
+
Object.entries(response.headers).forEach(([key, value]) => {
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
value.forEach((v) => {
|
|
71
|
+
responseHeaders.append(key, v);
|
|
72
|
+
});
|
|
73
|
+
} else if (typeof value === "string") {
|
|
74
|
+
responseHeaders.set(key, value);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
let responseBody = "";
|
|
79
|
+
if (typeof response.text === "function") {
|
|
80
|
+
responseBody = await response.text();
|
|
81
|
+
} else if (response.body) {
|
|
82
|
+
responseBody = response.body;
|
|
83
|
+
}
|
|
84
|
+
const fetchResponse = new Response(responseBody, {
|
|
85
|
+
status: response.status,
|
|
86
|
+
statusText: "",
|
|
87
|
+
// CycleTLS doesn't provide status text
|
|
88
|
+
headers: responseHeaders
|
|
89
|
+
});
|
|
90
|
+
return fetchResponse;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
log(`CycleTLS request failed: ${error}`);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
exports.cycleTLSExit = cycleTLSExit;
|
|
98
|
+
exports.cycleTLSFetch = cycleTLSFetch;
|
|
99
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../../src/cycletls-fetch.ts"],"sourcesContent":["import initCycleTLS from 'cycletls';\nimport { Headers } from 'headers-polyfill';\nimport debug from 'debug';\n\nconst log = debug('twitter-scraper:cycletls');\n\nlet cycleTLSInstance: Awaited<ReturnType<typeof initCycleTLS>> | null = null;\n\n/**\n * Initialize the CycleTLS instance. This should be called once before using the fetch wrapper.\n */\nexport async function initCycleTLSFetch() {\n if (!cycleTLSInstance) {\n log('Initializing CycleTLS...');\n cycleTLSInstance = await initCycleTLS();\n log('CycleTLS initialized successfully');\n }\n return cycleTLSInstance;\n}\n\n/**\n * Cleanup the CycleTLS instance. Call this when you're done making requests.\n */\nexport function cycleTLSExit() {\n if (cycleTLSInstance) {\n log('Exiting CycleTLS...');\n cycleTLSInstance.exit();\n cycleTLSInstance = null;\n }\n}\n\n/**\n * A fetch-compatible wrapper around CycleTLS that mimics Chrome's TLS fingerprint\n * to bypass Cloudflare and other bot detection systems.\n */\nexport async function cycleTLSFetch(\n input: RequestInfo | URL,\n init?: RequestInit,\n): Promise<Response> {\n const instance = await initCycleTLSFetch();\n\n const url =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : input.url;\n const method = (init?.method || 'GET').toUpperCase();\n\n log(`Making ${method} request to ${url}`);\n\n // Extract headers from RequestInit\n const headers: Record<string, string> = {};\n if (init?.headers) {\n if (init.headers instanceof Headers) {\n init.headers.forEach((value, key) => {\n headers[key] = value;\n });\n } else if (Array.isArray(init.headers)) {\n init.headers.forEach(([key, value]) => {\n headers[key] = value;\n });\n } else {\n Object.assign(headers, init.headers);\n }\n }\n\n // Convert body to string if needed\n let body: string | undefined;\n if (init?.body) {\n if (typeof init.body === 'string') {\n body = init.body;\n } else if (init.body instanceof URLSearchParams) {\n body = init.body.toString();\n } else {\n body = init.body.toString();\n }\n }\n\n // Use Chrome 120 JA3 fingerprint for maximum compatibility\n const options = {\n body,\n headers,\n // Chrome 120 on Windows 10\n ja3: '771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0',\n userAgent:\n headers['user-agent'] ||\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',\n };\n\n try {\n const response = await instance(\n url,\n options,\n method.toLowerCase() as\n | 'get'\n | 'post'\n | 'put'\n | 'delete'\n | 'patch'\n | 'head'\n | 'options',\n );\n\n // Convert CycleTLS response to fetch Response\n // CycleTLS returns headers as an object\n const responseHeaders = new Headers();\n if (response.headers) {\n Object.entries(response.headers).forEach(([key, value]) => {\n if (Array.isArray(value)) {\n value.forEach((v) => {\n responseHeaders.append(key, v);\n });\n } else if (typeof value === 'string') {\n responseHeaders.set(key, value);\n }\n });\n }\n\n // Get response body - cycletls provides helper methods, but we need the raw text\n // The response object has a text() method that returns the body as text\n let responseBody = '';\n if (typeof response.text === 'function') {\n responseBody = await response.text();\n } else if ((response as any).body) {\n responseBody = (response as any).body;\n }\n\n // Create a proper Response object using standard Response constructor\n const fetchResponse = new Response(responseBody, {\n status: response.status,\n statusText: '', // CycleTLS doesn't provide status text\n headers: responseHeaders,\n });\n\n return fetchResponse;\n } catch (error) {\n log(`CycleTLS request failed: ${error}`);\n throw error;\n }\n}\n"],"names":["Headers"],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import initCycleTLS from 'cycletls';
|
|
2
|
+
import { Headers } from 'headers-polyfill';
|
|
3
|
+
import debug from 'debug';
|
|
4
|
+
|
|
5
|
+
const log = debug("twitter-scraper:cycletls");
|
|
6
|
+
let cycleTLSInstance = null;
|
|
7
|
+
async function initCycleTLSFetch() {
|
|
8
|
+
if (!cycleTLSInstance) {
|
|
9
|
+
log("Initializing CycleTLS...");
|
|
10
|
+
cycleTLSInstance = await initCycleTLS();
|
|
11
|
+
log("CycleTLS initialized successfully");
|
|
12
|
+
}
|
|
13
|
+
return cycleTLSInstance;
|
|
14
|
+
}
|
|
15
|
+
function cycleTLSExit() {
|
|
16
|
+
if (cycleTLSInstance) {
|
|
17
|
+
log("Exiting CycleTLS...");
|
|
18
|
+
cycleTLSInstance.exit();
|
|
19
|
+
cycleTLSInstance = null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function cycleTLSFetch(input, init) {
|
|
23
|
+
const instance = await initCycleTLSFetch();
|
|
24
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
25
|
+
const method = (init?.method || "GET").toUpperCase();
|
|
26
|
+
log(`Making ${method} request to ${url}`);
|
|
27
|
+
const headers = {};
|
|
28
|
+
if (init?.headers) {
|
|
29
|
+
if (init.headers instanceof Headers) {
|
|
30
|
+
init.headers.forEach((value, key) => {
|
|
31
|
+
headers[key] = value;
|
|
32
|
+
});
|
|
33
|
+
} else if (Array.isArray(init.headers)) {
|
|
34
|
+
init.headers.forEach(([key, value]) => {
|
|
35
|
+
headers[key] = value;
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
Object.assign(headers, init.headers);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let body;
|
|
42
|
+
if (init?.body) {
|
|
43
|
+
if (typeof init.body === "string") {
|
|
44
|
+
body = init.body;
|
|
45
|
+
} else if (init.body instanceof URLSearchParams) {
|
|
46
|
+
body = init.body.toString();
|
|
47
|
+
} else {
|
|
48
|
+
body = init.body.toString();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const options = {
|
|
52
|
+
body,
|
|
53
|
+
headers,
|
|
54
|
+
// Chrome 120 on Windows 10
|
|
55
|
+
ja3: "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0",
|
|
56
|
+
userAgent: headers["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"
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
const response = await instance(
|
|
60
|
+
url,
|
|
61
|
+
options,
|
|
62
|
+
method.toLowerCase()
|
|
63
|
+
);
|
|
64
|
+
const responseHeaders = new Headers();
|
|
65
|
+
if (response.headers) {
|
|
66
|
+
Object.entries(response.headers).forEach(([key, value]) => {
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
value.forEach((v) => {
|
|
69
|
+
responseHeaders.append(key, v);
|
|
70
|
+
});
|
|
71
|
+
} else if (typeof value === "string") {
|
|
72
|
+
responseHeaders.set(key, value);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
let responseBody = "";
|
|
77
|
+
if (typeof response.text === "function") {
|
|
78
|
+
responseBody = await response.text();
|
|
79
|
+
} else if (response.body) {
|
|
80
|
+
responseBody = response.body;
|
|
81
|
+
}
|
|
82
|
+
const fetchResponse = new Response(responseBody, {
|
|
83
|
+
status: response.status,
|
|
84
|
+
statusText: "",
|
|
85
|
+
// CycleTLS doesn't provide status text
|
|
86
|
+
headers: responseHeaders
|
|
87
|
+
});
|
|
88
|
+
return fetchResponse;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
log(`CycleTLS request failed: ${error}`);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { cycleTLSExit, cycleTLSFetch };
|
|
96
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../../../src/cycletls-fetch.ts"],"sourcesContent":["import initCycleTLS from 'cycletls';\nimport { Headers } from 'headers-polyfill';\nimport debug from 'debug';\n\nconst log = debug('twitter-scraper:cycletls');\n\nlet cycleTLSInstance: Awaited<ReturnType<typeof initCycleTLS>> | null = null;\n\n/**\n * Initialize the CycleTLS instance. This should be called once before using the fetch wrapper.\n */\nexport async function initCycleTLSFetch() {\n if (!cycleTLSInstance) {\n log('Initializing CycleTLS...');\n cycleTLSInstance = await initCycleTLS();\n log('CycleTLS initialized successfully');\n }\n return cycleTLSInstance;\n}\n\n/**\n * Cleanup the CycleTLS instance. Call this when you're done making requests.\n */\nexport function cycleTLSExit() {\n if (cycleTLSInstance) {\n log('Exiting CycleTLS...');\n cycleTLSInstance.exit();\n cycleTLSInstance = null;\n }\n}\n\n/**\n * A fetch-compatible wrapper around CycleTLS that mimics Chrome's TLS fingerprint\n * to bypass Cloudflare and other bot detection systems.\n */\nexport async function cycleTLSFetch(\n input: RequestInfo | URL,\n init?: RequestInit,\n): Promise<Response> {\n const instance = await initCycleTLSFetch();\n\n const url =\n typeof input === 'string'\n ? input\n : input instanceof URL\n ? input.toString()\n : input.url;\n const method = (init?.method || 'GET').toUpperCase();\n\n log(`Making ${method} request to ${url}`);\n\n // Extract headers from RequestInit\n const headers: Record<string, string> = {};\n if (init?.headers) {\n if (init.headers instanceof Headers) {\n init.headers.forEach((value, key) => {\n headers[key] = value;\n });\n } else if (Array.isArray(init.headers)) {\n init.headers.forEach(([key, value]) => {\n headers[key] = value;\n });\n } else {\n Object.assign(headers, init.headers);\n }\n }\n\n // Convert body to string if needed\n let body: string | undefined;\n if (init?.body) {\n if (typeof init.body === 'string') {\n body = init.body;\n } else if (init.body instanceof URLSearchParams) {\n body = init.body.toString();\n } else {\n body = init.body.toString();\n }\n }\n\n // Use Chrome 120 JA3 fingerprint for maximum compatibility\n const options = {\n body,\n headers,\n // Chrome 120 on Windows 10\n ja3: '771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0',\n userAgent:\n headers['user-agent'] ||\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',\n };\n\n try {\n const response = await instance(\n url,\n options,\n method.toLowerCase() as\n | 'get'\n | 'post'\n | 'put'\n | 'delete'\n | 'patch'\n | 'head'\n | 'options',\n );\n\n // Convert CycleTLS response to fetch Response\n // CycleTLS returns headers as an object\n const responseHeaders = new Headers();\n if (response.headers) {\n Object.entries(response.headers).forEach(([key, value]) => {\n if (Array.isArray(value)) {\n value.forEach((v) => {\n responseHeaders.append(key, v);\n });\n } else if (typeof value === 'string') {\n responseHeaders.set(key, value);\n }\n });\n }\n\n // Get response body - cycletls provides helper methods, but we need the raw text\n // The response object has a text() method that returns the body as text\n let responseBody = '';\n if (typeof response.text === 'function') {\n responseBody = await response.text();\n } else if ((response as any).body) {\n responseBody = (response as any).body;\n }\n\n // Create a proper Response object using standard Response constructor\n const fetchResponse = new Response(responseBody, {\n status: response.status,\n statusText: '', // CycleTLS doesn't provide status text\n headers: responseHeaders,\n });\n\n return fetchResponse;\n } catch (error) {\n log(`CycleTLS request failed: ${error}`);\n throw error;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cleanup the CycleTLS instance. Call this when you're done making requests.
|
|
3
|
+
*/
|
|
4
|
+
declare function cycleTLSExit(): void;
|
|
5
|
+
/**
|
|
6
|
+
* A fetch-compatible wrapper around CycleTLS that mimics Chrome's TLS fingerprint
|
|
7
|
+
* to bypass Cloudflare and other bot detection systems.
|
|
8
|
+
*/
|
|
9
|
+
declare function cycleTLSFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
10
|
+
|
|
11
|
+
export { cycleTLSExit, cycleTLSFetch };
|
|
@@ -70,13 +70,13 @@ class AuthenticationError extends Error {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const log$
|
|
73
|
+
const log$4 = debug("twitter-scraper:rate-limit");
|
|
74
74
|
class WaitingRateLimitStrategy {
|
|
75
75
|
async onRateLimit({ response: res }) {
|
|
76
76
|
const xRateLimitLimit = res.headers.get("x-rate-limit-limit");
|
|
77
77
|
const xRateLimitRemaining = res.headers.get("x-rate-limit-remaining");
|
|
78
78
|
const xRateLimitReset = res.headers.get("x-rate-limit-reset");
|
|
79
|
-
log$
|
|
79
|
+
log$4(
|
|
80
80
|
`Rate limit event: limit=${xRateLimitLimit}, remaining=${xRateLimitRemaining}, reset=${xRateLimitReset}`
|
|
81
81
|
);
|
|
82
82
|
if (xRateLimitRemaining == "0" && xRateLimitReset) {
|
|
@@ -108,16 +108,47 @@ class Platform {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
const log$3 = debug("twitter-scraper:requests");
|
|
111
112
|
async function updateCookieJar(cookieJar, headers) {
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
let setCookieHeaders = [];
|
|
114
|
+
if (typeof headers.getSetCookie === "function") {
|
|
115
|
+
setCookieHeaders = headers.getSetCookie();
|
|
116
|
+
} else {
|
|
117
|
+
const setCookieHeader = headers.get("set-cookie");
|
|
118
|
+
if (setCookieHeader) {
|
|
119
|
+
setCookieHeaders = setCookie.splitCookiesString(setCookieHeader);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (setCookieHeaders.length > 0) {
|
|
123
|
+
for (const cookieStr of setCookieHeaders) {
|
|
124
|
+
const cookie = toughCookie.Cookie.parse(cookieStr);
|
|
125
|
+
if (!cookie) {
|
|
126
|
+
log$3(`Failed to parse cookie: ${cookieStr.substring(0, 100)}`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (cookie.maxAge === 0 || cookie.expires && cookie.expires < /* @__PURE__ */ new Date()) {
|
|
130
|
+
if (cookie.key === "ct0") {
|
|
131
|
+
log$3(`Skipping deletion of ct0 cookie (Max-Age=0)`);
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const url = `${cookie.secure ? "https" : "http"}://${cookie.domain}${cookie.path}`;
|
|
137
|
+
await cookieJar.setCookie(cookie, url);
|
|
138
|
+
if (cookie.key === "ct0") {
|
|
139
|
+
log$3(
|
|
140
|
+
`Successfully set ct0 cookie with value: ${cookie.value.substring(
|
|
141
|
+
0,
|
|
142
|
+
20
|
|
143
|
+
)}...`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
log$3(`Failed to set cookie ${cookie.key}: ${err}`);
|
|
148
|
+
if (cookie.key === "ct0") {
|
|
149
|
+
log$3(`FAILED to set ct0 cookie! Error: ${err}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
121
152
|
}
|
|
122
153
|
} else if (typeof document !== "undefined") {
|
|
123
154
|
for (const cookie of document.cookie.split(";")) {
|
|
@@ -135,9 +166,8 @@ async function jitter(maxMs) {
|
|
|
135
166
|
const jitter2 = Math.random() * maxMs;
|
|
136
167
|
await new Promise((resolve) => setTimeout(resolve, jitter2));
|
|
137
168
|
}
|
|
138
|
-
async function requestApi(url, auth, method = "GET", platform = new Platform()) {
|
|
169
|
+
async function requestApi(url, auth, method = "GET", platform = new Platform(), headers = new headersPolyfill.Headers()) {
|
|
139
170
|
log$2(`Making ${method} request to ${url}`);
|
|
140
|
-
const headers = new headersPolyfill.Headers();
|
|
141
171
|
await auth.installTo(headers, url);
|
|
142
172
|
await platform.randomizeCiphers();
|
|
143
173
|
let res;
|
|
@@ -176,7 +206,7 @@ async function requestApi(url, auth, method = "GET", platform = new Platform())
|
|
|
176
206
|
err: await ApiError.fromResponse(res)
|
|
177
207
|
};
|
|
178
208
|
}
|
|
179
|
-
const value = await res
|
|
209
|
+
const value = await flexParseJson(res);
|
|
180
210
|
if (res.headers.get("x-rate-limit-incoming") == "0") {
|
|
181
211
|
auth.deleteToken();
|
|
182
212
|
return { success: true, value };
|
|
@@ -184,6 +214,16 @@ async function requestApi(url, auth, method = "GET", platform = new Platform())
|
|
|
184
214
|
return { success: true, value };
|
|
185
215
|
}
|
|
186
216
|
}
|
|
217
|
+
async function flexParseJson(res) {
|
|
218
|
+
try {
|
|
219
|
+
return await res.json();
|
|
220
|
+
} catch {
|
|
221
|
+
log$2("Failed to parse response as JSON, trying text parse...");
|
|
222
|
+
const text = await res.text();
|
|
223
|
+
log$2("Response text:", text);
|
|
224
|
+
return JSON.parse(text);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
187
227
|
function addApiFeatures(o) {
|
|
188
228
|
return {
|
|
189
229
|
...o,
|
|
@@ -316,6 +356,10 @@ class TwitterGuestAuth {
|
|
|
316
356
|
}
|
|
317
357
|
headers.set("authorization", `Bearer ${this.bearerToken}`);
|
|
318
358
|
headers.set("x-guest-token", token);
|
|
359
|
+
headers.set(
|
|
360
|
+
"user-agent",
|
|
361
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
|
362
|
+
);
|
|
319
363
|
const cookies = await this.getCookies();
|
|
320
364
|
const xCsrfToken = cookies.find((cookie) => cookie.key === "ct0");
|
|
321
365
|
if (xCsrfToken) {
|
|
@@ -323,6 +367,16 @@ class TwitterGuestAuth {
|
|
|
323
367
|
}
|
|
324
368
|
headers.set("cookie", await this.getCookieString());
|
|
325
369
|
}
|
|
370
|
+
async setCookie(key, value) {
|
|
371
|
+
const cookie = toughCookie.Cookie.parse(`${key}=${value}`);
|
|
372
|
+
if (!cookie) {
|
|
373
|
+
throw new Error("Failed to parse cookie.");
|
|
374
|
+
}
|
|
375
|
+
await this.jar.setCookie(cookie, this.getCookieJarUrl());
|
|
376
|
+
if (typeof document !== "undefined") {
|
|
377
|
+
document.cookie = cookie.toString();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
326
380
|
async getCookies() {
|
|
327
381
|
return this.jar.getCookies(this.getCookieJarUrl());
|
|
328
382
|
}
|
|
@@ -363,7 +417,7 @@ class TwitterGuestAuth {
|
|
|
363
417
|
if (!res.ok) {
|
|
364
418
|
throw new AuthenticationError(await res.text());
|
|
365
419
|
}
|
|
366
|
-
const o = await res
|
|
420
|
+
const o = await flexParseJson(res);
|
|
367
421
|
if (o == null || o["guest_token"] == null) {
|
|
368
422
|
throw new AuthenticationError("guest_token not found.");
|
|
369
423
|
}
|
|
@@ -373,6 +427,8 @@ class TwitterGuestAuth {
|
|
|
373
427
|
}
|
|
374
428
|
this.guestToken = newGuestToken;
|
|
375
429
|
this.guestCreatedAt = /* @__PURE__ */ new Date();
|
|
430
|
+
await this.setCookie("gt", newGuestToken);
|
|
431
|
+
log$1(`Updated guest token: ${newGuestToken}`);
|
|
376
432
|
}
|
|
377
433
|
/**
|
|
378
434
|
* Returns if the authentication token needs to be updated or not.
|
|
@@ -499,7 +555,15 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
499
555
|
}
|
|
500
556
|
async installTo(headers) {
|
|
501
557
|
headers.set("authorization", `Bearer ${this.bearerToken}`);
|
|
502
|
-
|
|
558
|
+
const cookie = await this.getCookieString();
|
|
559
|
+
headers.set("cookie", cookie);
|
|
560
|
+
if (this.guestToken) {
|
|
561
|
+
headers.set("x-guest-token", this.guestToken);
|
|
562
|
+
}
|
|
563
|
+
headers.set(
|
|
564
|
+
"user-agent",
|
|
565
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
|
566
|
+
);
|
|
503
567
|
await this.installCsrfToken(headers);
|
|
504
568
|
}
|
|
505
569
|
async initLogin() {
|
|
@@ -712,16 +776,27 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
712
776
|
);
|
|
713
777
|
}
|
|
714
778
|
const headers = new headersPolyfill.Headers({
|
|
715
|
-
|
|
716
|
-
|
|
779
|
+
accept: "*/*",
|
|
780
|
+
"accept-language": "en-US,en;q=0.9",
|
|
717
781
|
"content-type": "application/json",
|
|
718
|
-
"
|
|
782
|
+
"cache-control": "no-cache",
|
|
783
|
+
origin: "https://x.com",
|
|
784
|
+
pragma: "no-cache",
|
|
785
|
+
priority: "u=1, i",
|
|
786
|
+
referer: "https://x.com/",
|
|
787
|
+
"sec-ch-ua": '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
|
|
788
|
+
"sec-ch-ua-mobile": "?0",
|
|
789
|
+
"sec-ch-ua-platform": '"Windows"',
|
|
790
|
+
"sec-fetch-dest": "empty",
|
|
791
|
+
"sec-fetch-mode": "cors",
|
|
792
|
+
"sec-fetch-site": "same-origin",
|
|
793
|
+
"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",
|
|
719
794
|
"x-guest-token": token,
|
|
720
795
|
"x-twitter-auth-type": "OAuth2Client",
|
|
721
796
|
"x-twitter-active-user": "yes",
|
|
722
797
|
"x-twitter-client-language": "en"
|
|
723
798
|
});
|
|
724
|
-
await this.
|
|
799
|
+
await this.installTo(headers);
|
|
725
800
|
let res;
|
|
726
801
|
do {
|
|
727
802
|
const fetchParameters = [
|
|
@@ -756,7 +831,7 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
756
831
|
if (!res.ok) {
|
|
757
832
|
return { status: "error", err: await ApiError.fromResponse(res) };
|
|
758
833
|
}
|
|
759
|
-
const flow = await res
|
|
834
|
+
const flow = await flexParseJson(res);
|
|
760
835
|
if (flow?.flow_token == null) {
|
|
761
836
|
return {
|
|
762
837
|
status: "error",
|
|
@@ -794,12 +869,12 @@ class TwitterUserAuth extends TwitterGuestAuth {
|
|
|
794
869
|
|
|
795
870
|
const endpoints = {
|
|
796
871
|
// TODO: Migrate other endpoint URLs here
|
|
797
|
-
UserTweets: "https://
|
|
872
|
+
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",
|
|
798
873
|
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",
|
|
799
874
|
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",
|
|
800
|
-
UserByScreenName: "https://
|
|
801
|
-
TweetDetail: "https://x.com/i/api/graphql/
|
|
802
|
-
TweetResultByRestId: "https://api.x.com/graphql/
|
|
875
|
+
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",
|
|
876
|
+
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",
|
|
877
|
+
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",
|
|
803
878
|
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"
|
|
804
879
|
};
|
|
805
880
|
class ApiRequest {
|