""" Обработчики создания заявки. Поток: 1. Нажатие "Создать заявку" → переход в waiting_description 2. Первое сообщение с текстом → сохранение описания, переход в collecting_attachments 3. Дополнительные сообщения → накопление вложений 4. Нажатие "Готово" → сохранение заявки в БД, подтверждение """ import asyncio from maxapi.context import BaseContext from maxapi.dispatcher import Router from maxapi.enums.attachment import AttachmentType from maxapi.filters import F from maxapi.types.attachments.attachment import ( OtherAttachmentPayload, PhotoAttachmentPayload, ) from maxapi.types.updates.message_callback import MessageCallback from maxapi.types.updates.message_created import MessageCreated from bot.keyboards import done_ticket_kb, main_menu_kb from bot.logging_setup import get_logger from bot.send_helper import bot_send from bot.states import TicketStates logger = get_logger("bot.handlers.create_ticket") from database.queries.ticket_attachments import add_attachment from database.queries.tickets import create_ticket import external.client as ext router = Router(router_id="create_ticket") _MEDIA_TYPES = { AttachmentType.IMAGE, AttachmentType.VIDEO, AttachmentType.AUDIO, AttachmentType.FILE, } def _extract_attachments(event: MessageCreated) -> list[dict]: """Извлекает метаданные вложений из сообщения.""" result = [] body = event.message.body if not body or not body.attachments: return result for att in body.attachments: att_type = getattr(att, "type", None) if att_type not in _MEDIA_TYPES: continue url = None payload = getattr(att, "payload", None) if isinstance(payload, (PhotoAttachmentPayload, OtherAttachmentPayload)): url = payload.url result.append({"type": str(att_type), "url": url}) return result @router.message_callback(F.callback.payload == "create_ticket") async def cb_create_ticket( event: MessageCallback, context: BaseContext, bot_user: dict ) -> None: await context.set_state(TicketStates.waiting_description) await context.set_data({"pending_attachments": []}) logger.info("📝 create_ticket started | user_id=%s", bot_user["max_user_id"]) await event.answer(notification="") chat_id = event.message.recipient.chat_id if event.message else None if chat_id is None: return await bot_send( event.bot, chat_id=chat_id, user_id=bot_user["max_user_id"], text=( "📝 Опишите вашу проблему.\n\n" "Вы также можете прикрепить файлы, фото или голосовые сообщения.\n" "Когда закончите — нажмите кнопку «Готово»." ), attachments=[done_ticket_kb()], ) @router.message_created(states=TicketStates.waiting_description) async def on_ticket_description( event: MessageCreated, context: BaseContext ) -> None: chat_id, user_id = event.get_ids() if chat_id is None: return body = event.message.body text = (body.text or "").strip() if body else "" attachments = _extract_attachments(event) await context.update_data( description=text, pending_attachments=attachments, ) await context.set_state(TicketStates.collecting_attachments) await bot_send( event.bot, chat_id=chat_id, user_id=user_id, text=( "✏️ Описание принято. Вы можете добавить ещё файлы " "или нажмите «Готово» для отправки заявки." ), attachments=[done_ticket_kb()], ) @router.message_created(states=TicketStates.collecting_attachments) async def on_ticket_attachment( event: MessageCreated, context: BaseContext ) -> None: data = await context.get_data() body = event.message.body extra_text = (body.text or "").strip() if body else "" new_atts = _extract_attachments(event) if extra_text: current = data.get("description", "") data["description"] = (current + "\n" + extra_text).strip() pending = data.get("pending_attachments", []) pending.extend(new_atts) data["pending_attachments"] = pending await context.set_data(data) @router.message_callback( F.callback.payload == "done_ticket", states=[TicketStates.waiting_description, TicketStates.collecting_attachments], ) async def cb_done_ticket( event: MessageCallback, context: BaseContext, bot_user: dict ) -> None: data = await context.get_data() description = (data.get("description") or "").strip() chat_id = event.message.recipient.chat_id if event.message else None if chat_id is None: return if not description: await event.answer(notification="Сначала опишите проблему!") await bot_send( event.bot, chat_id=chat_id, user_id=bot_user["max_user_id"], text="❗ Пожалуйста, сначала напишите описание проблемы.", attachments=[done_ticket_kb()], ) return ticket_id = await create_ticket( org_id=bot_user["org_id"], created_by=bot_user["id"], description=description, ) logger.info( "✅ ticket created | ticket_id=%s user_id=%s org_id=%s attachments=%s", ticket_id, bot_user["max_user_id"], bot_user["org_id"], len(data.get("pending_attachments", [])), ) for att in data.get("pending_attachments", []): await add_attachment( ticket_id=ticket_id, attachment_type=att["type"], attachment_url=att.get("url"), ) await context.clear() await event.answer(notification="Заявка создана!") await bot_send( event.bot, chat_id=chat_id, user_id=bot_user["max_user_id"], text=( f"✅ Заявка №{ticket_id} успешно создана!\n" "Наши специалисты свяжутся с вами в ближайшее время." ), attachments=[main_menu_kb()], ) # Fire-and-forget: отправка во внешнюю систему async def _sync_external(): loop = asyncio.get_event_loop() await loop.run_in_executor(None, ext.sync_ticket, ticket_id) asyncio.create_task(_sync_external())