ForcePilot/backend/package/yuxi/channels/adapters/googlechat/pairing.py

121 lines
3.8 KiB
Python
Raw Normal View History

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