emusks 0.0.3 → 2.0.0

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 (42) hide show
  1. package/README.md +4 -277
  2. package/build/graphql.js +24 -0
  3. package/build/v1.1.js +28 -0
  4. package/build/v2.js +28 -0
  5. package/package.json +16 -8
  6. package/src/clients.js +62 -0
  7. package/src/cycletls.js +26 -0
  8. package/src/flow.js +399 -0
  9. package/src/graphql.js +70 -0
  10. package/src/helpers/account.js +271 -0
  11. package/src/helpers/bookmarks.js +120 -0
  12. package/src/helpers/communities.js +271 -0
  13. package/src/helpers/dms.js +131 -0
  14. package/src/helpers/index.js +32 -0
  15. package/src/helpers/lists.js +229 -0
  16. package/src/helpers/media.js +290 -0
  17. package/src/helpers/notifications.js +49 -0
  18. package/src/helpers/search.js +129 -0
  19. package/src/helpers/spaces.js +55 -0
  20. package/src/helpers/syndication.js +33 -0
  21. package/src/helpers/timelines.js +84 -0
  22. package/src/helpers/topics.js +99 -0
  23. package/src/helpers/trends.js +86 -0
  24. package/src/helpers/tweets.js +405 -0
  25. package/src/helpers/users.js +321 -0
  26. package/src/index.js +125 -55
  27. package/src/parsers/timeline.js +141 -0
  28. package/src/static/graphql.js +1 -0
  29. package/src/static/v1.1.js +1 -0
  30. package/src/static/v2.js +1 -0
  31. package/src/v1.1.js +64 -0
  32. package/src/v2.js +64 -0
  33. package/src/methods/bookmarks.js +0 -91
  34. package/src/methods/follow.js +0 -88
  35. package/src/methods/like.js +0 -87
  36. package/src/methods/notifications.js +0 -121
  37. package/src/methods/retweet.js +0 -91
  38. package/src/methods/search.js +0 -124
  39. package/src/methods/timeline.js +0 -215
  40. package/src/methods/tweet.js +0 -396
  41. package/src/methods/users.js +0 -209
  42. package/src/utils/headers.js +0 -23
@@ -0,0 +1,131 @@
1
+ export default (client) => ({
2
+ async inbox(params = {}) {
3
+ const res = await client.v1_1("dm/inbox_initial_state", { params });
4
+ return await res.json();
5
+ },
6
+
7
+ async conversation(conversationId, params = {}) {
8
+ const res = await client.v1_1("dm/conversation", {
9
+ params: { id: conversationId, ...params },
10
+ });
11
+ return await res.json();
12
+ },
13
+
14
+ async search(query, opts = {}) {
15
+ return await client.graphql("DmAllSearchSlice", {
16
+ variables: {
17
+ query,
18
+ count: opts.count || 20,
19
+ cursor: opts.cursor,
20
+ ...opts.variables,
21
+ },
22
+ });
23
+ },
24
+
25
+ async searchGroups(query, opts = {}) {
26
+ return await client.graphql("DmGroupSearchSlice", {
27
+ variables: {
28
+ query,
29
+ count: opts.count || 20,
30
+ cursor: opts.cursor,
31
+ ...opts.variables,
32
+ },
33
+ });
34
+ },
35
+
36
+ async searchPeople(query, opts = {}) {
37
+ return await client.graphql("DmPeopleSearchSlice", {
38
+ variables: {
39
+ query,
40
+ count: opts.count || 20,
41
+ cursor: opts.cursor,
42
+ ...opts.variables,
43
+ },
44
+ });
45
+ },
46
+
47
+ async block(userId) {
48
+ return await client.graphql("dmBlockUser", {
49
+ body: { variables: { user_id: userId } },
50
+ });
51
+ },
52
+
53
+ async unblock(userId) {
54
+ return await client.graphql("dmUnblockUser", {
55
+ body: { variables: { user_id: userId } },
56
+ });
57
+ },
58
+
59
+ async deleteConversations(conversationIds) {
60
+ const ids = Array.isArray(conversationIds) ? conversationIds : [conversationIds];
61
+ const res = await client.v1_1("dm/conversation/bulk_delete", {
62
+ body: JSON.stringify({ conversation_ids: ids }),
63
+ });
64
+ return await res.json();
65
+ },
66
+
67
+ async updateLastSeen(eventId) {
68
+ const res = await client.v1_1("dm/update_last_seen_event_id", {
69
+ body: JSON.stringify({ last_seen_event_id: eventId, trusted_last_seen_event_id: eventId }),
70
+ });
71
+ return await res.json();
72
+ },
73
+
74
+ async muted(opts = {}) {
75
+ return await client.graphql("DmMutedTimeline", {
76
+ variables: {
77
+ count: opts.count || 20,
78
+ cursor: opts.cursor,
79
+ ...opts.variables,
80
+ },
81
+ });
82
+ },
83
+
84
+ async edit(messageId, conversationId, text) {
85
+ const res = await client.v1_1("dm/edit", {
86
+ body: JSON.stringify({
87
+ message_id: messageId,
88
+ conversation_id: conversationId,
89
+ text,
90
+ }),
91
+ });
92
+ return await res.json();
93
+ },
94
+
95
+ async permissions(params = {}) {
96
+ const res = await client.v1_1("dm/permissions", { params });
97
+ return await res.json();
98
+ },
99
+
100
+ async nsfwFilter(enabled) {
101
+ return await client.graphql("DmNsfwMediaFilterUpdate", {
102
+ body: { variables: { enabled } },
103
+ });
104
+ },
105
+
106
+ async updateRelationship(userId, action) {
107
+ const res = await client.v1_1("dm/user/update_relationship_state", {
108
+ body: JSON.stringify({ user_id: userId, action }),
109
+ });
110
+ return await res.json();
111
+ },
112
+
113
+ async reportSpam(conversationId, messageId) {
114
+ const res = await client.v1_1("direct_messages/report_spam", {
115
+ body: JSON.stringify({ conversation_id: conversationId, message_id: messageId }),
116
+ });
117
+ return await res.json();
118
+ },
119
+
120
+ async report(conversationId, messageId) {
121
+ const res = await client.v1_1("dm/report", {
122
+ body: JSON.stringify({ conversation_id: conversationId, message_id: messageId }),
123
+ });
124
+ return await res.json();
125
+ },
126
+
127
+ async userUpdates(params = {}) {
128
+ const res = await client.v1_1("dm/user_updates", { params });
129
+ return await res.json();
130
+ },
131
+ });
@@ -0,0 +1,32 @@
1
+ import account from "./account.js";
2
+ import bookmarks from "./bookmarks.js";
3
+ import communities from "./communities.js";
4
+ import dms from "./dms.js";
5
+ import lists from "./lists.js";
6
+ import media from "./media.js";
7
+ import notifications from "./notifications.js";
8
+ import search from "./search.js";
9
+ import spaces from "./spaces.js";
10
+ import timelines from "./timelines.js";
11
+ import topics from "./topics.js";
12
+ import trends from "./trends.js";
13
+ import tweets from "./tweets.js";
14
+ import users from "./users.js";
15
+
16
+ export default (client) => ({
17
+ tweets: tweets(client),
18
+ users: users(client),
19
+ timelines: timelines(client),
20
+ bookmarks: bookmarks(client),
21
+ dms: dms(client),
22
+ lists: lists(client),
23
+ communities: communities(client),
24
+ search: search(client),
25
+ spaces: spaces(client),
26
+ account: account(client),
27
+ notifications: notifications(client),
28
+ trends: trends(client),
29
+ topics: topics(client),
30
+ media: media(client),
31
+ syndication: syndication(client),
32
+ });
@@ -0,0 +1,229 @@
1
+ export default (client) => ({
2
+ async create(name, opts = {}) {
3
+ return await client.graphql("CreateList", {
4
+ body: {
5
+ variables: {
6
+ isPrivate: opts.private || false,
7
+ name,
8
+ description: opts.description || "",
9
+ },
10
+ },
11
+ });
12
+ },
13
+
14
+ async delete(listId) {
15
+ return await client.graphql("DeleteList", {
16
+ body: { variables: { listId } },
17
+ });
18
+ },
19
+
20
+ async update(listId, opts = {}) {
21
+ return await client.graphql("UpdateList", {
22
+ body: {
23
+ variables: {
24
+ listId,
25
+ ...(opts.name !== undefined ? { name: opts.name } : {}),
26
+ ...(opts.description !== undefined ? { description: opts.description } : {}),
27
+ ...(opts.private !== undefined ? { isPrivate: opts.private } : {}),
28
+ },
29
+ },
30
+ });
31
+ },
32
+
33
+ async get(listId) {
34
+ return await client.graphql("ListByRestId", {
35
+ variables: { listId },
36
+ });
37
+ },
38
+
39
+ async getBySlug(slug, opts = {}) {
40
+ return await client.graphql("ListBySlug", {
41
+ variables: { slug, listOwnerScreenName: opts.ownerScreenName || "" },
42
+ });
43
+ },
44
+
45
+ async addMember(listId, userId) {
46
+ return await client.graphql("ListAddMember", {
47
+ body: { variables: { listId, userId } },
48
+ });
49
+ },
50
+
51
+ async removeMember(listId, userId) {
52
+ return await client.graphql("ListRemoveMember", {
53
+ body: { variables: { listId, userId } },
54
+ });
55
+ },
56
+
57
+ async members(listId, opts = {}) {
58
+ return await client.graphql("ListMembers", {
59
+ variables: {
60
+ listId,
61
+ count: opts.count || 20,
62
+ cursor: opts.cursor,
63
+ ...opts.variables,
64
+ },
65
+ });
66
+ },
67
+
68
+ async subscribers(listId, opts = {}) {
69
+ return await client.graphql("ListSubscribers", {
70
+ variables: {
71
+ listId,
72
+ count: opts.count || 20,
73
+ cursor: opts.cursor,
74
+ ...opts.variables,
75
+ },
76
+ });
77
+ },
78
+
79
+ async subscribe(listId) {
80
+ return await client.graphql("ListSubscribe", {
81
+ body: { variables: { listId } },
82
+ });
83
+ },
84
+
85
+ async unsubscribe(listId) {
86
+ return await client.graphql("ListUnsubscribe", {
87
+ body: { variables: { listId } },
88
+ });
89
+ },
90
+
91
+ async mute(listId) {
92
+ return await client.graphql("MuteList", {
93
+ body: { variables: { listId } },
94
+ });
95
+ },
96
+
97
+ async unmute(listId) {
98
+ return await client.graphql("UnmuteList", {
99
+ body: { variables: { listId } },
100
+ });
101
+ },
102
+
103
+ async timeline(listId, opts = {}) {
104
+ return await client.graphql("ListLatestTweetsTimeline", {
105
+ variables: {
106
+ listId,
107
+ count: opts.count || 20,
108
+ cursor: opts.cursor,
109
+ ...opts.variables,
110
+ },
111
+ fieldToggles: {
112
+ withArticlePlainText: false,
113
+ withArticleRichContentState: false,
114
+ withAuxiliaryUserLabels: false,
115
+ },
116
+ });
117
+ },
118
+
119
+ async ranked(listId, opts = {}) {
120
+ return await client.graphql("ListRankedTweetsTimeline", {
121
+ variables: {
122
+ listId,
123
+ count: opts.count || 20,
124
+ cursor: opts.cursor,
125
+ ...opts.variables,
126
+ },
127
+ fieldToggles: {
128
+ withArticlePlainText: false,
129
+ withArticleRichContentState: false,
130
+ withAuxiliaryUserLabels: false,
131
+ },
132
+ });
133
+ },
134
+
135
+ async search(listId, query, opts = {}) {
136
+ return await client.graphql("ListSearchTimeline", {
137
+ variables: {
138
+ listId,
139
+ search_query: query,
140
+ count: opts.count || 20,
141
+ cursor: opts.cursor,
142
+ ...opts.variables,
143
+ },
144
+ });
145
+ },
146
+
147
+ async ownerships(userId, opts = {}) {
148
+ return await client.graphql("ListOwnerships", {
149
+ variables: {
150
+ userId,
151
+ count: opts.count || 20,
152
+ cursor: opts.cursor,
153
+ isCreator: true,
154
+ ...opts.variables,
155
+ },
156
+ });
157
+ },
158
+
159
+ async memberships(userId, opts = {}) {
160
+ return await client.graphql("ListMemberships", {
161
+ variables: {
162
+ userId,
163
+ count: opts.count || 20,
164
+ cursor: opts.cursor,
165
+ ...opts.variables,
166
+ },
167
+ });
168
+ },
169
+
170
+ async discover(opts = {}) {
171
+ return await client.graphql("ListsDiscovery", {
172
+ variables: {
173
+ count: opts.count || 20,
174
+ cursor: opts.cursor,
175
+ ...opts.variables,
176
+ },
177
+ });
178
+ },
179
+
180
+ async combined(opts = {}) {
181
+ return await client.graphql("CombinedLists", {
182
+ variables: {
183
+ count: opts.count || 20,
184
+ cursor: opts.cursor,
185
+ ...opts.variables,
186
+ },
187
+ });
188
+ },
189
+
190
+ async manage(opts = {}) {
191
+ return await client.graphql("ListsManagementPageTimeline", {
192
+ variables: {
193
+ count: opts.count || 20,
194
+ cursor: opts.cursor,
195
+ ...opts.variables,
196
+ },
197
+ });
198
+ },
199
+
200
+ async editBanner(listId, mediaId) {
201
+ return await client.graphql("EditListBanner", {
202
+ body: { variables: { listId, mediaId } },
203
+ });
204
+ },
205
+
206
+ async deleteBanner(listId) {
207
+ return await client.graphql("DeleteListBanner", {
208
+ body: { variables: { listId } },
209
+ });
210
+ },
211
+
212
+ async pinTimeline(timelineId) {
213
+ return await client.graphql("PinTimeline", {
214
+ body: { variables: { timeline_id: timelineId } },
215
+ });
216
+ },
217
+
218
+ async unpinTimeline(timelineId) {
219
+ return await client.graphql("UnpinTimeline", {
220
+ body: { variables: { timeline_id: timelineId } },
221
+ });
222
+ },
223
+
224
+ async pinned() {
225
+ return await client.graphql("PinnedTimelines", {
226
+ variables: {},
227
+ });
228
+ },
229
+ });
@@ -0,0 +1,290 @@
1
+ import { readFile } from "fs/promises";
2
+ import { extname } from "path";
3
+ import getCycleTLS from "../cycletls.js";
4
+
5
+ const UPLOAD_URL = "https://upload.twitter.com/i/media/upload.json";
6
+ const CHUNK_SIZE = 4 * 1024 * 1024; // 4 MB
7
+
8
+ const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5 MB
9
+ const MAX_GIF_SIZE = 15 * 1024 * 1024; // 15 MB
10
+ const MAX_VIDEO_SIZE = 512 * 1024 * 1024; // 512 MB
11
+
12
+ const MIME_BY_EXT = {
13
+ ".jpg": "image/jpeg",
14
+ ".jpeg": "image/jpeg",
15
+ ".png": "image/png",
16
+ ".gif": "image/gif",
17
+ ".webp": "image/webp",
18
+ ".mp4": "video/mp4",
19
+ ".mov": "video/quicktime",
20
+ ".webm": "video/webm",
21
+ ".avi": "video/x-msvideo",
22
+ };
23
+
24
+ function detectMediaType(buf) {
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";
28
+ if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) return "image/gif";
29
+ if (
30
+ buf[0] === 0x52 &&
31
+ buf[1] === 0x49 &&
32
+ buf[2] === 0x46 &&
33
+ buf[3] === 0x46 &&
34
+ buf[8] === 0x57 &&
35
+ buf[9] === 0x45 &&
36
+ buf[10] === 0x42 &&
37
+ buf[11] === 0x50
38
+ )
39
+ 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
+ )
47
+ return "video/mp4";
48
+ if (buf[0] === 0x1a && buf[1] === 0x45 && buf[2] === 0xdf && buf[3] === 0xa3)
49
+ return "video/webm";
50
+ return null;
51
+ }
52
+
53
+ function getMediaCategory(mediaType, uploadType) {
54
+ if (mediaType === "image/gif") return `${uploadType}_gif`;
55
+ if (mediaType.startsWith("video/")) return `${uploadType}_video`;
56
+ if (mediaType.startsWith("image/")) return `${uploadType}_image`;
57
+ throw new Error(`unsupported media type: ${mediaType}`);
58
+ }
59
+
60
+ function checkMediaSize(category, size) {
61
+ 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 — max is ${fmt(MAX_IMAGE_SIZE)}`,
69
+ );
70
+ if (category.includes("gif") && size > MAX_GIF_SIZE)
71
+ throw new Error(
72
+ `cannot upload ${fmt(size)} gif — max is ${fmt(MAX_GIF_SIZE)}`,
73
+ );
74
+ if (category.includes("video") && size > MAX_VIDEO_SIZE)
75
+ throw new Error(
76
+ `cannot upload ${fmt(size)} video — max is ${fmt(MAX_VIDEO_SIZE)}`,
77
+ );
78
+ }
79
+
80
+ async function makeUploadRequest(
81
+ client,
82
+ method,
83
+ params,
84
+ body,
85
+ extraHeaders = {},
86
+ ) {
87
+ const cycleTLS = await getCycleTLS();
88
+ const url = `${UPLOAD_URL}?${new URLSearchParams(params).toString()}`;
89
+
90
+ const headers = {
91
+ accept: "*/*",
92
+ "accept-language": "en-US,en;q=0.9",
93
+ authorization: `Bearer ${client.auth.client.bearer}`,
94
+ "x-csrf-token": client.auth.csrfToken,
95
+ "x-twitter-active-user": "yes",
96
+ "x-twitter-auth-type": "OAuth2Session",
97
+ "x-twitter-client-language": "en",
98
+ priority: "u=1, i",
99
+ "sec-ch-ua": '"Not(A:Brand";v="8", "Chromium";v="144"',
100
+ "sec-ch-ua-mobile": "?0",
101
+ "sec-ch-ua-platform": '"macOS"',
102
+ "sec-fetch-dest": "empty",
103
+ "sec-fetch-mode": "cors",
104
+ "sec-fetch-site": "same-site",
105
+ "sec-gpc": "1",
106
+ cookie:
107
+ client.auth.client.headers.cookie +
108
+ (client.elevatedCookies ? `; ${client.elevatedCookies}` : ""),
109
+ ...extraHeaders,
110
+ };
111
+
112
+ return await cycleTLS(
113
+ url,
114
+ {
115
+ headers,
116
+ userAgent:
117
+ client.auth.client.fingerprints?.userAgent ||
118
+ client.auth.client.fingerprints?.["user-agent"],
119
+ ja3: client.auth.client.fingerprints?.ja3,
120
+ ja4r: client.auth.client.fingerprints?.ja4r,
121
+ body: body || undefined,
122
+ proxy: client.proxy || undefined,
123
+ referrer: "https://x.com/",
124
+ },
125
+ method,
126
+ );
127
+ }
128
+
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
+ async create(source, opts = {}) {
141
+ if (!client.auth) throw new Error("you must be logged in to upload media");
142
+
143
+ // ── normalise input to Buffer ──────────────────────────────────────
144
+ let buf;
145
+ let mediaType = opts.mediaType;
146
+
147
+ if (typeof source === "string") {
148
+ buf = await readFile(source);
149
+ if (!mediaType) mediaType = MIME_BY_EXT[extname(source).toLowerCase()];
150
+ } else if (typeof Blob !== "undefined" && source instanceof Blob) {
151
+ buf = Buffer.from(await source.arrayBuffer());
152
+ if (!mediaType) mediaType = source.type || undefined;
153
+ } else if (
154
+ source instanceof ArrayBuffer ||
155
+ source instanceof SharedArrayBuffer
156
+ ) {
157
+ buf = Buffer.from(source);
158
+ } else if (Buffer.isBuffer(source) || source instanceof Uint8Array) {
159
+ buf = Buffer.from(source);
160
+ } else {
161
+ throw new Error(
162
+ "source must be a file path (string), Buffer, Uint8Array, ArrayBuffer, or Blob",
163
+ );
164
+ }
165
+
166
+ if (!mediaType) mediaType = detectMediaType(buf);
167
+ if (!mediaType)
168
+ throw new Error(
169
+ "could not detect media type — pass opts.mediaType (e.g. 'image/png')",
170
+ );
171
+
172
+ const totalBytes = buf.length;
173
+ const uploadType = opts.type === "dm" ? "dm" : "tweet";
174
+ const category = getMediaCategory(mediaType, uploadType);
175
+ checkMediaSize(category, totalBytes);
176
+
177
+ // ── INIT ───────────────────────────────────────────────────────────
178
+ const initRes = await makeUploadRequest(client, "post", {
179
+ command: "INIT",
180
+ media_type: mediaType,
181
+ total_bytes: totalBytes.toString(),
182
+ media_category: category,
183
+ });
184
+
185
+ const initData = await initRes.json();
186
+ if (!initData?.media_id_string) {
187
+ throw new Error(`upload INIT failed: ${JSON.stringify(initData)}`);
188
+ }
189
+ const mediaId = initData.media_id_string;
190
+
191
+ // ── APPEND (chunked, base64) ───────────────────────────────────────
192
+ let segmentIndex = 0;
193
+ for (let offset = 0; offset < totalBytes; offset += CHUNK_SIZE) {
194
+ const chunk = buf.slice(
195
+ offset,
196
+ Math.min(offset + CHUNK_SIZE, totalBytes),
197
+ );
198
+ const base64 = chunk.toString("base64");
199
+
200
+ await makeUploadRequest(
201
+ client,
202
+ "post",
203
+ {
204
+ command: "APPEND",
205
+ media_id: mediaId,
206
+ segment_index: segmentIndex.toString(),
207
+ },
208
+ `media_data=${encodeURIComponent(base64)}`,
209
+ { "content-type": "application/x-www-form-urlencoded" },
210
+ );
211
+ segmentIndex++;
212
+ }
213
+
214
+ // ── FINALIZE ───────────────────────────────────────────────────────
215
+ const finalizeRes = await makeUploadRequest(client, "post", {
216
+ command: "FINALIZE",
217
+ media_id: mediaId,
218
+ allow_async: "true",
219
+ });
220
+
221
+ const finalizeData = await finalizeRes.json();
222
+ if (finalizeData?.error) {
223
+ throw new Error(
224
+ `upload FINALIZE failed: ${JSON.stringify(finalizeData)}`,
225
+ );
226
+ }
227
+
228
+ // ── poll for async processing (videos / gifs) ──────────────────────
229
+ let processingInfo = finalizeData?.processing_info;
230
+ while (processingInfo) {
231
+ if (processingInfo.error) {
232
+ throw new Error(
233
+ `media processing error: ${JSON.stringify(processingInfo.error)}`,
234
+ );
235
+ }
236
+ if (processingInfo.state === "succeeded") break;
237
+ if (processingInfo.state === "failed") {
238
+ throw new Error(
239
+ `media processing failed: ${JSON.stringify(processingInfo)}`,
240
+ );
241
+ }
242
+
243
+ const wait = (processingInfo.check_after_secs || 2) * 1000;
244
+ await new Promise((r) => setTimeout(r, wait));
245
+
246
+ const statusRes = await makeUploadRequest(client, "get", {
247
+ command: "STATUS",
248
+ media_id: mediaId,
249
+ });
250
+ const statusData = await statusRes.json();
251
+ processingInfo = statusData?.processing_info;
252
+ }
253
+
254
+ // ── alt text ───────────────────────────────────────────────────────
255
+ if (opts.alt_text) {
256
+ await client.v1_1("media/metadata/create", {
257
+ body: JSON.stringify({
258
+ media_id: mediaId,
259
+ alt_text: { text: opts.alt_text },
260
+ }),
261
+ });
262
+ }
263
+
264
+ return { media_id: mediaId, ...finalizeData };
265
+ },
266
+
267
+ async createMetadata(mediaId, altText, opts = {}) {
268
+ const res = await client.v1_1("media/metadata/create", {
269
+ body: JSON.stringify({
270
+ media_id: mediaId,
271
+ alt_text: { text: altText },
272
+ ...opts,
273
+ }),
274
+ });
275
+ return await res.json();
276
+ },
277
+
278
+ async createSubtitles(mediaId, subtitles) {
279
+ const res = await client.v1_1("media/subtitles/create", {
280
+ body: JSON.stringify({
281
+ media_id: mediaId,
282
+ media_category: "tweet_video",
283
+ subtitle_info: {
284
+ subtitles: Array.isArray(subtitles) ? subtitles : [subtitles],
285
+ },
286
+ }),
287
+ });
288
+ return await res.json();
289
+ },
290
+ });