from __future__ import annotations import logging from typing import TYPE_CHECKING, Any import httpx if TYPE_CHECKING: from yuxi.channels.adapters.dingding.token import DingDingTokenManager logger = logging.getLogger(__name__) DINGTALK_API_BASE = "https://api.dingtalk.com" class DingDingDirectoryClient: def __init__( self, token_manager: DingDingTokenManager, *, http_client: httpx.AsyncClient | None = None, ): self._token_manager = token_manager self._http_client = http_client async def _get_http_client(self) -> httpx.AsyncClient: if self._http_client is not None and not self._http_client.is_closed: return self._http_client return httpx.AsyncClient(timeout=httpx.Timeout(15)) async def _request( self, method: str, path: str, *, params: dict[str, Any] | None = None, json_data: dict[str, Any] | None = None, ) -> httpx.Response: token = await self._token_manager.get_token() url = f"{DINGTALK_API_BASE}{path}" headers = { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json", } if self._http_client is not None and not self._http_client.is_closed: return await self._http_client.request(method, url, headers=headers, params=params, json=json_data) async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client: return await client.request(method, url, headers=headers, params=params, json=json_data) async def get_user_info(self, user_id: str) -> dict[str, Any]: resp = await self._request("GET", f"/v1.0/contact/users/{user_id}") if resp.status_code != 200: logger.warning(f"[DingDing Directory] get_user_info HTTP {resp.status_code}") return {} data = resp.json() return { "user_id": data.get("openId", data.get("unionId", user_id)), "name": data.get("nick", data.get("name", "")), "avatar_url": data.get("avatarUrl", data.get("avatar", "")), "email": data.get("email", ""), "mobile": data.get("mobile", ""), "title": data.get("title", data.get("position", "")), "dept_ids": data.get("deptIdList", data.get("departmentIds", [])), "raw": data, } async def list_peers(self, *, page_size: int = 100, page_token: str = "") -> dict[str, Any]: try: size = min(page_size, 200) params: dict[str, Any] = {"maxResults": size} if page_token: params["nextToken"] = page_token resp = await self._request("GET", "/v1.0/contact/users", params=params) if resp.status_code != 200: logger.warning(f"[DingDing Directory] list_peers HTTP {resp.status_code}") return {"items": [], "has_more": False, "page_token": ""} data = resp.json() items = data.get("items", data.get("userList", [])) peers = [] for item in items: peers.append({ "id": item.get("openId", item.get("userId", "")), "name": item.get("nick", item.get("name", "")), "avatar_url": item.get("avatarUrl", item.get("avatar", "")), }) return { "items": peers, "has_more": data.get("hasMore", False), "page_token": data.get("nextToken", ""), } except Exception as e: logger.error(f"[DingDing Directory] list_peers failed: {e}") return {"items": [], "has_more": False, "page_token": "", "error": str(e)} async def list_groups(self, *, page_size: int = 100, page_token: str = "") -> dict[str, Any]: try: size = min(page_size, 200) params: dict[str, Any] = {"maxResults": size} if page_token: params["nextToken"] = page_token resp = await self._request("GET", "/v1.0/im/conversations", params=params) if resp.status_code != 200: logger.warning(f"[DingDing Directory] list_groups HTTP {resp.status_code}") return {"items": [], "has_more": False, "page_token": ""} data = resp.json() items = data.get("items", data.get("conversationList", [])) groups = [] for item in items: groups.append({ "id": item.get("openConversationId", ""), "name": item.get("title", item.get("name", "")), "avatar_url": item.get("icon", item.get("avatarUrl", "")), "member_count": item.get("memberCount", 0), "type": item.get("conversationType", "group"), }) return { "items": groups, "has_more": data.get("hasMore", False), "page_token": data.get("nextToken", ""), } except Exception as e: logger.error(f"[DingDing Directory] list_groups failed: {e}") return {"items": [], "has_more": False, "page_token": "", "error": str(e)} async def get_channel_info(self, chat_id: str) -> dict[str, Any]: open_conv_id = chat_id.replace("group_", "").replace("dm_", "") try: resp = await self._request("GET", f"/v1.0/im/conversations/{open_conv_id}") if resp.status_code != 200: logger.warning(f"[DingDing Directory] get_channel_info HTTP {resp.status_code}") return {"chat_id": chat_id, "name": "", "member_count": 0} data = resp.json() return { "chat_id": chat_id, "name": data.get("title", data.get("name", "")), "avatar_url": data.get("icon", data.get("avatarUrl", "")), "member_count": data.get("memberCount", 0), "type": data.get("conversationType", "group"), "owner_id": data.get("ownerUserId", ""), "create_time": data.get("createTime", ""), } except Exception as e: logger.error(f"[DingDing Directory] get_channel_info failed: {e}") return {"chat_id": chat_id, "name": "", "member_count": 0, "error": str(e)} async def search_users( self, query: str, *, page_size: int = 20, ) -> list[dict[str, Any]]: try: size = min(page_size, 100) params: dict[str, Any] = { "maxResults": size, "searchKey": query, } resp = await self._request("GET", "/v1.0/contact/users", params=params) if resp.status_code != 200: return [] data = resp.json() items = data.get("items", data.get("userList", [])) return [ { "user_id": item.get("openId", item.get("userId", "")), "name": item.get("nick", item.get("name", "")), "avatar_url": item.get("avatarUrl", item.get("avatar", "")), "email": item.get("email", ""), "mobile": item.get("mobile", ""), } for item in items ] except Exception as e: logger.error(f"[DingDing Directory] search_users failed: {e}") return []