feat: добавлено управление сообщениями пользователей в админ-панель
All checks were successful
continuous-integration/drone/push Build is passing

- Добавлена кнопка 'Сообщения пользователей' в админ меню
- Реализован просмотр последних сообщений с фильтрацией
- Возможность просмотра медиа (фото, видео) прямо в боте
- Функция удаления сообщений администратором
- Удаление происходит как в БД, так и у пользователей в Telegram
- Просмотр всех сообщений конкретного пользователя
- Добавлены методы в ChatMessageService и UserService
- Метод get_user_messages_all для получения всех сообщений
- Метод mark_as_deleted для пометки сообщений как удаленных
- Метод count_messages для подсчета количества сообщений
- Метод get_user_by_id в UserService
This commit is contained in:
2025-11-22 19:46:38 +09:00
parent 49f220c2a2
commit 8e692d2f61
4 changed files with 343 additions and 2 deletions

View File

@@ -30,8 +30,9 @@ class KeyboardBuilderImpl(IKeyboardBuilder):
"""Получить админскую клавиатуру""" """Получить админскую клавиатуру"""
buttons = [ buttons = [
[InlineKeyboardButton(text="🎲 Управление розыгрышами", callback_data="admin_lotteries")], [InlineKeyboardButton(text="🎲 Управление розыгрышами", callback_data="admin_lotteries")],
[InlineKeyboardButton(text="<EFBFBD> Управление участниками", callback_data="admin_participants")], [InlineKeyboardButton(text="👥 Управление участниками", callback_data="admin_participants")],
[InlineKeyboardButton(text="👑 Управление победителями", callback_data="admin_winners")], [InlineKeyboardButton(text="👑 Управление победителями", callback_data="admin_winners")],
[InlineKeyboardButton(text="💬 Сообщения пользователей", callback_data="admin_messages")],
[InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stats")], [InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stats")],
[InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_settings")], [InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_settings")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")] [InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]

View File

@@ -285,6 +285,58 @@ class ChatMessageService:
result = await session.execute(query) result = await session.execute(query)
return result.scalars().all() 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: class ChatPermissionService:
"""Сервис проверки прав на отправку сообщений""" """Сервис проверки прав на отправку сообщений"""

View File

@@ -49,6 +49,12 @@ class UserService:
) )
return result.scalar_one_or_none() 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 @staticmethod
async def get_user_by_username(session: AsyncSession, username: str) -> Optional[User]: async def get_user_by_username(session: AsyncSession, username: str) -> Optional[User]:
"""Получить пользователя по username""" """Получить пользователя по username"""

View File

@@ -16,8 +16,9 @@ import json
from ..core.database import async_session_maker from ..core.database import async_session_maker
from ..core.services import UserService, LotteryService, ParticipationService from ..core.services import UserService, LotteryService, ParticipationService
from ..core.chat_services import ChatMessageService
from ..core.config import ADMIN_IDS 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__) logger = logging.getLogger(__name__)
@@ -3412,5 +3413,286 @@ async def apply_display_type(callback: CallbackQuery, state: FSMContext):
await state.clear() 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'] __all__ = ['admin_router']