@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,296 lines (1,274 loc) • 81.2 kB
JavaScript
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,