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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emusks",
3
- "version": "2.0.10",
3
+ "version": "2.0.12",
4
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
5
  "keywords": [
6
6
  "client",
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, baseUrl, features, queryId] = entry;
46
+ const [method, , features, queryId] = entry;
11
47
  const isPost = method.toLowerCase() === "post";
12
48
 
13
- let finalUrl = baseUrl;
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": "same-origin",
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: "https://x.com/",
137
+ referrer: endpoint.referrer,
82
138
  },
83
139
  method,
84
140
  )
@@ -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
- category.includes("image") &&
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": 'Not(A:Brand;v=8, Chromium;v=144',
75
+ "sec-ch-ua": "Not(A:Brand;v=8, Chromium;v=144",
100
76
  "sec-ch-ua-mobile": "?0",
101
- "sec-ch-ua-platform": 'macOS',
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
+ }
@@ -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": 'Not(A:Brand;v=8, Chromium;v=144',
61
+ "sec-ch-ua": "Not(A:Brand;v=8, Chromium;v=144",
69
62
  "sec-ch-ua-mobile": "?0",
70
- "sec-ch-ua-platform": 'macOS',
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
- opts.mediaIds?.map((id) => ({
116
- media_id: id,
117
- tagged_users: [],
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
- reply: {
125
- in_reply_to_tweet_id: opts.replyTo,
126
- exclude_reply_user_ids: [],
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
- reply: {
163
- in_reply_to_tweet_id: opts.replyTo,
164
- exclude_reply_user_ids: [],
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 document = await handleXMigration();
143
- const transaction = new ClientTransaction(document);
144
- await transaction.initialize();
145
- this.auth.generateTransactionId = transaction.generateTransactionId.bind(transaction);
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);