emusks 2.0.16 → 2.0.18

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/v1.1.js +56 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emusks",
3
- "version": "2.0.16",
3
+ "version": "2.0.18",
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/v1.1.js CHANGED
@@ -20,11 +20,36 @@ export default async function v1_1(queryName, { params, body, headers } = {}) {
20
20
  const url = new URL(finalUrl);
21
21
  const pathname = url.pathname;
22
22
 
23
+ // v1.1 endpoints want form-urlencoded bodies. helpers historically passed
24
+ // `body: JSON.stringify(...)`, which twitter silently rejected — the most
25
+ // recent example was friendships/create returning "Cannot find specified
26
+ // user" because user_id wasn't extractable from a JSON body. if the caller
27
+ // hasn't set its own content-type, transparently convert a JSON-string body
28
+ // into form-urlencoded so existing helpers Just Work.
29
+ const callerSetContentType = headers && Object.keys(headers).some(
30
+ (k) => k.toLowerCase() === "content-type",
31
+ );
32
+ let finalBody = body;
33
+ let coercedContentType = null;
34
+ if (!callerSetContentType && typeof finalBody === "string" && finalBody.startsWith("{")) {
35
+ try {
36
+ const parsed = JSON.parse(finalBody);
37
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
38
+ finalBody = new URLSearchParams(
39
+ Object.fromEntries(
40
+ Object.entries(parsed).map(([k, v]) => [k, v == null ? "" : String(v)]),
41
+ ),
42
+ ).toString();
43
+ coercedContentType = "application/x-www-form-urlencoded";
44
+ }
45
+ } catch {}
46
+ }
47
+
23
48
  const requestHeaders = {
24
49
  accept: "*/*",
25
50
  "accept-language": "en-US,en;q=0.9",
26
51
  authorization: `Bearer ${this.auth.client.bearer}`,
27
- "content-type": "application/json",
52
+ "content-type": coercedContentType ?? "application/json",
28
53
  "x-csrf-token": this.auth.csrfToken,
29
54
  "x-twitter-active-user": "yes",
30
55
  "x-twitter-auth-type": "OAuth2Session",
@@ -48,17 +73,45 @@ export default async function v1_1(queryName, { params, body, headers } = {}) {
48
73
 
49
74
  const cycleTLS = await getCycleTLS();
50
75
 
51
- return await cycleTLS(
76
+ const res = await cycleTLS(
52
77
  finalUrl,
53
78
  {
54
79
  headers: requestHeaders,
55
80
  userAgent: this.auth.client.fingerprints.userAgent,
56
81
  ja3: this.auth.client.fingerprints.ja3,
57
82
  ja4r: this.auth.client.fingerprints.ja4r,
58
- body: body || undefined,
83
+ body: finalBody || undefined,
59
84
  proxy: this.proxy || undefined,
60
85
  referrer: "https://x.com/",
61
86
  },
62
87
  method,
63
88
  );
89
+
90
+ // surface twitter errors instead of returning silently. matches the pattern
91
+ // in graphql.js (it checks res.errors after .json()). without this, callers
92
+ // that just `await res.json()` swallow 4xx error bodies — e.g. profile
93
+ // updates over 160 chars used to no-op silently.
94
+ let bodyText;
95
+ try {
96
+ bodyText = await res.text();
97
+ } catch {
98
+ return res;
99
+ }
100
+ let bodyJson;
101
+ if (bodyText) {
102
+ try { bodyJson = JSON.parse(bodyText); } catch {}
103
+ }
104
+ if (bodyJson?.errors?.length) {
105
+ const messages = bodyJson.errors
106
+ .map((e) => e.message || (e.code != null ? `code ${e.code}` : "unknown"))
107
+ .join("; ");
108
+ throw new Error(`twitter v1.1 ${queryName} ${res.status}: ${messages}`);
109
+ }
110
+ // re-expose the response so existing helpers keep working
111
+ return {
112
+ status: res.status,
113
+ headers: res.headers,
114
+ json: async () => bodyJson,
115
+ text: async () => bodyText,
116
+ };
64
117
  }