"""Abstract base class for IM channels.""" from __future__ import annotations import logging from abc import ABC, abstractmethod from typing import Any from src.channels.message_bus import InboundMessage, InboundMessageType, MessageBus, OutboundMessage logger = logging.getLogger(__name__) class Channel(ABC): """Base class for all IM channel implementations. Each channel connects to an external messaging platform and: 1. Receives messages, wraps them as InboundMessage, publishes to the bus. 2. Subscribes to outbound messages and sends replies back to the platform. Subclasses must implement ``start``, ``stop``, and ``send``. """ def __init__(self, name: str, bus: MessageBus, config: dict[str, Any]) -> None: self.name = name self.bus = bus self.config = config self._running = False @property def is_running(self) -> bool: return self._running # -- lifecycle --------------------------------------------------------- @abstractmethod async def start(self) -> None: """Start listening for messages from the external platform.""" @abstractmethod async def stop(self) -> None: """Gracefully stop the channel.""" # -- outbound ---------------------------------------------------------- @abstractmethod async def send(self, msg: OutboundMessage) -> None: """Send a message back to the external platform. The implementation should use ``msg.chat_id`` and ``msg.thread_ts`` to route the reply to the correct conversation/thread. """ # -- helpers ----------------------------------------------------------- def _make_inbound( self, chat_id: str, user_id: str, text: str, *, msg_type: InboundMessageType = InboundMessageType.CHAT, thread_ts: str | None = None, files: list[dict[str, Any]] | None = None, metadata: dict[str, Any] | None = None, ) -> InboundMessage: """Convenience factory for creating InboundMessage instances.""" return InboundMessage( channel_name=self.name, chat_id=chat_id, user_id=user_id, text=text, msg_type=msg_type, thread_ts=thread_ts, files=files or [], metadata=metadata or {}, ) async def _on_outbound(self, msg: OutboundMessage) -> None: """Outbound callback registered with the bus. Only forwards messages targeted at this channel. """ if msg.channel_name == self.name: try: await self.send(msg) except Exception: logger.exception("Failed to send outbound message on channel %s", self.name)