"""Обработчики P2P чата между пользователями""" from aiogram import Router, F from aiogram.filters import Command, StateFilter from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton from src.filters.case_insensitive import CaseInsensitiveCommand from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from sqlalchemy.ext.asyncio import AsyncSession from typing import Optional from src.core.p2p_services import P2PMessageService from src.core.services import UserService from src.core.models import User from src.core.database import async_session_maker from src.core.config import ADMIN_IDS router = Router(name='p2p_chat_router') class P2PChatStates(StatesGroup): """Состояния для P2P чата""" waiting_for_recipient = State() # Ожидание выбора получателя chatting = State() # В процессе переписки с пользователем def is_admin(user_id: int) -> bool: """Проверка прав администратора""" return user_id in ADMIN_IDS def format_sender_name(user: User, is_current_user: bool = False, current_user_is_admin: bool = False) -> str: """ Форматирует имя отправителя для отображения в чате Args: user: Объект пользователя is_current_user: Текущий ли это пользователь current_user_is_admin: Админ ли текущий пользователь Returns: Отформатированное имя """ if is_current_user: return "🔵 Вы" # Если это администратор и текущий пользователь не админ - показываем "Админ" if user.is_admin and not current_user_is_admin: return "🔵 Админ" # Формируем базовое имя (используем nickname из профиля) name = user.nickname or user.first_name or f"@{user.username}" or "Unknown" # Добавляем информацию о карте если пользователь админ и текущий юзер админ if current_user_is_admin and user.club_card_number: name += f" (карта: {user.club_card_number})" return f"🔵 {name}" @router.message(CaseInsensitiveCommand("chat")) async def show_chat_menu(message: Message, state: FSMContext): """ Главное меню чата (регистронезависимо) /chat - показать меню с опциями общения """ # Очищаем состояние при входе в меню (выход из диалога) await state.clear() async with async_session_maker() as session: user = await UserService.get_or_create_user( session, message.from_user.id, username=message.from_user.username, first_name=message.from_user.first_name, last_name=message.from_user.last_name ) if not user: await message.answer("❌ Вы не зарегистрированы. Используйте /start") return # Получаем количество непрочитанных сообщений unread_count = await P2PMessageService.get_unread_count(session, user.id) # Получаем последние диалоги recent = await P2PMessageService.get_recent_conversations(session, user.id, limit=5) text = "💬 Чат\n\n" if unread_count > 0: text += f"📨 У вас {unread_count} непрочитанных сообщений\n\n" text += "Выберите действие:" buttons = [ [InlineKeyboardButton( text="✉️ Написать пользователю", callback_data="p2p:select_user" )], [InlineKeyboardButton( text="📋 Мои диалоги", callback_data="p2p:my_conversations" )] ] if is_admin(message.from_user.id): buttons.append([InlineKeyboardButton( text="📢 Написать всем (broadcast)", callback_data="p2p:broadcast" )]) await message.answer( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @router.callback_query(F.data == "p2p:select_user") async def select_recipient(callback: CallbackQuery, state: FSMContext): """Выбор получателя для P2P сообщения""" await callback.answer() async with async_session_maker() as session: # Получаем всех зарегистрированных пользователей кроме себя users = await UserService.get_all_users(session) users = [u for u in users if u.telegram_id != callback.from_user.id and u.is_registered] if not users: await callback.message.edit_text("❌ Нет доступных пользователей для общения") return # Создаём кнопки с пользователями (по 1 на строку) buttons = [] for user in users[:20]: # Ограничение 20 пользователей на странице display_name = user.nickname or f"@{user.username}" or user.first_name or "Unknown" if user.club_card_number: display_name += f" (карта: {user.club_card_number})" buttons.append([InlineKeyboardButton( text=display_name, callback_data=f"p2p:user:{user.id}" )]) buttons.append([InlineKeyboardButton( text="« Назад", callback_data="p2p:back_to_menu" )]) await callback.message.edit_text( "👥 Выберите пользователя:\n\n" "Кликните на пользователя, чтобы начать диалог", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @router.callback_query(F.data.startswith("p2p:user:")) async def start_conversation(callback: CallbackQuery, state: FSMContext): """Начать диалог с выбранным пользователем""" await callback.answer() user_id = int(callback.data.split(":")[2]) async with async_session_maker() as session: recipient = await session.get(User, user_id) if not recipient: await callback.message.edit_text("❌ Пользователь не найден") return sender = await UserService.get_or_create_user( session, callback.from_user.id, username=callback.from_user.username, first_name=callback.from_user.first_name, last_name=callback.from_user.last_name ) # Получаем последние 10 сообщений из диалога messages = await P2PMessageService.get_conversation( session, sender.id, recipient.id, limit=10 ) # Сохраняем ID получателя в состоянии await state.update_data(recipient_id=recipient.id, recipient_telegram_id=recipient.telegram_id) await state.set_state(P2PChatStates.chatting) recipient_name = recipient.nickname or f"@{recipient.username}" or recipient.first_name or "Unknown" text = f"💬 Диалог с {recipient_name}\n\n" if messages: text += "📝 Последние сообщения:\n\n" for msg in reversed(messages[-5:]): # Последние 5 сообщений # Определяем имя отправителя is_current = msg.sender_id == sender.id user_for_display = sender if is_current else recipient sender_name = format_sender_name(user_for_display, is_current, is_admin(sender.telegram_id)) msg_text = msg.text[:50] + "..." if msg.text and len(msg.text) > 50 else (msg.text or f"[{msg.message_type}]") text += f"• {sender_name}: {msg_text}\n" text += "\n" text += "✍️ Отправьте сообщение (текст, фото, видео...)\n\n" text += "⚠️ Важно: В режиме диалога все сообщения отправляются только собеседнику.\n" text += "Для выхода в общий чат используйте кнопку ниже или команду /chat" buttons = [[InlineKeyboardButton( text="« Завершить диалог", callback_data="p2p:end_conversation" )]] await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @router.callback_query(F.data == "p2p:my_conversations") async def show_conversations(callback: CallbackQuery): """Показать список диалогов""" await callback.answer() async with async_session_maker() as session: sender = await UserService.get_or_create_user( session, callback.from_user.id, username=callback.from_user.username, first_name=callback.from_user.first_name, last_name=callback.from_user.last_name ) conversations = await P2PMessageService.get_recent_conversations(session, sender.id, limit=10) if not conversations: await callback.message.edit_text( "📭 У вас пока нет диалогов\n\n" "Используйте /chat чтобы написать кому-нибудь" ) return text = "📋 Ваши диалоги:\n\n" buttons = [] for peer, last_msg, unread in conversations: peer_name = peer.nickname or f"@{peer.username}" or peer.first_name or "Unknown" # Иконка в зависимости от непрочитанных icon = "🔴" if unread > 0 else "💬" # Превью последнего сообщения preview = last_msg.text[:30] + "..." if last_msg.text and len(last_msg.text) > 30 else (last_msg.text or f"[{last_msg.message_type}]") button_text = f"{icon} {peer_name}" if unread > 0: button_text += f" ({unread})" buttons.append([InlineKeyboardButton( text=button_text, callback_data=f"p2p:user:{peer.id}" )]) text += f"{icon} {peer_name}" # Показываем номер карты если есть if peer.club_card_number: text += f" (карта: {peer.club_card_number})" text += "\n" text += f" {preview}\n" if unread > 0: text += f" 📨 Непрочитанных: {unread}\n" text += "\n" buttons.append([InlineKeyboardButton( text="« Назад", callback_data="p2p:back_to_menu" )]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) @router.callback_query(F.data == "p2p:end_conversation") async def end_conversation(callback: CallbackQuery, state: FSMContext): """Завершить текущий диалог""" await callback.answer("Диалог завершён") await state.clear() await callback.message.edit_text( "✅ Диалог завершён\n\n" "Используйте /chat чтобы открыть меню чата" ) @router.callback_query(F.data == "p2p:back_to_menu") async def back_to_menu(callback: CallbackQuery, state: FSMContext): """Вернуться в главное меню""" await callback.answer() await state.clear() async with async_session_maker() as session: user = await UserService.get_or_create_user( session, callback.from_user.id, username=callback.from_user.username, first_name=callback.from_user.first_name, last_name=callback.from_user.last_name ) if not user: await callback.message.edit_text("❌ Вы не зарегистрированы. Используйте /start") return # Получаем количество непрочитанных сообщений unread_count = await P2PMessageService.get_unread_count(session, user.id) # Получаем последние диалоги recent = await P2PMessageService.get_recent_conversations(session, user.id, limit=5) text = "💬 Чат\n\n" if unread_count > 0: text += f"📨 У вас {unread_count} непрочитанных сообщений\n\n" text += "Выберите действие:" buttons = [ [InlineKeyboardButton( text="✉️ Написать пользователю", callback_data="p2p:select_user" )], [InlineKeyboardButton( text="📋 Мои диалоги", callback_data="p2p:my_conversations" )] ] if recent: text += "\n\nПоследние диалоги:\n" for peer, last_msg, unread in recent: unread_badge = f" ({unread})" if unread > 0 else "" text += f" • @{peer.username or peer.first_name}{unread_badge}\n" kb = InlineKeyboardMarkup(inline_keyboard=buttons) await callback.message.edit_text(text, reply_markup=kb, parse_mode="HTML") # Обработчик сообщений в состоянии chatting @router.message(StateFilter(P2PChatStates.chatting), F.text | F.photo | F.video | F.document) async def handle_p2p_message(message: Message, state: FSMContext): """Обработка P2P сообщения от пользователя""" import logging logger = logging.getLogger(__name__) logger.info(f"[P2P] handle_p2p_message вызван: user={message.from_user.id}, в состоянии P2P chatting") data = await state.get_data() recipient_id = data.get("recipient_id") recipient_telegram_id = data.get("recipient_telegram_id") if not recipient_id or not recipient_telegram_id: await message.answer("❌ Ошибка: получатель не найден. Начните диалог заново с /chat") await state.clear() return async with async_session_maker() as session: sender = await UserService.get_or_create_user( session, message.from_user.id, username=message.from_user.username, first_name=message.from_user.first_name, last_name=message.from_user.last_name ) # Получаем информацию о получателе для определения как подписать сообщение recipient = await UserService.get_by_telegram_id(session, recipient_telegram_id) # Формируем подпись сообщения для получателя if sender.is_admin: sender_name = "АДМИН" else: sender_name = sender.nickname or f"@{sender.username}" or sender.first_name or "Unknown" # Добавляем карту если получатель админ if recipient and recipient.is_admin and sender.club_card_number: sender_name += f" (карта: {sender.club_card_number})" # Определяем тип сообщения message_type = "text" text = message.text file_id = None if message.photo: message_type = "photo" file_id = message.photo[-1].file_id text = message.caption elif message.video: message_type = "video" file_id = message.video.file_id text = message.caption elif message.document: message_type = "document" file_id = message.document.file_id text = message.caption # Отправляем сообщение получателю try: if message_type == "text": sent = await message.bot.send_message( recipient_telegram_id, f"{sender_name}\n\n{text}", parse_mode="HTML" ) elif message_type == "photo": sent = await message.bot.send_photo( recipient_telegram_id, photo=file_id, caption=f"{sender_name}\n\n{text or ''}" , parse_mode="HTML" ) elif message_type == "video": sent = await message.bot.send_video( recipient_telegram_id, video=file_id, caption=f"{sender_name}\n\n{text or ''}", parse_mode="HTML" ) elif message_type == "document": sent = await message.bot.send_document( recipient_telegram_id, document=file_id, caption=f"{sender_name}\n\n{text or ''}", parse_mode="HTML" ) # Сохраняем в БД await P2PMessageService.send_message( session, sender_id=sender.id, recipient_id=recipient_id, message_type=message_type, text=text, file_id=file_id, sender_message_id=message.message_id, recipient_message_id=sent.message_id ) await message.answer("✅ Сообщение доставлено") except Exception as e: await message.answer(f"❌ Не удалось доставить сообщение: {e}")