188 lines
8.2 KiB
Python
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)
|