import type { getReplyFromConfig } from "../../../auto-reply/reply.js"; import type { MsgContext } from "../../../auto-reply/templating.js"; import type { loadConfig } from "../../../config/config.js"; import type { MentionConfig } from "../mentions.js"; import type { WebInboundMsg } from "../types.js"; import type { EchoTracker } from "./echo.js"; import type { GroupHistoryEntry } from "./group-gating.js"; import { logVerbose } from "../../../globals.js"; import { resolveAgentRoute } from "../../../routing/resolve-route.js"; import { buildGroupHistoryKey } from "../../../routing/session-key.js"; import { normalizeE164 } from "../../../utils.js"; import { maybeBroadcastMessage } from "./broadcast.js"; import { applyGroupGating } from "./group-gating.js"; import { updateLastRouteInBackground } from "./last-route.js"; import { resolvePeerId } from "./peer.js"; import { processMessage } from "./process-message.js"; export function createWebOnMessageHandler(params: { cfg: ReturnType; verbose: boolean; connectionId: string; maxMediaBytes: number; groupHistoryLimit: number; groupHistories: Map; groupMemberNames: Map>; echoTracker: EchoTracker; backgroundTasks: Set>; replyResolver: typeof getReplyFromConfig; replyLogger: ReturnType<(typeof import("../../../logging.js"))["getChildLogger"]>; baseMentionConfig: MentionConfig; account: { authDir?: string; accountId?: string }; }) { const processForRoute = async ( msg: WebInboundMsg, route: ReturnType, groupHistoryKey: string, opts?: { groupHistory?: GroupHistoryEntry[]; suppressGroupHistoryClear?: boolean; }, ) => processMessage({ cfg: params.cfg, msg, route, groupHistoryKey, groupHistories: params.groupHistories, groupMemberNames: params.groupMemberNames, connectionId: params.connectionId, verbose: params.verbose, maxMediaBytes: params.maxMediaBytes, replyResolver: params.replyResolver, replyLogger: params.replyLogger, backgroundTasks: params.backgroundTasks, rememberSentText: params.echoTracker.rememberText, echoHas: params.echoTracker.has, echoForget: params.echoTracker.forget, buildCombinedEchoKey: params.echoTracker.buildCombinedKey, groupHistory: opts?.groupHistory, suppressGroupHistoryClear: opts?.suppressGroupHistoryClear, }); return async (msg: WebInboundMsg) => { const conversationId = msg.conversationId ?? msg.from; const peerId = resolvePeerId(msg); const route = resolveAgentRoute({ cfg: params.cfg, channel: "whatsapp", accountId: msg.accountId, peer: { kind: msg.chatType === "group" ? "group" : "dm", id: peerId, }, }); const groupHistoryKey = msg.chatType === "group" ? buildGroupHistoryKey({ channel: "whatsapp", accountId: route.accountId, peerKind: "group", peerId, }) : route.sessionKey; // Same-phone mode logging retained if (msg.from === msg.to) { logVerbose(`📱 Same-phone mode detected (from === to: ${msg.from})`); } // Skip if this is a message we just sent (echo detection) if (params.echoTracker.has(msg.body)) { logVerbose("Skipping auto-reply: detected echo (message matches recently sent text)"); params.echoTracker.forget(msg.body); return; } if (msg.chatType === "group") { const metaCtx = { From: msg.from, To: msg.to, SessionKey: route.sessionKey, AccountId: route.accountId, ChatType: msg.chatType, ConversationLabel: conversationId, GroupSubject: msg.groupSubject, SenderName: msg.senderName, SenderId: msg.senderJid?.trim() || msg.senderE164, SenderE164: msg.senderE164, Provider: "whatsapp", Surface: "whatsapp", OriginatingChannel: "whatsapp", OriginatingTo: conversationId, } satisfies MsgContext; updateLastRouteInBackground({ cfg: params.cfg, backgroundTasks: params.backgroundTasks, storeAgentId: route.agentId, sessionKey: route.sessionKey, channel: "whatsapp", to: conversationId, accountId: route.accountId, ctx: metaCtx, warn: params.replyLogger.warn.bind(params.replyLogger), }); const gating = applyGroupGating({ cfg: params.cfg, msg, conversationId, groupHistoryKey, agentId: route.agentId, sessionKey: route.sessionKey, baseMentionConfig: params.baseMentionConfig, authDir: params.account.authDir, groupHistories: params.groupHistories, groupHistoryLimit: params.groupHistoryLimit, groupMemberNames: params.groupMemberNames, logVerbose, replyLogger: params.replyLogger, }); if (!gating.shouldProcess) { return; } } else { // Ensure `peerId` for DMs is stable and stored as E.164 when possible. if (!msg.senderE164 && peerId && peerId.startsWith("+")) { msg.senderE164 = normalizeE164(peerId) ?? msg.senderE164; } } // Broadcast groups: when we'd reply anyway, run multiple agents. // Does not bypass group mention/activation gating above. if ( await maybeBroadcastMessage({ cfg: params.cfg, msg, peerId, route, groupHistoryKey, groupHistories: params.groupHistories, processMessage: processForRoute, }) ) { return; } await processForRoute(msg, route, groupHistoryKey); }; }