emusks 0.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.
package/README.md ADDED
@@ -0,0 +1,279 @@
1
+ # emusks — reverse-engineered Twitter API
2
+
3
+ emusks is a reverse-engineered Twitter API wrapper that **just works**. beta for now.
4
+
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.
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "emusks",
3
+ "version": "0.0.1",
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"
11
+ }
package/src/index.js ADDED
@@ -0,0 +1,89 @@
1
+ import parseUser from "./parsers/user.js";
2
+ import headers from "./utils/headers.js";
3
+
4
+ import * as tweeting from "./methods/tweet.js";
5
+ import * as retweeting from "./methods/retweet.js";
6
+ import * as liking from "./methods/like.js";
7
+ import * as following from "./methods/follow.js";
8
+ import * as bookmarks from "./methods/bookmarks.js";
9
+ import * as notifications from "./methods/notifications.js";
10
+ import * as search from "./methods/search.js";
11
+ import * as users from "./methods/users.js";
12
+ import * as timeline from "./methods/timeline.js";
13
+
14
+ class Client {
15
+ auth = {};
16
+ user = null;
17
+
18
+ constructor({ authToken } = {}) {
19
+ if (authToken) {
20
+ this.auth.token = authToken;
21
+ }
22
+ }
23
+
24
+ async login() {
25
+ if (!this.auth.token) {
26
+ throw new Error("auth token is required to login");
27
+ }
28
+
29
+ // gets the csrf token from headers
30
+ const res = await fetch("https://pro.x.com/", {
31
+ headers: {
32
+ accept:
33
+ "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,
37
+ },
38
+ });
39
+
40
+ const csrfToken = res.headers
41
+ .get("set-cookie")
42
+ .split(", ct0=")[1]
43
+ .split(";")[0];
44
+
45
+ if (!csrfToken) {
46
+ throw new Error("[emusks] failed to log in");
47
+ }
48
+
49
+ this.auth.csrfToken = csrfToken;
50
+ this.auth.cookies = `auth_token=${this.auth.token}; ct0=${csrfToken};`;
51
+
52
+ const initialStateMatch = (await res.text()).match(
53
+ /window\.__INITIAL_STATE__\s*=\s*({.*?});/s
54
+ );
55
+
56
+ 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
+ );
60
+ return;
61
+ }
62
+
63
+ const usersEntities = JSON.parse(initialStateMatch[1])?.entities?.users
64
+ ?.entities;
65
+ const initialStateUser = usersEntities && Object.values(usersEntities)[0];
66
+
67
+ 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
+ );
71
+ return;
72
+ }
73
+
74
+ this.user = parseUser(initialStateUser);
75
+ return this.user;
76
+ }
77
+ }
78
+
79
+ Object.assign(Client.prototype, tweeting);
80
+ Object.assign(Client.prototype, retweeting);
81
+ Object.assign(Client.prototype, liking);
82
+ Object.assign(Client.prototype, following);
83
+ Object.assign(Client.prototype, bookmarks);
84
+ Object.assign(Client.prototype, notifications);
85
+ Object.assign(Client.prototype, search);
86
+ Object.assign(Client.prototype, users);
87
+ Object.assign(Client.prototype, timeline);
88
+
89
+ export default Client;
@@ -0,0 +1,91 @@
1
+ import headers from "../utils/headers.js";
2
+
3
+ export async function bookmark(tweetId) {
4
+ if (!this.auth.cookies) {
5
+ throw new Error("you must be logged in to bookmark a tweet");
6
+ }
7
+
8
+ const out = await (
9
+ await fetch(
10
+ "https://pro.x.com/i/api/graphql/aoDbu3RHznuiSkQ9aNM67Q/CreateBookmark",
11
+ {
12
+ headers: {
13
+ Accept: "*/*",
14
+ "accept-language": "en-US,en;q=0.9",
15
+
16
+ authorization:
17
+ "Bearer AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF",
18
+ "content-type": "application/json",
19
+
20
+ "x-client-transaction-id": Math.random().toString().replace("0.", ""),
21
+ "x-csrf-token": this.auth.csrfToken,
22
+
23
+ "x-twitter-active-user": "yes",
24
+ "x-twitter-auth-type": "OAuth2Session",
25
+ "x-twitter-client-language": "en",
26
+
27
+ cookie: this.auth.cookies,
28
+ ...headers,
29
+ },
30
+ body: JSON.stringify({
31
+ variables: { tweet_id: tweetId },
32
+ queryId: "aoDbu3RHznuiSkQ9aNM67Q",
33
+ }),
34
+ method: "POST",
35
+ }
36
+ )
37
+ ).json();
38
+
39
+ if (!out?.data?.tweet_bookmark_put) {
40
+ throw new Error(
41
+ `failed to bookmark tweet: ${JSON.stringify(out, null, 2)}`
42
+ );
43
+ }
44
+
45
+ return true;
46
+ }
47
+
48
+ export async function unbookmark(tweetId) {
49
+ if (!this.auth.cookies) {
50
+ throw new Error("you must be logged in to unbookmark a tweet");
51
+ }
52
+
53
+ const out = await (
54
+ await fetch(
55
+ "https://pro.x.com/i/api/graphql/Wlmlj2-xzyS1GN3a6cj-mQ/DeleteBookmark",
56
+ {
57
+ headers: {
58
+ Accept: "*/*",
59
+ "accept-language": "en-US,en;q=0.9",
60
+
61
+ authorization:
62
+ "Bearer AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF",
63
+ "content-type": "application/json",
64
+
65
+ "x-client-transaction-id": Math.random().toString().replace("0.", ""),
66
+ "x-csrf-token": this.auth.csrfToken,
67
+
68
+ "x-twitter-active-user": "yes",
69
+ "x-twitter-auth-type": "OAuth2Session",
70
+ "x-twitter-client-language": "en",
71
+
72
+ cookie: this.auth.cookies,
73
+ ...headers,
74
+ },
75
+ body: JSON.stringify({
76
+ variables: { tweet_id: tweetId },
77
+ queryId: "Wlmlj2-xzyS1GN3a6cj-mQ",
78
+ }),
79
+ method: "POST",
80
+ }
81
+ )
82
+ ).json();
83
+
84
+ if (!out?.data?.tweet_bookmark_delete) {
85
+ throw new Error(
86
+ `failed to unbookmark tweet: ${JSON.stringify(out, null, 2)}`
87
+ );
88
+ }
89
+
90
+ return true;
91
+ }
@@ -0,0 +1,88 @@
1
+ import parseUser from "../parsers/user.js";
2
+ import headers from "../utils/headers.js";
3
+
4
+ export async function follow(userId) {
5
+ if (!this.auth.cookies) {
6
+ throw new Error("you must be logged in to follow someone");
7
+ }
8
+
9
+ const out = await (
10
+ await fetch("https://pro.x.com/i/api/1.1/friendships/create.json", {
11
+ headers: {
12
+ Accept: "*/*",
13
+ "accept-language": "en-US,en;q=0.9",
14
+
15
+ authorization:
16
+ "Bearer AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF",
17
+ "content-type": "application/x-www-form-urlencoded",
18
+
19
+ "x-client-transaction-id": Math.random().toString().replace("0.", ""),
20
+ "x-csrf-token": this.auth.csrfToken,
21
+
22
+ "x-twitter-active-user": "yes",
23
+ "x-twitter-auth-type": "OAuth2Session",
24
+ "x-twitter-client-language": "en",
25
+
26
+ cookie: this.auth.cookies,
27
+ ...headers,
28
+ },
29
+
30
+ // i'm too lazy to make form data manually and this works
31
+ // well so i'm just gonna use that
32
+ body: `include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&include_ext_is_blue_verified=1&include_ext_verified_type=1&include_ext_profile_image_shape=1&skip_status=1&user_id=${encodeURIComponent(
33
+ userId
34
+ )}`,
35
+ method: "POST",
36
+ })
37
+ ).json();
38
+
39
+ if (!out?.id) {
40
+ throw new Error(`failed to follow user: ${JSON.stringify(out, null, 2)}`);
41
+ }
42
+
43
+ return {
44
+ user: parseUser(out),
45
+ };
46
+ }
47
+
48
+ export async function unfollow(userId) {
49
+ if (!this.auth.cookies) {
50
+ throw new Error("you must be logged in to follow someone");
51
+ }
52
+
53
+ const out = await (
54
+ await fetch("https://pro.x.com/i/api/1.1/friendships/destroy.json", {
55
+ headers: {
56
+ Accept: "*/*",
57
+ "accept-language": "en-US,en;q=0.9",
58
+
59
+ authorization:
60
+ "Bearer AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF",
61
+ "content-type": "application/x-www-form-urlencoded",
62
+
63
+ "x-client-transaction-id": Math.random().toString().replace("0.", ""),
64
+ "x-csrf-token": this.auth.csrfToken,
65
+
66
+ "x-twitter-active-user": "yes",
67
+ "x-twitter-auth-type": "OAuth2Session",
68
+ "x-twitter-client-language": "en",
69
+
70
+ cookie: this.auth.cookies,
71
+ ...headers,
72
+ },
73
+
74
+ body: `include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&include_ext_is_blue_verified=1&include_ext_verified_type=1&include_ext_profile_image_shape=1&skip_status=1&user_id=${encodeURIComponent(
75
+ userId
76
+ )}`,
77
+ method: "POST",
78
+ })
79
+ ).json();
80
+
81
+ if (!out?.id) {
82
+ throw new Error(`failed to unfollow user: ${JSON.stringify(out, null, 2)}`);
83
+ }
84
+
85
+ return {
86
+ user: parseUser(out),
87
+ };
88
+ }
@@ -0,0 +1,87 @@
1
+ import headers from "../utils/headers.js";
2
+
3
+ export async function like(tweetId) {
4
+ if (!this.auth.cookies) {
5
+ throw new Error("you must be logged in to like a tweet");
6
+ }
7
+
8
+ const out = await (
9
+ await fetch(
10
+ "https://pro.x.com/i/api/graphql/lI07N6Otwv1PhnEgXILM7A/FavoriteTweet",
11
+ {
12
+ headers: {
13
+ Accept: "*/*",
14
+ "accept-language": "en-US,en;q=0.9",
15
+
16
+ authorization:
17
+ "Bearer AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF",
18
+ "content-type": "application/json",
19
+
20
+ "x-client-transaction-id": Math.random().toString().replace("0.", ""),
21
+ "x-csrf-token": this.auth.csrfToken,
22
+
23
+ "x-twitter-active-user": "yes",
24
+ "x-twitter-auth-type": "OAuth2Session",
25
+ "x-twitter-client-language": "en",
26
+
27
+ cookie: this.auth.cookies,
28
+ ...headers,
29
+ },
30
+ body: JSON.stringify({
31
+ variables: { tweet_id: tweetId },
32
+ queryId: "lI07N6Otwv1PhnEgXILM7A",
33
+ }),
34
+ method: "POST",
35
+ }
36
+ )
37
+ ).json();
38
+
39
+ if (!out?.data?.favorite_tweet) {
40
+ throw new Error(`failed to like tweet: ${JSON.stringify(out, null, 2)}`);
41
+ }
42
+
43
+ return true;
44
+ }
45
+
46
+ export async function dislike(tweetId) {
47
+ if (!this.auth.cookies) {
48
+ throw new Error("you must be logged in to dislike a tweet");
49
+ }
50
+
51
+ const out = await (
52
+ await fetch(
53
+ "https://pro.x.com/i/api/graphql/ZYKSe-w7KEslx3JhSIk5LA/UnfavoriteTweet",
54
+ {
55
+ headers: {
56
+ Accept: "*/*",
57
+ "accept-language": "en-US,en;q=0.9",
58
+
59
+ authorization:
60
+ "Bearer AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF",
61
+ "content-type": "application/json",
62
+
63
+ "x-client-transaction-id": Math.random().toString().replace("0.", ""),
64
+ "x-csrf-token": this.auth.csrfToken,
65
+
66
+ "x-twitter-active-user": "yes",
67
+ "x-twitter-auth-type": "OAuth2Session",
68
+ "x-twitter-client-language": "en",
69
+
70
+ cookie: this.auth.cookies,
71
+ ...headers,
72
+ },
73
+ body: JSON.stringify({
74
+ variables: { tweet_id: tweetId },
75
+ queryId: "ZYKSe-w7KEslx3JhSIk5LA",
76
+ }),
77
+ method: "POST",
78
+ }
79
+ )
80
+ ).json();
81
+
82
+ if (!out?.data?.unfavorite_tweet) {
83
+ throw new Error(`failed to dislike tweet: ${JSON.stringify(out, null, 2)}`);
84
+ }
85
+
86
+ return true;
87
+ }