ForcePilot/backend/package/yuxi/channels/adapters/mattermost/approval.py
Kris 002d601d1b feat(mattermost): 实现完整的 Mattermost 适配器模块
新增 Mattermost 渠道完整实现,包含适配器核心、消息处理、交互回调、命令支持、安全校验、多账号管理等功能,支持机器人消息发送、交互按钮、命令注册、投票功能以及配置动态修改等特性。
2026-05-12 00:46:12 +08:00

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 True
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]