fix: fallback command replies when original message is unavailable
This commit is contained in:
+17
-16
@@ -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
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user