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.
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 +274 -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 +403 -0
  25. package/src/helpers/users.js +321 -0
  26. package/src/index.js +125 -55
  27. package/src/parsers/timeline.js +105 -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,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
+ }