emusks 2.0.10 → 2.0.12
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/package.json +1 -1
- package/src/graphql.js +64 -8
- package/src/helpers/media.js +72 -55
- package/src/helpers/tweets.js +42 -35
- package/src/index.js +27 -5
package/package.json
CHANGED
package/src/graphql.js
CHANGED
|
@@ -1,16 +1,60 @@
|
|
|
1
|
+
import { ClientTransaction, handleXMigration } from "x-client-transaction-id";
|
|
1
2
|
import getCycleTLS from "./cycletls.js";
|
|
2
3
|
import graphqlApi from "./static/graphql.js";
|
|
3
4
|
|
|
5
|
+
const GRAPHQL_ENDPOINTS = {
|
|
6
|
+
main: {
|
|
7
|
+
base: "https://api.x.com/graphql",
|
|
8
|
+
referrer: "https://x.com/",
|
|
9
|
+
secFetchSite: "same-site",
|
|
10
|
+
},
|
|
11
|
+
main_twitter: {
|
|
12
|
+
base: "https://api.twitter.com/graphql",
|
|
13
|
+
referrer: "https://twitter.com/",
|
|
14
|
+
secFetchSite: "same-site",
|
|
15
|
+
},
|
|
16
|
+
web: {
|
|
17
|
+
base: "https://x.com/i/api/graphql",
|
|
18
|
+
referrer: "https://x.com/",
|
|
19
|
+
secFetchSite: "same-origin",
|
|
20
|
+
},
|
|
21
|
+
web_twitter: {
|
|
22
|
+
base: "https://twitter.com/i/api/graphql",
|
|
23
|
+
referrer: "https://twitter.com/",
|
|
24
|
+
secFetchSite: "same-origin",
|
|
25
|
+
},
|
|
26
|
+
tweetdeck: {
|
|
27
|
+
base: "https://pro.x.com/i/api/graphql",
|
|
28
|
+
referrer: "https://pro.x.com/",
|
|
29
|
+
secFetchSite: "same-origin",
|
|
30
|
+
},
|
|
31
|
+
tweetdeck_twitter: {
|
|
32
|
+
base: "https://pro.twitter.com/i/api/graphql",
|
|
33
|
+
referrer: "https://pro.twitter.com/",
|
|
34
|
+
secFetchSite: "same-origin",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export { GRAPHQL_ENDPOINTS };
|
|
39
|
+
|
|
4
40
|
export default async function graphql(queryName, { variables, fieldToggles, body, headers } = {}) {
|
|
5
41
|
const entry = graphqlApi[queryName];
|
|
6
42
|
if (!entry) {
|
|
7
43
|
throw new Error(`graphql query ${queryName} not found`);
|
|
8
44
|
}
|
|
9
45
|
|
|
10
|
-
const [method,
|
|
46
|
+
const [method, , features, queryId] = entry;
|
|
11
47
|
const isPost = method.toLowerCase() === "post";
|
|
12
48
|
|
|
13
|
-
|
|
49
|
+
const endpointName = this.graphqlEndpoint || "web";
|
|
50
|
+
const endpoint = GRAPHQL_ENDPOINTS[endpointName];
|
|
51
|
+
if (!endpoint) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`unknown graphql endpoint "${endpointName}", expected: ${Object.keys(GRAPHQL_ENDPOINTS).join(", ")}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let finalUrl = `${endpoint.base}/${queryId}/${queryName}`;
|
|
14
58
|
let requestBody;
|
|
15
59
|
|
|
16
60
|
if (isPost) {
|
|
@@ -41,6 +85,9 @@ export default async function graphql(queryName, { variables, fieldToggles, body
|
|
|
41
85
|
const url = new URL(finalUrl);
|
|
42
86
|
const pathname = url.pathname;
|
|
43
87
|
|
|
88
|
+
const useTransactionIds =
|
|
89
|
+
this.transactionIds !== undefined ? this.transactionIds : endpointName === "web";
|
|
90
|
+
|
|
44
91
|
const requestHeaders = {
|
|
45
92
|
accept: "*/*",
|
|
46
93
|
"accept-language": "en-US,en;q=0.9",
|
|
@@ -50,23 +97,32 @@ export default async function graphql(queryName, { variables, fieldToggles, body
|
|
|
50
97
|
"x-twitter-active-user": "yes",
|
|
51
98
|
"x-twitter-auth-type": "OAuth2Session",
|
|
52
99
|
"x-twitter-client-language": "en",
|
|
53
|
-
"x-client-transaction-id": await this.auth.generateTransactionId(
|
|
54
|
-
method.toUpperCase(),
|
|
55
|
-
pathname,
|
|
56
|
-
),
|
|
57
100
|
priority: "u=1, i",
|
|
58
101
|
"sec-ch-ua": '"Not(A:Brand";v="8", "Chromium";v="144"',
|
|
59
102
|
"sec-ch-ua-mobile": "?0",
|
|
60
103
|
"sec-ch-ua-platform": '"macOS"',
|
|
61
104
|
"sec-fetch-dest": "empty",
|
|
62
105
|
"sec-fetch-mode": "cors",
|
|
63
|
-
"sec-fetch-site":
|
|
106
|
+
"sec-fetch-site": endpoint.secFetchSite,
|
|
64
107
|
"sec-gpc": "1",
|
|
65
108
|
cookie:
|
|
66
109
|
this.auth.client.headers.cookie + (this.elevatedCookies ? `; ${this.elevatedCookies}` : ""),
|
|
67
110
|
...headers,
|
|
68
111
|
};
|
|
69
112
|
|
|
113
|
+
if (useTransactionIds) {
|
|
114
|
+
if (!this.auth.generateTransactionId) {
|
|
115
|
+
const document = await handleXMigration();
|
|
116
|
+
const transaction = new ClientTransaction(document);
|
|
117
|
+
await transaction.initialize();
|
|
118
|
+
this.auth.generateTransactionId = transaction.generateTransactionId.bind(transaction);
|
|
119
|
+
}
|
|
120
|
+
requestHeaders["x-client-transaction-id"] = await this.auth.generateTransactionId(
|
|
121
|
+
method.toUpperCase(),
|
|
122
|
+
pathname,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
70
126
|
const cycleTLS = await getCycleTLS();
|
|
71
127
|
const res = await (
|
|
72
128
|
await cycleTLS(
|
|
@@ -78,7 +134,7 @@ export default async function graphql(queryName, { variables, fieldToggles, body
|
|
|
78
134
|
ja4r: this.auth.client.fingerprints.ja4r,
|
|
79
135
|
body: isPost ? JSON.stringify(requestBody) : undefined,
|
|
80
136
|
proxy: this.proxy || undefined,
|
|
81
|
-
referrer:
|
|
137
|
+
referrer: endpoint.referrer,
|
|
82
138
|
},
|
|
83
139
|
method,
|
|
84
140
|
)
|
package/src/helpers/media.js
CHANGED
|
@@ -23,8 +23,7 @@ const MIME_BY_EXT = {
|
|
|
23
23
|
|
|
24
24
|
function detectMediaType(buf) {
|
|
25
25
|
if (buf[0] === 0xff && buf[1] === 0xd8) return "image/jpeg";
|
|
26
|
-
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47)
|
|
27
|
-
return "image/png";
|
|
26
|
+
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return "image/png";
|
|
28
27
|
if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) return "image/gif";
|
|
29
28
|
if (
|
|
30
29
|
buf[0] === 0x52 &&
|
|
@@ -37,16 +36,9 @@ function detectMediaType(buf) {
|
|
|
37
36
|
buf[11] === 0x50
|
|
38
37
|
)
|
|
39
38
|
return "image/webp";
|
|
40
|
-
if (
|
|
41
|
-
buf.length > 7 &&
|
|
42
|
-
buf[4] === 0x66 &&
|
|
43
|
-
buf[5] === 0x74 &&
|
|
44
|
-
buf[6] === 0x79 &&
|
|
45
|
-
buf[7] === 0x70
|
|
46
|
-
)
|
|
39
|
+
if (buf.length > 7 && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70)
|
|
47
40
|
return "video/mp4";
|
|
48
|
-
if (buf[0] === 0x1a && buf[1] === 0x45 && buf[2] === 0xdf && buf[3] === 0xa3)
|
|
49
|
-
return "video/webm";
|
|
41
|
+
if (buf[0] === 0x1a && buf[1] === 0x45 && buf[2] === 0xdf && buf[3] === 0xa3) return "video/webm";
|
|
50
42
|
return null;
|
|
51
43
|
}
|
|
52
44
|
|
|
@@ -59,31 +51,15 @@ function getMediaCategory(mediaType, uploadType) {
|
|
|
59
51
|
|
|
60
52
|
function checkMediaSize(category, size) {
|
|
61
53
|
const fmt = (x) => `${(x / 1e6).toFixed(2)} MB`;
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
!category.includes("gif") &&
|
|
65
|
-
size > MAX_IMAGE_SIZE
|
|
66
|
-
)
|
|
67
|
-
throw new Error(
|
|
68
|
-
`cannot upload ${fmt(size)} image \u2014 max is ${fmt(MAX_IMAGE_SIZE)}`,
|
|
69
|
-
);
|
|
54
|
+
if (category.includes("image") && !category.includes("gif") && size > MAX_IMAGE_SIZE)
|
|
55
|
+
throw new Error(`cannot upload ${fmt(size)} image \u2014 max is ${fmt(MAX_IMAGE_SIZE)}`);
|
|
70
56
|
if (category.includes("gif") && size > MAX_GIF_SIZE)
|
|
71
|
-
throw new Error(
|
|
72
|
-
`cannot upload ${fmt(size)} gif \u2014 max is ${fmt(MAX_GIF_SIZE)}`,
|
|
73
|
-
);
|
|
57
|
+
throw new Error(`cannot upload ${fmt(size)} gif \u2014 max is ${fmt(MAX_GIF_SIZE)}`);
|
|
74
58
|
if (category.includes("video") && size > MAX_VIDEO_SIZE)
|
|
75
|
-
throw new Error(
|
|
76
|
-
`cannot upload ${fmt(size)} video \u2014 max is ${fmt(MAX_VIDEO_SIZE)}`,
|
|
77
|
-
);
|
|
59
|
+
throw new Error(`cannot upload ${fmt(size)} video \u2014 max is ${fmt(MAX_VIDEO_SIZE)}`);
|
|
78
60
|
}
|
|
79
61
|
|
|
80
|
-
async function makeUploadRequest(
|
|
81
|
-
instance,
|
|
82
|
-
method,
|
|
83
|
-
params,
|
|
84
|
-
body,
|
|
85
|
-
extraHeaders = {},
|
|
86
|
-
) {
|
|
62
|
+
async function makeUploadRequest(instance, method, params, body, extraHeaders = {}) {
|
|
87
63
|
const cycleTLS = await getCycleTLS();
|
|
88
64
|
const url = `${UPLOAD_URL}?${new URLSearchParams(params).toString()}`;
|
|
89
65
|
|
|
@@ -96,9 +72,9 @@ async function makeUploadRequest(
|
|
|
96
72
|
"x-twitter-auth-type": "OAuth2Session",
|
|
97
73
|
"x-twitter-client-language": "en",
|
|
98
74
|
priority: "u=1, i",
|
|
99
|
-
"sec-ch-ua":
|
|
75
|
+
"sec-ch-ua": "Not(A:Brand;v=8, Chromium;v=144",
|
|
100
76
|
"sec-ch-ua-mobile": "?0",
|
|
101
|
-
"sec-ch-ua-platform":
|
|
77
|
+
"sec-ch-ua-platform": "macOS",
|
|
102
78
|
"sec-fetch-dest": "empty",
|
|
103
79
|
"sec-fetch-mode": "cors",
|
|
104
80
|
"sec-fetch-site": "same-site",
|
|
@@ -138,10 +114,7 @@ export async function create(source, opts = {}) {
|
|
|
138
114
|
} else if (typeof Blob !== "undefined" && source instanceof Blob) {
|
|
139
115
|
buf = Buffer.from(await source.arrayBuffer());
|
|
140
116
|
if (!mediaType) mediaType = source.type || undefined;
|
|
141
|
-
} else if (
|
|
142
|
-
source instanceof ArrayBuffer ||
|
|
143
|
-
source instanceof SharedArrayBuffer
|
|
144
|
-
) {
|
|
117
|
+
} else if (source instanceof ArrayBuffer || source instanceof SharedArrayBuffer) {
|
|
145
118
|
buf = Buffer.from(source);
|
|
146
119
|
} else if (Buffer.isBuffer(source) || source instanceof Uint8Array) {
|
|
147
120
|
buf = Buffer.from(source);
|
|
@@ -153,9 +126,7 @@ export async function create(source, opts = {}) {
|
|
|
153
126
|
|
|
154
127
|
if (!mediaType) mediaType = detectMediaType(buf);
|
|
155
128
|
if (!mediaType)
|
|
156
|
-
throw new Error(
|
|
157
|
-
"could not detect media type \u2014 pass opts.mediaType (e.g. 'image/png')",
|
|
158
|
-
);
|
|
129
|
+
throw new Error("could not detect media type \u2014 pass opts.mediaType (e.g. 'image/png')");
|
|
159
130
|
|
|
160
131
|
const totalBytes = buf.length;
|
|
161
132
|
const uploadType = opts.type === "dm" ? "dm" : "tweet";
|
|
@@ -177,10 +148,7 @@ export async function create(source, opts = {}) {
|
|
|
177
148
|
|
|
178
149
|
let segmentIndex = 0;
|
|
179
150
|
for (let offset = 0; offset < totalBytes; offset += CHUNK_SIZE) {
|
|
180
|
-
const chunk = buf.slice(
|
|
181
|
-
offset,
|
|
182
|
-
Math.min(offset + CHUNK_SIZE, totalBytes),
|
|
183
|
-
);
|
|
151
|
+
const chunk = buf.slice(offset, Math.min(offset + CHUNK_SIZE, totalBytes));
|
|
184
152
|
const base64 = chunk.toString("base64");
|
|
185
153
|
|
|
186
154
|
await makeUploadRequest(
|
|
@@ -205,23 +173,17 @@ export async function create(source, opts = {}) {
|
|
|
205
173
|
|
|
206
174
|
const finalizeData = await finalizeRes.json();
|
|
207
175
|
if (finalizeData?.error) {
|
|
208
|
-
throw new Error(
|
|
209
|
-
`upload FINALIZE failed: ${JSON.stringify(finalizeData)}`,
|
|
210
|
-
);
|
|
176
|
+
throw new Error(`upload FINALIZE failed: ${JSON.stringify(finalizeData)}`);
|
|
211
177
|
}
|
|
212
178
|
|
|
213
179
|
let processingInfo = finalizeData?.processing_info;
|
|
214
180
|
while (processingInfo) {
|
|
215
181
|
if (processingInfo.error) {
|
|
216
|
-
throw new Error(
|
|
217
|
-
`media processing error: ${JSON.stringify(processingInfo.error)}`,
|
|
218
|
-
);
|
|
182
|
+
throw new Error(`media processing error: ${JSON.stringify(processingInfo.error)}`);
|
|
219
183
|
}
|
|
220
184
|
if (processingInfo.state === "succeeded") break;
|
|
221
185
|
if (processingInfo.state === "failed") {
|
|
222
|
-
throw new Error(
|
|
223
|
-
`media processing failed: ${JSON.stringify(processingInfo)}`,
|
|
224
|
-
);
|
|
186
|
+
throw new Error(`media processing failed: ${JSON.stringify(processingInfo)}`);
|
|
225
187
|
}
|
|
226
188
|
|
|
227
189
|
const wait = (processingInfo.check_after_secs || 2) * 1000;
|
|
@@ -247,6 +209,61 @@ export async function create(source, opts = {}) {
|
|
|
247
209
|
return { media_id: mediaId, ...finalizeData };
|
|
248
210
|
}
|
|
249
211
|
|
|
212
|
+
export async function createFromUrl(url, opts = {}) {
|
|
213
|
+
if (!this.auth) throw new Error("you must be logged in to upload media");
|
|
214
|
+
|
|
215
|
+
const mediaType = opts.mediaType || "image/gif";
|
|
216
|
+
const uploadType = opts.type === "dm" ? "dm" : "tweet";
|
|
217
|
+
const category = opts.mediaCategory || getMediaCategory(mediaType, uploadType);
|
|
218
|
+
|
|
219
|
+
const initRes = await makeUploadRequest(this, "post", {
|
|
220
|
+
command: "INIT",
|
|
221
|
+
source_url: url,
|
|
222
|
+
media_type: mediaType,
|
|
223
|
+
media_category: category,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const initData = await initRes.json();
|
|
227
|
+
if (!initData?.media_id_string) {
|
|
228
|
+
throw new Error(`upload INIT failed: ${JSON.stringify(initData)}`);
|
|
229
|
+
}
|
|
230
|
+
const mediaId = initData.media_id_string;
|
|
231
|
+
|
|
232
|
+
let processingInfo = initData?.processing_info;
|
|
233
|
+
while (processingInfo) {
|
|
234
|
+
if (processingInfo.error) {
|
|
235
|
+
throw new Error(`media processing error: ${JSON.stringify(processingInfo.error)}`);
|
|
236
|
+
}
|
|
237
|
+
if (processingInfo.state === "succeeded") break;
|
|
238
|
+
if (processingInfo.state === "failed") {
|
|
239
|
+
throw new Error(`media processing failed: ${JSON.stringify(processingInfo)}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const wait = (processingInfo.check_after_secs || 2) * 1000;
|
|
243
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
244
|
+
|
|
245
|
+
const statusRes = await makeUploadRequest(this, "get", {
|
|
246
|
+
command: "STATUS",
|
|
247
|
+
media_id: mediaId,
|
|
248
|
+
});
|
|
249
|
+
const statusData = await statusRes.json();
|
|
250
|
+
processingInfo = statusData?.processing_info;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const metadataBody = { media_id: mediaId };
|
|
254
|
+
const altText = opts.altText || opts.alt_text;
|
|
255
|
+
if (altText) metadataBody.alt_text = { text: altText };
|
|
256
|
+
if (opts.origin) metadataBody.found_media_origin = opts.origin;
|
|
257
|
+
|
|
258
|
+
if (altText || opts.origin) {
|
|
259
|
+
await this.v1_1("media/metadata/create", {
|
|
260
|
+
body: JSON.stringify(metadataBody),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { media_id: mediaId, ...initData };
|
|
265
|
+
}
|
|
266
|
+
|
|
250
267
|
export async function createMetadata(mediaId, altText, opts = {}) {
|
|
251
268
|
const res = await this.v1_1("media/metadata/create", {
|
|
252
269
|
body: JSON.stringify({
|
|
@@ -269,4 +286,4 @@ export async function createSubtitles(mediaId, subtitles) {
|
|
|
269
286
|
}),
|
|
270
287
|
});
|
|
271
288
|
return await res.json();
|
|
272
|
-
}
|
|
289
|
+
}
|
package/src/helpers/tweets.js
CHANGED
|
@@ -4,30 +4,24 @@ import parseTweet from "../parsers/tweet.js";
|
|
|
4
4
|
async function createPollCard(instance, poll) {
|
|
5
5
|
if (!poll.choices || poll.choices.length < 2)
|
|
6
6
|
throw new Error("a poll must have at least 2 choices");
|
|
7
|
-
if (poll.choices.length > 4)
|
|
8
|
-
throw new Error("a poll must not have more than 4 choices");
|
|
7
|
+
if (poll.choices.length > 4) throw new Error("a poll must not have more than 4 choices");
|
|
9
8
|
|
|
10
9
|
const hasImages = poll.choices.some((c) => typeof c === "object" && c.image);
|
|
11
10
|
const allImages = poll.choices.every((c) => typeof c === "object" && c.image);
|
|
12
11
|
if (hasImages && !allImages)
|
|
13
|
-
throw new Error(
|
|
14
|
-
"either all poll choices must have images, or none of them",
|
|
15
|
-
);
|
|
12
|
+
throw new Error("either all poll choices must have images, or none of them");
|
|
16
13
|
|
|
17
14
|
const labels = poll.choices.map((c) => (typeof c === "object" ? c.label : c));
|
|
18
15
|
|
|
19
16
|
for (const label of labels) {
|
|
20
17
|
if (typeof label !== "string" || label.length === 0)
|
|
21
18
|
throw new Error("each poll choice must have a non-empty label");
|
|
22
|
-
if (label.length > 25)
|
|
23
|
-
throw new Error(`poll choice "${label}" exceeds the 25-character limit`);
|
|
19
|
+
if (label.length > 25) throw new Error(`poll choice "${label}" exceeds the 25-character limit`);
|
|
24
20
|
}
|
|
25
21
|
|
|
26
22
|
const duration = poll.duration_minutes ?? 1440;
|
|
27
23
|
if (duration < 5 || duration > 10080)
|
|
28
|
-
throw new Error(
|
|
29
|
-
"poll duration must be between 5 and 10 080 minutes (7 days)",
|
|
30
|
-
);
|
|
24
|
+
throw new Error("poll duration must be between 5 and 10 080 minutes (7 days)");
|
|
31
25
|
|
|
32
26
|
const cycleTLS = await getCycleTLS();
|
|
33
27
|
|
|
@@ -40,8 +34,7 @@ async function createPollCard(instance, poll) {
|
|
|
40
34
|
cardObj["twitter:card"] = "poll_choice_images";
|
|
41
35
|
for (let i = 0; i < poll.choices.length; i++) {
|
|
42
36
|
cardObj[`twitter:string:choice${i + 1}_label`] = labels[i];
|
|
43
|
-
cardObj[`twitter:image:choice${i + 1}_image:src:id`] =
|
|
44
|
-
`mis://${poll.choices[i].image}`;
|
|
37
|
+
cardObj[`twitter:image:choice${i + 1}_image:src:id`] = `mis://${poll.choices[i].image}`;
|
|
45
38
|
}
|
|
46
39
|
} else {
|
|
47
40
|
cardObj["twitter:card"] = `poll${poll.choices.length}choice_text_only`;
|
|
@@ -65,9 +58,9 @@ async function createPollCard(instance, poll) {
|
|
|
65
58
|
"x-twitter-auth-type": "OAuth2Session",
|
|
66
59
|
"x-twitter-client-language": "en",
|
|
67
60
|
priority: "u=1, i",
|
|
68
|
-
"sec-ch-ua":
|
|
61
|
+
"sec-ch-ua": "Not(A:Brand;v=8, Chromium;v=144",
|
|
69
62
|
"sec-ch-ua-mobile": "?0",
|
|
70
|
-
"sec-ch-ua-platform":
|
|
63
|
+
"sec-ch-ua-platform": "macOS",
|
|
71
64
|
"sec-fetch-dest": "empty",
|
|
72
65
|
"sec-fetch-mode": "cors",
|
|
73
66
|
"sec-fetch-site": "same-site",
|
|
@@ -99,11 +92,28 @@ export async function create(text, opts = {}) {
|
|
|
99
92
|
let cardUri = opts.cardUri;
|
|
100
93
|
|
|
101
94
|
if (opts.poll) {
|
|
102
|
-
if (cardUri)
|
|
103
|
-
throw new Error("a tweet can\u0027t have both a poll and a cardUri");
|
|
95
|
+
if (cardUri) throw new Error("a tweet can\u0027t have both a poll and a cardUri");
|
|
104
96
|
cardUri = await createPollCard(this, opts.poll);
|
|
105
97
|
}
|
|
106
98
|
|
|
99
|
+
const mediaIds = opts.mediaIds ? [...opts.mediaIds] : [];
|
|
100
|
+
|
|
101
|
+
if (opts.gif) {
|
|
102
|
+
const gif = opts.gif;
|
|
103
|
+
const gifUrl = gif.original_image?.url || gif.url;
|
|
104
|
+
if (!gifUrl) throw new Error("gif must have a url or original_image.url");
|
|
105
|
+
|
|
106
|
+
const provider = gif.found_media_origin?.provider || gif.provider;
|
|
107
|
+
const gifId = gif.found_media_origin?.id || gif.id;
|
|
108
|
+
const altText = gif.alt_text || gif.altText;
|
|
109
|
+
|
|
110
|
+
const uploaded = await this.media.createFromUrl(gifUrl, {
|
|
111
|
+
origin: provider && gifId ? { provider, id: gifId } : undefined,
|
|
112
|
+
altText,
|
|
113
|
+
});
|
|
114
|
+
mediaIds.push(uploaded.media_id);
|
|
115
|
+
}
|
|
116
|
+
|
|
107
117
|
const res = await this.graphql("CreateTweet", {
|
|
108
118
|
body: {
|
|
109
119
|
variables: {
|
|
@@ -111,21 +121,20 @@ export async function create(text, opts = {}) {
|
|
|
111
121
|
dark_request: false,
|
|
112
122
|
card_uri: cardUri || undefined,
|
|
113
123
|
media: {
|
|
114
|
-
media_entities:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
})) || [],
|
|
124
|
+
media_entities: mediaIds.map((id) => ({
|
|
125
|
+
media_id: id,
|
|
126
|
+
tagged_users: [],
|
|
127
|
+
})),
|
|
119
128
|
possibly_sensitive: opts.sensitive || false,
|
|
120
129
|
},
|
|
121
130
|
semantic_annotation_ids: [],
|
|
122
131
|
...(opts.replyTo
|
|
123
132
|
? {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
reply: {
|
|
134
|
+
in_reply_to_tweet_id: opts.replyTo,
|
|
135
|
+
exclude_reply_user_ids: [],
|
|
136
|
+
},
|
|
137
|
+
}
|
|
129
138
|
: {}),
|
|
130
139
|
...(opts.quoteTweetId
|
|
131
140
|
? { attachment_url: `https://x.com/i/status/${opts.quoteTweetId}` }
|
|
@@ -159,11 +168,11 @@ export async function createNote(text, opts = {}) {
|
|
|
159
168
|
richtext_options: { richtext_tags: opts.richtext_tags || [] },
|
|
160
169
|
...(opts.replyTo
|
|
161
170
|
? {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
171
|
+
reply: {
|
|
172
|
+
in_reply_to_tweet_id: opts.replyTo,
|
|
173
|
+
exclude_reply_user_ids: [],
|
|
174
|
+
},
|
|
175
|
+
}
|
|
167
176
|
: {}),
|
|
168
177
|
...opts.variables,
|
|
169
178
|
},
|
|
@@ -248,9 +257,7 @@ export async function getMany(tweetIds) {
|
|
|
248
257
|
},
|
|
249
258
|
});
|
|
250
259
|
const results = res?.data?.tweetResult || [];
|
|
251
|
-
return Array.isArray(results)
|
|
252
|
-
? results.map((r) => (r?.result ? parseTweet(r.result) : r))
|
|
253
|
-
: res;
|
|
260
|
+
return Array.isArray(results) ? results.map((r) => (r?.result ? parseTweet(r.result) : r)) : res;
|
|
254
261
|
}
|
|
255
262
|
|
|
256
263
|
export async function detail(tweetId, opts = {}) {
|
|
@@ -398,4 +405,4 @@ export async function similar(tweetId) {
|
|
|
398
405
|
return await this.graphql("SimilarPosts", {
|
|
399
406
|
variables: { tweet_id: tweetId },
|
|
400
407
|
});
|
|
401
|
-
}
|
|
408
|
+
}
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { ClientTransaction, handleXMigration } from "x-client-transaction-id";
|
|
|
2
2
|
import clients from "./clients.js";
|
|
3
3
|
import getCycleTLS from "./cycletls.js";
|
|
4
4
|
import flowLogin from "./flow.js";
|
|
5
|
-
import graphql from "./graphql.js";
|
|
5
|
+
import graphql, { GRAPHQL_ENDPOINTS } from "./graphql.js";
|
|
6
6
|
import initHelpers from "./helpers/index.js";
|
|
7
7
|
import parseUser from "./parsers/user.js";
|
|
8
8
|
import v1_1 from "./v1.1.js";
|
|
@@ -11,6 +11,8 @@ import v2 from "./v2.js";
|
|
|
11
11
|
export default class Emusks {
|
|
12
12
|
auth = null;
|
|
13
13
|
elevatedCookies = null;
|
|
14
|
+
graphqlEndpoint = "web";
|
|
15
|
+
transactionIds = undefined;
|
|
14
16
|
|
|
15
17
|
async elevate(password) {
|
|
16
18
|
if (!this.auth) throw new Error("must be logged in before calling elevate");
|
|
@@ -76,6 +78,19 @@ export default class Emusks {
|
|
|
76
78
|
if (!p.client) throw new Error("invalid client!");
|
|
77
79
|
if (p.proxy) this.proxy = p.proxy;
|
|
78
80
|
|
|
81
|
+
if (p.endpoint) {
|
|
82
|
+
if (!GRAPHQL_ENDPOINTS[p.endpoint]) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`unknown graphql endpoint "${p.endpoint}", expected: ${Object.keys(GRAPHQL_ENDPOINTS).join(", ")}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
this.graphqlEndpoint = p.endpoint;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (p.transactionIds !== undefined) {
|
|
91
|
+
this.transactionIds = p.transactionIds;
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
if (!p.client.bearer) {
|
|
80
95
|
throw new Error("client is missing bearer token!");
|
|
81
96
|
}
|
|
@@ -139,10 +154,17 @@ export default class Emusks {
|
|
|
139
154
|
}
|
|
140
155
|
this.auth.client.headers.cookie = cookieParts.join("; ");
|
|
141
156
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
157
|
+
const needsTransactionIds =
|
|
158
|
+
this.transactionIds !== undefined
|
|
159
|
+
? this.transactionIds
|
|
160
|
+
: this.graphqlEndpoint === "web" || this.graphqlEndpoint === "web_twitter";
|
|
161
|
+
|
|
162
|
+
if (needsTransactionIds) {
|
|
163
|
+
const document = await handleXMigration();
|
|
164
|
+
const transaction = new ClientTransaction(document);
|
|
165
|
+
await transaction.initialize();
|
|
166
|
+
this.auth.generateTransactionId = transaction.generateTransactionId.bind(transaction);
|
|
167
|
+
}
|
|
146
168
|
|
|
147
169
|
const responseText = await res.text();
|
|
148
170
|
const initialStateMatch = responseText.match(/window\.__INITIAL_STATE__\s*=\s*({.*?});/s);
|