@kodelyth/zalouser 2026.5.39 → 2026.6.1

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.
@@ -0,0 +1,708 @@
1
+ import { a as resolveZalouserMessageSid, c as isZalouserGroupEntryAllowed, i as formatZalouserMessageSidFull, n as getZalouserRuntime, o as buildZalouserGroupCandidates, s as findZalouserGroupEntry } from "./channel-pby_3Sur.js";
2
+ import { o as listZaloGroups, r as listZaloFriends, u as resolveZaloGroupContext, v as startZaloListener } from "./zalo-js-B80cRyDF.js";
3
+ import { i as sendMessageZalouser, o as sendSeenZalouser, s as sendTypingZalouser, t as sendDeliveredZalouser } from "./send-uRjUB8mG.js";
4
+ import { createDeferred } from "klaw/plugin-sdk/extension-shared";
5
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeStringEntries } from "klaw/plugin-sdk/string-coerce-runtime";
6
+ import { mergeAllowlist, summarizeMapping } from "klaw/plugin-sdk/allow-from";
7
+ import { KeyedAsyncQueue } from "klaw/plugin-sdk/core";
8
+ import { isDangerousNameMatchingEnabled } from "klaw/plugin-sdk/dangerous-name-runtime";
9
+ import { deliverTextOrMediaReply, resolveSendableOutboundReplyParts } from "klaw/plugin-sdk/reply-payload";
10
+ import { createChannelPairingController } from "klaw/plugin-sdk/channel-pairing";
11
+ import { resolveDefaultGroupPolicy, resolveOpenProviderRuntimeGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "klaw/plugin-sdk/runtime-group-policy";
12
+ import { implicitMentionKindWhen, resolveInboundMentionDecision } from "klaw/plugin-sdk/channel-inbound";
13
+ import { resolveStableChannelMessageIngress } from "klaw/plugin-sdk/channel-ingress-runtime";
14
+ import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "klaw/plugin-sdk/reply-history";
15
+ //#region extensions/zalouser/src/monitor.ts
16
+ const ZALOUSER_TEXT_LIMIT = 2e3;
17
+ function buildNameIndex(items, nameFn) {
18
+ const index = /* @__PURE__ */ new Map();
19
+ for (const item of items) {
20
+ const name = normalizeOptionalLowercaseString(nameFn(item));
21
+ if (!name) continue;
22
+ const list = index.get(name) ?? [];
23
+ list.push(item);
24
+ index.set(name, list);
25
+ }
26
+ return index;
27
+ }
28
+ function resolveUserAllowlistEntries(entries, byName) {
29
+ const additions = [];
30
+ const mapping = [];
31
+ const unresolved = [];
32
+ for (const entry of entries) {
33
+ if (/^\d+$/.test(entry)) {
34
+ additions.push(entry);
35
+ continue;
36
+ }
37
+ const id = (byName.get(normalizeLowercaseStringOrEmpty(entry)) ?? [])[0]?.userId;
38
+ if (id) {
39
+ additions.push(id);
40
+ mapping.push(`${entry}->${id}`);
41
+ } else unresolved.push(entry);
42
+ }
43
+ return {
44
+ additions,
45
+ mapping,
46
+ unresolved
47
+ };
48
+ }
49
+ function normalizeZalouserAllowEntry(entry) {
50
+ return entry.replace(/^(zalouser|zlu):/i, "").trim();
51
+ }
52
+ function normalizeZalouserSender(value) {
53
+ return normalizeOptionalLowercaseString(normalizeZalouserAllowEntry(value)) || null;
54
+ }
55
+ function resolveInboundQueueKey(message) {
56
+ const threadId = message.threadId?.trim() || "unknown";
57
+ if (message.isGroup) return `group:${threadId}`;
58
+ return `direct:${message.senderId?.trim() || threadId}`;
59
+ }
60
+ function resolveZalouserDmSessionScope(config) {
61
+ const configured = config.session?.dmScope;
62
+ return configured === "main" || !configured ? "per-channel-peer" : configured;
63
+ }
64
+ function resolveZalouserRouteAccess(params) {
65
+ if (params.groupPolicy === "disabled") return {
66
+ allowed: false,
67
+ reason: "disabled"
68
+ };
69
+ if (params.matched && params.enabled === false) return {
70
+ allowed: false,
71
+ reason: "route_disabled"
72
+ };
73
+ if (params.groupPolicy !== "allowlist") return { allowed: true };
74
+ if (!params.configured) return {
75
+ allowed: false,
76
+ reason: "empty_allowlist"
77
+ };
78
+ return params.matched ? { allowed: true } : {
79
+ allowed: false,
80
+ reason: "route_not_allowlisted"
81
+ };
82
+ }
83
+ function senderScopedZalouserGroupPolicy(params) {
84
+ if (params.groupPolicy === "disabled") return "disabled";
85
+ return params.groupAllowFrom.length > 0 ? "allowlist" : "open";
86
+ }
87
+ function resolveZalouserInboundSessionKey(params) {
88
+ if (params.isGroup) return params.route.sessionKey;
89
+ const directSessionKey = normalizeLowercaseStringOrEmpty(params.core.channel.routing.buildAgentSessionKey({
90
+ agentId: params.route.agentId,
91
+ channel: "zalouser",
92
+ accountId: params.route.accountId,
93
+ peer: {
94
+ kind: "direct",
95
+ id: params.senderId
96
+ },
97
+ dmScope: resolveZalouserDmSessionScope(params.config),
98
+ identityLinks: params.config.session?.identityLinks
99
+ }));
100
+ const legacySessionKey = normalizeLowercaseStringOrEmpty(params.core.channel.routing.buildAgentSessionKey({
101
+ agentId: params.route.agentId,
102
+ channel: "zalouser",
103
+ accountId: params.route.accountId,
104
+ peer: {
105
+ kind: "group",
106
+ id: params.senderId
107
+ }
108
+ }));
109
+ const hasDirectSession = params.core.channel.session.readSessionUpdatedAt({
110
+ storePath: params.storePath,
111
+ sessionKey: directSessionKey
112
+ }) !== void 0;
113
+ return params.core.channel.session.readSessionUpdatedAt({
114
+ storePath: params.storePath,
115
+ sessionKey: legacySessionKey
116
+ }) !== void 0 && !hasDirectSession ? legacySessionKey : directSessionKey;
117
+ }
118
+ function logVerbose(core, runtime, message) {
119
+ if (core.logging.shouldLogVerbose()) runtime.log(`[zalouser] ${message}`);
120
+ }
121
+ function resolveGroupRequireMention(params) {
122
+ const entry = findZalouserGroupEntry(params.groups ?? {}, buildZalouserGroupCandidates({
123
+ groupId: params.groupId,
124
+ groupName: params.groupName,
125
+ includeGroupIdAlias: true,
126
+ includeWildcard: true,
127
+ allowNameMatching: params.allowNameMatching
128
+ }));
129
+ if (typeof entry?.requireMention === "boolean") return entry.requireMention;
130
+ return true;
131
+ }
132
+ async function sendZalouserDeliveryAcks(params) {
133
+ await sendDeliveredZalouser({
134
+ profile: params.profile,
135
+ isGroup: params.isGroup,
136
+ message: params.message,
137
+ isSeen: true
138
+ });
139
+ await sendSeenZalouser({
140
+ profile: params.profile,
141
+ isGroup: params.isGroup,
142
+ message: params.message
143
+ });
144
+ }
145
+ async function processMessage(message, account, config, core, runtime, historyState, statusSink) {
146
+ const pairing = createChannelPairingController({
147
+ core,
148
+ channel: "zalouser",
149
+ accountId: account.accountId
150
+ });
151
+ const rawBody = message.content?.trim();
152
+ if (!rawBody) return;
153
+ const commandBody = message.commandContent?.trim() || rawBody;
154
+ const isGroup = message.isGroup;
155
+ const chatId = message.threadId;
156
+ const senderId = message.senderId?.trim();
157
+ if (!senderId) {
158
+ logVerbose(core, runtime, `zalouser: drop message ${chatId} (missing senderId)`);
159
+ return;
160
+ }
161
+ const senderName = message.senderName ?? "";
162
+ const configuredGroupName = message.groupName?.trim() || "";
163
+ const groupContext = isGroup && !configuredGroupName ? await resolveZaloGroupContext(account.profile, chatId).catch((err) => {
164
+ logVerbose(core, runtime, `zalouser: group context lookup failed for ${chatId}: ${String(err)}`);
165
+ return null;
166
+ }) : null;
167
+ const groupName = configuredGroupName || groupContext?.name?.trim() || "";
168
+ const groupMembers = groupContext?.members?.slice(0, 20).join(", ") || void 0;
169
+ if (message.eventMessage) try {
170
+ await sendZalouserDeliveryAcks({
171
+ profile: account.profile,
172
+ isGroup,
173
+ message: message.eventMessage
174
+ });
175
+ } catch (err) {
176
+ logVerbose(core, runtime, `zalouser: delivery/seen ack failed for ${chatId}: ${String(err)}`);
177
+ }
178
+ const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
179
+ const { groupPolicy, providerMissingFallbackApplied } = resolveOpenProviderRuntimeGroupPolicy({
180
+ providerConfigPresent: config.channels?.zalouser !== void 0,
181
+ groupPolicy: account.config.groupPolicy,
182
+ defaultGroupPolicy
183
+ });
184
+ warnMissingProviderGroupPolicyFallbackOnce({
185
+ providerMissingFallbackApplied,
186
+ providerKey: "zalouser",
187
+ accountId: account.accountId,
188
+ log: (entry) => logVerbose(core, runtime, entry)
189
+ });
190
+ const groups = account.config.groups ?? {};
191
+ const routeAllowlistConfigured = Object.keys(groups).length > 0;
192
+ const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
193
+ if (isGroup) {
194
+ const groupEntry = findZalouserGroupEntry(groups, buildZalouserGroupCandidates({
195
+ groupId: chatId,
196
+ groupName,
197
+ includeGroupIdAlias: true,
198
+ includeWildcard: true,
199
+ allowNameMatching
200
+ }));
201
+ const routeAccess = resolveZalouserRouteAccess({
202
+ groupPolicy,
203
+ configured: routeAllowlistConfigured,
204
+ matched: Boolean(groupEntry),
205
+ enabled: isZalouserGroupEntryAllowed(groupEntry)
206
+ });
207
+ if (!routeAccess.allowed) {
208
+ if (routeAccess.reason === "disabled") logVerbose(core, runtime, `zalouser: drop group ${chatId} (groupPolicy=disabled)`);
209
+ else if (routeAccess.reason === "empty_allowlist") logVerbose(core, runtime, `zalouser: drop group ${chatId} (groupPolicy=allowlist, no allowlist)`);
210
+ else if (routeAccess.reason === "route_not_allowlisted") logVerbose(core, runtime, `zalouser: drop group ${chatId} (not allowlisted)`);
211
+ else if (routeAccess.reason === "route_disabled") logVerbose(core, runtime, `zalouser: drop group ${chatId} (group disabled)`);
212
+ return;
213
+ }
214
+ }
215
+ const dmPolicy = account.config.dmPolicy ?? "pairing";
216
+ const configAllowFrom = normalizeStringEntries(account.config.allowFrom);
217
+ const configGroupAllowFrom = normalizeStringEntries(account.config.groupAllowFrom);
218
+ const senderGroupPolicy = routeAllowlistConfigured && configGroupAllowFrom.length === 0 ? groupPolicy : senderScopedZalouserGroupPolicy({
219
+ groupPolicy,
220
+ groupAllowFrom: configGroupAllowFrom
221
+ });
222
+ const shouldComputeCommandAuth = core.channel.commands.shouldComputeCommandAuthorized(commandBody, config);
223
+ const accessDecision = await resolveStableChannelMessageIngress({
224
+ channelId: "zalouser",
225
+ accountId: account.accountId,
226
+ identity: {
227
+ normalize: normalizeZalouserSender,
228
+ sensitivity: "pii",
229
+ entryIdPrefix: "zalouser-entry"
230
+ },
231
+ cfg: config,
232
+ readStoreAllowFrom: async () => await pairing.readAllowFromStore(),
233
+ subject: { stableId: senderId },
234
+ conversation: {
235
+ kind: isGroup ? "group" : "direct",
236
+ id: isGroup ? "group" : senderId
237
+ },
238
+ dmPolicy,
239
+ groupPolicy: senderGroupPolicy,
240
+ policy: { groupAllowFromFallbackToAllowFrom: false },
241
+ allowFrom: configAllowFrom,
242
+ groupAllowFrom: configGroupAllowFrom,
243
+ command: shouldComputeCommandAuth ? {
244
+ directGroupAllowFrom: "effective",
245
+ commandGroupAllowFromFallbackToAllowFrom: true
246
+ } : void 0
247
+ });
248
+ if (isGroup && accessDecision.senderAccess.decision !== "allow") {
249
+ if (accessDecision.senderAccess.reasonCode === "group_policy_empty_allowlist") logVerbose(core, runtime, "Blocked zalouser group message (no group allowlist)");
250
+ else if (accessDecision.senderAccess.reasonCode === "group_policy_not_allowlisted") logVerbose(core, runtime, `Blocked zalouser sender ${senderId} (not in groupAllowFrom/allowFrom)`);
251
+ return;
252
+ }
253
+ if (!isGroup && accessDecision.senderAccess.decision !== "allow") {
254
+ if (accessDecision.senderAccess.decision === "pairing") {
255
+ await pairing.issueChallenge({
256
+ senderId,
257
+ senderIdLine: `Your Zalo user id: ${senderId}`,
258
+ meta: { name: senderName || void 0 },
259
+ onCreated: () => {
260
+ logVerbose(core, runtime, `zalouser pairing request sender=${senderId}`);
261
+ },
262
+ sendPairingReply: async (text) => {
263
+ await sendMessageZalouser(chatId, text, { profile: account.profile });
264
+ statusSink?.({ lastOutboundAt: Date.now() });
265
+ },
266
+ onReplyError: (err) => {
267
+ logVerbose(core, runtime, `zalouser pairing reply failed for ${senderId}: ${String(err)}`);
268
+ }
269
+ });
270
+ return;
271
+ }
272
+ if (accessDecision.senderAccess.reasonCode === "dm_policy_disabled") logVerbose(core, runtime, `Blocked zalouser DM from ${senderId} (dmPolicy=disabled)`);
273
+ else logVerbose(core, runtime, `Blocked unauthorized zalouser sender ${senderId} (dmPolicy=${dmPolicy})`);
274
+ return;
275
+ }
276
+ const commandAuthorized = accessDecision.commandAccess.requested ? accessDecision.commandAccess.authorized : void 0;
277
+ const hasControlCommand = core.channel.commands.isControlCommandMessage(commandBody, config);
278
+ if (isGroup && hasControlCommand && commandAuthorized !== true) {
279
+ logVerbose(core, runtime, `zalouser: drop control command from unauthorized sender ${senderId}`);
280
+ return;
281
+ }
282
+ const peer = isGroup ? {
283
+ kind: "group",
284
+ id: chatId
285
+ } : {
286
+ kind: "direct",
287
+ id: senderId
288
+ };
289
+ const route = core.channel.routing.resolveAgentRoute({
290
+ cfg: config,
291
+ channel: "zalouser",
292
+ accountId: account.accountId,
293
+ peer: {
294
+ kind: peer.kind,
295
+ id: peer.id
296
+ }
297
+ });
298
+ const historyKey = isGroup ? route.sessionKey : void 0;
299
+ const channelHistory = createChannelHistoryWindow({ historyMap: historyState.groupHistories });
300
+ const requireMention = isGroup ? resolveGroupRequireMention({
301
+ groupId: chatId,
302
+ groupName,
303
+ groups,
304
+ allowNameMatching
305
+ }) : false;
306
+ const mentionRegexes = core.channel.mentions.buildMentionRegexes(config, route.agentId);
307
+ const explicitMention = {
308
+ hasAnyMention: message.hasAnyMention === true,
309
+ isExplicitlyMentioned: message.wasExplicitlyMentioned === true,
310
+ canResolveExplicit: message.canResolveExplicitMention === true
311
+ };
312
+ const wasMentioned = isGroup ? core.channel.mentions.matchesMentionWithExplicit({
313
+ text: rawBody,
314
+ mentionRegexes,
315
+ explicit: explicitMention
316
+ }) : true;
317
+ const canDetectMention = mentionRegexes.length > 0 || explicitMention.canResolveExplicit;
318
+ const mentionDecision = resolveInboundMentionDecision({
319
+ facts: {
320
+ canDetectMention,
321
+ wasMentioned,
322
+ hasAnyMention: explicitMention.hasAnyMention,
323
+ implicitMentionKinds: implicitMentionKindWhen("quoted_bot", message.implicitMention === true)
324
+ },
325
+ policy: {
326
+ isGroup,
327
+ requireMention,
328
+ allowTextCommands: core.channel.commands.shouldHandleTextCommands({
329
+ cfg: config,
330
+ surface: "zalouser"
331
+ }),
332
+ hasControlCommand,
333
+ commandAuthorized: commandAuthorized === true
334
+ }
335
+ });
336
+ if (isGroup && requireMention && !canDetectMention && !mentionDecision.effectiveWasMentioned) {
337
+ runtime.error?.(`[${account.accountId}] zalouser mention required but detection unavailable (missing mention regexes and bot self id); dropping group ${chatId}`);
338
+ return;
339
+ }
340
+ if (isGroup && mentionDecision.shouldSkip) {
341
+ channelHistory.record({
342
+ historyKey: historyKey ?? "",
343
+ limit: historyState.historyLimit,
344
+ entry: historyKey && rawBody ? {
345
+ sender: senderName || senderId,
346
+ body: rawBody,
347
+ timestamp: message.timestampMs,
348
+ messageId: resolveZalouserMessageSid({
349
+ msgId: message.msgId,
350
+ cliMsgId: message.cliMsgId,
351
+ fallback: `${message.timestampMs}`
352
+ })
353
+ } : null
354
+ });
355
+ logVerbose(core, runtime, `zalouser: skip group ${chatId} (mention required, not mentioned)`);
356
+ return;
357
+ }
358
+ const fromLabel = isGroup ? groupName || `group:${chatId}` : senderName || `user:${senderId}`;
359
+ const storePath = core.channel.session.resolveStorePath(config.session?.store, { agentId: route.agentId });
360
+ const inboundSessionKey = resolveZalouserInboundSessionKey({
361
+ core,
362
+ config,
363
+ route,
364
+ storePath,
365
+ isGroup,
366
+ senderId
367
+ });
368
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
369
+ const previousTimestamp = core.channel.session.readSessionUpdatedAt({
370
+ storePath,
371
+ sessionKey: inboundSessionKey
372
+ });
373
+ const body = core.channel.reply.formatAgentEnvelope({
374
+ channel: "Zalo Personal",
375
+ from: fromLabel,
376
+ timestamp: message.timestampMs,
377
+ previousTimestamp,
378
+ envelope: envelopeOptions,
379
+ body: rawBody
380
+ });
381
+ const combinedBody = isGroup && historyKey ? channelHistory.buildPendingContext({
382
+ historyKey,
383
+ limit: historyState.historyLimit,
384
+ currentMessage: body,
385
+ formatEntry: (entry) => core.channel.reply.formatAgentEnvelope({
386
+ channel: "Zalo Personal",
387
+ from: fromLabel,
388
+ timestamp: entry.timestamp,
389
+ envelope: envelopeOptions,
390
+ body: `${entry.sender}: ${entry.body}${entry.messageId ? ` [id:${entry.messageId}]` : ""}`
391
+ })
392
+ }) : body;
393
+ const inboundHistory = isGroup && historyKey && historyState.historyLimit > 0 ? channelHistory.buildInboundHistory({
394
+ historyKey,
395
+ limit: historyState.historyLimit
396
+ }) : void 0;
397
+ const normalizedTo = isGroup ? `zalouser:group:${chatId}` : `zalouser:${chatId}`;
398
+ const messageSid = resolveZalouserMessageSid({
399
+ msgId: message.msgId,
400
+ cliMsgId: message.cliMsgId,
401
+ fallback: `${message.timestampMs}`
402
+ });
403
+ const messageSidFull = formatZalouserMessageSidFull({
404
+ msgId: message.msgId,
405
+ cliMsgId: message.cliMsgId
406
+ });
407
+ const ctxPayload = core.channel.turn.buildContext({
408
+ channel: "zalouser",
409
+ accountId: route.accountId,
410
+ messageId: messageSid,
411
+ messageIdFull: messageSidFull,
412
+ timestamp: message.timestampMs,
413
+ from: isGroup ? `zalouser:group:${chatId}` : `zalouser:${senderId}`,
414
+ sender: {
415
+ id: senderId,
416
+ name: senderName || void 0
417
+ },
418
+ conversation: {
419
+ kind: isGroup ? "group" : "direct",
420
+ id: chatId,
421
+ label: fromLabel,
422
+ routePeer: {
423
+ kind: isGroup ? "group" : "direct",
424
+ id: chatId
425
+ }
426
+ },
427
+ route: {
428
+ agentId: route.agentId,
429
+ accountId: route.accountId,
430
+ routeSessionKey: route.sessionKey,
431
+ dispatchSessionKey: inboundSessionKey
432
+ },
433
+ reply: {
434
+ to: normalizedTo,
435
+ originatingTo: normalizedTo
436
+ },
437
+ message: {
438
+ body: combinedBody,
439
+ bodyForAgent: rawBody,
440
+ rawBody,
441
+ commandBody,
442
+ inboundHistory,
443
+ envelopeFrom: fromLabel
444
+ },
445
+ extra: {
446
+ BodyForCommands: commandBody,
447
+ GroupSubject: isGroup ? groupName || void 0 : void 0,
448
+ GroupChannel: isGroup ? groupName || void 0 : void 0,
449
+ GroupMembers: isGroup ? groupMembers : void 0,
450
+ WasMentioned: isGroup ? mentionDecision.effectiveWasMentioned : void 0,
451
+ CommandAuthorized: commandAuthorized
452
+ }
453
+ });
454
+ await core.channel.turn.runAssembled({
455
+ channel: "zalouser",
456
+ accountId: account.accountId,
457
+ cfg: config,
458
+ agentId: route.agentId,
459
+ routeSessionKey: route.sessionKey,
460
+ storePath,
461
+ ctxPayload,
462
+ recordInboundSession: core.channel.session.recordInboundSession,
463
+ dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
464
+ delivery: {
465
+ preparePayload: (payload) => {
466
+ if (payload.text === void 0) return payload;
467
+ return {
468
+ ...payload,
469
+ text: core.channel.text.convertMarkdownTables(payload.text, core.channel.text.resolveMarkdownTableMode({
470
+ cfg: config,
471
+ channel: "zalouser",
472
+ accountId: account.accountId
473
+ }))
474
+ };
475
+ },
476
+ durable: () => ({ to: normalizedTo }),
477
+ deliver: async (payload) => {
478
+ return await deliverZalouserReply({
479
+ payload,
480
+ profile: account.profile,
481
+ chatId,
482
+ isGroup,
483
+ runtime,
484
+ core,
485
+ config,
486
+ accountId: account.accountId,
487
+ tableMode: "off"
488
+ });
489
+ },
490
+ onDelivered: (_payload, _info, result) => {
491
+ if (result?.visibleReplySent !== false) statusSink?.({ lastOutboundAt: Date.now() });
492
+ },
493
+ onError: (err, info) => {
494
+ runtime.error(`[${account.accountId}] Zalouser ${info.kind} reply failed: ${String(err)}`);
495
+ }
496
+ },
497
+ replyPipeline: { typing: {
498
+ start: async () => {
499
+ await sendTypingZalouser(chatId, {
500
+ profile: account.profile,
501
+ isGroup
502
+ });
503
+ },
504
+ onStartError: (err) => {
505
+ runtime.error?.(`[${account.accountId}] zalouser typing start failed for ${chatId}: ${String(err)}`);
506
+ logVerbose(core, runtime, `zalouser typing failed for ${chatId}: ${String(err)}`);
507
+ }
508
+ } },
509
+ record: { onRecordError: (err) => {
510
+ runtime.error?.(`zalouser: failed updating session meta: ${String(err)}`);
511
+ } }
512
+ });
513
+ if (isGroup && historyKey) channelHistory.clear({
514
+ historyKey,
515
+ limit: historyState.historyLimit
516
+ });
517
+ }
518
+ async function deliverZalouserReply(params) {
519
+ const { payload, profile, chatId, isGroup, runtime, core, config, accountId } = params;
520
+ const tableMode = params.tableMode ?? "code";
521
+ let visibleReplySent = false;
522
+ const reply = resolveSendableOutboundReplyParts(payload, { text: core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode) });
523
+ const chunkMode = core.channel.text.resolveChunkMode(config, "zalouser", accountId);
524
+ const textChunkLimit = core.channel.text.resolveTextChunkLimit(config, "zalouser", accountId, { fallbackLimit: ZALOUSER_TEXT_LIMIT });
525
+ await deliverTextOrMediaReply({
526
+ payload,
527
+ text: reply.text,
528
+ sendText: async (chunk) => {
529
+ try {
530
+ await sendMessageZalouser(chatId, chunk, {
531
+ profile,
532
+ isGroup,
533
+ textMode: "markdown",
534
+ textChunkMode: chunkMode,
535
+ textChunkLimit
536
+ });
537
+ visibleReplySent = true;
538
+ } catch (err) {
539
+ runtime.error(`Zalouser message send failed: ${String(err)}`);
540
+ }
541
+ },
542
+ sendMedia: async ({ mediaUrl, caption }) => {
543
+ logVerbose(core, runtime, `Sending media to ${chatId}`);
544
+ await sendMessageZalouser(chatId, caption ?? "", {
545
+ profile,
546
+ mediaUrl,
547
+ isGroup,
548
+ textMode: "markdown",
549
+ textChunkMode: chunkMode,
550
+ textChunkLimit
551
+ });
552
+ visibleReplySent = true;
553
+ },
554
+ onMediaError: (error) => {
555
+ runtime.error(`Zalouser media send failed: ${error instanceof Error ? error.message : JSON.stringify(error)}`);
556
+ }
557
+ });
558
+ return { visibleReplySent };
559
+ }
560
+ async function monitorZalouserProvider(options) {
561
+ let { account, config } = options;
562
+ const { abortSignal, statusSink, runtime } = options;
563
+ const core = getZalouserRuntime();
564
+ const inboundQueue = new KeyedAsyncQueue();
565
+ const historyLimit = Math.max(0, account.config.historyLimit ?? config.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT);
566
+ const groupHistories = /* @__PURE__ */ new Map();
567
+ try {
568
+ const profile = account.profile;
569
+ const allowFromEntries = (account.config.allowFrom ?? []).map((entry) => normalizeZalouserAllowEntry(String(entry))).filter((entry) => entry && entry !== "*");
570
+ const groupAllowFromEntries = (account.config.groupAllowFrom ?? []).map((entry) => normalizeZalouserAllowEntry(String(entry))).filter((entry) => entry && entry !== "*");
571
+ const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
572
+ if (allowNameMatching && (allowFromEntries.length > 0 || groupAllowFromEntries.length > 0)) {
573
+ const byName = buildNameIndex(await listZaloFriends(profile), (friend) => friend.displayName);
574
+ if (allowFromEntries.length > 0) {
575
+ const { additions, mapping, unresolved } = resolveUserAllowlistEntries(allowFromEntries, byName);
576
+ const allowFrom = mergeAllowlist({
577
+ existing: account.config.allowFrom,
578
+ additions
579
+ });
580
+ account = {
581
+ ...account,
582
+ config: {
583
+ ...account.config,
584
+ allowFrom
585
+ }
586
+ };
587
+ summarizeMapping("zalouser users", mapping, unresolved, runtime);
588
+ }
589
+ if (groupAllowFromEntries.length > 0) {
590
+ const { additions, mapping, unresolved } = resolveUserAllowlistEntries(groupAllowFromEntries, byName);
591
+ const groupAllowFrom = mergeAllowlist({
592
+ existing: account.config.groupAllowFrom,
593
+ additions
594
+ });
595
+ account = {
596
+ ...account,
597
+ config: {
598
+ ...account.config,
599
+ groupAllowFrom
600
+ }
601
+ };
602
+ summarizeMapping("zalouser group users", mapping, unresolved, runtime);
603
+ }
604
+ }
605
+ const groupsConfig = account.config.groups ?? {};
606
+ const groupKeys = Object.keys(groupsConfig).filter((key) => key !== "*");
607
+ if (allowNameMatching && groupKeys.length > 0) {
608
+ const byName = buildNameIndex(await listZaloGroups(profile), (group) => group.name);
609
+ const mapping = [];
610
+ const unresolved = [];
611
+ const nextGroups = { ...groupsConfig };
612
+ for (const entry of groupKeys) {
613
+ const cleaned = normalizeZalouserAllowEntry(entry);
614
+ if (/^\d+$/.test(cleaned)) {
615
+ if (!nextGroups[cleaned]) nextGroups[cleaned] = groupsConfig[entry];
616
+ mapping.push(`${entry}→${cleaned}`);
617
+ continue;
618
+ }
619
+ const id = (byName.get(normalizeLowercaseStringOrEmpty(cleaned)) ?? [])[0]?.groupId;
620
+ if (id) {
621
+ if (!nextGroups[id]) nextGroups[id] = groupsConfig[entry];
622
+ mapping.push(`${entry}→${id}`);
623
+ } else unresolved.push(entry);
624
+ }
625
+ account = {
626
+ ...account,
627
+ config: {
628
+ ...account.config,
629
+ groups: nextGroups
630
+ }
631
+ };
632
+ summarizeMapping("zalouser groups", mapping, unresolved, runtime);
633
+ }
634
+ } catch (err) {
635
+ runtime.log?.(`zalouser resolve failed; using config entries. ${String(err)}`);
636
+ }
637
+ let listenerStop = null;
638
+ let stopped = false;
639
+ const stop = () => {
640
+ if (stopped) return;
641
+ stopped = true;
642
+ listenerStop?.();
643
+ listenerStop = null;
644
+ };
645
+ let settled = false;
646
+ const { promise: waitForExit, resolve: resolveRun, reject: rejectRun } = createDeferred();
647
+ const settleSuccess = () => {
648
+ if (settled) return;
649
+ settled = true;
650
+ stop();
651
+ resolveRun();
652
+ };
653
+ const settleFailure = (error) => {
654
+ if (settled) return;
655
+ settled = true;
656
+ stop();
657
+ rejectRun(error instanceof Error ? error : new Error(String(error)));
658
+ };
659
+ const onAbort = () => {
660
+ settleSuccess();
661
+ };
662
+ abortSignal.addEventListener("abort", onAbort, { once: true });
663
+ let listener;
664
+ try {
665
+ listener = await startZaloListener({
666
+ accountId: account.accountId,
667
+ profile: account.profile,
668
+ abortSignal,
669
+ onMessage: (msg) => {
670
+ if (stopped) return;
671
+ logVerbose(core, runtime, `[${account.accountId}] inbound message`);
672
+ statusSink?.({ lastInboundAt: Date.now() });
673
+ const queueKey = resolveInboundQueueKey(msg);
674
+ inboundQueue.enqueue(queueKey, async () => {
675
+ if (stopped || abortSignal.aborted) return;
676
+ await processMessage(msg, account, config, core, runtime, {
677
+ historyLimit,
678
+ groupHistories
679
+ }, statusSink);
680
+ }).catch((err) => {
681
+ runtime.error(`[${account.accountId}] Failed to process message: ${String(err)}`);
682
+ });
683
+ },
684
+ onError: (err) => {
685
+ if (stopped || abortSignal.aborted) return;
686
+ runtime.error(`[${account.accountId}] Zalo listener error: ${String(err)}`);
687
+ settleFailure(err);
688
+ }
689
+ });
690
+ } catch (error) {
691
+ abortSignal.removeEventListener("abort", onAbort);
692
+ throw error;
693
+ }
694
+ listenerStop = listener.stop;
695
+ if (stopped) {
696
+ listenerStop();
697
+ listenerStop = null;
698
+ }
699
+ if (abortSignal.aborted) settleSuccess();
700
+ try {
701
+ await waitForExit;
702
+ } finally {
703
+ abortSignal.removeEventListener("abort", onAbort);
704
+ }
705
+ return { stop };
706
+ }
707
+ //#endregion
708
+ export { monitorZalouserProvider };