emusks 0.0.2 → 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.
Files changed (43) hide show
  1. package/README.md +4 -277
  2. package/build/graphql.js +24 -0
  3. package/build/v1.1.js +28 -0
  4. package/build/v2.js +28 -0
  5. package/package.json +16 -8
  6. package/src/clients.js +62 -0
  7. package/src/cycletls.js +26 -0
  8. package/src/flow.js +399 -0
  9. package/src/graphql.js +70 -0
  10. package/src/helpers/account.js +271 -0
  11. package/src/helpers/bookmarks.js +120 -0
  12. package/src/helpers/communities.js +271 -0
  13. package/src/helpers/dms.js +131 -0
  14. package/src/helpers/index.js +32 -0
  15. package/src/helpers/lists.js +229 -0
  16. package/src/helpers/media.js +290 -0
  17. package/src/helpers/notifications.js +49 -0
  18. package/src/helpers/search.js +129 -0
  19. package/src/helpers/spaces.js +55 -0
  20. package/src/helpers/syndication.js +33 -0
  21. package/src/helpers/timelines.js +84 -0
  22. package/src/helpers/topics.js +99 -0
  23. package/src/helpers/trends.js +86 -0
  24. package/src/helpers/tweets.js +405 -0
  25. package/src/helpers/users.js +321 -0
  26. package/src/index.js +125 -55
  27. package/src/parsers/timeline.js +141 -0
  28. package/src/parsers/tweet.js +9 -2
  29. package/src/static/graphql.js +1 -0
  30. package/src/static/v1.1.js +1 -0
  31. package/src/static/v2.js +1 -0
  32. package/src/v1.1.js +64 -0
  33. package/src/v2.js +64 -0
  34. package/src/methods/bookmarks.js +0 -91
  35. package/src/methods/follow.js +0 -88
  36. package/src/methods/like.js +0 -87
  37. package/src/methods/notifications.js +0 -121
  38. package/src/methods/retweet.js +0 -91
  39. package/src/methods/search.js +0 -124
  40. package/src/methods/timeline.js +0 -215
  41. package/src/methods/tweet.js +0 -396
  42. package/src/methods/users.js +0 -209
  43. package/src/utils/headers.js +0 -23
package/src/flow.js ADDED
@@ -0,0 +1,399 @@
1
+ import getCycleTLS from "./cycletls.js";
2
+ import clients from "./clients.js";
3
+
4
+ const BASE_URL = "https://api.x.com/1.1/onboarding/task.json";
5
+ const GUEST_ACTIVATE_URL = "https://api.x.com/1.1/guest/activate.json";
6
+
7
+ const USER_AGENT =
8
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36";
9
+
10
+ const SUBTASK_VERSIONS = {
11
+ action_list: 2,
12
+ alert_dialog: 1,
13
+ app_download_cta: 1,
14
+ check_logged_in_account: 2,
15
+ choice_selection: 3,
16
+ contacts_live_sync_permission_prompt: 0,
17
+ cta: 7,
18
+ email_verification: 2,
19
+ end_flow: 1,
20
+ enter_date: 1,
21
+ enter_email: 2,
22
+ enter_password: 5,
23
+ enter_phone: 2,
24
+ enter_recaptcha: 1,
25
+ enter_text: 5,
26
+ generic_urt: 3,
27
+ in_app_notification: 1,
28
+ interest_picker: 3,
29
+ js_instrumentation: 1,
30
+ menu_dialog: 1,
31
+ notifications_permission_prompt: 2,
32
+ open_account: 2,
33
+ open_home_timeline: 1,
34
+ open_link: 1,
35
+ phone_verification: 4,
36
+ privacy_options: 1,
37
+ security_key: 3,
38
+ select_avatar: 4,
39
+ select_banner: 2,
40
+ settings_list: 7,
41
+ show_code: 1,
42
+ sign_up: 2,
43
+ sign_up_review: 4,
44
+ tweet_selection_urt: 1,
45
+ update_users: 1,
46
+ upload_media: 1,
47
+ user_recommendations_list: 4,
48
+ user_recommendations_urt: 1,
49
+ wait_spinner: 3,
50
+ web_modal: 1,
51
+ };
52
+
53
+ const MAX_FLOW_STEPS = 20;
54
+
55
+ class CookieSession {
56
+ constructor(cycleTLS, proxy) {
57
+ this.cycleTLS = cycleTLS;
58
+ this.cookies = {};
59
+ this.proxy = proxy;
60
+ }
61
+
62
+ getCookieString() {
63
+ return Object.entries(this.cookies)
64
+ .map(([k, v]) => `${k}=${v}`)
65
+ .join("; ");
66
+ }
67
+
68
+ parseCookies(cookieValue) {
69
+ if (!cookieValue) return;
70
+ const cookies = Array.isArray(cookieValue) ? cookieValue : [cookieValue];
71
+ for (const cookie of cookies) {
72
+ const parts = cookie.split(";")[0].split("=");
73
+ if (parts.length >= 2) {
74
+ this.cookies[parts[0].trim()] = parts.slice(1).join("=").trim();
75
+ }
76
+ }
77
+ }
78
+
79
+ async post(url, options = {}) {
80
+ const headers = options.headers || {};
81
+ const cookieString = this.getCookieString();
82
+ if (cookieString) {
83
+ headers["Cookie"] = cookieString;
84
+ }
85
+
86
+ const response = await this.cycleTLS(
87
+ url,
88
+ {
89
+ body: options.json ? JSON.stringify(options.json) : options.body,
90
+ ja3: clients.web.fingerprints.ja3,
91
+ ja4r: clients.web.fingerprints.ja4r,
92
+ userAgent: USER_AGENT,
93
+ headers,
94
+ proxy: this.proxy || undefined,
95
+ },
96
+ "post",
97
+ );
98
+
99
+ const setCookie = response.headers?.["Set-Cookie"] || response.headers?.["set-cookie"];
100
+ if (setCookie) {
101
+ this.parseCookies(setCookie);
102
+ }
103
+
104
+ return response;
105
+ }
106
+ }
107
+
108
+ function getFlowHeaders(guestToken) {
109
+ const headers = {
110
+ Authorization: `Bearer ${clients.tweetdeck.bearer}`,
111
+ "Content-Type": "application/json",
112
+ Accept: "*/*",
113
+ "Accept-Language": "en-US",
114
+ "X-Twitter-Client-Language": "en-US",
115
+ Origin: "https://x.com",
116
+ Referer: "https://x.com/",
117
+ };
118
+ if (guestToken) {
119
+ headers["X-Guest-Token"] = guestToken;
120
+ }
121
+ return headers;
122
+ }
123
+
124
+ async function makeRequest(session, headers, flowToken, subtaskData) {
125
+ const payload = {
126
+ flow_token: flowToken,
127
+ subtask_inputs: Array.isArray(subtaskData) ? subtaskData : [subtaskData],
128
+ };
129
+
130
+ const response = await session.post(BASE_URL, { json: payload, headers });
131
+
132
+ if (response.status !== 200) {
133
+ const errorBody =
134
+ typeof response.body === "string"
135
+ ? response.body
136
+ : JSON.stringify(response.body || response.data);
137
+ throw new Error(`Flow request failed: ${response.status} - ${errorBody}`);
138
+ }
139
+
140
+ const data =
141
+ typeof response.body === "string" ? JSON.parse(response.body) : response.body || response.data;
142
+
143
+ const newFlowToken = data.flow_token;
144
+ if (!newFlowToken) {
145
+ throw new Error("Failed to get flow token from response");
146
+ }
147
+
148
+ return [newFlowToken, data];
149
+ }
150
+
151
+ async function getGuestToken(session) {
152
+ const response = await session.post(GUEST_ACTIVATE_URL, {
153
+ headers: { Authorization: `Bearer ${clients.tweetdeck.bearer}` },
154
+ });
155
+
156
+ if (response.status !== 200) {
157
+ throw new Error("Failed to obtain guest token");
158
+ }
159
+
160
+ const data =
161
+ typeof response.body === "string" ? JSON.parse(response.body) : response.body || response.data;
162
+
163
+ const guestToken = data.guest_token;
164
+ if (!guestToken) {
165
+ throw new Error("Failed to obtain guest token");
166
+ }
167
+
168
+ return guestToken;
169
+ }
170
+
171
+ async function initFlow(session, guestToken) {
172
+ const headers = getFlowHeaders(guestToken);
173
+ const payload = {
174
+ input_flow_data: {
175
+ flow_context: {
176
+ debug_overrides: {},
177
+ start_location: { location: "manual_link" },
178
+ },
179
+ subtask_versions: SUBTASK_VERSIONS,
180
+ },
181
+ };
182
+
183
+ const response = await session.post(`${BASE_URL}?flow_name=login`, {
184
+ json: payload,
185
+ headers,
186
+ });
187
+
188
+ if (response.status !== 200) {
189
+ throw new Error("Failed to initialize login flow");
190
+ }
191
+
192
+ const data =
193
+ typeof response.body === "string" ? JSON.parse(response.body) : response.body || response.data;
194
+
195
+ const flowToken = data.flow_token;
196
+ if (!flowToken) {
197
+ throw new Error("Failed to get initial flow token");
198
+ }
199
+
200
+ return [flowToken, headers, data];
201
+ }
202
+
203
+ async function submitJsInstrumentation(session, flowToken, headers) {
204
+ return await makeRequest(session, headers, flowToken, {
205
+ subtask_id: "LoginJsInstrumentationSubtask",
206
+ js_instrumentation: {
207
+ response:
208
+ '{"rf":{"a4fc506d24bb4843c48a1966940c2796bf4fb7617a2d515ad3297b7df6b459b6":121,"bff66e16f1d7ea28c04653dc32479cf416a9c8b67c80cb8ad533b2a44fee82a3":-1,"ac4008077a7e6ca03210159dbe2134dea72a616f03832178314bb9931645e4f7":-22,"c3a8a81a9b2706c6fec42c771da65a9597c537b8e4d9b39e8e58de9fe31ff239":-12},"s":"ZHYaDA9iXRxOl2J3AZ9cc23iJx-Fg5E82KIBA_fgeZFugZGYzRtf8Bl3EUeeYgsK30gLFD2jTQx9fAMsnYCw0j8ahEy4Pb5siM5zD6n7YgOeWmFFaXoTwaGY4H0o-jQnZi5yWZRAnFi4lVuCVouNz_xd2BO2sobCO7QuyOsOxQn2CWx7bjD8vPAzT5BS1mICqUWyjZDjLnRZJU6cSQG5YFIHEPBa8Kj-v1JFgkdAfAMIdVvP7C80HWoOqYivQR7IBuOAI4xCeLQEdxlGeT-JYStlP9dcU5St7jI6ExyMeQnRicOcxXLXsan8i5Joautk2M8dAJFByzBaG4wtrPhQ3QAAAZEi-_t7"}',
209
+ link: "next_link",
210
+ },
211
+ });
212
+ }
213
+
214
+ async function submitUsername(session, flowToken, headers, username) {
215
+ const [newFlowToken, data] = await makeRequest(session, headers, flowToken, {
216
+ subtask_id: "LoginEnterUserIdentifierSSO",
217
+ settings_list: {
218
+ setting_responses: [
219
+ {
220
+ key: "user_identifier",
221
+ response_data: { text_data: { result: username } },
222
+ },
223
+ ],
224
+ link: "next_link",
225
+ },
226
+ });
227
+
228
+ if (data.subtasks?.[0]?.cta?.primary_text?.text) {
229
+ throw new Error(`Login denied: ${data.subtasks[0].cta.primary_text.text}`);
230
+ }
231
+
232
+ return [newFlowToken, data];
233
+ }
234
+
235
+ async function submitPassword(session, flowToken, headers, password) {
236
+ return await makeRequest(session, headers, flowToken, {
237
+ subtask_id: "LoginEnterPassword",
238
+ enter_password: { password, link: "next_link" },
239
+ });
240
+ }
241
+
242
+ async function submitAlternateIdentifier(session, flowToken, headers, text) {
243
+ return await makeRequest(session, headers, flowToken, {
244
+ subtask_id: "LoginEnterAlternateIdentifierSubtask",
245
+ enter_text: { text: text.trim(), link: "next_link" },
246
+ });
247
+ }
248
+
249
+ async function submit2FA(session, flowToken, headers, code) {
250
+ return await makeRequest(session, headers, flowToken, {
251
+ subtask_id: "LoginTwoFactorAuthChallenge",
252
+ enter_text: { text: code.trim(), link: "next_link" },
253
+ });
254
+ }
255
+
256
+ async function submitLoginAcid(session, flowToken, headers, code) {
257
+ return await makeRequest(session, headers, flowToken, {
258
+ subtask_id: "LoginAcid",
259
+ enter_text: { text: code.trim(), link: "next_link" },
260
+ });
261
+ }
262
+
263
+ async function submitAccountDuplicationCheck(session, flowToken, headers) {
264
+ return await makeRequest(session, headers, flowToken, {
265
+ subtask_id: "AccountDuplicationCheck",
266
+ check_logged_in_account: { link: "AccountDuplicationCheck_false" },
267
+ });
268
+ }
269
+
270
+ function getSubtaskIds(data) {
271
+ if (!data?.subtasks) return [];
272
+ return data.subtasks.map((s) => s.subtask_id);
273
+ }
274
+
275
+ function isLoginComplete(data) {
276
+ if (!data?.subtasks) return false;
277
+ for (const subtask of data.subtasks) {
278
+ if (subtask.open_account || subtask.subtask_id === "LoginSuccessSubtask") {
279
+ return true;
280
+ }
281
+ }
282
+ return false;
283
+ }
284
+
285
+ function extractUserId(cookies) {
286
+ const twid = (cookies.twid || "").replace(/"/g, "");
287
+ for (const prefix of ["u=", "u%3D"]) {
288
+ if (twid.includes(prefix)) {
289
+ return twid.split(prefix)[1].split("&")[0].replace(/"/g, "");
290
+ }
291
+ }
292
+ return null;
293
+ }
294
+
295
+ async function resolve(staticValue, onRequest, type) {
296
+ if (staticValue) return staticValue;
297
+
298
+ if (onRequest) {
299
+ const value = await onRequest(type);
300
+ if (value != null && value !== "") return value;
301
+ }
302
+
303
+ throw new Error(
304
+ `the login flow is asking for "${type}" but no value was provided. either pass it directly or handle it in onRequest.`,
305
+ );
306
+ }
307
+
308
+ export default async function flowLogin(opts) {
309
+ const { username, password, email, phone, onRequest, proxy } = opts;
310
+
311
+ if (!username) throw new Error("username is required for flow login");
312
+ if (!password) throw new Error("password is required for flow login");
313
+
314
+ const cycleTLS = await getCycleTLS();
315
+ const session = new CookieSession(cycleTLS, proxy);
316
+
317
+ const guestToken = await getGuestToken(session);
318
+
319
+ let [flowToken, headers, data] = await initFlow(session, guestToken);
320
+ headers["X-Guest-Token"] = guestToken;
321
+
322
+ let step = 0;
323
+
324
+ while (!isLoginComplete(data) && step < MAX_FLOW_STEPS) {
325
+ step++;
326
+ const subtaskIds = getSubtaskIds(data);
327
+
328
+ if (subtaskIds.length === 0) {
329
+ throw new Error("Login flow returned no subtasks and login is not complete");
330
+ }
331
+
332
+ const current = subtaskIds[0];
333
+
334
+ switch (current) {
335
+ case "LoginJsInstrumentationSubtask":
336
+ [flowToken, data] = await submitJsInstrumentation(session, flowToken, headers);
337
+ break;
338
+
339
+ case "LoginEnterUserIdentifierSSO":
340
+ [flowToken, data] = await submitUsername(session, flowToken, headers, username);
341
+ break;
342
+
343
+ case "LoginEnterPassword":
344
+ [flowToken, data] = await submitPassword(session, flowToken, headers, password);
345
+ break;
346
+
347
+ case "LoginEnterAlternateIdentifierSubtask": {
348
+ const value = await resolve(email || phone, onRequest, "alternate_identifier");
349
+ [flowToken, data] = await submitAlternateIdentifier(session, flowToken, headers, value);
350
+ break;
351
+ }
352
+
353
+ case "LoginTwoFactorAuthChallenge": {
354
+ const code = await resolve(null, onRequest, "two_factor_code");
355
+ [flowToken, data] = await submit2FA(session, flowToken, headers, code);
356
+ break;
357
+ }
358
+
359
+ case "LoginAcid": {
360
+ const code = await resolve(null, onRequest, "email_code");
361
+ [flowToken, data] = await submitLoginAcid(session, flowToken, headers, code);
362
+ break;
363
+ }
364
+
365
+ case "AccountDuplicationCheck":
366
+ [flowToken, data] = await submitAccountDuplicationCheck(session, flowToken, headers);
367
+ break;
368
+
369
+ case "LoginSuccessSubtask":
370
+ break;
371
+
372
+ case "DenyLoginSubtask":
373
+ throw new Error("Login was denied by Twitter. Your account may be locked or suspended.");
374
+
375
+ default:
376
+ throw new Error(
377
+ `Unhandled login subtask: "${current}". All subtasks: [${subtaskIds.join(", ")}]`,
378
+ );
379
+ }
380
+ }
381
+
382
+ if (!isLoginComplete(data)) {
383
+ throw new Error(
384
+ `Login flow did not complete after ${MAX_FLOW_STEPS} steps. ` +
385
+ `Last subtasks: [${getSubtaskIds(data).join(", ")}]`,
386
+ );
387
+ }
388
+
389
+ const cookies = { ...session.cookies };
390
+ const authToken = cookies.auth_token || null;
391
+ const csrfToken = cookies.ct0 || null;
392
+ const userId = extractUserId(cookies);
393
+
394
+ if (!authToken) {
395
+ throw new Error("Login flow completed but no auth_token was found in cookies");
396
+ }
397
+
398
+ return { authToken, csrfToken, userId, cookies };
399
+ }
package/src/graphql.js ADDED
@@ -0,0 +1,70 @@
1
+ import getCycleTLS from "./cycletls.js";
2
+ import graphqlApi from "./static/graphql.js";
3
+
4
+ export default async function graphql(queryName, { variables, fieldToggles, body, headers } = {}) {
5
+ const entry = graphqlApi[queryName];
6
+ if (!entry) {
7
+ throw new Error(`graphql query ${queryName} not found`);
8
+ }
9
+
10
+ const [method, baseUrl] = entry;
11
+
12
+ let finalUrl = baseUrl;
13
+
14
+ if (variables && Object.keys(variables).length) {
15
+ const separator = baseUrl.includes("?") ? "&" : "?";
16
+ finalUrl = `${baseUrl}${separator}variables=${encodeURIComponent(JSON.stringify(variables))}`;
17
+ }
18
+
19
+ if (fieldToggles && Object.keys(fieldToggles).length) {
20
+ const separator = finalUrl.includes("?") ? "&" : "?";
21
+ finalUrl = `${finalUrl}${separator}fieldToggles=${encodeURIComponent(JSON.stringify(fieldToggles))}`;
22
+ }
23
+
24
+ const url = new URL(finalUrl);
25
+ const pathname = url.pathname;
26
+
27
+ const requestHeaders = {
28
+ accept: "*/*",
29
+ "accept-language": "en-US,en;q=0.9",
30
+ authorization: `Bearer ${this.auth.client.bearer}`,
31
+ "content-type": "application/json",
32
+ "x-csrf-token": this.auth.csrfToken,
33
+ "x-twitter-active-user": "yes",
34
+ "x-twitter-auth-type": "OAuth2Session",
35
+ "x-twitter-client-language": "en",
36
+ "x-client-transaction-id": await this.auth.generateTransactionId(
37
+ method.toUpperCase(),
38
+ pathname,
39
+ ),
40
+ priority: "u=1, i",
41
+ "sec-ch-ua": '"Not(A:Brand";v="8", "Chromium";v="144"',
42
+ "sec-ch-ua-mobile": "?0",
43
+ "sec-ch-ua-platform": '"macOS"',
44
+ "sec-fetch-dest": "empty",
45
+ "sec-fetch-mode": "cors",
46
+ "sec-fetch-site": "same-origin",
47
+ "sec-gpc": "1",
48
+ cookie:
49
+ this.auth.client.headers.cookie + (this.elevatedCookies ? `; ${this.elevatedCookies}` : ""),
50
+ ...headers,
51
+ };
52
+
53
+ const cycleTLS = await getCycleTLS();
54
+
55
+ return await (
56
+ await cycleTLS(
57
+ finalUrl,
58
+ {
59
+ headers: requestHeaders,
60
+ userAgent: this.auth.client.fingerprints.userAgent,
61
+ ja3: this.auth.client.fingerprints.ja3,
62
+ ja4r: this.auth.client.fingerprints.ja4r,
63
+ body: typeof body === "object" ? JSON.stringify(body) : undefined,
64
+ proxy: this.proxy || undefined,
65
+ referrer: "https://x.com/",
66
+ },
67
+ method,
68
+ )
69
+ ).json();
70
+ }