@kodelyth/zalouser 2026.5.39 → 2026.5.42

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