emusks 0.0.3 → 2.0.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 +4 -277
- package/build/graphql.js +24 -0
- package/build/v1.1.js +28 -0
- package/build/v2.js +28 -0
- package/package.json +16 -8
- package/src/clients.js +62 -0
- package/src/cycletls.js +26 -0
- package/src/flow.js +399 -0
- package/src/graphql.js +70 -0
- package/src/helpers/account.js +271 -0
- package/src/helpers/bookmarks.js +120 -0
- package/src/helpers/communities.js +271 -0
- package/src/helpers/dms.js +131 -0
- package/src/helpers/index.js +32 -0
- package/src/helpers/lists.js +229 -0
- package/src/helpers/media.js +274 -0
- package/src/helpers/notifications.js +49 -0
- package/src/helpers/search.js +129 -0
- package/src/helpers/spaces.js +55 -0
- package/src/helpers/syndication.js +33 -0
- package/src/helpers/timelines.js +84 -0
- package/src/helpers/topics.js +99 -0
- package/src/helpers/trends.js +86 -0
- package/src/helpers/tweets.js +403 -0
- package/src/helpers/users.js +321 -0
- package/src/index.js +125 -55
- package/src/parsers/timeline.js +105 -0
- package/src/static/graphql.js +1 -0
- package/src/static/v1.1.js +1 -0
- package/src/static/v2.js +1 -0
- package/src/v1.1.js +64 -0
- package/src/v2.js +64 -0
- package/src/methods/bookmarks.js +0 -91
- package/src/methods/follow.js +0 -88
- package/src/methods/like.js +0 -87
- package/src/methods/notifications.js +0 -121
- package/src/methods/retweet.js +0 -91
- package/src/methods/search.js +0 -124
- package/src/methods/timeline.js +0 -215
- package/src/methods/tweet.js +0 -396
- package/src/methods/users.js +0 -209
- package/src/utils/headers.js +0 -23
package/src/index.js
CHANGED
|
@@ -1,91 +1,161 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as following from "./methods/follow.js";
|
|
3
|
-
import * as liking from "./methods/like.js";
|
|
4
|
-
import * as notifications from "./methods/notifications.js";
|
|
5
|
-
import * as retweeting from "./methods/retweet.js";
|
|
6
|
-
import * as search from "./methods/search.js";
|
|
7
|
-
import * as timeline from "./methods/timeline.js";
|
|
8
|
-
import * as tweeting from "./methods/tweet.js";
|
|
9
|
-
import * as users from "./methods/users.js";
|
|
10
|
-
|
|
1
|
+
import { ClientTransaction, handleXMigration } from "x-client-transaction-id";
|
|
11
2
|
import parseUser from "./parsers/user.js";
|
|
12
|
-
import
|
|
3
|
+
import getCycleTLS from "./cycletls.js";
|
|
4
|
+
import clients from "./clients.js";
|
|
5
|
+
import graphql from "./graphql.js";
|
|
6
|
+
import v1_1 from "./v1.1.js";
|
|
7
|
+
import v2 from "./v2.js";
|
|
8
|
+
import flowLogin from "./flow.js";
|
|
9
|
+
import initHelpers from "./helpers/index.js";
|
|
10
|
+
|
|
11
|
+
export default class Emusks {
|
|
12
|
+
auth = null;
|
|
13
|
+
elevatedCookies = null;
|
|
14
|
+
graphql = graphql;
|
|
15
|
+
v1_1 = v1_1;
|
|
16
|
+
v2 = v2;
|
|
17
|
+
|
|
18
|
+
async elevate(password) {
|
|
19
|
+
if (!this.auth) throw new Error("must be logged in before calling elevate");
|
|
20
|
+
|
|
21
|
+
const res = await this.v1_1("account/verify_password", {
|
|
22
|
+
body: `password=${encodeURIComponent(password)}`,
|
|
23
|
+
headers: {
|
|
24
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
13
27
|
|
|
14
|
-
|
|
15
|
-
auth = {};
|
|
16
|
-
user = null;
|
|
28
|
+
const setCookies = res.headers["Set-Cookie"] || [];
|
|
17
29
|
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
30
|
+
const json = await res.json();
|
|
31
|
+
if (json.status !== "ok") {
|
|
32
|
+
throw new Error("invalid password");
|
|
21
33
|
}
|
|
34
|
+
|
|
35
|
+
const cookieParts = [];
|
|
36
|
+
for (const setCookie of setCookies) {
|
|
37
|
+
const cookiePair = setCookie.split(";")[0];
|
|
38
|
+
if (cookiePair) {
|
|
39
|
+
cookieParts.push(cookiePair);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.elevatedCookies = cookieParts.join("; ");
|
|
44
|
+
|
|
45
|
+
return json;
|
|
22
46
|
}
|
|
23
47
|
|
|
24
|
-
async login() {
|
|
25
|
-
if (
|
|
26
|
-
|
|
48
|
+
async login(p) {
|
|
49
|
+
if (typeof p === "string") {
|
|
50
|
+
if (p.length > 50 || p.length < 20) {
|
|
51
|
+
throw new Error("invalid auth token length!");
|
|
52
|
+
}
|
|
53
|
+
p = { auth_token: p };
|
|
27
54
|
}
|
|
28
55
|
|
|
29
|
-
|
|
30
|
-
|
|
56
|
+
if (p.type === "password") {
|
|
57
|
+
if (!p.username) throw new Error("username is required for password login");
|
|
58
|
+
if (!p.password) throw new Error("password is required for password login");
|
|
59
|
+
|
|
60
|
+
const flowResult = await flowLogin({
|
|
61
|
+
username: p.username,
|
|
62
|
+
password: p.password,
|
|
63
|
+
email: p.email,
|
|
64
|
+
phone: p.phone,
|
|
65
|
+
onRequest: p.onRequest,
|
|
66
|
+
proxy: p.proxy,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
p = {
|
|
70
|
+
auth_token: flowResult.authToken,
|
|
71
|
+
client: p.client,
|
|
72
|
+
proxy: p.proxy,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!p.client) p.client = "web";
|
|
77
|
+
if (!p.auth_token) throw new Error("auth_token is required!");
|
|
78
|
+
if (typeof p.client === "string") p.client = clients[p.client];
|
|
79
|
+
if (!p.client) throw new Error("invalid client!");
|
|
80
|
+
if (p.proxy) this.proxy = p.proxy;
|
|
81
|
+
|
|
82
|
+
p.client.headers = {
|
|
83
|
+
"accept-language": "en-US,en;q=0.9",
|
|
84
|
+
priority: "u=1, i",
|
|
85
|
+
"sec-ch-ua": '"Not(A:Brand";v="8", "Chromium";v="144"',
|
|
86
|
+
"sec-ch-ua-mobile": "?0",
|
|
87
|
+
"sec-ch-ua-platform": '"macOS"',
|
|
88
|
+
"sec-fetch-dest": "empty",
|
|
89
|
+
"sec-fetch-mode": "cors",
|
|
90
|
+
"sec-fetch-site": "same-site",
|
|
91
|
+
"sec-gpc": "1",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const cycleTLS = await getCycleTLS();
|
|
95
|
+
const res = await cycleTLS("https://x.com/", {
|
|
31
96
|
headers: {
|
|
32
97
|
accept:
|
|
33
98
|
"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",
|
|
34
|
-
cookie: `auth_token=${
|
|
35
|
-
|
|
36
|
-
...headers,
|
|
99
|
+
cookie: `auth_token=${p.auth_token};`,
|
|
100
|
+
...p.client.headers,
|
|
37
101
|
},
|
|
102
|
+
userAgent: p.client.fingerprints.userAgent,
|
|
103
|
+
ja3: p.client.fingerprints.ja3,
|
|
104
|
+
ja4r: p.client.fingerprints.ja4r,
|
|
105
|
+
proxy: p.proxy || undefined,
|
|
106
|
+
referrer: "https://x.com/",
|
|
38
107
|
});
|
|
39
108
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
109
|
+
const setCookies = res.headers["Set-Cookie"] || [];
|
|
110
|
+
const csrfToken = setCookies
|
|
111
|
+
.find((c) => c?.startsWith?.("ct0="))
|
|
112
|
+
?.split?.(";")?.[0]
|
|
113
|
+
?.split?.("=")?.[1];
|
|
44
114
|
|
|
45
115
|
if (!csrfToken) {
|
|
46
116
|
throw new Error("[emusks] failed to log in");
|
|
47
117
|
}
|
|
48
118
|
|
|
119
|
+
this.auth = p;
|
|
49
120
|
this.auth.csrfToken = csrfToken;
|
|
50
|
-
this.auth.cookies = `auth_token=${this.auth.token}; ct0=${csrfToken};`;
|
|
51
121
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
122
|
+
const cookieParts = [`auth_token=${p.auth_token}`];
|
|
123
|
+
for (const setCookie of setCookies) {
|
|
124
|
+
const cookiePair = setCookie.split(";")[0];
|
|
125
|
+
if (cookiePair && !cookiePair.startsWith("auth_token=")) {
|
|
126
|
+
cookieParts.push(cookiePair);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
this.auth.client.headers.cookie = cookieParts.join("; ");
|
|
130
|
+
|
|
131
|
+
const responseText = await res.text();
|
|
132
|
+
const initialStateMatch = responseText.match(/window\.__INITIAL_STATE__\s*=\s*({.*?});/s);
|
|
55
133
|
|
|
56
134
|
if (!initialStateMatch) {
|
|
57
|
-
console.
|
|
58
|
-
"[emusks] failed to extract initial state from response. auth will work, \n but user data will not be available (reason: no match)"
|
|
59
|
-
);
|
|
135
|
+
console.warn("[emusks] failed to extract initial state from response");
|
|
60
136
|
return;
|
|
61
137
|
}
|
|
62
138
|
|
|
63
|
-
const
|
|
64
|
-
|
|
139
|
+
const initialState = JSON.parse(initialStateMatch[1]);
|
|
140
|
+
const usersEntities = initialState?.entities?.users?.entities;
|
|
65
141
|
const initialStateUser = usersEntities && Object.values(usersEntities)[0];
|
|
66
142
|
|
|
67
143
|
if (!initialStateUser) {
|
|
68
|
-
console.
|
|
69
|
-
"[emusks] failed to extract initial state from response. auth will work, \n but user data will not be available (reason: user not parsed)"
|
|
70
|
-
);
|
|
144
|
+
console.warn("[emusks] failed to extract user from initial state");
|
|
71
145
|
return;
|
|
72
146
|
}
|
|
73
147
|
|
|
74
148
|
this.user = parseUser(initialStateUser);
|
|
75
|
-
this.settings =
|
|
149
|
+
this.settings = initialState?.settings?.remote?.settings;
|
|
150
|
+
|
|
151
|
+
const document = await handleXMigration();
|
|
152
|
+
const transaction = new ClientTransaction(document);
|
|
153
|
+
await transaction.initialize();
|
|
154
|
+
this.auth.generateTransactionId = transaction.generateTransactionId.bind(transaction);
|
|
155
|
+
|
|
156
|
+
const helpers = initHelpers(this);
|
|
157
|
+
Object.assign(this, helpers);
|
|
76
158
|
|
|
77
159
|
return this.user;
|
|
78
160
|
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
Object.assign(Client.prototype, tweeting);
|
|
82
|
-
Object.assign(Client.prototype, retweeting);
|
|
83
|
-
Object.assign(Client.prototype, liking);
|
|
84
|
-
Object.assign(Client.prototype, following);
|
|
85
|
-
Object.assign(Client.prototype, bookmarks);
|
|
86
|
-
Object.assign(Client.prototype, notifications);
|
|
87
|
-
Object.assign(Client.prototype, search);
|
|
88
|
-
Object.assign(Client.prototype, users);
|
|
89
|
-
Object.assign(Client.prototype, timeline);
|
|
90
|
-
|
|
91
|
-
export default Client;
|
|
161
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import parseTweet from "./tweet.js";
|
|
2
|
+
import parseUser from "./user.js";
|
|
3
|
+
|
|
4
|
+
function findInstructions(obj) {
|
|
5
|
+
if (!obj || typeof obj !== "object") return null;
|
|
6
|
+
|
|
7
|
+
if (Array.isArray(obj.instructions)) {
|
|
8
|
+
return obj.instructions;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
for (const key of Object.keys(obj)) {
|
|
12
|
+
const val = obj[key];
|
|
13
|
+
if (val && typeof val === "object") {
|
|
14
|
+
const found = findInstructions(val);
|
|
15
|
+
if (found) return found;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractTweetFromEntry(entry) {
|
|
23
|
+
const tweetResult =
|
|
24
|
+
entry?.content?.itemContent?.tweet_results?.result ||
|
|
25
|
+
entry?.item?.itemContent?.tweet_results?.result;
|
|
26
|
+
if (tweetResult) {
|
|
27
|
+
return parseTweet(tweetResult);
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractUserFromEntry(entry) {
|
|
33
|
+
const userResult =
|
|
34
|
+
entry?.content?.itemContent?.user_results?.result ||
|
|
35
|
+
entry?.item?.itemContent?.user_results?.result;
|
|
36
|
+
if (userResult) {
|
|
37
|
+
return parseUser(userResult);
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default function parseTimeline(raw) {
|
|
43
|
+
const instructions = findInstructions(raw?.data || raw);
|
|
44
|
+
|
|
45
|
+
if (!instructions) {
|
|
46
|
+
return { tweets: [], users: [], nextCursor: null, previousCursor: null, raw };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const tweets = [];
|
|
50
|
+
const users = [];
|
|
51
|
+
let nextCursor = null;
|
|
52
|
+
let previousCursor = null;
|
|
53
|
+
|
|
54
|
+
for (const instruction of instructions) {
|
|
55
|
+
if (instruction.type === "TimelineAddEntries" || instruction.entries) {
|
|
56
|
+
for (const entry of instruction.entries || []) {
|
|
57
|
+
processEntry(entry, tweets, users);
|
|
58
|
+
|
|
59
|
+
if (entry.entryId?.startsWith("cursor-bottom") || entry.entryId?.includes("cursor-bottom")) {
|
|
60
|
+
nextCursor =
|
|
61
|
+
entry.content?.value || entry.content?.itemContent?.value || null;
|
|
62
|
+
}
|
|
63
|
+
if (entry.entryId?.startsWith("cursor-top") || entry.entryId?.includes("cursor-top")) {
|
|
64
|
+
previousCursor =
|
|
65
|
+
entry.content?.value || entry.content?.itemContent?.value || null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (instruction.type === "TimelineReplaceEntry" && instruction.entry) {
|
|
71
|
+
processEntry(instruction.entry, tweets, users);
|
|
72
|
+
if (instruction.entry.entryId?.includes("cursor-bottom")) {
|
|
73
|
+
nextCursor =
|
|
74
|
+
instruction.entry.content?.value ||
|
|
75
|
+
instruction.entry.content?.itemContent?.value ||
|
|
76
|
+
null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { tweets, users, nextCursor, previousCursor, raw };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function processEntry(entry, tweets, users) {
|
|
85
|
+
const tweet = extractTweetFromEntry(entry);
|
|
86
|
+
if (tweet) {
|
|
87
|
+
tweets.push(tweet);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const user = extractUserFromEntry(entry);
|
|
92
|
+
if (user) {
|
|
93
|
+
users.push(user);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (entry.content?.items) {
|
|
98
|
+
for (const item of entry.content.items) {
|
|
99
|
+
const t = extractTweetFromEntry(item);
|
|
100
|
+
if (t) tweets.push(t);
|
|
101
|
+
const u = extractUserFromEntry(item);
|
|
102
|
+
if (u) users.push(u);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|