from __future__ import annotations import logging import time from dataclasses import dataclass, field from typing import Any logger = logging.getLogger(__name__) @dataclass class FeishuRateLimiter: max_requests_per_minute: int = 100 _timestamps: list[float] = field(default_factory=list) def acquire(self) -> bool: now = time.monotonic() window = now - 60 self._timestamps = [t for t in self._timestamps if t > window] if len(self._timestamps) >= self.max_requests_per_minute: return False self._timestamps.append(now) return True def reset(self) -> None: self._timestamps.clear() @dataclass class FeishuAccountPermission: allowed_chat_ids: set[str] = field(default_factory=set) allowed_user_ids: set[str] = field(default_factory=set) allowed_tool_scopes: set[str] = field(default_factory=set) is_admin: bool = False allow_all: bool = False def can_access_chat(self, chat_id: str) -> bool: if self.allow_all or self.is_admin: return True return chat_id in self.allowed_chat_ids def can_access_user(self, user_id: str) -> bool: if self.allow_all or self.is_admin: return True return user_id in self.allowed_user_ids def can_use_tool(self, tool_name: str) -> bool: if self.allow_all or self.is_admin: return True if not self.allowed_tool_scopes: return False return tool_name in self.allowed_tool_scopes @classmethod def from_config(cls, perm_cfg: dict[str, Any] | None) -> FeishuAccountPermission: if not perm_cfg: return cls() return cls( allowed_chat_ids=set(perm_cfg.get("allowedChatIds", perm_cfg.get("allowed_chat_ids", []))), allowed_user_ids=set(perm_cfg.get("allowedUserIds", perm_cfg.get("allowed_user_ids", []))), allowed_tool_scopes=set(perm_cfg.get("allowedToolScopes", perm_cfg.get("allowed_tool_scopes", []))), is_admin=perm_cfg.get("isAdmin", perm_cfg.get("is_admin", False)), allow_all=perm_cfg.get("allowAll", perm_cfg.get("allow_all", False)), ) @dataclass class FeishuAccount: name: str app_id: str = "" app_secret: str = "" bot_open_id: str = "" platform: str = "feishu" domain: str = "" encrypt_key: str = "" verify_token: str = "" enabled: bool = True config: dict[str, Any] = field(default_factory=dict) rate_limiter: FeishuRateLimiter = field(default_factory=FeishuRateLimiter) permissions: FeishuAccountPermission = field(default_factory=FeishuAccountPermission) class FeishuAccountManager: def __init__(self, config: dict[str, Any] | None = None): self._accounts: dict[str, FeishuAccount] = {} self._default_account: str = "" if config: self._load_from_config(config) def _load_from_config(self, config: dict[str, Any]) -> None: accounts_cfg = config.get("accounts", {}) if not accounts_cfg: main = FeishuAccount( name="default", app_id=config.get("app_id", ""), app_secret=config.get("app_secret", ""), bot_open_id=config.get("bot_open_id", ""), platform=config.get("platform", "feishu"), domain=config.get("domain", ""), encrypt_key=config.get("encrypt_key", ""), verify_token=config.get("verify_token", ""), config=config, ) self._accounts["default"] = main self._default_account = "default" return self._default_account = config.get("defaultAccount", "") for name, acct_cfg in accounts_cfg.items(): if not isinstance(acct_cfg, dict): continue merged = {**config, **acct_cfg} rpm = acct_cfg.get("rateLimitPerMinute", acct_cfg.get("rate_limit_per_minute", 100)) perm_cfg = acct_cfg.get("permissions", acct_cfg.get("permission", {})) account = FeishuAccount( name=name, app_id=acct_cfg.get("appId", acct_cfg.get("app_id", "")), app_secret=acct_cfg.get("appSecret", acct_cfg.get("app_secret", "")), bot_open_id=acct_cfg.get("botOpenId", acct_cfg.get("bot_open_id", "")), platform=acct_cfg.get("platform", merged.get("platform", "feishu")), domain=acct_cfg.get("domain", merged.get("domain", "")), encrypt_key=acct_cfg.get("encryptKey", acct_cfg.get("encrypt_key", "")), verify_token=acct_cfg.get("verifyToken", acct_cfg.get("verify_token", "")), enabled=acct_cfg.get("enabled", True), config=merged, rate_limiter=FeishuRateLimiter(max_requests_per_minute=rpm if isinstance(rpm, int) else 100), permissions=FeishuAccountPermission.from_config(perm_cfg if isinstance(perm_cfg, dict) else {}), ) self._accounts[name] = account if self._default_account not in self._accounts and self._accounts: self._default_account = next(iter(self._accounts)) def get_account(self, name: str = "") -> FeishuAccount | None: key = name or self._default_account account = self._accounts.get(key) if account and account.enabled: return account return None def get_default_account(self) -> FeishuAccount | None: return self.get_account(self._default_account) def list_enabled(self) -> list[FeishuAccount]: return [a for a in self._accounts.values() if a.enabled] def list_all(self) -> list[FeishuAccount]: return list(self._accounts.values()) def check_rate_limit(self, name: str = "") -> bool: account = self.get_account(name) if account is None: return False return account.rate_limiter.acquire() def check_chat_access(self, chat_id: str, account_name: str = "") -> bool: account = self.get_account(account_name) if account is None: return False return account.permissions.can_access_chat(chat_id) def check_user_access(self, user_id: str, account_name: str = "") -> bool: account = self.get_account(account_name) if account is None: return False return account.permissions.can_access_user(user_id) def check_tool_access(self, tool_name: str, account_name: str = "") -> bool: account = self.get_account(account_name) if account is None: return False return account.permissions.can_use_tool(tool_name) def delete_account(self, name: str) -> bool: if name == self._default_account: logger.warning("[FeishuAccount] Cannot delete default account '%s'", name) return False if name not in self._accounts: logger.warning("[FeishuAccount] Account '%s' not found", name) return False self._accounts.pop(name) logger.info("[FeishuAccount] Deleted account '%s'", name) return True def get_account_status_line(self, name: str = "") -> str: account = self.get_account(name) if name else self.get_default_account() if account is None: return "unknown: no account configured" status = "active" if account.enabled else "disabled" return f"{account.name}: {status} (platform={account.platform})"