First commit
This commit is contained in:
368
bot/handlers/commands/users/new_cmd.py
Normal file
368
bot/handlers/commands/users/new_cmd.py
Normal file
@@ -0,0 +1,368 @@
|
||||
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}")
|
||||
Reference in New Issue
Block a user