import logging import time from yuxi.channel.extensions.freshdesk.types import FreshdeskIdentity, FreshdeskInboundEvent from yuxi.channel.message.models import GroupContext, MessageType, PeerInfo, UnifiedMessage from yuxi.channel.routing.models import PeerKind logger = logging.getLogger(__name__) class FreshdeskMonitor: def __init__(self): self._dedupe: dict[str, float] = {} async def convert_event( self, event: FreshdeskInboundEvent, identity: FreshdeskIdentity, account_id: str, mode: str, ) -> UnifiedMessage | None: if self._is_echo(event, identity): return None if self._is_duplicate(event): return None if mode == "freshchat" and not event.conversation_id: return None if mode == "freshdesk" and not event.ticket_id: return None target_id = event.conversation_id or event.ticket_id or "" return UnifiedMessage( msg_id=f"freshdesk:{event.message_id}", channel_type="freshdesk", account_id=account_id, content=event.content, sender=PeerInfo( kind=PeerKind.DIRECT, id=event.actor_id or "unknown", display_name=event.actor_name.strip() or "Customer", ), message_type=self._detect_type(event), group=GroupContext(id=target_id), raw=event.raw, timestamp=event.created_at, ) def _is_echo(self, event: FreshdeskInboundEvent, identity: FreshdeskIdentity) -> bool: if event.actor_type == "agent" and event.actor_id == identity.freshdesk_agent_id: return True return False def _is_duplicate(self, event: FreshdeskInboundEvent) -> bool: dedupe_key = f"{event.source}:{event.event_id}" now = time.time() if dedupe_key in self._dedupe: return True self._dedupe[dedupe_key] = now self._cleanup_expired(now) return False def _cleanup_expired(self, now: float): expired = [k for k, ts in self._dedupe.items() if now - ts > 60] for k in expired: del self._dedupe[k] def _detect_type(self, event: FreshdeskInboundEvent) -> MessageType: if event.attachments: for att in event.attachments: content_type = att.get("content_type", "") if content_type.startswith("image/"): return MessageType.IMAGE if content_type.startswith("video/"): return MessageType.VIDEO return MessageType.FILE return MessageType.TEXT