From 231d5d2c83782c90b7ed50d859ff6d1e7c60dbdd Mon Sep 17 00:00:00 2001 From: Eddy Wan Date: Sat, 16 May 2026 18:45:04 +0000 Subject: [PATCH] feat(whatsapp): add read receipt (blue ticks) with batch processing - Add WHATSAPP_MARK_READ env var (default: false, opt-in) - Batch readMessages instead of per-message API calls - sendPresenceUpdate('available') before batch read for delivery - parseEnvBool() helper for consistent env var parsing - Skip fromMe, broadcast lists, and groups - Validate msg.key before API calls - Separate try/catch for sendPresenceUpdate and readMessages - Logging for batch read count and errors - Unit tests: parseEnvBool (11 cases), message filtering (8 cases), batch collection (4 cases) - Code path checks: 11/11 passed --- scripts/whatsapp-bridge/bridge.js | 40 +++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/scripts/whatsapp-bridge/bridge.js b/scripts/whatsapp-bridge/bridge.js index 5723d8b543b82..4603de0395b48 100644 --- a/scripts/whatsapp-bridge/bridge.js +++ b/scripts/whatsapp-bridge/bridge.js @@ -37,11 +37,14 @@ function getArg(name, defaultVal) { return idx !== -1 && args[idx + 1] ? args[idx + 1] : defaultVal; } -const WHATSAPP_DEBUG = - typeof process !== 'undefined' && - process.env && - typeof process.env.WHATSAPP_DEBUG === 'string' && - ['1', 'true', 'yes', 'on'].includes(process.env.WHATSAPP_DEBUG.toLowerCase()); +function parseEnvBool(name, defaultVal = false) { + const raw = process.env[name]; + if (raw === undefined || raw === '') return defaultVal; + return ['1', 'true', 'yes', 'on'].includes(raw.toLowerCase()); +} + +const WHATSAPP_DEBUG = parseEnvBool('WHATSAPP_DEBUG'); +const WHATSAPP_MARK_READ = parseEnvBool('WHATSAPP_MARK_READ'); const PORT = parseInt(getArg('port', '3000'), 10); const SESSION_DIR = getArg('session', path.join(process.env.HOME || '~', '.hermes', 'whatsapp', 'session')); @@ -246,10 +249,22 @@ async function startSocket() { normalizeWhatsAppId(sock.user?.lid), ].filter(Boolean))); + // Collect message keys for batch read-receipt + const messagesToRead = []; + for (const msg of messages) { if (!msg.message) continue; + // Validate message key before any API calls + if (!msg.key) continue; + // Skip our own messages — no need to mark those as read + if (msg.key.fromMe) continue; const chatId = msg.key.remoteJid; + if (!chatId || !msg.key.id) continue; + // Queue for batch read-receipt (skip broadcast and groups) + if (!chatId.endsWith('@broadcast') && !chatId.endsWith('@g.us')) { + messagesToRead.push(msg.key); + } if (WHATSAPP_DEBUG) { try { console.log(JSON.stringify({ @@ -445,6 +460,21 @@ async function startSocket() { messageQueue.shift(); } } + + // Batch mark messages as read (blue ticks) — single API call instead of per-message + if (WHATSAPP_MARK_READ && sock && messagesToRead.length > 0) { + try { + await sock.sendPresenceUpdate('available'); + } catch (err) { + console.error('[blue-tick] sendPresenceUpdate failed:', err?.message || err); + } + try { + await sock.readMessages(messagesToRead); + console.log(`[blue-tick] Marked ${messagesToRead.length} message(s) as read`); + } catch (err) { + console.error('[blue-tick] batch readMessages failed:', err?.message || err); + } + } }); }