fix: fallback command replies when original message is unavailable

This commit is contained in:
ngfchl
2026-07-07 10:14:34 +08:00
parent c377034e00
commit 52ee83a235
3 changed files with 42 additions and 26 deletions
+17 -16
View File
@@ -10,6 +10,7 @@ import config
from db.models import Action, Ban, Message, Warning
from services.admin_utils import require_admin, resolve_target_user
from services.stats_service import get_daily_stats
from services.message_utils import safe_reply
logger = logging.getLogger("spam_guard")
@@ -77,7 +78,7 @@ async def cmd_stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
f"🔓 解封: {stats['unbans']}\n"
f"📦 历史封禁: {stats['total_bans']}\n"
)
reply = await update.message.reply_text(text)
reply = await safe_reply(update, context, text)
asyncio.create_task(auto_delete(reply))
@@ -88,7 +89,7 @@ async def cmd_ban(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id, target = await resolve_target_user(update, context.args)
if not user_id:
reply = await update.message.reply_text("用法: 回复用户消息 /ban,或 /ban 用户ID/@历史用户名")
reply = await safe_reply(update, context, "用法: 回复用户消息 /ban,或 /ban 用户ID/@历史用户名")
asyncio.create_task(auto_delete(reply))
return
@@ -108,7 +109,7 @@ async def cmd_ban(update: Update, context: ContextTypes.DEFAULT_TYPE):
reply = await update.effective_chat.send_message(f"🔨 已封禁 {target} ({user_id}) {config.BAN_DURATION_MINUTES} 分钟")
except Exception as e:
logger.error(f"手动封禁失败: {type(e).__name__}: {e}")
reply = await update.message.reply_text("❌ 封禁失败,请检查 Bot 权限或目标用户状态")
reply = await safe_reply(update, context, "❌ 封禁失败,请检查 Bot 权限或目标用户状态")
asyncio.create_task(auto_delete(reply))
@@ -120,7 +121,7 @@ async def cmd_unban(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id, target = await resolve_target_user(update, context.args)
if not user_id:
reply = await update.message.reply_text("用法: /unban 用户ID/@历史用户名")
reply = await safe_reply(update, context, "用法: /unban 用户ID/@历史用户名")
asyncio.create_task(auto_delete(reply))
return
@@ -128,10 +129,10 @@ async def cmd_unban(update: Update, context: ContextTypes.DEFAULT_TYPE):
await context.bot.unban_chat_member(chat_id=config.TG_CHAT_ID, user_id=user_id)
await Ban.filter(user_id=user_id).update(auto_unban=False, unban_at=datetime.now())
await Action.create(action="UNBAN", user_id=user_id, username=target, operator_id=update.effective_user.id)
reply = await update.message.reply_text(f"🔓 已解封用户 {target} ({user_id})")
reply = await safe_reply(update, context, f"🔓 已解封用户 {target} ({user_id})")
except Exception as e:
logger.error(f"手动解封失败: {type(e).__name__}: {e}")
reply = await update.message.reply_text("❌ 解封失败,请检查 Bot 权限或目标用户状态")
reply = await safe_reply(update, context, "❌ 解封失败,请检查 Bot 权限或目标用户状态")
asyncio.create_task(auto_delete(reply))
@@ -143,7 +144,7 @@ async def cmd_warn(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id, target = await resolve_target_user(update, context.args)
if not user_id:
reply = await update.message.reply_text("用法: 回复用户消息 /warn [原因],或 /warn 用户ID/@历史用户名 [原因]")
reply = await safe_reply(update, context, "用法: 回复用户消息 /warn [原因],或 /warn 用户ID/@历史用户名 [原因]")
asyncio.create_task(auto_delete(reply))
return
@@ -162,10 +163,10 @@ async def cmd_warn(update: Update, context: ContextTypes.DEFAULT_TYPE):
await delete_command_message(update)
reply = await update.effective_chat.send_message(f"⚠️ 已警告 {target} ({user_id})\n原因: {reason}\n累计警告: {warn_count}")
else:
reply = await update.message.reply_text(f"⚠️ 已警告 {target} ({user_id})\n原因: {reason}\n累计警告: {warn_count}")
reply = await safe_reply(update, context, f"⚠️ 已警告 {target} ({user_id})\n原因: {reason}\n累计警告: {warn_count}")
except Exception as e:
logger.error(f"警告失败: {type(e).__name__}: {e}")
reply = await update.message.reply_text("❌ 警告失败,请检查 Bot 权限或数据库状态")
reply = await safe_reply(update, context, "❌ 警告失败,请检查 Bot 权限或数据库状态")
asyncio.create_task(auto_delete(reply))
@@ -176,7 +177,7 @@ async def cmd_mute(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id, target = await resolve_target_user(update, context.args)
if not user_id:
reply = await update.message.reply_text("用法: 回复用户消息 /mute [分钟],或 /mute 用户ID/@历史用户名 [分钟]")
reply = await safe_reply(update, context, "用法: 回复用户消息 /mute [分钟],或 /mute 用户ID/@历史用户名 [分钟]")
asyncio.create_task(auto_delete(reply))
return
@@ -200,10 +201,10 @@ async def cmd_mute(update: Update, context: ContextTypes.DEFAULT_TYPE):
await delete_command_message(update)
reply = await update.effective_chat.send_message(f"🔇 已禁言 {target} ({user_id}) {minutes} 分钟")
else:
reply = await update.message.reply_text(f"🔇 已禁言 {target} ({user_id}) {minutes} 分钟")
reply = await safe_reply(update, context, f"🔇 已禁言 {target} ({user_id}) {minutes} 分钟")
except Exception as e:
logger.error(f"禁言失败: {type(e).__name__}: {e}")
reply = await update.message.reply_text("❌ 禁言失败,请检查 Bot 权限或目标用户状态")
reply = await safe_reply(update, context, "❌ 禁言失败,请检查 Bot 权限或目标用户状态")
asyncio.create_task(auto_delete(reply))
@@ -212,7 +213,7 @@ async def require_reply_shortcut(update: Update, command: str, usage: str) -> bo
"""引用消息快捷管理命令必须回复目标消息。"""
if update.message and update.message.reply_to_message:
return True
reply = await update.message.reply_text(f"用法: 回复目标消息后使用 {usage}")
reply = await safe_reply(update, context, f"用法: 回复目标消息后使用 {usage}")
asyncio.create_task(auto_delete(reply))
return False
@@ -250,12 +251,12 @@ async def cmd_purge(update: Update, context: ContextTypes.DEFAULT_TYPE):
return
if not context.args or not context.args[0].isdigit():
reply = await update.message.reply_text("用法: 回复一条消息后 /purge 数量(最多50)")
reply = await safe_reply(update, context, "用法: 回复一条消息后 /purge 数量(最多50)")
asyncio.create_task(auto_delete(reply))
return
if not update.message.reply_to_message:
reply = await update.message.reply_text("❌ /purge 需要回复一条消息作为删除起点")
reply = await safe_reply(update, context, "❌ /purge 需要回复一条消息作为删除起点")
asyncio.create_task(auto_delete(reply))
return
@@ -305,7 +306,7 @@ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
"↪️ 回复消息 /ban /mute /warn 也支持同样处理\n"
"🧹 回复消息 /purge 数量 - 从被引用消息开始尝试批量删除,最多 50 条\n"
)
reply = await update.message.reply_text(text)
reply = await safe_reply(update, context, text)
asyncio.create_task(auto_delete(reply, seconds=60))
+11 -10
View File
@@ -8,6 +8,7 @@ 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")
@@ -27,7 +28,7 @@ async def cmd_bind(update: Update, context: ContextTypes.DEFAULT_TYPE):
f"📅 绑定时间: {existing.get('bound_at')}\n\n"
"如需重新绑定,先 /unbind"
)
reply = await update.message.reply_text(text)
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
@@ -42,7 +43,7 @@ async def cmd_bind(update: Update, context: ContextTypes.DEFAULT_TYPE):
f"👉 点击进入:{url}"
)
try:
reply = await update.message.reply_text(text, reply_markup=keyboard, disable_web_page_preview=True)
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)
@@ -51,7 +52,7 @@ async def cmd_bind(update: Update, context: ContextTypes.DEFAULT_TYPE):
return
context.user_data["awaiting_bind"] = True
await update.message.reply_text(
await safe_reply(update, context,
"🔐 授权绑定\n"
"请发送邮箱和 Token,支持空格或换行分隔:\n\n"
"邮箱 Token\n\n"
@@ -71,15 +72,15 @@ async def handle_private_bind_text(update: Update, context: ContextTypes.DEFAULT
text = (update.message.text or "").strip()
parts = text.replace("\n", " ").split()
if len(parts) < 2:
await update.message.reply_text("❌ 格式不对,请发送:邮箱 Token")
await safe_reply(update, context, "❌ 格式不对,请发送:邮箱 Token")
return
email, token = parts[0].strip(), parts[1].strip()
if not EMAIL_RE.match(email):
await update.message.reply_text("❌ 邮箱格式不正确,请重新发送:邮箱 Token")
await safe_reply(update, context, "❌ 邮箱格式不正确,请重新发送:邮箱 Token")
return
processing = await update.message.reply_text("⏳ 正在验证授权信息...")
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,
@@ -96,14 +97,14 @@ async def handle_private_bind_text(update: Update, context: ContextTypes.DEFAULT
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 update.message.reply_text(
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 update.message.reply_text(f"❌ 验证失败:{result.get('msg') or result.get('reason') or '授权信息无效'}\n请重新发送:邮箱 Token")
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):
@@ -111,10 +112,10 @@ async def cmd_unbind(update: Update, context: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
deleted = await unbind(user.id)
if deleted:
reply = await update.message.reply_text("🔓 已解除账号绑定")
reply = await safe_reply(update, context, "🔓 已解除账号绑定")
await Action.create(action="UNBIND", user_id=user.id, username=user.username)
else:
reply = await update.message.reply_text("❌ 你没有绑定的账号")
reply = await safe_reply(update, context, "❌ 你没有绑定的账号")
asyncio.create_task(auto_delete(update.message, seconds=60))
asyncio.create_task(auto_delete(reply, seconds=60))
+14
View File
@@ -23,3 +23,17 @@ async def auto_delete(message, seconds: int | None = None) -> None:
def schedule_delete(message, seconds: int | None = None) -> None:
"""创建自动删除任务。"""
asyncio.create_task(auto_delete(message, seconds=seconds))
async def safe_reply(update, context, text: str, **kwargs):
"""优先回复原消息;原消息已被删除/不可回复时,降级为普通群消息。"""
try:
if update and update.message:
return await update.message.reply_text(text, **kwargs)
except Exception as e:
if "Message to be replied not found" not in str(e):
logger.warning(f"回复消息失败,降级普通发送: {type(e).__name__}: {e}")
chat_id = update.effective_chat.id if update and update.effective_chat else None
if not chat_id:
raise RuntimeError("missing chat_id for safe_reply")
return await context.bot.send_message(chat_id=chat_id, text=text, **kwargs)