zalo-agent-cli 1.0.8 → 1.0.10
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/package.json +1 -1
- package/src/commands/friend.js +138 -2
- package/src/commands/msg.js +86 -24
package/package.json
CHANGED
package/src/commands/friend.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Friend commands — list, find, info, add, accept, remove, block, unblock, last-online, online.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { getApi } from "../core/zalo-client.js";
|
|
6
|
-
import { success, error, info, output } from "../utils/output.js";
|
|
5
|
+
import { getApi, autoLogin, clearSession } from "../core/zalo-client.js";
|
|
6
|
+
import { success, error, info, warning, output } from "../utils/output.js";
|
|
7
7
|
|
|
8
8
|
/** Extract numeric error code from zca-js error message string. */
|
|
9
9
|
function extractErrorCode(msg) {
|
|
@@ -166,4 +166,140 @@ export function registerFriendCommands(program) {
|
|
|
166
166
|
error(e.message);
|
|
167
167
|
}
|
|
168
168
|
});
|
|
169
|
+
|
|
170
|
+
/** Map FriendEventType enum to readable labels */
|
|
171
|
+
const FRIEND_EVENT_LABELS = {
|
|
172
|
+
0: "FRIEND_ADDED",
|
|
173
|
+
1: "FRIEND_REMOVED",
|
|
174
|
+
2: "FRIEND_REQUEST",
|
|
175
|
+
3: "UNDO_REQUEST",
|
|
176
|
+
4: "REJECT_REQUEST",
|
|
177
|
+
5: "SEEN_REQUEST",
|
|
178
|
+
6: "BLOCKED",
|
|
179
|
+
7: "UNBLOCKED",
|
|
180
|
+
8: "BLOCK_CALL",
|
|
181
|
+
9: "UNBLOCK_CALL",
|
|
182
|
+
10: "PIN_UNPIN",
|
|
183
|
+
11: "PIN_CREATE",
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
friend
|
|
187
|
+
.command("listen")
|
|
188
|
+
.description(
|
|
189
|
+
"Listen for friend events: new requests, accepts, removes, blocks. Use --json for machine parsing. Auto-reconnect enabled.",
|
|
190
|
+
)
|
|
191
|
+
.option(
|
|
192
|
+
"-f, --filter <type>",
|
|
193
|
+
"Filter events: request (new requests only), add (accepted only), all (default)",
|
|
194
|
+
"all",
|
|
195
|
+
)
|
|
196
|
+
.option("-w, --webhook <url>", "POST each event as JSON to this URL (for n8n, Make, etc.)")
|
|
197
|
+
.option("--auto-accept", "Automatically accept incoming friend requests")
|
|
198
|
+
.action(async (opts) => {
|
|
199
|
+
const jsonMode = program.opts().json;
|
|
200
|
+
const startTime = Date.now();
|
|
201
|
+
|
|
202
|
+
function uptime() {
|
|
203
|
+
const s = Math.floor((Date.now() - startTime) / 1000);
|
|
204
|
+
const h = Math.floor(s / 3600);
|
|
205
|
+
const m = Math.floor((s % 3600) / 60);
|
|
206
|
+
return h > 0 ? `${h}h${m}m` : `${m}m${s % 60}s`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function startListener() {
|
|
210
|
+
try {
|
|
211
|
+
const api = getApi();
|
|
212
|
+
|
|
213
|
+
api.listener.on("friend_event", async (event) => {
|
|
214
|
+
const label = FRIEND_EVENT_LABELS[event.type] || "UNKNOWN";
|
|
215
|
+
|
|
216
|
+
// Filter
|
|
217
|
+
if (opts.filter === "request" && event.type !== 2) return;
|
|
218
|
+
if (opts.filter === "add" && event.type !== 0) return;
|
|
219
|
+
|
|
220
|
+
const data = {
|
|
221
|
+
event: label,
|
|
222
|
+
type: event.type,
|
|
223
|
+
threadId: event.threadId,
|
|
224
|
+
isSelf: event.isSelf,
|
|
225
|
+
data: event.data,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
if (jsonMode) {
|
|
229
|
+
console.log(JSON.stringify(data));
|
|
230
|
+
} else {
|
|
231
|
+
const msg =
|
|
232
|
+
event.type === 2
|
|
233
|
+
? `Friend request from ${event.data.fromUid}: "${event.data.message || ""}"`
|
|
234
|
+
: `${label} — ${event.threadId}`;
|
|
235
|
+
info(msg);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Webhook
|
|
239
|
+
if (opts.webhook) {
|
|
240
|
+
try {
|
|
241
|
+
await fetch(opts.webhook, {
|
|
242
|
+
method: "POST",
|
|
243
|
+
headers: { "Content-Type": "application/json" },
|
|
244
|
+
body: JSON.stringify(data),
|
|
245
|
+
});
|
|
246
|
+
} catch {
|
|
247
|
+
// Silent
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Auto-accept friend requests
|
|
252
|
+
if (opts.autoAccept && event.type === 2 && !event.isSelf) {
|
|
253
|
+
try {
|
|
254
|
+
await api.acceptFriendRequest(event.data.fromUid);
|
|
255
|
+
success(`Auto-accepted friend request from ${event.data.fromUid}`);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
error(`Auto-accept failed: ${e.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
api.listener.on("closed", async (code, _reason) => {
|
|
263
|
+
if (code === 3000) {
|
|
264
|
+
error("Another Zalo Web session opened. Listener stopped.");
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
warning(`Connection closed (code: ${code}). Re-login in 5s...`);
|
|
268
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
269
|
+
try {
|
|
270
|
+
clearSession();
|
|
271
|
+
await autoLogin(jsonMode);
|
|
272
|
+
startListener();
|
|
273
|
+
} catch (e) {
|
|
274
|
+
error(`Re-login failed: ${e.message}. Retrying in 30s...`);
|
|
275
|
+
await new Promise((r) => setTimeout(r, 30000));
|
|
276
|
+
startListener();
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
api.listener.start({ retryOnClose: true });
|
|
281
|
+
|
|
282
|
+
info("Listening for friend events... Press Ctrl+C to stop.");
|
|
283
|
+
info("Auto-reconnect enabled.");
|
|
284
|
+
if (opts.filter !== "all") info(`Filter: ${opts.filter} events only`);
|
|
285
|
+
if (opts.webhook) info(`Webhook: POST to ${opts.webhook}`);
|
|
286
|
+
if (opts.autoAccept) info("Auto-accept: ON — will accept all incoming requests");
|
|
287
|
+
} catch (e) {
|
|
288
|
+
error(`Listen failed: ${e.message}`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await startListener();
|
|
294
|
+
|
|
295
|
+
await new Promise((resolve) => {
|
|
296
|
+
process.on("SIGINT", () => {
|
|
297
|
+
try {
|
|
298
|
+
getApi().listener.stop();
|
|
299
|
+
} catch {}
|
|
300
|
+
info(`Listener stopped. Uptime: ${uptime()}`);
|
|
301
|
+
resolve();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
169
305
|
}
|
package/src/commands/msg.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { resolve } from "path";
|
|
7
|
-
import { getApi } from "../core/zalo-client.js";
|
|
8
|
-
import { success, error, info, output } from "../utils/output.js";
|
|
7
|
+
import { getApi, autoLogin, clearSession } from "../core/zalo-client.js";
|
|
8
|
+
import { success, error, info, warning, output } from "../utils/output.js";
|
|
9
9
|
|
|
10
10
|
export function registerMsgCommands(program) {
|
|
11
11
|
const msg = program.command("msg").description("Send and manage messages");
|
|
@@ -238,19 +238,23 @@ export function registerMsgCommands(program) {
|
|
|
238
238
|
.option("-w, --webhook <url>", "POST each message as JSON to this URL (for n8n, Make, etc.)")
|
|
239
239
|
.option("--no-self", "Exclude messages sent by this account")
|
|
240
240
|
.action(async (opts) => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
241
|
+
const jsonMode = program.opts().json;
|
|
242
|
+
const startTime = Date.now();
|
|
243
|
+
let reconnectCount = 0;
|
|
244
|
+
|
|
245
|
+
/** Format uptime as human-readable string */
|
|
246
|
+
function uptime() {
|
|
247
|
+
const s = Math.floor((Date.now() - startTime) / 1000);
|
|
248
|
+
const h = Math.floor(s / 3600);
|
|
249
|
+
const m = Math.floor((s % 3600) / 60);
|
|
250
|
+
return h > 0 ? `${h}h${m}m` : `${m}m${s % 60}s`;
|
|
251
|
+
}
|
|
248
252
|
|
|
253
|
+
/** Attach message handler to current API listener */
|
|
254
|
+
function attachMessageHandler(api) {
|
|
249
255
|
api.listener.on("message", async (msg) => {
|
|
250
|
-
// Filter by type: 0=User, 1=Group
|
|
251
256
|
if (opts.filter === "user" && msg.type !== 0) return;
|
|
252
257
|
if (opts.filter === "group" && msg.type !== 1) return;
|
|
253
|
-
// Filter self messages
|
|
254
258
|
if (!opts.self && msg.isSelf) return;
|
|
255
259
|
|
|
256
260
|
const content = typeof msg.data.content === "string" ? msg.data.content : "[non-text]";
|
|
@@ -263,7 +267,6 @@ export function registerMsgCommands(program) {
|
|
|
263
267
|
content,
|
|
264
268
|
};
|
|
265
269
|
|
|
266
|
-
// Output to stdout
|
|
267
270
|
if (jsonMode) {
|
|
268
271
|
console.log(JSON.stringify(data));
|
|
269
272
|
} else {
|
|
@@ -272,7 +275,6 @@ export function registerMsgCommands(program) {
|
|
|
272
275
|
console.log(` ${dir} [${typeLabel}] [${msg.threadId}] ${content} (msgId: ${msg.data.msgId})`);
|
|
273
276
|
}
|
|
274
277
|
|
|
275
|
-
// POST to webhook if configured
|
|
276
278
|
if (opts.webhook) {
|
|
277
279
|
try {
|
|
278
280
|
await fetch(opts.webhook, {
|
|
@@ -281,24 +283,84 @@ export function registerMsgCommands(program) {
|
|
|
281
283
|
body: JSON.stringify(data),
|
|
282
284
|
});
|
|
283
285
|
} catch {
|
|
284
|
-
// Silent webhook failure
|
|
286
|
+
// Silent webhook failure
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
289
|
});
|
|
290
|
+
}
|
|
288
291
|
|
|
289
|
-
|
|
292
|
+
/** Start listener with auto-reconnect and re-login */
|
|
293
|
+
async function startListener() {
|
|
294
|
+
try {
|
|
295
|
+
const api = getApi();
|
|
290
296
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
info("Listener stopped.");
|
|
296
|
-
resolve();
|
|
297
|
+
api.listener.on("connected", () => {
|
|
298
|
+
if (reconnectCount > 0) {
|
|
299
|
+
info(`Reconnected (attempt #${reconnectCount}, uptime: ${uptime()})`);
|
|
300
|
+
}
|
|
297
301
|
});
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
302
|
+
|
|
303
|
+
api.listener.on("disconnected", (code, _reason) => {
|
|
304
|
+
warning(`Disconnected (code: ${code}). Auto-retrying...`);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// "closed" = permanent close (retries exhausted or duplicate connection)
|
|
308
|
+
api.listener.on("closed", async (code, _reason) => {
|
|
309
|
+
if (code === 3000) {
|
|
310
|
+
// Duplicate connection — someone opened Zalo Web
|
|
311
|
+
error("Another Zalo Web session opened. Listener stopped.");
|
|
312
|
+
error("Close the other session and restart listener.");
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
reconnectCount++;
|
|
317
|
+
warning(`Connection closed (code: ${code}). Re-login in 5s... (uptime: ${uptime()})`);
|
|
318
|
+
|
|
319
|
+
// Re-login and restart
|
|
320
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
321
|
+
try {
|
|
322
|
+
clearSession();
|
|
323
|
+
await autoLogin(jsonMode);
|
|
324
|
+
info(`Re-login successful. Restarting listener...`);
|
|
325
|
+
attachMessageHandler(getApi());
|
|
326
|
+
getApi().listener.start({ retryOnClose: true });
|
|
327
|
+
} catch (e) {
|
|
328
|
+
error(`Re-login failed: ${e.message}. Retrying in 30s...`);
|
|
329
|
+
await new Promise((r) => setTimeout(r, 30000));
|
|
330
|
+
startListener();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
api.listener.on("error", (_err) => {
|
|
335
|
+
// Log but don't crash — WS errors are usually followed by close/disconnect
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
attachMessageHandler(api);
|
|
339
|
+
// retryOnClose: true = zca-js auto-retries on temporary disconnects
|
|
340
|
+
api.listener.start({ retryOnClose: true });
|
|
341
|
+
|
|
342
|
+
info("Listening for messages... Press Ctrl+C to stop.");
|
|
343
|
+
info("Auto-reconnect enabled. Will survive network drops.");
|
|
344
|
+
if (opts.filter !== "all") info(`Filter: ${opts.filter} messages only`);
|
|
345
|
+
if (opts.webhook) info(`Webhook: POST to ${opts.webhook}`);
|
|
346
|
+
} catch (e) {
|
|
347
|
+
error(`Listen failed: ${e.message}`);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
301
350
|
}
|
|
351
|
+
|
|
352
|
+
await startListener();
|
|
353
|
+
|
|
354
|
+
// Keep alive until Ctrl+C
|
|
355
|
+
await new Promise((resolve) => {
|
|
356
|
+
process.on("SIGINT", () => {
|
|
357
|
+
try {
|
|
358
|
+
getApi().listener.stop();
|
|
359
|
+
} catch {}
|
|
360
|
+
info(`Listener stopped. Uptime: ${uptime()}, reconnects: ${reconnectCount}`);
|
|
361
|
+
resolve();
|
|
362
|
+
});
|
|
363
|
+
});
|
|
302
364
|
});
|
|
303
365
|
|
|
304
366
|
msg.command("delete <msgId> <threadId>")
|