diff --git a/src/web/auto-reply/monitor/on-message.ts b/src/web/auto-reply/monitor/on-message.ts index 7e260d49e..1769510df 100644 --- a/src/web/auto-reply/monitor/on-message.ts +++ b/src/web/auto-reply/monitor/on-message.ts @@ -94,6 +94,32 @@ export function createWebOnMessageHandler(params: { return; } + // Context-only messages: store for group history but never trigger a reply. + // This happens when a group message passes group allowlist checks for the group + // itself but the sender is not in groupAllowFrom. + if (msg.contextOnly && msg.chatType === "group") { + const sender = + msg.senderName && msg.senderE164 + ? `${msg.senderName} (${msg.senderE164})` + : (msg.senderName ?? msg.senderE164 ?? "Unknown"); + logVerbose(`Storing context-only group message from ${sender} in ${conversationId}`); + const { recordPendingHistoryEntryIfEnabled } = + await import("../../../auto-reply/reply/history.js"); + recordPendingHistoryEntryIfEnabled({ + historyMap: params.groupHistories, + historyKey: groupHistoryKey, + limit: params.groupHistoryLimit, + entry: { + sender, + body: msg.body, + timestamp: msg.timestamp, + id: msg.id, + senderJid: msg.senderJid, + }, + }); + return; + } + if (msg.chatType === "group") { const metaCtx = { From: msg.from, diff --git a/src/web/inbound/access-control.ts b/src/web/inbound/access-control.ts index 891712015..ebb2ec218 100644 --- a/src/web/inbound/access-control.ts +++ b/src/web/inbound/access-control.ts @@ -10,6 +10,8 @@ import { resolveWhatsAppAccount } from "../accounts.js"; export type InboundAccessControlResult = { allowed: boolean; + /** When true the message should be stored as pending group context even though it did not pass sender allowlists. */ + storeForContext: boolean; shouldMarkRead: boolean; isSelfChat: boolean; resolvedAccountId: string; @@ -84,6 +86,7 @@ export async function checkInboundAccessControl(params: { logVerbose("Blocked group message (groupPolicy: disabled)"); return { allowed: false, + storeForContext: false, shouldMarkRead: false, isSelfChat, resolvedAccountId: account.accountId, @@ -94,6 +97,7 @@ export async function checkInboundAccessControl(params: { logVerbose("Blocked group message (groupPolicy: allowlist, no groupAllowFrom)"); return { allowed: false, + storeForContext: false, shouldMarkRead: false, isSelfChat, resolvedAccountId: account.accountId, @@ -104,10 +108,11 @@ export async function checkInboundAccessControl(params: { (params.senderE164 != null && normalizedGroupAllowFrom.includes(params.senderE164)); if (!senderAllowed) { logVerbose( - `Blocked group message from ${params.senderE164 ?? "unknown sender"} (groupPolicy: allowlist)`, + `Group message from ${params.senderE164 ?? "unknown sender"} not in groupAllowFrom (storing for context)`, ); return { allowed: false, + storeForContext: true, shouldMarkRead: false, isSelfChat, resolvedAccountId: account.accountId, @@ -121,6 +126,7 @@ export async function checkInboundAccessControl(params: { logVerbose("Skipping outbound DM (fromMe); no pairing reply needed."); return { allowed: false, + storeForContext: false, shouldMarkRead: false, isSelfChat, resolvedAccountId: account.accountId, @@ -130,6 +136,7 @@ export async function checkInboundAccessControl(params: { logVerbose("Blocked dm (dmPolicy: disabled)"); return { allowed: false, + storeForContext: false, shouldMarkRead: false, isSelfChat, resolvedAccountId: account.accountId, @@ -172,6 +179,7 @@ export async function checkInboundAccessControl(params: { } return { allowed: false, + storeForContext: false, shouldMarkRead: false, isSelfChat, resolvedAccountId: account.accountId, @@ -182,6 +190,7 @@ export async function checkInboundAccessControl(params: { return { allowed: true, + storeForContext: false, shouldMarkRead: true, isSelfChat, resolvedAccountId: account.accountId, diff --git a/src/web/inbound/monitor.ts b/src/web/inbound/monitor.ts index 3633cbce9..328c3d2e2 100644 --- a/src/web/inbound/monitor.ts +++ b/src/web/inbound/monitor.ts @@ -190,9 +190,10 @@ export async function monitorWebInbox(options: { sock: { sendMessage: (jid, content) => sock.sendMessage(jid, content) }, remoteJid, }); - if (!access.allowed) continue; + if (!access.allowed && !access.storeForContext) continue; + const contextOnly = !access.allowed && access.storeForContext; - if (id && !access.isSelfChat && options.sendReadReceipts !== false) { + if (id && !contextOnly && !access.isSelfChat && options.sendReadReceipts !== false) { const participant = msg.key?.participant; try { await sock.readMessages([{ remoteJid, id, participant, fromMe: false }]); @@ -298,6 +299,7 @@ export async function monitorWebInbox(options: { sendMedia, mediaPath, mediaType, + contextOnly, }; try { const task = Promise.resolve(debouncer.enqueue(inboundMessage)); diff --git a/src/web/inbound/types.ts b/src/web/inbound/types.ts index 5f861fcc8..f19e43a06 100644 --- a/src/web/inbound/types.ts +++ b/src/web/inbound/types.ts @@ -39,4 +39,6 @@ export type WebInboundMessage = { mediaType?: string; mediaUrl?: string; wasMentioned?: boolean; + /** When true, message should only be stored for group context — not trigger a reply. */ + contextOnly?: boolean; };