from __future__ import annotations import logging import time from typing import Any logger = logging.getLogger(__name__) try: import lark_oapi HAS_LARK_SDK = True except ImportError: HAS_LARK_SDK = False lark_oapi = None _client_cache: dict[str, Any] = {} _CACHE_TTL = 3600 MAX_CLIENTS = 32 def get_client(app_id: str, app_secret: str, domain: str = "feishu", http_timeout_ms: int = 30000) -> Any: if not HAS_LARK_SDK: raise ImportError("lark-oapi SDK is required. Install with: pip install lark-oapi") domain_key = domain.lower() if domain_key in ("lark", "larksuite"): base_url = "https://open.larksuite.com" elif domain_key.startswith("http"): base_url = domain_key.rstrip("/") else: base_url = "https://open.feishu.cn" cache_key = f"{app_id}:{base_url}" entry = _client_cache.get(cache_key) now = time.monotonic() if entry and now - entry["ts"] < _CACHE_TTL and entry["secret"] == app_secret: return entry["client"] timeout_seconds = max(http_timeout_ms, 1000) / 1000.0 client = lark_oapi.Client.builder() \ .app_id(app_id) \ .app_secret(app_secret) \ .open_api_url(base_url) \ .timeout(timeout_seconds) \ .build() _client_cache[cache_key] = {"client": client, "ts": now, "secret": app_secret} if len(_client_cache) > MAX_CLIENTS: oldest = min(_client_cache, key=lambda k: _client_cache[k]["ts"]) del _client_cache[oldest] return client def invalidate_client(app_id: str, domain: str = "feishu") -> None: domain_key = domain.lower() if domain_key in ("lark", "larksuite"): base_url = "https://open.larksuite.com" elif domain_key.startswith("http"): base_url = domain_key.rstrip("/") else: base_url = "https://open.feishu.cn" cache_key = f"{app_id}:{base_url}" _client_cache.pop(cache_key, None) def get_ws_client(app_id: str, app_secret: str, domain: str = "feishu") -> Any: if not HAS_LARK_SDK: raise ImportError("lark-oapi SDK is required") domain_key = domain.lower() if domain_key in ("lark", "larksuite"): base_url = "https://open.larksuite.com" elif domain_key.startswith("http"): base_url = domain_key.rstrip("/") else: base_url = "https://open.feishu.cn" return lark_oapi.ws.Client.builder() \ .app_id(app_id) \ .app_secret(app_secret) \ .open_api_url(base_url) \ .build() async def get_bot_info(client: Any) -> dict: resp = await client.im.v1.bot_info.get_async() if resp.success(): data = resp.data bot = getattr(data, "bot", {}) or {} return { "bot_name": getattr(bot, "app_name", "") if hasattr(bot, "app_name") else "", "activate_status": getattr(bot, "activate_status", 0) if hasattr(bot, "activate_status") else 0, "open_id": getattr(bot, "open_id", "") if hasattr(bot, "open_id") else "", } return {} async def get_chat_info(client: Any, chat_id: str) -> dict | None: resp = await client.im.v1.chat.get_async(lark_oapi.im.v1.GetChatRequest(chat_id=chat_id)) if resp.success() and resp.data: return { "name": getattr(resp.data, "name", ""), "description": getattr(resp.data, "description", ""), "chat_type": getattr(resp.data, "chat_type", ""), "owner_id": getattr(resp.data, "owner_id", ""), "owner_id_type": getattr(resp.data, "owner_id_type", ""), } return None async def get_chat_members(client: Any, chat_id: str, page_token: str = "", page_size: int = 100) -> dict: resp = await client.im.v1.chat_members.get_async( lark_oapi.im.v1.GetChatMembersRequest( chat_id=chat_id, page_token=page_token, page_size=page_size, member_id_type="open_id", ) ) if resp.success() and resp.data: items = getattr(resp.data, "items", []) or [] return { "members": [ { "member_id": getattr(m, "member_id", ""), "name": getattr(m, "name", ""), "member_type": getattr(m, "member_type", ""), } for m in items ], "page_token": getattr(resp.data, "page_token", ""), "has_more": getattr(resp.data, "has_more", False), } return {"members": [], "page_token": "", "has_more": False} async def get_user_info(client: Any, user_id: str) -> dict | None: resp = await client.contact.v3.user.get_async( lark_oapi.contact.v3.GetUserRequest( user_id=user_id, user_id_type="open_id", ) ) if resp.success() and resp.data and resp.data.user: u = resp.data.user return { "open_id": getattr(u, "open_id", ""), "name": getattr(u, "name", ""), "avatar_url": getattr(u, "avatar_url", ""), "email": getattr(u, "email", ""), "mobile": getattr(u, "mobile", ""), "department_ids": getattr(u, "department_ids", []) or [], } return None async def get_message(client: Any, message_id: str) -> dict | None: resp = await client.im.v1.message.get_async( lark_oapi.im.v1.GetMessageRequest(message_id=message_id) ) if resp.success() and resp.data: items = getattr(resp.data, "items", []) or [] if items: item = items[0] return { "message_id": getattr(item, "message_id", ""), "msg_type": getattr(item, "msg_type", ""), "body": getattr(item, "body", {}), "sender": { "id": getattr(item, "sender", {}).get("id", ""), "id_type": getattr(item, "sender", {}).get("id_type", ""), }, "root_id": getattr(item, "root_id", ""), "parent_id": getattr(item, "parent_id", ""), "thread_id": getattr(item, "thread_id", ""), } return None async def list_messages( client: Any, container_id_type: str = "chat", container_id: str = "", page_token: str = "", page_size: int = 20, start_time: str = "", end_time: str = "", sort_type: str = "ByCreateTimeDesc", ) -> dict: req = lark_oapi.im.v1.ListMessageRequest( container_id_type=container_id_type, container_id=container_id, page_token=page_token if page_token else None, page_size=page_size, start_time=start_time if start_time else None, end_time=end_time if end_time else None, sort_type=sort_type, ) resp = await client.im.v1.message.list_async(req) if resp.success() and resp.data: items = getattr(resp.data, "items", []) or [] return { "messages": [ { "message_id": getattr(m, "message_id", ""), "msg_type": getattr(m, "msg_type", ""), "root_id": getattr(m, "root_id", ""), "parent_id": getattr(m, "parent_id", ""), "thread_id": getattr(m, "thread_id", ""), "create_time": getattr(m, "create_time", ""), } for m in items ], "has_more": getattr(resp.data, "has_more", False), "page_token": getattr(resp.data, "page_token", ""), } return {"messages": [], "has_more": False, "page_token": ""} async def batch_get_users(client: Any, user_ids: list[str]) -> list[dict]: if not user_ids: return [] results = [] for user_id in user_ids: info = await get_user_info(client, user_id) if info: results.append(info) return results async def create_chat( client: Any, name: str, description: str = "", user_ids: list[str] | None = None, chat_type: str = "group", ) -> dict: resp = await client.im.v1.chat.create_async( lark_oapi.im.v1.CreateChatRequest( request_body=lark_oapi.im.v1.CreateChatRequestBody( name=name, description=description, user_ids=user_ids or [], chat_type=chat_type, ), ) ) if resp.success() and resp.data: return { "success": True, "chat_id": getattr(resp.data, "chat_id", ""), } return {"success": False, "error": getattr(resp, "msg", "")} async def update_chat( client: Any, chat_id: str, name: str = "", description: str = "", ) -> dict: body = {} if name: body["name"] = name if description: body["description"] = description resp = await client.im.v1.chat.update_async( lark_oapi.im.v1.UpdateChatRequest( chat_id=chat_id, request_body=lark_oapi.im.v1.UpdateChatRequestBody(**body), ) ) if resp.success(): return {"success": True} return {"success": False, "error": getattr(resp, "msg", "")} async def delete_chat(client: Any, chat_id: str) -> dict: resp = await client.im.v1.chat.delete_async( lark_oapi.im.v1.DeleteChatRequest(chat_id=chat_id) ) if resp.success(): return {"success": True} return {"success": False, "error": getattr(resp, "msg", "")} async def add_chat_members( client: Any, chat_id: str, member_ids: list[str], ) -> dict: resp = await client.im.v1.chat_members.create_async( lark_oapi.im.v1.CreateChatMembersRequest( chat_id=chat_id, member_id_type="open_id", request_body=lark_oapi.im.v1.CreateChatMembersRequestBody( id_list=member_ids, ), ) ) if resp.success(): return {"success": True} return {"success": False, "error": getattr(resp, "msg", "")} async def remove_chat_members( client: Any, chat_id: str, member_ids: list[str], ) -> dict: resp = await client.im.v1.chat_members.delete_async( lark_oapi.im.v1.DeleteChatMembersRequest( chat_id=chat_id, member_id_type="open_id", request_body=lark_oapi.im.v1.DeleteChatMembersRequestBody( id_list=member_ids, ), ) ) if resp.success(): return {"success": True} return {"success": False, "error": getattr(resp, "msg", "")} async def list_user_chats( client: Any, user_id: str = "", page_token: str = "", page_size: int = 50, ) -> dict: req = lark_oapi.im.v1.ListChatRequest( user_id_type="open_id" if user_id else None, page_token=page_token if page_token else None, page_size=page_size, ) resp = await client.im.v1.chat.list_async(req) if resp.success() and resp.data: items = getattr(resp.data, "items", []) or [] return { "chats": [ { "chat_id": getattr(c, "chat_id", ""), "name": getattr(c, "name", ""), "chat_type": getattr(c, "chat_type", ""), } for c in items ], "has_more": getattr(resp.data, "has_more", False), "page_token": getattr(resp.data, "page_token", ""), } return {"chats": [], "has_more": False, "page_token": ""} async def search_chats( client: Any, query: str = "", page_token: str = "", page_size: int = 50, ) -> dict: req = lark_oapi.im.v1.SearchChatRequest( query=query if query else None, page_token=page_token if page_token else None, page_size=page_size, ) resp = await client.im.v1.chat.search_async(req) if resp.success() and resp.data: items = getattr(resp.data, "items", []) or [] return { "chats": [ { "chat_id": getattr(c, "chat_id", ""), "name": getattr(c, "name", ""), "chat_type": getattr(c, "chat_type", ""), } for c in items ], "has_more": getattr(resp.data, "has_more", False), "page_token": getattr(resp.data, "page_token", ""), } return {"chats": [], "has_more": False, "page_token": ""} async def list_departments( client: Any, parent_department_id: str = "0", page_token: str = "", page_size: int = 50, ) -> dict: resp = await client.contact.v3.department.list_async( lark_oapi.contact.v3.ListDepartmentRequest( parent_department_id=parent_department_id, page_token=page_token if page_token else None, page_size=page_size, ) ) if resp.success() and resp.data: items = getattr(resp.data, "items", []) or [] return { "departments": [ { "department_id": getattr(d, "department_id", ""), "name": getattr(d, "name", ""), "parent_department_id": getattr(d, "parent_department_id", ""), } for d in items ], "has_more": getattr(resp.data, "has_more", False), "page_token": getattr(resp.data, "page_token", ""), } return {"departments": [], "has_more": False, "page_token": ""} async def find_users_by_department( client: Any, department_id: str, page_token: str = "", page_size: int = 50, ) -> dict: resp = await client.contact.v3.user.find_by_department_async( lark_oapi.contact.v3.FindByDepartmentUserRequest( department_id=department_id, page_token=page_token if page_token else None, page_size=page_size, ) ) if resp.success() and resp.data: items = getattr(resp.data, "items", []) or [] return { "users": [ { "open_id": getattr(u, "open_id", ""), "name": getattr(u, "name", ""), "email": getattr(u, "email", ""), } for u in items ], "has_more": getattr(resp.data, "has_more", False), "page_token": getattr(resp.data, "page_token", ""), } return {"users": [], "has_more": False, "page_token": ""}