emusks 2.0.0 → 2.0.2

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 CHANGED
@@ -1,6 +1,6 @@
1
- <img src="docs/src/icon.svg" width="120">
1
+ <img src="https://emusks.tiago.zip/icon.svg" width="120">
2
2
  <h1>emusks: Reverse-engineered Twitter API client</h1>
3
3
 
4
4
  Log in and interact with the unofficial X API using any client identity — web, Android, iOS, or TweetDeck
5
5
 
6
- [Learn more →](https://emusks.js.org)
6
+ [Learn more →](https://emusks.tiago.zip)
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "emusks",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
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": ["twitter", "twitter-api", "x", "x-api", "reverse-engineering", "client"],
6
- "homepage": "https://github.com/tiagozip/cap",
6
+ "homepage": "https://github.com/tiagozip/emusks",
7
7
  "bugs": {
8
- "url": "https://github.com/tiagozip/cap/issues"
8
+ "url": "https://github.com/tiagozip/emusks/issues"
9
9
  },
10
10
  "license": "AGPL-3.0-only",
11
11
  "repository": {
12
12
  "type": "git",
13
- "url": "https://github.com/tiagozip/cap.git"
13
+ "url": "https://github.com/tiagozip/emusks.git"
14
14
  },
15
15
  "dependencies": {
16
16
  "cycletls": "^2.0.5",
@@ -127,20 +127,9 @@ async function makeUploadRequest(
127
127
  }
128
128
 
129
129
  export default (client) => ({
130
- /**
131
- * Upload media (image, GIF, or video) to Twitter.
132
- *
133
- * @param {string|Buffer|Uint8Array|ArrayBuffer|Blob} source - file path, Buffer, Uint8Array, ArrayBuffer, or Blob
134
- * @param {object} [opts]
135
- * @param {string} [opts.alt_text] - alt text for accessibility
136
- * @param {string} [opts.mediaType] - explicit MIME type (e.g. "image/png"); auto-detected when omitted
137
- * @param {"tweet"|"dm"} [opts.type] - upload context; defaults to "tweet"
138
- * @returns {{ media_id: string }}
139
- */
140
130
  async create(source, opts = {}) {
141
131
  if (!client.auth) throw new Error("you must be logged in to upload media");
142
132
 
143
- // ── normalise input to Buffer ──────────────────────────────────────
144
133
  let buf;
145
134
  let mediaType = opts.mediaType;
146
135
 
@@ -174,7 +163,6 @@ export default (client) => ({
174
163
  const category = getMediaCategory(mediaType, uploadType);
175
164
  checkMediaSize(category, totalBytes);
176
165
 
177
- // ── INIT ───────────────────────────────────────────────────────────
178
166
  const initRes = await makeUploadRequest(client, "post", {
179
167
  command: "INIT",
180
168
  media_type: mediaType,
@@ -188,7 +176,6 @@ export default (client) => ({
188
176
  }
189
177
  const mediaId = initData.media_id_string;
190
178
 
191
- // ── APPEND (chunked, base64) ───────────────────────────────────────
192
179
  let segmentIndex = 0;
193
180
  for (let offset = 0; offset < totalBytes; offset += CHUNK_SIZE) {
194
181
  const chunk = buf.slice(
@@ -211,7 +198,6 @@ export default (client) => ({
211
198
  segmentIndex++;
212
199
  }
213
200
 
214
- // ── FINALIZE ───────────────────────────────────────────────────────
215
201
  const finalizeRes = await makeUploadRequest(client, "post", {
216
202
  command: "FINALIZE",
217
203
  media_id: mediaId,
@@ -225,7 +211,6 @@ export default (client) => ({
225
211
  );
226
212
  }
227
213
 
228
- // ── poll for async processing (videos / gifs) ──────────────────────
229
214
  let processingInfo = finalizeData?.processing_info;
230
215
  while (processingInfo) {
231
216
  if (processingInfo.error) {
@@ -251,7 +236,6 @@ export default (client) => ({
251
236
  processingInfo = statusData?.processing_info;
252
237
  }
253
238
 
254
- // ── alt text ───────────────────────────────────────────────────────
255
239
  if (opts.alt_text) {
256
240
  await client.v1_1("media/metadata/create", {
257
241
  body: JSON.stringify({
@@ -37,7 +37,6 @@ async function createPollCard(client, poll) {
37
37
  };
38
38
 
39
39
  if (hasImages) {
40
- // media poll — uses a different card type and includes image media IDs
41
40
  cardObj["twitter:card"] = `poll_choice_images`;
42
41
  for (let i = 0; i < poll.choices.length; i++) {
43
42
  cardObj[`twitter:string:choice${i + 1}_label`] = labels[i];
@@ -45,7 +44,6 @@ async function createPollCard(client, poll) {
45
44
  `mis://${poll.choices[i].image}`;
46
45
  }
47
46
  } else {
48
- // text-only poll
49
47
  cardObj["twitter:card"] = `poll${poll.choices.length}choice_text_only`;
50
48
  for (let i = 0; i < labels.length; i++) {
51
49
  cardObj[`twitter:string:choice${i + 1}_label`] = labels[i];
@@ -1,19 +1,6 @@
1
1
  import parseTweet from "./tweet.js";
2
2
  import parseUser from "./user.js";
3
3
 
4
- /**
5
- * Recursively find the `instructions` array from any Twitter timeline response.
6
- *
7
- * Known response shapes:
8
- * data.home.home_timeline_urt.instructions
9
- * data.user.result.timeline_v2.timeline.instructions
10
- * data.search_by_raw_query.search_timeline.timeline.instructions
11
- * data.bookmark_timeline_v2.timeline.instructions
12
- * data.bookmark_search_timeline.timeline.instructions
13
- * data.list.tweets_timeline.timeline.instructions
14
- * data.timeline_by_id.timeline.instructions
15
- * ... and many more
16
- */
17
4
  function findInstructions(obj) {
18
5
  if (!obj || typeof obj !== "object") return null;
19
6
 
@@ -32,11 +19,7 @@ function findInstructions(obj) {
32
19
  return null;
33
20
  }
34
21
 
35
- /**
36
- * Extract tweet results from a single timeline entry.
37
- */
38
22
  function extractTweetFromEntry(entry) {
39
- // Standard tweet entry
40
23
  const tweetResult =
41
24
  entry?.content?.itemContent?.tweet_results?.result ||
42
25
  entry?.item?.itemContent?.tweet_results?.result;
@@ -46,9 +29,6 @@ function extractTweetFromEntry(entry) {
46
29
  return null;
47
30
  }
48
31
 
49
- /**
50
- * Extract user results from a single timeline entry.
51
- */
52
32
  function extractUserFromEntry(entry) {
53
33
  const userResult =
54
34
  entry?.content?.itemContent?.user_results?.result ||
@@ -59,12 +39,6 @@ function extractUserFromEntry(entry) {
59
39
  return null;
60
40
  }
61
41
 
62
- /**
63
- * Parse a raw Twitter timeline/search/bookmark GraphQL response into
64
- * `{ tweets, users, nextCursor, previousCursor }`.
65
- *
66
- * Works universally across all timeline-shaped endpoints.
67
- */
68
42
  export default function parseTimeline(raw) {
69
43
  const instructions = findInstructions(raw?.data || raw);
70
44
 
@@ -78,17 +52,10 @@ export default function parseTimeline(raw) {
78
52
  let previousCursor = null;
79
53
 
80
54
  for (const instruction of instructions) {
81
- const entries =
82
- instruction.entries || // TimelineAddEntries
83
- instruction.entry // TimelineReplaceEntry (single)
84
- ? [instruction.entry]
85
- : null;
86
-
87
55
  if (instruction.type === "TimelineAddEntries" || instruction.entries) {
88
56
  for (const entry of instruction.entries || []) {
89
57
  processEntry(entry, tweets, users);
90
58
 
91
- // Extract cursors
92
59
  if (entry.entryId?.startsWith("cursor-bottom") || entry.entryId?.includes("cursor-bottom")) {
93
60
  nextCursor =
94
61
  entry.content?.value || entry.content?.itemContent?.value || null;
@@ -115,21 +82,18 @@ export default function parseTimeline(raw) {
115
82
  }
116
83
 
117
84
  function processEntry(entry, tweets, users) {
118
- // Direct tweet entry (tweet-*)
119
85
  const tweet = extractTweetFromEntry(entry);
120
86
  if (tweet) {
121
87
  tweets.push(tweet);
122
88
  return;
123
89
  }
124
90
 
125
- // Direct user entry (user-*)
126
91
  const user = extractUserFromEntry(entry);
127
92
  if (user) {
128
93
  users.push(user);
129
94
  return;
130
95
  }
131
96
 
132
- // Module entries (conversationthread-*, profile-grid-*, etc.)
133
97
  if (entry.content?.items) {
134
98
  for (const item of entry.content.items) {
135
99
  const t = extractTweetFromEntry(item);