from __future__ import annotations import hashlib import threading import time from collections import OrderedDict class MessageDeduplicator: def __init__(self, ttl_seconds: int = 300, max_size: int = 10000): self._ttl = ttl_seconds self._max_size = max_size self._cache: OrderedDict[str, float] = OrderedDict() self._lock = threading.Lock() def is_duplicate(self, message_id: str) -> bool: key = self._normalize_key(message_id) now = time.monotonic() with self._lock: self._evict_expired(now) if key in self._cache: return True self._cache[key] = now self._cache.move_to_end(key) return False def record(self, message_id: str) -> None: key = self._normalize_key(message_id) now = time.monotonic() with self._lock: self._evict_expired(now) self._cache[key] = now self._cache.move_to_end(key) def clear(self) -> None: with self._lock: self._cache.clear() def _evict_expired(self, now: float) -> None: expired = [] for key, timestamp in self._cache.items(): if now - timestamp > self._ttl: expired.append(key) for key in expired: del self._cache[key] while len(self._cache) > self._max_size: self._cache.popitem(last=False) @staticmethod def _fingerprint(payload: dict) -> str: stable_fields = ( payload.get("key", {}).get("id", ""), payload.get("key", {}).get("remoteJid", ""), payload.get("messageTimestamp", ""), payload.get("pushName", ""), ) raw = "|".join(str(f) for f in stable_fields) return hashlib.sha256(raw.encode()).hexdigest()[:32] @staticmethod def _normalize_key(message_id: str) -> str: return message_id.strip() class ButtonDeduplicator: def __init__(self, ttl_seconds: float = 5.0, max_size: int = 2000): self._ttl = ttl_seconds self._max_size = max_size self._cache: OrderedDict[str, float] = OrderedDict() self._lock = threading.Lock() def is_duplicate(self, sender_jid: str, button_id: str) -> bool: key = f"{sender_jid}|{button_id}" now = time.monotonic() with self._lock: self._evict_expired(now) if key in self._cache: return True self._cache[key] = now self._cache.move_to_end(key) return False def _evict_expired(self, now: float) -> None: expired = [k for k, ts in self._cache.items() if now - ts > self._ttl] for k in expired: del self._cache[k] while len(self._cache) > self._max_size: self._cache.popitem(last=False) def create_deduplicator(ttl_seconds: int = 300, max_size: int = 10000) -> MessageDeduplicator: return MessageDeduplicator(ttl_seconds=ttl_seconds, max_size=max_size)