intra_max_chatbot/bot/handlers/quick_reply.py
2026-04-03 00:30:24 +03:00

187 lines
6.5 KiB
Python

"""
Обработчик быстрого ответа на заявку.
Перехватывает свободные сообщения авторизованных пользователей вне активного
флоу и предлагает отправить их как комментарий к последней активной заявке.
Последняя активная заявка определяется по:
- уведомлению, которое было отправлено пользователю (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)