v1.6.0: max_bot fixes — feature keys, flush→commit, test-run, categories, broadcast page, proxy error handling, deploy scripts
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user