feat: support delete-and-ban message actions
This commit is contained in:
+44
-11
@@ -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
@@ -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
@@ -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}`)
|
||||
|
||||
Reference in New Issue
Block a user