"""账号绑定服务 — TG ↔ Harvest""" import secrets import logging import httpx import config from db.models import UserBinding logger = logging.getLogger("spam_guard") # 临时绑定 token 存储(token -> user_id,5分钟过期) _pending_bindings: dict[str, dict] = {} def generate_bind_token(tg_user_id: int, tg_username: str) -> tuple[str, str]: """生成绑定链接,返回 (token, url)""" token = secrets.token_urlsafe(16) _pending_bindings[token] = { "tg_user_id": tg_user_id, "tg_username": tg_username, } url = f"{config.HARVEST_SERVER_URL}/bind?token={token}&tg_user_id={tg_user_id}" return token, url async def verify_binding(token: str) -> dict: """验证绑定(harvest-server 回调时调用)""" info = _pending_bindings.pop(token, None) if not info: return {"ok": False, "reason": "invalid_or_expired_token"} tg_user_id = info["tg_user_id"] tg_username = info["tg_username"] # 尝试调用 harvest-server verify try: async with httpx.AsyncClient(timeout=10) as client: resp = await client.post( f"{config.HARVEST_SERVER_URL}/api/verify", json={"token": token, "tg_user_id": tg_user_id}, ) if resp.status_code == 200: data = resp.json() harvest_uid = data.get("uid", "") harvest_token = data.get("token", "") else: harvest_uid = str(tg_user_id) harvest_token = token except Exception: harvest_uid = str(tg_user_id) harvest_token = token # 写入绑定 binding, _ = await UserBinding.get_or_create( tg_user_id=tg_user_id, defaults={ "tg_username": tg_username, "harvest_uid": harvest_uid, "harvest_token": harvest_token, }, ) logger.info(f"🔗 账号绑定成功: TG={tg_user_id} Harvest={harvest_uid}") return {"ok": True, "harvest_uid": harvest_uid} async def unbind(tg_user_id: int) -> bool: """解绑""" deleted = await UserBinding.filter(tg_user_id=tg_user_id).delete() if deleted: logger.info(f"🔗 账号解绑: TG={tg_user_id}") return deleted > 0 async def get_binding(tg_user_id: int) -> dict | None: """查询绑定""" b = await UserBinding.filter(tg_user_id=tg_user_id).first() if b: return {"harvest_uid": b.harvest_uid, "bound_at": b.bound_at} return None