twitter-api-browser-js 0.0.3 → 0.0.4
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/dist/consts.d.ts +9 -0
- package/dist/consts.js +9 -0
- package/dist/example.d.ts +1 -0
- package/dist/example.js +109 -0
- package/dist/inject.d.ts +27 -0
- package/dist/inject.js +64 -0
- package/dist/main.d.ts +51 -0
- package/dist/main.js +203 -0
- package/dist/twitter-types.d.ts +16 -0
- package/dist/twitter-types.js +1 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +35 -0
- package/package.json +1 -1
package/dist/consts.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const REQUEST_FUNC_GLOBAL_KEY = "elonmusk_810_request";
|
|
2
|
+
export declare const INITIAL_STATE_PROP_KEY = "__INITIAL_STATE__";
|
|
3
|
+
export declare const INITIAL_STATE_GLOBAL_KEY = "elonmusk_810_init_state";
|
|
4
|
+
export declare const OPERATIONS_GLOBAL_KEY = "elonmusk_810_operations";
|
|
5
|
+
export declare const DEFAULT_USER_DATA_DIR = "./.user_data";
|
|
6
|
+
export declare const METHOD_MAP: {
|
|
7
|
+
readonly query: "GET";
|
|
8
|
+
readonly mutation: "POST";
|
|
9
|
+
};
|
package/dist/consts.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const REQUEST_FUNC_GLOBAL_KEY = "elonmusk_810_request";
|
|
2
|
+
export const INITIAL_STATE_PROP_KEY = "__INITIAL_STATE__";
|
|
3
|
+
export const INITIAL_STATE_GLOBAL_KEY = "elonmusk_810_init_state";
|
|
4
|
+
export const OPERATIONS_GLOBAL_KEY = "elonmusk_810_operations";
|
|
5
|
+
export const DEFAULT_USER_DATA_DIR = "./.user_data";
|
|
6
|
+
export const METHOD_MAP = {
|
|
7
|
+
query: "GET",
|
|
8
|
+
mutation: "POST",
|
|
9
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/example.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { TwitterAPIBrowser } from "./main.ts";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
function prompt(question) {
|
|
4
|
+
const rl = createInterface({
|
|
5
|
+
input: process.stdin,
|
|
6
|
+
output: process.stdout,
|
|
7
|
+
});
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
rl.question(question, (answer) => {
|
|
10
|
+
rl.close();
|
|
11
|
+
resolve(answer);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function formatLog(...args) {
|
|
16
|
+
console.dir(args, { depth: null });
|
|
17
|
+
}
|
|
18
|
+
async function main() {
|
|
19
|
+
const userDataDir = "./.data";
|
|
20
|
+
const browser = new TwitterAPIBrowser(userDataDir);
|
|
21
|
+
console.log("Setup browser");
|
|
22
|
+
await browser.setup();
|
|
23
|
+
try {
|
|
24
|
+
if (await browser.isLoggedIn()) {
|
|
25
|
+
console.log("User is logged in");
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.log("User is not logged in, please login manually");
|
|
29
|
+
await browser.manualLogin();
|
|
30
|
+
}
|
|
31
|
+
while (true) {
|
|
32
|
+
console.log("=".repeat(20));
|
|
33
|
+
const operation = await prompt("Choose operation [CreateTweet, HomeTimeline, UserByScreenName, CreateRetweet, FavoriteTweet, SearchTimeline, UsersByRestIds, exit]: ");
|
|
34
|
+
if (!operation || operation === "exit") {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const request = async () => {
|
|
38
|
+
if (operation === "CreateTweet") {
|
|
39
|
+
const res = await browser.request("CreateTweet", {
|
|
40
|
+
tweet_text: `Hello, World! ${new Date().toISOString()}`,
|
|
41
|
+
dark_request: false,
|
|
42
|
+
media: { media_entities: [], possibly_sensitive: false },
|
|
43
|
+
semantic_annotation_ids: [],
|
|
44
|
+
disallowed_reply_options: null,
|
|
45
|
+
});
|
|
46
|
+
return res;
|
|
47
|
+
}
|
|
48
|
+
else if (operation === "HomeTimeline") {
|
|
49
|
+
const res = await browser.request("HomeTimeline", {
|
|
50
|
+
count: 20,
|
|
51
|
+
includePromotedContent: true,
|
|
52
|
+
latestControlAvailable: true,
|
|
53
|
+
withCommunity: true,
|
|
54
|
+
});
|
|
55
|
+
return res;
|
|
56
|
+
}
|
|
57
|
+
else if (operation === "UserByScreenName") {
|
|
58
|
+
const res = await browser.request("UserByScreenName", {
|
|
59
|
+
screen_name: "elonmusk",
|
|
60
|
+
withSafetyModeUserFields: true,
|
|
61
|
+
withSuperFollowsUserFields: true,
|
|
62
|
+
withBirdwatchPivots: false,
|
|
63
|
+
}, {
|
|
64
|
+
withAuxiliaryUserLabels: true,
|
|
65
|
+
});
|
|
66
|
+
return res;
|
|
67
|
+
}
|
|
68
|
+
else if (operation === "CreateRetweet") {
|
|
69
|
+
const res = await browser.request("CreateRetweet", {
|
|
70
|
+
tweet_id: "1987547856664993831",
|
|
71
|
+
dark_request: false,
|
|
72
|
+
});
|
|
73
|
+
return res;
|
|
74
|
+
}
|
|
75
|
+
else if (operation === "FavoriteTweet") {
|
|
76
|
+
const res = await browser.request("FavoriteTweet", {
|
|
77
|
+
tweet_id: "1987547856664993831",
|
|
78
|
+
});
|
|
79
|
+
return res;
|
|
80
|
+
}
|
|
81
|
+
else if (operation === "SearchTimeline") {
|
|
82
|
+
const res = await browser.request("SearchTimeline", {
|
|
83
|
+
rawQuery: "from:elonmusk",
|
|
84
|
+
count: 20,
|
|
85
|
+
querySource: "typed_query",
|
|
86
|
+
product: "Top",
|
|
87
|
+
withGrokTranslatedBio: false,
|
|
88
|
+
});
|
|
89
|
+
return res;
|
|
90
|
+
}
|
|
91
|
+
else if (operation === "UsersByRestIds") {
|
|
92
|
+
const res = await browser.request("UsersByRestIds", {
|
|
93
|
+
userIds: ["900282258736545792"],
|
|
94
|
+
});
|
|
95
|
+
return res;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.log(`Unknown operation: ${operation}`);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const res = await request();
|
|
102
|
+
formatLog(res);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await browser.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
await main();
|
package/dist/inject.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { METHOD_MAP } from "./consts.js";
|
|
2
|
+
import { LooseType } from "./twitter-types.js";
|
|
3
|
+
export declare const SETUP_SCRIPT = "\n(async () => {\n if (globalThis.elonmusk_810_request) {\n return;\n }\n const __origApply = Function.prototype.apply;\n const client = await new Promise((resolve) => {\n Function.prototype.apply = function (thisArg, argsArray) {\n if (thisArg && typeof thisArg === \"object\" && thisArg.dispatch === this) {\n resolve(thisArg);\n }\n return __origApply.bind(this)(thisArg, argsArray);\n };\n });\n Function.prototype.apply = __origApply;\n globalThis.elonmusk_810_request = (query) => {\n return client.dispatch.apply(client, [query]);\n };\n})();\n";
|
|
4
|
+
export declare const INITIAL_STATE_SCRIPT = "\n(async () => {\n const init_state_promise = new Promise((resolve) => {\n Object.defineProperty(window, \"__INITIAL_STATE__\", {\n configurable: true,\n enumerable: true,\n get() {\n return undefined;\n },\n set(v) {\n resolve(v);\n Object.defineProperty(window, \"__INITIAL_STATE__\", {\n value: v,\n writable: true,\n enumerable: true,\n configurable: true,\n });\n },\n });\n });\n\n globalThis.elonmusk_810_init_state = init_state_promise;\n})();\n";
|
|
5
|
+
export declare const OPERATIONS_SCRIPT = "\n(async () => {\n globalThis.elonmusk_810_operations = [];\n const origCall = Function.prototype.call;\n Function.prototype.call = function (thisArg, ...args) {\n const module = args[0];\n const ret = origCall.bind(this)(thisArg, ...args);\n try {\n const exp = module.exports;\n if (exp.operationName) {\n globalThis.elonmusk_810_operations.push(exp);\n }\n } catch (_) {}\n return ret;\n };\n await new Promise((resolve) => setTimeout(resolve, 5000));\n Function.prototype.call = origCall;\n})();\n";
|
|
6
|
+
export type Operation = {
|
|
7
|
+
operationName: string;
|
|
8
|
+
queryId: string;
|
|
9
|
+
operationType: keyof typeof METHOD_MAP;
|
|
10
|
+
metadata: {
|
|
11
|
+
featureSwitches: string[];
|
|
12
|
+
allowFieldToggles: string[];
|
|
13
|
+
fieldToggles: string[];
|
|
14
|
+
};
|
|
15
|
+
} & Record<string, LooseType>;
|
|
16
|
+
type FeatureSwitchValue = {
|
|
17
|
+
value: boolean;
|
|
18
|
+
} & Record<string, LooseType>;
|
|
19
|
+
export type InitialState = {
|
|
20
|
+
featureSwitch: {
|
|
21
|
+
defaultConfig: Record<string, FeatureSwitchValue>;
|
|
22
|
+
user: Record<string, FeatureSwitchValue>;
|
|
23
|
+
debug: Record<string, FeatureSwitchValue>;
|
|
24
|
+
customOverrides: Record<string, FeatureSwitchValue>;
|
|
25
|
+
};
|
|
26
|
+
} & Record<string, LooseType>;
|
|
27
|
+
export {};
|
package/dist/inject.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { INITIAL_STATE_GLOBAL_KEY, INITIAL_STATE_PROP_KEY, OPERATIONS_GLOBAL_KEY, REQUEST_FUNC_GLOBAL_KEY, } from "./consts.js";
|
|
2
|
+
export const SETUP_SCRIPT = `
|
|
3
|
+
(async () => {
|
|
4
|
+
if (globalThis.${REQUEST_FUNC_GLOBAL_KEY}) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const __origApply = Function.prototype.apply;
|
|
8
|
+
const client = await new Promise((resolve) => {
|
|
9
|
+
Function.prototype.apply = function (thisArg, argsArray) {
|
|
10
|
+
if (thisArg && typeof thisArg === "object" && thisArg.dispatch === this) {
|
|
11
|
+
resolve(thisArg);
|
|
12
|
+
}
|
|
13
|
+
return __origApply.bind(this)(thisArg, argsArray);
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
Function.prototype.apply = __origApply;
|
|
17
|
+
globalThis.${REQUEST_FUNC_GLOBAL_KEY} = (query) => {
|
|
18
|
+
return client.dispatch.apply(client, [query]);
|
|
19
|
+
};
|
|
20
|
+
})();
|
|
21
|
+
`;
|
|
22
|
+
export const INITIAL_STATE_SCRIPT = `
|
|
23
|
+
(async () => {
|
|
24
|
+
const init_state_promise = new Promise((resolve) => {
|
|
25
|
+
Object.defineProperty(window, "${INITIAL_STATE_PROP_KEY}", {
|
|
26
|
+
configurable: true,
|
|
27
|
+
enumerable: true,
|
|
28
|
+
get() {
|
|
29
|
+
return undefined;
|
|
30
|
+
},
|
|
31
|
+
set(v) {
|
|
32
|
+
resolve(v);
|
|
33
|
+
Object.defineProperty(window, "${INITIAL_STATE_PROP_KEY}", {
|
|
34
|
+
value: v,
|
|
35
|
+
writable: true,
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
globalThis.${INITIAL_STATE_GLOBAL_KEY} = init_state_promise;
|
|
44
|
+
})();
|
|
45
|
+
`;
|
|
46
|
+
export const OPERATIONS_SCRIPT = `
|
|
47
|
+
(async () => {
|
|
48
|
+
globalThis.${OPERATIONS_GLOBAL_KEY} = [];
|
|
49
|
+
const origCall = Function.prototype.call;
|
|
50
|
+
Function.prototype.call = function (thisArg, ...args) {
|
|
51
|
+
const module = args[0];
|
|
52
|
+
const ret = origCall.bind(this)(thisArg, ...args);
|
|
53
|
+
try {
|
|
54
|
+
const exp = module.exports;
|
|
55
|
+
if (exp.operationName) {
|
|
56
|
+
globalThis.${OPERATIONS_GLOBAL_KEY}.push(exp);
|
|
57
|
+
}
|
|
58
|
+
} catch (_) {}
|
|
59
|
+
return ret;
|
|
60
|
+
};
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
62
|
+
Function.prototype.call = origCall;
|
|
63
|
+
})();
|
|
64
|
+
`;
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { LooseErrorResponse, LooseType, SuccessResponse, TwitterOpenAPIModelsMapping } from "./twitter-types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* @classdesc Base class for operating Twitter API Browser
|
|
4
|
+
*/
|
|
5
|
+
export declare class TwitterAPIBrowser {
|
|
6
|
+
private readonly userDataDir;
|
|
7
|
+
/**
|
|
8
|
+
* @description Constructor for TwitterAPIBrowser.
|
|
9
|
+
* @param userDataDir - The directory to store the user data for the browser (example: "./.user_data")
|
|
10
|
+
*/
|
|
11
|
+
constructor(userDataDir?: string);
|
|
12
|
+
private browser;
|
|
13
|
+
private page;
|
|
14
|
+
private operations;
|
|
15
|
+
private initialState;
|
|
16
|
+
/**
|
|
17
|
+
* @description Launches a new browser context and page, and then injects scripts.
|
|
18
|
+
* @param waitForReady - The number of seconds to wait for the browser to be ready.
|
|
19
|
+
* @param headless - Whether to run the browser in headless mode. (Recommended: false)
|
|
20
|
+
*/
|
|
21
|
+
setup(waitForReady?: number, headless?: boolean): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* @description Closes the browser context and page.
|
|
24
|
+
*/
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
[Symbol.asyncDispose]: () => Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* @description Is user logged in?
|
|
29
|
+
*/
|
|
30
|
+
isLoggedIn(): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* @description Manually login to Twitter.
|
|
33
|
+
*/
|
|
34
|
+
manualLogin(): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* @description Send a GraphQL request to the Twitter API
|
|
37
|
+
* @param method - The method of the request
|
|
38
|
+
* @param path - The path of the request
|
|
39
|
+
* @param body - The body of the request
|
|
40
|
+
* @returns Response of the request
|
|
41
|
+
*/
|
|
42
|
+
graphql<T extends string = string>(method: string, path: string, body: LooseType): Promise<SuccessResponse<T> | LooseErrorResponse>;
|
|
43
|
+
/**
|
|
44
|
+
* Sends a request to the Twitter API
|
|
45
|
+
* @param operationName - The name of the operation to request
|
|
46
|
+
* @param variables - The variables to pass to the operation
|
|
47
|
+
* @param fieldToggles - The fields to toggle on or off in the operation
|
|
48
|
+
* @returns Response of the request
|
|
49
|
+
*/
|
|
50
|
+
request<T extends keyof TwitterOpenAPIModelsMapping = string>(operationName: T, variables: LooseType, fieldToggles?: Record<string, boolean>): Promise<SuccessResponse<T> | LooseErrorResponse>;
|
|
51
|
+
}
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Twitter API Browser for JavaScript
|
|
3
|
+
* @module
|
|
4
|
+
* @exports TwitterAPIBrowser
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import { INITIAL_STATE_SCRIPT, OPERATIONS_SCRIPT, SETUP_SCRIPT, } from "./inject.js";
|
|
8
|
+
import { DEFAULT_USER_DATA_DIR, INITIAL_STATE_GLOBAL_KEY, METHOD_MAP, OPERATIONS_GLOBAL_KEY, REQUEST_FUNC_GLOBAL_KEY, } from "./consts.js";
|
|
9
|
+
import { chromium } from "playwright";
|
|
10
|
+
import { pickFirstItem, removeNullRecursively, sleep } from "./utils.js";
|
|
11
|
+
/**
|
|
12
|
+
* @classdesc Base class for operating Twitter API Browser
|
|
13
|
+
*/
|
|
14
|
+
export class TwitterAPIBrowser {
|
|
15
|
+
userDataDir;
|
|
16
|
+
/**
|
|
17
|
+
* @description Constructor for TwitterAPIBrowser.
|
|
18
|
+
* @param userDataDir - The directory to store the user data for the browser (example: "./.user_data")
|
|
19
|
+
*/
|
|
20
|
+
constructor(userDataDir = DEFAULT_USER_DATA_DIR) {
|
|
21
|
+
this.userDataDir = userDataDir;
|
|
22
|
+
if (!fs.existsSync(userDataDir)) {
|
|
23
|
+
fs.mkdirSync(userDataDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
browser;
|
|
27
|
+
page;
|
|
28
|
+
operations;
|
|
29
|
+
initialState;
|
|
30
|
+
/**
|
|
31
|
+
* @description Launches a new browser context and page, and then injects scripts.
|
|
32
|
+
* @param waitForReady - The number of seconds to wait for the browser to be ready.
|
|
33
|
+
* @param headless - Whether to run the browser in headless mode. (Recommended: false)
|
|
34
|
+
*/
|
|
35
|
+
async setup(waitForReady = 5, headless = false) {
|
|
36
|
+
if (this.browser && this.page) {
|
|
37
|
+
await this.close();
|
|
38
|
+
}
|
|
39
|
+
const { resolve, reject, promise: browserPromise, } = Promise.withResolvers();
|
|
40
|
+
chromium
|
|
41
|
+
.launchPersistentContext(this.userDataDir, {
|
|
42
|
+
headless: headless,
|
|
43
|
+
viewport: null,
|
|
44
|
+
args: [
|
|
45
|
+
"--disable-blink-features=AutomationControlled",
|
|
46
|
+
"--no-sandbox",
|
|
47
|
+
"--disable-dev-shm-usage",
|
|
48
|
+
"--disable-gpu",
|
|
49
|
+
],
|
|
50
|
+
})
|
|
51
|
+
.then(resolve)
|
|
52
|
+
.catch(reject);
|
|
53
|
+
const raceResult = await Promise.race([
|
|
54
|
+
browserPromise,
|
|
55
|
+
sleep(waitForReady * 1000, new Error("Browser launch timed out")),
|
|
56
|
+
]);
|
|
57
|
+
if (raceResult instanceof Error) {
|
|
58
|
+
throw raceResult;
|
|
59
|
+
}
|
|
60
|
+
this.browser = raceResult;
|
|
61
|
+
this.page = await this.browser.newPage();
|
|
62
|
+
await this.page.addInitScript(SETUP_SCRIPT);
|
|
63
|
+
await this.page.addInitScript(OPERATIONS_SCRIPT);
|
|
64
|
+
await this.page.addInitScript(INITIAL_STATE_SCRIPT);
|
|
65
|
+
await this.page.goto("https://x.com/home");
|
|
66
|
+
await sleep(waitForReady * 1000);
|
|
67
|
+
this.operations = await this.page.evaluate(`globalThis.${OPERATIONS_GLOBAL_KEY}`);
|
|
68
|
+
this.initialState = await this.page.evaluate(`globalThis.${INITIAL_STATE_GLOBAL_KEY}`);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* @description Closes the browser context and page.
|
|
72
|
+
*/
|
|
73
|
+
async close() {
|
|
74
|
+
if (this.browser && this.page) {
|
|
75
|
+
await this.page.close();
|
|
76
|
+
await this.browser.close();
|
|
77
|
+
}
|
|
78
|
+
// NOTE: Patch memory leak
|
|
79
|
+
this.browser = undefined;
|
|
80
|
+
this.page = undefined;
|
|
81
|
+
this.operations = undefined;
|
|
82
|
+
this.initialState = undefined;
|
|
83
|
+
}
|
|
84
|
+
[Symbol.asyncDispose] = this.close;
|
|
85
|
+
/**
|
|
86
|
+
* @description Is user logged in?
|
|
87
|
+
*/
|
|
88
|
+
async isLoggedIn() {
|
|
89
|
+
const cookies = await this.browser?.cookies("https://x.com");
|
|
90
|
+
if (!cookies) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return cookies.some((cookie) => cookie.name === "auth_token");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* @description Manually login to Twitter.
|
|
97
|
+
*/
|
|
98
|
+
async manualLogin() {
|
|
99
|
+
if (!this.page) {
|
|
100
|
+
throw new Error("Maybe you forgot to call setup()?");
|
|
101
|
+
}
|
|
102
|
+
await this.page.goto("https://x.com/login");
|
|
103
|
+
await sleep(1000);
|
|
104
|
+
// NOTE: Wait for the page to load the home page after login
|
|
105
|
+
await this.page.waitForURL("https://x.com/home", {
|
|
106
|
+
timeout: 0,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* @description Send a GraphQL request to the Twitter API
|
|
111
|
+
* @param method - The method of the request
|
|
112
|
+
* @param path - The path of the request
|
|
113
|
+
* @param body - The body of the request
|
|
114
|
+
* @returns Response of the request
|
|
115
|
+
*/
|
|
116
|
+
async graphql(method, path, body) {
|
|
117
|
+
if (!this.page) {
|
|
118
|
+
throw new Error("Maybe you forgot to call setup()?");
|
|
119
|
+
}
|
|
120
|
+
const args = {
|
|
121
|
+
headers: {
|
|
122
|
+
"content-type": "application/json",
|
|
123
|
+
},
|
|
124
|
+
method,
|
|
125
|
+
path,
|
|
126
|
+
params: null,
|
|
127
|
+
data: null,
|
|
128
|
+
};
|
|
129
|
+
if (method === "GET") {
|
|
130
|
+
const params = Object.fromEntries(Object.entries(body).map(([k, v]) => [k, JSON.stringify(v)]));
|
|
131
|
+
args["params"] = {
|
|
132
|
+
queryId: body.queryId,
|
|
133
|
+
variables: JSON.stringify(body.variables),
|
|
134
|
+
};
|
|
135
|
+
if (body.features) {
|
|
136
|
+
args["params"]["features"] = JSON.stringify(body.features);
|
|
137
|
+
}
|
|
138
|
+
if (body.fieldToggle) {
|
|
139
|
+
args["params"]["fieldToggle"] = JSON.stringify(body.fieldToggle);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (method === "POST") {
|
|
143
|
+
args["data"] = body;
|
|
144
|
+
}
|
|
145
|
+
await this.page.evaluate(SETUP_SCRIPT);
|
|
146
|
+
await sleep(500);
|
|
147
|
+
const response = [await this.page.evaluate(`globalThis.${REQUEST_FUNC_GLOBAL_KEY}(${JSON.stringify(removeNullRecursively(args))})`)].flat();
|
|
148
|
+
const result = pickFirstItem(response, "response");
|
|
149
|
+
if (result instanceof Error) {
|
|
150
|
+
console.log(`!!! Please report this error to the developer !!!`);
|
|
151
|
+
throw result;
|
|
152
|
+
}
|
|
153
|
+
// TODO: more strict type check
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Sends a request to the Twitter API
|
|
158
|
+
* @param operationName - The name of the operation to request
|
|
159
|
+
* @param variables - The variables to pass to the operation
|
|
160
|
+
* @param fieldToggles - The fields to toggle on or off in the operation
|
|
161
|
+
* @returns Response of the request
|
|
162
|
+
*/
|
|
163
|
+
async request(operationName, variables, fieldToggles = {}) {
|
|
164
|
+
if (!this.page || !this.operations || !this.initialState) {
|
|
165
|
+
throw new Error("Maybe you forgot to call setup()?");
|
|
166
|
+
}
|
|
167
|
+
const exp = pickFirstItem(this.operations.filter((x) => x.operationName === operationName) ?? [], "operation");
|
|
168
|
+
if (exp instanceof Error) {
|
|
169
|
+
throw exp;
|
|
170
|
+
}
|
|
171
|
+
const queryId = exp.queryId;
|
|
172
|
+
const operationType = exp.operationType;
|
|
173
|
+
const featureSwitches = exp.metadata.featureSwitches;
|
|
174
|
+
const allowFieldToggles = exp.metadata.allowFieldToggles;
|
|
175
|
+
const fieldToggle = {};
|
|
176
|
+
for (const [k, v] of Object.entries(fieldToggles)) {
|
|
177
|
+
if (allowFieldToggles && allowFieldToggles.includes(k)) {
|
|
178
|
+
fieldToggle[k] = v;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const method = METHOD_MAP[operationType];
|
|
182
|
+
const featureSwitch = {
|
|
183
|
+
...this.initialState.featureSwitch.defaultConfig,
|
|
184
|
+
...this.initialState.featureSwitch.user,
|
|
185
|
+
...this.initialState.featureSwitch.debug,
|
|
186
|
+
...this.initialState.featureSwitch.customOverrides,
|
|
187
|
+
};
|
|
188
|
+
const featureSwitchesMap = Object.fromEntries(Object.entries(featureSwitch).filter(([k]) => featureSwitches.includes(k)).map(([k, v]) => [k, v.value]));
|
|
189
|
+
const body = {
|
|
190
|
+
variables: variables,
|
|
191
|
+
queryId: queryId,
|
|
192
|
+
features: null,
|
|
193
|
+
fieldToggle: null,
|
|
194
|
+
};
|
|
195
|
+
if (featureSwitchesMap && Object.keys(featureSwitchesMap).length > 0) {
|
|
196
|
+
body.features = featureSwitchesMap;
|
|
197
|
+
}
|
|
198
|
+
if (fieldToggle && Object.keys(fieldToggle).length > 0) {
|
|
199
|
+
body.fieldToggle = fieldToggle;
|
|
200
|
+
}
|
|
201
|
+
return await this.graphql(method, `/graphql/${queryId}/${operationName}`, body);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type * as TwitterOpenAPIModels from "twitter-openapi-typescript-generated/dist/models/index.d.ts";
|
|
2
|
+
export type LooseType = any;
|
|
3
|
+
export type TwitterOpenAPIModelsMapping = {
|
|
4
|
+
"CreateTweet": TwitterOpenAPIModels.CreateTweet;
|
|
5
|
+
"HomeTimeline": TwitterOpenAPIModels.HomeTimelineHome;
|
|
6
|
+
"UserByScreenName": TwitterOpenAPIModels.UserResultByScreenName;
|
|
7
|
+
"CreateRetweet": TwitterOpenAPIModels.CreateRetweet;
|
|
8
|
+
"FavoriteTweet": TwitterOpenAPIModels.FavoriteTweet;
|
|
9
|
+
"SearchTimeline": TwitterOpenAPIModels.SearchTimeline;
|
|
10
|
+
"UsersByRestIds": TwitterOpenAPIModels.UsersResponse;
|
|
11
|
+
} & Record<string, LooseType>;
|
|
12
|
+
export type SuccessResponse<T extends string> = TwitterOpenAPIModelsMapping[T];
|
|
13
|
+
export type LooseErrorResponse = {
|
|
14
|
+
erros: TwitterOpenAPIModels.ErrorResponse[];
|
|
15
|
+
data: {};
|
|
16
|
+
} & Record<string, LooseType>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const pickFirstItem: <T>(values: T[], itemName?: string) => T | Error;
|
|
2
|
+
export declare const sleep: <T>(ms: number, value?: T) => Promise<T>;
|
|
3
|
+
type RemoveNullRecursively<T> = T extends null ? never : T extends (infer U)[] ? RemoveNullRecursively<U>[] : T extends Record<string, unknown> ? {
|
|
4
|
+
[K in keyof T as T[K] extends null ? never : K]: RemoveNullRecursively<T[K]>;
|
|
5
|
+
} : T;
|
|
6
|
+
export declare const removeNullRecursively: <T>(obj: T) => RemoveNullRecursively<T>;
|
|
7
|
+
export {};
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const pickFirstItem = (values, itemName = "item") => {
|
|
2
|
+
if (values.length === 0) {
|
|
3
|
+
return new Error(`No ${itemName} provided`);
|
|
4
|
+
}
|
|
5
|
+
else if (values.length > 1) {
|
|
6
|
+
return new Error(`Multiple ${itemName} provided`);
|
|
7
|
+
}
|
|
8
|
+
return values[0];
|
|
9
|
+
};
|
|
10
|
+
export const sleep = (ms, value = undefined) => {
|
|
11
|
+
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
|
|
12
|
+
};
|
|
13
|
+
export const removeNullRecursively = (obj) => {
|
|
14
|
+
if (obj === null || obj === undefined) {
|
|
15
|
+
return obj;
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(obj)) {
|
|
18
|
+
return obj
|
|
19
|
+
.map((item) => removeNullRecursively(item))
|
|
20
|
+
.filter((item) => item !== null);
|
|
21
|
+
}
|
|
22
|
+
if (typeof obj === "object") {
|
|
23
|
+
const result = {};
|
|
24
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
25
|
+
if (value !== null) {
|
|
26
|
+
const cleanedValue = removeNullRecursively(value);
|
|
27
|
+
if (cleanedValue !== null) {
|
|
28
|
+
result[key] = cleanedValue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
return obj;
|
|
35
|
+
};
|