From 7a683a4b628a02f302d3df822a38143ccdfa54fb Mon Sep 17 00:00:00 2001 From: "Christian A. Rodriguez" Date: Tue, 13 Jan 2026 22:01:55 -0400 Subject: [PATCH] feat(whatsapp): add sendReadReceipts config option Add option to disable automatic read receipts for WhatsApp messages. When set to false, Clawdbot will not mark messages as read (blue ticks). Closes #344 Changes: - Add sendReadReceipts to WhatsAppConfig and WhatsAppAccountConfig types - Add sendReadReceipts to zod schemas for validation - Add sendReadReceipts to ResolvedWhatsAppAccount with fallback chain - Pass sendReadReceipts through to monitorWebInbox - Gate sock.readMessages() call based on config option Default behavior (true) is preserved - only explicitly setting false will disable read receipts. --- src/config/types.whatsapp.ts | 4 +++ src/config/zod-schema.providers-whatsapp.ts | 2 ++ src/web/accounts.ts | 27 +++++++++++---------- src/web/auto-reply/monitor.ts | 1 + src/web/inbound/monitor.ts | 8 +++++- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/config/types.whatsapp.ts b/src/config/types.whatsapp.ts index 82f467530..c28178f06 100644 --- a/src/config/types.whatsapp.ts +++ b/src/config/types.whatsapp.ts @@ -14,6 +14,8 @@ export type WhatsAppConfig = { capabilities?: string[]; /** Allow channel-initiated config writes (default: true). */ configWrites?: boolean; + /** Send read receipts for incoming messages (default true). */ + sendReadReceipts?: boolean; /** * Inbound message prefix (WhatsApp only). * Default: `[{agents.list[].identity.name}]` (or `[clawdbot]`) when allowFrom is empty, else `""`. @@ -84,6 +86,8 @@ export type WhatsAppAccountConfig = { configWrites?: boolean; /** If false, do not start this WhatsApp account provider. Default: true. */ enabled?: boolean; + /** Send read receipts for incoming messages (default true). */ + sendReadReceipts?: boolean; /** Inbound message prefix override for this account (WhatsApp only). */ messagePrefix?: string; /** Override auth directory (Baileys multi-file auth state). */ diff --git a/src/config/zod-schema.providers-whatsapp.ts b/src/config/zod-schema.providers-whatsapp.ts index 3636c302c..962bb5619 100644 --- a/src/config/zod-schema.providers-whatsapp.ts +++ b/src/config/zod-schema.providers-whatsapp.ts @@ -13,6 +13,7 @@ export const WhatsAppAccountSchema = z capabilities: z.array(z.string()).optional(), configWrites: z.boolean().optional(), enabled: z.boolean().optional(), + sendReadReceipts: z.boolean().optional(), messagePrefix: z.string().optional(), /** Override auth directory for this WhatsApp account (Baileys multi-file auth state). */ authDir: z.string().optional(), @@ -62,6 +63,7 @@ export const WhatsAppConfigSchema = z accounts: z.record(z.string(), WhatsAppAccountSchema.optional()).optional(), capabilities: z.array(z.string()).optional(), configWrites: z.boolean().optional(), + sendReadReceipts: z.boolean().optional(), dmPolicy: DmPolicySchema.optional().default("pairing"), messagePrefix: z.string().optional(), selfChatMode: z.boolean().optional(), diff --git a/src/web/accounts.ts b/src/web/accounts.ts index d1c4e1722..618b57d2d 100644 --- a/src/web/accounts.ts +++ b/src/web/accounts.ts @@ -12,6 +12,7 @@ export type ResolvedWhatsAppAccount = { accountId: string; name?: string; enabled: boolean; + sendReadReceipts: boolean; messagePrefix?: string; authDir: string; isLegacyAuthDir: boolean; @@ -125,6 +126,7 @@ export function resolveWhatsAppAccount(params: { cfg: ClawdbotConfig; accountId?: string | null; }): ResolvedWhatsAppAccount { + const rootCfg = params.cfg.channels?.whatsapp; const accountId = params.accountId?.trim() || resolveDefaultWhatsAppAccountId(params.cfg); const accountCfg = resolveAccountConfig(params.cfg, accountId); const enabled = accountCfg?.enabled !== false; @@ -136,22 +138,21 @@ export function resolveWhatsAppAccount(params: { accountId, name: accountCfg?.name?.trim() || undefined, enabled, + sendReadReceipts: accountCfg?.sendReadReceipts ?? rootCfg?.sendReadReceipts ?? true, messagePrefix: - accountCfg?.messagePrefix ?? - params.cfg.channels?.whatsapp?.messagePrefix ?? - params.cfg.messages?.messagePrefix, + accountCfg?.messagePrefix ?? rootCfg?.messagePrefix ?? params.cfg.messages?.messagePrefix, authDir, isLegacyAuthDir: isLegacy, - selfChatMode: accountCfg?.selfChatMode ?? params.cfg.channels?.whatsapp?.selfChatMode, - dmPolicy: accountCfg?.dmPolicy ?? params.cfg.channels?.whatsapp?.dmPolicy, - allowFrom: accountCfg?.allowFrom ?? params.cfg.channels?.whatsapp?.allowFrom, - groupAllowFrom: accountCfg?.groupAllowFrom ?? params.cfg.channels?.whatsapp?.groupAllowFrom, - groupPolicy: accountCfg?.groupPolicy ?? params.cfg.channels?.whatsapp?.groupPolicy, - textChunkLimit: accountCfg?.textChunkLimit ?? params.cfg.channels?.whatsapp?.textChunkLimit, - mediaMaxMb: accountCfg?.mediaMaxMb ?? params.cfg.channels?.whatsapp?.mediaMaxMb, - blockStreaming: accountCfg?.blockStreaming ?? params.cfg.channels?.whatsapp?.blockStreaming, - ackReaction: accountCfg?.ackReaction ?? params.cfg.channels?.whatsapp?.ackReaction, - groups: accountCfg?.groups ?? params.cfg.channels?.whatsapp?.groups, + selfChatMode: accountCfg?.selfChatMode ?? rootCfg?.selfChatMode, + dmPolicy: accountCfg?.dmPolicy ?? rootCfg?.dmPolicy, + allowFrom: accountCfg?.allowFrom ?? rootCfg?.allowFrom, + groupAllowFrom: accountCfg?.groupAllowFrom ?? rootCfg?.groupAllowFrom, + groupPolicy: accountCfg?.groupPolicy ?? rootCfg?.groupPolicy, + textChunkLimit: accountCfg?.textChunkLimit ?? rootCfg?.textChunkLimit, + mediaMaxMb: accountCfg?.mediaMaxMb ?? rootCfg?.mediaMaxMb, + blockStreaming: accountCfg?.blockStreaming ?? rootCfg?.blockStreaming, + ackReaction: accountCfg?.ackReaction ?? rootCfg?.ackReaction, + groups: accountCfg?.groups ?? rootCfg?.groups, }; } diff --git a/src/web/auto-reply/monitor.ts b/src/web/auto-reply/monitor.ts index 328b49a55..c8d139032 100644 --- a/src/web/auto-reply/monitor.ts +++ b/src/web/auto-reply/monitor.ts @@ -174,6 +174,7 @@ export async function monitorWebChannel( accountId: account.accountId, authDir: account.authDir, mediaMaxMb: account.mediaMaxMb, + sendReadReceipts: account.sendReadReceipts, onMessage: async (msg: WebInboundMsg) => { handledMessages += 1; lastMessageAt = Date.now(); diff --git a/src/web/inbound/monitor.ts b/src/web/inbound/monitor.ts index 395de7cb6..f0511aeee 100644 --- a/src/web/inbound/monitor.ts +++ b/src/web/inbound/monitor.ts @@ -26,6 +26,8 @@ export async function monitorWebInbox(options: { authDir: string; onMessage: (msg: WebInboundMessage) => Promise; mediaMaxMb?: number; + /** Send read receipts for incoming messages (default true). */ + sendReadReceipts?: boolean; }) { const inboundLogger = getChildLogger({ module: "web-inbound" }); const inboundConsoleLog = createSubsystemLogger("gateway/channels/whatsapp").child("inbound"); @@ -139,7 +141,11 @@ export async function monitorWebInbox(options: { }); if (!access.allowed) continue; - if (id && !access.isSelfChat) { + if ( + id && + !access.isSelfChat && + options.sendReadReceipts !== false + ) { const participant = msg.key?.participant; try { await sock.readMessages([{ remoteJid, id, participant, fromMe: false }]);