import json import time from typing import Any import httpx from fastapi import HTTPException from app.core.config import get_settings class FeishuClient: """Small Feishu Open Platform client for tenant token and message APIs.""" def __init__(self) -> None: self.settings = get_settings() self._tenant_access_token: str | None = None self._token_expires_at: float = 0 def _is_configured(self) -> bool: return bool(self.settings.feishu_app_id and self.settings.feishu_app_secret) def _get_tenant_access_token(self) -> str: if not self._is_configured(): raise HTTPException(status_code=503, detail="Feishu app credentials are not configured") if self._tenant_access_token and time.time() < self._token_expires_at: return self._tenant_access_token url = f"{self.settings.feishu_base_url}/auth/v3/tenant_access_token/internal" payload = { "app_id": self.settings.feishu_app_id, "app_secret": self.settings.feishu_app_secret, } with httpx.Client(timeout=20) as client: response = client.post(url, json=payload) response.raise_for_status() data = response.json() if data.get("code") != 0: raise HTTPException(status_code=502, detail={"feishu_error": data}) self._tenant_access_token = data["tenant_access_token"] self._token_expires_at = time.time() + int(data.get("expire", 7200)) - 300 return self._tenant_access_token def send_message( self, receive_id: str, receive_id_type: str, msg_type: str, content: dict[str, Any], ) -> dict[str, Any]: token = self._get_tenant_access_token() url = f"{self.settings.feishu_base_url}/im/v1/messages" headers = {"Authorization": f"Bearer {token}"} params = {"receive_id_type": receive_id_type} payload = { "receive_id": receive_id, "msg_type": msg_type, "content": json.dumps(content, ensure_ascii=False), } with httpx.Client(timeout=20) as client: response = client.post(url, headers=headers, params=params, json=payload) response.raise_for_status() data = response.json() return data def send_text( self, text: str, receive_id: str | None = None, receive_id_type: str = "chat_id", ) -> dict: chat_id = receive_id or self.settings.feishu_default_chat_id if not chat_id: raise HTTPException( status_code=400, detail="receive_id or FEISHU_DEFAULT_CHAT_ID is required", ) return self.send_message(chat_id, receive_id_type, "text", {"text": text}) def send_card( self, card: dict[str, Any], receive_id: str | None = None, receive_id_type: str = "chat_id", ) -> dict: chat_id = receive_id or self.settings.feishu_default_chat_id if not chat_id: raise HTTPException( status_code=400, detail="receive_id or FEISHU_DEFAULT_CHAT_ID is required", ) return self.send_message(chat_id, receive_id_type, "interactive", card)