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.
Files changed (42) 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/static/graphql.js +1 -0
  29. package/src/static/v1.1.js +1 -0
  30. package/src/static/v2.js +1 -0
  31. package/src/v1.1.js +64 -0
  32. package/src/v2.js +64 -0
  33. package/src/methods/bookmarks.js +0 -91
  34. package/src/methods/follow.js +0 -88
  35. package/src/methods/like.js +0 -87
  36. package/src/methods/notifications.js +0 -121
  37. package/src/methods/retweet.js +0 -91
  38. package/src/methods/search.js +0 -124
  39. package/src/methods/timeline.js +0 -215
  40. package/src/methods/tweet.js +0 -396
  41. package/src/methods/users.js +0 -209
  42. package/src/utils/headers.js +0 -23
package/src/index.js CHANGED
@@ -1,91 +1,161 @@
1
- import * as bookmarks from "./methods/bookmarks.js";
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 headers from "./utils/headers.js";
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
- class Client {
15
- auth = {};
16
- user = null;
28
+ const setCookies = res.headers["Set-Cookie"] || [];
17
29
 
18
- constructor({ authToken } = {}) {
19
- if (authToken) {
20
- this.auth.token = authToken;
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 (!this.auth.token) {
26
- throw new Error("auth token is required to login");
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
- // gets the csrf token from headers
30
- const res = await fetch("https://pro.x.com/", {
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=${this.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 csrfToken = res.headers
41
- .get("set-cookie")
42
- .split(", ct0=")[1]
43
- .split(";")[0];
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 initialStateMatch = (await res.text()).match(
53
- /window\.__INITIAL_STATE__\s*=\s*({.*?});/s
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.error(
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 usersEntities = JSON.parse(initialStateMatch[1])?.entities?.users
64
- ?.entities;
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.error(
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 = JSON.parse(initialStateMatch[1])?.settings?.remote?.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
+ }