feat: support delete-and-ban message actions

This commit is contained in:
ngfchl
2026-07-07 09:09:59 +08:00
parent 1437bb7e4b
commit 3ee9b1174e
3 changed files with 92 additions and 19 deletions
+44 -11
View File
@@ -14,6 +14,31 @@ from services.stats_service import get_daily_stats
logger = logging.getLogger("spam_guard")
async def ban_user_with_record(chat, bot, user_id: int, target: str | None, operator_id: int | None, reason: str, method: str = "command"):
unban_at = datetime.now() + timedelta(minutes=config.BAN_DURATION_MINUTES)
await chat.ban_member(user_id=user_id, until_date=int(unban_at.timestamp()))
ban = await Ban.create(
user_id=user_id,
username=target,
reason=reason,
method=method,
duration_minutes=config.BAN_DURATION_MINUTES,
unban_at=unban_at,
)
await Action.create(action="BAN", user_id=user_id, username=target, operator_id=operator_id, details={"method": method})
return ban
async def mark_reply_message_deleted(reply_msg, reason: str = "command_ban_reply"):
rec = await Message.get_or_none(chat_id=reply_msg.chat_id, msg_id=reply_msg.message_id)
if rec:
rec.manually_deleted = True
rec.deleted_at = datetime.now()
rec.delete_reason = reason
await rec.save()
async def cmd_stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""今日统计"""
stats = await get_daily_stats()
@@ -45,18 +70,26 @@ async def cmd_ban(update: Update, context: ContextTypes.DEFAULT_TYPE):
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 ban_user_with_record(
update.effective_chat,
context.bot,
user_id,
target,
update.effective_user.id,
f"手动封禁 by {update.effective_user.username or update.effective_user.id}",
method="reply_command" if update.message.reply_to_message else "command",
)
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} 分钟")
if update.message.reply_to_message:
try:
await update.message.reply_to_message.delete()
await mark_reply_message_deleted(update.message.reply_to_message)
except Exception:
pass
try:
await update.message.delete()
except Exception:
pass
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(f"❌ 封禁失败: {e}")
+37 -2
View File
@@ -193,13 +193,16 @@ async def api_send(request: Request):
@app.post("/api/messages/{message_id}/delete")
async def api_delete_message(message_id: int):
async def api_delete_message(message_id: int, request: Request):
"""手动删除聊天记录:先删 Telegram 消息,再把数据库记录标记为已删除。"""
import config
from telegram import Bot
from telegram.error import BadRequest
from telegram.request import HTTPXRequest
data = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {}
ban_user = bool(data.get("ban_user"))
rec = await Message.get_or_none(id=message_id)
if not rec:
return JSONResponse({"ok": False, "error": "消息不存在"}, status_code=404)
@@ -226,6 +229,24 @@ async def api_delete_message(message_id: int):
rec.deleted_at = now_tz()
rec.delete_reason = "web_manual_delete" if not tg_error else f"web_manual_delete_after_tg_error: {tg_error[:160]}"
await rec.save()
ban = None
if ban_user and rec.user_id:
try:
from datetime import timedelta
unban_at = now_tz() + timedelta(minutes=config.BAN_DURATION_MINUTES)
await bot.ban_chat_member(chat_id=rec.chat_id, user_id=rec.user_id, until_date=int(unban_at.timestamp()))
ban = await Ban.create(
user_id=rec.user_id,
username=rec.username,
first_name=rec.first_name,
reason="Web 删除消息时手动封禁",
method="web_delete_ban",
duration_minutes=config.BAN_DURATION_MINUTES,
unban_at=unban_at,
)
except Exception as e:
return JSONResponse({"ok": False, "error": f"消息已删除,但封禁失败: {type(e).__name__}: {e}"}, status_code=502)
await Action.create(
action="MANUAL_DELETE_MESSAGE",
user_id=rec.user_id,
@@ -239,7 +260,21 @@ async def api_delete_message(message_id: int):
"chat_id": rec.chat_id,
"deleted_at": rec.deleted_at,
}))
return JSONResponse(json_safe({"ok": True, "tg_deleted": tg_deleted, "tg_error": tg_error, "deleted_at": rec.deleted_at}))
if ban:
await Action.create(action="BAN", user_id=rec.user_id, username=rec.username, details={"method": "web_delete_ban", "message_id": rec.id})
await broadcast_sse("ban", json_safe({
"id": ban.id,
"user_id": rec.user_id,
"username": rec.username,
"first_name": rec.first_name,
"reason": ban.reason,
"method": ban.method,
"duration_minutes": config.BAN_DURATION_MINUTES,
"auto_unban": True,
"unban_at": unban_at,
"time": ban.created_at,
}))
return JSONResponse(json_safe({"ok": True, "tg_deleted": tg_deleted, "tg_error": tg_error, "deleted_at": rec.deleted_at, "banned": bool(ban)}))
@app.get("/api/scores")
+11 -6
View File
@@ -217,7 +217,8 @@
<span class="meta-right">
<span class="time">#{{ item.msg_id || '-' }} · {{ formatTime(item.time) }}</span>
<el-tag v-if="item.deleted" size="mini" type="info">手动删除</el-tag>
<el-button v-else type="text" size="mini" icon="el-icon-delete" :loading="item._deleting" @click="deleteChatMessage(item)">删除</el-button>
<el-button v-else type="text" size="mini" icon="el-icon-delete" :loading="item._deleting" @click="deleteChatMessage(item, false)">删除</el-button>
<el-button v-if="!item.deleted && item.user_id" type="text" size="mini" icon="el-icon-remove-outline" :loading="item._deleting" @click="deleteChatMessage(item, true)">删封</el-button>
</span>
</div>
<div class="text">{{ item.text }}</div>
@@ -402,20 +403,24 @@ new Vue({
}
this.sending = false
},
async deleteChatMessage(item) {
this.$confirm('先删除 Telegram 群内消息,再把数据库记录标记为手动删除。继续吗?', '删除聊天记录', {
confirmButtonText: '删除',
async deleteChatMessage(item, banUser = false) {
this.$confirm(banUser ? '先删除 Telegram 群内消息,再封禁该用户,并标记数据库。继续吗?' : '先删除 Telegram 群内消息,再把数据库记录标记为手动删除。继续吗?', banUser ? '删除并封禁' : '删除聊天记录', {
confirmButtonText: banUser ? '删封' : '删除',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
this.$set(item, '_deleting', true)
try {
const data = await (await fetch(`/api/messages/${item.id}/delete`, { method: 'POST' })).json()
const data = await (await fetch(`/api/messages/${item.id}/delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ban_user: banUser })
})).json()
if (data.ok) {
this.$set(item, 'deleted', true)
this.$set(item, 'manually_deleted', true)
this.$set(item, 'deleted_at', data.deleted_at || new Date().toISOString())
this.$message.success('✅ 已标记为手动删除')
this.$message.success(data.banned ? '✅ 已删除并封禁' : '✅ 已标记为手动删除')
this.loadStats()
} else {
this.$message.error(`❌ 删除失败: ${data.error}`)