187 lines
6.5 KiB
Python
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)
|