Files
tg-spam-guard/handlers/bind.py
T

136 lines
5.4 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
from services.bind_service import bind_with_email_token, unbind, get_binding
from services.message_utils import safe_reply
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))
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))
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_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))
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("unbind", cmd_unbind))
app.add_handler(MessageHandler(filters.TEXT & filters.ChatType.PRIVATE & ~filters.COMMAND, handle_private_bind_text), group=0)