from __future__ import annotations import hashlib import hmac import time from dataclasses import dataclass from typing import Any APPROVAL_TIMEOUT_S = 300 APPROVAL_MAX_RETRIES = 3 @dataclass class ApprovalRequest: request_id: str action: str description: str params: dict[str, Any] from_user_id: str = "" from_channel_id: str = "" created_at: float = 0.0 status: str = "pending" # pending, approved, denied, expired, cancelled @property def expired(self) -> bool: return time.monotonic() - self.created_at > APPROVAL_TIMEOUT_S @dataclass class ApprovalConfig: enabled: bool = False required_for: list[str] = None approval_timeout_s: int = APPROVAL_TIMEOUT_S approval_secret: str = "" def __post_init__(self): if self.required_for is None: self.required_for = ["restart", "sudo", "exec", "delete"] @classmethod def from_config(cls, config: dict) -> ApprovalConfig: return cls( enabled=bool(config.get("approval_enabled", False)), required_for=config.get("approval_required_for", ["restart", "sudo", "exec", "delete"]), approval_timeout_s=config.get("approval_timeout_s", APPROVAL_TIMEOUT_S), approval_secret=config.get("approval_secret", ""), ) class ApprovalManager: def __init__(self, config: dict | None = None): self._config = ApprovalConfig.from_config(config or {}) self._pending_approvals: dict[str, ApprovalRequest] = {} @property def enabled(self) -> bool: return self._config.enabled def requires_approval(self, action: str) -> bool: if not self._config.enabled: return False return action in self._config.required_for def create_request( self, action: str, description: str, params: dict[str, Any], from_user_id: str = "", from_channel_id: str = "", ) -> ApprovalRequest: request_id = _generate_request_id(action, from_user_id) req = ApprovalRequest( request_id=request_id, action=action, description=description, params=params, from_user_id=from_user_id, from_channel_id=from_channel_id, created_at=time.monotonic(), ) self._pending_approvals[request_id] = req return req def approve(self, request_id: str) -> bool: req = self._pending_approvals.get(request_id) if req is None or req.expired: return False req.status = "approved" return True def deny(self, request_id: str) -> bool: req = self._pending_approvals.get(request_id) if req is None or req.expired: return False req.status = "denied" return True def get_request(self, request_id: str) -> ApprovalRequest | None: req = self._pending_approvals.get(request_id) if req and req.expired: req.status = "expired" return req return req def build_approval_message(self, req: ApprovalRequest) -> str: return ( f"⚠️ **需要审批**\n" f"操作: `{req.action}`\n" f"描述: {req.description}\n" f"发起者: <@{req.from_user_id}>\n" f"审批 ID: `{req.request_id}`\n\n" f"回复 `approve {req.request_id}` 批准\n" f"回复 `deny {req.request_id}` 拒绝\n" f"超时时间: {self._config.approval_timeout_s} 秒" ) def verify_hmac(self, payload: bytes, signature: str) -> bool: secret = self._config.approval_secret if not secret or not signature: return False expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) def _generate_request_id(action: str, user_id: str) -> str: raw = f"{action}:{user_id}:{time.time()}" return hashlib.sha256(raw.encode()).hexdigest()[:16]