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 <noreply@anthropic.com>
This commit is contained in:
spartan077
2026-02-28 13:44:22 +05:30
parent e86cfcde22
commit c0ad986504

View File

@@ -2,6 +2,7 @@
import asyncio import asyncio
import json import json
from collections import deque
from typing import Any from typing import Any
from loguru import logger from loguru import logger
@@ -15,18 +16,20 @@ from nanobot.config.schema import WhatsAppConfig
class WhatsAppChannel(BaseChannel): class WhatsAppChannel(BaseChannel):
""" """
WhatsApp channel that connects to a Node.js bridge. WhatsApp channel that connects to a Node.js bridge.
The bridge uses @whiskeysockets/baileys to handle the WhatsApp Web protocol. The bridge uses @whiskeysockets/baileys to handle the WhatsApp Web protocol.
Communication between Python and Node.js is via WebSocket. Communication between Python and Node.js is via WebSocket.
""" """
name = "whatsapp" name = "whatsapp"
MAX_PROCESSED_MESSAGE_IDS = 2000
def __init__(self, config: WhatsAppConfig, bus: MessageBus): def __init__(self, config: WhatsAppConfig, bus: MessageBus):
super().__init__(config, bus) super().__init__(config, bus)
self.config: WhatsAppConfig = config self.config: WhatsAppConfig = config
self._ws = None self._ws = None
self._connected = False self._connected = False
self._processed_message_ids: deque[str] = deque(maxlen=self.MAX_PROCESSED_MESSAGE_IDS)
async def start(self) -> None: async def start(self) -> None:
"""Start the WhatsApp channel by connecting to the bridge.""" """Start the WhatsApp channel by connecting to the bridge."""
@@ -105,26 +108,35 @@ class WhatsAppChannel(BaseChannel):
# Incoming message from WhatsApp # Incoming message from WhatsApp
# Deprecated by whatsapp: old phone number style typically: <phone>@s.whatspp.net # Deprecated by whatsapp: old phone number style typically: <phone>@s.whatspp.net
pn = data.get("pn", "") pn = data.get("pn", "")
# New LID sytle typically: # New LID sytle typically:
sender = data.get("sender", "") sender = data.get("sender", "")
content = data.get("content", "") 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 # Extract just the phone number or lid as chat_id
user_id = pn if pn else sender user_id = pn if pn else sender
sender_id = user_id.split("@")[0] if "@" in user_id else user_id sender_id = user_id.split("@")[0] if "@" in user_id else user_id
logger.info("Sender {}", sender) logger.info("Sender {}", sender)
# Handle voice transcription if it's a voice message # Handle voice transcription if it's a voice message
if content == "[Voice Message]": if content == "[Voice Message]":
logger.info("Voice message received from {}, but direct download from bridge is not yet supported.", sender_id) 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]" content = "[Voice Message: Transcription not available for WhatsApp yet]"
await self._handle_message( await self._handle_message(
sender_id=sender_id, sender_id=sender_id,
chat_id=sender, # Use full LID for replies chat_id=sender, # Use full LID for replies
content=content, content=content,
metadata={ metadata={
"message_id": data.get("id"), "message_id": message_id,
"timestamp": data.get("timestamp"), "timestamp": data.get("timestamp"),
"is_group": data.get("isGroup", False) "is_group": data.get("isGroup", False)
} }