"""Telegram channel connector.""" from __future__ import annotations from collections.abc import Callable from typing import Any from .models import ChannelRuntimeSpec, ValidationResult from .store import ChannelConnectionStore, CredentialStore class TelegramConnector: kind = "telegram" def __init__( self, *, connection_store: ChannelConnectionStore, credential_store: CredentialStore, client_factory: Callable[[str], Any] | None = None, ) -> None: self.connection_store = connection_store self.credential_store = credential_store self.client_factory = client_factory or _default_client_factory async def validate(self, connection_id: str) -> ValidationResult: connection = self.connection_store.get(connection_id) token = self._bot_token(connection.credentials_ref) try: client = self.client_factory(token) raw = await client.get_me() bot_id = _value(raw, "id") username = _value(raw, "username") first_name = _value(raw, "first_name") or "Telegram Bot" account_id = f"telegram:{bot_id}" if bot_id else connection.account_id display_name = f"{first_name} (@{username})" if username else first_name connection.account_id = account_id connection.display_name = display_name connection.capabilities = ["receive_text", "send_text", "receive_media", "groups"] self.connection_store.update(connection) return ValidationResult( ok=True, status="connected", account_id=account_id, display_name=display_name, metadata={"username": username} if username else {}, ) except Exception as exc: return ValidationResult(ok=False, status="error", error=str(exc)) async def materialize_runtime(self, connection_id: str) -> ChannelRuntimeSpec: connection = self.connection_store.get(connection_id) if connection.status not in {"connected", "running"}: raise ValueError(f"Connection is not connected: {connection.connection_id}") return ChannelRuntimeSpec( channel_id=connection.channel_id, kind=connection.kind, mode=connection.mode, account_id=connection.account_id, display_name=connection.display_name, config=dict(connection.runtime_config), secrets_ref=connection.credentials_ref, ) async def revoke(self, connection_id: str) -> None: # Telegram bot tokens do not have a Beaver-managed platform revoke action. # The registry owns local connection state transitions. return None def _bot_token(self, credentials_ref: str | None) -> str: if not credentials_ref: raise ValueError("Telegram credentials are missing") token = self.credential_store.get(credentials_ref).get("botToken") if not token: raise ValueError("botToken is required") return token def _value(raw: Any, key: str) -> str: if isinstance(raw, dict): value = raw.get(key) else: value = getattr(raw, key, None) return str(value).strip() if value is not None else "" def _default_client_factory(token: str) -> Any: try: from telegram import Bot except ImportError as exc: # pragma: no cover - optional live dependency raise RuntimeError("Install beaver-backend[telegram] to validate Telegram connections") from exc return Bot(token=token)