370 lines
14 KiB
Python
370 lines
14 KiB
Python
"""Обработчики 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
|
||
|
||
|
||
@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 = "💬 <b>Чат</b>\n\n"
|
||
|
||
if unread_count > 0:
|
||
text += f"📨 У вас <b>{unread_count}</b> непрочитанных сообщений\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 = f"@{user.username}" if user.username else user.first_name
|
||
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(
|
||
"👥 <b>Выберите пользователя:</b>\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 = f"@{recipient.username}" if recipient.username else recipient.first_name
|
||
|
||
text = f"💬 <b>Диалог с {recipient_name}</b>\n\n"
|
||
|
||
if messages:
|
||
text += "📝 <b>Последние сообщения:</b>\n\n"
|
||
for msg in reversed(messages[-5:]): # Последние 5 сообщений
|
||
sender_name = "Вы" if msg.sender_id == sender.id else recipient_name
|
||
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 += "⚠️ <b>Важно:</b> В режиме диалога все сообщения отправляются только собеседнику.\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, user.id, limit=10)
|
||
|
||
if not conversations:
|
||
await callback.message.edit_text(
|
||
"📭 У вас пока нет диалогов\n\n"
|
||
"Используйте /chat чтобы написать кому-нибудь"
|
||
)
|
||
return
|
||
|
||
text = "📋 <b>Ваши диалоги:</b>\n\n"
|
||
|
||
buttons = []
|
||
for peer, last_msg, unread in conversations:
|
||
peer_name = f"@{peer.username}" if peer.username else peer.first_name
|
||
|
||
# Иконка в зависимости от непрочитанных
|
||
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} <b>{peer_name}</b>\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()
|
||
|
||
# Имитируем команду /chat
|
||
fake_message = callback.message
|
||
fake_message.from_user = callback.from_user
|
||
|
||
await show_chat_menu(fake_message, state)
|
||
|
||
|
||
# Обработчик сообщений в состоянии 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
|
||
)
|
||
sender_name = f"@{sender.username}" if sender.username else sender.first_name
|
||
|
||
# Определяем тип сообщения
|
||
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"💬 <b>Сообщение от {sender_name}:</b>\n\n{text}",
|
||
parse_mode="HTML"
|
||
)
|
||
elif message_type == "photo":
|
||
sent = await message.bot.send_photo(
|
||
recipient_telegram_id,
|
||
photo=file_id,
|
||
caption=f"💬 <b>Фото от {sender_name}</b>\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"💬 <b>Видео от {sender_name}</b>\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"💬 <b>Документ от {sender_name}</b>\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}")
|