"""Обработчики 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}")