zalo-agent-cli 1.0.10 → 1.0.12

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zalo-agent-cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "CLI tool for Zalo automation — multi-account, proxy support, bank transfers, QR payments",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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, autoLogin, clearSession } from "../core/zalo-client.js";
6
- import { success, error, info, warning, output } from "../utils/output.js";
5
+ import { getApi } from "../core/zalo-client.js";
6
+ import { success, error, info, 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,140 +166,4 @@ 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
- });
305
169
  }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Unified listener — combines message, friend, and group events in one WebSocket connection.
3
+ * Production-ready with auto-reconnect and re-login.
4
+ */
5
+
6
+ import { getApi, autoLogin, clearSession } from "../core/zalo-client.js";
7
+ import { success, error, info, warning } from "../utils/output.js";
8
+
9
+ /** Friend event type enum → readable label */
10
+ const FRIEND_EVENT_LABELS = {
11
+ 0: "friend_added",
12
+ 1: "friend_removed",
13
+ 2: "friend_request",
14
+ 3: "undo_request",
15
+ 4: "reject_request",
16
+ 5: "seen_request",
17
+ 6: "blocked",
18
+ 7: "unblocked",
19
+ };
20
+
21
+ export function registerListenCommand(program) {
22
+ program
23
+ .command("listen")
24
+ .description(
25
+ "Listen for all Zalo events (messages, friend requests, group events) via one WebSocket. Auto-reconnect enabled.",
26
+ )
27
+ .option(
28
+ "-e, --events <types>",
29
+ "Comma-separated event types: message,friend,group,reaction (default: message,friend)",
30
+ "message,friend",
31
+ )
32
+ .option("-f, --filter <type>", "Message filter: user (DM only), group (groups only), all", "all")
33
+ .option("-w, --webhook <url>", "POST each event as JSON to this URL (for n8n, Make, etc.)")
34
+ .option("--no-self", "Exclude self-sent messages")
35
+ .option("--auto-accept", "Auto-accept incoming friend requests")
36
+ .action(async (opts) => {
37
+ const jsonMode = program.opts().json;
38
+ const startTime = Date.now();
39
+ let reconnectCount = 0;
40
+ let eventCount = 0;
41
+ const enabledEvents = new Set(opts.events.split(",").map((e) => e.trim()));
42
+
43
+ function uptime() {
44
+ const s = Math.floor((Date.now() - startTime) / 1000);
45
+ const h = Math.floor(s / 3600);
46
+ const m = Math.floor((s % 3600) / 60);
47
+ return h > 0 ? `${h}h${m}m` : `${m}m${s % 60}s`;
48
+ }
49
+
50
+ /** Send event data to webhook */
51
+ async function postWebhook(data) {
52
+ if (!opts.webhook) return;
53
+ try {
54
+ await fetch(opts.webhook, {
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/json" },
57
+ body: JSON.stringify(data),
58
+ });
59
+ } catch {
60
+ // Silent — don't block listener
61
+ }
62
+ }
63
+
64
+ /** Attach all event handlers to current API */
65
+ function attachHandlers(api) {
66
+ // --- Message events ---
67
+ if (enabledEvents.has("message")) {
68
+ api.listener.on("message", async (msg) => {
69
+ if (opts.filter === "user" && msg.type !== 0) return;
70
+ if (opts.filter === "group" && msg.type !== 1) return;
71
+ if (!opts.self && msg.isSelf) return;
72
+
73
+ eventCount++;
74
+ const content = typeof msg.data.content === "string" ? msg.data.content : "[non-text]";
75
+ const data = {
76
+ event: "message",
77
+ msgId: msg.data.msgId,
78
+ cliMsgId: msg.data.cliMsgId,
79
+ threadId: msg.threadId,
80
+ type: msg.type,
81
+ isSelf: msg.isSelf,
82
+ content,
83
+ };
84
+
85
+ if (jsonMode) {
86
+ console.log(JSON.stringify(data));
87
+ } else {
88
+ const dir = msg.isSelf ? "→" : "←";
89
+ const typeLabel = msg.type === 0 ? "DM" : "GR";
90
+ console.log(
91
+ ` ${dir} [${typeLabel}] [${msg.threadId}] ${content} (msgId: ${msg.data.msgId})`,
92
+ );
93
+ }
94
+ await postWebhook(data);
95
+ });
96
+ }
97
+
98
+ // --- Friend events ---
99
+ if (enabledEvents.has("friend")) {
100
+ api.listener.on("friend_event", async (event) => {
101
+ eventCount++;
102
+ const label = FRIEND_EVENT_LABELS[event.type] || "friend_unknown";
103
+ const data = {
104
+ event: label,
105
+ threadId: event.threadId,
106
+ isSelf: event.isSelf,
107
+ data: event.data,
108
+ };
109
+
110
+ if (jsonMode) {
111
+ console.log(JSON.stringify(data));
112
+ } else {
113
+ const msg =
114
+ event.type === 2
115
+ ? `Friend request from ${event.data.fromUid}: "${event.data.message || ""}"`
116
+ : `${label} — ${event.threadId}`;
117
+ info(msg);
118
+ }
119
+ await postWebhook(data);
120
+
121
+ // Auto-accept
122
+ if (opts.autoAccept && event.type === 2 && !event.isSelf) {
123
+ try {
124
+ await api.acceptFriendRequest(event.data.fromUid);
125
+ success(`Auto-accepted friend request from ${event.data.fromUid}`);
126
+ } catch (e) {
127
+ error(`Auto-accept failed: ${e.message}`);
128
+ }
129
+ }
130
+ });
131
+ }
132
+
133
+ // --- Group events ---
134
+ if (enabledEvents.has("group")) {
135
+ api.listener.on("group_event", async (event) => {
136
+ eventCount++;
137
+ const data = {
138
+ event: `group_${event.type}`,
139
+ threadId: event.threadId,
140
+ isSelf: event.isSelf,
141
+ data: event.data,
142
+ };
143
+
144
+ if (jsonMode) {
145
+ console.log(JSON.stringify(data));
146
+ } else {
147
+ info(`Group: ${event.type} — ${event.threadId}`);
148
+ }
149
+ await postWebhook(data);
150
+ });
151
+ }
152
+
153
+ // --- Reaction events ---
154
+ if (enabledEvents.has("reaction")) {
155
+ api.listener.on("reaction", async (reaction) => {
156
+ if (!opts.self && reaction.isSelf) return;
157
+ eventCount++;
158
+ const data = {
159
+ event: "reaction",
160
+ threadId: reaction.threadId,
161
+ isSelf: reaction.isSelf,
162
+ isGroup: reaction.isGroup,
163
+ data: reaction.data,
164
+ };
165
+
166
+ if (jsonMode) {
167
+ console.log(JSON.stringify(data));
168
+ } else {
169
+ info(`Reaction in ${reaction.threadId}`);
170
+ }
171
+ await postWebhook(data);
172
+ });
173
+ }
174
+ }
175
+
176
+ /** Start listener with auto-reconnect */
177
+ async function startListener() {
178
+ try {
179
+ const api = getApi();
180
+
181
+ api.listener.on("connected", () => {
182
+ if (reconnectCount > 0) {
183
+ info(`Reconnected (#${reconnectCount}, uptime: ${uptime()}, events: ${eventCount})`);
184
+ }
185
+ });
186
+
187
+ api.listener.on("disconnected", (code, _reason) => {
188
+ warning(`Disconnected (code: ${code}). Auto-retrying...`);
189
+ });
190
+
191
+ api.listener.on("closed", async (code, _reason) => {
192
+ if (code === 3000) {
193
+ error("Another Zalo Web session opened. Listener stopped.");
194
+ process.exit(1);
195
+ }
196
+ reconnectCount++;
197
+ warning(`Connection closed (code: ${code}). Re-login in 5s... (uptime: ${uptime()})`);
198
+ await new Promise((r) => setTimeout(r, 5000));
199
+ try {
200
+ clearSession();
201
+ await autoLogin(jsonMode);
202
+ info("Re-login successful. Restarting listener...");
203
+ attachHandlers(getApi());
204
+ getApi().listener.start({ retryOnClose: true });
205
+ } catch (e) {
206
+ error(`Re-login failed: ${e.message}. Retrying in 30s...`);
207
+ await new Promise((r) => setTimeout(r, 30000));
208
+ startListener();
209
+ }
210
+ });
211
+
212
+ api.listener.on("error", (_err) => {
213
+ // Log but don't crash
214
+ });
215
+
216
+ attachHandlers(api);
217
+ api.listener.start({ retryOnClose: true });
218
+
219
+ info("Listening for Zalo events... Press Ctrl+C to stop.");
220
+ info(`Events: ${opts.events}`);
221
+ info("Auto-reconnect enabled.");
222
+ if (opts.filter !== "all") info(`Message filter: ${opts.filter}`);
223
+ if (opts.webhook) info(`Webhook: ${opts.webhook}`);
224
+ if (opts.autoAccept) info("Auto-accept friend requests: ON");
225
+ } catch (e) {
226
+ error(`Listen failed: ${e.message}`);
227
+ process.exit(1);
228
+ }
229
+ }
230
+
231
+ await startListener();
232
+
233
+ await new Promise((resolve) => {
234
+ process.on("SIGINT", () => {
235
+ try {
236
+ getApi().listener.stop();
237
+ } catch {}
238
+ info(`Stopped. Uptime: ${uptime()}, events: ${eventCount}, reconnects: ${reconnectCount}`);
239
+ resolve();
240
+ });
241
+ });
242
+ });
243
+ }
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  import { resolve } from "path";
7
- import { getApi, autoLogin, clearSession } from "../core/zalo-client.js";
8
- import { success, error, info, warning, output } from "../utils/output.js";
7
+ import { getApi } from "../core/zalo-client.js";
8
+ import { success, error, info, 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");
@@ -230,139 +230,6 @@ export function registerMsgCommands(program) {
230
230
  }
231
231
  });
232
232
 
233
- msg.command("listen")
234
- .description(
235
- "Listen for incoming messages in real-time via WebSocket. Outputs msgId + cliMsgId needed for react. Use --json for machine parsing.",
236
- )
237
- .option("-f, --filter <type>", "Filter messages: user (DM only), group (groups only), all (default)", "all")
238
- .option("-w, --webhook <url>", "POST each message as JSON to this URL (for n8n, Make, etc.)")
239
- .option("--no-self", "Exclude messages sent by this account")
240
- .action(async (opts) => {
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
- }
252
-
253
- /** Attach message handler to current API listener */
254
- function attachMessageHandler(api) {
255
- api.listener.on("message", async (msg) => {
256
- if (opts.filter === "user" && msg.type !== 0) return;
257
- if (opts.filter === "group" && msg.type !== 1) return;
258
- if (!opts.self && msg.isSelf) return;
259
-
260
- const content = typeof msg.data.content === "string" ? msg.data.content : "[non-text]";
261
- const data = {
262
- msgId: msg.data.msgId,
263
- cliMsgId: msg.data.cliMsgId,
264
- threadId: msg.threadId,
265
- type: msg.type,
266
- isSelf: msg.isSelf,
267
- content,
268
- };
269
-
270
- if (jsonMode) {
271
- console.log(JSON.stringify(data));
272
- } else {
273
- const dir = msg.isSelf ? "→" : "←";
274
- const typeLabel = msg.type === 0 ? "DM" : "GR";
275
- console.log(` ${dir} [${typeLabel}] [${msg.threadId}] ${content} (msgId: ${msg.data.msgId})`);
276
- }
277
-
278
- if (opts.webhook) {
279
- try {
280
- await fetch(opts.webhook, {
281
- method: "POST",
282
- headers: { "Content-Type": "application/json" },
283
- body: JSON.stringify(data),
284
- });
285
- } catch {
286
- // Silent webhook failure
287
- }
288
- }
289
- });
290
- }
291
-
292
- /** Start listener with auto-reconnect and re-login */
293
- async function startListener() {
294
- try {
295
- const api = getApi();
296
-
297
- api.listener.on("connected", () => {
298
- if (reconnectCount > 0) {
299
- info(`Reconnected (attempt #${reconnectCount}, uptime: ${uptime()})`);
300
- }
301
- });
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
- }
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
- });
364
- });
365
-
366
233
  msg.command("delete <msgId> <threadId>")
367
234
  .description("Delete a message you sent")
368
235
  .option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
package/src/index.js CHANGED
@@ -18,6 +18,7 @@ import { registerFriendCommands } from "./commands/friend.js";
18
18
  import { registerGroupCommands } from "./commands/group.js";
19
19
  import { registerConvCommands } from "./commands/conv.js";
20
20
  import { registerAccountCommands } from "./commands/account.js";
21
+ import { registerListenCommand } from "./commands/listen.js";
21
22
  import { autoLogin } from "./core/zalo-client.js";
22
23
  import { checkForUpdates, selfUpdate } from "./utils/update-check.js";
23
24
  import { success, error } from "./utils/output.js";
@@ -63,5 +64,6 @@ registerFriendCommands(program);
63
64
  registerGroupCommands(program);
64
65
  registerConvCommands(program);
65
66
  registerAccountCommands(program);
67
+ registerListenCommand(program);
66
68
 
67
69
  program.parse();