269 lines
11 KiB
Python
269 lines
11 KiB
Python
import json
|
|
import logging
|
|
from typing import Optional
|
|
from app.max_api import MaxAPIClient
|
|
from app.settings_cache import SettingsCache
|
|
from app.fsm import BotState, FSM
|
|
from app.conversation import (
|
|
get_or_create_user, add_message,
|
|
get_template_rendered, find_in_knowledge_base
|
|
)
|
|
from app.utils.emotion_detector import detect_emotion
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def handle_update(bot, update: dict, db, max_api: MaxAPIClient):
|
|
update_type = update.get("update_type", "")
|
|
user_data = update.get("user", {})
|
|
max_user_id = user_data.get("user_id")
|
|
|
|
if not max_user_id:
|
|
return
|
|
|
|
await SettingsCache.refresh(db)
|
|
|
|
if update_type == "bot_started":
|
|
await _handle_bot_started(bot, user_data, db, max_api)
|
|
return
|
|
|
|
if update_type == "message_created":
|
|
await _handle_message_created(bot, user_data, update, db, max_api)
|
|
return
|
|
|
|
if update_type == "message_callback":
|
|
await _handle_callback(bot, user_data, update, db, max_api)
|
|
return
|
|
|
|
|
|
async def _handle_bot_started(bot, user_data, db, max_api):
|
|
await SettingsCache.refresh(db)
|
|
if not SettingsCache.is_work_hours():
|
|
from app.handlers.out_of_hours import handle_out_of_hours
|
|
await handle_out_of_hours(bot, user_data, db, max_api)
|
|
return
|
|
from app.handlers.greeting import handle_start
|
|
await handle_start(bot, user_data, db, max_api)
|
|
|
|
|
|
async def _handle_message_created(bot, user_data, update, db, max_api):
|
|
max_user_id = user_data["user_id"]
|
|
message = update.get("message", {})
|
|
body = message.get("body", {})
|
|
text = body.get("text", "").strip()
|
|
attachments = body.get("attachments", [])
|
|
|
|
user_data["text"] = text
|
|
user_data["ip"] = update.get("ip", "")
|
|
|
|
has_attachment = len(attachments) > 0
|
|
attachment_type = ""
|
|
attachment_path = ""
|
|
|
|
if has_attachment:
|
|
att = attachments[0]
|
|
attachment_type = att.get("type", "")
|
|
if attachment_type == "contact":
|
|
payload = att.get("payload", {})
|
|
vcf_info = payload.get("vcf_info", "")
|
|
import re
|
|
phone_match = re.search(r'TEL[^:]*:(\+?\d+)', vcf_info)
|
|
name_match = re.search(r'FN:(.+)', vcf_info)
|
|
if phone_match:
|
|
user_data["phone"] = phone_match.group(1)
|
|
if name_match:
|
|
user_data["first_name"] = name_match.group(1).strip()
|
|
await _handle_contact_shared(bot, user_data, db, max_api)
|
|
return
|
|
attachment_path = att.get("url", "")
|
|
|
|
user_data["has_attachment"] = has_attachment
|
|
user_data["attachment_type"] = attachment_type
|
|
user_data["attachment_path"] = attachment_path
|
|
|
|
state = FSM.get_state(max_user_id)
|
|
|
|
if state == BotState.IDLE:
|
|
if text.lower() in ("/start", "/help"):
|
|
if not SettingsCache.is_work_hours():
|
|
from app.handlers.out_of_hours import handle_out_of_hours
|
|
await handle_out_of_hours(bot, user_data, db, max_api)
|
|
return
|
|
from app.handlers.greeting import handle_start
|
|
await handle_start(bot, user_data, db, max_api)
|
|
return
|
|
|
|
if not SettingsCache.is_work_hours():
|
|
from app.handlers.out_of_hours import handle_out_of_hours
|
|
await handle_out_of_hours(bot, user_data, db, max_api)
|
|
return
|
|
|
|
emotion = await detect_emotion(text)
|
|
if emotion == "negative":
|
|
tmpl = await get_template_rendered(db, "emotion_escalate")
|
|
await max_api.send_message(max_user_id, tmpl)
|
|
from app.handlers.handoff import handle_handoff_request
|
|
await handle_handoff_request(bot, user_data, db, max_api)
|
|
from app.max_notifications import notify_handoff_escalation
|
|
user = await get_or_create_user(db, max_user_id)
|
|
conv = None
|
|
try:
|
|
from app.conversation import get_open_conversation
|
|
conv = await get_open_conversation(db, user.id)
|
|
except Exception:
|
|
pass
|
|
await notify_handoff_escalation(
|
|
db, max_api, user,
|
|
conv.id if conv else 0, text
|
|
)
|
|
return
|
|
|
|
kb_entry = await find_in_knowledge_base(db, text)
|
|
if kb_entry:
|
|
await max_api.send_message(max_user_id, kb_entry.answer, format="markdown")
|
|
return
|
|
|
|
from app.conversation import get_or_create_user
|
|
user = await get_or_create_user(db, max_user_id)
|
|
if not user.consent_given:
|
|
from app.handlers.greeting import handle_start
|
|
await handle_start(bot, user_data, db, max_api)
|
|
return
|
|
|
|
from app.handlers.main_menu import handle_main_menu
|
|
user_data["payload"] = "new_inquiry"
|
|
await handle_main_menu(bot, user_data, db, max_api)
|
|
return
|
|
|
|
if state == BotState.NAME_REQUEST:
|
|
if not text:
|
|
await max_api.send_message(max_user_id, "Как к вам обращаться?")
|
|
return
|
|
user = await get_or_create_user(db, max_user_id)
|
|
user.first_name = text
|
|
await db.flush()
|
|
from app.handlers.consent import handle_consent_request
|
|
await handle_consent_request(bot, user_data, db, max_api)
|
|
return
|
|
|
|
if state == BotState.CONSENT_REQUEST:
|
|
if text.lower() in ("да", "даю согласие", "согласен", "yes"):
|
|
from app.handlers.consent import handle_consent_given
|
|
await handle_consent_given(bot, user_data, db, max_api)
|
|
elif text.lower() in ("нет", "не даю", "не согласен", "no"):
|
|
from app.handlers.consent import handle_consent_refused
|
|
await handle_consent_refused(bot, user_data, db, max_api)
|
|
else:
|
|
from app.handlers.main_menu import handle_main_menu
|
|
user_data["payload"] = text.lower()
|
|
await handle_main_menu(bot, user_data, db, max_api)
|
|
return
|
|
|
|
if state == BotState.CONTACT_REQUEST:
|
|
from app.handlers.contact import handle_contact_text
|
|
await handle_contact_text(bot, user_data, db, max_api)
|
|
return
|
|
|
|
if state == BotState.INQUIRY_REQUEST:
|
|
from app.handlers.inquiry import handle_inquiry
|
|
await handle_inquiry(bot, user_data, db, max_api)
|
|
return
|
|
|
|
if state == BotState.CATEGORY_SELECT:
|
|
from app.handlers.inquiry import handle_inquiry
|
|
await handle_inquiry(bot, user_data, db, max_api)
|
|
return
|
|
|
|
if state == BotState.OUT_OF_HOURS:
|
|
from app.handlers.out_of_hours import handle_out_of_hours_callback
|
|
await handle_out_of_hours_callback(bot, user_data, db, max_api, text.lower())
|
|
return
|
|
|
|
_feature_handlers = {
|
|
BotState.FEATURE_RISK_SCORE: ("app.handlers.features.risk_score", "handle_risk_score"),
|
|
BotState.FEATURE_SLA_STATUS: ("app.handlers.features.sla_status", "handle_sla_status"),
|
|
BotState.FEATURE_PHOTO_REPORT: ("app.handlers.features.photo_report", "handle_photo_report"),
|
|
BotState.FEATURE_APPOINTMENT: ("app.handlers.features.appointment", "handle_appointment"),
|
|
BotState.FEATURE_COMMERCIAL: ("app.handlers.features.commercial", "handle_commercial"),
|
|
BotState.FEATURE_MY_OBJECTS: ("app.handlers.features.my_objects", "handle_my_objects"),
|
|
BotState.FEATURE_REMINDERS: ("app.handlers.features.reminders", "handle_reminders"),
|
|
BotState.FEATURE_QUALITY_RATE: ("app.handlers.features.quality_rate", "handle_quality_rate"),
|
|
BotState.FEATURE_NOTIFICATIONS: ("app.handlers.features.notifications", "handle_notifications"),
|
|
BotState.FEATURE_BROADCAST: ("app.handlers.features.broadcasts", "handle_broadcast_send"),
|
|
BotState.SLA_TICKET_CREATE: ("app.handlers.features.sla_ticket", "handle_sla_ticket"),
|
|
}
|
|
if state in _feature_handlers:
|
|
mod_path, func_name = _feature_handlers[state]
|
|
import importlib
|
|
mod = importlib.import_module(mod_path)
|
|
func = getattr(mod, func_name)
|
|
await func(bot, user_data, db, max_api)
|
|
return
|
|
|
|
if state == BotState.KB_SEARCH:
|
|
kb_entry = await find_in_knowledge_base(db, text)
|
|
if kb_entry:
|
|
await max_api.send_message(max_user_id, kb_entry.answer, format="markdown")
|
|
FSM.transition(max_user_id, "found")
|
|
else:
|
|
await max_api.send_message(max_user_id,
|
|
"К сожалению, не нашёл ответа на ваш вопрос. "
|
|
"Опишите подробнее или нажмите «Оставить заявку» для создания обращения.")
|
|
FSM.transition(max_user_id, "not_found")
|
|
from app.keyboards.inline import build_main_menu_buttons
|
|
buttons = build_main_menu_buttons()
|
|
menu_text = await get_template_rendered(db, "main_menu")
|
|
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
|
return
|
|
|
|
from app.handlers.main_menu import handle_main_menu
|
|
user_data["payload"] = text.lower()
|
|
await handle_main_menu(bot, user_data, db, max_api)
|
|
|
|
|
|
async def _handle_callback(bot, user_data, update, db, max_api):
|
|
max_user_id = user_data["user_id"]
|
|
callback = update.get("callback", {})
|
|
payload = callback.get("payload", "")
|
|
callback_id = callback.get("id", "")
|
|
|
|
user_data["payload"] = payload
|
|
|
|
if payload == "cancel":
|
|
FSM.reset(max_user_id)
|
|
from app.keyboards.inline import build_main_menu_buttons
|
|
buttons = build_main_menu_buttons()
|
|
menu_text = await get_template_rendered(db, "main_menu")
|
|
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
|
await max_api.answer_callback(callback_id)
|
|
return
|
|
|
|
if payload.startswith("rate_"):
|
|
from app.handlers.features.quality_rate import handle_quality_rate
|
|
await handle_quality_rate(bot, user_data, db, max_api)
|
|
await max_api.answer_callback(callback_id)
|
|
return
|
|
|
|
from app.handlers.main_menu import handle_main_menu
|
|
await handle_main_menu(bot, user_data, db, max_api)
|
|
await max_api.answer_callback(callback_id)
|
|
|
|
|
|
async def _handle_contact_shared(bot, user_data, db, max_api):
|
|
max_user_id = user_data["user_id"]
|
|
user = await get_or_create_user(db, max_user_id)
|
|
if user_data.get("phone"):
|
|
user.phone = user_data["phone"]
|
|
if user_data.get("first_name"):
|
|
user.first_name = user_data["first_name"]
|
|
await db.flush()
|
|
|
|
state = FSM.get_state(max_user_id)
|
|
if state == BotState.CONTACT_REQUEST:
|
|
from app.handlers.contact import handle_contact_provided
|
|
await handle_contact_provided(bot, user_data, db, max_api)
|
|
else:
|
|
from app.handlers.main_menu import handle_main_menu
|
|
user_data["payload"] = "new_inquiry"
|
|
await handle_main_menu(bot, user_data, db, max_api)
|