"""Async HTTP-Client für die MCM REST-API. Die TUI kommuniziert ausschließlich über diesen Client mit dem Backend. Dadurch läuft die TUI sowohl im Terminal (python main.py) als auch im Browser (textual serve tui_standalone.py) ohne Änderung. """ from __future__ import annotations import logging from typing import Any import httpx from config import settings logger = logging.getLogger(__name__) class MCMApiClient: """Thin async wrapper um die MCM REST-API.""" def __init__(self) -> None: self._base = f"http://127.0.0.1:{settings.port}/api/v1" self._token: str | None = None @property def _headers(self) -> dict[str, str]: if self._token: return {"Authorization": f"Bearer {self._token}"} return {} # ── Authentifizierung ────────────────────────────────────────────────────── async def login(self, username: str, password: str) -> bool: """Meldet den Benutzer an und speichert den JWT-Token. Gibt True bei Erfolg zurück.""" try: async with httpx.AsyncClient(timeout=5.0) as client: resp = await client.post( self._base + "/auth/login", json={"username": username, "password": password}, ) if resp.status_code == 200: self._token = resp.json()["access_token"] return True return False except Exception as exc: logger.warning("Login fehlgeschlagen: %s", exc) return False def logout(self) -> None: self._token = None @property def is_authenticated(self) -> bool: return self._token is not None # ── Konversationen ───────────────────────────────────────────────────────── async def get_conversations(self, channel: str | None = None) -> list[dict[str, Any]]: params: dict[str, Any] = {} if channel: params["channel"] = channel return await self._get("/conversations", params=params) async def get_conversation(self, conv_id: str) -> dict[str, Any] | None: try: return await self._get(f"/conversations/{conv_id}") except httpx.HTTPStatusError: return None async def get_messages( self, conv_id: str, limit: int = 100, offset: int = 0 ) -> list[dict[str, Any]]: return await self._get( f"/conversations/{conv_id}/messages", params={"limit": limit, "offset": offset}, ) # ── Nachrichten senden ───────────────────────────────────────────────────── async def send_message( self, channel: str, text: str, recipient_phone: str | None = None, recipient_telegram_id: str | None = None, reply_to_id: str | None = None, ) -> dict[str, Any]: body: dict[str, Any] = {"channel": channel, "text": text} if recipient_phone: body["recipient_phone"] = recipient_phone if recipient_telegram_id: body["recipient_telegram_id"] = recipient_telegram_id if reply_to_id: body["reply_to_id"] = reply_to_id return await self._post("/messages", body) # ── Kontakte ─────────────────────────────────────────────────────────────── async def get_contacts(self) -> list[dict[str, Any]]: return await self._get("/contacts") # ── Status ───────────────────────────────────────────────────────────────── async def get_channel_status(self) -> dict[str, Any]: return await self._get("/channels/status") # ── Interne Hilfsmethoden ────────────────────────────────────────────────── async def _get(self, path: str, params: dict | None = None) -> Any: async with httpx.AsyncClient(timeout=5.0, follow_redirects=True) as client: resp = await client.get( self._base + path, headers=self._headers, params=params ) resp.raise_for_status() return resp.json() async def _post(self, path: str, body: dict) -> Any: async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client: resp = await client.post( self._base + path, headers=self._headers, json=body ) resp.raise_for_status() return resp.json()