From 8e692d2f61b95af02bbafffeda271c8582ea59bf Mon Sep 17 00:00:00 2001 From: "Andrew K. Choi" Date: Sat, 22 Nov 2025 19:46:38 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=D0=BC=D0=B8=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=B9=20=D0=B2=20=D0=B0?= =?UTF-8?q?=D0=B4=D0=BC=D0=B8=D0=BD-=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлена кнопка 'Сообщения пользователей' в админ меню - Реализован просмотр последних сообщений с фильтрацией - Возможность просмотра медиа (фото, видео) прямо в боте - Функция удаления сообщений администратором - Удаление происходит как в БД, так и у пользователей в Telegram - Просмотр всех сообщений конкретного пользователя - Добавлены методы в ChatMessageService и UserService - Метод get_user_messages_all для получения всех сообщений - Метод mark_as_deleted для пометки сообщений как удаленных - Метод count_messages для подсчета количества сообщений - Метод get_user_by_id в UserService --- src/components/ui.py | 3 +- src/core/chat_services.py | 52 +++++++ src/core/services.py | 6 + src/handlers/admin_panel.py | 284 +++++++++++++++++++++++++++++++++++- 4 files changed, 343 insertions(+), 2 deletions(-) diff --git a/src/components/ui.py b/src/components/ui.py index 0de955b..784a5d3 100644 --- a/src/components/ui.py +++ b/src/components/ui.py @@ -30,8 +30,9 @@ class KeyboardBuilderImpl(IKeyboardBuilder): """Получить админскую клавиатуру""" buttons = [ [InlineKeyboardButton(text="🎲 Управление розыгрышами", callback_data="admin_lotteries")], - [InlineKeyboardButton(text="� Управление участниками", callback_data="admin_participants")], + [InlineKeyboardButton(text="👥 Управление участниками", callback_data="admin_participants")], [InlineKeyboardButton(text="👑 Управление победителями", callback_data="admin_winners")], + [InlineKeyboardButton(text="💬 Сообщения пользователей", callback_data="admin_messages")], [InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stats")], [InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_settings")], [InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")] diff --git a/src/core/chat_services.py b/src/core/chat_services.py index 6542517..4463d9d 100644 --- a/src/core/chat_services.py +++ b/src/core/chat_services.py @@ -284,6 +284,58 @@ class ChatMessageService: result = await session.execute(query) return result.scalars().all() + + @staticmethod + async def get_user_messages_all( + session: AsyncSession, + limit: int = 50, + offset: int = 0, + include_deleted: bool = False + ) -> List[ChatMessage]: + """Получить последние сообщения всех пользователей""" + query = select(ChatMessage).options(selectinload(ChatMessage.sender)) + + if not include_deleted: + query = query.where(ChatMessage.is_deleted == False) + + query = query.order_by(ChatMessage.created_at.desc()).limit(limit).offset(offset) + + result = await session.execute(query) + return result.scalars().all() + + @staticmethod + async def count_messages( + session: AsyncSession, + include_deleted: bool = False + ) -> int: + """Подсчитать количество сообщений""" + from sqlalchemy import func + query = select(func.count(ChatMessage.id)) + + if not include_deleted: + query = query.where(ChatMessage.is_deleted == False) + + result = await session.execute(query) + return result.scalar() or 0 + + @staticmethod + async def mark_as_deleted( + session: AsyncSession, + message_id: int, + deleted_by: int + ) -> bool: + """Пометить сообщение как удаленное""" + result = await session.execute( + update(ChatMessage) + .where(ChatMessage.id == message_id) + .values( + is_deleted=True, + deleted_by=deleted_by, + deleted_at=datetime.now(timezone.utc) + ) + ) + await session.commit() + return result.rowcount > 0 class ChatPermissionService: diff --git a/src/core/services.py b/src/core/services.py index 8b8ed5a..3e46467 100644 --- a/src/core/services.py +++ b/src/core/services.py @@ -49,6 +49,12 @@ class UserService: ) return result.scalar_one_or_none() + @staticmethod + async def get_user_by_id(session: AsyncSession, user_id: int) -> Optional[User]: + """Получить пользователя по ID""" + result = await session.execute(select(User).where(User.id == user_id)) + return result.scalar_one_or_none() + @staticmethod async def get_user_by_username(session: AsyncSession, username: str) -> Optional[User]: """Получить пользователя по username""" diff --git a/src/handlers/admin_panel.py b/src/handlers/admin_panel.py index bd5784a..bf02edb 100644 --- a/src/handlers/admin_panel.py +++ b/src/handlers/admin_panel.py @@ -16,8 +16,9 @@ import json from ..core.database import async_session_maker from ..core.services import UserService, LotteryService, ParticipationService +from ..core.chat_services import ChatMessageService from ..core.config import ADMIN_IDS -from ..core.models import User, Lottery, Participation, Account +from ..core.models import User, Lottery, Participation, Account, ChatMessage logger = logging.getLogger(__name__) @@ -3412,5 +3413,286 @@ async def apply_display_type(callback: CallbackQuery, state: FSMContext): await state.clear() +# ============= УПРАВЛЕНИЕ СООБЩЕНИЯМИ ПОЛЬЗОВАТЕЛЕЙ ============= + +@admin_router.callback_query(F.data == "admin_messages") +async def show_messages_menu(callback: CallbackQuery): + """Показать меню управления сообщениями""" + if not is_admin(callback.from_user.id): + await callback.answer("❌ Недостаточно прав", show_alert=True) + return + + text = "💬 *Управление сообщениями пользователей*\n\n" + text += "Здесь вы можете просматривать и удалять сообщения пользователей.\n\n" + text += "Выберите действие:" + + buttons = [ + [InlineKeyboardButton(text="📋 Последние сообщения", callback_data="admin_messages_recent")], + [InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")] + ] + + await callback.message.edit_text( + text, + reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), + parse_mode="Markdown" + ) + + +@admin_router.callback_query(F.data == "admin_messages_recent") +async def show_recent_messages(callback: CallbackQuery, page: int = 0): + """Показать последние сообщения""" + if not is_admin(callback.from_user.id): + await callback.answer("❌ Недостаточно прав", show_alert=True) + return + + limit = 10 + offset = page * limit + + async with async_session_maker() as session: + messages = await ChatMessageService.get_user_messages_all( + session, + limit=limit, + offset=offset, + include_deleted=False + ) + + if not messages: + text = "💬 Нет сообщений для отображения" + buttons = [[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_messages")]] + else: + text = f"💬 *Последние сообщения*\n\n" + + # Добавляем кнопки для просмотра сообщений + buttons = [] + for msg in messages: + sender = msg.sender + username = f"@{sender.username}" if sender.username else f"ID{sender.telegram_id}" + msg_preview = "" + if msg.text: + msg_preview = msg.text[:20] + "..." if len(msg.text) > 20 else msg.text + else: + msg_preview = msg.message_type + + buttons.append([InlineKeyboardButton( + text=f"👁 {username}: {msg_preview}", + callback_data=f"admin_message_view_{msg.id}" + )]) + + buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_messages")]) + + await callback.message.edit_text( + text, + reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), + parse_mode="Markdown" + ) + + +@admin_router.callback_query(F.data.startswith("admin_message_view_")) +async def view_message(callback: CallbackQuery): + """Просмотр конкретного сообщения""" + if not is_admin(callback.from_user.id): + await callback.answer("❌ Недостаточно прав", show_alert=True) + return + + message_id = int(callback.data.split("_")[-1]) + + async with async_session_maker() as session: + msg = await ChatMessageService.get_message(session, message_id) + + if not msg: + await callback.answer("❌ Сообщение не найдено", show_alert=True) + return + + sender = msg.sender + username = f"@{sender.username}" if sender.username else f"ID: {sender.telegram_id}" + + text = f"💬 *Просмотр сообщения*\n\n" + text += f"👤 Отправитель: {username}\n" + text += f"🆔 Telegram ID: `{sender.telegram_id}`\n" + text += f"📝 Тип: {msg.message_type}\n" + text += f"📅 Дата: {msg.created_at.strftime('%d.%m.%Y %H:%M:%S')}\n\n" + + if msg.text: + text += f"📄 *Текст:*\n{msg.text}\n\n" + + if msg.file_id: + text += f"📎 File ID: `{msg.file_id}`\n\n" + + if msg.is_deleted: + text += f"🗑 *Удалено:* Да\n" + if msg.deleted_at: + text += f" Дата: {msg.deleted_at.strftime('%d.%m.%Y %H:%M')}\n" + + buttons = [] + + # Кнопка удаления (если еще не удалено) + if not msg.is_deleted: + buttons.append([InlineKeyboardButton( + text="🗑 Удалить сообщение", + callback_data=f"admin_message_delete_{message_id}" + )]) + + # Кнопка для просмотра всех сообщений пользователя + buttons.append([InlineKeyboardButton( + text="📋 Все сообщения пользователя", + callback_data=f"admin_messages_user_{sender.id}" + )]) + + buttons.append([InlineKeyboardButton(text="🔙 К списку", callback_data="admin_messages_recent")]) + + # Если сообщение содержит медиа, попробуем его показать + if msg.file_id and msg.message_type in ['photo', 'video', 'document', 'animation']: + try: + if msg.message_type == 'photo': + await callback.message.answer_photo( + photo=msg.file_id, + caption=text, + reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), + parse_mode="Markdown" + ) + await callback.message.delete() + await callback.answer() + return + elif msg.message_type == 'video': + await callback.message.answer_video( + video=msg.file_id, + caption=text, + reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), + parse_mode="Markdown" + ) + await callback.message.delete() + await callback.answer() + return + except Exception as e: + logger.error(f"Ошибка при отправке медиа: {e}") + + await callback.message.edit_text( + text, + reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), + parse_mode="Markdown" + ) + + +@admin_router.callback_query(F.data.startswith("admin_message_delete_")) +async def delete_message(callback: CallbackQuery): + """Удалить сообщение пользователя""" + if not is_admin(callback.from_user.id): + await callback.answer("❌ Недостаточно прав", show_alert=True) + return + + message_id = int(callback.data.split("_")[-1]) + + async with async_session_maker() as session: + msg = await ChatMessageService.get_message(session, message_id) + + if not msg: + await callback.answer("❌ Сообщение не найдено", show_alert=True) + return + + # Получаем админа + admin = await UserService.get_or_create_user( + session, + callback.from_user.id, + callback.from_user.username, + callback.from_user.first_name, + callback.from_user.last_name + ) + + # Помечаем сообщение как удаленное + success = await ChatMessageService.mark_as_deleted( + session, + message_id, + admin.id + ) + + if success: + # Пытаемся удалить сообщение из чата пользователя + try: + if msg.forwarded_message_ids: + # Удаляем пересланные копии у всех пользователей + for user_tg_id, tg_msg_id in msg.forwarded_message_ids.items(): + try: + await callback.bot.delete_message( + chat_id=int(user_tg_id), + message_id=tg_msg_id + ) + except Exception as e: + logger.warning(f"Не удалось удалить сообщение {tg_msg_id} у пользователя {user_tg_id}: {e}") + + # Удаляем оригинальное сообщение у отправителя + try: + await callback.bot.delete_message( + chat_id=msg.sender.telegram_id, + message_id=msg.telegram_message_id + ) + except Exception as e: + logger.warning(f"Не удалось удалить оригинальное сообщение: {e}") + + await callback.answer("✅ Сообщение удалено!", show_alert=True) + except Exception as e: + logger.error(f"Ошибка при удалении сообщений: {e}") + await callback.answer("⚠️ Помечено как удаленное", show_alert=True) + else: + await callback.answer("❌ Ошибка при удалении", show_alert=True) + + # Возвращаемся к списку + await show_recent_messages(callback, 0) + + +@admin_router.callback_query(F.data.startswith("admin_messages_user_")) +async def show_user_messages(callback: CallbackQuery): + """Показать все сообщения конкретного пользователя""" + if not is_admin(callback.from_user.id): + await callback.answer("❌ Недостаточно прав", show_alert=True) + return + + user_id = int(callback.data.split("_")[-1]) + + async with async_session_maker() as session: + user = await UserService.get_user_by_id(session, user_id) + + if not user: + await callback.answer("❌ Пользователь не найден", show_alert=True) + return + + messages = await ChatMessageService.get_user_messages( + session, + user_id, + limit=20, + include_deleted=True + ) + + username = f"@{user.username}" if user.username else f"ID: {user.telegram_id}" + + text = f"💬 *Сообщения {username}*\n\n" + + if not messages: + text += "Нет сообщений" + buttons = [[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_messages_recent")]] + else: + # Кнопки для просмотра отдельных сообщений + buttons = [] + for msg in messages[:15]: + status = "🗑" if msg.is_deleted else "✅" + msg_preview = "" + if msg.text: + msg_preview = msg.text[:25] + "..." if len(msg.text) > 25 else msg.text + else: + msg_preview = msg.message_type + + buttons.append([InlineKeyboardButton( + text=f"{status} {msg_preview} ({msg.created_at.strftime('%d.%m %H:%M')})", + callback_data=f"admin_message_view_{msg.id}" + )]) + + buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_messages_recent")]) + + await callback.message.edit_text( + text, + reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), + parse_mode="Markdown" + ) + + # Экспорт роутера __all__ = ['admin_router'] \ No newline at end of file