emusks 0.0.3 → 2.0.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 +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 +290 -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 +405 -0
- package/src/helpers/users.js +321 -0
- package/src/index.js +125 -55
- package/src/parsers/timeline.js +141 -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,141 @@
|
|
|
1
|
+
import parseTweet from "./tweet.js";
|
|
2
|
+
import parseUser from "./user.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Recursively find the `instructions` array from any Twitter timeline response.
|
|
6
|
+
*
|
|
7
|
+
* Known response shapes:
|
|
8
|
+
* data.home.home_timeline_urt.instructions
|
|
9
|
+
* data.user.result.timeline_v2.timeline.instructions
|
|
10
|
+
* data.search_by_raw_query.search_timeline.timeline.instructions
|
|
11
|
+
* data.bookmark_timeline_v2.timeline.instructions
|
|
12
|
+
* data.bookmark_search_timeline.timeline.instructions
|
|
13
|
+
* data.list.tweets_timeline.timeline.instructions
|
|
14
|
+
* data.timeline_by_id.timeline.instructions
|
|
15
|
+
* ... and many more
|
|
16
|
+
*/
|
|
17
|
+
function findInstructions(obj) {
|
|
18
|
+
if (!obj || typeof obj !== "object") return null;
|
|
19
|
+
|
|
20
|
+
if (Array.isArray(obj.instructions)) {
|
|
21
|
+
return obj.instructions;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const key of Object.keys(obj)) {
|
|
25
|
+
const val = obj[key];
|
|
26
|
+
if (val && typeof val === "object") {
|
|
27
|
+
const found = findInstructions(val);
|
|
28
|
+
if (found) return found;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract tweet results from a single timeline entry.
|
|
37
|
+
*/
|
|
38
|
+
function extractTweetFromEntry(entry) {
|
|
39
|
+
// Standard tweet entry
|
|
40
|
+
const tweetResult =
|
|
41
|
+
entry?.content?.itemContent?.tweet_results?.result ||
|
|
42
|
+
entry?.item?.itemContent?.tweet_results?.result;
|
|
43
|
+
if (tweetResult) {
|
|
44
|
+
return parseTweet(tweetResult);
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract user results from a single timeline entry.
|
|
51
|
+
*/
|
|
52
|
+
function extractUserFromEntry(entry) {
|
|
53
|
+
const userResult =
|
|
54
|
+
entry?.content?.itemContent?.user_results?.result ||
|
|
55
|
+
entry?.item?.itemContent?.user_results?.result;
|
|
56
|
+
if (userResult) {
|
|
57
|
+
return parseUser(userResult);
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse a raw Twitter timeline/search/bookmark GraphQL response into
|
|
64
|
+
* `{ tweets, users, nextCursor, previousCursor }`.
|
|
65
|
+
*
|
|
66
|
+
* Works universally across all timeline-shaped endpoints.
|
|
67
|
+
*/
|
|
68
|
+
export default function parseTimeline(raw) {
|
|
69
|
+
const instructions = findInstructions(raw?.data || raw);
|
|
70
|
+
|
|
71
|
+
if (!instructions) {
|
|
72
|
+
return { tweets: [], users: [], nextCursor: null, previousCursor: null, raw };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const tweets = [];
|
|
76
|
+
const users = [];
|
|
77
|
+
let nextCursor = null;
|
|
78
|
+
let previousCursor = null;
|
|
79
|
+
|
|
80
|
+
for (const instruction of instructions) {
|
|
81
|
+
const entries =
|
|
82
|
+
instruction.entries || // TimelineAddEntries
|
|
83
|
+
instruction.entry // TimelineReplaceEntry (single)
|
|
84
|
+
? [instruction.entry]
|
|
85
|
+
: null;
|
|
86
|
+
|
|
87
|
+
if (instruction.type === "TimelineAddEntries" || instruction.entries) {
|
|
88
|
+
for (const entry of instruction.entries || []) {
|
|
89
|
+
processEntry(entry, tweets, users);
|
|
90
|
+
|
|
91
|
+
// Extract cursors
|
|
92
|
+
if (entry.entryId?.startsWith("cursor-bottom") || entry.entryId?.includes("cursor-bottom")) {
|
|
93
|
+
nextCursor =
|
|
94
|
+
entry.content?.value || entry.content?.itemContent?.value || null;
|
|
95
|
+
}
|
|
96
|
+
if (entry.entryId?.startsWith("cursor-top") || entry.entryId?.includes("cursor-top")) {
|
|
97
|
+
previousCursor =
|
|
98
|
+
entry.content?.value || entry.content?.itemContent?.value || null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (instruction.type === "TimelineReplaceEntry" && instruction.entry) {
|
|
104
|
+
processEntry(instruction.entry, tweets, users);
|
|
105
|
+
if (instruction.entry.entryId?.includes("cursor-bottom")) {
|
|
106
|
+
nextCursor =
|
|
107
|
+
instruction.entry.content?.value ||
|
|
108
|
+
instruction.entry.content?.itemContent?.value ||
|
|
109
|
+
null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { tweets, users, nextCursor, previousCursor, raw };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function processEntry(entry, tweets, users) {
|
|
118
|
+
// Direct tweet entry (tweet-*)
|
|
119
|
+
const tweet = extractTweetFromEntry(entry);
|
|
120
|
+
if (tweet) {
|
|
121
|
+
tweets.push(tweet);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Direct user entry (user-*)
|
|
126
|
+
const user = extractUserFromEntry(entry);
|
|
127
|
+
if (user) {
|
|
128
|
+
users.push(user);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Module entries (conversationthread-*, profile-grid-*, etc.)
|
|
133
|
+
if (entry.content?.items) {
|
|
134
|
+
for (const item of entry.content.items) {
|
|
135
|
+
const t = extractTweetFromEntry(item);
|
|
136
|
+
if (t) tweets.push(t);
|
|
137
|
+
const u = extractUserFromEntry(item);
|
|
138
|
+
if (u) users.push(u);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|