feat: добавлено управление сообщениями пользователей в админ-панель
All checks were successful
continuous-integration/drone/push Build is passing
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:
@@ -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")]
|
||||||
|
|||||||
@@ -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:
|
||||||
"""Сервис проверки прав на отправку сообщений"""
|
"""Сервис проверки прав на отправку сообщений"""
|
||||||
|
|||||||
@@ -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"""
|
||||||
|
|||||||
@@ -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']
|
||||||
Reference in New Issue
Block a user