236 lines
9.9 KiB
Python
236 lines
9.9 KiB
Python
"""管理命令 handler"""
|
||
import asyncio
|
||
import logging
|
||
from datetime import datetime, timedelta
|
||
|
||
from telegram import Update
|
||
from telegram.ext import CommandHandler, ContextTypes, filters
|
||
|
||
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
|
||
|
||
logger = logging.getLogger("spam_guard")
|
||
|
||
|
||
async def cmd_stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""今日统计"""
|
||
stats = await get_daily_stats()
|
||
text = (
|
||
f"📊 今日群组统计 ({stats['date']})\n"
|
||
f"━━━━━━━━━━━━━━━━━━━━\n"
|
||
f"💬 消息: {stats['messages']} 条\n"
|
||
f"🗑️ 删除垃圾: {stats['deleted']} 条\n"
|
||
f"🚨 垃圾判定: {stats['spam_messages']} 条\n"
|
||
f"🆕 入群: {stats['joins']} 人\n"
|
||
f"🚪 退群: {stats['leaves']} 人\n"
|
||
f"🔨 封禁: {stats['today_bans']} 人\n"
|
||
f"🔓 解封: {stats['unbans']} 人\n"
|
||
f"📦 历史封禁: {stats['total_bans']} 条\n"
|
||
)
|
||
reply = await update.message.reply_text(text)
|
||
asyncio.create_task(auto_delete(update.message))
|
||
asyncio.create_task(auto_delete(reply))
|
||
|
||
|
||
async def cmd_ban(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""手动封禁。支持回复消息、用户ID、历史 username。"""
|
||
if not await require_admin(update, context):
|
||
return
|
||
|
||
user_id, target = await resolve_target_user(update, context.args)
|
||
if not user_id:
|
||
reply = await update.message.reply_text("用法: 回复用户消息 /ban,或 /ban 用户ID/@历史用户名")
|
||
asyncio.create_task(auto_delete(update.message))
|
||
asyncio.create_task(auto_delete(reply))
|
||
return
|
||
|
||
try:
|
||
unban_at = datetime.now() + timedelta(minutes=config.BAN_DURATION_MINUTES)
|
||
await update.effective_chat.ban_member(user_id=user_id, until_date=int(unban_at.timestamp()))
|
||
await Ban.create(
|
||
user_id=user_id,
|
||
username=target,
|
||
reason=f"手动封禁 by {update.effective_user.username or update.effective_user.id}",
|
||
method="command",
|
||
duration_minutes=config.BAN_DURATION_MINUTES,
|
||
unban_at=unban_at,
|
||
)
|
||
await Action.create(action="BAN", user_id=user_id, username=target, operator_id=update.effective_user.id)
|
||
reply = await update.message.reply_text(f"🔨 已封禁 {target} ({user_id}) {config.BAN_DURATION_MINUTES} 分钟")
|
||
except Exception as e:
|
||
logger.exception("手动封禁失败")
|
||
reply = await update.message.reply_text(f"❌ 封禁失败: {e}")
|
||
|
||
asyncio.create_task(auto_delete(update.message))
|
||
asyncio.create_task(auto_delete(reply))
|
||
|
||
|
||
async def cmd_unban(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""手动解封"""
|
||
if not await require_admin(update, context):
|
||
return
|
||
|
||
user_id, target = await resolve_target_user(update, context.args)
|
||
if not user_id:
|
||
reply = await update.message.reply_text("用法: /unban 用户ID/@历史用户名")
|
||
asyncio.create_task(auto_delete(update.message))
|
||
asyncio.create_task(auto_delete(reply))
|
||
return
|
||
|
||
try:
|
||
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})")
|
||
except Exception as e:
|
||
logger.exception("手动解封失败")
|
||
reply = await update.message.reply_text(f"❌ 解封失败: {e}")
|
||
|
||
asyncio.create_task(auto_delete(update.message))
|
||
asyncio.create_task(auto_delete(reply))
|
||
|
||
|
||
async def cmd_warn(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""警告用户"""
|
||
if not await require_admin(update, context):
|
||
return
|
||
|
||
user_id, target = await resolve_target_user(update, context.args)
|
||
if not user_id:
|
||
reply = await update.message.reply_text("用法: 回复用户消息 /warn [原因],或 /warn 用户ID/@历史用户名 [原因]")
|
||
asyncio.create_task(auto_delete(update.message))
|
||
asyncio.create_task(auto_delete(reply))
|
||
return
|
||
|
||
# 回复模式下 args 全部是原因;非回复模式下 args[1:] 是原因
|
||
if update.message.reply_to_message:
|
||
reason = " ".join(context.args) if context.args else "未说明原因"
|
||
else:
|
||
reason = " ".join(context.args[1:]) if len(context.args) > 1 else "未说明原因"
|
||
|
||
warn_count = await Warning.filter(user_id=user_id).count() + 1
|
||
await Warning.create(user_id=user_id, username=target, reason=reason, operator_id=update.effective_user.id)
|
||
await Action.create(action="WARN", user_id=user_id, username=target, operator_id=update.effective_user.id, details={"reason": reason})
|
||
|
||
reply = await update.message.reply_text(f"⚠️ 已警告 {target} ({user_id})\n原因: {reason}\n累计警告: {warn_count}")
|
||
asyncio.create_task(auto_delete(update.message))
|
||
asyncio.create_task(auto_delete(reply))
|
||
|
||
|
||
async def cmd_mute(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""禁言用户"""
|
||
if not await require_admin(update, context):
|
||
return
|
||
|
||
user_id, target = await resolve_target_user(update, context.args)
|
||
if not user_id:
|
||
reply = await update.message.reply_text("用法: 回复用户消息 /mute [分钟],或 /mute 用户ID/@历史用户名 [分钟]")
|
||
asyncio.create_task(auto_delete(update.message))
|
||
asyncio.create_task(auto_delete(reply))
|
||
return
|
||
|
||
minutes = 10
|
||
minute_arg = context.args[0] if update.message.reply_to_message and context.args else (context.args[1] if len(context.args) > 1 else "")
|
||
if minute_arg.isdigit():
|
||
minutes = max(1, min(int(minute_arg), 1440))
|
||
|
||
try:
|
||
from telegram import ChatPermissions
|
||
|
||
until_date = int((datetime.now() + timedelta(minutes=minutes)).timestamp())
|
||
await update.effective_chat.restrict_member(
|
||
user_id=user_id,
|
||
permissions=ChatPermissions(can_send_messages=False),
|
||
until_date=until_date,
|
||
)
|
||
await Action.create(action="MUTE", user_id=user_id, username=target, operator_id=update.effective_user.id, details={"minutes": minutes})
|
||
reply = await update.message.reply_text(f"🔇 已禁言 {target} ({user_id}) {minutes} 分钟")
|
||
except Exception as e:
|
||
logger.exception("禁言失败")
|
||
reply = await update.message.reply_text(f"❌ 禁言失败: {e}")
|
||
|
||
asyncio.create_task(auto_delete(update.message))
|
||
asyncio.create_task(auto_delete(reply))
|
||
|
||
|
||
async def cmd_purge(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""批量删除消息。Telegram Bot API 无法拉历史,只支持回复某条消息后向前按已知 message_id 尝试删除。"""
|
||
if not await require_admin(update, context):
|
||
return
|
||
|
||
if not context.args or not context.args[0].isdigit():
|
||
reply = await update.message.reply_text("用法: 回复一条消息后 /purge 数量(最多50)")
|
||
asyncio.create_task(auto_delete(update.message))
|
||
asyncio.create_task(auto_delete(reply))
|
||
return
|
||
|
||
if not update.message.reply_to_message:
|
||
reply = await update.message.reply_text("❌ /purge 需要回复一条消息作为删除起点")
|
||
asyncio.create_task(auto_delete(update.message))
|
||
asyncio.create_task(auto_delete(reply))
|
||
return
|
||
|
||
count = min(max(int(context.args[0]), 1), 50)
|
||
start_id = update.message.reply_to_message.message_id
|
||
deleted = 0
|
||
|
||
for mid in range(start_id, start_id - count, -1):
|
||
try:
|
||
await context.bot.delete_message(chat_id=update.effective_chat.id, message_id=mid)
|
||
deleted += 1
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
await update.message.delete()
|
||
except Exception:
|
||
pass
|
||
|
||
reply = await update.effective_chat.send_message(f"🧹 已尝试清理 {count} 条,成功删除 {deleted} 条")
|
||
await Action.create(action="PURGE", operator_id=update.effective_user.id, details={"count": count, "deleted": deleted})
|
||
asyncio.create_task(auto_delete(reply))
|
||
|
||
|
||
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""帮助"""
|
||
text = (
|
||
"📖 收割机群管机器人帮助\n"
|
||
"━━━━━━━━━━━━━━━━━━━━\n"
|
||
"🤖 /ask <问题> - 向知识库提问\n"
|
||
"📊 /stats - 今日统计\n"
|
||
"✅ /checkin - 每日签到\n"
|
||
"🎮 /trivia - 知识答题\n"
|
||
"🏆 /rank - 积分排行\n"
|
||
"🔗 /bind - 绑定账号\n"
|
||
"🌙 /night on/off - 夜间模式(管理员)\n\n"
|
||
"管理命令(管理员):\n"
|
||
"🔨 回复消息 /ban 或 /ban 用户ID\n"
|
||
"🔓 /unban 用户ID\n"
|
||
"⚠️ 回复消息 /warn [原因]\n"
|
||
"🔇 回复消息 /mute [分钟]\n"
|
||
"🧹 回复消息 /purge 数量\n"
|
||
)
|
||
reply = await update.message.reply_text(text)
|
||
asyncio.create_task(auto_delete(update.message))
|
||
asyncio.create_task(auto_delete(reply, seconds=120))
|
||
|
||
|
||
async def auto_delete(message, seconds=60):
|
||
try:
|
||
await asyncio.sleep(seconds)
|
||
await message.delete()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def register_admin(app):
|
||
app.add_handler(CommandHandler("help", cmd_help, filters.ChatType.GROUPS))
|
||
app.add_handler(CommandHandler("stats", cmd_stats, filters.ChatType.GROUPS))
|
||
app.add_handler(CommandHandler("ban", cmd_ban, filters.ChatType.GROUPS))
|
||
app.add_handler(CommandHandler("unban", cmd_unban, filters.ChatType.GROUPS))
|
||
app.add_handler(CommandHandler("warn", cmd_warn, filters.ChatType.GROUPS))
|
||
app.add_handler(CommandHandler("mute", cmd_mute, filters.ChatType.GROUPS))
|
||
app.add_handler(CommandHandler("purge", cmd_purge, filters.ChatType.GROUPS))
|