"""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 POLICY_CONFIG_KEYS = { "allowFrom", "groupAllowFrom", "requireMentionInGroups", "respondToMentionAll", "dmMode", "maxMessageChars", "textBatchDelayMs", "textBatchMaxMessages", "textBatchMaxChars", } 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]: runtime_config = {"sidecarBaseUrl": self.sidecar_base_url} runtime_config.update(_policy_runtime_config(options)) 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=runtime_config, 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 "") connection = self.connection_store.update(connection) connection = self._apply_session_view(connection, view) 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) connection = self._apply_session_view(connection, view) view["connectionId"] = connection.connection_id view["channelId"] = connection.channel_id return view def _apply_session_view(self, connection: Any, view: dict[str, Any]) -> Any: 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), ) return self.connection_store.get(connection.connection_id) 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, **dict(connection.runtime_config), "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"] def _policy_runtime_config(options: dict[str, Any]) -> dict[str, Any]: result: dict[str, Any] = {} for key in POLICY_CONFIG_KEYS: if key not in options: continue value = options[key] if key in {"allowFrom", "groupAllowFrom"}: items = _string_list(value) if items: result[key] = items continue if key in {"maxMessageChars", "textBatchDelayMs", "textBatchMaxMessages", "textBatchMaxChars"}: number = _positive_int(value) if number is not None: result[key] = number continue if key in {"requireMentionInGroups", "respondToMentionAll"}: result[key] = _bool(value) continue text = str(value or "").strip() if text: result[key] = text return result def _string_list(value: Any) -> list[str]: if isinstance(value, str): raw_items = value.replace("\n", ",").split(",") elif isinstance(value, list): raw_items = value else: raw_items = [] return [str(item).strip() for item in raw_items if str(item).strip()] def _positive_int(value: Any) -> int | None: try: number = int(value) except (TypeError, ValueError): return None return number if number > 0 else None def _bool(value: Any) -> bool: if isinstance(value, bool): return value return str(value).strip().lower() in {"1", "true", "yes", "on"}