@northmoon-labs/social-cli 0.1.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.
- package/LICENSE +17 -0
- package/README.md +478 -0
- package/dist/bin/nmsocial.js +14 -0
- package/dist/cli/args.js +103 -0
- package/dist/cli/config.js +69 -0
- package/dist/cli/help.js +21 -0
- package/dist/cli/output.js +386 -0
- package/dist/cli/router.js +82 -0
- package/dist/commands/accounts.js +19 -0
- package/dist/commands/auth.js +4 -0
- package/dist/commands/auto-replies.js +88 -0
- package/dist/commands/comments.js +49 -0
- package/dist/commands/materials.js +19 -0
- package/dist/commands/posts.js +40 -0
- package/dist/commands/publish.js +69 -0
- package/dist/commands/session.js +40 -0
- package/dist/commands/stats.js +65 -0
- package/dist/commands/tasks.js +17 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands/update.js +138 -0
- package/dist/commands/webhook.js +16 -0
- package/dist/commands/youtube.js +16 -0
- package/dist/social/client.js +95 -0
- package/dist/social/types.js +47 -0
- package/dist/utils/query.js +30 -0
- package/dist/utils/stdin.js +17 -0
- package/dist/utils/validation.js +76 -0
- package/package.json +50 -0
- package/skills/nmsocial-cli/SKILL.md +155 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import Table from 'cli-table3';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
export class CliError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
status;
|
|
6
|
+
payload;
|
|
7
|
+
meta;
|
|
8
|
+
constructor(code, message, options = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'CliError';
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.status = options.status ?? null;
|
|
13
|
+
this.payload = options.payload ?? null;
|
|
14
|
+
this.meta = options.meta ?? null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function maskSecret(value) {
|
|
18
|
+
if (!value)
|
|
19
|
+
return null;
|
|
20
|
+
if (value.length <= 8)
|
|
21
|
+
return '***';
|
|
22
|
+
return `${value.slice(0, 4)}***${value.slice(-4)}`;
|
|
23
|
+
}
|
|
24
|
+
export function printSuccess(data, provider, json) {
|
|
25
|
+
if (json) {
|
|
26
|
+
console.log(JSON.stringify({
|
|
27
|
+
ok: true,
|
|
28
|
+
data,
|
|
29
|
+
error: null,
|
|
30
|
+
meta: provider ? {
|
|
31
|
+
providerCode: provider.c,
|
|
32
|
+
providerMessage: provider.m,
|
|
33
|
+
providerTimestamp: provider.t,
|
|
34
|
+
requestId: provider.lb
|
|
35
|
+
} : null
|
|
36
|
+
}, null, 2));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (typeof data === 'string') {
|
|
40
|
+
console.log(data);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.log(formatHuman(data));
|
|
44
|
+
}
|
|
45
|
+
export function printError(error, json) {
|
|
46
|
+
const cliError = normalizeError(error);
|
|
47
|
+
if (json) {
|
|
48
|
+
console.log(JSON.stringify({
|
|
49
|
+
ok: false,
|
|
50
|
+
data: null,
|
|
51
|
+
error: {
|
|
52
|
+
code: cliError.code,
|
|
53
|
+
message: cliError.message,
|
|
54
|
+
status: cliError.status,
|
|
55
|
+
payload: cliError.payload
|
|
56
|
+
},
|
|
57
|
+
meta: cliError.meta
|
|
58
|
+
}, null, 2));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.error(`${stderrColor('nmsocial:', pc.red)} ${cliError.message}`);
|
|
62
|
+
}
|
|
63
|
+
export function normalizeError(error) {
|
|
64
|
+
if (error instanceof CliError)
|
|
65
|
+
return error;
|
|
66
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
67
|
+
return new CliError('NETWORK_TIMEOUT', 'request timed out');
|
|
68
|
+
}
|
|
69
|
+
return new CliError('NETWORK_ERROR', error instanceof Error ? error.message : 'unexpected error', { payload: error });
|
|
70
|
+
}
|
|
71
|
+
function formatHuman(data) {
|
|
72
|
+
if (data === null || data === undefined)
|
|
73
|
+
return stdoutColor('OK', pc.green);
|
|
74
|
+
if (typeof data === 'number' || typeof data === 'boolean' || typeof data === 'bigint')
|
|
75
|
+
return String(data);
|
|
76
|
+
if (Array.isArray(data))
|
|
77
|
+
return formatTable(data);
|
|
78
|
+
if (!isRecord(data))
|
|
79
|
+
return String(data);
|
|
80
|
+
if (data.dryRun === true && isRecord(data.body)) {
|
|
81
|
+
return JSON.stringify(data.body, null, 2);
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(data.records))
|
|
84
|
+
return formatTable(data.records);
|
|
85
|
+
if (Array.isArray(data.items))
|
|
86
|
+
return formatTable(data.items);
|
|
87
|
+
if (isConfigLike(data))
|
|
88
|
+
return formatKeyValues(configLabels(data));
|
|
89
|
+
if (isOverviewLike(data))
|
|
90
|
+
return formatKeyValues(metricLabels(data));
|
|
91
|
+
const keys = Object.keys(data);
|
|
92
|
+
if (keys.length === 0)
|
|
93
|
+
return stdoutColor('OK', pc.green);
|
|
94
|
+
if (keys.length <= 12 && keys.every((key) => isScalar(data[key]))) {
|
|
95
|
+
return formatKeyValues(data);
|
|
96
|
+
}
|
|
97
|
+
return JSON.stringify(data, null, 2);
|
|
98
|
+
}
|
|
99
|
+
function formatTable(rows) {
|
|
100
|
+
if (rows.length === 0)
|
|
101
|
+
return 'No results';
|
|
102
|
+
const records = rows.filter(isRecord);
|
|
103
|
+
if (records.length !== rows.length)
|
|
104
|
+
return rows.map((row) => String(row)).join('\n');
|
|
105
|
+
const columns = selectColumns(records);
|
|
106
|
+
if (!columns.length)
|
|
107
|
+
return `${records.length} result${records.length === 1 ? '' : 's'}`;
|
|
108
|
+
const table = new Table({
|
|
109
|
+
head: columns.map((column) => stdoutColor(humanizeKey(column).toLowerCase(), pc.bold)),
|
|
110
|
+
chars: {
|
|
111
|
+
top: '',
|
|
112
|
+
'top-mid': '',
|
|
113
|
+
'top-left': '',
|
|
114
|
+
'top-right': '',
|
|
115
|
+
bottom: '',
|
|
116
|
+
'bottom-mid': '',
|
|
117
|
+
'bottom-left': '',
|
|
118
|
+
'bottom-right': '',
|
|
119
|
+
left: '',
|
|
120
|
+
'left-mid': '',
|
|
121
|
+
mid: '-',
|
|
122
|
+
'mid-mid': ' ',
|
|
123
|
+
right: '',
|
|
124
|
+
'right-mid': '',
|
|
125
|
+
middle: ' '
|
|
126
|
+
},
|
|
127
|
+
style: {
|
|
128
|
+
border: [],
|
|
129
|
+
compact: true,
|
|
130
|
+
head: []
|
|
131
|
+
},
|
|
132
|
+
wordWrap: true
|
|
133
|
+
});
|
|
134
|
+
for (const record of records) {
|
|
135
|
+
table.push(columns.map((column) => displayCell(record[column], column, record)));
|
|
136
|
+
}
|
|
137
|
+
return table.toString();
|
|
138
|
+
}
|
|
139
|
+
function selectColumns(records) {
|
|
140
|
+
const preferred = [
|
|
141
|
+
'id',
|
|
142
|
+
'taskId',
|
|
143
|
+
'name',
|
|
144
|
+
'title',
|
|
145
|
+
'type',
|
|
146
|
+
'platform',
|
|
147
|
+
'platformName',
|
|
148
|
+
'openId',
|
|
149
|
+
'accountName',
|
|
150
|
+
'accountNameSummary',
|
|
151
|
+
'username',
|
|
152
|
+
'nickname',
|
|
153
|
+
'commentUsername',
|
|
154
|
+
'text',
|
|
155
|
+
'status',
|
|
156
|
+
'visibilityStatus',
|
|
157
|
+
'enabled',
|
|
158
|
+
'taskType',
|
|
159
|
+
'postType',
|
|
160
|
+
'triggerRule',
|
|
161
|
+
'sendRule',
|
|
162
|
+
'metric',
|
|
163
|
+
'value',
|
|
164
|
+
'increase',
|
|
165
|
+
'createTime',
|
|
166
|
+
'updateTime',
|
|
167
|
+
'publishTime',
|
|
168
|
+
'startTime',
|
|
169
|
+
'endTime',
|
|
170
|
+
'url'
|
|
171
|
+
];
|
|
172
|
+
const seen = new Set();
|
|
173
|
+
const available = new Set(records.flatMap((record) => Object.keys(record)));
|
|
174
|
+
const columns = preferred.filter((column) => available.has(column) && !seen.has(column) && seen.add(column));
|
|
175
|
+
for (const record of records) {
|
|
176
|
+
for (const key of Object.keys(record)) {
|
|
177
|
+
if (columns.length >= 8)
|
|
178
|
+
return columns;
|
|
179
|
+
if (!columns.includes(key) && isScalar(record[key]))
|
|
180
|
+
columns.push(key);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return columns;
|
|
184
|
+
}
|
|
185
|
+
function formatKeyValues(record) {
|
|
186
|
+
return Object.entries(record)
|
|
187
|
+
.filter(([, val]) => val !== undefined && val !== null && val !== '')
|
|
188
|
+
.map(([key, val]) => `${stdoutColor(humanizeKey(key), pc.cyan)}: ${displayValue(val)}`)
|
|
189
|
+
.join('\n') || stdoutColor('OK', pc.green);
|
|
190
|
+
}
|
|
191
|
+
function configLabels(record) {
|
|
192
|
+
return {
|
|
193
|
+
'API URL': record.apiUrl,
|
|
194
|
+
'API Key': 'hasApiKey' in record ? record.hasApiKey ? record.apiKey || 'configured' : 'not configured' : record.apiKey,
|
|
195
|
+
'API Key Source': record.apiKeySource,
|
|
196
|
+
'Config': record.configPath,
|
|
197
|
+
'Config Mode': record.configMode
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function metricLabels(record) {
|
|
201
|
+
return {
|
|
202
|
+
Accounts: record.accountCount,
|
|
203
|
+
Followers: record.followers,
|
|
204
|
+
'Follower Increase': record.followerIncrease,
|
|
205
|
+
Posts: record.postCount,
|
|
206
|
+
Views: record.viewCount,
|
|
207
|
+
'View Increase': record.viewIncrease,
|
|
208
|
+
Likes: record.likeCount,
|
|
209
|
+
Comments: record.commentCount,
|
|
210
|
+
Shares: record.shareCount,
|
|
211
|
+
Interactions: record.totalInteractions
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function displayValue(value) {
|
|
215
|
+
if (value === null || value === undefined)
|
|
216
|
+
return '';
|
|
217
|
+
if (typeof value === 'string')
|
|
218
|
+
return value;
|
|
219
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint')
|
|
220
|
+
return String(value);
|
|
221
|
+
return JSON.stringify(value);
|
|
222
|
+
}
|
|
223
|
+
function displayCell(value, column, record) {
|
|
224
|
+
return truncate(displayValue(mapDisplayValue(value, column, record)), maxCellWidth(column));
|
|
225
|
+
}
|
|
226
|
+
function mapDisplayValue(value, column, record) {
|
|
227
|
+
if (isTimeColumn(column))
|
|
228
|
+
return formatDateTime(value) ?? value;
|
|
229
|
+
if (column === 'platform')
|
|
230
|
+
return platformLabel(value) ?? value;
|
|
231
|
+
if (column === 'taskType' || column === 'postType')
|
|
232
|
+
return postTypeLabel(value) ?? value;
|
|
233
|
+
if (column === 'type' && isMaterialLike(record))
|
|
234
|
+
return postTypeLabel(value) ?? value;
|
|
235
|
+
if (column === 'start')
|
|
236
|
+
return startLabel(value) ?? value;
|
|
237
|
+
if (column === 'triggerRule')
|
|
238
|
+
return triggerRuleLabel(value) ?? value;
|
|
239
|
+
if (column === 'sendRule')
|
|
240
|
+
return sendRuleLabel(value) ?? value;
|
|
241
|
+
if (column === 'enabled')
|
|
242
|
+
return enabledLabel(value) ?? value;
|
|
243
|
+
if (isYesNoColumn(column))
|
|
244
|
+
return yesNoLabel(value) ?? value;
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
function platformLabel(value) {
|
|
248
|
+
const labels = {
|
|
249
|
+
'1': 'tiktok',
|
|
250
|
+
'2': 'youtube',
|
|
251
|
+
'3': 'instagram',
|
|
252
|
+
'4': 'facebook'
|
|
253
|
+
};
|
|
254
|
+
return labels[String(value).trim()] ?? null;
|
|
255
|
+
}
|
|
256
|
+
function postTypeLabel(value) {
|
|
257
|
+
const labels = {
|
|
258
|
+
'1': 'video',
|
|
259
|
+
'2': 'image'
|
|
260
|
+
};
|
|
261
|
+
return labels[String(value).trim()] ?? null;
|
|
262
|
+
}
|
|
263
|
+
function startLabel(value) {
|
|
264
|
+
const labels = {
|
|
265
|
+
'1': 'now',
|
|
266
|
+
'2': 'scheduled'
|
|
267
|
+
};
|
|
268
|
+
return labels[String(value).trim()] ?? null;
|
|
269
|
+
}
|
|
270
|
+
function triggerRuleLabel(value) {
|
|
271
|
+
const labels = {
|
|
272
|
+
'1': 'keyword',
|
|
273
|
+
'2': 'instant'
|
|
274
|
+
};
|
|
275
|
+
return labels[String(value).trim()] ?? null;
|
|
276
|
+
}
|
|
277
|
+
function sendRuleLabel(value) {
|
|
278
|
+
const labels = {
|
|
279
|
+
'1': 'random',
|
|
280
|
+
'2': 'sequence'
|
|
281
|
+
};
|
|
282
|
+
return labels[String(value).trim()] ?? null;
|
|
283
|
+
}
|
|
284
|
+
function enabledLabel(value) {
|
|
285
|
+
const labels = {
|
|
286
|
+
'1': 'enabled',
|
|
287
|
+
'2': 'disabled',
|
|
288
|
+
true: 'enabled',
|
|
289
|
+
false: 'disabled'
|
|
290
|
+
};
|
|
291
|
+
return labels[String(value).trim()] ?? null;
|
|
292
|
+
}
|
|
293
|
+
function yesNoLabel(value) {
|
|
294
|
+
const labels = {
|
|
295
|
+
'0': 'no',
|
|
296
|
+
'1': 'yes',
|
|
297
|
+
'2': 'no',
|
|
298
|
+
true: 'yes',
|
|
299
|
+
false: 'no'
|
|
300
|
+
};
|
|
301
|
+
return labels[String(value).trim()] ?? null;
|
|
302
|
+
}
|
|
303
|
+
function formatDateTime(value) {
|
|
304
|
+
const date = parseDateTime(value);
|
|
305
|
+
if (!date)
|
|
306
|
+
return null;
|
|
307
|
+
const year = date.getFullYear();
|
|
308
|
+
const month = pad2(date.getMonth() + 1);
|
|
309
|
+
const day = pad2(date.getDate());
|
|
310
|
+
const hour = pad2(date.getHours());
|
|
311
|
+
const minute = pad2(date.getMinutes());
|
|
312
|
+
return `${year}-${month}-${day} ${hour}:${minute}`;
|
|
313
|
+
}
|
|
314
|
+
function parseDateTime(value) {
|
|
315
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 10000000000) {
|
|
316
|
+
const date = new Date(value);
|
|
317
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
318
|
+
}
|
|
319
|
+
if (typeof value !== 'string')
|
|
320
|
+
return null;
|
|
321
|
+
const trimmed = value.trim();
|
|
322
|
+
if (!trimmed || /^\d{4}-\d{2}-\d{2}$/.test(trimmed))
|
|
323
|
+
return null;
|
|
324
|
+
const date = new Date(trimmed);
|
|
325
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
326
|
+
}
|
|
327
|
+
function isTimeColumn(column) {
|
|
328
|
+
return /(?:time|date)$/i.test(column);
|
|
329
|
+
}
|
|
330
|
+
function pad2(value) {
|
|
331
|
+
return String(value).padStart(2, '0');
|
|
332
|
+
}
|
|
333
|
+
function maxCellWidth(column) {
|
|
334
|
+
if (column === 'name' || column === 'title')
|
|
335
|
+
return 36;
|
|
336
|
+
if (column === 'text')
|
|
337
|
+
return 42;
|
|
338
|
+
if (column === 'url')
|
|
339
|
+
return 48;
|
|
340
|
+
if (isTimeColumn(column))
|
|
341
|
+
return 16;
|
|
342
|
+
return 28;
|
|
343
|
+
}
|
|
344
|
+
function truncate(value, maxLength) {
|
|
345
|
+
if (value.length <= maxLength)
|
|
346
|
+
return value;
|
|
347
|
+
if (maxLength <= 3)
|
|
348
|
+
return value.slice(0, maxLength);
|
|
349
|
+
return `${value.slice(0, maxLength - 3)}...`;
|
|
350
|
+
}
|
|
351
|
+
function humanizeKey(key) {
|
|
352
|
+
if (key.includes(' '))
|
|
353
|
+
return key;
|
|
354
|
+
return key
|
|
355
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
356
|
+
.replace(/^./, (char) => char.toUpperCase());
|
|
357
|
+
}
|
|
358
|
+
function isRecord(value) {
|
|
359
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
360
|
+
}
|
|
361
|
+
function isScalar(value) {
|
|
362
|
+
return value === null
|
|
363
|
+
|| value === undefined
|
|
364
|
+
|| ['string', 'number', 'boolean', 'bigint'].includes(typeof value);
|
|
365
|
+
}
|
|
366
|
+
function isConfigLike(record) {
|
|
367
|
+
return 'configPath' in record && ('apiUrl' in record || 'hasApiKey' in record);
|
|
368
|
+
}
|
|
369
|
+
function isOverviewLike(record) {
|
|
370
|
+
return 'accountCount' in record || 'postCount' in record || 'viewCount' in record || 'totalInteractions' in record;
|
|
371
|
+
}
|
|
372
|
+
function isMaterialLike(record) {
|
|
373
|
+
return 'url' in record && ('title' in record || 'type' in record);
|
|
374
|
+
}
|
|
375
|
+
function isYesNoColumn(column) {
|
|
376
|
+
return column === 'isBusinessAccount'
|
|
377
|
+
|| column === 'isVerified'
|
|
378
|
+
|| column === 'oncePerUser'
|
|
379
|
+
|| column === 'stopOnUserReply';
|
|
380
|
+
}
|
|
381
|
+
function stdoutColor(value, color) {
|
|
382
|
+
return process.stdout.isTTY ? color(value) : value;
|
|
383
|
+
}
|
|
384
|
+
function stderrColor(value, color) {
|
|
385
|
+
return process.stderr.isTTY ? color(value) : value;
|
|
386
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { has, positiveIntOption, value } from './args.js';
|
|
2
|
+
import { defaultApiUrl, resolveConfig } from './config.js';
|
|
3
|
+
import { CliError } from './output.js';
|
|
4
|
+
import { HELP } from './help.js';
|
|
5
|
+
import { SocialClient } from '../social/client.js';
|
|
6
|
+
import { authUrl } from '../commands/auth.js';
|
|
7
|
+
import { autoRepliesCommand } from '../commands/auto-replies.js';
|
|
8
|
+
import { commentsCommand } from '../commands/comments.js';
|
|
9
|
+
import { login, logout, status } from '../commands/session.js';
|
|
10
|
+
import { listAccounts } from '../commands/accounts.js';
|
|
11
|
+
import { materialsCommand } from '../commands/materials.js';
|
|
12
|
+
import { postsCommand } from '../commands/posts.js';
|
|
13
|
+
import { publishCommand } from '../commands/publish.js';
|
|
14
|
+
import { statsCommand } from '../commands/stats.js';
|
|
15
|
+
import { listTasks } from '../commands/tasks.js';
|
|
16
|
+
import { packageMetadata, updateCommand } from '../commands/update.js';
|
|
17
|
+
import { webhookCommand } from '../commands/webhook.js';
|
|
18
|
+
import { youtubeSeriesCommand } from '../commands/youtube.js';
|
|
19
|
+
export async function runCommand(parsed) {
|
|
20
|
+
const { positionals, options } = parsed;
|
|
21
|
+
if (has(options, 'version'))
|
|
22
|
+
return { data: (await packageMetadata()).version };
|
|
23
|
+
if (has(options, 'help') || positionals[0] === 'help' || positionals.length === 0)
|
|
24
|
+
return { data: HELP };
|
|
25
|
+
const [group, action, subaction] = positionals;
|
|
26
|
+
if (group === 'login')
|
|
27
|
+
return login(options);
|
|
28
|
+
if (group === 'logout')
|
|
29
|
+
return logout();
|
|
30
|
+
if (group === 'status')
|
|
31
|
+
return status(options);
|
|
32
|
+
const dryRun = Boolean(options['dry-run']);
|
|
33
|
+
if (group === 'update') {
|
|
34
|
+
const result = await updateCommand(action, options, dryRun);
|
|
35
|
+
if (result)
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
const timeout = positiveIntOption(options, 'timeout', 30000);
|
|
39
|
+
const resolved = dryRun ? { apiUrl: value(options, 'api-url') || defaultApiUrl(), apiKey: 'dry-run' } : await resolveConfig(options);
|
|
40
|
+
const client = new SocialClient({ apiUrl: resolved.apiUrl, apiKey: resolved.apiKey || 'dry-run' }, { timeout });
|
|
41
|
+
if (group === 'auth-url')
|
|
42
|
+
return authUrl(client, options);
|
|
43
|
+
if (group === 'accounts' && action === 'list')
|
|
44
|
+
return listAccounts(client, options);
|
|
45
|
+
if (group === 'youtube' && action === 'series') {
|
|
46
|
+
const result = youtubeSeriesCommand(client, subaction, options, dryRun);
|
|
47
|
+
if (result)
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
if (group === 'materials') {
|
|
51
|
+
const result = await materialsCommand(client, action, positionals[2], options, dryRun);
|
|
52
|
+
if (result)
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
if (group === 'publish')
|
|
56
|
+
return publishCommand(client, action, options, dryRun);
|
|
57
|
+
if (group === 'tasks' && action === 'list')
|
|
58
|
+
return listTasks(client, options);
|
|
59
|
+
if (group === 'posts') {
|
|
60
|
+
const result = postsCommand(client, action, options);
|
|
61
|
+
if (result)
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
if (group === 'comments') {
|
|
65
|
+
const result = commentsCommand(client, action, options, dryRun);
|
|
66
|
+
if (result)
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
if (group === 'webhook') {
|
|
70
|
+
const result = webhookCommand(client, action, options, dryRun);
|
|
71
|
+
if (result)
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
if (group === 'stats')
|
|
75
|
+
return statsCommand(client, action, subaction, options);
|
|
76
|
+
if (group === 'auto-replies') {
|
|
77
|
+
const result = await autoRepliesCommand(client, action, options, dryRun);
|
|
78
|
+
if (result)
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
throw new CliError('VALIDATION_INVALID_OPTION', `unknown command: ${positionals.join(' ')}`);
|
|
82
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { intOption, value } from '../cli/args.js';
|
|
2
|
+
import { pageQuery } from '../utils/query.js';
|
|
3
|
+
import { platformOption, timeOption } from '../utils/validation.js';
|
|
4
|
+
export function listAccounts(client, options) {
|
|
5
|
+
return client.get('/api/bs/auth-account/page', pageQuery(options, 100, {
|
|
6
|
+
platform: platformOption(options),
|
|
7
|
+
openId: value(options, 'open-id'),
|
|
8
|
+
accountName: value(options, 'account-name'),
|
|
9
|
+
username: value(options, 'username'),
|
|
10
|
+
nickname: value(options, 'nickname'),
|
|
11
|
+
isBusinessAccount: intOption(options, 'is-business-account', [0, 1]),
|
|
12
|
+
isVerified: intOption(options, 'is-verified', [0, 1]),
|
|
13
|
+
status: intOption(options, 'status', [0, 1]),
|
|
14
|
+
authTimeBegin: timeOption(options, 'auth-time-begin'),
|
|
15
|
+
authTimeEnd: timeOption(options, 'auth-time-end'),
|
|
16
|
+
createTimeBegin: timeOption(options, 'create-time-begin'),
|
|
17
|
+
createTimeEnd: timeOption(options, 'create-time-end')
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { intOption, requireOption, value, values } from '../cli/args.js';
|
|
3
|
+
import { CliError } from '../cli/output.js';
|
|
4
|
+
import { dryRunResult, pageQuery, pickDefined } from '../utils/query.js';
|
|
5
|
+
import { platformOption } from '../utils/validation.js';
|
|
6
|
+
export async function autoRepliesCommand(client, action, options, dryRun) {
|
|
7
|
+
if (action === 'list') {
|
|
8
|
+
return client.get('/api/bs/auto-reply/page', pageQuery(options, 100, {
|
|
9
|
+
name: value(options, 'name'),
|
|
10
|
+
creatorName: value(options, 'creator-name'),
|
|
11
|
+
triggerRule: triggerRuleOption(options),
|
|
12
|
+
platform: platformOption(options)
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
if (action === 'detail')
|
|
16
|
+
return client.get('/api/bs/auto-reply/detail', { id: requireOption(options, 'id') });
|
|
17
|
+
if (action === 'delete') {
|
|
18
|
+
const body = { id: requireOption(options, 'id') };
|
|
19
|
+
return dryRunResult(dryRun, body) || client.post('/api/bs/auto-reply/delete', body);
|
|
20
|
+
}
|
|
21
|
+
if (action === 'enable' || action === 'disable') {
|
|
22
|
+
const body = { id: requireOption(options, 'id'), enabled: action === 'enable' ? 1 : 2 };
|
|
23
|
+
return dryRunResult(dryRun, body) || client.post('/api/bs/auto-reply/enabled', body);
|
|
24
|
+
}
|
|
25
|
+
if (action === 'create' || action === 'update') {
|
|
26
|
+
const body = await autoReplyBody(options, action === 'update');
|
|
27
|
+
return dryRunResult(dryRun, body) || client.post(`/api/bs/auto-reply/${action}`, body);
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
async function autoReplyBody(options, requireId) {
|
|
32
|
+
let body = {};
|
|
33
|
+
const fromFile = value(options, 'from-file');
|
|
34
|
+
if (fromFile)
|
|
35
|
+
body = JSON.parse(await readFile(fromFile, 'utf8'));
|
|
36
|
+
body = {
|
|
37
|
+
...body,
|
|
38
|
+
id: requireId ? requireOption(options, 'id') : value(options, 'id') || body.id,
|
|
39
|
+
name: value(options, 'name') || body.name,
|
|
40
|
+
platform: platformOption(options) || body.platform,
|
|
41
|
+
accountIds: values(options, 'account-id').join(',') || body.accountIds,
|
|
42
|
+
triggerRule: triggerRuleOption(options) || body.triggerRule,
|
|
43
|
+
keywords: values(options, 'keyword').join('\n') || body.keywords,
|
|
44
|
+
sendRule: sendRuleOption(options) || body.sendRule,
|
|
45
|
+
contents: autoReplyContents(options) || body.contents,
|
|
46
|
+
oncePerUser: boolEnum(options, 'once-per-user') || body.oncePerUser,
|
|
47
|
+
repeatIntervalHours: intOption(options, 'repeat-interval-hours') || body.repeatIntervalHours,
|
|
48
|
+
stopOnUserReply: boolEnum(options, 'stop-on-user-reply') || body.stopOnUserReply,
|
|
49
|
+
enabled: boolEnum(options, 'enabled') || body.enabled
|
|
50
|
+
};
|
|
51
|
+
return pickDefined(body);
|
|
52
|
+
}
|
|
53
|
+
function autoReplyContents(options) {
|
|
54
|
+
const contents = values(options, 'content');
|
|
55
|
+
if (!contents.length)
|
|
56
|
+
return undefined;
|
|
57
|
+
return JSON.stringify(contents.map((contentText, index) => ({ contentText, delaySeconds: Number(values(options, 'content-delay')[index] || 0) })));
|
|
58
|
+
}
|
|
59
|
+
function triggerRuleOption(options) {
|
|
60
|
+
const raw = value(options, 'trigger-rule');
|
|
61
|
+
if (raw === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (raw === 'keyword' || raw === '1')
|
|
64
|
+
return 1;
|
|
65
|
+
if (raw === 'instant' || raw === '2')
|
|
66
|
+
return 2;
|
|
67
|
+
throw new CliError('VALIDATION_INVALID_OPTION', '--trigger-rule must be keyword, instant, 1, or 2');
|
|
68
|
+
}
|
|
69
|
+
function sendRuleOption(options) {
|
|
70
|
+
const raw = value(options, 'send-rule');
|
|
71
|
+
if (raw === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
if (raw === 'random' || raw === '1')
|
|
74
|
+
return 1;
|
|
75
|
+
if (raw === 'sequence' || raw === '2')
|
|
76
|
+
return 2;
|
|
77
|
+
throw new CliError('VALIDATION_INVALID_OPTION', '--send-rule must be random, sequence, 1, or 2');
|
|
78
|
+
}
|
|
79
|
+
function boolEnum(options, key) {
|
|
80
|
+
const raw = value(options, key);
|
|
81
|
+
if (raw === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
if (raw === 'true' || raw === '1')
|
|
84
|
+
return 1;
|
|
85
|
+
if (raw === 'false' || raw === '2')
|
|
86
|
+
return 2;
|
|
87
|
+
throw new CliError('VALIDATION_INVALID_OPTION', `--${key} must be true, false, 1, or 2`);
|
|
88
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { intOption, requireOption, value } from '../cli/args.js';
|
|
2
|
+
import { CliError } from '../cli/output.js';
|
|
3
|
+
import { dryRunResult, pageQuery, pickDefined } from '../utils/query.js';
|
|
4
|
+
import { platformOption, timeOption } from '../utils/validation.js';
|
|
5
|
+
export function commentsCommand(client, action, options, dryRun) {
|
|
6
|
+
if (action === 'list')
|
|
7
|
+
return client.get('/api/bs/comment/page', commentsQuery(options));
|
|
8
|
+
if (action === 'reply') {
|
|
9
|
+
const body = commentReplyBody(options);
|
|
10
|
+
return dryRunResult(dryRun, body) || client.post('/api/bs/comment/reply-create', body);
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
function commentsQuery(options) {
|
|
15
|
+
return pageQuery(options, 100, {
|
|
16
|
+
platform: platformOption(options),
|
|
17
|
+
openId: value(options, 'open-id'),
|
|
18
|
+
videoId: value(options, 'video-id'),
|
|
19
|
+
socialPostId: value(options, 'social-post-id'),
|
|
20
|
+
commentType: value(options, 'comment-type'),
|
|
21
|
+
commentAction: value(options, 'comment-action'),
|
|
22
|
+
status: value(options, 'status'),
|
|
23
|
+
visibilityStatus: value(options, 'visibility-status'),
|
|
24
|
+
commentUsername: value(options, 'comment-username'),
|
|
25
|
+
keyword: value(options, 'keyword'),
|
|
26
|
+
startTime: timeOption(options, 'start-time'),
|
|
27
|
+
endTime: timeOption(options, 'end-time')
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function commentReplyBody(options) {
|
|
31
|
+
const body = pickDefined({
|
|
32
|
+
platform: platformOption(options, true),
|
|
33
|
+
openId: requireOption(options, 'open-id'),
|
|
34
|
+
videoId: requireOption(options, 'video-id'),
|
|
35
|
+
commentId: requireOption(options, 'comment-id'),
|
|
36
|
+
text: value(options, 'text'),
|
|
37
|
+
replyImageUrl: value(options, 'reply-image-url'),
|
|
38
|
+
imageUri: value(options, 'image-uri'),
|
|
39
|
+
imageWidth: intOption(options, 'image-width'),
|
|
40
|
+
imageHeight: intOption(options, 'image-height')
|
|
41
|
+
});
|
|
42
|
+
if (!body.text && !body.replyImageUrl && !body.imageUri) {
|
|
43
|
+
throw new CliError('VALIDATION_REQUIRED_OPTION', '--text, --reply-image-url, or --image-uri is required');
|
|
44
|
+
}
|
|
45
|
+
if (body.imageUri && (body.imageWidth === undefined || body.imageHeight === undefined)) {
|
|
46
|
+
throw new CliError('VALIDATION_REQUIRED_OPTION', '--image-width and --image-height are required with --image-uri');
|
|
47
|
+
}
|
|
48
|
+
return body;
|
|
49
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { intOption, value } from '../cli/args.js';
|
|
2
|
+
import { CliError } from '../cli/output.js';
|
|
3
|
+
import { dryRunResult, pageQuery } from '../utils/query.js';
|
|
4
|
+
import { validateUploadFile } from '../utils/validation.js';
|
|
5
|
+
export async function materialsCommand(client, action, file, options, dryRun) {
|
|
6
|
+
if (action === 'list') {
|
|
7
|
+
return client.get('/api/bs/material/page', pageQuery(options, 200, {
|
|
8
|
+
type: intOption(options, 'type', [1, 2]),
|
|
9
|
+
title: value(options, 'title')
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
if (action === 'upload') {
|
|
13
|
+
if (!file)
|
|
14
|
+
throw new CliError('VALIDATION_REQUIRED_OPTION', 'missing upload file path');
|
|
15
|
+
await validateUploadFile(file);
|
|
16
|
+
return dryRunResult(dryRun, { file }) || client.upload('/api/bs/material/upload', file);
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|