from __future__ import annotations import re import time from typing import Any _WILDCARD_PATTERN = re.compile(r"[*?\[\]]") class PolicyAccessTracker: MAX_HISTORY_PER_KEY = 100 def __init__(self): self._access_log: dict[str, list[tuple[float, str, bool]]] = {} def record_access(self, key: str, resource_id: str, allowed: bool) -> None: if key not in self._access_log: self._access_log[key] = [] entry = (time.monotonic(), resource_id, allowed) self._access_log[key].append(entry) if len(self._access_log[key]) > self.MAX_HISTORY_PER_KEY: self._access_log[key] = self._access_log[key][-self.MAX_HISTORY_PER_KEY :] def get_history(self, key: str) -> list[dict[str, Any]]: entries = self._access_log.get(key, []) return [{"timestamp": ts, "resource_id": rid, "allowed": allowed} for ts, rid, allowed in entries] def count_recent(self, key: str, window_s: float = 60) -> tuple[int, int]: entries = self._access_log.get(key, []) now = time.monotonic() allowed = sum(1 for ts, _, ok in entries if now - ts <= window_s and ok) denied = sum(1 for ts, _, ok in entries if now - ts <= window_s and not ok) return allowed, denied def clear(self, key: str = "") -> None: if key: self._access_log.pop(key, None) else: self._access_log.clear() class DingDingPolicyMatcher: def __init__(self, config: dict[str, Any]): self._group_policy = config.get("groupPolicy", config.get("group_policy", "allowlist")) self._dm_policy = config.get("dmPolicy", config.get("dm_policy", "pairing")) self._allowlist = set(config.get("allowFrom", config.get("allowlist", []))) self._blocklist = set(config.get("blockFrom", config.get("blocklist", []))) self._deny_message = config.get("denyMessage", "对不起,您没有权限使用此机器人。") self._global_require_mention = config.get("requireMention", True) self._global_system_prompt = config.get("systemPrompt", config.get("system_prompt", "")) self._groups_config: dict[str, dict[str, Any]] = config.get("groups", {}) self._dms_config: dict[str, dict[str, Any]] = config.get("dms", {}) self._tracker = PolicyAccessTracker() def check_chat_access(self, chat_id: str, chat_type: str) -> tuple[bool, str]: if self._blocklist: if any(self._match_pattern(chat_id, pattern) for pattern in self._blocklist): self._tracker.record_access("blocklist", chat_id, False) return False, self._deny_message policy = self._dm_policy if chat_type == "direct" else self._group_policy if policy == "open": self._tracker.record_access(policy, chat_id, True) return True, "" if policy == "allowlist": if not self._allowlist: self._tracker.record_access(policy, chat_id, False) return False, self._deny_message matched = any(self._match_pattern(chat_id, pattern) for pattern in self._allowlist) self._tracker.record_access(policy, chat_id, matched) if matched: return True, "" return False, self._deny_message if policy == "disabled": self._tracker.record_access(policy, chat_id, False) return False, self._deny_message if policy == "pairing": return True, "" self._tracker.record_access(policy, chat_id, False) return False, self._deny_message def resolve_require_mention(self, chat_id: str) -> bool: group_cfg = self._groups_config.get(chat_id, {}) if "requireMention" in group_cfg: return bool(group_cfg["requireMention"]) return self._global_require_mention def resolve_group_config(self, chat_id: str) -> dict[str, Any]: return self._groups_config.get(chat_id, {}) def resolve_dm_config(self, chat_id: str) -> dict[str, Any]: return self._dms_config.get(chat_id, {}) def resolve_system_prompt(self, chat_id: str, chat_type: str) -> str: if chat_type == "direct": dm_cfg = self._dms_config.get(chat_id, {}) return dm_cfg.get("systemPrompt", dm_cfg.get("system_prompt", self._global_system_prompt)) group_cfg = self._groups_config.get(chat_id, {}) return group_cfg.get("systemPrompt", group_cfg.get("system_prompt", self._global_system_prompt)) def get_access_history(self, policy_type: str = "") -> list[dict[str, Any]]: if policy_type: return self._tracker.get_history(policy_type) result = [] for key in ("open", "allowlist", "disabled", "blocklist", "pairing"): result.extend(self._tracker.get_history(key)) return sorted(result, key=lambda x: x.get("timestamp", 0)) def reset_tracker(self) -> None: self._tracker.clear() @staticmethod def _match_pattern(value: str, pattern: str) -> bool: if value == pattern: return True if _WILDCARD_PATTERN.search(pattern): try: return bool(re.fullmatch(pattern.replace("*", ".*").replace("?", "."), value)) except re.error: return False return False