from __future__ import annotations import time from dataclasses import dataclass, field from enum import StrEnum from typing import Any from yuxi.utils.logging_config import logger class ApprovalStatus(StrEnum): PENDING = "pending" APPROVED = "approved" REJECTED = "rejected" EXPIRED = "expired" @dataclass class ApprovalRequest: request_id: str channel_chat_id: str channel_user_id: str action: str action_payload: dict[str, Any] = field(default_factory=dict) status: ApprovalStatus = ApprovalStatus.PENDING created_at: float = field(default_factory=time.time) expires_at: float = field(default_factory=lambda: time.time() + 300) approved_by: str = "" result: Any = None class ExecApprovalManager: def __init__(self, approval_ttl_s: float = 300.0, max_pending: int = 10): self._pending: dict[str, ApprovalRequest] = {} self._history: list[ApprovalRequest] = [] self._approval_ttl = approval_ttl_s self._max_pending = max_pending def request_approval( self, request_id: str, channel_chat_id: str, channel_user_id: str, action: str, action_payload: dict[str, Any] | None = None, ) -> ApprovalRequest: self._cleanup() if len(self._pending) >= self._max_pending: oldest = min(self._pending.values(), key=lambda r: r.created_at) self._pending.pop(oldest.request_id, None) req = ApprovalRequest( request_id=request_id, channel_chat_id=channel_chat_id, channel_user_id=channel_user_id, action=action, action_payload=action_payload or {}, ) self._pending[request_id] = req logger.info(f"[iMessage/Approval] New request: {request_id} action={action}") return req def approve(self, request_id: str, approved_by: str = "") -> ApprovalRequest | None: self._cleanup() req = self._pending.get(request_id) if req is None: return None req.status = ApprovalStatus.APPROVED req.approved_by = approved_by self._pending.pop(request_id, None) self._history.append(req) logger.info(f"[iMessage/Approval] Approved: {request_id}") return req def reject(self, request_id: str, reason: str = "") -> ApprovalRequest | None: self._cleanup() req = self._pending.get(request_id) if req is None: return None req.status = ApprovalStatus.REJECTED self._pending.pop(request_id, None) self._history.append(req) logger.info(f"[iMessage/Approval] Rejected: {request_id} ({reason})") return req def get(self, request_id: str) -> ApprovalRequest | None: return self._pending.get(request_id) def list_pending(self) -> list[ApprovalRequest]: self._cleanup() return list(self._pending.values()) def _cleanup(self) -> None: now = time.time() expired = [rid for rid, req in self._pending.items() if req.expires_at < now] for rid in expired: req = self._pending.pop(rid) req.status = ApprovalStatus.EXPIRED self._history.append(req)