新增 Google Chat 对接的全套工具模块,包括: - 会话线程管理、消息解析与格式化 - Pub/Sub 消息解码、提及和命令识别 - 权限审批、目录管理和审计日志 - 消息缓存、媒体上传下载和 SSFR 防护 - 策略配置、卡片构建和流式回复支持
83 lines
2.7 KiB
Python
83 lines
2.7 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import time
|
|
|
|
import httpx
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
_CERT_URL = "https://www.googleapis.com/service_accounts/v1/metadata/x509/chat@system.gserviceaccount.com"
|
|
_CERT_CACHE_TTL_S = 600
|
|
_CERT_FETCH_TIMEOUT_S = 10.0
|
|
|
|
|
|
class GoogleChatCertCache:
|
|
def __init__(self, ttl_s: int = _CERT_CACHE_TTL_S):
|
|
self._ttl = ttl_s
|
|
self._cache: dict[str, str] | None = None
|
|
self._cache_at: float = 0.0
|
|
|
|
@property
|
|
def is_valid(self) -> bool:
|
|
return self._cache is not None and (time.monotonic() - self._cache_at) < self._ttl
|
|
|
|
async def get_certs(self) -> dict[str, str]:
|
|
if self.is_valid and self._cache is not None:
|
|
return dict(self._cache)
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=_CERT_FETCH_TIMEOUT_S) as client:
|
|
resp = await client.get(_CERT_URL)
|
|
resp.raise_for_status()
|
|
certs: dict[str, str] = resp.json()
|
|
self._cache = certs
|
|
self._cache_at = time.monotonic()
|
|
logger.info(f"Google Chat cert cache refreshed: {len(certs)} certificates")
|
|
return dict(certs)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to fetch Google Chat certs: {e}")
|
|
if self._cache is not None:
|
|
logger.info("Using stale cert cache")
|
|
return dict(self._cache)
|
|
return {}
|
|
|
|
def clear(self) -> None:
|
|
self._cache = None
|
|
self._cache_at = 0.0
|
|
|
|
|
|
def verify_project_number_token(token: str, project_number: str, certs: dict[str, str]) -> bool:
|
|
try:
|
|
from google.auth import jwt
|
|
except ImportError:
|
|
logger.warning("google-auth not installed, skipping project-number token verification")
|
|
return True
|
|
|
|
if not certs:
|
|
logger.warning("No certificates available for project-number verification")
|
|
return False
|
|
|
|
certs_json = json.dumps(certs)
|
|
try:
|
|
payload = jwt.decode(token, certs=certs_json, audience=project_number)
|
|
issuer = payload.get("iss", "")
|
|
expected_issuer = "chat@system.gserviceaccount.com"
|
|
if issuer != expected_issuer:
|
|
logger.warning(f"project-number token issuer mismatch: expected '{expected_issuer}', got '{issuer}'")
|
|
return False
|
|
return True
|
|
except Exception as e:
|
|
logger.warning(f"project-number token verification failed: {e}")
|
|
return False
|
|
|
|
|
|
_global_cert_cache: GoogleChatCertCache | None = None
|
|
|
|
|
|
def get_cert_cache() -> GoogleChatCertCache:
|
|
global _global_cert_cache
|
|
if _global_cert_cache is None:
|
|
_global_cert_cache = GoogleChatCertCache()
|
|
return _global_cert_cache
|