Merge pull request #882 from chrisrodz/feat/whatsapp-send-read-receipts-option
feat(whatsapp): add sendReadReceipts config option
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
- Browser: add Chrome extension relay takeover mode (toolbar button), plus `clawdbot browser extension install/path` and remote browser control via `clawdbot browser serve` + `browser.controlToken`.
|
||||
- CLI/Docs: add per-command CLI doc pages and link them from `clawdbot <command> --help`.
|
||||
- Browser: copy the installed Chrome extension path to clipboard after `clawdbot browser extension install/path`.
|
||||
- WhatsApp: add `channels.whatsapp.sendReadReceipts` to disable auto read receipts. (#882) — thanks @chrisrodz.
|
||||
|
||||
### Fixes
|
||||
- Browser: add tests for snapshot labels/efficient query params and labeled image responses.
|
||||
|
||||
@@ -93,6 +93,7 @@ group messages, so use admin if you need full visibility.
|
||||
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
|
||||
- Replies always route back to the same Telegram chat.
|
||||
- Long-polling uses grammY runner with per-chat sequencing; overall concurrency is capped by `agents.defaults.maxConcurrent`.
|
||||
- Telegram Bot API does not support read receipts; there is no `sendReadReceipts` option.
|
||||
|
||||
## Formatting (Telegram HTML)
|
||||
- Outbound Telegram text uses `parse_mode: "HTML"` (Telegram’s supported tag subset).
|
||||
|
||||
@@ -138,6 +138,32 @@ Behavior:
|
||||
- Self-chat mode (allowFrom includes your number) avoids auto read receipts and ignores mention JIDs.
|
||||
- Read receipts sent for non-self-chat DMs.
|
||||
|
||||
## Read receipts
|
||||
By default, the gateway marks inbound WhatsApp messages as read (blue ticks) once they are accepted.
|
||||
|
||||
Disable globally:
|
||||
```json5
|
||||
{
|
||||
channels: { whatsapp: { sendReadReceipts: false } }
|
||||
}
|
||||
```
|
||||
|
||||
Disable per account:
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
personal: { sendReadReceipts: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Self-chat mode always skips read receipts.
|
||||
|
||||
## WhatsApp FAQ: sending messages + pairing
|
||||
|
||||
**Will Clawdbot message random contacts when I link WhatsApp?**
|
||||
|
||||
@@ -430,6 +430,22 @@ For groups, use `channels.whatsapp.groupPolicy` + `channels.whatsapp.groupAllowF
|
||||
}
|
||||
```
|
||||
|
||||
### `channels.whatsapp.sendReadReceipts`
|
||||
|
||||
Controls whether inbound WhatsApp messages are marked as read (blue ticks). Default: `true`.
|
||||
|
||||
Self-chat mode always skips read receipts, even when enabled.
|
||||
|
||||
Per-account override: `channels.whatsapp.accounts.<id>.sendReadReceipts`.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: { sendReadReceipts: false }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `channels.whatsapp.accounts` (multi-account)
|
||||
|
||||
Run multiple WhatsApp accounts in one gateway:
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,7 @@ 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 }]);
|
||||
|
||||
@@ -210,6 +210,35 @@ describe("web monitor inbox", () => {
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("skips read receipts when disabled", async () => {
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage,
|
||||
sendReadReceipts: false,
|
||||
});
|
||||
const sock = await createWaSocket();
|
||||
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
key: { id: "rr-off-1", fromMe: false, remoteJid: "222@s.whatsapp.net" },
|
||||
message: { conversation: "read receipts off" },
|
||||
messageTimestamp: 1_700_000_000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
sock.ev.emit("messages.upsert", upsert);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(onMessage).toHaveBeenCalledTimes(1);
|
||||
expect(sock.readMessages).not.toHaveBeenCalled();
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("lets group messages through even when sender not in allowFrom", async () => {
|
||||
mockLoadConfig.mockReturnValue({
|
||||
channels: { whatsapp: { allowFrom: ["+1234"], groupPolicy: "open" } },
|
||||
|
||||
Reference in New Issue
Block a user