from __future__ import annotations import json import os import threading from typing import TYPE_CHECKING if TYPE_CHECKING: from googleapiclient.discovery import Resource from yuxi.channels.models import DeliveryResult from yuxi.utils.logging_config import logger _PAIRING_CHALLENGE_MESSAGE = ( "{agent_name} Approval Required\n\n" "To continue, send the following:\n" "```\n/approve {user_id}\n```\n" "This confirmation is required to pair with this bot account." ) class PairingStore: def __init__(self, storage_path: str = ""): self._paired_users: dict[str, str] = {} self._lock = threading.Lock() self._storage_path = storage_path or os.path.join( os.path.expanduser("~"), ".forcepilot", "googlechat_pairing.json" ) self._load() def _load(self) -> None: try: if os.path.exists(self._storage_path): with open(self._storage_path, encoding="utf-8") as f: self._paired_users = json.load(f) logger.debug(f"Loaded {len(self._paired_users)} paired users from {self._storage_path}") except Exception as e: logger.warning(f"Failed to load pairing store: {e}") self._paired_users = {} def _save(self) -> None: try: os.makedirs(os.path.dirname(self._storage_path), exist_ok=True) with open(self._storage_path, "w", encoding="utf-8") as f: json.dump(self._paired_users, f, indent=2) except Exception as e: logger.warning(f"Failed to save pairing store: {e}") def is_paired(self, space_id: str) -> bool: with self._lock: return space_id in self._paired_users def pair(self, space_id: str, user_id: str) -> None: with self._lock: self._paired_users[space_id] = user_id self._save() def unpair(self, space_id: str) -> bool: with self._lock: if space_id in self._paired_users: del self._paired_users[space_id] self._save() return True return False def get_paired_user(self, space_id: str) -> str | None: with self._lock: return self._paired_users.get(space_id) def list_paired_spaces(self) -> list[str]: with self._lock: return list(self._paired_users.keys()) def clear(self) -> None: with self._lock: self._paired_users.clear() self._save() _global_pairing_store: PairingStore | None = None def get_pairing_store(storage_path: str = "") -> PairingStore: global _global_pairing_store if _global_pairing_store is None: _global_pairing_store = PairingStore(storage_path) return _global_pairing_store async def send_pairing_challenge( chat_service: Resource, space_name: str, user_id: str, agent_name: str = "Bot", ) -> DeliveryResult: content = _PAIRING_CHALLENGE_MESSAGE.format( agent_name=agent_name, user_id=user_id, ) body = {"text": content} try: result = chat_service.spaces().messages().create(parent=space_name, body=body).execute() msg_id = result.get("name", "") logger.info(f"Pairing challenge sent to {space_name}: user_id={user_id}") return DeliveryResult(success=True, message_id=msg_id) except Exception as e: logger.warning(f"Failed to send pairing challenge to {space_name}: {e}") return DeliveryResult(success=False, error=str(e)) async def check_pairing_approval( chat_service: Resource, space_name: str, user_id: str, message_text: str, ) -> bool: approved = f"/approve {user_id}" in message_text.strip().lower() if approved: logger.info(f"Pairing approved for {space_name}: user_id={user_id}") return approved