实现了完整的Flock渠道接入能力,包含消息收发、Webhook事件监听、账号配置、安全校验、媒体文件处理等功能,支持私聊和群组聊天,适配ForcePilot插件规范。
122 lines
3.8 KiB
Python
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
|