Files
tg-spam-guard/handlers/admin.py
T
2026-07-06 19:26:00 +08:00

236 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""管理命令 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))