这是一个批量整理提交,包含以下主要改动: 1. 删除多处冗余的空行和未使用的导入 2. 修复文件末尾缺少换行符的问题 3. 调整部分模块的导入顺序与代码排版 4. 修复部分配置默认值与策略逻辑 5. 新增多个功能模块与辅助工具 6. 完善异常处理与日志记录 7. 修复速率限制、消息缓存、权限校验等逻辑bug 8. 废弃部分旧有API与配置项并添加警告提示
166 lines
5.5 KiB
Python
166 lines
5.5 KiB
Python
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._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}"
|
|
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
|
|
del self._user_cache[cache_key]
|
|
|
|
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", {})
|
|
if user:
|
|
self._user_cache[cache_key] = (user, time.monotonic())
|
|
return user
|
|
except Exception:
|
|
return None
|
|
|
|
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:
|
|
if time.monotonic() - self._user_list_age < CACHE_TTL_USERS_S and self._user_list_cache:
|
|
return {"items": 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:
|
|
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
|
|
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
|
|
del self._dept_cache[cache_key]
|
|
|
|
path = f"/open-apis/contact/v3/departments/{department_id}"
|
|
try:
|
|
resp = await self._api.get(path)
|
|
dept = resp.get("data", {}).get("department", {})
|
|
if dept:
|
|
self._dept_cache[cache_key] = (dept, time.monotonic())
|
|
return dept
|
|
except Exception:
|
|
return None
|
|
|
|
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:
|
|
if time.monotonic() - self._dept_list_age < CACHE_TTL_DEPARTMENTS_S and self._dept_list_cache:
|
|
return {"items": 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:
|
|
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 |