""" Обработчик быстрого ответа на заявку. Перехватывает свободные сообщения авторизованных пользователей вне активного флоу и предлагает отправить их как комментарий к последней активной заявке. Последняя активная заявка определяется по: - уведомлению, которое было отправлено пользователю (notifier) - карточке заявки, которую пользователь открывал (cb_iticket_detail) TTL контекста — 60 минут (настраивается в reply_context.py). """ from __future__ import annotations from maxapi.context import BaseContext from maxapi.dispatcher import Router from maxapi.filters import F from maxapi.types.updates.message_callback import MessageCallback from maxapi.types.updates.message_created import MessageCreated from bot.filters import PayloadStartsWith from bot.flow_config import PAYLOADS, TRANSITIONS from bot.handlers.create_ticket import _extract_attachments from bot.keyboards import main_menu_kb, reply_confirm_kb from bot.nav_helper import nav_set from bot.send_helper import bot_send from bot.states import AuthStates, CommentStates, TicketStates from bot.texts import QuickReplyTexts from database.connection import get_db from database.queries.intradesk import get_intradesk_contact_id_for_phone from database.queries.organizations import get_organization from database.queries.reply_context import clear_reply_context, get_reply_context from external.intradesk_api import ( add_task_comment, invalidate_client_cache, invalidate_task_comment, upload_attachments, ) router = Router(router_id="quick_reply") _ACTIVE_STATES = [ AuthStates.waiting_contact, TicketStates.waiting_description, TicketStates.collecting_attachments, TicketStates.confirming, TicketStates.pending_confirm, CommentStates.waiting_text, ] @router.message_created() async def on_idle_message( event: MessageCreated, context: BaseContext, bot_user: dict ) -> None: """Перехватить свободное сообщение вне активного флоу.""" state = await context.get_state() if state in _ACTIVE_STATES: return chat_id, user_id = event.get_ids() if chat_id is None or user_id is None: return body = event.message.body text = (body.text or "").strip() if body else "" file_atts = _extract_attachments(event) if not text and not file_atts: return # стикер, локация и т.п. — игнорируем ctx = await get_reply_context(user_id) if not ctx: return # нет активного контекста заявки — молча игнорируем task_id: int = ctx["task_id"] task_number: int | None = ctx["task_number"] await context.set_state(TRANSITIONS["reply:pending"]) await context.set_data({ "pending_reply_text": text, "pending_reply_atts": file_atts, "pending_reply_task_id": task_id, "pending_reply_task_number": task_number, }) display = text[:300] + ("…" if len(text) > 300 else "") if text else "" if file_atts: atts_note = f"\n📎 +{len(file_atts)} файл(ов)" display = (display + atts_note) if display else atts_note.strip() await bot_send( event.bot, chat_id=chat_id, user_id=user_id, text=QuickReplyTexts.CONFIRM.format( number=task_number or task_id, text=display, ), attachments=[reply_confirm_kb(task_id)], ) @router.message_callback(PayloadStartsWith(PAYLOADS.PFX_CONFIRM_REPLY)) async def cb_confirm_reply( event: MessageCallback, context: BaseContext, bot_user: dict ) -> None: state = await context.get_state() if state != TRANSITIONS["reply:pending"]: await event.answer(notification="") return await event.answer(notification="") data = await context.get_data() task_id: int | None = data.get("pending_reply_task_id") task_number: int | None = data.get("pending_reply_task_number") text: str = data.get("pending_reply_text") or "" file_atts: list[dict] = data.get("pending_reply_atts") or [] await context.clear() await clear_reply_context(bot_user["max_user_id"]) chat_id = event.message.recipient.chat_id if event.message else None if not chat_id or not task_id: return # Данные организации для contact_person_id и инвалидации кэша intradesk_id: int | None = None contact_person_id: int | None = None if bot_user.get("org_id"): org = await get_organization(bot_user["org_id"]) intradesk_id = org.get("intradesk_id") if org else None if intradesk_id and bot_user.get("phone"): db = get_db() contact_person_id = await get_intradesk_contact_id_for_phone( db, intradesk_id, bot_user["phone"] ) if file_atts: file_atts = await upload_attachments(file_atts, task_id=task_id, target="Comment") ok = await add_task_comment( task_number=task_number, text=text or "📎 Файл прикреплён", contact_person_id=contact_person_id, attachments=file_atts or None, ) if ok: invalidate_task_comment(task_id) if intradesk_id: invalidate_client_cache(intradesk_id) result = ( QuickReplyTexts.SENT.format(number=task_number or task_id) if ok else QuickReplyTexts.ERROR ) mid = await bot_send( event.bot, chat_id=chat_id, user_id=bot_user["max_user_id"], text=result, attachments=[main_menu_kb()], ) await nav_set(context, mid) @router.message_callback(F.callback.payload == PAYLOADS.CANCEL_REPLY) async def cb_cancel_reply( event: MessageCallback, context: BaseContext, bot_user: dict ) -> None: await event.answer(notification="") await context.clear() chat_id = event.message.recipient.chat_id if event.message else None if not chat_id: return from bot.handlers.menu import build_main_menu_text mid = await bot_send( event.bot, chat_id=chat_id, user_id=bot_user["max_user_id"], text=await build_main_menu_text(bot_user), attachments=[main_menu_kb()], ) await nav_set(context, mid)