@openclaw/zalouser 2026.5.2 → 2026.5.3-beta.2
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/dist/accounts-C00IMUgd.js +63 -0
- package/dist/accounts.runtime-uG7S8cXT.js +2 -0
- package/dist/api-BRwdUWuS.js +139 -0
- package/dist/api.js +7 -0
- package/dist/channel-ou_w_2j-.js +484 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-C9WxiAiR.js +25 -0
- package/dist/channel.setup-CiDeBFrn.js +10 -0
- package/dist/contract-api.js +3 -0
- package/dist/doctor-contract-DgqHp8E2.js +128 -0
- package/dist/doctor-contract-api.js +2 -0
- package/dist/index.js +27 -0
- package/dist/monitor-Cg7K_s_s.js +705 -0
- package/dist/runtime-QNU7vLgI.js +106 -0
- package/dist/runtime-api.js +22 -0
- package/dist/secret-contract-api.js +5 -0
- package/dist/security-audit-BZLhil-V.js +34 -0
- package/dist/send-BsmySxe3.js +534 -0
- package/dist/session-route-C0-Xr8bt.js +92 -0
- package/dist/setup-core-CqipqY98.js +40 -0
- package/dist/setup-entry.js +11 -0
- package/dist/setup-plugin-api.js +2 -0
- package/dist/setup-surface-NCOuKu-l.js +359 -0
- package/dist/shared-DSy8aIUx.js +120 -0
- package/dist/test-api.js +5 -0
- package/dist/zalo-js-CHCUlY3c.js +1279 -0
- package/package.json +15 -6
- package/api.ts +0 -9
- package/channel-plugin-api.ts +0 -3
- package/contract-api.ts +0 -2
- package/doctor-contract-api.ts +0 -1
- package/index.ts +0 -34
- package/runtime-api.ts +0 -67
- package/secret-contract-api.ts +0 -4
- package/setup-entry.ts +0 -9
- package/setup-plugin-api.ts +0 -2
- package/src/accounts.runtime.ts +0 -1
- package/src/accounts.test-mocks.ts +0 -14
- package/src/accounts.test.ts +0 -266
- package/src/accounts.ts +0 -131
- package/src/channel-api.ts +0 -20
- package/src/channel.adapters.ts +0 -391
- package/src/channel.directory.test.ts +0 -59
- package/src/channel.runtime.ts +0 -12
- package/src/channel.sendpayload.test.ts +0 -172
- package/src/channel.setup.test.ts +0 -33
- package/src/channel.setup.ts +0 -12
- package/src/channel.test.ts +0 -377
- package/src/channel.ts +0 -219
- package/src/config-schema.ts +0 -33
- package/src/directory.ts +0 -54
- package/src/doctor-contract.ts +0 -156
- package/src/doctor.test.ts +0 -77
- package/src/doctor.ts +0 -37
- package/src/group-policy.test.ts +0 -61
- package/src/group-policy.ts +0 -83
- package/src/message-sid.test.ts +0 -66
- package/src/message-sid.ts +0 -80
- package/src/monitor.account-scope.test.ts +0 -107
- package/src/monitor.group-gating.test.ts +0 -816
- package/src/monitor.send-mocks.ts +0 -20
- package/src/monitor.ts +0 -1044
- package/src/probe.test.ts +0 -60
- package/src/probe.ts +0 -35
- package/src/qr-temp-file.ts +0 -22
- package/src/reaction.test.ts +0 -19
- package/src/reaction.ts +0 -32
- package/src/runtime.ts +0 -9
- package/src/security-audit.test.ts +0 -80
- package/src/security-audit.ts +0 -71
- package/src/send.test.ts +0 -395
- package/src/send.ts +0 -272
- package/src/session-route.ts +0 -121
- package/src/setup-core.ts +0 -33
- package/src/setup-surface.test.ts +0 -363
- package/src/setup-surface.ts +0 -470
- package/src/setup-test-helpers.ts +0 -42
- package/src/shared.ts +0 -92
- package/src/status-issues.test.ts +0 -31
- package/src/status-issues.ts +0 -58
- package/src/test-helpers.ts +0 -26
- package/src/text-styles.test.ts +0 -203
- package/src/text-styles.ts +0 -540
- package/src/tool.test.ts +0 -212
- package/src/tool.ts +0 -210
- package/src/types.ts +0 -125
- package/src/zalo-js.credentials.test.ts +0 -465
- package/src/zalo-js.test-mocks.ts +0 -89
- package/src/zalo-js.ts +0 -1911
- package/src/zca-client.test.ts +0 -24
- package/src/zca-client.ts +0 -259
- package/src/zca-constants.ts +0 -55
- package/src/zca-js-exports.d.ts +0 -22
- package/test-api.ts +0 -21
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,1279 @@
|
|
|
1
|
+
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
|
|
7
|
+
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
8
|
+
//#region extensions/zalouser/src/zca-constants.ts
|
|
9
|
+
const ThreadType = {
|
|
10
|
+
User: 0,
|
|
11
|
+
Group: 1
|
|
12
|
+
};
|
|
13
|
+
const LoginQRCallbackEventType = {
|
|
14
|
+
QRCodeGenerated: 0,
|
|
15
|
+
QRCodeExpired: 1,
|
|
16
|
+
QRCodeScanned: 2,
|
|
17
|
+
QRCodeDeclined: 3,
|
|
18
|
+
GotLoginInfo: 4
|
|
19
|
+
};
|
|
20
|
+
const Reactions = {
|
|
21
|
+
HEART: "/-heart",
|
|
22
|
+
LIKE: "/-strong",
|
|
23
|
+
HAHA: ":>",
|
|
24
|
+
WOW: ":o",
|
|
25
|
+
CRY: ":-((",
|
|
26
|
+
ANGRY: ":-h",
|
|
27
|
+
NONE: ""
|
|
28
|
+
};
|
|
29
|
+
const TextStyle = {
|
|
30
|
+
Bold: "b",
|
|
31
|
+
Italic: "i",
|
|
32
|
+
Underline: "u",
|
|
33
|
+
StrikeThrough: "s",
|
|
34
|
+
Red: "c_db342e",
|
|
35
|
+
Orange: "c_f27806",
|
|
36
|
+
Yellow: "c_f7b503",
|
|
37
|
+
Green: "c_15a85f",
|
|
38
|
+
Small: "f_13",
|
|
39
|
+
Big: "f_18",
|
|
40
|
+
UnorderedList: "lst_1",
|
|
41
|
+
OrderedList: "lst_2",
|
|
42
|
+
Indent: "ind_$"
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region extensions/zalouser/src/reaction.ts
|
|
46
|
+
const REACTION_ALIAS_MAP = new Map([
|
|
47
|
+
["like", Reactions.LIKE],
|
|
48
|
+
["👍", Reactions.LIKE],
|
|
49
|
+
[":+1:", Reactions.LIKE],
|
|
50
|
+
["heart", Reactions.HEART],
|
|
51
|
+
["❤️", Reactions.HEART],
|
|
52
|
+
["<3", Reactions.HEART],
|
|
53
|
+
["haha", Reactions.HAHA],
|
|
54
|
+
["laugh", Reactions.HAHA],
|
|
55
|
+
["😂", Reactions.HAHA],
|
|
56
|
+
["wow", Reactions.WOW],
|
|
57
|
+
["😮", Reactions.WOW],
|
|
58
|
+
["cry", Reactions.CRY],
|
|
59
|
+
["😢", Reactions.CRY],
|
|
60
|
+
["angry", Reactions.ANGRY],
|
|
61
|
+
["😡", Reactions.ANGRY]
|
|
62
|
+
]);
|
|
63
|
+
function normalizeZaloReactionIcon(raw) {
|
|
64
|
+
const trimmed = raw.trim();
|
|
65
|
+
if (!trimmed) return Reactions.LIKE;
|
|
66
|
+
return REACTION_ALIAS_MAP.get(normalizeLowercaseStringOrEmpty(trimmed)) ?? REACTION_ALIAS_MAP.get(trimmed) ?? trimmed;
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region extensions/zalouser/src/zca-client.ts
|
|
70
|
+
let zcaJsRuntimePromise = null;
|
|
71
|
+
async function loadZcaJsRuntime() {
|
|
72
|
+
zcaJsRuntimePromise ??= import("zca-js").then((mod) => mod);
|
|
73
|
+
return await zcaJsRuntimePromise;
|
|
74
|
+
}
|
|
75
|
+
async function createZalo(options) {
|
|
76
|
+
const Zalo = (await loadZcaJsRuntime()).Zalo;
|
|
77
|
+
return new Zalo(options);
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region extensions/zalouser/src/zalo-js.ts
|
|
81
|
+
const API_LOGIN_TIMEOUT_MS = 2e4;
|
|
82
|
+
const QR_LOGIN_TTL_MS = 3 * 6e4;
|
|
83
|
+
const DEFAULT_QR_START_TIMEOUT_MS = 3e4;
|
|
84
|
+
const DEFAULT_QR_WAIT_TIMEOUT_MS = 12e4;
|
|
85
|
+
const GROUP_INFO_CHUNK_SIZE = 80;
|
|
86
|
+
const GROUP_CONTEXT_CACHE_TTL_MS = 5 * 6e4;
|
|
87
|
+
const GROUP_CONTEXT_CACHE_MAX_ENTRIES = 500;
|
|
88
|
+
const LISTENER_WATCHDOG_INTERVAL_MS = 3e4;
|
|
89
|
+
const LISTENER_WATCHDOG_MAX_GAP_MS = 35e3;
|
|
90
|
+
const apiByProfile = /* @__PURE__ */ new Map();
|
|
91
|
+
const apiInitByProfile = /* @__PURE__ */ new Map();
|
|
92
|
+
const credentialSignaturesByProfile = /* @__PURE__ */ new Map();
|
|
93
|
+
const activeQrLogins = /* @__PURE__ */ new Map();
|
|
94
|
+
const activeListeners = /* @__PURE__ */ new Map();
|
|
95
|
+
const groupContextCache = /* @__PURE__ */ new Map();
|
|
96
|
+
function resolveStateDir$1(env = process.env) {
|
|
97
|
+
return resolveStateDir(env, os.homedir);
|
|
98
|
+
}
|
|
99
|
+
function resolveCredentialsDir(env = process.env) {
|
|
100
|
+
return path.join(resolveStateDir$1(env), "credentials", "zalouser");
|
|
101
|
+
}
|
|
102
|
+
function credentialsFilename(profile) {
|
|
103
|
+
const trimmed = normalizeLowercaseStringOrEmpty(profile);
|
|
104
|
+
if (!trimmed || trimmed === "default") return "credentials.json";
|
|
105
|
+
return `credentials-${encodeURIComponent(trimmed)}.json`;
|
|
106
|
+
}
|
|
107
|
+
function resolveCredentialsPath(profile, env = process.env) {
|
|
108
|
+
return path.join(resolveCredentialsDir(env), credentialsFilename(profile));
|
|
109
|
+
}
|
|
110
|
+
function isNodeErrorCode(error, code) {
|
|
111
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
112
|
+
}
|
|
113
|
+
function ensureCredentialsDir() {
|
|
114
|
+
const dir = resolveCredentialsDir();
|
|
115
|
+
fs.mkdirSync(dir, {
|
|
116
|
+
recursive: true,
|
|
117
|
+
mode: 448
|
|
118
|
+
});
|
|
119
|
+
const stat = fs.lstatSync(dir);
|
|
120
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Refusing to use non-directory Zalo credentials path");
|
|
121
|
+
try {
|
|
122
|
+
fs.chmodSync(dir, 448);
|
|
123
|
+
} catch {}
|
|
124
|
+
return dir;
|
|
125
|
+
}
|
|
126
|
+
function isReadableCredentialFile(filePath) {
|
|
127
|
+
try {
|
|
128
|
+
const stat = fs.lstatSync(filePath);
|
|
129
|
+
return stat.isFile() && !stat.isSymbolicLink();
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (isNodeErrorCode(error, "ENOENT")) return false;
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function assertWritableCredentialTarget(filePath) {
|
|
136
|
+
try {
|
|
137
|
+
const stat = fs.lstatSync(filePath);
|
|
138
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Refusing to write Zalo credentials to symlinked path");
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (isNodeErrorCode(error, "ENOENT")) return;
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function writeCredentialFileAtomic(filePath, payload) {
|
|
145
|
+
const dir = ensureCredentialsDir();
|
|
146
|
+
assertWritableCredentialTarget(filePath);
|
|
147
|
+
const tempPath = path.join(dir, `.${path.basename(filePath)}.tmp-${process.pid}-${randomUUID()}`);
|
|
148
|
+
try {
|
|
149
|
+
fs.writeFileSync(tempPath, payload, {
|
|
150
|
+
encoding: "utf-8",
|
|
151
|
+
mode: 384,
|
|
152
|
+
flag: "wx"
|
|
153
|
+
});
|
|
154
|
+
try {
|
|
155
|
+
fs.chmodSync(tempPath, 384);
|
|
156
|
+
} catch {}
|
|
157
|
+
fs.renameSync(tempPath, filePath);
|
|
158
|
+
try {
|
|
159
|
+
fs.chmodSync(filePath, 384);
|
|
160
|
+
} catch {}
|
|
161
|
+
} finally {
|
|
162
|
+
try {
|
|
163
|
+
fs.unlinkSync(tempPath);
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const timer = setTimeout(() => {
|
|
170
|
+
reject(new Error(label));
|
|
171
|
+
}, timeoutMs);
|
|
172
|
+
promise.then((result) => {
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
resolve(result);
|
|
175
|
+
}).catch((err) => {
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
reject(err);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function delay(ms) {
|
|
182
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
183
|
+
}
|
|
184
|
+
function normalizeProfile(profile) {
|
|
185
|
+
const trimmed = profile?.trim();
|
|
186
|
+
return trimmed && trimmed.length > 0 ? trimmed : "default";
|
|
187
|
+
}
|
|
188
|
+
function toErrorMessage(error) {
|
|
189
|
+
if (error instanceof Error) return error.message;
|
|
190
|
+
return String(error);
|
|
191
|
+
}
|
|
192
|
+
function clampTextStyles(text, styles) {
|
|
193
|
+
if (!styles || styles.length === 0) return;
|
|
194
|
+
const maxLength = text.length;
|
|
195
|
+
const clamped = styles.map((style) => {
|
|
196
|
+
const start = Math.max(0, Math.min(style.start, maxLength));
|
|
197
|
+
const end = Math.min(style.start + style.len, maxLength);
|
|
198
|
+
if (end <= start) return null;
|
|
199
|
+
if (style.st === TextStyle.Indent) return {
|
|
200
|
+
start,
|
|
201
|
+
len: end - start,
|
|
202
|
+
st: style.st,
|
|
203
|
+
indentSize: style.indentSize
|
|
204
|
+
};
|
|
205
|
+
return {
|
|
206
|
+
start,
|
|
207
|
+
len: end - start,
|
|
208
|
+
st: style.st
|
|
209
|
+
};
|
|
210
|
+
}).filter((style) => style !== null);
|
|
211
|
+
return clamped.length > 0 ? clamped : void 0;
|
|
212
|
+
}
|
|
213
|
+
function toNumberId(value) {
|
|
214
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(Math.trunc(value));
|
|
215
|
+
if (typeof value === "string") {
|
|
216
|
+
const trimmed = value.trim();
|
|
217
|
+
if (trimmed.length > 0) return trimmed.replace(/_\d+$/, "");
|
|
218
|
+
}
|
|
219
|
+
return "";
|
|
220
|
+
}
|
|
221
|
+
function toStringValue(value) {
|
|
222
|
+
if (typeof value === "string") return value.trim();
|
|
223
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(Math.trunc(value));
|
|
224
|
+
return "";
|
|
225
|
+
}
|
|
226
|
+
function normalizeAccountInfoUser(info) {
|
|
227
|
+
if (!info || typeof info !== "object") return null;
|
|
228
|
+
if ("profile" in info) {
|
|
229
|
+
const profile = info.profile;
|
|
230
|
+
if (profile && typeof profile === "object") return profile;
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
return info;
|
|
234
|
+
}
|
|
235
|
+
function toInteger(value, fallback = 0) {
|
|
236
|
+
if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
|
|
237
|
+
const parsed = Number.parseInt(typeof value === "string" ? value : typeof value === "number" ? String(value) : "", 10);
|
|
238
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
239
|
+
return Math.trunc(parsed);
|
|
240
|
+
}
|
|
241
|
+
function normalizeMessageContent(content) {
|
|
242
|
+
if (typeof content === "string") return content;
|
|
243
|
+
if (!content || typeof content !== "object") return "";
|
|
244
|
+
const record = content;
|
|
245
|
+
const combined = [
|
|
246
|
+
typeof record.title === "string" ? record.title.trim() : "",
|
|
247
|
+
typeof record.description === "string" ? record.description.trim() : "",
|
|
248
|
+
typeof record.href === "string" ? record.href.trim() : ""
|
|
249
|
+
].filter(Boolean).join("\n").trim();
|
|
250
|
+
if (combined) return combined;
|
|
251
|
+
try {
|
|
252
|
+
return JSON.stringify(content);
|
|
253
|
+
} catch {
|
|
254
|
+
return "";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function resolveInboundTimestamp(rawTs) {
|
|
258
|
+
if (typeof rawTs === "number" && Number.isFinite(rawTs)) return rawTs > 0xe8d4a51000 ? rawTs : rawTs * 1e3;
|
|
259
|
+
const parsed = Number.parseInt(typeof rawTs === "string" ? rawTs : typeof rawTs === "number" ? String(rawTs) : "", 10);
|
|
260
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return Date.now();
|
|
261
|
+
return parsed > 0xe8d4a51000 ? parsed : parsed * 1e3;
|
|
262
|
+
}
|
|
263
|
+
function extractMentionIds(rawMentions) {
|
|
264
|
+
if (!Array.isArray(rawMentions)) return [];
|
|
265
|
+
const sink = /* @__PURE__ */ new Set();
|
|
266
|
+
for (const entry of rawMentions) {
|
|
267
|
+
if (!entry || typeof entry !== "object") continue;
|
|
268
|
+
const id = toNumberId(entry.uid);
|
|
269
|
+
if (id) sink.add(id);
|
|
270
|
+
}
|
|
271
|
+
return Array.from(sink);
|
|
272
|
+
}
|
|
273
|
+
function toNonNegativeInteger(value) {
|
|
274
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
275
|
+
const normalized = Math.trunc(value);
|
|
276
|
+
return normalized >= 0 ? normalized : null;
|
|
277
|
+
}
|
|
278
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
279
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
280
|
+
if (Number.isFinite(parsed)) return parsed >= 0 ? parsed : null;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
function extractOwnMentionSpans(rawMentions, ownUserId, contentLength) {
|
|
285
|
+
if (!Array.isArray(rawMentions) || !ownUserId || contentLength <= 0) return [];
|
|
286
|
+
const spans = [];
|
|
287
|
+
for (const entry of rawMentions) {
|
|
288
|
+
if (!entry || typeof entry !== "object") continue;
|
|
289
|
+
const record = entry;
|
|
290
|
+
const uid = toNumberId(record.uid);
|
|
291
|
+
if (!uid || uid !== ownUserId) continue;
|
|
292
|
+
const startRaw = toNonNegativeInteger(record.pos ?? record.start ?? record.offset);
|
|
293
|
+
const lengthRaw = toNonNegativeInteger(record.len ?? record.length);
|
|
294
|
+
if (startRaw === null || lengthRaw === null || lengthRaw <= 0) continue;
|
|
295
|
+
const start = Math.min(startRaw, contentLength);
|
|
296
|
+
const end = Math.min(start + lengthRaw, contentLength);
|
|
297
|
+
if (end <= start) continue;
|
|
298
|
+
spans.push({
|
|
299
|
+
start,
|
|
300
|
+
end
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (spans.length <= 1) return spans;
|
|
304
|
+
spans.sort((a, b) => a.start - b.start);
|
|
305
|
+
const merged = [];
|
|
306
|
+
for (const span of spans) {
|
|
307
|
+
const last = merged[merged.length - 1];
|
|
308
|
+
if (!last || span.start > last.end) {
|
|
309
|
+
merged.push({ ...span });
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
last.end = Math.max(last.end, span.end);
|
|
313
|
+
}
|
|
314
|
+
return merged;
|
|
315
|
+
}
|
|
316
|
+
function stripOwnMentionsForCommandBody(content, rawMentions, ownUserId) {
|
|
317
|
+
if (!content || !ownUserId) return content;
|
|
318
|
+
const spans = extractOwnMentionSpans(rawMentions, ownUserId, content.length);
|
|
319
|
+
if (spans.length === 0) return stripLeadingAtMentionForCommand(content);
|
|
320
|
+
let cursor = 0;
|
|
321
|
+
let output = "";
|
|
322
|
+
for (const span of spans) {
|
|
323
|
+
if (span.start > cursor) output += content.slice(cursor, span.start);
|
|
324
|
+
cursor = Math.max(cursor, span.end);
|
|
325
|
+
}
|
|
326
|
+
if (cursor < content.length) output += content.slice(cursor);
|
|
327
|
+
return output.replace(/\s+/g, " ").trim();
|
|
328
|
+
}
|
|
329
|
+
function stripLeadingAtMentionForCommand(content) {
|
|
330
|
+
const fallbackMatch = content.match(/^\s*@[^\s]+(?:\s+|[:,-]\s*)([/!][\s\S]*)$/);
|
|
331
|
+
if (!fallbackMatch) return content;
|
|
332
|
+
return fallbackMatch[1].trim();
|
|
333
|
+
}
|
|
334
|
+
function resolveGroupNameFromMessageData(data) {
|
|
335
|
+
const candidates = [
|
|
336
|
+
data.groupName,
|
|
337
|
+
data.gName,
|
|
338
|
+
data.idToName,
|
|
339
|
+
data.threadName,
|
|
340
|
+
data.roomName
|
|
341
|
+
];
|
|
342
|
+
for (const candidate of candidates) {
|
|
343
|
+
const value = toStringValue(candidate);
|
|
344
|
+
if (value) return value;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function buildEventMessage(data) {
|
|
348
|
+
const msgId = toStringValue(data.msgId);
|
|
349
|
+
const cliMsgId = toStringValue(data.cliMsgId);
|
|
350
|
+
const uidFrom = toStringValue(data.uidFrom);
|
|
351
|
+
const idTo = toStringValue(data.idTo);
|
|
352
|
+
if (!msgId || !cliMsgId || !uidFrom || !idTo) return;
|
|
353
|
+
return {
|
|
354
|
+
msgId,
|
|
355
|
+
cliMsgId,
|
|
356
|
+
uidFrom,
|
|
357
|
+
idTo,
|
|
358
|
+
msgType: toStringValue(data.msgType) || "webchat",
|
|
359
|
+
st: toInteger(data.st, 0),
|
|
360
|
+
at: toInteger(data.at, 0),
|
|
361
|
+
cmd: toInteger(data.cmd, 0),
|
|
362
|
+
ts: toStringValue(data.ts) || Date.now()
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function extractSendMessageId(result) {
|
|
366
|
+
if (!result || typeof result !== "object") return;
|
|
367
|
+
const payload = result;
|
|
368
|
+
const direct = payload.msgId;
|
|
369
|
+
if (direct !== void 0 && direct !== null) return String(direct);
|
|
370
|
+
const primary = payload.message?.msgId;
|
|
371
|
+
if (primary !== void 0 && primary !== null) return String(primary);
|
|
372
|
+
const attachmentId = payload.attachment?.[0]?.msgId;
|
|
373
|
+
if (attachmentId !== void 0 && attachmentId !== null) return String(attachmentId);
|
|
374
|
+
}
|
|
375
|
+
function resolveMediaFileName(params) {
|
|
376
|
+
const explicit = params.fileName?.trim();
|
|
377
|
+
if (explicit) return explicit;
|
|
378
|
+
try {
|
|
379
|
+
const parsed = new URL(params.mediaUrl);
|
|
380
|
+
const fromPath = path.basename(parsed.pathname).trim();
|
|
381
|
+
if (fromPath) return fromPath;
|
|
382
|
+
} catch {}
|
|
383
|
+
return `upload.${params.contentType === "image/png" ? "png" : params.contentType === "image/webp" ? "webp" : params.contentType === "image/jpeg" ? "jpg" : params.contentType === "video/mp4" ? "mp4" : params.contentType === "audio/mpeg" ? "mp3" : params.contentType === "audio/ogg" ? "ogg" : params.contentType === "audio/wav" ? "wav" : params.kind === "video" ? "mp4" : params.kind === "audio" ? "mp3" : params.kind === "image" ? "jpg" : "bin"}`;
|
|
384
|
+
}
|
|
385
|
+
function resolveUploadedVoiceAsset(uploaded) {
|
|
386
|
+
for (const item of uploaded) {
|
|
387
|
+
if (!item || typeof item !== "object") continue;
|
|
388
|
+
const fileType = normalizeOptionalLowercaseString(item.fileType);
|
|
389
|
+
const fileUrl = item.fileUrl?.trim();
|
|
390
|
+
if (!fileUrl) continue;
|
|
391
|
+
if (fileType === "others" || fileType === "video") return {
|
|
392
|
+
fileUrl,
|
|
393
|
+
fileName: normalizeOptionalString(item.fileName)
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function buildZaloVoicePlaybackUrl(asset) {
|
|
398
|
+
return asset.fileUrl.trim();
|
|
399
|
+
}
|
|
400
|
+
function mapFriend(friend) {
|
|
401
|
+
return {
|
|
402
|
+
userId: friend.userId,
|
|
403
|
+
displayName: friend.displayName || friend.zaloName || friend.username || friend.userId,
|
|
404
|
+
avatar: friend.avatar || void 0
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function mapGroup(groupId, group) {
|
|
408
|
+
const totalMember = typeof group.totalMember === "number" && Number.isFinite(group.totalMember) ? group.totalMember : void 0;
|
|
409
|
+
return {
|
|
410
|
+
groupId,
|
|
411
|
+
name: group.name?.trim() || groupId,
|
|
412
|
+
memberCount: totalMember
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function readCredentials(profile) {
|
|
416
|
+
const filePath = resolveCredentialsPath(profile);
|
|
417
|
+
try {
|
|
418
|
+
if (!isReadableCredentialFile(filePath)) return null;
|
|
419
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
420
|
+
const parsed = JSON.parse(raw);
|
|
421
|
+
if (typeof parsed.imei !== "string" || !parsed.imei || !parsed.cookie || typeof parsed.userAgent !== "string" || !parsed.userAgent) return null;
|
|
422
|
+
const credentials = {
|
|
423
|
+
imei: parsed.imei,
|
|
424
|
+
cookie: parsed.cookie,
|
|
425
|
+
userAgent: parsed.userAgent,
|
|
426
|
+
language: typeof parsed.language === "string" ? parsed.language : void 0,
|
|
427
|
+
createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
428
|
+
lastUsedAt: typeof parsed.lastUsedAt === "string" ? parsed.lastUsedAt : void 0
|
|
429
|
+
};
|
|
430
|
+
credentialSignaturesByProfile.set(profile, credentialSignature(credentials));
|
|
431
|
+
return credentials;
|
|
432
|
+
} catch {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function credentialSignature(credentials) {
|
|
437
|
+
return JSON.stringify({
|
|
438
|
+
imei: credentials.imei,
|
|
439
|
+
cookie: canonicalCredentialCookie(credentials.cookie),
|
|
440
|
+
userAgent: credentials.userAgent,
|
|
441
|
+
language: credentials.language
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
function stableCanonicalValue(value) {
|
|
445
|
+
if (Array.isArray(value)) return value.map(stableCanonicalValue);
|
|
446
|
+
if (!value || typeof value !== "object") return value;
|
|
447
|
+
return Object.fromEntries(Object.entries(value).toSorted(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, stableCanonicalValue(entry)]));
|
|
448
|
+
}
|
|
449
|
+
function stableSignatureValue(value) {
|
|
450
|
+
return JSON.stringify(stableCanonicalValue(value)) ?? "undefined";
|
|
451
|
+
}
|
|
452
|
+
function canonicalCookieArray(value) {
|
|
453
|
+
return value.map(stableCanonicalValue).toSorted((left, right) => stableSignatureValue(left).localeCompare(stableSignatureValue(right)));
|
|
454
|
+
}
|
|
455
|
+
function canonicalCredentialCookie(cookie) {
|
|
456
|
+
if (Array.isArray(cookie)) return canonicalCookieArray(cookie);
|
|
457
|
+
if (!cookie || typeof cookie !== "object") return cookie;
|
|
458
|
+
return Object.fromEntries(Object.entries(cookie).toSorted(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, key === "cookies" && Array.isArray(entry) ? canonicalCookieArray(entry) : stableCanonicalValue(entry)]));
|
|
459
|
+
}
|
|
460
|
+
function writeCredentials(profile, credentials) {
|
|
461
|
+
const existing = readCredentials(profile);
|
|
462
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
463
|
+
const next = {
|
|
464
|
+
...credentials,
|
|
465
|
+
createdAt: existing?.createdAt ?? now,
|
|
466
|
+
lastUsedAt: now
|
|
467
|
+
};
|
|
468
|
+
writeCredentialFileAtomic(resolveCredentialsPath(profile), JSON.stringify(next, null, 2));
|
|
469
|
+
credentialSignaturesByProfile.set(profile, credentialSignature(next));
|
|
470
|
+
}
|
|
471
|
+
function snapshotApiCredentials(api, fallback) {
|
|
472
|
+
const ctx = api.getContext();
|
|
473
|
+
const cookieJson = api.getCookie().toJSON();
|
|
474
|
+
const refreshedCookies = Array.isArray(cookieJson?.cookies) && cookieJson.cookies.length > 0 ? cookieJson.cookies : fallback?.cookie;
|
|
475
|
+
const imei = normalizeOptionalString(ctx.imei) ?? normalizeOptionalString(fallback?.imei);
|
|
476
|
+
const userAgent = normalizeOptionalString(ctx.userAgent) ?? normalizeOptionalString(fallback?.userAgent);
|
|
477
|
+
if (!imei || !refreshedCookies || !userAgent) throw new Error("Zalo API session did not expose refreshed credentials");
|
|
478
|
+
return {
|
|
479
|
+
imei,
|
|
480
|
+
cookie: refreshedCookies,
|
|
481
|
+
userAgent,
|
|
482
|
+
language: normalizeOptionalString(ctx.language) ?? normalizeOptionalString(fallback?.language)
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function writeApiCredentials(profile, api, fallback) {
|
|
486
|
+
writeCredentials(profile, snapshotApiCredentials(api, fallback));
|
|
487
|
+
}
|
|
488
|
+
function writeApiCredentialsIfChanged(profile, api) {
|
|
489
|
+
const credentials = snapshotApiCredentials(api);
|
|
490
|
+
const signature = credentialSignature(credentials);
|
|
491
|
+
if (credentialSignaturesByProfile.get(profile) === signature) return false;
|
|
492
|
+
writeCredentials(profile, credentials);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
function persistApiCredentialsIfChanged(profile, api) {
|
|
496
|
+
try {
|
|
497
|
+
writeApiCredentialsIfChanged(profile, api);
|
|
498
|
+
} catch {}
|
|
499
|
+
}
|
|
500
|
+
function clearCredentials(profile) {
|
|
501
|
+
const filePath = resolveCredentialsPath(profile);
|
|
502
|
+
try {
|
|
503
|
+
if (fs.existsSync(filePath)) {
|
|
504
|
+
fs.unlinkSync(filePath);
|
|
505
|
+
credentialSignaturesByProfile.delete(profile);
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
} catch {}
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
async function ensureApi(profileInput, timeoutMs = API_LOGIN_TIMEOUT_MS) {
|
|
512
|
+
const profile = normalizeProfile(profileInput);
|
|
513
|
+
const cached = apiByProfile.get(profile);
|
|
514
|
+
if (cached) return cached;
|
|
515
|
+
const pending = apiInitByProfile.get(profile);
|
|
516
|
+
if (pending) return await pending;
|
|
517
|
+
const initPromise = (async () => {
|
|
518
|
+
const stored = readCredentials(profile);
|
|
519
|
+
if (!stored) throw new Error(`No saved Zalo session for profile "${profile}"`);
|
|
520
|
+
const api = await withTimeout((await createZalo({
|
|
521
|
+
logging: false,
|
|
522
|
+
selfListen: false
|
|
523
|
+
})).login({
|
|
524
|
+
imei: stored.imei,
|
|
525
|
+
cookie: stored.cookie,
|
|
526
|
+
userAgent: stored.userAgent,
|
|
527
|
+
language: stored.language
|
|
528
|
+
}), timeoutMs, `Timed out restoring Zalo session for profile "${profile}"`);
|
|
529
|
+
apiByProfile.set(profile, api);
|
|
530
|
+
writeApiCredentials(profile, api, stored);
|
|
531
|
+
return api;
|
|
532
|
+
})();
|
|
533
|
+
apiInitByProfile.set(profile, initPromise);
|
|
534
|
+
try {
|
|
535
|
+
return await initPromise;
|
|
536
|
+
} catch (error) {
|
|
537
|
+
apiByProfile.delete(profile);
|
|
538
|
+
throw error;
|
|
539
|
+
} finally {
|
|
540
|
+
apiInitByProfile.delete(profile);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async function withZaloApi(profileInput, operation, options = {}) {
|
|
544
|
+
const profile = normalizeProfile(profileInput);
|
|
545
|
+
const api = await ensureApi(profile, options.timeoutMs);
|
|
546
|
+
const result = await operation(api);
|
|
547
|
+
if (options.shouldPersist?.(result) ?? true) persistApiCredentialsIfChanged(profile, api);
|
|
548
|
+
return result;
|
|
549
|
+
}
|
|
550
|
+
function invalidateApi(profileInput) {
|
|
551
|
+
const profile = normalizeProfile(profileInput);
|
|
552
|
+
const api = apiByProfile.get(profile);
|
|
553
|
+
if (api) try {
|
|
554
|
+
api.listener.stop();
|
|
555
|
+
} catch {}
|
|
556
|
+
apiByProfile.delete(profile);
|
|
557
|
+
apiInitByProfile.delete(profile);
|
|
558
|
+
}
|
|
559
|
+
function isQrLoginFresh(login) {
|
|
560
|
+
return Date.now() - login.startedAt < QR_LOGIN_TTL_MS;
|
|
561
|
+
}
|
|
562
|
+
function resetQrLogin(profileInput) {
|
|
563
|
+
const profile = normalizeProfile(profileInput);
|
|
564
|
+
const active = activeQrLogins.get(profile);
|
|
565
|
+
if (!active) return;
|
|
566
|
+
try {
|
|
567
|
+
active.abort?.();
|
|
568
|
+
} catch {}
|
|
569
|
+
activeQrLogins.delete(profile);
|
|
570
|
+
}
|
|
571
|
+
async function fetchGroupsByIds(api, ids) {
|
|
572
|
+
const result = /* @__PURE__ */ new Map();
|
|
573
|
+
for (let index = 0; index < ids.length; index += GROUP_INFO_CHUNK_SIZE) {
|
|
574
|
+
const chunk = ids.slice(index, index + GROUP_INFO_CHUNK_SIZE);
|
|
575
|
+
if (chunk.length === 0) continue;
|
|
576
|
+
const map = (await api.getGroupInfo(chunk)).gridInfoMap ?? {};
|
|
577
|
+
for (const [groupId, info] of Object.entries(map)) result.set(groupId, info);
|
|
578
|
+
}
|
|
579
|
+
return result;
|
|
580
|
+
}
|
|
581
|
+
function makeGroupContextCacheKey(profile, groupId) {
|
|
582
|
+
return `${profile}:${groupId}`;
|
|
583
|
+
}
|
|
584
|
+
function readCachedGroupContext(profile, groupId) {
|
|
585
|
+
const key = makeGroupContextCacheKey(profile, groupId);
|
|
586
|
+
const cached = groupContextCache.get(key);
|
|
587
|
+
if (!cached) return null;
|
|
588
|
+
if (cached.expiresAt <= Date.now()) {
|
|
589
|
+
groupContextCache.delete(key);
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
groupContextCache.delete(key);
|
|
593
|
+
groupContextCache.set(key, cached);
|
|
594
|
+
return cached.value;
|
|
595
|
+
}
|
|
596
|
+
function trimGroupContextCache(now) {
|
|
597
|
+
for (const [key, value] of groupContextCache) {
|
|
598
|
+
if (value.expiresAt > now) continue;
|
|
599
|
+
groupContextCache.delete(key);
|
|
600
|
+
}
|
|
601
|
+
while (groupContextCache.size > GROUP_CONTEXT_CACHE_MAX_ENTRIES) {
|
|
602
|
+
const oldestKey = groupContextCache.keys().next().value;
|
|
603
|
+
if (!oldestKey) break;
|
|
604
|
+
groupContextCache.delete(oldestKey);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function writeCachedGroupContext(profile, context) {
|
|
608
|
+
const now = Date.now();
|
|
609
|
+
const key = makeGroupContextCacheKey(profile, context.groupId);
|
|
610
|
+
if (groupContextCache.has(key)) groupContextCache.delete(key);
|
|
611
|
+
groupContextCache.set(key, {
|
|
612
|
+
value: context,
|
|
613
|
+
expiresAt: now + GROUP_CONTEXT_CACHE_TTL_MS
|
|
614
|
+
});
|
|
615
|
+
trimGroupContextCache(now);
|
|
616
|
+
}
|
|
617
|
+
function clearCachedGroupContext(profile) {
|
|
618
|
+
for (const key of groupContextCache.keys()) if (key.startsWith(`${profile}:`)) groupContextCache.delete(key);
|
|
619
|
+
}
|
|
620
|
+
function extractGroupMembersFromInfo(groupInfo) {
|
|
621
|
+
if (!groupInfo || !Array.isArray(groupInfo.currentMems)) return;
|
|
622
|
+
const members = groupInfo.currentMems.map((member) => {
|
|
623
|
+
if (!member || typeof member !== "object") return "";
|
|
624
|
+
const record = member;
|
|
625
|
+
return toStringValue(record.dName) || toStringValue(record.zaloName);
|
|
626
|
+
}).filter(Boolean);
|
|
627
|
+
if (members.length === 0) return;
|
|
628
|
+
return members;
|
|
629
|
+
}
|
|
630
|
+
function toInboundMessage(message, ownUserId) {
|
|
631
|
+
const data = message.data;
|
|
632
|
+
const isGroup = message.type === ThreadType.Group;
|
|
633
|
+
const senderId = toNumberId(data.uidFrom);
|
|
634
|
+
const threadId = isGroup ? toNumberId(data.idTo) : toNumberId(data.uidFrom) || toNumberId(data.idTo);
|
|
635
|
+
if (!threadId || !senderId) return null;
|
|
636
|
+
const content = normalizeMessageContent(data.content);
|
|
637
|
+
const normalizedOwnUserId = toNumberId(ownUserId);
|
|
638
|
+
const mentionIds = extractMentionIds(data.mentions);
|
|
639
|
+
const quoteOwnerId = data.quote && typeof data.quote === "object" ? toNumberId(data.quote.ownerId) : "";
|
|
640
|
+
const hasAnyMention = mentionIds.length > 0;
|
|
641
|
+
const canResolveExplicitMention = Boolean(normalizedOwnUserId);
|
|
642
|
+
const wasExplicitlyMentioned = Boolean(normalizedOwnUserId && mentionIds.some((id) => id === normalizedOwnUserId));
|
|
643
|
+
const commandContent = wasExplicitlyMentioned ? stripOwnMentionsForCommandBody(content, data.mentions, normalizedOwnUserId) : hasAnyMention && !canResolveExplicitMention ? stripLeadingAtMentionForCommand(content) : content;
|
|
644
|
+
const implicitMention = Boolean(normalizedOwnUserId && quoteOwnerId && quoteOwnerId === normalizedOwnUserId);
|
|
645
|
+
const eventMessage = buildEventMessage(data);
|
|
646
|
+
return {
|
|
647
|
+
threadId,
|
|
648
|
+
isGroup,
|
|
649
|
+
senderId,
|
|
650
|
+
senderName: typeof data.dName === "string" ? data.dName.trim() || void 0 : void 0,
|
|
651
|
+
groupName: isGroup ? resolveGroupNameFromMessageData(data) : void 0,
|
|
652
|
+
content,
|
|
653
|
+
commandContent,
|
|
654
|
+
timestampMs: resolveInboundTimestamp(data.ts),
|
|
655
|
+
msgId: typeof data.msgId === "string" ? data.msgId : void 0,
|
|
656
|
+
cliMsgId: typeof data.cliMsgId === "string" ? data.cliMsgId : void 0,
|
|
657
|
+
hasAnyMention,
|
|
658
|
+
canResolveExplicitMention,
|
|
659
|
+
wasExplicitlyMentioned,
|
|
660
|
+
implicitMention,
|
|
661
|
+
eventMessage,
|
|
662
|
+
raw: message
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function zalouserSessionExists(profileInput) {
|
|
666
|
+
return readCredentials(normalizeProfile(profileInput)) !== null;
|
|
667
|
+
}
|
|
668
|
+
async function checkZaloAuthenticated(profileInput) {
|
|
669
|
+
const profile = normalizeProfile(profileInput);
|
|
670
|
+
if (!zalouserSessionExists(profile)) return false;
|
|
671
|
+
try {
|
|
672
|
+
await withZaloApi(profile, async (api) => await withTimeout(api.fetchAccountInfo(), 12e3, "Timed out checking Zalo session"), { timeoutMs: 12e3 });
|
|
673
|
+
return true;
|
|
674
|
+
} catch {
|
|
675
|
+
invalidateApi(profile);
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async function getZaloUserInfo(profileInput) {
|
|
680
|
+
return await withZaloApi(normalizeProfile(profileInput), async (api) => {
|
|
681
|
+
const user = normalizeAccountInfoUser(await api.fetchAccountInfo());
|
|
682
|
+
if (!user?.userId) return null;
|
|
683
|
+
return {
|
|
684
|
+
userId: user.userId,
|
|
685
|
+
displayName: user.displayName || user.zaloName || user.userId,
|
|
686
|
+
avatar: user.avatar || void 0
|
|
687
|
+
};
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
async function listZaloFriends(profileInput) {
|
|
691
|
+
return await withZaloApi(normalizeProfile(profileInput), async (api) => {
|
|
692
|
+
return (await api.getAllFriends()).map(mapFriend);
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
async function listZaloFriendsMatching(profileInput, query) {
|
|
696
|
+
const friends = await listZaloFriends(profileInput);
|
|
697
|
+
const q = normalizeOptionalLowercaseString(query);
|
|
698
|
+
if (!q) return friends;
|
|
699
|
+
return friends.map((friend) => {
|
|
700
|
+
const id = normalizeLowercaseStringOrEmpty(friend.userId);
|
|
701
|
+
const name = normalizeLowercaseStringOrEmpty(friend.displayName);
|
|
702
|
+
return {
|
|
703
|
+
friend,
|
|
704
|
+
exact: id === q || name === q,
|
|
705
|
+
includes: id.includes(q) || name.includes(q)
|
|
706
|
+
};
|
|
707
|
+
}).filter((entry) => entry.includes).toSorted((a, b) => Number(b.exact) - Number(a.exact)).map((entry) => entry.friend);
|
|
708
|
+
}
|
|
709
|
+
async function listZaloGroups(profileInput) {
|
|
710
|
+
return await withZaloApi(normalizeProfile(profileInput), async (api) => {
|
|
711
|
+
const allGroups = await api.getAllGroups();
|
|
712
|
+
const ids = Object.keys(allGroups.gridVerMap ?? {});
|
|
713
|
+
if (ids.length === 0) return [];
|
|
714
|
+
const details = await fetchGroupsByIds(api, ids);
|
|
715
|
+
const rows = [];
|
|
716
|
+
for (const id of ids) {
|
|
717
|
+
const info = details.get(id);
|
|
718
|
+
if (!info) {
|
|
719
|
+
rows.push({
|
|
720
|
+
groupId: id,
|
|
721
|
+
name: id
|
|
722
|
+
});
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
rows.push(mapGroup(id, info));
|
|
726
|
+
}
|
|
727
|
+
return rows;
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
async function listZaloGroupsMatching(profileInput, query) {
|
|
731
|
+
const groups = await listZaloGroups(profileInput);
|
|
732
|
+
const q = normalizeOptionalLowercaseString(query);
|
|
733
|
+
if (!q) return groups;
|
|
734
|
+
return groups.filter((group) => {
|
|
735
|
+
const id = normalizeLowercaseStringOrEmpty(group.groupId);
|
|
736
|
+
const name = normalizeLowercaseStringOrEmpty(group.name);
|
|
737
|
+
return id.includes(q) || name.includes(q);
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
async function listZaloGroupMembers(profileInput, groupId) {
|
|
741
|
+
return await withZaloApi(normalizeProfile(profileInput), async (api) => {
|
|
742
|
+
const groupInfo = (await api.getGroupInfo(groupId)).gridInfoMap?.[groupId];
|
|
743
|
+
if (!groupInfo) return [];
|
|
744
|
+
const memberIds = Array.isArray(groupInfo.memberIds) ? groupInfo.memberIds.map((id) => toNumberId(id)).filter(Boolean) : [];
|
|
745
|
+
const memVerIds = Array.isArray(groupInfo.memVerList) ? groupInfo.memVerList.map((id) => toNumberId(id)).filter(Boolean) : [];
|
|
746
|
+
const currentMembers = Array.isArray(groupInfo.currentMems) ? groupInfo.currentMems : [];
|
|
747
|
+
const currentById = /* @__PURE__ */ new Map();
|
|
748
|
+
for (const member of currentMembers) {
|
|
749
|
+
const id = toNumberId(member?.id);
|
|
750
|
+
if (!id) continue;
|
|
751
|
+
currentById.set(id, {
|
|
752
|
+
displayName: normalizeOptionalString(member.dName) ?? normalizeOptionalString(member.zaloName),
|
|
753
|
+
avatar: member.avatar || void 0
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
const uniqueIds = Array.from(new Set([
|
|
757
|
+
...memberIds,
|
|
758
|
+
...memVerIds,
|
|
759
|
+
...currentById.keys()
|
|
760
|
+
]));
|
|
761
|
+
const profileMap = /* @__PURE__ */ new Map();
|
|
762
|
+
if (uniqueIds.length > 0) {
|
|
763
|
+
const profileEntries = (await api.getGroupMembersInfo(uniqueIds)).profiles;
|
|
764
|
+
for (const [rawId, profileValue] of Object.entries(profileEntries)) {
|
|
765
|
+
const id = toNumberId(rawId) || toNumberId(profileValue?.id);
|
|
766
|
+
if (!id || !profileValue) continue;
|
|
767
|
+
profileMap.set(id, {
|
|
768
|
+
displayName: normalizeOptionalString(profileValue.displayName) ?? normalizeOptionalString(profileValue.zaloName),
|
|
769
|
+
avatar: profileValue.avatar || void 0
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return uniqueIds.map((id) => ({
|
|
774
|
+
userId: id,
|
|
775
|
+
displayName: profileMap.get(id)?.displayName || currentById.get(id)?.displayName || id,
|
|
776
|
+
avatar: profileMap.get(id)?.avatar || currentById.get(id)?.avatar
|
|
777
|
+
}));
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
async function resolveZaloGroupContext(profileInput, groupId) {
|
|
781
|
+
const profile = normalizeProfile(profileInput);
|
|
782
|
+
const normalizedGroupId = toNumberId(groupId) || groupId.trim();
|
|
783
|
+
if (!normalizedGroupId) throw new Error("groupId is required");
|
|
784
|
+
const cached = readCachedGroupContext(profile, normalizedGroupId);
|
|
785
|
+
if (cached) return cached;
|
|
786
|
+
return await withZaloApi(profile, async (api) => {
|
|
787
|
+
const groupInfo = (await api.getGroupInfo(normalizedGroupId)).gridInfoMap?.[normalizedGroupId];
|
|
788
|
+
const context = {
|
|
789
|
+
groupId: normalizedGroupId,
|
|
790
|
+
name: normalizeOptionalString(groupInfo?.name),
|
|
791
|
+
members: extractGroupMembersFromInfo(groupInfo)
|
|
792
|
+
};
|
|
793
|
+
writeCachedGroupContext(profile, context);
|
|
794
|
+
return context;
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
async function sendZaloTextMessage(threadId, text, options = {}) {
|
|
798
|
+
const profile = normalizeProfile(options.profile);
|
|
799
|
+
const trimmedThreadId = threadId.trim();
|
|
800
|
+
if (!trimmedThreadId) return {
|
|
801
|
+
ok: false,
|
|
802
|
+
error: "No threadId provided"
|
|
803
|
+
};
|
|
804
|
+
return await withZaloApi(profile, async (api) => {
|
|
805
|
+
const type = options.isGroup ? ThreadType.Group : ThreadType.User;
|
|
806
|
+
try {
|
|
807
|
+
if (options.mediaUrl?.trim()) {
|
|
808
|
+
const media = await loadOutboundMediaFromUrl(options.mediaUrl.trim(), {
|
|
809
|
+
mediaLocalRoots: options.mediaLocalRoots,
|
|
810
|
+
mediaReadFile: options.mediaReadFile
|
|
811
|
+
});
|
|
812
|
+
const fileName = resolveMediaFileName({
|
|
813
|
+
mediaUrl: options.mediaUrl,
|
|
814
|
+
fileName: media.fileName,
|
|
815
|
+
contentType: media.contentType,
|
|
816
|
+
kind: media.kind
|
|
817
|
+
});
|
|
818
|
+
const payloadText = (text || options.caption || "").slice(0, 2e3);
|
|
819
|
+
const textStyles = clampTextStyles(payloadText, options.textStyles);
|
|
820
|
+
if (media.kind === "audio") {
|
|
821
|
+
let textMessageId;
|
|
822
|
+
if (payloadText) textMessageId = extractSendMessageId(await api.sendMessage(textStyles ? {
|
|
823
|
+
msg: payloadText,
|
|
824
|
+
styles: textStyles
|
|
825
|
+
} : payloadText, trimmedThreadId, type));
|
|
826
|
+
const attachmentFileName = fileName.includes(".") ? fileName : `${fileName}.bin`;
|
|
827
|
+
const voiceAsset = resolveUploadedVoiceAsset(await api.uploadAttachment([{
|
|
828
|
+
data: media.buffer,
|
|
829
|
+
filename: attachmentFileName,
|
|
830
|
+
metadata: { totalSize: media.buffer.length }
|
|
831
|
+
}], trimmedThreadId, type));
|
|
832
|
+
if (!voiceAsset) throw new Error("Failed to resolve uploaded audio URL for voice message");
|
|
833
|
+
const voiceUrl = buildZaloVoicePlaybackUrl(voiceAsset);
|
|
834
|
+
return {
|
|
835
|
+
ok: true,
|
|
836
|
+
messageId: extractSendMessageId(await api.sendVoice({ voiceUrl }, trimmedThreadId, type)) ?? textMessageId
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
ok: true,
|
|
841
|
+
messageId: extractSendMessageId(await api.sendMessage({
|
|
842
|
+
msg: payloadText,
|
|
843
|
+
...textStyles ? { styles: textStyles } : {},
|
|
844
|
+
attachments: [{
|
|
845
|
+
data: media.buffer,
|
|
846
|
+
filename: fileName.includes(".") ? fileName : `${fileName}.bin`,
|
|
847
|
+
metadata: { totalSize: media.buffer.length }
|
|
848
|
+
}]
|
|
849
|
+
}, trimmedThreadId, type))
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
const payloadText = text.slice(0, 2e3);
|
|
853
|
+
const textStyles = clampTextStyles(payloadText, options.textStyles);
|
|
854
|
+
return {
|
|
855
|
+
ok: true,
|
|
856
|
+
messageId: extractSendMessageId(await api.sendMessage(textStyles ? {
|
|
857
|
+
msg: payloadText,
|
|
858
|
+
styles: textStyles
|
|
859
|
+
} : payloadText, trimmedThreadId, type))
|
|
860
|
+
};
|
|
861
|
+
} catch (error) {
|
|
862
|
+
return {
|
|
863
|
+
ok: false,
|
|
864
|
+
error: toErrorMessage(error)
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
}, { shouldPersist: (result) => result.ok });
|
|
868
|
+
}
|
|
869
|
+
async function sendZaloTypingEvent(threadId, options = {}) {
|
|
870
|
+
const profile = normalizeProfile(options.profile);
|
|
871
|
+
const trimmedThreadId = threadId.trim();
|
|
872
|
+
if (!trimmedThreadId) throw new Error("No threadId provided");
|
|
873
|
+
await withZaloApi(profile, async (api) => {
|
|
874
|
+
const type = options.isGroup ? ThreadType.Group : ThreadType.User;
|
|
875
|
+
if ("sendTypingEvent" in api && typeof api.sendTypingEvent === "function") {
|
|
876
|
+
await api.sendTypingEvent(trimmedThreadId, type);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
throw new Error("Zalo typing indicator is not supported by current API session");
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
async function resolveOwnUserId(api) {
|
|
883
|
+
try {
|
|
884
|
+
const resolved = toNumberId(normalizeAccountInfoUser(await api.fetchAccountInfo())?.userId);
|
|
885
|
+
if (resolved) return resolved;
|
|
886
|
+
} catch {}
|
|
887
|
+
try {
|
|
888
|
+
const ownId = toNumberId(api.getOwnId());
|
|
889
|
+
if (ownId) return ownId;
|
|
890
|
+
} catch {}
|
|
891
|
+
return "";
|
|
892
|
+
}
|
|
893
|
+
async function sendZaloReaction(params) {
|
|
894
|
+
const profile = normalizeProfile(params.profile);
|
|
895
|
+
const threadId = params.threadId.trim();
|
|
896
|
+
const msgId = toStringValue(params.msgId);
|
|
897
|
+
const cliMsgId = toStringValue(params.cliMsgId);
|
|
898
|
+
if (!threadId || !msgId || !cliMsgId) return {
|
|
899
|
+
ok: false,
|
|
900
|
+
error: "threadId, msgId, and cliMsgId are required"
|
|
901
|
+
};
|
|
902
|
+
try {
|
|
903
|
+
return await withZaloApi(profile, async (api) => {
|
|
904
|
+
const type = params.isGroup ? ThreadType.Group : ThreadType.User;
|
|
905
|
+
const icon = params.remove ? {
|
|
906
|
+
rType: -1,
|
|
907
|
+
source: 6,
|
|
908
|
+
icon: ""
|
|
909
|
+
} : normalizeZaloReactionIcon(params.emoji);
|
|
910
|
+
await api.addReaction(icon, {
|
|
911
|
+
data: {
|
|
912
|
+
msgId,
|
|
913
|
+
cliMsgId
|
|
914
|
+
},
|
|
915
|
+
threadId,
|
|
916
|
+
type
|
|
917
|
+
});
|
|
918
|
+
return { ok: true };
|
|
919
|
+
}, { shouldPersist: (result) => result.ok });
|
|
920
|
+
} catch (error) {
|
|
921
|
+
return {
|
|
922
|
+
ok: false,
|
|
923
|
+
error: toErrorMessage(error)
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
async function sendZaloDeliveredEvent(params) {
|
|
928
|
+
await withZaloApi(normalizeProfile(params.profile), async (api) => {
|
|
929
|
+
const type = params.isGroup ? ThreadType.Group : ThreadType.User;
|
|
930
|
+
await api.sendDeliveredEvent(params.isSeen === true, params.message, type);
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
async function sendZaloSeenEvent(params) {
|
|
934
|
+
await withZaloApi(normalizeProfile(params.profile), async (api) => {
|
|
935
|
+
const type = params.isGroup ? ThreadType.Group : ThreadType.User;
|
|
936
|
+
await api.sendSeenEvent(params.message, type);
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
async function sendZaloLink(threadId, url, options = {}) {
|
|
940
|
+
const profile = normalizeProfile(options.profile);
|
|
941
|
+
const trimmedThreadId = threadId.trim();
|
|
942
|
+
const trimmedUrl = url.trim();
|
|
943
|
+
if (!trimmedThreadId) return {
|
|
944
|
+
ok: false,
|
|
945
|
+
error: "No threadId provided"
|
|
946
|
+
};
|
|
947
|
+
if (!trimmedUrl) return {
|
|
948
|
+
ok: false,
|
|
949
|
+
error: "No URL provided"
|
|
950
|
+
};
|
|
951
|
+
try {
|
|
952
|
+
return await withZaloApi(profile, async (api) => {
|
|
953
|
+
const type = options.isGroup ? ThreadType.Group : ThreadType.User;
|
|
954
|
+
const response = await api.sendLink({
|
|
955
|
+
link: trimmedUrl,
|
|
956
|
+
msg: options.caption
|
|
957
|
+
}, trimmedThreadId, type);
|
|
958
|
+
return {
|
|
959
|
+
ok: true,
|
|
960
|
+
messageId: String(response.msgId)
|
|
961
|
+
};
|
|
962
|
+
}, { shouldPersist: (result) => result.ok });
|
|
963
|
+
} catch (error) {
|
|
964
|
+
return {
|
|
965
|
+
ok: false,
|
|
966
|
+
error: toErrorMessage(error)
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
async function startZaloQrLogin(params) {
|
|
971
|
+
const profile = normalizeProfile(params.profile);
|
|
972
|
+
if (!params.force && await checkZaloAuthenticated(profile)) {
|
|
973
|
+
const info = await getZaloUserInfo(profile).catch(() => null);
|
|
974
|
+
return { message: `Zalo is already linked${info?.displayName ? ` (${info.displayName})` : ""}.` };
|
|
975
|
+
}
|
|
976
|
+
if (params.force) await logoutZaloProfile(profile);
|
|
977
|
+
const existing = activeQrLogins.get(profile);
|
|
978
|
+
if (existing && isQrLoginFresh(existing)) {
|
|
979
|
+
if (existing.qrDataUrl) return {
|
|
980
|
+
qrDataUrl: existing.qrDataUrl,
|
|
981
|
+
message: "QR already active. Scan it with the Zalo app."
|
|
982
|
+
};
|
|
983
|
+
} else if (existing) resetQrLogin(profile);
|
|
984
|
+
if (!activeQrLogins.has(profile)) {
|
|
985
|
+
const login = {
|
|
986
|
+
id: randomUUID(),
|
|
987
|
+
profile,
|
|
988
|
+
startedAt: Date.now(),
|
|
989
|
+
connected: false,
|
|
990
|
+
waitPromise: Promise.resolve()
|
|
991
|
+
};
|
|
992
|
+
login.waitPromise = (async () => {
|
|
993
|
+
let capturedCredentials = null;
|
|
994
|
+
try {
|
|
995
|
+
const api = await (await createZalo({
|
|
996
|
+
logging: false,
|
|
997
|
+
selfListen: false
|
|
998
|
+
})).loginQR(void 0, (event) => {
|
|
999
|
+
const current = activeQrLogins.get(profile);
|
|
1000
|
+
if (!current || current.id !== login.id) return;
|
|
1001
|
+
if (event.actions?.abort) current.abort = () => {
|
|
1002
|
+
try {
|
|
1003
|
+
event.actions?.abort?.();
|
|
1004
|
+
} catch {}
|
|
1005
|
+
};
|
|
1006
|
+
switch (event.type) {
|
|
1007
|
+
case LoginQRCallbackEventType.QRCodeGenerated: {
|
|
1008
|
+
const image = event.data.image.replace(/^data:image\/png;base64,/, "");
|
|
1009
|
+
current.qrDataUrl = image.startsWith("data:image") ? image : `data:image/png;base64,${image}`;
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
1012
|
+
case LoginQRCallbackEventType.QRCodeExpired:
|
|
1013
|
+
try {
|
|
1014
|
+
event.actions.retry();
|
|
1015
|
+
} catch {
|
|
1016
|
+
current.error = "QR expired before confirmation. Start login again.";
|
|
1017
|
+
}
|
|
1018
|
+
break;
|
|
1019
|
+
case LoginQRCallbackEventType.QRCodeDeclined:
|
|
1020
|
+
current.error = "QR login was declined on the phone.";
|
|
1021
|
+
break;
|
|
1022
|
+
case LoginQRCallbackEventType.GotLoginInfo:
|
|
1023
|
+
capturedCredentials = {
|
|
1024
|
+
imei: event.data.imei,
|
|
1025
|
+
cookie: event.data.cookie,
|
|
1026
|
+
userAgent: event.data.userAgent
|
|
1027
|
+
};
|
|
1028
|
+
break;
|
|
1029
|
+
default: break;
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
const current = activeQrLogins.get(profile);
|
|
1033
|
+
if (!current || current.id !== login.id) return;
|
|
1034
|
+
if (!capturedCredentials) {
|
|
1035
|
+
const ctx = api.getContext();
|
|
1036
|
+
const cookieJson = api.getCookie().toJSON();
|
|
1037
|
+
capturedCredentials = {
|
|
1038
|
+
imei: ctx.imei,
|
|
1039
|
+
cookie: cookieJson?.cookies ?? [],
|
|
1040
|
+
userAgent: ctx.userAgent,
|
|
1041
|
+
language: ctx.language
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
writeApiCredentials(profile, api, capturedCredentials ?? void 0);
|
|
1045
|
+
invalidateApi(profile);
|
|
1046
|
+
apiByProfile.set(profile, api);
|
|
1047
|
+
current.connected = true;
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
const current = activeQrLogins.get(profile);
|
|
1050
|
+
if (current && current.id === login.id) current.error = toErrorMessage(error);
|
|
1051
|
+
}
|
|
1052
|
+
})();
|
|
1053
|
+
activeQrLogins.set(profile, login);
|
|
1054
|
+
}
|
|
1055
|
+
const active = activeQrLogins.get(profile);
|
|
1056
|
+
if (!active) return { message: "Failed to initialize Zalo QR login." };
|
|
1057
|
+
const timeoutMs = Math.max(params.timeoutMs ?? DEFAULT_QR_START_TIMEOUT_MS, 3e3);
|
|
1058
|
+
const deadline = Date.now() + timeoutMs;
|
|
1059
|
+
while (Date.now() < deadline) {
|
|
1060
|
+
if (active.error) {
|
|
1061
|
+
resetQrLogin(profile);
|
|
1062
|
+
return { message: `Failed to start QR login: ${active.error}` };
|
|
1063
|
+
}
|
|
1064
|
+
if (active.connected) {
|
|
1065
|
+
resetQrLogin(profile);
|
|
1066
|
+
return { message: "Zalo already connected." };
|
|
1067
|
+
}
|
|
1068
|
+
if (active.qrDataUrl) return {
|
|
1069
|
+
qrDataUrl: active.qrDataUrl,
|
|
1070
|
+
message: "Scan this QR with the Zalo app."
|
|
1071
|
+
};
|
|
1072
|
+
await delay(150);
|
|
1073
|
+
}
|
|
1074
|
+
return { message: "Still preparing QR. Call wait to continue checking login status." };
|
|
1075
|
+
}
|
|
1076
|
+
async function waitForZaloQrLogin(params) {
|
|
1077
|
+
const profile = normalizeProfile(params.profile);
|
|
1078
|
+
const active = activeQrLogins.get(profile);
|
|
1079
|
+
if (!active) {
|
|
1080
|
+
const connected = await checkZaloAuthenticated(profile);
|
|
1081
|
+
return {
|
|
1082
|
+
connected,
|
|
1083
|
+
message: connected ? "Zalo session is ready." : "No active Zalo QR login in progress."
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
if (!isQrLoginFresh(active)) {
|
|
1087
|
+
resetQrLogin(profile);
|
|
1088
|
+
return {
|
|
1089
|
+
connected: false,
|
|
1090
|
+
message: "QR login expired. Start again to generate a fresh QR code."
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
const timeoutMs = Math.max(params.timeoutMs ?? DEFAULT_QR_WAIT_TIMEOUT_MS, 1e3);
|
|
1094
|
+
const deadline = Date.now() + timeoutMs;
|
|
1095
|
+
while (Date.now() < deadline) {
|
|
1096
|
+
if (active.error) {
|
|
1097
|
+
const message = `Zalo login failed: ${active.error}`;
|
|
1098
|
+
resetQrLogin(profile);
|
|
1099
|
+
return {
|
|
1100
|
+
connected: false,
|
|
1101
|
+
message
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
if (active.connected) {
|
|
1105
|
+
resetQrLogin(profile);
|
|
1106
|
+
return {
|
|
1107
|
+
connected: true,
|
|
1108
|
+
message: "Login successful."
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
await Promise.race([active.waitPromise, delay(400)]);
|
|
1112
|
+
}
|
|
1113
|
+
return {
|
|
1114
|
+
connected: false,
|
|
1115
|
+
message: "Still waiting for QR scan confirmation."
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
async function logoutZaloProfile(profileInput) {
|
|
1119
|
+
const profile = normalizeProfile(profileInput);
|
|
1120
|
+
resetQrLogin(profile);
|
|
1121
|
+
clearCachedGroupContext(profile);
|
|
1122
|
+
const listener = activeListeners.get(profile);
|
|
1123
|
+
if (listener) {
|
|
1124
|
+
try {
|
|
1125
|
+
listener.stop();
|
|
1126
|
+
} catch {}
|
|
1127
|
+
activeListeners.delete(profile);
|
|
1128
|
+
}
|
|
1129
|
+
invalidateApi(profile);
|
|
1130
|
+
const cleared = clearCredentials(profile);
|
|
1131
|
+
return {
|
|
1132
|
+
cleared,
|
|
1133
|
+
loggedOut: true,
|
|
1134
|
+
message: cleared ? "Logged out and cleared local session." : "No local session to clear."
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
async function startZaloListener(params) {
|
|
1138
|
+
const profile = normalizeProfile(params.profile);
|
|
1139
|
+
const existing = activeListeners.get(profile);
|
|
1140
|
+
if (existing) throw new Error(`Zalo listener already running for profile "${profile}" (account "${existing.accountId}")`);
|
|
1141
|
+
const { api, ownUserId } = await withZaloApi(profile, async (api) => ({
|
|
1142
|
+
api,
|
|
1143
|
+
ownUserId: await resolveOwnUserId(api)
|
|
1144
|
+
}));
|
|
1145
|
+
let stopped = false;
|
|
1146
|
+
let watchdogTimer = null;
|
|
1147
|
+
let lastWatchdogTickAt = Date.now();
|
|
1148
|
+
const cleanup = () => {
|
|
1149
|
+
if (stopped) return;
|
|
1150
|
+
stopped = true;
|
|
1151
|
+
if (watchdogTimer) {
|
|
1152
|
+
clearInterval(watchdogTimer);
|
|
1153
|
+
watchdogTimer = null;
|
|
1154
|
+
}
|
|
1155
|
+
try {
|
|
1156
|
+
api.listener.off("message", onMessage);
|
|
1157
|
+
api.listener.off("error", onError);
|
|
1158
|
+
api.listener.off("closed", onClosed);
|
|
1159
|
+
} catch {}
|
|
1160
|
+
try {
|
|
1161
|
+
api.listener.stop();
|
|
1162
|
+
} catch {}
|
|
1163
|
+
activeListeners.delete(profile);
|
|
1164
|
+
};
|
|
1165
|
+
const onMessage = (incoming) => {
|
|
1166
|
+
if (incoming.isSelf) return;
|
|
1167
|
+
const normalized = toInboundMessage(incoming, ownUserId);
|
|
1168
|
+
if (!normalized) return;
|
|
1169
|
+
params.onMessage(normalized);
|
|
1170
|
+
};
|
|
1171
|
+
const failListener = (error) => {
|
|
1172
|
+
if (stopped || params.abortSignal.aborted) return;
|
|
1173
|
+
cleanup();
|
|
1174
|
+
invalidateApi(profile);
|
|
1175
|
+
params.onError(error);
|
|
1176
|
+
};
|
|
1177
|
+
const onError = (error) => {
|
|
1178
|
+
failListener(error instanceof Error ? error : new Error(String(error)));
|
|
1179
|
+
};
|
|
1180
|
+
const onClosed = (code, reason) => {
|
|
1181
|
+
failListener(/* @__PURE__ */ new Error(`Zalo listener closed (${code}): ${reason || "no reason"}`));
|
|
1182
|
+
};
|
|
1183
|
+
api.listener.on("message", onMessage);
|
|
1184
|
+
api.listener.on("error", onError);
|
|
1185
|
+
api.listener.on("closed", onClosed);
|
|
1186
|
+
try {
|
|
1187
|
+
api.listener.start({ retryOnClose: false });
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
cleanup();
|
|
1190
|
+
throw error;
|
|
1191
|
+
}
|
|
1192
|
+
watchdogTimer = setInterval(() => {
|
|
1193
|
+
if (stopped || params.abortSignal.aborted) return;
|
|
1194
|
+
const now = Date.now();
|
|
1195
|
+
const gapMs = now - lastWatchdogTickAt;
|
|
1196
|
+
lastWatchdogTickAt = now;
|
|
1197
|
+
if (gapMs <= LISTENER_WATCHDOG_MAX_GAP_MS) return;
|
|
1198
|
+
failListener(/* @__PURE__ */ new Error(`Zalo listener watchdog gap detected (${Math.round(gapMs / 1e3)}s): forcing reconnect`));
|
|
1199
|
+
}, LISTENER_WATCHDOG_INTERVAL_MS);
|
|
1200
|
+
watchdogTimer.unref?.();
|
|
1201
|
+
params.abortSignal.addEventListener("abort", () => {
|
|
1202
|
+
cleanup();
|
|
1203
|
+
}, { once: true });
|
|
1204
|
+
activeListeners.set(profile, {
|
|
1205
|
+
profile,
|
|
1206
|
+
accountId: params.accountId,
|
|
1207
|
+
stop: cleanup
|
|
1208
|
+
});
|
|
1209
|
+
return { stop: cleanup };
|
|
1210
|
+
}
|
|
1211
|
+
async function resolveZaloGroupsByEntries(params) {
|
|
1212
|
+
const groups = await listZaloGroups(params.profile);
|
|
1213
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1214
|
+
for (const group of groups) {
|
|
1215
|
+
const key = normalizeOptionalLowercaseString(group.name);
|
|
1216
|
+
if (!key) continue;
|
|
1217
|
+
const list = byName.get(key) ?? [];
|
|
1218
|
+
list.push(group);
|
|
1219
|
+
byName.set(key, list);
|
|
1220
|
+
}
|
|
1221
|
+
return params.entries.map((input) => {
|
|
1222
|
+
const trimmed = input.trim();
|
|
1223
|
+
if (!trimmed) return {
|
|
1224
|
+
input,
|
|
1225
|
+
resolved: false
|
|
1226
|
+
};
|
|
1227
|
+
if (/^\d+$/.test(trimmed)) return {
|
|
1228
|
+
input,
|
|
1229
|
+
resolved: true,
|
|
1230
|
+
id: trimmed
|
|
1231
|
+
};
|
|
1232
|
+
const match = (byName.get(normalizeLowercaseStringOrEmpty(trimmed)) ?? [])[0];
|
|
1233
|
+
return match ? {
|
|
1234
|
+
input,
|
|
1235
|
+
resolved: true,
|
|
1236
|
+
id: match.groupId
|
|
1237
|
+
} : {
|
|
1238
|
+
input,
|
|
1239
|
+
resolved: false
|
|
1240
|
+
};
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
async function resolveZaloAllowFromEntries(params) {
|
|
1244
|
+
const friends = await listZaloFriends(params.profile);
|
|
1245
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1246
|
+
for (const friend of friends) {
|
|
1247
|
+
const key = normalizeOptionalLowercaseString(friend.displayName);
|
|
1248
|
+
if (!key) continue;
|
|
1249
|
+
const list = byName.get(key) ?? [];
|
|
1250
|
+
list.push(friend);
|
|
1251
|
+
byName.set(key, list);
|
|
1252
|
+
}
|
|
1253
|
+
return params.entries.map((input) => {
|
|
1254
|
+
const trimmed = input.trim();
|
|
1255
|
+
if (!trimmed) return {
|
|
1256
|
+
input,
|
|
1257
|
+
resolved: false
|
|
1258
|
+
};
|
|
1259
|
+
if (/^\d+$/.test(trimmed)) return {
|
|
1260
|
+
input,
|
|
1261
|
+
resolved: true,
|
|
1262
|
+
id: trimmed
|
|
1263
|
+
};
|
|
1264
|
+
const matches = byName.get(normalizeLowercaseStringOrEmpty(trimmed)) ?? [];
|
|
1265
|
+
const match = matches[0];
|
|
1266
|
+
if (!match) return {
|
|
1267
|
+
input,
|
|
1268
|
+
resolved: false
|
|
1269
|
+
};
|
|
1270
|
+
return {
|
|
1271
|
+
input,
|
|
1272
|
+
resolved: true,
|
|
1273
|
+
id: match.userId,
|
|
1274
|
+
note: matches.length > 1 ? "multiple matches; chose first" : void 0
|
|
1275
|
+
};
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
//#endregion
|
|
1279
|
+
export { sendZaloTypingEvent as _, listZaloGroupMembers as a, waitForZaloQrLogin as b, logoutZaloProfile as c, resolveZaloGroupsByEntries as d, sendZaloDeliveredEvent as f, sendZaloTextMessage as g, sendZaloSeenEvent as h, listZaloFriendsMatching as i, resolveZaloAllowFromEntries as l, sendZaloReaction as m, getZaloUserInfo as n, listZaloGroups as o, sendZaloLink as p, listZaloFriends as r, listZaloGroupsMatching as s, checkZaloAuthenticated as t, resolveZaloGroupContext as u, startZaloListener as v, TextStyle as x, startZaloQrLogin as y };
|