This commit is contained in:
adam91holt 2026-01-30 17:05:54 +05:30 committed by GitHub
commit 87085a8324
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 67 additions and 2 deletions

View File

@ -47,6 +47,12 @@ export type WhatsAppConfig = {
* - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom * - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom
*/ */
groupPolicy?: GroupPolicy; groupPolicy?: GroupPolicy;
/**
* When true, messages from group members not in groupAllowFrom are still
* stored as pending context (visible in "[Chat messages since your last reply]")
* even though they cannot trigger a reply. Default: false (old behavior: drop entirely).
*/
groupContextFromAll?: boolean;
/** Max group messages to keep as history context (0 disables). */ /** Max group messages to keep as history context (0 disables). */
historyLimit?: number; historyLimit?: number;
/** Max DM turns to keep as history context. */ /** Max DM turns to keep as history context. */
@ -118,6 +124,8 @@ export type WhatsAppAccountConfig = {
allowFrom?: string[]; allowFrom?: string[];
groupAllowFrom?: string[]; groupAllowFrom?: string[];
groupPolicy?: GroupPolicy; groupPolicy?: GroupPolicy;
/** Store context from non-allowlisted group senders (default: false). */
groupContextFromAll?: boolean;
/** Max group messages to keep as history context (0 disables). */ /** Max group messages to keep as history context (0 disables). */
historyLimit?: number; historyLimit?: number;
/** Max DM turns to keep as history context. */ /** Max DM turns to keep as history context. */

View File

@ -28,6 +28,7 @@ export const WhatsAppAccountSchema = z
allowFrom: z.array(z.string()).optional(), allowFrom: z.array(z.string()).optional(),
groupAllowFrom: z.array(z.string()).optional(), groupAllowFrom: z.array(z.string()).optional(),
groupPolicy: GroupPolicySchema.optional().default("allowlist"), groupPolicy: GroupPolicySchema.optional().default("allowlist"),
groupContextFromAll: z.boolean().optional().default(false),
historyLimit: z.number().int().min(0).optional(), historyLimit: z.number().int().min(0).optional(),
dmHistoryLimit: z.number().int().min(0).optional(), dmHistoryLimit: z.number().int().min(0).optional(),
dms: z.record(z.string(), DmConfigSchema.optional()).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
@ -85,6 +86,7 @@ export const WhatsAppConfigSchema = z
allowFrom: z.array(z.string()).optional(), allowFrom: z.array(z.string()).optional(),
groupAllowFrom: z.array(z.string()).optional(), groupAllowFrom: z.array(z.string()).optional(),
groupPolicy: GroupPolicySchema.optional().default("allowlist"), groupPolicy: GroupPolicySchema.optional().default("allowlist"),
groupContextFromAll: z.boolean().optional().default(false),
historyLimit: z.number().int().min(0).optional(), historyLimit: z.number().int().min(0).optional(),
dmHistoryLimit: z.number().int().min(0).optional(), dmHistoryLimit: z.number().int().min(0).optional(),
dms: z.record(z.string(), DmConfigSchema.optional()).optional(), dms: z.record(z.string(), DmConfigSchema.optional()).optional(),

View File

@ -20,6 +20,7 @@ export type ResolvedWhatsAppAccount = {
allowFrom?: string[]; allowFrom?: string[];
groupAllowFrom?: string[]; groupAllowFrom?: string[];
groupPolicy?: GroupPolicy; groupPolicy?: GroupPolicy;
groupContextFromAll?: boolean;
dmPolicy?: DmPolicy; dmPolicy?: DmPolicy;
textChunkLimit?: number; textChunkLimit?: number;
chunkMode?: "length" | "newline"; chunkMode?: "length" | "newline";
@ -150,6 +151,7 @@ export function resolveWhatsAppAccount(params: {
allowFrom: accountCfg?.allowFrom ?? rootCfg?.allowFrom, allowFrom: accountCfg?.allowFrom ?? rootCfg?.allowFrom,
groupAllowFrom: accountCfg?.groupAllowFrom ?? rootCfg?.groupAllowFrom, groupAllowFrom: accountCfg?.groupAllowFrom ?? rootCfg?.groupAllowFrom,
groupPolicy: accountCfg?.groupPolicy ?? rootCfg?.groupPolicy, groupPolicy: accountCfg?.groupPolicy ?? rootCfg?.groupPolicy,
groupContextFromAll: accountCfg?.groupContextFromAll ?? rootCfg?.groupContextFromAll,
textChunkLimit: accountCfg?.textChunkLimit ?? rootCfg?.textChunkLimit, textChunkLimit: accountCfg?.textChunkLimit ?? rootCfg?.textChunkLimit,
chunkMode: accountCfg?.chunkMode ?? rootCfg?.chunkMode, chunkMode: accountCfg?.chunkMode ?? rootCfg?.chunkMode,
mediaMaxMb: accountCfg?.mediaMaxMb ?? rootCfg?.mediaMaxMb, mediaMaxMb: accountCfg?.mediaMaxMb ?? rootCfg?.mediaMaxMb,

View File

@ -94,6 +94,32 @@ export function createWebOnMessageHandler(params: {
return; 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") { if (msg.chatType === "group") {
const metaCtx = { const metaCtx = {
From: msg.from, From: msg.from,

View File

@ -10,6 +10,8 @@ import { resolveWhatsAppAccount } from "../accounts.js";
export type InboundAccessControlResult = { export type InboundAccessControlResult = {
allowed: boolean; 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; shouldMarkRead: boolean;
isSelfChat: boolean; isSelfChat: boolean;
resolvedAccountId: string; resolvedAccountId: string;
@ -51,6 +53,8 @@ export async function checkInboundAccessControl(params: {
const groupAllowFrom = const groupAllowFrom =
account.groupAllowFrom ?? account.groupAllowFrom ??
(configuredAllowFrom && configuredAllowFrom.length > 0 ? configuredAllowFrom : undefined); (configuredAllowFrom && configuredAllowFrom.length > 0 ? configuredAllowFrom : undefined);
const groupContextFromAll =
account.groupContextFromAll ?? cfg.channels?.whatsapp?.groupContextFromAll ?? false;
const isSamePhone = params.from === params.selfE164; const isSamePhone = params.from === params.selfE164;
const isSelfChat = isSelfChatMode(params.selfE164, configuredAllowFrom); const isSelfChat = isSelfChatMode(params.selfE164, configuredAllowFrom);
const pairingGraceMs = const pairingGraceMs =
@ -84,6 +88,7 @@ export async function checkInboundAccessControl(params: {
logVerbose("Blocked group message (groupPolicy: disabled)"); logVerbose("Blocked group message (groupPolicy: disabled)");
return { return {
allowed: false, allowed: false,
storeForContext: false,
shouldMarkRead: false, shouldMarkRead: false,
isSelfChat, isSelfChat,
resolvedAccountId: account.accountId, resolvedAccountId: account.accountId,
@ -94,6 +99,7 @@ export async function checkInboundAccessControl(params: {
logVerbose("Blocked group message (groupPolicy: allowlist, no groupAllowFrom)"); logVerbose("Blocked group message (groupPolicy: allowlist, no groupAllowFrom)");
return { return {
allowed: false, allowed: false,
storeForContext: false,
shouldMarkRead: false, shouldMarkRead: false,
isSelfChat, isSelfChat,
resolvedAccountId: account.accountId, resolvedAccountId: account.accountId,
@ -103,11 +109,24 @@ export async function checkInboundAccessControl(params: {
groupHasWildcard || groupHasWildcard ||
(params.senderE164 != null && normalizedGroupAllowFrom.includes(params.senderE164)); (params.senderE164 != null && normalizedGroupAllowFrom.includes(params.senderE164));
if (!senderAllowed) { if (!senderAllowed) {
if (groupContextFromAll) {
logVerbose(
`Group message from ${params.senderE164 ?? "unknown sender"} not in groupAllowFrom (storing for context)`,
);
return {
allowed: false,
storeForContext: true,
shouldMarkRead: false,
isSelfChat,
resolvedAccountId: account.accountId,
};
}
logVerbose( logVerbose(
`Blocked group message from ${params.senderE164 ?? "unknown sender"} (groupPolicy: allowlist)`, `Blocked group message from ${params.senderE164 ?? "unknown sender"} (groupPolicy: allowlist)`,
); );
return { return {
allowed: false, allowed: false,
storeForContext: false,
shouldMarkRead: false, shouldMarkRead: false,
isSelfChat, isSelfChat,
resolvedAccountId: account.accountId, resolvedAccountId: account.accountId,
@ -121,6 +140,7 @@ export async function checkInboundAccessControl(params: {
logVerbose("Skipping outbound DM (fromMe); no pairing reply needed."); logVerbose("Skipping outbound DM (fromMe); no pairing reply needed.");
return { return {
allowed: false, allowed: false,
storeForContext: false,
shouldMarkRead: false, shouldMarkRead: false,
isSelfChat, isSelfChat,
resolvedAccountId: account.accountId, resolvedAccountId: account.accountId,
@ -130,6 +150,7 @@ export async function checkInboundAccessControl(params: {
logVerbose("Blocked dm (dmPolicy: disabled)"); logVerbose("Blocked dm (dmPolicy: disabled)");
return { return {
allowed: false, allowed: false,
storeForContext: false,
shouldMarkRead: false, shouldMarkRead: false,
isSelfChat, isSelfChat,
resolvedAccountId: account.accountId, resolvedAccountId: account.accountId,
@ -172,6 +193,7 @@ export async function checkInboundAccessControl(params: {
} }
return { return {
allowed: false, allowed: false,
storeForContext: false,
shouldMarkRead: false, shouldMarkRead: false,
isSelfChat, isSelfChat,
resolvedAccountId: account.accountId, resolvedAccountId: account.accountId,
@ -182,6 +204,7 @@ export async function checkInboundAccessControl(params: {
return { return {
allowed: true, allowed: true,
storeForContext: false,
shouldMarkRead: true, shouldMarkRead: true,
isSelfChat, isSelfChat,
resolvedAccountId: account.accountId, resolvedAccountId: account.accountId,

View File

@ -190,9 +190,10 @@ export async function monitorWebInbox(options: {
sock: { sendMessage: (jid, content) => sock.sendMessage(jid, content) }, sock: { sendMessage: (jid, content) => sock.sendMessage(jid, content) },
remoteJid, 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; const participant = msg.key?.participant;
try { try {
await sock.readMessages([{ remoteJid, id, participant, fromMe: false }]); await sock.readMessages([{ remoteJid, id, participant, fromMe: false }]);
@ -298,6 +299,7 @@ export async function monitorWebInbox(options: {
sendMedia, sendMedia,
mediaPath, mediaPath,
mediaType, mediaType,
contextOnly,
}; };
try { try {
const task = Promise.resolve(debouncer.enqueue(inboundMessage)); const task = Promise.resolve(debouncer.enqueue(inboundMessage));

View File

@ -39,4 +39,6 @@ export type WebInboundMessage = {
mediaType?: string; mediaType?: string;
mediaUrl?: string; mediaUrl?: string;
wasMentioned?: boolean; wasMentioned?: boolean;
/** When true, message should only be stored for group context — not trigger a reply. */
contextOnly?: boolean;
}; };