"""账号绑定 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)