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.
This commit is contained in:
Christian A. Rodriguez
2026-01-13 22:01:55 -04:00
committed by Peter Steinberger
parent 44a237b637
commit 7a683a4b62
5 changed files with 28 additions and 14 deletions

View File

@@ -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). */

View File

@@ -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(),

View File

@@ -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,
};
}

View File

@@ -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();

View File

@@ -26,6 +26,8 @@ export async function monitorWebInbox(options: {
authDir: string;
onMessage: (msg: WebInboundMessage) => Promise<void>;
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 }]);