这是一个批量整理提交,包含以下主要改动: 1. 删除多处冗余的空行和未使用的导入 2. 修复文件末尾缺少换行符的问题 3. 调整部分模块的导入顺序与代码排版 4. 修复部分配置默认值与策略逻辑 5. 新增多个功能模块与辅助工具 6. 完善异常处理与日志记录 7. 修复速率限制、消息缓存、权限校验等逻辑bug 8. 废弃部分旧有API与配置项并添加警告提示
129 lines
4.0 KiB
Python
129 lines
4.0 KiB
Python
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]
|