From c0ad98650414aa22ad9d3364ded069e94e5f1479 Mon Sep 17 00:00:00 2001 From: spartan077 Date: Sat, 28 Feb 2026 13:44:22 +0530 Subject: [PATCH 1/2] fix: add message deduplication to WhatsApp channel Prevent infinite loops by tracking processed message IDs in WhatsApp channel. The bridge may send duplicate messages which caused the bot to respond repeatedly with the same generic message. Changes: - Add _processed_message_ids deque (max 2000) to track seen messages - Skip processing if message_id was already processed - Align WhatsApp dedup with other channels (Feishu, Email, Mochat, QQ) This fixes the issue where WhatsApp gets stuck in a loop sending identical responses repeatedly. Co-Authored-By: Claude Sonnet 4.6 --- nanobot/channels/whatsapp.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/nanobot/channels/whatsapp.py b/nanobot/channels/whatsapp.py index f5fb521..b171b6c 100644 --- a/nanobot/channels/whatsapp.py +++ b/nanobot/channels/whatsapp.py @@ -2,6 +2,7 @@ import asyncio import json +from collections import deque from typing import Any from loguru import logger @@ -15,18 +16,20 @@ from nanobot.config.schema import WhatsAppConfig class WhatsAppChannel(BaseChannel): """ WhatsApp channel that connects to a Node.js bridge. - + The bridge uses @whiskeysockets/baileys to handle the WhatsApp Web protocol. Communication between Python and Node.js is via WebSocket. """ - + name = "whatsapp" - + MAX_PROCESSED_MESSAGE_IDS = 2000 + def __init__(self, config: WhatsAppConfig, bus: MessageBus): super().__init__(config, bus) self.config: WhatsAppConfig = config self._ws = None self._connected = False + self._processed_message_ids: deque[str] = deque(maxlen=self.MAX_PROCESSED_MESSAGE_IDS) async def start(self) -> None: """Start the WhatsApp channel by connecting to the bridge.""" @@ -105,26 +108,35 @@ class WhatsAppChannel(BaseChannel): # Incoming message from WhatsApp # Deprecated by whatsapp: old phone number style typically: @s.whatspp.net pn = data.get("pn", "") - # New LID sytle typically: + # New LID sytle typically: sender = data.get("sender", "") content = data.get("content", "") - + message_id = data.get("id", "") + + # Dedup by message ID to prevent loops + if message_id and message_id in self._processed_message_ids: + logger.debug("Duplicate message {}, skipping", message_id) + return + + if message_id: + self._processed_message_ids.append(message_id) + # Extract just the phone number or lid as chat_id user_id = pn if pn else sender sender_id = user_id.split("@")[0] if "@" in user_id else user_id logger.info("Sender {}", sender) - + # Handle voice transcription if it's a voice message if content == "[Voice Message]": logger.info("Voice message received from {}, but direct download from bridge is not yet supported.", sender_id) content = "[Voice Message: Transcription not available for WhatsApp yet]" - + await self._handle_message( sender_id=sender_id, chat_id=sender, # Use full LID for replies content=content, metadata={ - "message_id": data.get("id"), + "message_id": message_id, "timestamp": data.get("timestamp"), "is_group": data.get("isGroup", False) } From 95ffe47e343b6411aa7a20b32cbfaf8aa369f749 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Sat, 28 Feb 2026 08:38:29 +0000 Subject: [PATCH 2/2] refactor: use OrderedDict for WhatsApp dedup, consistent with Feishu --- nanobot/channels/whatsapp.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/nanobot/channels/whatsapp.py b/nanobot/channels/whatsapp.py index b171b6c..49d2390 100644 --- a/nanobot/channels/whatsapp.py +++ b/nanobot/channels/whatsapp.py @@ -2,7 +2,7 @@ import asyncio import json -from collections import deque +from collections import OrderedDict from typing import Any from loguru import logger @@ -22,14 +22,13 @@ class WhatsAppChannel(BaseChannel): """ name = "whatsapp" - MAX_PROCESSED_MESSAGE_IDS = 2000 def __init__(self, config: WhatsAppConfig, bus: MessageBus): super().__init__(config, bus) self.config: WhatsAppConfig = config self._ws = None self._connected = False - self._processed_message_ids: deque[str] = deque(maxlen=self.MAX_PROCESSED_MESSAGE_IDS) + self._processed_message_ids: OrderedDict[str, None] = OrderedDict() async def start(self) -> None: """Start the WhatsApp channel by connecting to the bridge.""" @@ -113,13 +112,12 @@ class WhatsAppChannel(BaseChannel): content = data.get("content", "") message_id = data.get("id", "") - # Dedup by message ID to prevent loops - if message_id and message_id in self._processed_message_ids: - logger.debug("Duplicate message {}, skipping", message_id) - return - if message_id: - self._processed_message_ids.append(message_id) + if message_id in self._processed_message_ids: + return + self._processed_message_ids[message_id] = None + while len(self._processed_message_ids) > 1000: + self._processed_message_ids.popitem(last=False) # Extract just the phone number or lid as chat_id user_id = pn if pn else sender