Files
tg-spam-guard/services/bind_service.py
T

121 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""账号绑定服务 — 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_id5分钟过期)
_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