from typing import Dict from aiogram import Router, F from aiogram.filters import Command, StateFilter from aiogram.fsm.context import FSMContext from aiogram.types import Message, InlineKeyboardButton, CallbackQuery from aiogram.utils.keyboard import InlineKeyboardBuilder from bot.core.bots import BotInfo from bot.utils import status_clear from configs import COMMANDS, ImportantID from middleware.loggers import log # user_id -> thread_id (топик пользователя) user_topic_map: Dict[int, int] = {} # message_id в топике -> user_id topic_message_map: Dict[int, int] = {} __all__ = ("router", "user_topic_map") CMD: str = "new" router: Router = Router(name=f"{CMD}_cmd_router") STATE_WAITING_REQUEST = "waiting_request" def has_active_topic(user_id: int) -> bool: """Проверяет, есть ли у пользователя активный топик""" return user_id in user_topic_map async def send_topic_message(user_id: int, text: str, reply_markup=None): """Отправляет сообщение в топик пользователя""" thread_id = user_topic_map.get(user_id) if not thread_id: return False try: await BotInfo.bot.send_message( chat_id=ImportantID.SUPPORT_CHAT_ID, message_thread_id=thread_id, text=text, parse_mode="HTML", reply_markup=reply_markup ) return True except Exception as e: log(level='ERROR', log_type='TOPIC_SEND', text=f"Ошибка отправки в топик: {e}") return False # ===================== Продолжение диалога ===================== @router.callback_query(F.data == "continue_dialog") async def continue_dialog_callback(callback: CallbackQuery, state: FSMContext) -> None: """Обработчик продолжения существующего диалога""" user_id = callback.from_user.id if not has_active_topic(user_id): await callback.answer("❌ Активный диалог не найден", show_alert=True) return ikb: InlineKeyboardBuilder = InlineKeyboardBuilder() ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start')) await callback.message.edit_text( text="💬 У вас уже есть активный диалог с поддержкой. Просто отправьте ваше сообщение (не через reply) и оно будет переслано администратору.", reply_markup=ikb.as_markup() ) await callback.answer() # ===================== Обработчик callback /new ===================== @router.callback_query(F.data.casefold() == CMD) @log(level='INFO', log_type=f"{CMD.upper()}_CBD", text=f"использовал команду /{CMD} через кнопку") async def new_cmd_callback(callback: CallbackQuery, state: FSMContext) -> None: """Обработчик команды /new из callback кнопки""" user_id = callback.from_user.id # Проверяем, есть ли уже активный топик if has_active_topic(user_id): ikb: InlineKeyboardBuilder = InlineKeyboardBuilder() ikb.row(InlineKeyboardButton(text="Продолжить диалог💬", callback_data='continue_dialog')) ikb.row(InlineKeyboardButton(text="Создать новый📝", callback_data='force_new')) await callback.message.edit_text( text="⚠️ У вас уже есть активный диалог с поддержкой.\n\n" "• Продолжить текущий - чтобы писать в существующий диалог\n" "• Создать новый - если хотите начать новый запрос (старый диалог будет архивирован)", reply_markup=ikb.as_markup(), parse_mode="HTML" ) await callback.answer() return await status_clear(message=callback.message, state=state) await state.set_state(STATE_WAITING_REQUEST) ikb: InlineKeyboardBuilder = InlineKeyboardBuilder() ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start')) try: await callback.message.edit_text( text="Отправьте свой запрос:", reply_markup=ikb.as_markup() ) except Exception: await callback.message.answer( text="Отправьте свой запрос:", reply_markup=ikb.as_markup() ) await callback.answer() # ===================== Принудительное создание нового топика ===================== @router.callback_query(F.data == "force_new") async def force_new_callback(callback: CallbackQuery, state: FSMContext) -> None: """Принудительное создание нового топика (при наличии активного)""" user_id = callback.from_user.id # Уведомляем в старом топике о создании нового if has_active_topic(user_id): await send_topic_message( user_id, f"🔔 Пользователь начал новый запрос\n" f"Старый топик будет архивирован." ) # Не удаляем старый топик из мапы сразу - он перезапишется при создании нового await status_clear(message=callback.message, state=state) await state.set_state(STATE_WAITING_REQUEST) ikb: InlineKeyboardBuilder = InlineKeyboardBuilder() ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start')) await callback.message.edit_text( text="📝 Создание нового запроса\n\nОтправьте ваш запрос:", reply_markup=ikb.as_markup(), parse_mode="HTML" ) await callback.answer() # ===================== Обработчик сообщения /new ===================== @router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True)) @log(level='INFO', log_type=CMD.upper(), text=f"использовал команду /{CMD}") async def new_cmd_message(message: Message, state: FSMContext) -> None: """Обработчик команды /new из текстового сообщения""" user_id = message.from_user.id # Проверяем, есть ли уже активный топик if has_active_topic(user_id): ikb: InlineKeyboardBuilder = InlineKeyboardBuilder() ikb.row(InlineKeyboardButton(text="Продолжить диалог💬", callback_data='continue_dialog')) await message.answer( text="⚠️ У вас уже есть активный диалог с поддержкой.\n\n" "Используйте кнопку ниже чтобы продолжить общение в существующем диалоге.", reply_markup=ikb.as_markup(), parse_mode="HTML" ) return await status_clear(message=message, state=state) await state.set_state(STATE_WAITING_REQUEST) ikb: InlineKeyboardBuilder = InlineKeyboardBuilder() ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start')) await message.answer( text="Отправьте свой запрос:", reply_markup=ikb.as_markup() ) # ===================== Создание топика и отправка запроса ===================== @router.message(StateFilter(STATE_WAITING_REQUEST)) async def process_request(message: Message, state: FSMContext) -> None: """Создание топика и отправка запроса пользователя""" text = message.text.strip() if not text: await message.reply("⚠️ Пожалуйста, отправьте непустое сообщение.") return user = message.from_user try: # Создаем новый топик для пользователя topic_name = f"👤 {user.full_name} (ID: {user.id})" topic_result = await message.bot.create_forum_topic( chat_id=ImportantID.SUPPORT_CHAT_ID, name=topic_name ) thread_id = topic_result.message_thread_id # Отправляем сообщение пользователя в новый топик formatted_text = f"📩 Сообщение от {user.full_name}:\n{text}" sent_msg = await message.bot.send_message( chat_id=ImportantID.SUPPORT_CHAT_ID, message_thread_id=thread_id, text=formatted_text, parse_mode="HTML" ) # Отправляем сообщение с уведомлением (со звуком) await message.bot.send_message( chat_id=ImportantID.SUPPORT_CHAT_ID, message_thread_id=thread_id, text="🔔 Новый запрос создан\nАдминистратор уведомлен.", parse_mode="HTML" ) # Сохраняем связь пользователя и топика user_topic_map[user.id] = thread_id topic_message_map[sent_msg.message_id] = user.id ikb: InlineKeyboardBuilder = InlineKeyboardBuilder() ikb.row(InlineKeyboardButton(text="Перейти к диалогу💬", callback_data='continue_dialog')) ikb.row(InlineKeyboardButton(text="В меню↩️", callback_data='start')) await message.answer( text="✅ Запрос отправлен!\n\n" "Администратор ответит в этом боте. Вы можете продолжить общение через меню.", reply_markup=ikb.as_markup(), parse_mode="HTML" ) await state.clear() except Exception as e: await message.reply(f"⚠️ Не удалось создать запрос: {e}") # ===================== Пересылка сообщений пользователя в топик ===================== @router.message(F.chat.type == "private", ~F.reply_to_message) async def forward_user_to_admin(message: Message) -> None: """Пересылает сообщения пользователя в топик (если есть активный диалог)""" if message.from_user.is_bot: return user_id = message.from_user.id # Проверяем, есть ли активный топик if not has_active_topic(user_id): return # Нет активного топика - игнорируем # Получаем топик пользователя thread_id = user_topic_map.get(user_id) if not thread_id: return try: # Отправляем сообщение пользователя в топик if message.text: formatted_text = f"💬 Сообщение от {message.from_user.full_name}:\n{message.html_text}" sent_msg = await message.bot.send_message( chat_id=ImportantID.SUPPORT_CHAT_ID, message_thread_id=thread_id, text=formatted_text, parse_mode="HTML" ) topic_message_map[sent_msg.message_id] = user_id elif message.photo: caption = f"💬 Сообщение от {message.from_user.full_name}:\n{message.html_text}" if message.caption else f"💬 Сообщение от {message.from_user.full_name}:" sent_msg = await message.bot.send_photo( chat_id=ImportantID.SUPPORT_CHAT_ID, message_thread_id=thread_id, photo=message.photo[-1].file_id, caption=caption, parse_mode="HTML" ) topic_message_map[sent_msg.message_id] = user_id await message.answer("✅ Сообщение отправлено администратору") except Exception as e: await message.answer(f"⚠️ Не удалось отправить сообщение: {e}") # ===================== Пересылка ответов админа пользователю ===================== @router.message(F.chat.id == ImportantID.SUPPORT_CHAT_ID, F.message_thread_id) async def forward_admin_to_user(message: Message) -> None: """Пересылает сообщения админа из топика пользователю""" if message.from_user.is_bot: return thread_id = message.message_thread_id # Ищем пользователя по thread_id топика user_id = None for uid, tid in user_topic_map.items(): if tid == thread_id: user_id = uid break if not user_id: return # Не наш топик try: # Пересылаем сообщение админа пользователю if message.text: text = f"👨‍💼 Ответ администратора:\n{message.html_text}" sent_msg = await message.bot.send_message( chat_id=user_id, text=text, parse_mode="HTML" ) # Сохраняем связь для возможного ответа пользователя topic_message_map[sent_msg.message_id] = user_id elif message.photo: caption = f"👨‍💼 Ответ администратора:\n{message.html_text}" if message.caption else "👨‍💼 Ответ администратора:" await message.bot.send_photo( chat_id=user_id, photo=message.photo[-1].file_id, caption=caption, parse_mode="HTML" ) except Exception as e: log(level='ERROR', log_type='FORWARD', text=f"Ошибка пересылки админ->пользователь: {e}") # ===================== Пересылка ответов пользователя в топик ===================== @router.message(F.chat.type == "private", F.reply_to_message) async def forward_user_reply_to_admin(message: Message) -> None: """Пересылает ответы пользователя (reply) в топик""" if message.from_user.is_bot: return user_id = message.from_user.id reply_to_id = message.reply_to_message.message_id # Проверяем, является ли это ответом на сообщение из топика original_user_id = topic_message_map.get(reply_to_id) if not original_user_id or original_user_id != user_id: return # Получаем топик пользователя thread_id = user_topic_map.get(user_id) if not thread_id: await message.reply("⚠️ Не найден активный диалог. Используйте /new для нового запроса.") return try: # Отправляем ответ пользователя в топик if message.text: formatted_text = f"💬 Ответ от {message.from_user.full_name}:\n{message.html_text}" sent_msg = await message.bot.send_message( chat_id=ImportantID.SUPPORT_CHAT_ID, message_thread_id=thread_id, text=formatted_text, parse_mode="HTML" ) topic_message_map[sent_msg.message_id] = user_id elif message.photo: caption = f"💬 Ответ от {message.from_user.full_name}:\n{message.html_text}" if message.caption else f"💬 Ответ от {message.from_user.full_name}:" sent_msg = await message.bot.send_photo( chat_id=ImportantID.SUPPORT_CHAT_ID, message_thread_id=thread_id, photo=message.photo[-1].file_id, caption=caption, parse_mode="HTML" ) topic_message_map[sent_msg.message_id] = user_id await message.reply("✅ Ответ отправлен администратору.") except Exception as e: await message.reply(f"⚠️ Не удалось отправить ответ: {e}")