"""Sidecar-backed channel connectors.""" from __future__ import annotations from typing import Any from .models import ChannelRuntimeSpec, ValidationResult from .sidecar_client import ConnectorSidecarClient from .store import ChannelConnectionStore, CredentialStore class ExternalConnectorBase: kind = "" capabilities: list[str] = [] def __init__( self, *, connection_store: ChannelConnectionStore, credential_store: CredentialStore, sidecar_client: ConnectorSidecarClient | Any, sidecar_base_url: str, ) -> None: self.connection_store = connection_store self.credential_store = credential_store self.sidecar_client = sidecar_client self.sidecar_base_url = sidecar_base_url async def start_session( self, *, display_name: str, owner_user_id: str | None, options: dict[str, Any], ) -> dict[str, Any]: connection = self.connection_store.create( kind=self.kind, mode="sidecar", display_name=display_name or self.kind, account_id="", owner_user_id=owner_user_id, auth_type="connector_session", runtime_config={"sidecarBaseUrl": self.sidecar_base_url}, capabilities=list(self.capabilities), ) connection = self.connection_store.update_status(connection.connection_id, status="pairing", last_error=None) payload = { "kind": self.kind, "connectionId": connection.connection_id, "channelId": connection.channel_id, "displayName": connection.display_name, "callbackBaseUrl": "", "options": dict(options), } view = dict(await self.sidecar_client.start_session(payload)) connection.pairing_session_id = str(view.get("sessionId") or "") self.connection_store.update(connection) view["connectionId"] = connection.connection_id view["channelId"] = connection.channel_id return view async def poll_session(self, session_id: str) -> dict[str, Any]: view = dict(await self.sidecar_client.get_session(session_id)) connection = self._connection_for_session(session_id) status = str(view.get("status") or "") if status == "connected": connection.account_id = str(view.get("accountId") or connection.account_id) connection.display_name = str(view.get("displayName") or connection.display_name) metadata = view.get("metadata") if isinstance(view.get("metadata"), dict) else {} state_ref = metadata.get("stateRef") if state_ref: connection.credentials_ref = self.credential_store.put(kind=self.kind, values={"stateRef": state_ref}) self.connection_store.update(connection) self.connection_store.update_status(connection.connection_id, status="connected", last_error=None) elif status in {"expired", "error", "cancelled"}: self.connection_store.update_status( connection.connection_id, status="error", last_error=str(view.get("error") or status), ) view["connectionId"] = connection.connection_id view["channelId"] = connection.channel_id return view async def validate(self, connection_id: str) -> ValidationResult: connection = self.connection_store.get(connection_id) if connection.status in {"connected", "running"}: return ValidationResult( ok=True, status="connected", account_id=connection.account_id, display_name=connection.display_name, ) return ValidationResult(ok=False, status=connection.status, error=connection.last_error) 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="external_connector", mode="http", account_id=connection.account_id, display_name=connection.display_name, config={ "platformKind": self.kind, "connectionId": connection.connection_id, "sidecarBaseUrl": connection.runtime_config.get("sidecarBaseUrl") or self.sidecar_base_url, }, secrets_ref=None, ) async def revoke(self, connection_id: str) -> None: await self.sidecar_client.logout(connection_id) def _connection_for_session(self, session_id: str): for connection in self.connection_store.list(): if connection.pairing_session_id == session_id: return connection raise KeyError(session_id) class WeixinConnector(ExternalConnectorBase): kind = "weixin" capabilities = ["receive_text", "send_text", "receive_media", "direct_messages"] class FeishuConnector(ExternalConnectorBase): kind = "feishu" capabilities = ["receive_text", "send_text", "receive_media", "groups"]