"""账号绑定服务 — TG ↔ Harvest""" import secrets import logging import httpx import config from db.models import UserBinding logger = logging.getLogger("spam_guard") VERIFY_URL = "https://repeat.ptools.fun/api/user/users/verify" # 临时绑定 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 bind_with_email_token(tg_user_id: int, tg_username: str, email: str, token: str) -> tuple[bool, dict]: """调用授权接口验证邮箱+Token,成功后绑定 TG 用户。""" try: async with httpx.AsyncClient(timeout=15) as client: resp = await client.post(VERIFY_URL, json={"email": email, "token": token}) data = resp.json() except Exception as e: logger.warning(f"🔗 授权验证请求失败: {type(e).__name__}: {e}") return False, {"reason": "验证服务暂时不可用"} if not data.get("succeed"): return False, data auth = data.get("data") or {} auth_email = auth.get("email") or email time_expire = auth.get("time_expire") binding, created = await UserBinding.get_or_create( tg_user_id=tg_user_id, defaults={ "tg_username": tg_username, "harvest_uid": auth_email, "harvest_token": token, "auth_email": auth_email, "time_expire": time_expire, }, ) if not created: binding.tg_username = tg_username binding.harvest_uid = auth_email binding.harvest_token = token binding.auth_email = auth_email binding.time_expire = time_expire await binding.save() logger.info(f"🔗 授权绑定成功: TG={tg_user_id} email={auth_email}") return True, data 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, "email": b.auth_email or b.harvest_uid, "time_expire": b.time_expire, "bound_at": b.bound_at} return None