apitweet-cli 0.1.1
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/LICENSE +21 -0
- package/README.md +349 -0
- package/bin/apitweet.js +11 -0
- package/package.json +37 -0
- package/src/commands.js +747 -0
- package/src/config.js +301 -0
- package/src/constants.js +34 -0
- package/src/help.js +86 -0
- package/src/index.js +15 -0
- package/src/parser.js +55 -0
- package/src/request.js +95 -0
- package/src/utils.js +164 -0
package/src/commands.js
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
apiPath,
|
|
5
|
+
collectPositionals,
|
|
6
|
+
exitWithError,
|
|
7
|
+
findAllOptions,
|
|
8
|
+
findOption,
|
|
9
|
+
hasFlag,
|
|
10
|
+
printJson,
|
|
11
|
+
readCountOption,
|
|
12
|
+
requireCommandValue,
|
|
13
|
+
sanitizeForOutput,
|
|
14
|
+
} from "./utils.js";
|
|
15
|
+
import {
|
|
16
|
+
handleAuthAppsCommand,
|
|
17
|
+
handleAuthProfilesCommand,
|
|
18
|
+
handleConfigCommand,
|
|
19
|
+
resolveCookieArg,
|
|
20
|
+
resolveAppState,
|
|
21
|
+
saveConfig,
|
|
22
|
+
} from "./config.js";
|
|
23
|
+
import { performRequest } from "./request.js";
|
|
24
|
+
|
|
25
|
+
const ARTICLE_PUBLISH_MD_USAGE = "Usage: apitweet article publish-md <file.md> --title <title> [--cover-image <url_or_path>] [--visibility Public|Followers|Mentioned]";
|
|
26
|
+
const ARTICLE_ID_PLACEHOLDER = "{article_id_from_step_1}";
|
|
27
|
+
const ARTICLE_VISIBILITIES = new Set(["Public", "Followers", "Mentioned"]);
|
|
28
|
+
|
|
29
|
+
function tweetActionBody(args, state, config) {
|
|
30
|
+
const proxy = findOption(args, "--proxy");
|
|
31
|
+
return {
|
|
32
|
+
cookie: resolveCookieArg(args, state, config),
|
|
33
|
+
...(proxy ? { proxy } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildTweetBody(args, state, config) {
|
|
38
|
+
const text = requireCommandValue(findOption(args, "--text"), "Usage: apitweet tweet create --text <content> [--media-url <url>] [--reply-to <tweet_id>]");
|
|
39
|
+
const mediaUrls = findAllOptions(args, "--media-url");
|
|
40
|
+
const proxy = findOption(args, "--proxy");
|
|
41
|
+
const delegatedAccount = findOption(args, "--delegated-account");
|
|
42
|
+
const communityName = findOption(args, "--community");
|
|
43
|
+
const schedule = findOption(args, "--schedule");
|
|
44
|
+
const replyTweetId = findOption(args, "--reply-to");
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
tweet_content: text,
|
|
48
|
+
cookie: resolveCookieArg(args, state, config),
|
|
49
|
+
...(mediaUrls[0] ? { media_url: mediaUrls[0] } : {}),
|
|
50
|
+
...(delegatedAccount ? { delegated_account_username: delegatedAccount } : {}),
|
|
51
|
+
...(communityName ? { community_name: communityName } : {}),
|
|
52
|
+
...(schedule ? { schedule } : {}),
|
|
53
|
+
...(replyTweetId ? { reply_tweet_id: replyTweetId } : {}),
|
|
54
|
+
...(proxy ? { proxy } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function handleAuthCookieCommand(state, config, args) {
|
|
59
|
+
const authToken = requireCommandValue(findOption(args, "--auth-token"), "Usage: apitweet auth cookie --auth-token <token> [--save-as <profile>] [--ct0 <value>]");
|
|
60
|
+
const saveAs = findOption(args, "--save-as");
|
|
61
|
+
const response = await performRequest(state, config, {
|
|
62
|
+
method: "GET",
|
|
63
|
+
path: `/twitter/${encodeURIComponent(authToken)}/cookie`,
|
|
64
|
+
silent: Boolean(saveAs),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (state.dryRun || !saveAs) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const cookie = typeof response.data?.data === "string" ? response.data.data : "";
|
|
72
|
+
if (!cookie) {
|
|
73
|
+
exitWithError("Cookie response did not contain a usable cookie string.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const ct0 = findOption(args, "--ct0");
|
|
77
|
+
config.profiles[saveAs] = {
|
|
78
|
+
cookie,
|
|
79
|
+
authToken,
|
|
80
|
+
...(ct0 ? { ct0 } : {}),
|
|
81
|
+
};
|
|
82
|
+
if (!config.currentProfile || hasFlag(args, "--use")) {
|
|
83
|
+
config.currentProfile = saveAs;
|
|
84
|
+
}
|
|
85
|
+
await saveConfig(state.configDir, config);
|
|
86
|
+
|
|
87
|
+
console.log(JSON.stringify({
|
|
88
|
+
saved: saveAs,
|
|
89
|
+
currentProfile: config.currentProfile,
|
|
90
|
+
profile: {
|
|
91
|
+
cookie: cookie ? `${cookie.slice(0, 4)}...${cookie.slice(-4)}` : "",
|
|
92
|
+
authToken: authToken ? `${authToken.slice(0, 4)}...${authToken.slice(-4)}` : "",
|
|
93
|
+
ct0: ct0 ? `${ct0.slice(0, 4)}...${ct0.slice(-4)}` : "",
|
|
94
|
+
},
|
|
95
|
+
}, null, 2));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function handleAuthCommand(state, config, args) {
|
|
99
|
+
const area = args[1];
|
|
100
|
+
if (area === "apps") {
|
|
101
|
+
await handleAuthAppsCommand(state, config, args);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (area === "profiles") {
|
|
105
|
+
await handleAuthProfilesCommand(state, config, args);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (area === "cookie") {
|
|
109
|
+
await handleAuthCookieCommand(state, config, args);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
exitWithError("Unknown auth command.", "Use: auth apps | auth profiles | auth cookie");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function handleUsersCommand(state, config, args) {
|
|
117
|
+
const usernames = collectPositionals(args, 1);
|
|
118
|
+
if (usernames.length === 0) {
|
|
119
|
+
exitWithError("Usage: apitweet users <username...>");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await performRequest(state, config, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
path: "/twitter/users",
|
|
125
|
+
body: usernames,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function handleAboutCommand(state, config, args) {
|
|
130
|
+
const screenName = requireCommandValue(args[1], "Usage: apitweet about <screen_name>");
|
|
131
|
+
await performRequest(state, config, {
|
|
132
|
+
method: "GET",
|
|
133
|
+
path: `/twitter/${encodeURIComponent(screenName)}/about`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function handleSearchCommand(state, config, args) {
|
|
138
|
+
const scope = args[1];
|
|
139
|
+
|
|
140
|
+
if (scope === "tweets") {
|
|
141
|
+
const terms = collectPositionals(args, 2, ["--count", "--sort"]);
|
|
142
|
+
if (terms.length === 0) {
|
|
143
|
+
exitWithError("Usage: apitweet search tweets <term...> [--count <n>] [--sort Latest|Top]");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await performRequest(state, config, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
path: "/twitter/advanced_search",
|
|
149
|
+
body: {
|
|
150
|
+
searchTerms: terms,
|
|
151
|
+
maxItems: readCountOption(args, 20),
|
|
152
|
+
sortBy: findOption(args, "--sort") || "Latest",
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (scope === "users") {
|
|
159
|
+
const keyword = requireCommandValue(args[2], "Usage: apitweet search users <keyword> [--count <n>]");
|
|
160
|
+
await performRequest(state, config, {
|
|
161
|
+
method: "GET",
|
|
162
|
+
path: `/twitter/search-user/${encodeURIComponent(keyword)}/${readCountOption(args, 20)}`,
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
exitWithError("Unknown search command.", "Use: search tweets | search users");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function handleArticleCommand(state, config, args) {
|
|
171
|
+
const action = args[1];
|
|
172
|
+
if (action === "markdown") {
|
|
173
|
+
const tweetId = requireCommandValue(args[2], "Usage: apitweet article markdown <tweet_id>");
|
|
174
|
+
await performRequest(state, config, {
|
|
175
|
+
method: "GET",
|
|
176
|
+
path: `/x/article/${encodeURIComponent(tweetId)}/markdown`,
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (action === "lookup") {
|
|
182
|
+
const tweetIds = collectPositionals(args, 2);
|
|
183
|
+
if (tweetIds.length === 0) {
|
|
184
|
+
exitWithError("Usage: apitweet article lookup <tweet_id...>");
|
|
185
|
+
}
|
|
186
|
+
await performRequest(state, config, {
|
|
187
|
+
method: "POST",
|
|
188
|
+
path: "/x/article",
|
|
189
|
+
body: tweetIds,
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (action === "publish-md") {
|
|
195
|
+
await handleArticlePublishMdCommand(state, config, args);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
exitWithError("Unknown article command.", "Use: article markdown | article lookup | article publish-md");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function articlePublishMdRequests(articleId, data) {
|
|
203
|
+
const { cookie, coverImage, markdown, title, visibility } = data;
|
|
204
|
+
const articlePath = `/x/articles/${articleId}`;
|
|
205
|
+
const requests = [
|
|
206
|
+
{
|
|
207
|
+
name: "create_draft",
|
|
208
|
+
method: "POST",
|
|
209
|
+
path: "/x/articles/draft",
|
|
210
|
+
body: { cookie },
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
if (coverImage) {
|
|
215
|
+
requests.push({
|
|
216
|
+
name: "set_cover",
|
|
217
|
+
method: "PUT",
|
|
218
|
+
path: `${articlePath}/cover`,
|
|
219
|
+
body: {
|
|
220
|
+
cookie,
|
|
221
|
+
cover_image: coverImage,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
requests.push(
|
|
227
|
+
{
|
|
228
|
+
name: "set_title",
|
|
229
|
+
method: "PUT",
|
|
230
|
+
path: `${articlePath}/title`,
|
|
231
|
+
body: {
|
|
232
|
+
cookie,
|
|
233
|
+
title,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "set_content",
|
|
238
|
+
method: "PUT",
|
|
239
|
+
path: `${articlePath}/content`,
|
|
240
|
+
body: {
|
|
241
|
+
cookie,
|
|
242
|
+
markdown,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: "publish",
|
|
247
|
+
method: "POST",
|
|
248
|
+
path: `${articlePath}/publish`,
|
|
249
|
+
body: {
|
|
250
|
+
cookie,
|
|
251
|
+
visibility,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return requests;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function buildDryRunPreview(state, config, request) {
|
|
260
|
+
const appState = resolveAppState(state, config);
|
|
261
|
+
const target = apiPath(request.path);
|
|
262
|
+
const url = /^https?:\/\//i.test(target) ? target : `${appState.baseUrl}${target}`;
|
|
263
|
+
const headers = {
|
|
264
|
+
Accept: "application/json",
|
|
265
|
+
...state.headers,
|
|
266
|
+
...request.headers,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (appState.apiKey) {
|
|
270
|
+
headers.Authorization = `Bearer ${appState.apiKey}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return sanitizeForOutput({
|
|
274
|
+
name: request.name,
|
|
275
|
+
method: request.method,
|
|
276
|
+
url,
|
|
277
|
+
headers,
|
|
278
|
+
...(request.body !== undefined ? { body: request.body } : {}),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function articleIdFromDraftResponse(response) {
|
|
283
|
+
const articleId = response.data?.data?.article_id;
|
|
284
|
+
if (!articleId) {
|
|
285
|
+
exitWithError("Draft response did not contain article_id.");
|
|
286
|
+
}
|
|
287
|
+
return articleId;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function readMarkdownFile(filePath) {
|
|
291
|
+
try {
|
|
292
|
+
return await fs.readFile(filePath, "utf8");
|
|
293
|
+
} catch (error) {
|
|
294
|
+
exitWithError("Failed to read markdown file.", error.message);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function handleArticlePublishMdCommand(state, config, args) {
|
|
299
|
+
const filePath = requireCommandValue(args[2], ARTICLE_PUBLISH_MD_USAGE);
|
|
300
|
+
const title = requireCommandValue(findOption(args, "--title"), ARTICLE_PUBLISH_MD_USAGE);
|
|
301
|
+
const coverImage = findOption(args, "--cover-image");
|
|
302
|
+
const visibility = findOption(args, "--visibility") || "Public";
|
|
303
|
+
|
|
304
|
+
if (!ARTICLE_VISIBILITIES.has(visibility)) {
|
|
305
|
+
exitWithError(`Invalid --visibility value: ${visibility}`, "Use: Public | Followers | Mentioned");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const markdown = await readMarkdownFile(filePath);
|
|
309
|
+
const cookie = resolveCookieArg(args, state, config);
|
|
310
|
+
const data = {
|
|
311
|
+
cookie,
|
|
312
|
+
coverImage,
|
|
313
|
+
markdown,
|
|
314
|
+
title,
|
|
315
|
+
visibility,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
if (state.dryRun) {
|
|
319
|
+
printJson({
|
|
320
|
+
articleId: ARTICLE_ID_PLACEHOLDER,
|
|
321
|
+
steps: articlePublishMdRequests(ARTICLE_ID_PLACEHOLDER, data).map((request) => buildDryRunPreview(state, config, request)),
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const draftRequest = articlePublishMdRequests("", data)[0];
|
|
327
|
+
const draftResponse = await performRequest(state, config, {
|
|
328
|
+
...draftRequest,
|
|
329
|
+
silent: true,
|
|
330
|
+
});
|
|
331
|
+
const articleId = articleIdFromDraftResponse(draftResponse);
|
|
332
|
+
const remainingRequests = articlePublishMdRequests(encodeURIComponent(articleId), data).slice(1);
|
|
333
|
+
|
|
334
|
+
for (const [index, request] of remainingRequests.entries()) {
|
|
335
|
+
await performRequest(state, config, {
|
|
336
|
+
...request,
|
|
337
|
+
silent: index < remainingRequests.length - 1,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function handleDmCommand(state, config, args) {
|
|
343
|
+
const action = args[1];
|
|
344
|
+
const proxy = findOption(args, "--proxy");
|
|
345
|
+
|
|
346
|
+
if (action === "history") {
|
|
347
|
+
const username = requireCommandValue(args[2], "Usage: apitweet dm history <username> [--max-id <id>]");
|
|
348
|
+
const maxId = findOption(args, "--max-id");
|
|
349
|
+
await performRequest(state, config, {
|
|
350
|
+
method: "POST",
|
|
351
|
+
path: "/twitter/dm-history",
|
|
352
|
+
body: {
|
|
353
|
+
username,
|
|
354
|
+
cookie: resolveCookieArg(args, state, config),
|
|
355
|
+
...(maxId ? { max_id: maxId } : {}),
|
|
356
|
+
...(proxy ? { proxy } : {}),
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (action === "send") {
|
|
363
|
+
const username = requireCommandValue(args[2], "Usage: apitweet dm send <username> --text <content> [--media-url <url>] [--reply-to <id>]");
|
|
364
|
+
const text = requireCommandValue(findOption(args, "--text"), "Usage: apitweet dm send <username> --text <content> [--media-url <url>] [--reply-to <id>]");
|
|
365
|
+
const mediaUrl = findOption(args, "--media-url");
|
|
366
|
+
const replyTo = findOption(args, "--reply-to");
|
|
367
|
+
|
|
368
|
+
await performRequest(state, config, {
|
|
369
|
+
method: "POST",
|
|
370
|
+
path: "/twitter/send-dm",
|
|
371
|
+
body: {
|
|
372
|
+
username,
|
|
373
|
+
msg: text,
|
|
374
|
+
cookie: resolveCookieArg(args, state, config),
|
|
375
|
+
...(mediaUrl ? { media: mediaUrl } : {}),
|
|
376
|
+
...(replyTo ? { reply_to: replyTo } : {}),
|
|
377
|
+
...(proxy ? { proxy } : {}),
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
exitWithError("Unknown dm command.", "Use: dm history | dm send");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function handleFollowersCommand(state, config, args) {
|
|
387
|
+
const screenName = requireCommandValue(args[1], "Usage: apitweet followers <screen_name> [--count <n>]");
|
|
388
|
+
await performRequest(state, config, {
|
|
389
|
+
method: "GET",
|
|
390
|
+
path: `/twitter/followers/${encodeURIComponent(screenName)}/${readCountOption(args, 200)}`,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function handleFollowingCommand(state, config, args) {
|
|
395
|
+
const screenName = requireCommandValue(args[1], "Usage: apitweet following <screen_name> [--count <n>]");
|
|
396
|
+
await performRequest(state, config, {
|
|
397
|
+
method: "GET",
|
|
398
|
+
path: `/twitter/following/${encodeURIComponent(screenName)}/${readCountOption(args, 200)}`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function handleTimelineCommand(state, config, args) {
|
|
403
|
+
const action = args[1];
|
|
404
|
+
if (action === "user") {
|
|
405
|
+
const screenName = requireCommandValue(args[2], "Usage: apitweet timeline user <screen_name> [--cursor <token>] [--count <n>]");
|
|
406
|
+
const cursor = findOption(args, "--cursor");
|
|
407
|
+
const count = readCountOption(args, 20);
|
|
408
|
+
|
|
409
|
+
await performRequest(state, config, {
|
|
410
|
+
method: "POST",
|
|
411
|
+
path: `/twitter/${encodeURIComponent(screenName)}/timeline/page`,
|
|
412
|
+
body: {
|
|
413
|
+
...(cursor ? { next_cursor: cursor } : {}),
|
|
414
|
+
count,
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
exitWithError("Unknown timeline command.", "Use: timeline user");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function handleTrendingCommand(state, config, args) {
|
|
424
|
+
const action = args[1];
|
|
425
|
+
|
|
426
|
+
if (action === "tweets") {
|
|
427
|
+
const country = requireCommandValue(findOption(args, "--country"), "Usage: apitweet trending tweets --country <country> [--topic <topic>] [--content <content>] [--count <n>]");
|
|
428
|
+
const topic = findOption(args, "--topic");
|
|
429
|
+
const content = findOption(args, "--content");
|
|
430
|
+
const count = readCountOption(args, 20);
|
|
431
|
+
const params = new URLSearchParams({ country });
|
|
432
|
+
|
|
433
|
+
if (topic) {
|
|
434
|
+
params.set("topic", topic);
|
|
435
|
+
}
|
|
436
|
+
if (content) {
|
|
437
|
+
params.set("content", content);
|
|
438
|
+
}
|
|
439
|
+
params.set("count", String(count));
|
|
440
|
+
|
|
441
|
+
await performRequest(state, config, {
|
|
442
|
+
method: "GET",
|
|
443
|
+
path: `/twitter/global-trending/tweets?${params.toString()}`,
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
exitWithError("Unknown trending command.", "Use: trending tweets");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function handleProfileCommand(state, config, args) {
|
|
452
|
+
const action = args[1];
|
|
453
|
+
if (action === "update") {
|
|
454
|
+
const name = findOption(args, "--name");
|
|
455
|
+
const description = findOption(args, "--description");
|
|
456
|
+
const location = findOption(args, "--location");
|
|
457
|
+
const website = findOption(args, "--website");
|
|
458
|
+
const profileImage = findOption(args, "--image-url");
|
|
459
|
+
const profileBanner = findOption(args, "--banner-url");
|
|
460
|
+
const proxy = findOption(args, "--proxy");
|
|
461
|
+
|
|
462
|
+
await performRequest(state, config, {
|
|
463
|
+
method: "POST",
|
|
464
|
+
path: "/twitter/profile",
|
|
465
|
+
body: {
|
|
466
|
+
cookie: resolveCookieArg(args, state, config),
|
|
467
|
+
...(name ? { name } : {}),
|
|
468
|
+
...(description ? { description } : {}),
|
|
469
|
+
...(location ? { location } : {}),
|
|
470
|
+
...(website ? { website } : {}),
|
|
471
|
+
...(profileImage ? { profile_image: profileImage } : {}),
|
|
472
|
+
...(profileBanner ? { profile_banner: profileBanner } : {}),
|
|
473
|
+
...(proxy ? { proxy } : {}),
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
exitWithError("Unknown profile command.", "Use: profile update");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function handleListCommand(state, config, args) {
|
|
483
|
+
const action = args[1];
|
|
484
|
+
|
|
485
|
+
if (action === "search") {
|
|
486
|
+
const query = requireCommandValue(findOption(args, "--query"), "Usage: apitweet list search --query <text> [--count <n>]");
|
|
487
|
+
await performRequest(state, config, {
|
|
488
|
+
method: "POST",
|
|
489
|
+
path: "/twitter/list/search",
|
|
490
|
+
body: {
|
|
491
|
+
query,
|
|
492
|
+
target_count: readCountOption(args, 20),
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (action === "create") {
|
|
499
|
+
const listName = requireCommandValue(findOption(args, "--name"), "Usage: apitweet list create --name <name> --description <text> [--private]");
|
|
500
|
+
const listDescription = requireCommandValue(findOption(args, "--description"), "Usage: apitweet list create --name <name> --description <text> [--private]");
|
|
501
|
+
await performRequest(state, config, {
|
|
502
|
+
method: "POST",
|
|
503
|
+
path: "/twitter/list/create",
|
|
504
|
+
body: {
|
|
505
|
+
cookie: resolveCookieArg(args, state, config),
|
|
506
|
+
list_name: listName,
|
|
507
|
+
list_description: listDescription,
|
|
508
|
+
is_private: hasFlag(args, "--private"),
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (action === "members") {
|
|
515
|
+
const listId = requireCommandValue(args[2], "Usage: apitweet list members <list_id> [--count <n>]");
|
|
516
|
+
await performRequest(state, config, {
|
|
517
|
+
method: "GET",
|
|
518
|
+
path: `/twitter/list/${encodeURIComponent(listId)}/members/${readCountOption(args, 100)}`,
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (action === "subscribers") {
|
|
524
|
+
const listId = requireCommandValue(args[2], "Usage: apitweet list subscribers <list_id> [--count <n>]");
|
|
525
|
+
await performRequest(state, config, {
|
|
526
|
+
method: "GET",
|
|
527
|
+
path: `/twitter/list/${encodeURIComponent(listId)}/subscribers/${readCountOption(args, 100)}`,
|
|
528
|
+
});
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
exitWithError("Unknown list command.", "Use: list search | list create | list members | list subscribers");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function handleTweetCommand(state, config, args) {
|
|
536
|
+
const action = args[1];
|
|
537
|
+
|
|
538
|
+
if (action === "create") {
|
|
539
|
+
await performRequest(state, config, {
|
|
540
|
+
method: "POST",
|
|
541
|
+
path: "/twitter/tweets/create",
|
|
542
|
+
body: buildTweetBody(args, state, config),
|
|
543
|
+
});
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (action === "quote") {
|
|
548
|
+
const quoteUrl = requireCommandValue(findOption(args, "--quote-url"), "Usage: apitweet tweet quote --text <content> --quote-url <url>");
|
|
549
|
+
await performRequest(state, config, {
|
|
550
|
+
method: "POST",
|
|
551
|
+
path: "/twitter/tweets/quote",
|
|
552
|
+
body: {
|
|
553
|
+
...buildTweetBody(args, state, config),
|
|
554
|
+
quote_tweet_url: quoteUrl,
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (action === "lookup") {
|
|
561
|
+
const tweetIds = collectPositionals(args, 2);
|
|
562
|
+
if (tweetIds.length === 0) {
|
|
563
|
+
exitWithError("Usage: apitweet tweet lookup <tweet_id...>");
|
|
564
|
+
}
|
|
565
|
+
const summary = args.includes("--summary");
|
|
566
|
+
const result = await performRequest(state, config, {
|
|
567
|
+
method: "POST",
|
|
568
|
+
path: "/twitter/tweets/lookup",
|
|
569
|
+
body: tweetIds,
|
|
570
|
+
silent: summary,
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
if (summary && result.data && Array.isArray(result.data.data)) {
|
|
574
|
+
for (const tweet of result.data.data) {
|
|
575
|
+
console.log(`${tweet.tweet_id}:${tweet.is_paid_promotion}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (action === "replies") {
|
|
582
|
+
const tweetId = requireCommandValue(args[2], "Usage: apitweet tweet replies <tweet_id> [--count <n>] [--sort Relevance|Recency|Likes]");
|
|
583
|
+
const count = readCountOption(args, 50);
|
|
584
|
+
const sort = findOption(args, "--sort");
|
|
585
|
+
const querySuffix = sort ? `?sort_by=${encodeURIComponent(sort)}` : "";
|
|
586
|
+
await performRequest(state, config, {
|
|
587
|
+
method: "GET",
|
|
588
|
+
path: `/twitter/tweets/${encodeURIComponent(tweetId)}/replies/${count}${querySuffix}`,
|
|
589
|
+
});
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (action === "like" || action === "unlike" || action === "bookmark" || action === "unbookmark" || action === "retweet" || action === "unretweet") {
|
|
594
|
+
const tweetId = requireCommandValue(args[2], `Usage: apitweet tweet ${action} <tweet_id>`);
|
|
595
|
+
const mapping = {
|
|
596
|
+
like: { method: "POST", path: `/twitter/tweets/${encodeURIComponent(tweetId)}/like` },
|
|
597
|
+
unlike: { method: "DELETE", path: `/twitter/tweets/${encodeURIComponent(tweetId)}/like` },
|
|
598
|
+
bookmark: { method: "POST", path: `/twitter/tweets/${encodeURIComponent(tweetId)}/bookmark` },
|
|
599
|
+
unbookmark: { method: "DELETE", path: `/twitter/tweets/${encodeURIComponent(tweetId)}/bookmark` },
|
|
600
|
+
retweet: { method: "POST", path: `/twitter/tweets/${encodeURIComponent(tweetId)}/retweet` },
|
|
601
|
+
unretweet: { method: "DELETE", path: `/twitter/tweets/${encodeURIComponent(tweetId)}/retweet` },
|
|
602
|
+
};
|
|
603
|
+
await performRequest(state, config, {
|
|
604
|
+
...mapping[action],
|
|
605
|
+
body: tweetActionBody(args, state, config),
|
|
606
|
+
});
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
exitWithError(
|
|
611
|
+
"Unknown tweet command.",
|
|
612
|
+
"Use: tweet create | tweet quote | tweet lookup | tweet replies | tweet like | tweet unlike | tweet bookmark | tweet unbookmark | tweet retweet | tweet unretweet",
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function handleUserCommand(state, config, args) {
|
|
617
|
+
const action = args[1];
|
|
618
|
+
const username = requireCommandValue(args[2], "Usage: apitweet user follow <username> | user unfollow <username>");
|
|
619
|
+
const proxy = findOption(args, "--proxy");
|
|
620
|
+
|
|
621
|
+
if (action === "follow") {
|
|
622
|
+
await performRequest(state, config, {
|
|
623
|
+
method: "POST",
|
|
624
|
+
path: "/twitter/user/follow",
|
|
625
|
+
body: {
|
|
626
|
+
username,
|
|
627
|
+
cookie: resolveCookieArg(args, state, config),
|
|
628
|
+
...(proxy ? { proxy } : {}),
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (action === "unfollow") {
|
|
635
|
+
await performRequest(state, config, {
|
|
636
|
+
method: "DELETE",
|
|
637
|
+
path: "/twitter/user/follow",
|
|
638
|
+
body: {
|
|
639
|
+
username,
|
|
640
|
+
cookie: resolveCookieArg(args, state, config),
|
|
641
|
+
...(proxy ? { proxy } : {}),
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
exitWithError("Unknown user command.", "Use: user follow | user unfollow");
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function handleGenericRequest(state, config, args) {
|
|
651
|
+
const requestPath = requireCommandValue(args[0], "Usage: apitweet <path>");
|
|
652
|
+
await performRequest(state, config, {
|
|
653
|
+
method: state.method,
|
|
654
|
+
path: requestPath,
|
|
655
|
+
body: state.data,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export async function runCommand(state, config) {
|
|
660
|
+
const [command] = state.commandArgs;
|
|
661
|
+
|
|
662
|
+
if (!command) {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (command.startsWith("/") || /^https?:\/\//i.test(command)) {
|
|
667
|
+
await handleGenericRequest(state, config, state.commandArgs);
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (command === "config") {
|
|
672
|
+
await handleConfigCommand(state, config, state.commandArgs);
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (command === "auth") {
|
|
677
|
+
await handleAuthCommand(state, config, state.commandArgs);
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (command === "users") {
|
|
682
|
+
await handleUsersCommand(state, config, state.commandArgs);
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (command === "about") {
|
|
687
|
+
await handleAboutCommand(state, config, state.commandArgs);
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (command === "search") {
|
|
692
|
+
await handleSearchCommand(state, config, state.commandArgs);
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (command === "followers") {
|
|
697
|
+
await handleFollowersCommand(state, config, state.commandArgs);
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (command === "following") {
|
|
702
|
+
await handleFollowingCommand(state, config, state.commandArgs);
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (command === "list") {
|
|
707
|
+
await handleListCommand(state, config, state.commandArgs);
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (command === "tweet") {
|
|
712
|
+
await handleTweetCommand(state, config, state.commandArgs);
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (command === "user") {
|
|
717
|
+
await handleUserCommand(state, config, state.commandArgs);
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (command === "article") {
|
|
722
|
+
await handleArticleCommand(state, config, state.commandArgs);
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (command === "dm") {
|
|
727
|
+
await handleDmCommand(state, config, state.commandArgs);
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (command === "profile") {
|
|
732
|
+
await handleProfileCommand(state, config, state.commandArgs);
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (command === "timeline") {
|
|
737
|
+
await handleTimelineCommand(state, config, state.commandArgs);
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (command === "trending") {
|
|
742
|
+
await handleTrendingCommand(state, config, state.commandArgs);
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
exitWithError(`Unknown command: ${command}`, "Run apitweet --help for usage.");
|
|
747
|
+
}
|