from __future__ import annotations import re from dataclasses import dataclass, field from typing import Any from yuxi.channels.models import DeliveryResult _APPROVER_ID_PATTERNS = { "slack_format": re.compile(r"^slack:(U[A-Z0-9]+)$"), "mention_format": re.compile(r"^<@(U[A-Z0-9]+)>$"), "plain_id": re.compile(r"^(U[A-Z0-9]+)$"), } def normalize_approver_id(approver_id: str) -> str: for pattern in _APPROVER_ID_PATTERNS.values(): match = pattern.match(approver_id.strip()) if match: return match.group(1) return approver_id.strip() @dataclass class ExecApprovalConfig: enabled: str = "auto" approvers: list[str] = field(default_factory=list) @property def normalized_approvers(self) -> list[str]: return [normalize_approver_id(a) for a in self.approvers] def is_approver(self, user_id: str) -> bool: normalized = normalize_approver_id(user_id) return normalized in self.normalized_approvers or not self.normalized_approvers @classmethod def from_config(cls, config: dict[str, Any] | None) -> ExecApprovalConfig: if not config: return cls() exec_approvals = config.get("execApprovals", {}) or {} return cls( enabled=str(exec_approvals.get("enabled", "auto")), approvers=list(exec_approvals.get("approvers", [])), ) def resolve_origin_target( turn_source: str = "", session_target: str = "", fallback_target: str = "", ) -> str: return turn_source or session_target or fallback_target def is_authorized_sender(user_id: str, config: ExecApprovalConfig) -> bool: return config.is_approver(user_id) async def push_approval_to_app_home( client, user_id: str, approval_id: str, title: str, detail: str, ) -> DeliveryResult: try: from yuxi.channels.adapters.slack.blocks import build_approval_blocks blocks = build_approval_blocks( title=title, detail=detail, confirm_action_id=f"exec_approve_{approval_id}", reject_action_id=f"exec_reject_{approval_id}", ) view = { "type": "home", "blocks": blocks, } result = await client.views_publish(user_id=user_id, view=view) ok = result.get("ok", False) return DeliveryResult( success=ok, error=result.get("error") if not ok else None, ) except Exception as e: return DeliveryResult(success=False, error=str(e))