from __future__ import annotations import asyncio import time from typing import Any PAGE_SIZE_DEFAULT = 100 PAGE_SIZE_MAX = 200 CACHE_TTL_USERS_S = 300 CACHE_TTL_DEPARTMENTS_S = 600 class FeishuDirectoryClient: def __init__(self, api_client: Any, page_size: int = PAGE_SIZE_DEFAULT): self._api = api_client self._page_size = min(page_size, PAGE_SIZE_MAX) self._lock = asyncio.Lock() self._user_cache: dict[str, tuple[dict, float]] = {} self._user_list_cache: list[dict] = [] self._user_list_age: float = 0 self._dept_cache: dict[str, tuple[dict, float]] = {} self._dept_list_cache: list[dict] = [] self._dept_list_age: float = 0 async def get_user(self, user_id: str, *, user_id_type: str = "open_id") -> dict | None: cache_key = f"{user_id_type}:{user_id}" async with self._lock: cached = self._user_cache.get(cache_key) if cached is not None: data, ts = cached if time.monotonic() - ts < CACHE_TTL_USERS_S: return data path = f"/open-apis/contact/v3/users/{user_id}" try: resp = await self._api.get(path, user_id_type=user_id_type) user = resp.get("data", {}).get("user", {}) except Exception: return None async with self._lock: if user: self._user_cache[cache_key] = (user, time.monotonic()) else: self._user_cache.pop(cache_key, None) return user async def list_users( self, *, department_id: str = "", page_token: str = "", page_size: int = 0, ) -> dict[str, Any]: size = min(page_size or self._page_size, PAGE_SIZE_MAX) path = "/open-apis/contact/v3/users" params: dict[str, Any] = {"page_size": size} if department_id: params["department_id"] = department_id if page_token: params["page_token"] = page_token if not department_id and not page_token: async with self._lock: if time.monotonic() - self._user_list_age < CACHE_TTL_USERS_S and self._user_list_cache: return {"items": list(self._user_list_cache), "has_more": False, "page_token": ""} resp = await self._api.get(path, **params) data = resp.get("data", {}) items = data.get("items", []) if not department_id and not page_token: async with self._lock: self._user_list_cache = items self._user_list_age = time.monotonic() return { "items": items, "has_more": data.get("has_more", False), "page_token": data.get("page_token", ""), } async def get_department(self, department_id: str) -> dict | None: cache_key = department_id async with self._lock: cached = self._dept_cache.get(cache_key) if cached is not None: data, ts = cached if time.monotonic() - ts < CACHE_TTL_DEPARTMENTS_S: return data path = f"/open-apis/contact/v3/departments/{department_id}" try: resp = await self._api.get(path) dept = resp.get("data", {}).get("department", {}) except Exception: return None async with self._lock: if dept: self._dept_cache[cache_key] = (dept, time.monotonic()) else: self._dept_cache.pop(cache_key, None) return dept async def list_departments( self, *, parent_department_id: str = "", page_token: str = "", page_size: int = 0, ) -> dict[str, Any]: size = min(page_size or self._page_size, PAGE_SIZE_MAX) path = "/open-apis/contact/v3/departments" params: dict[str, Any] = {"page_size": size} if parent_department_id: params["parent_department_id"] = parent_department_id if page_token: params["page_token"] = page_token if not parent_department_id and not page_token: async with self._lock: if time.monotonic() - self._dept_list_age < CACHE_TTL_DEPARTMENTS_S and self._dept_list_cache: return {"items": list(self._dept_list_cache), "has_more": False, "page_token": ""} resp = await self._api.get(path, **params) data = resp.get("data", {}) items = data.get("items", []) if not parent_department_id and not page_token: async with self._lock: self._dept_list_cache = items self._dept_list_age = time.monotonic() return { "items": items, "has_more": data.get("has_more", False), "page_token": data.get("page_token", ""), } def invalidate_cache(self, scope: str = "all") -> None: if scope in ("all", "users"): self._user_cache.clear() self._user_list_cache.clear() self._user_list_age = 0 if scope in ("all", "departments"): self._dept_cache.clear() self._dept_list_cache.clear() self._dept_list_age = 0 async def list_groups(client: Any) -> list[dict]: resp = await client.chat.v1.list(page_size=200) if not resp.success(): return [] items = [] data = resp.data for group in getattr(data, "items", []) or []: items.append( { "id": getattr(group, "chat_id", ""), "name": getattr(group, "name", ""), } ) return items async def list_peers(client: Any) -> list[dict]: resp = await client.contact.v3.scope.list() if not resp.success(): return [] items = [] data = resp.data for user in getattr(data, "items", []) or []: items.append( { "id": getattr(user, "open_id", ""), "name": getattr(user, "name", ""), } ) return items