from __future__ import annotations import logging import re from .config import _apply_env_overrides, _dict_to_account from .constants import FLOCK_GROUP_ID_PATTERN, FLOCK_USER_ID_PATTERN logger = logging.getLogger(__name__) _user_id_re = re.compile(FLOCK_USER_ID_PATTERN) _group_id_re = re.compile(FLOCK_GROUP_ID_PATTERN) def resolve_dm_policy(config: dict, account_id: str = "default") -> dict: account_data = config.get("accounts", {}).get(account_id, {}) account = _dict_to_account(account_data) account = _apply_env_overrides(account) return {"mode": account.dm_policy, "allow_from": account.dm_allow_from} def resolve_group_policy(config: dict, account_id: str = "default") -> dict: account_data = config.get("accounts", {}).get(account_id, {}) account = _dict_to_account(account_data) account = _apply_env_overrides(account) return {"mode": account.group_policy, "group_allow_from": account.group_allow_from} async def check_allowlist( peer_id: str, channel_type: str, config: dict, account_id: str = "default", ) -> dict: account_data = config.get("accounts", {}).get(account_id, {}) account = _dict_to_account(account_data) account = _apply_env_overrides(account) if channel_type == "direct": policy = account.dm_policy allow_list = account.dm_allow_from else: policy = account.group_policy allow_list = account.group_allow_from if policy == "open": return {"allowed": True, "reason": ""} if policy == "disabled": return {"allowed": False, "reason": f"{channel_type} messaging is disabled"} if policy in ("allowlist", "pairing"): if peer_id in allow_list: return {"allowed": True, "reason": ""} return {"allowed": False, "reason": f"Peer {peer_id} not in {channel_type} allow list"} return {"allowed": False, "reason": f"Unknown policy: {policy}"} def collect_warnings( config: dict, account_id: str | None = None, account: dict | None = None, ) -> list[str]: warnings: list[str] = [] if account is None: if account_id: account = config.get("accounts", {}).get(account_id, {}) if account is None: return warnings acct = _dict_to_account(account) acct = _apply_env_overrides(acct) if acct.dm_policy == "open": warnings.append("DM 策略为 open,任何人可私信 Bot") if acct.group_policy == "open": warnings.append("Group 策略为 open,任何群组可 @Bot") if not acct.outgoing_webhook_token and not acct.event_listener_token: warnings.append("Outgoing Webhook token 和 Event Listener token 均未配置,Webhook 验证将跳过") if acct.event_listener_token and not acct.app_secret: warnings.append("Event Listener token 已配置但 app_secret 未配置,Event Token HMAC 签名验证将跳过") if not acct.bot_token and not acct.incoming_webhook_url: warnings.append("bot_token 和 incoming_webhook_url 均未配置,无法发送消息") return warnings def collect_audit_findings( config, account_id=None, account=None, source_config=None, ordered_account_ids=None, has_explicit_account_path=False, ) -> list[dict]: findings: list[dict] = [] if account is None and account_id: account = config.get("accounts", {}).get(account_id, {}) if account is None: return findings acct = _dict_to_account(account) acct = _apply_env_overrides(acct) if not acct.bot_token and not acct.incoming_webhook_url: findings.append({ "severity": "error", "category": "auth", "message": "bot_token 和 incoming_webhook_url 均未配置,无法发送消息", }) if acct.dm_policy == "open" and not acct.dm_allow_from: findings.append({ "severity": "warning", "category": "dm_policy", "message": "DM 策略为 open 且未配置 dm_allow_from,任何人可私信 Bot", }) if acct.group_policy == "open" and not acct.require_mention: findings.append({ "severity": "warning", "category": "group_policy", "message": "Group 策略为 open 且 require_mention=False,任何群组消息都会被处理", }) if acct.event_listener_token and not acct.app_secret: findings.append({ "severity": "error", "category": "security", "message": "Event Listener token 已配置但 app_secret 未配置,Event Token HMAC 签名验证将跳过", }) if not acct.event_listener_token and not acct.outgoing_webhook_token: findings.append({ "severity": "info", "category": "security", "message": "Event Listener token 和 Outgoing Webhook token 均未配置,Webhook 来源无法验证", }) return findings def normalize_allow_entry(entry: str) -> str: stripped = entry.strip() if not stripped.startswith("u:") and not stripped.startswith("g:"): stripped = f"u:{stripped}" return stripped def is_valid_flock_user_id(id_str: str) -> bool: return bool(_user_id_re.match(id_str)) def is_valid_flock_group_id(id_str: str) -> bool: return bool(_group_id_re.match(id_str)) def resolve_require_mention(config: dict, account_id: str = "default") -> bool: account_data = config.get("accounts", {}).get(account_id, {}) account = _dict_to_account(account_data) account = _apply_env_overrides(account) return account.require_mention