Files
2026-07-07 13:07:27 +08:00

188 lines
8.2 KiB
Python

"""账号绑定 handler"""
import asyncio
import logging
import re
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import ContextTypes, CommandHandler, MessageHandler, filters
from db.models import Action, GameScore
from services.bind_service import bind_with_email_token, unbind, get_binding
from services.message_utils import record_bot_message, safe_reply
from services.time_utils import format_dt
from services.game_service import get_scores_summary
logger = logging.getLogger("spam_guard")
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
async def cmd_bind(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""绑定授权账号。群内引导到私聊;私聊进入等待邮箱+Token 状态。"""
user = update.effective_user
logger.info(f"🔗 收到绑定指令: user={user.id}, chat_type={update.effective_chat.type}")
existing = await get_binding(user.id)
if existing:
text = (
"🔗 你已绑定授权账号\n"
f"📧 邮箱: {existing.get('email') or existing.get('harvest_uid') or '-'}\n"
f"⏳ 到期: {existing.get('time_expire') or '-'}\n"
f"📅 绑定时间: {existing.get('bound_at')}\n\n"
"如需重新绑定,先 /unbind"
)
reply = await safe_reply(update, context, text)
asyncio.create_task(auto_delete(update.message, seconds=60))
await record_bot_message(reply)
asyncio.create_task(auto_delete(reply, seconds=60))
return
if update.effective_chat.type != "private":
me = await context.bot.get_me()
bot_username = me.username
url = f"https://t.me/{bot_username}?start=bind"
keyboard = InlineKeyboardMarkup([[InlineKeyboardButton("去私聊绑定", url=url)]])
text = (
"🔐 为了保护 Token,请到私聊完成绑定。\n\n"
f"👉 点击进入:{url}"
)
try:
reply = await safe_reply(update, context, text, reply_markup=keyboard, disable_web_page_preview=True)
except Exception as e:
logger.warning(f"🔗 绑定跳转按钮发送失败,改发普通文本: {type(e).__name__}: {e}")
reply = await update.effective_chat.send_message(text, disable_web_page_preview=True)
asyncio.create_task(auto_delete(update.message, seconds=60))
await record_bot_message(reply)
asyncio.create_task(auto_delete(reply, seconds=60))
return
context.user_data["awaiting_bind"] = True
await safe_reply(update, context,
"🔐 授权绑定\n"
"请发送邮箱和 Token,支持空格或换行分隔:\n\n"
"邮箱 Token\n\n"
"例如:user@example.com xxxxx"
)
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
if update.effective_chat.type == "private" and context.args and context.args[0] == "bind":
await cmd_bind(update, context)
async def handle_private_bind_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
if update.effective_chat.type != "private" or not context.user_data.get("awaiting_bind"):
return
text = (update.message.text or "").strip()
parts = text.replace("\n", " ").split()
if len(parts) < 2:
await safe_reply(update, context, "❌ 格式不对,请发送:邮箱 Token")
return
email, token = parts[0].strip(), parts[1].strip()
if not EMAIL_RE.match(email):
await safe_reply(update, context, "❌ 邮箱格式不正确,请重新发送:邮箱 Token")
return
processing = await safe_reply(update, context, "⏳ 正在验证授权信息...")
ok, result = await bind_with_email_token(
tg_user_id=update.effective_user.id,
tg_username=update.effective_user.username or update.effective_user.first_name,
email=email,
token=token,
)
context.user_data["awaiting_bind"] = False
try:
await processing.delete()
except Exception:
pass
if ok:
data = result.get("data") or {}
await Action.create(action="BIND", user_id=update.effective_user.id, username=update.effective_user.username, details={"email": data.get("email")})
await safe_reply(update, context,
"✅ 绑定成功\n"
f"📧 邮箱: {data.get('email') or email}\n"
f"⏳ 到期: {data.get('time_expire') or '-'}"
)
else:
context.user_data["awaiting_bind"] = True
await safe_reply(update, context, f"❌ 验证失败:{result.get('msg') or result.get('reason') or '授权信息无效'}\n请重新发送:邮箱 Token")
async def cmd_me(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看当前 TG 用户绑定的授权信息和积分。"""
user = update.effective_user
binding = await get_binding(user.id)
summary = await get_scores_summary(user.id)
scores = summary["scores"]
if scores:
score_lines = []
for item in scores:
last = format_dt(item.last_played) if item.last_played else "未参与"
score_lines.append(f"🎮 {item.game_type}: {item.score} 分|次数 {item.total}|连续 {item.streak}|最近 {last}")
checkin = summary["by_type"].get("checkin")
trivia = summary["by_type"].get("trivia")
checkin_text = "✅ 今日签到:已签到" if checkin and checkin.last_played and checkin.last_played.strftime("%Y-%m-%d") == __import__("datetime").datetime.now().strftime("%Y-%m-%d") else "✅ 今日签到:未签到"
trivia_text = f"🎮 答题积分:{trivia.score} 分|正确 {trivia.wins}/{trivia.total}|连击 {trivia.streak}" if trivia else "🎮 答题积分:暂无"
score_text = f"📊 总积分:{summary['total']}\n{checkin_text}\n{trivia_text}\n" + "\n".join(score_lines)
else:
score_text = "📊 积分:暂无记录\n✅ 今日签到:未签到\n🎮 答题积分:暂无\n💡 发送 /checkin 签到,或 /trivia 答题获取积分"
if not binding:
text = (
"👤 我的账号\n"
"━━━━━━━━━━━━━━━━━━━━\n"
"🔗 授权状态:未绑定\n"
f"{score_text}\n\n"
"绑定授权后,我可以帮你查看授权邮箱和到期时间。\n"
"请发送 /bind,然后按提示到私聊里提交邮箱和 Token。"
)
else:
text = (
"👤 我的账号\n"
"━━━━━━━━━━━━━━━━━━━━\n"
"🔗 授权状态:已绑定\n"
f"📧 授权邮箱:{binding.get('email') or '-'}\n"
f"⏳ 到期时间:{binding.get('time_expire') or '未返回'}\n"
f"🕒 绑定时间:{format_dt(binding.get('bound_at')) or '-'}\n\n"
f"{score_text}\n\n"
"如需更换授权,请先 /unbind,再 /bind 重新绑定。"
)
reply = await safe_reply(update, context, text)
if update.effective_chat.type != "private":
asyncio.create_task(auto_delete(update.message, seconds=60))
await record_bot_message(reply)
asyncio.create_task(auto_delete(reply, seconds=60))
async def cmd_unbind(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""解绑 harvest 账号"""
user = update.effective_user
deleted = await unbind(user.id)
if deleted:
reply = await safe_reply(update, context, "🔓 已解除账号绑定")
await Action.create(action="UNBIND", user_id=user.id, username=user.username)
else:
reply = await safe_reply(update, context, "❌ 你没有绑定的账号")
asyncio.create_task(auto_delete(update.message, seconds=60))
await record_bot_message(reply)
asyncio.create_task(auto_delete(reply, seconds=60))
async def auto_delete(message, seconds=60):
try:
await asyncio.sleep(seconds)
await message.delete()
except Exception:
pass
def register_bind(app):
app.add_handler(CommandHandler("start", cmd_start, filters.ChatType.PRIVATE))
app.add_handler(CommandHandler("bind", cmd_bind))
app.add_handler(CommandHandler("me", cmd_me))
app.add_handler(CommandHandler("unbind", cmd_unbind))
app.add_handler(MessageHandler(filters.TEXT & filters.ChatType.PRIVATE & ~filters.COMMAND, handle_private_bind_text), group=0)