zalo-agent-cli 1.0.10 → 1.0.11
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/listen.js +243 -0
- package/src/index.js +2 -0
package/package.json
CHANGED
|
@@ -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
|
+
}
|
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();
|