Files
balance_bot/bot/handlers/commands/users/new_cmd.py
2026-01-23 04:45:55 +07:00

369 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
"• <b>Продолжить текущий</b> - чтобы писать в существующий диалог\n"
"• <b>Создать новый</b> - если хотите начать новый запрос (старый диалог будет архивирован)",
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"🔔 <b>Пользователь начал новый запрос</b>\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="📝 <b>Создание нового запроса</b>\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"<b>📩 Сообщение от <a href='tg://user?id={user.id}'>{user.full_name}</a>:</b>\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="🔔 <b>Новый запрос создан</b>\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="✅ <b>Запрос отправлен!</b>\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"<b>💬 Сообщение от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\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"<b>💬 Сообщение от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\n{message.html_text}" if message.caption else f"<b>💬 Сообщение от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>"
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"<b>👨‍💼 Ответ администратора:</b>\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"<b>👨‍💼 Ответ администратора:</b>\n{message.html_text}" if message.caption else "<b>👨‍💼 Ответ администратора:</b>"
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"<b>💬 Ответ от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\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"<b>💬 Ответ от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\n{message.html_text}" if message.caption else f"<b>💬 Ответ от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>"
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}")