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/README.md CHANGED
@@ -1,279 +1,6 @@
1
- # emusks — reverse-engineered Twitter API
1
+ <img src="docs/src/icon.svg" width="120">
2
+ <h1>emusks: Reverse-engineered Twitter API client</h1>
2
3
 
3
- emusks is a reverse-engineered Twitter API wrapper that **just works**. beta for now.
4
+ Log in and interact with the unofficial X API using any client identity web, Android, iOS, or TweetDeck
4
5
 
5
- ```javascript
6
- import emusks from "emusks";
7
-
8
- const client = new emusks({
9
- authToken: "your_auth_token_here",
10
- });
11
-
12
- await client.login();
13
-
14
- console.log(`logged in as ${client.user.username}`);
15
-
16
- const timeline = await client.getTimeline({
17
- count: 20,
18
- type: "foryou", // or "following"
19
- });
20
-
21
- console.log(`timeline has ${timeline.tweets.length} tweets`);
22
- ```
23
-
24
- ## features
25
-
26
- - **timeline**: get "For You" and "Following" timelines with full parsing
27
- - **tweets**: create, delete, and fetch tweets with polls and media
28
- - **interactions**: like, retweet, follow, bookmark functionality
29
- - **search**: search for tweets and users
30
- - **user data**: get detailed user profiles and statistics
31
- - **conversations**: handle threaded conversations and replies
32
- - **pagination**: cursor-based pagination for all timeline endpoints
33
-
34
- ## api reference
35
-
36
- ### timeline
37
-
38
- get tweets from your timeline with support for both "For You" and "Following" feeds:
39
-
40
- ```javascript
41
- const timeline = await client.getTimeline({
42
- count: 20, // tweets to fetch
43
- type: "foryou", // "foryou" or "following"
44
- cursor: undefined, // pagination cursor for next page
45
- includeAds: false, // include promoted content
46
- seenTweetIds: [], // array of tweet ids
47
- });
48
-
49
- // response structure:
50
- {
51
- tweets: [...], // array of parsed tweet objects
52
- conversations: [...], // array of conversation threads
53
- users: [...], // array of suggested users
54
- cursors: {
55
- top: "...", // cursor for newer tweets
56
- bottom: "..." // cursor for older tweets
57
- },
58
- metadata: {
59
- scribeConfig: {...} // Timeline metadata
60
- }
61
- }
62
- ```
63
-
64
- ### Tweets
65
-
66
- ```javascript
67
- // tweet something
68
-
69
- const result = await client.tweet({
70
- text: "Hello world!",
71
- reply: "1234567890", // (optional)
72
- poll: {
73
- // (optional) add a poll
74
- choices: ["Option 1", "Option 2"],
75
- duration_minutes: 1440,
76
- },
77
-
78
- // TODO: media upload & quote tweets
79
- });
80
-
81
- // delete a tweet
82
- await client.deleteTweet("1234567890");
83
-
84
- // get a single tweet (no auth required)
85
- const tweet = await client.getTweet("1234567890");
86
-
87
- // get full tweet with replies (auth required)
88
- const fullTweet = await client.getFullTweet("1234567890");
89
- ```
90
-
91
- ### Interactions
92
-
93
- ```javascript
94
- // like/unlike a tweet
95
- await client.likeTweet("1234567890");
96
- await client.unlikeTweet("1234567890");
97
-
98
- // retweet/unretweet
99
- await client.retweet("1234567890");
100
- await client.unretweet("1234567890");
101
-
102
- // follow/unfollow user
103
- await client.followUser("username");
104
- await client.unfollowUser("username");
105
-
106
- // bookmark/unbookmark
107
- await client.bookmarkTweet("1234567890");
108
- await client.unbookmarkTweet("1234567890");
109
- ```
110
-
111
- ### notifications
112
-
113
- ```javascript
114
- const notifications = await client.getNotifications({
115
- type: "All", // "All", "Verified", "Mentions"
116
- limit: 20, // number of notifications to fetch
117
- cursor: undefined, // pagination cursor for next page
118
- });
119
-
120
- // response structure:
121
- {
122
- entries: [...], // array of notification entries
123
- cursors: {
124
- top: "...", // cursor for newer notifications
125
- bottom: "..." // cursor for older notifications
126
- }
127
- }
128
-
129
- // paginate through notifications
130
- const nextPage = await client.getNotifications({
131
- type: "All",
132
- limit: 20,
133
- cursor: notifications.cursors.bottom
134
- });
135
- ```
136
-
137
- ### user tweets
138
-
139
- get tweets from a specific user with pagination:
140
-
141
- ```javascript
142
- const userTweets = await client.getUserTweets("1234567890", {
143
- count: 20, // number of tweets to fetch
144
- includePromotedContent: false, // include promoted content
145
- cursor: undefined // pagination cursor
146
- });
147
-
148
- // response structure:
149
- {
150
- tweets: [...], // array of parsed tweet objects
151
- cursors: {
152
- top: "...", // cursor for newer tweets
153
- bottom: "..." // cursor for older tweets
154
- }
155
- }
156
-
157
- // or:
158
- const user = await client.getUser("username");
159
- const tweets = await user.getTweets({
160
- count: 20,
161
- cursor: undefined
162
- });
163
- ```
164
-
165
- ### tweet replies
166
-
167
- get replies to a specific tweet with pagination:
168
-
169
- ```javascript
170
- // Get full tweet conversation with replies
171
- const fullTweet = await client.getFullTweet("1234567890", {
172
- rankingMode: "Relevance", // "Relevance" or "Recency"
173
- includeAds: false,
174
- cursor: undefined // pagination cursor for more replies
175
- });
176
-
177
- // response structure:
178
- {
179
- tweets: [...], // array including main tweet and replies
180
- cursors: {
181
- top: "...", // cursor for newer replies
182
- bottom: "..." // cursor for older replies
183
- }
184
- }
185
-
186
- // response structure:
187
- {
188
- mainTweet: {...}, // original tweet
189
- replies: [...], // array of reply tweets
190
- cursors: {
191
- top: "...",
192
- bottom: "..."
193
- }
194
- }
195
-
196
- // if you just need replies:
197
- const replies = await client.getTweetReplies("1234567890", {
198
- rankingMode: "Relevance",
199
- cursor: undefined
200
- });
201
- ```
202
-
203
- ### search
204
-
205
- search for tweets:
206
-
207
- ```javascript
208
- const searchResults = await client.search("bun javascript", {
209
- resultsCount: 20, // number of results to fetch
210
- product: "Latest",
211
- cursor: undefined // pagination cursor
212
- });
213
-
214
- // response structure:
215
- {
216
- entries: [...], // array of search result entries
217
- cursors: {
218
- top: "...", // cursor for newer results
219
- bottom: "..." // cursor for older results
220
- }
221
- }
222
-
223
- // paginate through search results
224
- const nextResults = await client.search("bun javascript", {
225
- resultsCount: 20,
226
- cursor: searchResults.cursors.bottom
227
- });
228
- ```
229
-
230
- ## requirements
231
-
232
- - twitter account auth cookie (not required for some endpoints)
233
-
234
- ## installation
235
-
236
- - **bun:** `bun add emusks`
237
- - **yarn:** `yarn add emusks`
238
- - **pnpm:** `pnpm add emusks`
239
- - **npm:** `npm install emusks`
240
-
241
- ### getting the auth cookie
242
-
243
- 1. log in to https://x.com
244
- 2. open devtools (F12)
245
- 3. go to the "Application" tab
246
- 4. find the "Cookies" section in the left sidebar
247
- 5. find the "x.com" domain
248
- 6. find the "auth_token" cookie
249
- 7. copy the value of the "auth_token" cookie
250
-
251
- ## limitations
252
-
253
- - the twitter API might change at any time, but this is unlikely to happen
254
-
255
- - emusks can **not** be used to login without an auth token. twitter has spent a lot of time and effort making it so that this is extremely hard to due to CAPTCHAs, complex flows, etc.
256
-
257
- - emusks does **not** support all possible endpoints.
258
-
259
- ## faq
260
-
261
- ### why?
262
-
263
- current ones are outdated, are hard to use, badly documented and such overall.
264
-
265
- ### no, like why did you name it emusks?
266
-
267
- because elon is responsable for the awful state of twitter's horrible api and twitter overall.
268
-
269
- ### how?
270
-
271
- endpoints are reverse-engineered from Tweetdeck Web (also known as X Pro), which uses separate endpoints that are less protected than the normal endpoints.
272
-
273
- unlike the actual X Pro, elon forgot to restrict these endpoints to premium users, so they can be used by anyone.
274
-
275
- other endpoints (mostly read ones) are reverse-engineered from other parts of twitter, such as embeds.
276
-
277
- ### why not the official API?
278
-
279
- it's heavily ratelimited, hard to use, and overall very badly documented.
6
+ [Learn more →](https://emusks.js.org)
@@ -0,0 +1,24 @@
1
+ const URL = `https://raw.githubusercontent.com/fa0311/TwitterInternalAPIDocument/refs/heads/develop/docs/json/API.json`;
2
+
3
+ const json = await fetch(URL).then((r) => r.json());
4
+ const graphql = json.graphql;
5
+
6
+ const buildUrl = (base, features) => {
7
+ const params = new URLSearchParams();
8
+
9
+ if (features && Object.keys(features).length) {
10
+ params.set(`features`, JSON.stringify(features));
11
+ }
12
+
13
+ const query = params.toString();
14
+ return query ? `${base}?${query}` : base;
15
+ };
16
+
17
+ const transformed = Object.fromEntries(
18
+ Object.entries(graphql).map(([name, data]) => {
19
+ const constructedUrl = buildUrl(data.url, data.features);
20
+ return [name, [data.method, constructedUrl]];
21
+ }),
22
+ );
23
+
24
+ await Bun.write(`./src/static/graphql.js`, `export default ${JSON.stringify(transformed)};`);
package/build/v1.1.js ADDED
@@ -0,0 +1,28 @@
1
+ const URL = `https://raw.githubusercontent.com/fa0311/TwitterInternalAPIDocument/refs/heads/develop/docs/json/v1.1.json`;
2
+
3
+ const json = await fetch(URL).then((r) => r.json());
4
+
5
+ const seen = new Set();
6
+ const duplicates = new Set();
7
+
8
+ for (const entry of json) {
9
+ const queryId = entry.queryId.replace(/^\//, "");
10
+ if (seen.has(queryId)) {
11
+ duplicates.add(queryId);
12
+ }
13
+ seen.add(queryId);
14
+ }
15
+
16
+ const transformed = {};
17
+
18
+ for (const entry of json) {
19
+ const queryId = entry.queryId.replace(/^\//, "");
20
+ const method = entry.dispatch[0];
21
+ const urlTemplate = entry.dispatch[2];
22
+ const url = urlTemplate.replace("{queryId}", queryId);
23
+
24
+ const key = duplicates.has(queryId) ? `${method}:${queryId}` : queryId;
25
+ transformed[key] = [method, url];
26
+ }
27
+
28
+ await Bun.write(`./src/static/v1.1.js`, `export default ${JSON.stringify(transformed)};`);
package/build/v2.js ADDED
@@ -0,0 +1,28 @@
1
+ const URL = `https://raw.githubusercontent.com/fa0311/TwitterInternalAPIDocument/refs/heads/develop/docs/json/v2.json`;
2
+
3
+ const json = await fetch(URL).then((r) => r.json());
4
+
5
+ const seen = new Set();
6
+ const duplicates = new Set();
7
+
8
+ for (const entry of json) {
9
+ const queryId = entry.queryId.replace(/^\//, "");
10
+ if (seen.has(queryId)) {
11
+ duplicates.add(queryId);
12
+ }
13
+ seen.add(queryId);
14
+ }
15
+
16
+ const transformed = {};
17
+
18
+ for (const entry of json) {
19
+ const queryId = entry.queryId.replace(/^\//, "");
20
+ const method = entry.dispatch[0];
21
+ const urlTemplate = entry.dispatch[2];
22
+ const url = urlTemplate.replace("{queryId}", queryId);
23
+
24
+ const key = duplicates.has(queryId) ? `${method}:${queryId}` : queryId;
25
+ transformed[key] = [method, url];
26
+ }
27
+
28
+ await Bun.write(`./src/static/v2.js`, `export default ${JSON.stringify(transformed)};`);
package/package.json CHANGED
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "emusks",
3
- "version": "0.0.2",
4
- "description": "Reverse-engineered X Pro (Tweetdeck) API",
5
- "main": "src/index.js",
6
- "keywords": [
7
- "twitter"
8
- ],
9
- "author": "Tiago",
10
- "license": "Apache-2.0"
3
+ "version": "2.0.0",
4
+ "description": "Reverse-engineered Twitter API client. Log in and interact with the unofficial X API using any client identity — web, Android, iOS, or TweetDeck",
5
+ "keywords": ["twitter", "twitter-api", "x", "x-api", "reverse-engineering", "client"],
6
+ "homepage": "https://github.com/tiagozip/cap",
7
+ "bugs": {
8
+ "url": "https://github.com/tiagozip/cap/issues"
9
+ },
10
+ "license": "AGPL-3.0-only",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/tiagozip/cap.git"
14
+ },
15
+ "dependencies": {
16
+ "cycletls": "^2.0.5",
17
+ "x-client-transaction-id": "^0.1.9"
18
+ }
11
19
  }
package/src/clients.js ADDED
@@ -0,0 +1,62 @@
1
+ const chrome_fingerprint = {
2
+ "user-agent":
3
+ "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",
4
+ ja3: "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,35-5-27-16-0-10-13-23-45-65037-17613-18-65281-51-43-11,4588-29-23-24,0",
5
+ ja4r: "t13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0005,000a,000b,000d,0012,0017,001b,0023,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601",
6
+ };
7
+
8
+ const android_fingerprint = {
9
+ ja3: "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,23-0-16-11-17613-27-43-35-13-65037-51-18-65281-45-5-10-41,4588-29-23-24,0",
10
+ ja4r: "t13d1517h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0005,000a,000b,000d,0012,0017,001b,0023,0029,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601",
11
+
12
+ userAgent: "TwitterAndroid/10.93.0-release.00 (Android 14; Google Pixel 7 Pro)",
13
+ };
14
+
15
+ const iphone_fingerprint = {
16
+ ja3: "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,23-0-16-11-17613-27-43-35-13-65037-51-18-65281-45-5-10-41,4588-29-23-24,0",
17
+ ja4r: "t13d1517h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0005,000a,000b,000d,0012,0017,001b,0023,0029,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601",
18
+
19
+ userAgent: "Twitter-iPhone/10.93.0-release.00 (iPhone; iOS 16.5.1; Scale/3.00)",
20
+ };
21
+
22
+ export default {
23
+ android: {
24
+ // beware! this might get your account locked when tweeting
25
+ bearer:
26
+ "AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F",
27
+ fingerprints: android_fingerprint,
28
+ },
29
+ iphone: {
30
+ bearer:
31
+ "AAAAAAAAAAAAAAAAAAAAAAj4AQAAAAAAPraK64zCZ9CSzdLesbE7LB%2Bw4uE%3DVJQREvQNCZJNiz3rHO7lOXlkVOQkzzdsgu6wWgcazdMUaGoUGm",
32
+ fingerprints: iphone_fingerprint,
33
+ },
34
+ ipad: {
35
+ bearer:
36
+ "AAAAAAAAAAAAAAAAAAAAAGHtAgAAAAAA%2Bx7ILXNILCqkSGIzy6faIHZ9s3Q%3DQy97w6SIrzE7lQwPJEYQBsArEE2fC25caFwRBvAGi456G09vGR",
37
+ fingerprints: iphone_fingerprint,
38
+ },
39
+ mac: {
40
+ bearer:
41
+ "AAAAAAAAAAAAAAAAAAAAAIWCCAAAAAAA2C25AxqI%2BYCS7pdfJKRH8Xh19zA%3D8vpDZzPHaEJhd20MKVWp3UR38YoPpuTX7UD2cVYo3YNikubuxd",
42
+ fingerprints: iphone_fingerprint,
43
+ },
44
+
45
+ old: {
46
+ // doesn't seem to work
47
+ bearer:
48
+ "AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw",
49
+ fingerprints: chrome_fingerprint,
50
+ },
51
+ web: {
52
+ bearer:
53
+ "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
54
+ fingerprints: chrome_fingerprint,
55
+ },
56
+ tweetdeck: {
57
+ // requires premium
58
+ bearer:
59
+ "AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF",
60
+ fingerprints: chrome_fingerprint,
61
+ },
62
+ };
@@ -0,0 +1,26 @@
1
+ import initCycleTLS from "cycletls";
2
+
3
+ let cycleTLS;
4
+
5
+ initCycleTLS().then((c) => {
6
+ cycleTLS = c;
7
+ });
8
+
9
+ export default async function getCycleTLS() {
10
+ if (!cycleTLS) {
11
+ await new Promise((r) => {
12
+ const c = setInterval(() => {
13
+ if (cycleTLS) {
14
+ clearInterval(c);
15
+ r();
16
+ }
17
+ }, 10);
18
+ });
19
+ }
20
+
21
+ return cycleTLS;
22
+ }
23
+
24
+ process.on("exit", () => {
25
+ if (cycleTLS) cycleTLS.exit();
26
+ });