UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,296 lines (1,274 loc) 81.2 kB
import "./paths-Bp5uKvNR.js"; import { i as buildAgentMainSessionKey, l as normalizeAgentId, o as buildGroupHistoryKey, r as DEFAULT_MAIN_KEY } from "./session-key-CgcjHuX_.js"; import { A as normalizeE164, B as toWhatsappJid, K as logVerbose, N as resolveJidToE164, O as isSelfChatMode, R as sleep, Y as shouldLogVerbose, k as jidToE164, tt as getChildLogger, x as clamp } from "./registry-dD2_jBuv.js"; import "./agent-scope-DiocriBl.js"; import { r as defaultRuntime, t as createSubsystemLogger } from "./subsystem-CGx2ESmP.js"; import "./model-selection-Xwi9H_jm.js"; import { c as resolveIdentityNamePrefix, l as resolveMessagePrefix, t as createReplyPrefixOptions } from "./reply-prefix-DzTZTe-b.js"; import "./github-copilot-token-ttqQRqMA.js"; import { r as formatCliCommand } from "./env-MtDjQbRJ.js"; import "./tokens-B8d2DiXw.js"; import { C as shouldComputeCommandAuthorized, S as hasControlCommand, T as normalizeMentionText, _ as enqueueSystemEvent, a as resolveMentionGating, b as formatInboundEnvelope, c as recordPendingHistoryEntryIfEnabled, f as createDedupeCache, g as formatDurationPrecise, h as parseActivationCommand, i as buildPairingReply, l as shouldAckReactionForWhatsApp, m as normalizeGroupActivation, n as computeBackoff, o as DEFAULT_GROUP_HISTORY_LIMIT, p as getReplyFromConfig, r as sleepWithAbort, s as buildHistoryContextFromEntries, u as dispatchReplyWithBufferedBlockDispatcher, v as createInboundDebouncer, w as buildMentionRegexes, x as resolveEnvelopeFormatOptions, y as resolveInboundDebounceMs } from "./pi-embedded-B8wrmSGk.js"; import "./normalize-BHlVpmSR.js"; import { i as logWebSelfId, l as readWebSelfId, n as WA_WEB_AUTH_DIR, p as webAuthExists, r as getWebAuthAgeMs, s as pickWebChannel, t as resolveWhatsAppAccount } from "./accounts-GdZ63CoS.js"; import "./bindings-Du1RGVFM.js"; import "./send-4u1WKX_q.js"; import "./plugins-C2vFjSxX.js"; import "./send-Cph0By1Y.js"; import "./deliver-REt7-XBO.js"; import "./diagnostic-BPqN_C5K.js"; import "./diagnostic-session-state-Bu3E4Enn.js"; import "./accounts-CATMjQVH.js"; import { G as formatLocationText, J as readChannelAllowFromStore, K as toLocationContext, X as upsertChannelPairingRequest } from "./send-BP9sgDMR.js"; import "./fs-safe-GrTh3Ydq.js"; import "./pi-model-discovery-DaNAekda.js"; import "./message-channel-BxsLQyfw.js"; import { B as loadSessionStore, H as recordSessionMetaFromInbound, V as readSessionUpdatedAt, W as updateLastRoute, xt as resolveGroupSessionKey } from "./pi-embedded-helpers-DBcV85DN.js"; import { n as loadConfig } from "./config-42hNXHap.js"; import "./manifest-registry-DmjapxrQ.js"; import "./common-BLbt_nyn.js"; import "./chrome-DQPy_ANn.js"; import "./skills-Bu-sOzsy.js"; import "./redact-DAKoy-tb.js"; import "./errors-BxR4oB-s.js"; import "./ssrf-DH-Ltk62.js"; import { r as saveMediaBuffer } from "./store-D4zsSWrO.js"; import { h as resolveChannelGroupRequireMention, m as resolveChannelGroupPolicy } from "./thinking-CzOYfSuG.js"; import "./accounts-Bfw4rqq8.js"; import { s as resolveStorePath } from "./paths-COn9vdws.js"; import "./image-BoiA3W-b.js"; import "./manager-C95DsgDp.js"; import "./gemini-auth-CfU8Crwd.js"; import "./sqlite-CNpRo6JS.js"; import "./retry-CZWUKIdy.js"; import { c as resolveChunkMode, i as chunkMarkdownTextWithMode, l as resolveTextChunkLimit } from "./chunk-YQGDBsHo.js"; import { n as resolveMarkdownTableMode } from "./markdown-tables-CNKgA0te.js"; import { t as getAgentScopedMediaLocalRoots } from "./local-roots-BBJH5K5T.js"; import { a as loadWebMedia } from "./ir-CEvNPW62.js"; import "./render-B1VqYyvo.js"; import "./commands-registry-DjtY0igS.js"; import "./skill-commands-DRwS63hJ.js"; import { t as finalizeInboundContext } from "./inbound-context-C5ohLdkB.js"; import { p as registerUnhandledRejectionHandler } from "./runner-P6cvr5RA.js"; import "./fetch-BMa0enEg.js"; import "./send-DsqWGLtP.js"; import "./outbound-attachment-BYcoEf4w.js"; import "./send-DLFfBRRB.js"; import { n as resolveAgentRoute, t as buildAgentSessionKey } from "./resolve-route-DE3F8eS0.js"; import { n as recordChannelActivity } from "./channel-activity-BQwrvw91.js"; import { t as convertMarkdownTables } from "./tables-CUmmBJsI.js"; import "./proxy-BaSHbVlA.js"; import "./replies-7zTKqDtM.js"; import { r as setActiveWebListener } from "./active-listener-QaB3KeD2.js"; import { i as markdownToWhatsApp, n as sendMessageWhatsApp, r as sendReactionWhatsApp } from "./outbound-C9CzQgCm.js"; import { i as waitForWaConnection, n as formatError, r as getStatusCode, t as createWaSocket } from "./session-C-fi7xla.js"; import { t as loginWeb } from "./login-BgdehE-0.js"; import { randomUUID } from "node:crypto"; import { DisconnectReason, downloadMediaMessage, extractMessageContent, getContentType, isJidGroup, normalizeMessageContent } from "@whiskeysockets/baileys"; //#region src/web/auto-reply/constants.ts const DEFAULT_WEB_MEDIA_BYTES = 5 * 1024 * 1024; //#endregion //#region src/web/reconnect.ts const DEFAULT_HEARTBEAT_SECONDS = 60; const DEFAULT_RECONNECT_POLICY = { initialMs: 2e3, maxMs: 3e4, factor: 1.8, jitter: .25, maxAttempts: 12 }; function resolveHeartbeatSeconds(cfg, overrideSeconds) { const candidate = overrideSeconds ?? cfg.web?.heartbeatSeconds; if (typeof candidate === "number" && candidate > 0) return candidate; return DEFAULT_HEARTBEAT_SECONDS; } function resolveReconnectPolicy(cfg, overrides) { const reconnectOverrides = cfg.web?.reconnect ?? {}; const overrideConfig = overrides ?? {}; const merged = { ...DEFAULT_RECONNECT_POLICY, ...reconnectOverrides, ...overrideConfig }; merged.initialMs = Math.max(250, merged.initialMs); merged.maxMs = Math.max(merged.initialMs, merged.maxMs); merged.factor = clamp(merged.factor, 1.1, 10); merged.jitter = clamp(merged.jitter, 0, 1); merged.maxAttempts = Math.max(0, Math.floor(merged.maxAttempts)); return merged; } function newConnectionId() { return randomUUID(); } //#endregion //#region src/web/auto-reply/loggers.ts const whatsappLog = createSubsystemLogger("gateway/channels/whatsapp"); const whatsappInboundLog = whatsappLog.child("inbound"); const whatsappOutboundLog = whatsappLog.child("outbound"); const whatsappHeartbeatLog = whatsappLog.child("heartbeat"); //#endregion //#region src/web/auto-reply/util.ts function elide(text, limit = 400) { if (!text) return text; if (text.length <= limit) return text; return `${text.slice(0, limit)}… (truncated ${text.length - limit} chars)`; } function isLikelyWhatsAppCryptoError(reason) { const formatReason = (value) => { if (value == null) return ""; if (typeof value === "string") return value; if (value instanceof Error) return `${value.message}\n${value.stack ?? ""}`; if (typeof value === "object") try { return JSON.stringify(value); } catch { return Object.prototype.toString.call(value); } if (typeof value === "number") return String(value); if (typeof value === "boolean") return String(value); if (typeof value === "bigint") return String(value); if (typeof value === "symbol") return value.description ?? value.toString(); if (typeof value === "function") return value.name ? `[function ${value.name}]` : "[function]"; return Object.prototype.toString.call(value); }; const haystack = (reason instanceof Error ? `${reason.message}\n${reason.stack ?? ""}` : formatReason(reason)).toLowerCase(); if (!(haystack.includes("unsupported state or unable to authenticate data") || haystack.includes("bad mac"))) return false; return haystack.includes("@whiskeysockets/baileys") || haystack.includes("baileys") || haystack.includes("noise-handler") || haystack.includes("aesdecryptgcm"); } //#endregion //#region src/cli/wait.ts function waitForever() { setInterval(() => {}, 1e6).unref(); return new Promise(() => {}); } //#endregion //#region src/web/inbound/dedupe.ts const recentInboundMessages = createDedupeCache({ ttlMs: 20 * 6e4, maxSize: 5e3 }); function isRecentInboundMessage(key) { return recentInboundMessages.check(key); } //#endregion //#region src/web/vcard.ts const ALLOWED_VCARD_KEYS = new Set([ "FN", "N", "TEL" ]); function parseVcard(vcard) { if (!vcard) return { phones: [] }; const lines = vcard.split(/\r?\n/); let nameFromN; let nameFromFn; const phones = []; for (const rawLine of lines) { const line = rawLine.trim(); if (!line) continue; const colonIndex = line.indexOf(":"); if (colonIndex === -1) continue; const key = line.slice(0, colonIndex).toUpperCase(); const rawValue = line.slice(colonIndex + 1).trim(); if (!rawValue) continue; const baseKey = normalizeVcardKey(key); if (!baseKey || !ALLOWED_VCARD_KEYS.has(baseKey)) continue; const value = cleanVcardValue(rawValue); if (!value) continue; if (baseKey === "FN" && !nameFromFn) { nameFromFn = normalizeVcardName(value); continue; } if (baseKey === "N" && !nameFromN) { nameFromN = normalizeVcardName(value); continue; } if (baseKey === "TEL") { const phone = normalizeVcardPhone(value); if (phone) phones.push(phone); } } return { name: nameFromFn ?? nameFromN, phones }; } function normalizeVcardKey(key) { const [primary] = key.split(";"); if (!primary) return; const segments = primary.split("."); return segments[segments.length - 1] || void 0; } function cleanVcardValue(value) { return value.replace(/\\n/gi, " ").replace(/\\,/g, ",").replace(/\\;/g, ";").trim(); } function normalizeVcardName(value) { return value.replace(/;/g, " ").replace(/\s+/g, " ").trim(); } function normalizeVcardPhone(value) { const trimmed = value.trim(); if (!trimmed) return ""; if (trimmed.toLowerCase().startsWith("tel:")) return trimmed.slice(4).trim(); return trimmed; } //#endregion //#region src/web/inbound/extract.ts function unwrapMessage$1(message) { return normalizeMessageContent(message); } function extractContextInfo(message) { if (!message) return; const contentType = getContentType(message); const candidate = contentType ? message[contentType] : void 0; const contextInfo = candidate && typeof candidate === "object" && "contextInfo" in candidate ? candidate.contextInfo : void 0; if (contextInfo) return contextInfo; const fallback = message.extendedTextMessage?.contextInfo ?? message.imageMessage?.contextInfo ?? message.videoMessage?.contextInfo ?? message.documentMessage?.contextInfo ?? message.audioMessage?.contextInfo ?? message.stickerMessage?.contextInfo ?? message.buttonsResponseMessage?.contextInfo ?? message.listResponseMessage?.contextInfo ?? message.templateButtonReplyMessage?.contextInfo ?? message.interactiveResponseMessage?.contextInfo ?? message.buttonsMessage?.contextInfo ?? message.listMessage?.contextInfo; if (fallback) return fallback; for (const value of Object.values(message)) { if (!value || typeof value !== "object") continue; if (!("contextInfo" in value)) continue; const candidateContext = value.contextInfo; if (candidateContext) return candidateContext; } } function extractMentionedJids(rawMessage) { const message = unwrapMessage$1(rawMessage); if (!message) return; const flattened = [ message.extendedTextMessage?.contextInfo?.mentionedJid, message.extendedTextMessage?.contextInfo?.quotedMessage?.extendedTextMessage?.contextInfo?.mentionedJid, message.imageMessage?.contextInfo?.mentionedJid, message.videoMessage?.contextInfo?.mentionedJid, message.documentMessage?.contextInfo?.mentionedJid, message.audioMessage?.contextInfo?.mentionedJid, message.stickerMessage?.contextInfo?.mentionedJid, message.buttonsResponseMessage?.contextInfo?.mentionedJid, message.listResponseMessage?.contextInfo?.mentionedJid ].flatMap((arr) => arr ?? []).filter(Boolean); if (flattened.length === 0) return; return Array.from(new Set(flattened)); } function extractText(rawMessage) { const message = unwrapMessage$1(rawMessage); if (!message) return; const extracted = extractMessageContent(message); const candidates = [message, extracted && extracted !== message ? extracted : void 0]; for (const candidate of candidates) { if (!candidate) continue; if (typeof candidate.conversation === "string" && candidate.conversation.trim()) return candidate.conversation.trim(); const extended = candidate.extendedTextMessage?.text; if (extended?.trim()) return extended.trim(); const caption = candidate.imageMessage?.caption ?? candidate.videoMessage?.caption ?? candidate.documentMessage?.caption; if (caption?.trim()) return caption.trim(); } const contactPlaceholder = extractContactPlaceholder(message) ?? (extracted && extracted !== message ? extractContactPlaceholder(extracted) : void 0); if (contactPlaceholder) return contactPlaceholder; } function extractMediaPlaceholder(rawMessage) { const message = unwrapMessage$1(rawMessage); if (!message) return; if (message.imageMessage) return "<media:image>"; if (message.videoMessage) return "<media:video>"; if (message.audioMessage) return "<media:audio>"; if (message.documentMessage) return "<media:document>"; if (message.stickerMessage) return "<media:sticker>"; } function extractContactPlaceholder(rawMessage) { const message = unwrapMessage$1(rawMessage); if (!message) return; const contact = message.contactMessage ?? void 0; if (contact) { const { name, phones } = describeContact({ displayName: contact.displayName, vcard: contact.vcard }); return formatContactPlaceholder(name, phones); } const contactsArray = message.contactsArrayMessage?.contacts ?? void 0; if (!contactsArray || contactsArray.length === 0) return; return formatContactsPlaceholder(contactsArray.map((entry) => describeContact({ displayName: entry.displayName, vcard: entry.vcard })).map((entry) => formatContactLabel(entry.name, entry.phones)).filter((value) => Boolean(value)), contactsArray.length); } function describeContact(input) { const displayName = (input.displayName ?? "").trim(); const parsed = parseVcard(input.vcard ?? void 0); return { name: displayName || parsed.name, phones: parsed.phones }; } function formatContactPlaceholder(name, phones) { const label = formatContactLabel(name, phones); if (!label) return "<contact>"; return `<contact: ${label}>`; } function formatContactsPlaceholder(labels, total) { const cleaned = labels.map((label) => label.trim()).filter(Boolean); if (cleaned.length === 0) return `<contacts: ${total} ${total === 1 ? "contact" : "contacts"}>`; const remaining = Math.max(total - cleaned.length, 0); const suffix = remaining > 0 ? ` +${remaining} more` : ""; return `<contacts: ${cleaned.join(", ")}${suffix}>`; } function formatContactLabel(name, phones) { const parts = [name, formatPhoneList(phones)].filter((value) => Boolean(value)); if (parts.length === 0) return; return parts.join(", "); } function formatPhoneList(phones) { const cleaned = phones?.map((phone) => phone.trim()).filter(Boolean) ?? []; if (cleaned.length === 0) return; const { shown, remaining } = summarizeList(cleaned, cleaned.length, 1); const [primary] = shown; if (!primary) return; if (remaining === 0) return primary; return `${primary} (+${remaining} more)`; } function summarizeList(values, total, maxShown) { const shown = values.slice(0, maxShown); return { shown, remaining: Math.max(total - shown.length, 0) }; } function extractLocationData(rawMessage) { const message = unwrapMessage$1(rawMessage); if (!message) return null; const live = message.liveLocationMessage ?? void 0; if (live) { const latitudeRaw = live.degreesLatitude; const longitudeRaw = live.degreesLongitude; if (latitudeRaw != null && longitudeRaw != null) { const latitude = Number(latitudeRaw); const longitude = Number(longitudeRaw); if (Number.isFinite(latitude) && Number.isFinite(longitude)) return { latitude, longitude, accuracy: live.accuracyInMeters ?? void 0, caption: live.caption ?? void 0, source: "live", isLive: true }; } } const location = message.locationMessage ?? void 0; if (location) { const latitudeRaw = location.degreesLatitude; const longitudeRaw = location.degreesLongitude; if (latitudeRaw != null && longitudeRaw != null) { const latitude = Number(latitudeRaw); const longitude = Number(longitudeRaw); if (Number.isFinite(latitude) && Number.isFinite(longitude)) { const isLive = Boolean(location.isLive); return { latitude, longitude, accuracy: location.accuracyInMeters ?? void 0, name: location.name ?? void 0, address: location.address ?? void 0, caption: location.comment ?? void 0, source: isLive ? "live" : location.name || location.address ? "place" : "pin", isLive }; } } } return null; } function describeReplyContext(rawMessage) { const message = unwrapMessage$1(rawMessage); if (!message) return null; const contextInfo = extractContextInfo(message); const quoted = normalizeMessageContent(contextInfo?.quotedMessage); if (!quoted) return null; const location = extractLocationData(quoted); const locationText = location ? formatLocationText(location) : void 0; let body = [extractText(quoted), locationText].filter(Boolean).join("\n").trim(); if (!body) body = extractMediaPlaceholder(quoted); if (!body) { const quotedType = quoted ? getContentType(quoted) : void 0; logVerbose(`Quoted message missing extractable body${quotedType ? ` (type ${quotedType})` : ""}`); return null; } const senderJid = contextInfo?.participant ?? void 0; const senderE164 = senderJid ? jidToE164(senderJid) ?? senderJid : void 0; const sender = senderE164 ?? "unknown sender"; return { id: contextInfo?.stanzaId ? String(contextInfo.stanzaId) : void 0, body, sender, senderJid, senderE164 }; } //#endregion //#region src/web/inbound/access-control.ts const PAIRING_REPLY_HISTORY_GRACE_MS = 3e4; async function checkInboundAccessControl(params) { const cfg = loadConfig(); const account = resolveWhatsAppAccount({ cfg, accountId: params.accountId }); const dmPolicy = account.dmPolicy ?? "pairing"; const configuredAllowFrom = account.allowFrom; const storeAllowFrom = await readChannelAllowFromStore("whatsapp", process.env, account.accountId).catch(() => []); const combinedAllowFrom = Array.from(new Set([...configuredAllowFrom ?? [], ...storeAllowFrom])); const defaultAllowFrom = combinedAllowFrom.length === 0 && params.selfE164 ? [params.selfE164] : void 0; const allowFrom = combinedAllowFrom.length > 0 ? combinedAllowFrom : defaultAllowFrom; const groupAllowFrom = account.groupAllowFrom ?? (configuredAllowFrom && configuredAllowFrom.length > 0 ? configuredAllowFrom : void 0); const isSamePhone = params.from === params.selfE164; const isSelfChat = isSelfChatMode(params.selfE164, configuredAllowFrom); const pairingGraceMs = typeof params.pairingGraceMs === "number" && params.pairingGraceMs > 0 ? params.pairingGraceMs : PAIRING_REPLY_HISTORY_GRACE_MS; const suppressPairingReply = typeof params.connectedAtMs === "number" && typeof params.messageTimestampMs === "number" && params.messageTimestampMs < params.connectedAtMs - pairingGraceMs; const dmHasWildcard = allowFrom?.includes("*") ?? false; const normalizedAllowFrom = allowFrom && allowFrom.length > 0 ? allowFrom.filter((entry) => entry !== "*").map(normalizeE164) : []; const groupHasWildcard = groupAllowFrom?.includes("*") ?? false; const normalizedGroupAllowFrom = groupAllowFrom && groupAllowFrom.length > 0 ? groupAllowFrom.filter((entry) => entry !== "*").map(normalizeE164) : []; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const groupPolicy = account.groupPolicy ?? defaultGroupPolicy ?? "open"; if (params.group && groupPolicy === "disabled") { logVerbose("Blocked group message (groupPolicy: disabled)"); return { allowed: false, shouldMarkRead: false, isSelfChat, resolvedAccountId: account.accountId }; } if (params.group && groupPolicy === "allowlist") { if (!groupAllowFrom || groupAllowFrom.length === 0) { logVerbose("Blocked group message (groupPolicy: allowlist, no groupAllowFrom)"); return { allowed: false, shouldMarkRead: false, isSelfChat, resolvedAccountId: account.accountId }; } if (!(groupHasWildcard || params.senderE164 != null && normalizedGroupAllowFrom.includes(params.senderE164))) { logVerbose(`Blocked group message from ${params.senderE164 ?? "unknown sender"} (groupPolicy: allowlist)`); return { allowed: false, shouldMarkRead: false, isSelfChat, resolvedAccountId: account.accountId }; } } if (!params.group) { if (params.isFromMe && !isSamePhone) { logVerbose("Skipping outbound DM (fromMe); no pairing reply needed."); return { allowed: false, shouldMarkRead: false, isSelfChat, resolvedAccountId: account.accountId }; } if (dmPolicy === "disabled") { logVerbose("Blocked dm (dmPolicy: disabled)"); return { allowed: false, shouldMarkRead: false, isSelfChat, resolvedAccountId: account.accountId }; } if (dmPolicy !== "open" && !isSamePhone) { const candidate = params.from; if (!(dmHasWildcard || normalizedAllowFrom.length > 0 && normalizedAllowFrom.includes(candidate))) { if (dmPolicy === "pairing") if (suppressPairingReply) logVerbose(`Skipping pairing reply for historical DM from ${candidate}.`); else { const { code, created } = await upsertChannelPairingRequest({ channel: "whatsapp", id: candidate, accountId: account.accountId, meta: { name: (params.pushName ?? "").trim() || void 0 } }); if (created) { logVerbose(`whatsapp pairing request sender=${candidate} name=${params.pushName ?? "unknown"}`); try { await params.sock.sendMessage(params.remoteJid, { text: buildPairingReply({ channel: "whatsapp", idLine: `Your WhatsApp phone number: ${candidate}`, code }) }); } catch (err) { logVerbose(`whatsapp pairing reply failed for ${candidate}: ${String(err)}`); } } } else logVerbose(`Blocked unauthorized sender ${candidate} (dmPolicy=${dmPolicy})`); return { allowed: false, shouldMarkRead: false, isSelfChat, resolvedAccountId: account.accountId }; } } } return { allowed: true, shouldMarkRead: true, isSelfChat, resolvedAccountId: account.accountId }; } //#endregion //#region src/web/inbound/media.ts function unwrapMessage(message) { return normalizeMessageContent(message); } /** * Resolve the MIME type for an inbound media message. * Falls back to WhatsApp's standard formats when Baileys omits the MIME. */ function resolveMediaMimetype(message) { const explicit = message.imageMessage?.mimetype ?? message.videoMessage?.mimetype ?? message.documentMessage?.mimetype ?? message.audioMessage?.mimetype ?? message.stickerMessage?.mimetype ?? void 0; if (explicit) return explicit; if (message.audioMessage) return "audio/ogg; codecs=opus"; if (message.imageMessage) return "image/jpeg"; if (message.videoMessage) return "video/mp4"; if (message.stickerMessage) return "image/webp"; } async function downloadInboundMedia(msg, sock) { const message = unwrapMessage(msg.message); if (!message) return; const mimetype = resolveMediaMimetype(message); const fileName = message.documentMessage?.fileName ?? void 0; if (!message.imageMessage && !message.videoMessage && !message.documentMessage && !message.audioMessage && !message.stickerMessage) return; try { return { buffer: await downloadMediaMessage(msg, "buffer", {}, { reuploadRequest: sock.updateMediaMessage, logger: sock.logger }), mimetype, fileName }; } catch (err) { logVerbose(`downloadMediaMessage failed: ${String(err)}`); return; } } //#endregion //#region src/web/inbound/send-api.ts function recordWhatsAppOutbound(accountId) { recordChannelActivity({ channel: "whatsapp", accountId, direction: "outbound" }); } function resolveOutboundMessageId(result) { return typeof result === "object" && result && "key" in result ? String(result.key?.id ?? "unknown") : "unknown"; } function createWebSendApi(params) { return { sendMessage: async (to, text, mediaBuffer, mediaType, sendOptions) => { const jid = toWhatsappJid(to); let payload; if (mediaBuffer && mediaType) if (mediaType.startsWith("image/")) payload = { image: mediaBuffer, caption: text || void 0, mimetype: mediaType }; else if (mediaType.startsWith("audio/")) payload = { audio: mediaBuffer, ptt: true, mimetype: mediaType }; else if (mediaType.startsWith("video/")) { const gifPlayback = sendOptions?.gifPlayback; payload = { video: mediaBuffer, caption: text || void 0, mimetype: mediaType, ...gifPlayback ? { gifPlayback: true } : {} }; } else payload = { document: mediaBuffer, fileName: sendOptions?.fileName?.trim() || "file", caption: text || void 0, mimetype: mediaType }; else payload = { text }; const result = await params.sock.sendMessage(jid, payload); recordWhatsAppOutbound(sendOptions?.accountId ?? params.defaultAccountId); return { messageId: resolveOutboundMessageId(result) }; }, sendPoll: async (to, poll) => { const jid = toWhatsappJid(to); const result = await params.sock.sendMessage(jid, { poll: { name: poll.question, values: poll.options, selectableCount: poll.maxSelections ?? 1 } }); recordWhatsAppOutbound(params.defaultAccountId); return { messageId: resolveOutboundMessageId(result) }; }, sendReaction: async (chatJid, messageId, emoji, fromMe, participant) => { const jid = toWhatsappJid(chatJid); await params.sock.sendMessage(jid, { react: { text: emoji, key: { remoteJid: jid, id: messageId, fromMe, participant: participant ? toWhatsappJid(participant) : void 0 } } }); }, sendComposingTo: async (to) => { const jid = toWhatsappJid(to); await params.sock.sendPresenceUpdate("composing", jid); } }; } //#endregion //#region src/web/inbound/monitor.ts async function monitorWebInbox(options) { const inboundLogger = getChildLogger({ module: "web-inbound" }); const inboundConsoleLog = createSubsystemLogger("gateway/channels/whatsapp").child("inbound"); const sock = await createWaSocket(false, options.verbose, { authDir: options.authDir }); await waitForWaConnection(sock); const connectedAtMs = Date.now(); let onCloseResolve = null; const onClose = new Promise((resolve) => { onCloseResolve = resolve; }); const resolveClose = (reason) => { if (!onCloseResolve) return; const resolver = onCloseResolve; onCloseResolve = null; resolver(reason); }; try { await sock.sendPresenceUpdate("available"); if (shouldLogVerbose()) logVerbose("Sent global 'available' presence on connect"); } catch (err) { logVerbose(`Failed to send 'available' presence on connect: ${String(err)}`); } const selfJid = sock.user?.id; const selfE164 = selfJid ? jidToE164(selfJid) : null; const debouncer = createInboundDebouncer({ debounceMs: options.debounceMs ?? 0, buildKey: (msg) => { const senderKey = msg.chatType === "group" ? msg.senderJid ?? msg.senderE164 ?? msg.senderName ?? msg.from : msg.from; if (!senderKey) return null; const conversationKey = msg.chatType === "group" ? msg.chatId : msg.from; return `${msg.accountId}:${conversationKey}:${senderKey}`; }, shouldDebounce: options.shouldDebounce, onFlush: async (entries) => { const last = entries.at(-1); if (!last) return; if (entries.length === 1) { await options.onMessage(last); return; } const mentioned = /* @__PURE__ */ new Set(); for (const entry of entries) for (const jid of entry.mentionedJids ?? []) mentioned.add(jid); const combinedBody = entries.map((entry) => entry.body).filter(Boolean).join("\n"); const combinedMessage = { ...last, body: combinedBody, mentionedJids: mentioned.size > 0 ? Array.from(mentioned) : void 0 }; await options.onMessage(combinedMessage); }, onError: (err) => { inboundLogger.error({ error: String(err) }, "failed handling inbound web message"); inboundConsoleLog.error(`Failed handling inbound web message: ${String(err)}`); } }); const groupMetaCache = /* @__PURE__ */ new Map(); const GROUP_META_TTL_MS = 300 * 1e3; const lidLookup = sock.signalRepository?.lidMapping; const resolveInboundJid = async (jid) => resolveJidToE164(jid, { authDir: options.authDir, lidLookup }); const getGroupMeta = async (jid) => { const cached = groupMetaCache.get(jid); if (cached && cached.expires > Date.now()) return cached; try { const meta = await sock.groupMetadata(jid); const participants = (await Promise.all(meta.participants?.map(async (p) => { return await resolveInboundJid(p.id) ?? p.id; }) ?? [])).filter(Boolean) ?? []; const entry = { subject: meta.subject, participants, expires: Date.now() + GROUP_META_TTL_MS }; groupMetaCache.set(jid, entry); return entry; } catch (err) { logVerbose(`Failed to fetch group metadata for ${jid}: ${String(err)}`); return { expires: Date.now() + GROUP_META_TTL_MS }; } }; const handleMessagesUpsert = async (upsert) => { if (upsert.type !== "notify" && upsert.type !== "append") return; for (const msg of upsert.messages ?? []) { recordChannelActivity({ channel: "whatsapp", accountId: options.accountId, direction: "inbound" }); const id = msg.key?.id ?? void 0; const remoteJid = msg.key?.remoteJid; if (!remoteJid) continue; if (remoteJid.endsWith("@status") || remoteJid.endsWith("@broadcast")) continue; const group = isJidGroup(remoteJid) === true; if (id) { if (isRecentInboundMessage(`${options.accountId}:${remoteJid}:${id}`)) continue; } const participantJid = msg.key?.participant ?? void 0; const from = group ? remoteJid : await resolveInboundJid(remoteJid); if (!from) continue; const senderE164 = group ? participantJid ? await resolveInboundJid(participantJid) : null : from; let groupSubject; let groupParticipants; if (group) { const meta = await getGroupMeta(remoteJid); groupSubject = meta.subject; groupParticipants = meta.participants; } const messageTimestampMs = msg.messageTimestamp ? Number(msg.messageTimestamp) * 1e3 : void 0; const access = await checkInboundAccessControl({ accountId: options.accountId, from, selfE164, senderE164, group, pushName: msg.pushName ?? void 0, isFromMe: Boolean(msg.key?.fromMe), messageTimestampMs, connectedAtMs, sock: { sendMessage: (jid, content) => sock.sendMessage(jid, content) }, remoteJid }); if (!access.allowed) continue; if (id && !access.isSelfChat && options.sendReadReceipts !== false) { const participant = msg.key?.participant; try { await sock.readMessages([{ remoteJid, id, participant, fromMe: false }]); if (shouldLogVerbose()) logVerbose(`Marked message ${id} as read for ${remoteJid}${participant ? ` (participant ${participant})` : ""}`); } catch (err) { logVerbose(`Failed to mark message ${id} read: ${String(err)}`); } } else if (id && access.isSelfChat && shouldLogVerbose()) logVerbose(`Self-chat mode: skipping read receipt for ${id}`); if (upsert.type === "append") continue; const location = extractLocationData(msg.message ?? void 0); const locationText = location ? formatLocationText(location) : void 0; let body = extractText(msg.message ?? void 0); if (locationText) body = [body, locationText].filter(Boolean).join("\n").trim(); if (!body) { body = extractMediaPlaceholder(msg.message ?? void 0); if (!body) continue; } const replyContext = describeReplyContext(msg.message); let mediaPath; let mediaType; let mediaFileName; try { const inboundMedia = await downloadInboundMedia(msg, sock); if (inboundMedia) { const maxBytes = (typeof options.mediaMaxMb === "number" && options.mediaMaxMb > 0 ? options.mediaMaxMb : 50) * 1024 * 1024; mediaPath = (await saveMediaBuffer(inboundMedia.buffer, inboundMedia.mimetype, "inbound", maxBytes, inboundMedia.fileName)).path; mediaType = inboundMedia.mimetype; mediaFileName = inboundMedia.fileName; } } catch (err) { logVerbose(`Inbound media download failed: ${String(err)}`); } const chatJid = remoteJid; const sendComposing = async () => { try { await sock.sendPresenceUpdate("composing", chatJid); } catch (err) { logVerbose(`Presence update failed: ${String(err)}`); } }; const reply = async (text) => { await sock.sendMessage(chatJid, { text }); }; const sendMedia = async (payload) => { await sock.sendMessage(chatJid, payload); }; const timestamp = messageTimestampMs; const mentionedJids = extractMentionedJids(msg.message); const senderName = msg.pushName ?? void 0; inboundLogger.info({ from, to: selfE164 ?? "me", body, mediaPath, mediaType, mediaFileName, timestamp }, "inbound message"); const inboundMessage = { id, from, conversationId: from, to: selfE164 ?? "me", accountId: access.resolvedAccountId, body, pushName: senderName, timestamp, chatType: group ? "group" : "direct", chatId: remoteJid, senderJid: participantJid, senderE164: senderE164 ?? void 0, senderName, replyToId: replyContext?.id, replyToBody: replyContext?.body, replyToSender: replyContext?.sender, replyToSenderJid: replyContext?.senderJid, replyToSenderE164: replyContext?.senderE164, groupSubject, groupParticipants, mentionedJids: mentionedJids ?? void 0, selfJid, selfE164, location: location ?? void 0, sendComposing, reply, sendMedia, mediaPath, mediaType, mediaFileName }; try { Promise.resolve(debouncer.enqueue(inboundMessage)).catch((err) => { inboundLogger.error({ error: String(err) }, "failed handling inbound web message"); inboundConsoleLog.error(`Failed handling inbound web message: ${String(err)}`); }); } catch (err) { inboundLogger.error({ error: String(err) }, "failed handling inbound web message"); inboundConsoleLog.error(`Failed handling inbound web message: ${String(err)}`); } } }; sock.ev.on("messages.upsert", handleMessagesUpsert); const handleConnectionUpdate = (update) => { try { if (update.connection === "close") { const status = getStatusCode(update.lastDisconnect?.error); resolveClose({ status, isLoggedOut: status === DisconnectReason.loggedOut, error: update.lastDisconnect?.error }); } } catch (err) { inboundLogger.error({ error: String(err) }, "connection.update handler error"); resolveClose({ status: void 0, isLoggedOut: false, error: err }); } }; sock.ev.on("connection.update", handleConnectionUpdate); return { close: async () => { try { const ev = sock.ev; const messagesUpsertHandler = handleMessagesUpsert; const connectionUpdateHandler = handleConnectionUpdate; if (typeof ev.off === "function") { ev.off("messages.upsert", messagesUpsertHandler); ev.off("connection.update", connectionUpdateHandler); } else if (typeof ev.removeListener === "function") { ev.removeListener("messages.upsert", messagesUpsertHandler); ev.removeListener("connection.update", connectionUpdateHandler); } sock.ws?.close(); } catch (err) { logVerbose(`Socket close failed: ${String(err)}`); } }, onClose, signalClose: (reason) => { resolveClose(reason ?? { status: void 0, isLoggedOut: false, error: "closed" }); }, ...createWebSendApi({ sock: { sendMessage: (jid, content) => sock.sendMessage(jid, content), sendPresenceUpdate: (presence, jid) => sock.sendPresenceUpdate(presence, jid) }, defaultAccountId: options.accountId }) }; } //#endregion //#region src/web/auto-reply/mentions.ts function buildMentionConfig(cfg, agentId) { return { mentionRegexes: buildMentionRegexes(cfg, agentId), allowFrom: cfg.channels?.whatsapp?.allowFrom }; } function resolveMentionTargets(msg, authDir) { const jidOptions = authDir ? { authDir } : void 0; return { normalizedMentions: msg.mentionedJids?.length ? msg.mentionedJids.map((jid) => jidToE164(jid, jidOptions) ?? jid).filter(Boolean) : [], selfE164: msg.selfE164 ?? (msg.selfJid ? jidToE164(msg.selfJid, jidOptions) : null), selfJid: msg.selfJid ? msg.selfJid.replace(/:\\d+/, "") : null }; } function isBotMentionedFromTargets(msg, mentionCfg, targets) { const clean = (text) => normalizeMentionText(text); const isSelfChat = isSelfChatMode(targets.selfE164, mentionCfg.allowFrom); const hasMentions = (msg.mentionedJids?.length ?? 0) > 0; if (hasMentions && !isSelfChat) { if (targets.selfE164 && targets.normalizedMentions.includes(targets.selfE164)) return true; if (targets.selfJid) { if (targets.normalizedMentions.includes(targets.selfJid)) return true; } return false; } else if (hasMentions && isSelfChat) {} const bodyClean = clean(msg.body); if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean))) return true; if (targets.selfE164) { const selfDigits = targets.selfE164.replace(/\D/g, ""); if (selfDigits) { if (bodyClean.replace(/[^\d]/g, "").includes(selfDigits)) return true; const bodyNoSpace = msg.body.replace(/[\s-]/g, ""); if (new RegExp(`\\+?${selfDigits}`, "i").test(bodyNoSpace)) return true; } } return false; } function debugMention(msg, mentionCfg, authDir) { const mentionTargets = resolveMentionTargets(msg, authDir); return { wasMentioned: isBotMentionedFromTargets(msg, mentionCfg, mentionTargets), details: { from: msg.from, body: msg.body, bodyClean: normalizeMentionText(msg.body), mentionedJids: msg.mentionedJids ?? null, normalizedMentionedJids: mentionTargets.normalizedMentions.length ? mentionTargets.normalizedMentions : null, selfJid: msg.selfJid ?? null, selfJidBare: mentionTargets.selfJid, selfE164: msg.selfE164 ?? null, resolvedSelfE164: mentionTargets.selfE164 } }; } function resolveOwnerList(mentionCfg, selfE164) { const allowFrom = mentionCfg.allowFrom; return (Array.isArray(allowFrom) && allowFrom.length > 0 ? allowFrom : selfE164 ? [selfE164] : []).filter((entry) => Boolean(entry && entry !== "*")).map((entry) => normalizeE164(entry)).filter((entry) => Boolean(entry)); } //#endregion //#region src/web/auto-reply/monitor/echo.ts function createEchoTracker(params) { const recentlySent = /* @__PURE__ */ new Set(); const maxItems = Math.max(1, params.maxItems ?? 100); const buildCombinedKey = (p) => `combined:${p.sessionKey}:${p.combinedBody}`; const trim = () => { while (recentlySent.size > maxItems) { const firstKey = recentlySent.values().next().value; if (!firstKey) break; recentlySent.delete(firstKey); } }; const rememberText = (text, opts) => { if (!text) return; recentlySent.add(text); if (opts.combinedBody && opts.combinedBodySessionKey) recentlySent.add(buildCombinedKey({ sessionKey: opts.combinedBodySessionKey, combinedBody: opts.combinedBody })); if (opts.logVerboseMessage) params.logVerbose?.(`Added to echo detection set (size now: ${recentlySent.size}): ${text.substring(0, 50)}...`); trim(); }; return { rememberText, has: (key) => recentlySent.has(key), forget: (key) => { recentlySent.delete(key); }, buildCombinedKey }; } //#endregion //#region src/web/auto-reply/monitor/broadcast.ts async function maybeBroadcastMessage(params) { const broadcastAgents = params.cfg.broadcast?.[params.peerId]; if (!broadcastAgents || !Array.isArray(broadcastAgents)) return false; if (broadcastAgents.length === 0) return false; const strategy = params.cfg.broadcast?.strategy || "parallel"; whatsappInboundLog.info(`Broadcasting message to ${broadcastAgents.length} agents (${strategy})`); const agentIds = params.cfg.agents?.list?.map((agent) => normalizeAgentId(agent.id)); const hasKnownAgents = (agentIds?.length ?? 0) > 0; const groupHistorySnapshot = params.msg.chatType === "group" ? params.groupHistories.get(params.groupHistoryKey) ?? [] : void 0; const processForAgent = async (agentId) => { const normalizedAgentId = normalizeAgentId(agentId); if (hasKnownAgents && !agentIds?.includes(normalizedAgentId)) { whatsappInboundLog.warn(`Broadcast agent ${agentId} not found in agents.list; skipping`); return false; } const agentRoute = { ...params.route, agentId: normalizedAgentId, sessionKey: buildAgentSessionKey({ agentId: normalizedAgentId, channel: "whatsapp", accountId: params.route.accountId, peer: { kind: params.msg.chatType === "group" ? "group" : "direct", id: params.peerId }, dmScope: params.cfg.session?.dmScope, identityLinks: params.cfg.session?.identityLinks }), mainSessionKey: buildAgentMainSessionKey({ agentId: normalizedAgentId, mainKey: DEFAULT_MAIN_KEY }) }; try { return await params.processMessage(params.msg, agentRoute, params.groupHistoryKey, { groupHistory: groupHistorySnapshot, suppressGroupHistoryClear: true }); } catch (err) { whatsappInboundLog.error(`Broadcast agent ${agentId} failed: ${formatError(err)}`); return false; } }; if (strategy === "sequential") for (const agentId of broadcastAgents) await processForAgent(agentId); else await Promise.allSettled(broadcastAgents.map(processForAgent)); if (params.msg.chatType === "group") params.groupHistories.set(params.groupHistoryKey, []); return true; } //#endregion //#region src/web/auto-reply/monitor/commands.ts function stripMentionsForCommand(text, mentionRegexes, selfE164) { let result = text; for (const re of mentionRegexes) result = result.replace(re, " "); if (selfE164) { const digits = selfE164.replace(/\D/g, ""); if (digits) { const pattern = new RegExp(`\\+?${digits}`, "g"); result = result.replace(pattern, " "); } } return result.replace(/\s+/g, " ").trim(); } //#endregion //#region src/web/auto-reply/monitor/group-activation.ts function resolveGroupPolicyFor(cfg, conversationId) { const groupId = resolveGroupSessionKey({ From: conversationId, ChatType: "group", Provider: "whatsapp" })?.id; return resolveChannelGroupPolicy({ cfg, channel: "whatsapp", groupId: groupId ?? conversationId }); } function resolveGroupRequireMentionFor(cfg, conversationId) { const groupId = resolveGroupSessionKey({ From: conversationId, ChatType: "group", Provider: "whatsapp" })?.id; return resolveChannelGroupRequireMention({ cfg, channel: "whatsapp", groupId: groupId ?? conversationId }); } function resolveGroupActivationFor(params) { const entry = loadSessionStore(resolveStorePath(params.cfg.session?.store, { agentId: params.agentId }))[params.sessionKey]; const defaultActivation = !resolveGroupRequireMentionFor(params.cfg, params.conversationId) ? "always" : "mention"; return normalizeGroupActivation(entry?.groupActivation) ?? defaultActivation; } //#endregion //#region src/web/auto-reply/monitor/group-members.ts function noteGroupMember(groupMemberNames, conversationId, e164, name) { if (!e164 || !name) return; const key = normalizeE164(e164) ?? e164; if (!key) return; let roster = groupMemberNames.get(conversationId); if (!roster) { roster = /* @__PURE__ */ new Map(); groupMemberNames.set(conversationId, roster); } roster.set(key, name); } function formatGroupMembers(params) { const { participants, roster, fallbackE164 } = params; const seen = /* @__PURE__ */ new Set(); const ordered = []; if (participants?.length) for (const entry of participants) { if (!entry) continue; const normalized = normalizeE164(entry) ?? entry; if (!normalized || seen.has(normalized)) continue; seen.add(normalized); ordered.push(normalized); } if (roster) for (const entry of roster.keys()) { const normalized = normalizeE164(entry) ?? entry; if (!normalized || seen.has(normalized)) continue; seen.add(normalized); ordered.push(normalized); } if (ordered.length === 0 && fallbackE164) { const normalized = normalizeE164(fallbackE164) ?? fallbackE164; if (normalized) ordered.push(normalized); } if (ordered.length === 0) return; return ordered.map((entry) => { const name = roster?.get(entry); return name ? `${name} (${entry})` : entry; }).join(", "); } //#endregion //#region src/web/auto-reply/monitor/group-gating.ts function isOwnerSender(baseMentionConfig, msg) { const sender = normalizeE164(msg.senderE164 ?? ""); if (!sender) return false; return resolveOwnerList(baseMentionConfig, msg.selfE164 ?? void 0).includes(sender); } function recordPendingGroupHistoryEntry(params) { const sender = params.msg.senderName && params.msg.senderE164 ? `${params.msg.senderName} (${params.msg.senderE164})` : params.msg.senderName ?? params.msg.senderE164 ?? "Unknown"; recordPendingHistoryEntryIfEnabled({ historyMap: params.groupHistories, historyKey: params.groupHistoryKey, limit: params.groupHistoryLimit, entry: { sender, body: params.msg.body, timestamp: params.msg.timestamp, id: params.msg.id, senderJid: params.msg.senderJid } }); } function applyGroupGating(params) { const groupPolicy = resolveGroupPolicyFor(params.cfg, params.conversationId); if (groupPolicy.allowlistEnabled && !groupPolicy.allowed) { params.logVerbose(`Skipping group message ${params.conversationId} (not in allowlist)`); return { shouldProcess: false }; } noteGroupMember(params.groupMemberNames, params.groupHistoryKey, params.msg.senderE164, params.msg.senderName); const mentionConfig = buildMentionConfig(params.cfg, params.agentId); const commandBody = stripMentionsForCommand(params.msg.body, mentionConfig.mentionRegexes, params.msg.selfE164); const activationCommand = parseActivationCommand(commandBody); const owner = isOwnerSender(params.baseMentionConfig, params.msg); const shouldBypassMention = owner && hasControlCommand(commandBody, params.cfg); if (activationCommand.hasCommand && !owner) { params.logVerbose(`Ignoring /activation from non-owner in group ${params.conversationId}`); recordPendingGroupHistoryEntry({ msg: params.msg, groupHistories: params.groupHistories, groupHistoryKey: params.groupHistoryKey, groupHistoryLimit: params.groupHistoryLimit }); return { shouldProcess: false }; } const mentionDebug = debugMention(params.msg, mentionConfig, params.authDir); params.replyLogger.debug({ conversationId: params.conversationId, wasMentioned: mentionDebug.wasMentioned, ...mentionDebug.details }, "group mention debug"); const wasMentioned = mentionDebug.wasMentioned; const requireMention = resolveGroupActivationFor({ cfg: params.cfg, agentId: params.agentId, sessionKey: params.sessionKey, conversationId: params.conversationId }) !== "always"; const selfJid = params.msg.selfJid?.replace(/:\\d+/, ""); const replySenderJid = params.msg.replyToSenderJid?.replace(/:\\d+/, ""); const selfE164 = params.msg.selfE164 ? normalizeE164(params.msg.selfE164) : null; const replySenderE164 = params.msg.replyToSenderE164 ? normalizeE164(params.msg.replyToSenderE164) : null; const mentionGate = resolveMentionGating({ requireMention, canDetectMention: true, wasMentioned, implicitMention: Boolean(selfJid && replySenderJid && selfJid === replySenderJid || selfE164 && replySenderE164 && selfE164 === replySenderE164), shouldBypassMention }); params.msg.wasMentioned = mentionGate.effectiveWasMentioned; if (!shouldBypassMention && requireMention && mentionGate.shouldSkip) { params.logVerbose(`Group message stored for context (no mention detected) in ${params.conversationId}: ${params.msg.body}`); recordPendingGroupHistoryEntry({ msg: params.msg, groupHistories: params.groupHistories, groupHistoryKey: params.groupHistoryKey, groupHistoryLimit: params.groupHistoryLimit }); return { shouldProcess: false }; } return { shouldProcess: true }; } //#endregion //#region src/web/auto-reply/monitor/last-route.ts function trackBackgroundTask(backgroundTasks, task) { backgroundTasks.add(task); task.finally(() => { backgroundTasks.delete(task); }); } function updateLastRouteInBackground(params) { const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.storeAgentId }); const task = updateLastRoute({ storePath, sessionKey: params.sessionKey, deliveryContext: { channel: params.channel, to: params.to, accountId: params.accountId }, ctx: params.ctx }).catch((err) => { params.warn({ error: formatError(err), storePath, sessionKey: params.sessionKey, to: params.to }, "failed updating last route"); }); trackBackgroundTask(params.backgroundTasks, task); } //#endregion //#region src/web/auto-reply/monitor/peer.ts function resolvePeerId(msg) { if (msg.chatType === "group") return msg.conversationId ?? msg.from; if (msg.senderE164) return normalizeE164(msg.senderE164) ?? msg.senderE164; if (msg.from.includes("@")) return jidToE164(msg.from) ?? msg.from; return normalizeE164(msg.from) ?? msg.from; } //#endregion //#region src/web/auto-reply/deliver-reply.ts async function deliverWebReply(params) { const { replyResult, msg, maxMediaBytes, textLimit, replyLogger, connectionId, skipLog } = params; const replyStarted = Date.now(); const tableMode = params.tableMode ?? "code"; const chunkMode = params.chunkMode ?? "length"; const textChunks = chunkMarkdownTextWithMode(markdownToWhatsApp(convertMarkdownTables(replyResult.text || "", tableMode)), textLimit, chunkMode); const mediaList = replyResult.mediaUrls?.length ? replyResult.mediaUrls : replyResult.mediaUrl ? [replyResult.mediaUrl] : []; const sendWithRetry = async (fn, label,