From 2f6739685f330c99b341e6b84347b2dddbc34336 Mon Sep 17 00:00:00 2001 From: ngfchl Date: Tue, 7 Jul 2026 21:56:52 +0800 Subject: [PATCH] feat: cleanup join notice with welcome message --- handlers/welcome.py | 89 +++++++++++++++++++++++++++++++++++++++++++-- web/api.py | 4 +- 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/handlers/welcome.py b/handlers/welcome.py index 9ac1562..8b8cffb 100644 --- a/handlers/welcome.py +++ b/handlers/welcome.py @@ -1,19 +1,46 @@ """入群欢迎 / 退群告别""" +import asyncio import logging from datetime import datetime, timedelta from telegram import ChatMember, InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.ext import ChatMemberHandler, ContextTypes +from telegram.ext import ChatMemberHandler, ContextTypes, MessageHandler, filters import config from services.message_utils import record_bot_message -from db.models import Action, Ban -from services.message_utils import schedule_delete +from db.models import Action, Ban, Message from services.spam_detector import classify_username_spam from services.verify_service import create_verify from web.api import broadcast_sse, json_safe logger = logging.getLogger("spam_guard") +JOIN_SERVICE_TEXT = "__service__:new_chat_member" + + +async def handle_new_chat_members_message(update: Update, context: ContextTypes.DEFAULT_TYPE): + """记录 Telegram 入群系统消息,供后台删除用户消息时联动清理。""" + msg = update.effective_message + if not msg or not msg.new_chat_members: + return + chat_id = update.effective_chat.id if update.effective_chat else msg.chat_id + for user in msg.new_chat_members: + if user.is_bot: + continue + username = user.username or "" + first_name = " ".join(x for x in [user.first_name, user.last_name] if x) or user.first_name or "" + exists = await Message.get_or_none(chat_id=chat_id, msg_id=msg.message_id, user_id=user.id, text=JOIN_SERVICE_TEXT) + if exists: + continue + await Message.create( + msg_id=msg.message_id, + chat_id=chat_id, + user_id=user.id, + username=username, + first_name=first_name, + text=JOIN_SERVICE_TEXT, + ) + logger.info(f"🧾 已记录入群系统消息: user={user.id}, msg_id={msg.message_id}") + WELCOME_TEXT = ( "👋 欢迎新成员 **{username}** 加入收割机群组!\n" @@ -34,6 +61,59 @@ def verify_keyboard() -> InlineKeyboardMarkup: ]) +async def delete_join_service_message(bot, chat_id: int, user_id: int) -> dict: + """删除指定用户最近的 Telegram 入群系统提示。""" + rows = await Message.filter( + chat_id=chat_id, + user_id=user_id, + text=JOIN_SERVICE_TEXT, + manually_deleted=False, + ).order_by("-created_at").limit(3) + result = {"attempted": 0, "deleted": 0, "errors": []} + seen_msg_ids: set[int] = set() + for row in rows: + if not row.msg_id or int(row.msg_id) in seen_msg_ids: + row.manually_deleted = True + row.deleted_at = datetime.now() + row.delete_reason = "welcome_cleanup_join_duplicate" + await row.save() + continue + seen_msg_ids.add(int(row.msg_id)) + result["attempted"] += 1 + try: + await bot.delete_message(chat_id=chat_id, message_id=row.msg_id) + result["deleted"] += 1 + row.delete_reason = "welcome_cleanup_join_service" + logger.info(f"🧹 已删除入群系统消息: user={user_id}, msg_id={row.msg_id}") + except Exception as e: + msg = str(e) + if "message to delete not found" in msg.lower(): + row.delete_reason = f"welcome_cleanup_join_not_found: {msg[:120]}" + else: + result["errors"].append(f"{type(e).__name__}: {msg}") + continue + row.manually_deleted = True + row.deleted_at = datetime.now() + await row.save() + if result["attempted"]: + await Action.create(action="WELCOME_CLEAN_JOIN_MESSAGE", chat_id=chat_id, user_id=user_id, details=result) + return result + + +async def auto_delete_welcome_and_join(bot, welcome_msg, chat_id: int, user_id: int, seconds: int) -> None: + """欢迎消息到期销毁时,同步删除该用户的入群系统记录。""" + await asyncio.sleep(seconds) + try: + await welcome_msg.delete() + logger.info(f"🧹 已删除入群欢迎消息: user={user_id}, msg_id={welcome_msg.message_id}") + except Exception as e: + logger.debug(f"自动删除入群欢迎消息失败: {e}") + try: + await delete_join_service_message(bot, chat_id, user_id) + except Exception as e: + logger.warning(f"删除入群系统消息失败: user={user_id}, {type(e).__name__}: {e}") + + async def handle_chat_member(update: Update, context: ContextTypes.DEFAULT_TYPE): """入群欢迎 / 退群告别""" cmu = update.chat_member @@ -144,7 +224,7 @@ async def handle_chat_member(update: Update, context: ContextTypes.DEFAULT_TYPE) parse_mode="Markdown", reply_markup=verify_keyboard(), ) - schedule_delete(msg, seconds=config.VERIFY_TIMEOUT_SECONDS) + asyncio.create_task(auto_delete_welcome_and_join(context.bot, msg, update.effective_chat.id, user.id, config.VERIFY_TIMEOUT_SECONDS)) except Exception as e: logger.error(f"❌ 欢迎消息发送失败: {e}") @@ -157,4 +237,5 @@ async def handle_chat_member(update: Update, context: ContextTypes.DEFAULT_TYPE) def register_welcome(app): """注册入群欢迎/退群告别""" + app.add_handler(MessageHandler(filters.StatusUpdate.NEW_CHAT_MEMBERS, handle_new_chat_members_message), group=-1) app.add_handler(ChatMemberHandler(handle_chat_member, ChatMemberHandler.CHAT_MEMBER)) diff --git a/web/api.py b/web/api.py index 11f6915..68695fa 100644 --- a/web/api.py +++ b/web/api.py @@ -401,7 +401,7 @@ def normalize_ban(row: dict) -> dict: @app.get("/api/messages") async def api_messages(limit: int = Query(default=50, ge=1, le=500), chat_id: int | None = None): - qs = Message.all() + qs = Message.exclude(text="__service__:new_chat_member") if chat_id is not None: qs = qs.filter(chat_id=chat_id) msgs = await qs.order_by("-created_at").limit(limit).values() @@ -411,7 +411,7 @@ async def api_messages(limit: int = Query(default=50, ge=1, le=500), chat_id: in @app.get("/api/chat") async def api_chat(limit: int = Query(default=100, ge=1, le=500), chat_id: int | None = None): """兼容旧前端:返回聊天记录。""" - qs = Message.all() + qs = Message.exclude(text="__service__:new_chat_member") if chat_id is not None: qs = qs.filter(chat_id=chat_id) msgs = await qs.order_by("-created_at").limit(limit).values()