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 +279 -0
- package/package.json +11 -0
- package/src/index.js +89 -0
- package/src/methods/bookmarks.js +91 -0
- package/src/methods/follow.js +88 -0
- package/src/methods/like.js +87 -0
- package/src/methods/notifications.js +121 -0
- package/src/methods/retweet.js +91 -0
- package/src/methods/search.js +124 -0
- package/src/methods/timeline.js +215 -0
- package/src/methods/tweet.js +396 -0
- package/src/methods/users.js +209 -0
- package/src/parsers/tweet.js +75 -0
- package/src/parsers/user.js +142 -0
- package/src/utils/headers.js +23 -0
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
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
|
+
}
|