@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.
Files changed (95) hide show
  1. package/dist/accounts-C00IMUgd.js +63 -0
  2. package/dist/accounts.runtime-uG7S8cXT.js +2 -0
  3. package/dist/api-BRwdUWuS.js +139 -0
  4. package/dist/api.js +7 -0
  5. package/dist/channel-ou_w_2j-.js +484 -0
  6. package/dist/channel-plugin-api.js +2 -0
  7. package/dist/channel.runtime-C9WxiAiR.js +25 -0
  8. package/dist/channel.setup-CiDeBFrn.js +10 -0
  9. package/dist/contract-api.js +3 -0
  10. package/dist/doctor-contract-DgqHp8E2.js +128 -0
  11. package/dist/doctor-contract-api.js +2 -0
  12. package/dist/index.js +27 -0
  13. package/dist/monitor-Cg7K_s_s.js +705 -0
  14. package/dist/runtime-QNU7vLgI.js +106 -0
  15. package/dist/runtime-api.js +22 -0
  16. package/dist/secret-contract-api.js +5 -0
  17. package/dist/security-audit-BZLhil-V.js +34 -0
  18. package/dist/send-BsmySxe3.js +534 -0
  19. package/dist/session-route-C0-Xr8bt.js +92 -0
  20. package/dist/setup-core-CqipqY98.js +40 -0
  21. package/dist/setup-entry.js +11 -0
  22. package/dist/setup-plugin-api.js +2 -0
  23. package/dist/setup-surface-NCOuKu-l.js +359 -0
  24. package/dist/shared-DSy8aIUx.js +120 -0
  25. package/dist/test-api.js +5 -0
  26. package/dist/zalo-js-CHCUlY3c.js +1279 -0
  27. package/package.json +15 -6
  28. package/api.ts +0 -9
  29. package/channel-plugin-api.ts +0 -3
  30. package/contract-api.ts +0 -2
  31. package/doctor-contract-api.ts +0 -1
  32. package/index.ts +0 -34
  33. package/runtime-api.ts +0 -67
  34. package/secret-contract-api.ts +0 -4
  35. package/setup-entry.ts +0 -9
  36. package/setup-plugin-api.ts +0 -2
  37. package/src/accounts.runtime.ts +0 -1
  38. package/src/accounts.test-mocks.ts +0 -14
  39. package/src/accounts.test.ts +0 -266
  40. package/src/accounts.ts +0 -131
  41. package/src/channel-api.ts +0 -20
  42. package/src/channel.adapters.ts +0 -391
  43. package/src/channel.directory.test.ts +0 -59
  44. package/src/channel.runtime.ts +0 -12
  45. package/src/channel.sendpayload.test.ts +0 -172
  46. package/src/channel.setup.test.ts +0 -33
  47. package/src/channel.setup.ts +0 -12
  48. package/src/channel.test.ts +0 -377
  49. package/src/channel.ts +0 -219
  50. package/src/config-schema.ts +0 -33
  51. package/src/directory.ts +0 -54
  52. package/src/doctor-contract.ts +0 -156
  53. package/src/doctor.test.ts +0 -77
  54. package/src/doctor.ts +0 -37
  55. package/src/group-policy.test.ts +0 -61
  56. package/src/group-policy.ts +0 -83
  57. package/src/message-sid.test.ts +0 -66
  58. package/src/message-sid.ts +0 -80
  59. package/src/monitor.account-scope.test.ts +0 -107
  60. package/src/monitor.group-gating.test.ts +0 -816
  61. package/src/monitor.send-mocks.ts +0 -20
  62. package/src/monitor.ts +0 -1044
  63. package/src/probe.test.ts +0 -60
  64. package/src/probe.ts +0 -35
  65. package/src/qr-temp-file.ts +0 -22
  66. package/src/reaction.test.ts +0 -19
  67. package/src/reaction.ts +0 -32
  68. package/src/runtime.ts +0 -9
  69. package/src/security-audit.test.ts +0 -80
  70. package/src/security-audit.ts +0 -71
  71. package/src/send.test.ts +0 -395
  72. package/src/send.ts +0 -272
  73. package/src/session-route.ts +0 -121
  74. package/src/setup-core.ts +0 -33
  75. package/src/setup-surface.test.ts +0 -363
  76. package/src/setup-surface.ts +0 -470
  77. package/src/setup-test-helpers.ts +0 -42
  78. package/src/shared.ts +0 -92
  79. package/src/status-issues.test.ts +0 -31
  80. package/src/status-issues.ts +0 -58
  81. package/src/test-helpers.ts +0 -26
  82. package/src/text-styles.test.ts +0 -203
  83. package/src/text-styles.ts +0 -540
  84. package/src/tool.test.ts +0 -212
  85. package/src/tool.ts +0 -210
  86. package/src/types.ts +0 -125
  87. package/src/zalo-js.credentials.test.ts +0 -465
  88. package/src/zalo-js.test-mocks.ts +0 -89
  89. package/src/zalo-js.ts +0 -1911
  90. package/src/zca-client.test.ts +0 -24
  91. package/src/zca-client.ts +0 -259
  92. package/src/zca-constants.ts +0 -55
  93. package/src/zca-js-exports.d.ts +0 -22
  94. package/test-api.ts +0 -21
  95. 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 };