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}")