ForcePilot/backend/package/yuxi/channel/extensions/flock/pairing.py
Kris 0bad70ec19 feat(flock): 新增Flock团队协作IM渠道插件
实现了完整的Flock渠道接入能力,包含消息收发、Webhook事件监听、账号配置、安全校验、媒体文件处理等功能,支持私聊和群组聊天,适配ForcePilot插件规范。
2026-05-21 10:47:01 +08:00

122 lines
3.8 KiB
Python

from __future__ import annotations
import logging
import secrets
import time
from .constants import (
PAIRING_CODE_TTL_SECONDS,
PAIRING_MAX_ATTEMPTS,
PAIRING_RATE_LIMIT_PER_PEER,
)
logger = logging.getLogger(__name__)
ID_LABEL = "flockUserId"
_RATE_LIMIT_WINDOW = 60
_PAIRING_CODES: dict[str, tuple[str, float, int]] = {}
_GENERATE_TIMESTAMPS: dict[str, list[float]] = {}
def _cleanup_expired() -> None:
now = time.monotonic()
expired = [k for k, (_, ts, _) in _PAIRING_CODES.items() if now - ts > PAIRING_CODE_TTL_SECONDS]
for k in expired:
del _PAIRING_CODES[k]
def _cleanup_rate_limits() -> None:
now = time.monotonic()
for key in list(_GENERATE_TIMESTAMPS):
_GENERATE_TIMESTAMPS[key] = [ts for ts in _GENERATE_TIMESTAMPS[key] if now - ts < _RATE_LIMIT_WINDOW]
if not _GENERATE_TIMESTAMPS[key]:
del _GENERATE_TIMESTAMPS[key]
async def generate_code(peer_id: str, config: dict, account_id: str = "default") -> str:
_cleanup_expired()
_cleanup_rate_limits()
now = time.monotonic()
recent = _GENERATE_TIMESTAMPS.get(peer_id, [])
recent = [ts for ts in recent if now - ts < _RATE_LIMIT_WINDOW]
if len(recent) >= PAIRING_RATE_LIMIT_PER_PEER:
logger.warning("Flock pairing rate limit exceeded for peer %s", peer_id)
raise RuntimeError(
f"Rate limit exceeded: max {PAIRING_RATE_LIMIT_PER_PEER} codes "
f"per {_RATE_LIMIT_WINDOW}s per peer"
)
recent.append(now)
_GENERATE_TIMESTAMPS[peer_id] = recent
code = f"{secrets.randbelow(1_000_000):06d}"
_PAIRING_CODES[peer_id] = (code, time.monotonic(), 0)
logger.info("Flock pairing code generated for peer %s", peer_id)
return code
async def verify_code(peer_id: str, code: str, config: dict, account_id: str = "default") -> bool:
_cleanup_expired()
stored = _PAIRING_CODES.get(peer_id)
if stored is None:
return False
stored_code, created_at, attempts = stored
if time.monotonic() - created_at > PAIRING_CODE_TTL_SECONDS:
_PAIRING_CODES.pop(peer_id, None)
logger.info("Flock pairing code expired for peer %s", peer_id)
return False
attempts += 1
if attempts > PAIRING_MAX_ATTEMPTS:
_PAIRING_CODES.pop(peer_id, None)
logger.warning("Flock pairing max attempts exceeded for peer %s", peer_id)
return False
if not secrets.compare_digest(stored_code, code):
_PAIRING_CODES[peer_id] = (stored_code, created_at, attempts)
logger.info("Flock pairing code mismatch for peer %s (attempt %d/%d)", peer_id, attempts, PAIRING_MAX_ATTEMPTS)
return False
_PAIRING_CODES.pop(peer_id, None)
logger.info("Flock pairing code verified for peer %s", peer_id)
return True
async def notify_approval(config: dict, peer_id: str, account_id: str = "default") -> None:
logger.info("Flock pairing approved for peer %s (account=%s)", peer_id, account_id)
try:
from .config import _apply_env_overrides, _dict_to_account
from .outbound import send_text as _ob_send_text
account_data = config.get("accounts", {}).get(account_id, {})
account = _dict_to_account(account_data)
account = _apply_env_overrides(account)
if not account.bot_token:
logger.warning("Cannot notify approval: no bot_token for account %s", account_id)
return
await _ob_send_text(
peer_id,
"您的配对已通过验证,现在可以与 Bot 私聊了",
account_id=account_id,
config=config,
)
except Exception:
logger.warning("Failed to send approval notification to peer %s", peer_id, exc_info=True)
def normalize_allow_entry(entry: str) -> str:
from .security import normalize_allow_entry as _sec_normalize
return _sec_normalize(entry)
id_label = ID_LABEL