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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,60 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, log_consent, get_template_rendered
|
||||
from app.keyboards.inline import build_consent_buttons, build_contact_buttons
|
||||
from app.fsm import BotState, FSM
|
||||
|
||||
|
||||
async def handle_consent_request(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
user = await get_or_create_user(db, max_user_id,
|
||||
user_data.get("first_name", ""),
|
||||
user_data.get("last_name", ""),
|
||||
user_data.get("username", ""))
|
||||
await db.flush()
|
||||
|
||||
await SettingsCache.refresh(db)
|
||||
|
||||
policy_url = SettingsCache.get("consent_policy_url", "https://aegisone.ru/politica.php")
|
||||
text = await get_template_rendered(db, "consent_request", policy_url=policy_url)
|
||||
buttons = build_consent_buttons()
|
||||
await max_api.send_message_with_keyboard(max_user_id, text, buttons, format="markdown")
|
||||
FSM.set_state(max_user_id, BotState.CONSENT_REQUEST)
|
||||
|
||||
|
||||
async def handle_consent_given(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
|
||||
user.consent_given = True
|
||||
from datetime import datetime
|
||||
user.consent_timestamp = datetime.now()
|
||||
user.consent_method = "button"
|
||||
await log_consent(db, user.id, "given", "button", user_data.get("ip", ""))
|
||||
await db.flush()
|
||||
|
||||
text = await get_template_rendered(db, "contact_request")
|
||||
buttons = build_contact_buttons()
|
||||
await max_api.send_message_with_keyboard(max_user_id, text, buttons)
|
||||
FSM.set_state(max_user_id, BotState.CONTACT_REQUEST)
|
||||
|
||||
|
||||
async def handle_consent_refused(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
|
||||
await log_consent(db, user.id, "revoked", "button", user_data.get("ip", ""))
|
||||
await db.flush()
|
||||
|
||||
await SettingsCache.refresh(db)
|
||||
phone_1 = SettingsCache.get("phone_1")
|
||||
phone_2 = SettingsCache.get("phone_2")
|
||||
email = SettingsCache.get("support_email")
|
||||
|
||||
text = await get_template_rendered(db, "consent_refused",
|
||||
phone_1=phone_1, phone_2=phone_2,
|
||||
support_email=email)
|
||||
from app.keyboards.inline import build_phone_email_buttons
|
||||
subject = SettingsCache.get("email_subject")
|
||||
buttons = build_phone_email_buttons(phone_1, phone_2, email, subject)
|
||||
await max_api.send_message_with_keyboard(max_user_id, text, buttons, format="markdown")
|
||||
FSM.reset(max_user_id)
|
||||
@@ -0,0 +1,43 @@
|
||||
import re
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
|
||||
|
||||
async def handle_contact_provided(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
|
||||
phone = user_data.get("phone", "")
|
||||
email = user_data.get("email", "")
|
||||
|
||||
if phone:
|
||||
user.phone = phone
|
||||
if email:
|
||||
user.email = email
|
||||
await db.flush()
|
||||
|
||||
text = await get_template_rendered(db, "inquiry_request")
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
FSM.set_state(max_user_id, BotState.INQUIRY_REQUEST)
|
||||
|
||||
|
||||
async def handle_contact_text(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
|
||||
phone_match = re.search(r'(\+?7[\s\-\(]?\d{3}[\s\-\)]?\d{3}[\s\-]?\d{2}[\s\-]?\d{2})', text)
|
||||
email_match = re.search(r'([\w.-]+@[\w.-]+\.\w+)', text)
|
||||
|
||||
if phone_match:
|
||||
user.phone = phone_match.group(1)
|
||||
if email_match:
|
||||
user.email = email_match.group(1)
|
||||
|
||||
if phone_match or email_match:
|
||||
await db.flush()
|
||||
await handle_contact_provided(bot, user_data, db, max_api)
|
||||
return
|
||||
|
||||
await max_api.send_message(max_user_id, "Пожалуйста, укажите номер телефона или email, чтобы мы могли с вами связаться.")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,105 @@
|
||||
from datetime import datetime
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
_DATE_KEY = "appt_date"
|
||||
_SLOT_KEY = "appt_slot"
|
||||
|
||||
|
||||
async def handle_appointment(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
payload = user_data.get("payload", "")
|
||||
state = FSM.get_state(max_user_id)
|
||||
|
||||
if state != BotState.FEATURE_APPOINTMENT:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_APPOINTMENT)
|
||||
FSM.set_temp(max_user_id, {})
|
||||
await max_api.send_message(
|
||||
max_user_id,
|
||||
"📅 **Запись на визит**\n\nУкажите желаемую дату (ДД.ММ.ГГГГ):"
|
||||
)
|
||||
return
|
||||
|
||||
tmp = FSM.get_temp(max_user_id)
|
||||
|
||||
if _DATE_KEY not in tmp:
|
||||
try:
|
||||
parsed = datetime.strptime(text.strip(), "%d.%m.%Y")
|
||||
tmp[_DATE_KEY] = parsed.strftime("%Y-%m-%d")
|
||||
FSM.set_temp(max_user_id, tmp)
|
||||
|
||||
slots = _generate_slots()
|
||||
msg_lines = [f"📅 **{parsed.strftime('%d.%m.%Y')}**\n\nДоступные слоты:\n"]
|
||||
for i, slot in enumerate(slots, 1):
|
||||
msg_lines.append(f"{i}. {slot}")
|
||||
msg_lines.append("\nВведите номер слота (1-{}):".format(len(slots)))
|
||||
await max_api.send_message(max_user_id, "\n".join(msg_lines), format="markdown")
|
||||
except Exception:
|
||||
await max_api.send_message(max_user_id, "Неверный формат даты. Укажите дату в формате ДД.ММ.ГГГГ")
|
||||
return
|
||||
|
||||
if _SLOT_KEY not in tmp:
|
||||
slots = _generate_slots()
|
||||
try:
|
||||
idx = int(text.strip()) - 1
|
||||
if 0 <= idx < len(slots):
|
||||
tmp[_SLOT_KEY] = slots[idx]
|
||||
FSM.set_temp(max_user_id, tmp)
|
||||
await _confirm_appointment(bot, user_data, db, max_api, tmp)
|
||||
return
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
await max_api.send_message(max_user_id, f"Укажите номер слота (1-{len(slots)}):")
|
||||
return
|
||||
|
||||
await _confirm_appointment(bot, user_data, db, max_api, tmp)
|
||||
|
||||
|
||||
def _generate_slots() -> list:
|
||||
start_h = SettingsCache.get_int("work_hours_start", 9)
|
||||
end_h = SettingsCache.get_int("work_hours_end", 18)
|
||||
slots = []
|
||||
for h in range(start_h, end_h):
|
||||
slots.append(f"{h:02d}:00-{h + 1:02d}:00")
|
||||
return slots
|
||||
|
||||
|
||||
async def _confirm_appointment(bot, user_data, db, max_api, tmp):
|
||||
max_user_id = user_data["user_id"]
|
||||
date_str = tmp.get(_DATE_KEY, "—")
|
||||
slot_str = tmp.get(_SLOT_KEY, "—")
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
text = await get_template_rendered(db, "appointment_confirm",
|
||||
date=f"{date_str} {slot_str}",
|
||||
engineer="будет назначен")
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
|
||||
conv = await _create_appointment_conv(db, user.id, f"{date_str} {slot_str}")
|
||||
from app.integrations import _1c_unf as ic_integration
|
||||
await ic_integration.send_to_1c(db, conv)
|
||||
|
||||
FSM.clear_temp(max_user_id)
|
||||
FSM.transition(max_user_id, "done")
|
||||
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)
|
||||
|
||||
|
||||
async def _create_appointment_conv(db, user_id, details):
|
||||
from app.conversation import create_conversation, add_message
|
||||
from sqlalchemy import select
|
||||
from app.models.models import BotCategory
|
||||
result = await db.execute(
|
||||
select(BotCategory).where(BotCategory.name == "Обслуживание").limit(1)
|
||||
)
|
||||
cat = result.scalar_one_or_none()
|
||||
conv = await create_conversation(db, user_id)
|
||||
conv.inquiry_text = f"Запись на визит: {details}"
|
||||
conv.inquiry_type = cat.id if cat else None
|
||||
conv.priority = "normal"
|
||||
await db.flush()
|
||||
await add_message(db, conv.id, "in", details)
|
||||
return conv
|
||||
@@ -0,0 +1,44 @@
|
||||
from datetime import datetime
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
|
||||
|
||||
async def handle_broadcast_send(bot, user_data: dict, db, max_api):
|
||||
from app.models.models import BotUser
|
||||
from sqlalchemy import select
|
||||
|
||||
text = user_data.get("text", "").strip()
|
||||
if not text:
|
||||
await max_api.send_message(
|
||||
user_data["user_id"],
|
||||
"Укажите текст рассылки в параметре broadcast_text."
|
||||
)
|
||||
return
|
||||
|
||||
result = await db.execute(select(BotUser).where(BotUser.consent_given == True))
|
||||
users = result.scalars().all()
|
||||
sent = 0
|
||||
failed = 0
|
||||
sent_ids = []
|
||||
for u in users:
|
||||
try:
|
||||
await max_api.send_message(user_id=u.max_user_id, text=text, format="markdown")
|
||||
sent += 1
|
||||
sent_ids.append(u.max_user_id)
|
||||
except Exception:
|
||||
failed += 1
|
||||
|
||||
from app.models.models import BotBroadcast
|
||||
bc = BotBroadcast(
|
||||
text=text,
|
||||
sent_at=datetime.now(),
|
||||
recipient_count=len(users),
|
||||
recipient_ids=sent_ids,
|
||||
status="sent",
|
||||
)
|
||||
db.add(bc)
|
||||
await db.flush()
|
||||
|
||||
result = f"📨 Рассылка завершена.\nОтправлено: {sent}\nОшибок: {failed}"
|
||||
await max_api.send_message(user_data["user_id"], result)
|
||||
@@ -0,0 +1,101 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
_STEPS = ["service_type", "scope", "address", "comment"]
|
||||
_STEP_LABELS = {
|
||||
"service_type": "Тип услуги (Видеонаблюдение, СКУД, Пожарная сигнализация, IT-инфраструктура, Обслуживание, Другое)",
|
||||
"scope": "Объём работ (краткое описание)",
|
||||
"address": "Адрес объекта",
|
||||
"comment": "Дополнительные пожелания (или отправьте «-» для пропуска)",
|
||||
}
|
||||
|
||||
_COLLECT_KEY = "commercial_data"
|
||||
_STEP_KEY = "commercial_step"
|
||||
|
||||
|
||||
async def handle_commercial(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
state = FSM.get_state(max_user_id)
|
||||
|
||||
if state != BotState.FEATURE_COMMERCIAL:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_COMMERCIAL)
|
||||
FSM.set_temp(max_user_id, {_STEP_KEY: 0, _COLLECT_KEY: {}})
|
||||
await max_api.send_message(
|
||||
max_user_id,
|
||||
f"📄 **Коммерческое предложение**\n\nШаг 1/{len(_STEPS)}:\n{_STEP_LABELS['service_type']}"
|
||||
)
|
||||
return
|
||||
|
||||
tmp = FSM.get_temp(max_user_id)
|
||||
step = tmp.get(_STEP_KEY, 0)
|
||||
data = tmp.get(_COLLECT_KEY, {})
|
||||
|
||||
if step >= len(_STEPS):
|
||||
await _finish_commercial(bot, user_data, db, max_api, data)
|
||||
return
|
||||
|
||||
step_key = _STEPS[step]
|
||||
if not text and step_key != "comment":
|
||||
await max_api.send_message(max_user_id, f"Пожалуйста, заполните поле:\n{_STEP_LABELS[step_key]}")
|
||||
return
|
||||
|
||||
data[step_key] = text if text else ""
|
||||
step += 1
|
||||
|
||||
if step < len(_STEPS):
|
||||
FSM.set_temp(max_user_id, {_STEP_KEY: step, _COLLECT_KEY: data})
|
||||
next_key = _STEPS[step]
|
||||
await max_api.send_message(
|
||||
max_user_id,
|
||||
f"📄 Шаг {step + 1}/{len(_STEPS)}:\n{_STEP_LABELS[next_key]}"
|
||||
)
|
||||
else:
|
||||
await _finish_commercial(bot, user_data, db, max_api, data)
|
||||
|
||||
|
||||
async def _finish_commercial(bot, user_data, db, max_api, data):
|
||||
max_user_id = user_data["user_id"]
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
summary = (
|
||||
f"📄 **Коммерческое предложение**\n\n"
|
||||
f"Услуга: {data.get('service_type', '—')}\n"
|
||||
f"Объём: {data.get('scope', '—')}\n"
|
||||
f"Адрес: {data.get('address', '—')}\n"
|
||||
f"Комментарий: {data.get('comment', '—')}\n\n"
|
||||
f"Спасибо, {user.first_name}! Мы подготовим КП и свяжемся с вами."
|
||||
)
|
||||
await max_api.send_message(max_user_id, summary, format="markdown")
|
||||
|
||||
conv_text = (
|
||||
f"Коммерческое предложение: услуга={data.get('service_type', '')}, "
|
||||
f"объём={data.get('scope', '')}, адрес={data.get('address', '')}, "
|
||||
f"комментарий={data.get('comment', '')}"
|
||||
)
|
||||
conv = await _create_commercial_conv(db, user.id, conv_text)
|
||||
from app.integrations import _1c_unf as ic_integration
|
||||
await ic_integration.send_to_1c(db, conv)
|
||||
|
||||
FSM.clear_temp(max_user_id)
|
||||
FSM.transition(max_user_id, "done")
|
||||
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)
|
||||
|
||||
|
||||
async def _create_commercial_conv(db, user_id, details):
|
||||
from app.conversation import create_conversation, add_message
|
||||
from sqlalchemy import select
|
||||
from app.models.models import BotCategory
|
||||
result = await db.execute(
|
||||
select(BotCategory).where(BotCategory.name == "Консультация").limit(1)
|
||||
)
|
||||
cat = result.scalar_one_or_none()
|
||||
conv = await create_conversation(db, user_id)
|
||||
conv.inquiry_text = details
|
||||
conv.inquiry_type = cat.id if cat else None
|
||||
await db.flush()
|
||||
await add_message(db, conv.id, "in", details)
|
||||
return conv
|
||||
@@ -0,0 +1,39 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
|
||||
async def handle_my_objects(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
|
||||
if FSM.get_state(max_user_id) != BotState.FEATURE_MY_OBJECTS:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_MY_OBJECTS)
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
await max_api.send_message(max_user_id,
|
||||
"🏢 **Мои объекты**\n\n"
|
||||
"Укажите название вашей компании для поиска объектов."
|
||||
)
|
||||
return
|
||||
|
||||
if not text:
|
||||
await max_api.send_message(max_user_id, "Укажите название компании.")
|
||||
return
|
||||
|
||||
from app.integrations.portal_db import get_objects_for_customer
|
||||
objects = await get_objects_for_customer(db, text)
|
||||
|
||||
if not objects:
|
||||
await max_api.send_message(max_user_id, "Объекты не найдены. Проверьте название компании.")
|
||||
else:
|
||||
lines = [f"🏢 **{obj.name}** — {obj.status}" for obj in objects[:10]]
|
||||
msg = "Ваши объекты:\n\n" + "\n".join(lines)
|
||||
if len(objects) > 10:
|
||||
msg += f"\n\n...и ещё {len(objects) - 10} объектов"
|
||||
await max_api.send_message(max_user_id, msg, format="markdown")
|
||||
|
||||
FSM.transition(max_user_id, "done")
|
||||
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)
|
||||
@@ -0,0 +1,68 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
|
||||
async def handle_notifications(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
payload = user_data.get("payload", "")
|
||||
|
||||
if FSM.get_state(max_user_id) != BotState.FEATURE_NOTIFICATIONS:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_NOTIFICATIONS)
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
from sqlalchemy import select
|
||||
from app.models.models import BotNotificationSubscription
|
||||
result = await db.execute(
|
||||
select(BotNotificationSubscription).where(
|
||||
BotNotificationSubscription.user_id == user.id
|
||||
).limit(1)
|
||||
)
|
||||
sub = result.scalar_one_or_none()
|
||||
status_change = "✅" if sub and sub.notify_on_status_change else "⬜"
|
||||
reply = "✅" if sub and sub.notify_on_reply else "⬜"
|
||||
broadcast = "✅" if sub and sub.notify_on_broadcast else "⬜"
|
||||
text = (
|
||||
"🔔 **Уведомления**\n\n"
|
||||
"Выберите, о чём уведомлять:\n\n"
|
||||
f"{status_change} /status — Смена статуса обращения\n"
|
||||
f"{reply} /reply — Ответ оператора\n"
|
||||
f"{broadcast} /broadcast — Новости и рассылки\n\n"
|
||||
"Нажмите на пункт, чтобы переключить."
|
||||
)
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
return
|
||||
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
from sqlalchemy import select
|
||||
from app.models.models import BotNotificationSubscription
|
||||
result = await db.execute(
|
||||
select(BotNotificationSubscription).where(
|
||||
BotNotificationSubscription.user_id == user.id
|
||||
).limit(1)
|
||||
)
|
||||
sub = result.scalar_one_or_none()
|
||||
if not sub:
|
||||
sub = BotNotificationSubscription(user_id=user.id)
|
||||
db.add(sub)
|
||||
await db.flush()
|
||||
|
||||
toggle_map = {
|
||||
"/status": "notify_on_status_change",
|
||||
"/reply": "notify_on_reply",
|
||||
"/broadcast": "notify_on_broadcast",
|
||||
}
|
||||
key = payload if payload.startswith("/") else (payload if payload in toggle_map else user_data.get("text", "").strip())
|
||||
if key in toggle_map:
|
||||
col = toggle_map[key]
|
||||
current = getattr(sub, col)
|
||||
setattr(sub, col, not current)
|
||||
await db.flush()
|
||||
await max_api.send_message(max_user_id, f"Настройка «{key}» изменена.")
|
||||
else:
|
||||
await max_api.send_message(max_user_id, "Нажмите /status, /reply или /broadcast для переключения.")
|
||||
|
||||
FSM.transition(max_user_id, "done")
|
||||
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)
|
||||
@@ -0,0 +1,69 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_open_conversation, create_conversation, add_message, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
|
||||
async def handle_photo_report(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
|
||||
if FSM.get_state(max_user_id) != BotState.FEATURE_PHOTO_REPORT:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_PHOTO_REPORT)
|
||||
await max_api.send_message(max_user_id,
|
||||
"📸 **Фото-отчёт**\n\n"
|
||||
"Отправьте фото проблемы и опишите, что произошло.\n"
|
||||
"Вы можете прикрепить документ (PDF, DOCX, XLSX, TXT)."
|
||||
)
|
||||
return
|
||||
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
conv = await get_open_conversation(db, user.id)
|
||||
if not conv:
|
||||
conv = await create_conversation(db, user.id)
|
||||
|
||||
has_attachment = user_data.get("has_attachment", False)
|
||||
attachment_type = user_data.get("attachment_type", "")
|
||||
attachment_path = user_data.get("attachment_path", "")
|
||||
|
||||
conv.inquiry_text = text or "Фото-отчёт"
|
||||
conv.has_attachment = has_attachment
|
||||
conv.attachment_type = attachment_type
|
||||
conv.attachment_path = attachment_path
|
||||
conv.priority = "high"
|
||||
await db.flush()
|
||||
|
||||
await add_message(db, conv.id, "in", text, has_attachment, attachment_type, attachment_path)
|
||||
|
||||
ooh = not SettingsCache.is_work_hours()
|
||||
if ooh:
|
||||
start = SettingsCache.get_int("work_hours_start", 9)
|
||||
end = SettingsCache.get_int("work_hours_end", 18)
|
||||
ooh_text = await get_template_rendered(db, "out_of_hours",
|
||||
work_hours_start=start, work_hours_end=end)
|
||||
await max_api.send_message(max_user_id, ooh_text)
|
||||
|
||||
text = await get_template_rendered(db, "confirmation_ooh" if ooh else "confirmation",
|
||||
first_name=user.first_name, conv_id=conv.id)
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
|
||||
from app.integrations import _1c_unf as ic_integration
|
||||
await ic_integration.send_to_1c(db, conv)
|
||||
|
||||
from app.models.models import Incident
|
||||
incident = Incident(
|
||||
object_id=0,
|
||||
reported_by=None,
|
||||
title=f"Фото-отчёт от {user.first_name or 'клиента'}",
|
||||
description=text or "Фото-отчёт через бота",
|
||||
severity="P3",
|
||||
status="open",
|
||||
)
|
||||
db.add(incident)
|
||||
await db.flush()
|
||||
conv.attachment_path = f"incident_{incident.id}"
|
||||
|
||||
FSM.transition(max_user_id, "done")
|
||||
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)
|
||||
@@ -0,0 +1,35 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_open_conversation, get_template_rendered
|
||||
from app.keyboards.inline import build_quality_buttons, build_main_menu_buttons
|
||||
from app.fsm import BotState, FSM
|
||||
|
||||
|
||||
async def handle_quality_rate(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
payload = user_data.get("payload", "")
|
||||
|
||||
if FSM.get_state(max_user_id) != BotState.FEATURE_QUALITY_RATE:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_QUALITY_RATE)
|
||||
conv = await get_open_conversation(db, (await get_or_create_user(db, max_user_id)).id)
|
||||
conv_id = conv.id if conv else "—"
|
||||
text = await get_template_rendered(db, "quality_request", conv_id=conv_id)
|
||||
buttons = build_quality_buttons()
|
||||
await max_api.send_message_with_keyboard(max_user_id, text, buttons)
|
||||
return
|
||||
|
||||
if payload.startswith("rate_"):
|
||||
try:
|
||||
rating = int(payload.replace("rate_", ""))
|
||||
except ValueError:
|
||||
return
|
||||
await max_api.send_message(max_user_id, f"Спасибо за оценку: {rating}/5! Ваш отзыв важен для нас.")
|
||||
FSM.transition(max_user_id, "done")
|
||||
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
|
||||
|
||||
FSM.transition(max_user_id, "done")
|
||||
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)
|
||||
@@ -0,0 +1,48 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
|
||||
async def handle_reminders(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
|
||||
if FSM.get_state(max_user_id) != BotState.FEATURE_REMINDERS:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_REMINDERS)
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
phone = user.phone or ""
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from app.models.models import BotConversation, BotTicket, Customer
|
||||
|
||||
open_convs = await db.execute(
|
||||
select(func.count(BotConversation.id))
|
||||
.where(BotConversation.status.in_(["open", "handoff"]))
|
||||
)
|
||||
total_open = open_convs.scalar() or 0
|
||||
|
||||
open_tickets = await db.execute(
|
||||
select(func.count(BotTicket.id))
|
||||
.where(BotTicket.status.in_(["open", "in_progress"]))
|
||||
)
|
||||
total_tickets = open_tickets.scalar() or 0
|
||||
|
||||
my_convs = await db.execute(
|
||||
select(func.count(BotConversation.id))
|
||||
.where(BotConversation.user_id == user.id, BotConversation.status.in_(["open", "handoff"]))
|
||||
)
|
||||
my_open = my_convs.scalar() or 0
|
||||
|
||||
text = (
|
||||
"🔔 **Напоминания**\n\n"
|
||||
f"У вас открыто обращений: {my_open}\n"
|
||||
f"Всего в системе: {total_open} обращений, {total_tickets} заявок\n\n"
|
||||
"Напоминания о плановом ТО настраиваются в портале."
|
||||
)
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
FSM.transition(max_user_id, "done")
|
||||
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
|
||||
@@ -0,0 +1,100 @@
|
||||
from sqlalchemy import select
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
_ANSWERS_KEY = "risk_answers"
|
||||
_STEP_KEY = "risk_step"
|
||||
|
||||
|
||||
async def _load_questions(db):
|
||||
from app.models.models import BotRiskQuestion
|
||||
result = await db.execute(
|
||||
select(BotRiskQuestion)
|
||||
.where(BotRiskQuestion.is_active == True)
|
||||
.order_by(BotRiskQuestion.sort_order)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def handle_risk_score(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
state = FSM.get_state(max_user_id)
|
||||
|
||||
questions = await _load_questions(db)
|
||||
if not questions:
|
||||
await max_api.send_message(max_user_id, "Анкета риск-инжиниринга временно недоступна.")
|
||||
FSM.transition(max_user_id, "cancel")
|
||||
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
|
||||
|
||||
if state != BotState.FEATURE_RISK_SCORE:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_RISK_SCORE)
|
||||
FSM.set_temp(max_user_id, {_STEP_KEY: 0, _ANSWERS_KEY: {}})
|
||||
q = questions[0]
|
||||
await max_api.send_message(
|
||||
max_user_id,
|
||||
f"📊 **Risk Score — анкета**\n\nВопрос 1/{len(questions)}:\n{q.question}\n\nОтветьте «да» или «нет»."
|
||||
)
|
||||
return
|
||||
|
||||
tmp = FSM.get_temp(max_user_id)
|
||||
step = tmp.get(_STEP_KEY, 0)
|
||||
answers = tmp.get(_ANSWERS_KEY, {})
|
||||
|
||||
if step >= len(questions):
|
||||
await _finish_risk_score(max_user_id, answers, questions, db, max_api)
|
||||
return
|
||||
|
||||
if text.lower() in ("да", "yes", "+", "1"):
|
||||
answers[questions[step].question_key] = True
|
||||
elif text.lower() in ("нет", "no", "-", "0"):
|
||||
answers[questions[step].question_key] = False
|
||||
else:
|
||||
await max_api.send_message(max_user_id, "Пожалуйста, ответьте «да» или «нет».")
|
||||
return
|
||||
|
||||
step += 1
|
||||
if step < len(questions):
|
||||
q = questions[step]
|
||||
FSM.set_temp(max_user_id, {_STEP_KEY: step, _ANSWERS_KEY: answers})
|
||||
await max_api.send_message(
|
||||
max_user_id,
|
||||
f"📊 Вопрос {step + 1}/{len(questions)}:\n{q.question}\n\n«да» или «нет»?"
|
||||
)
|
||||
else:
|
||||
await _finish_risk_score(max_user_id, answers, questions, db, max_api)
|
||||
|
||||
|
||||
async def _finish_risk_score(max_user_id, answers, questions, db, max_api):
|
||||
score = 0
|
||||
for q in questions:
|
||||
if answers.get(q.question_key):
|
||||
score += q.weight
|
||||
score = min(score, 100)
|
||||
|
||||
if score < 20:
|
||||
label = "Низкий"
|
||||
rec = "Поддерживайте текущий уровень обслуживания"
|
||||
elif score < 50:
|
||||
label = "Средний"
|
||||
rec = "Рекомендуем провести аудит систем"
|
||||
elif score < 75:
|
||||
label = "Высокий"
|
||||
rec = "Необходимо срочное обслуживание"
|
||||
else:
|
||||
label = "Критический"
|
||||
rec = "Требуется немедленное вмешательство"
|
||||
|
||||
tmpl = await get_template_rendered(db, "risk_result", score=score, label=label, recommendation=rec)
|
||||
await max_api.send_message(max_user_id, tmpl, format="markdown")
|
||||
|
||||
FSM.clear_temp(max_user_id)
|
||||
FSM.transition(max_user_id, "done")
|
||||
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)
|
||||
@@ -0,0 +1,54 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
|
||||
async def handle_sla_status(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
|
||||
if FSM.get_state(max_user_id) != BotState.FEATURE_SLA_STATUS:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_SLA_STATUS)
|
||||
await max_api.send_message(max_user_id,
|
||||
"🔍 **Проверка SLA статуса**\n\n"
|
||||
"Укажите номер или название объекта."
|
||||
)
|
||||
return
|
||||
|
||||
if not text:
|
||||
await max_api.send_message(max_user_id, "Укажите номер или название объекта.")
|
||||
return
|
||||
|
||||
from app.integrations.portal_db import get_object_by_name_or_id, get_sla_for_object, get_last_incident_for_object, get_active_tasks_for_object
|
||||
obj = await get_object_by_name_or_id(db, text)
|
||||
if not obj:
|
||||
await max_api.send_message(max_user_id, "Объект не найден.")
|
||||
FSM.transition(max_user_id, "done")
|
||||
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
|
||||
|
||||
sla = await get_sla_for_object(db, obj.id)
|
||||
if not sla:
|
||||
text = await get_template_rendered(db, "sla_not_found")
|
||||
await max_api.send_message(max_user_id, text)
|
||||
else:
|
||||
level_map = {"start": "Базовый", "business": "Оптимальный", "enterprise": "Максимальный"}
|
||||
last_incident = await get_last_incident_for_object(db, obj.id)
|
||||
open_tasks = await get_active_tasks_for_object(db, obj.id)
|
||||
last_visit = last_incident.created_at.strftime("%d.%m.%Y") if last_incident else "—"
|
||||
tasks_count = len(open_tasks)
|
||||
text = await get_template_rendered(db, "sla_found",
|
||||
object_name=obj.name,
|
||||
service_level=level_map.get(sla.service_level, sla.service_level),
|
||||
status=sla.status,
|
||||
price=sla.sla_price_monthly or 0)
|
||||
text += f"\n📅 Последний визит: {last_visit}\n📋 Открытых задач: {tasks_count}"
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
|
||||
FSM.transition(max_user_id, "done")
|
||||
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)
|
||||
@@ -0,0 +1,155 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
|
||||
async def handle_sla_ticket(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
|
||||
if FSM.get_state(max_user_id) != BotState.SLA_TICKET_CREATE:
|
||||
FSM.set_state(max_user_id, BotState.SLA_TICKET_CREATE)
|
||||
await max_api.send_message(max_user_id, "🔧 **Заявка по SLA**\n\nПроверяю ваш SLA контракт...")
|
||||
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
phone = user.phone or ""
|
||||
if not phone:
|
||||
await max_api.send_message(max_user_id, "Не могу найти ваш номер телефона. Укажите его, пожалуйста.")
|
||||
return
|
||||
|
||||
from sqlalchemy import select
|
||||
from app.models.models import Customer, Object, SLAContract
|
||||
result = await db.execute(
|
||||
select(Customer).where(Customer.contact_phone.ilike(f"%{phone[-10:]}%")).limit(1)
|
||||
)
|
||||
customer = result.scalar_one_or_none()
|
||||
if not customer:
|
||||
await _send_no_sla(bot, user_data, db, max_api)
|
||||
return
|
||||
|
||||
result = await db.execute(
|
||||
select(Object).where(Object.customer_id == customer.id, Object.status == "active").limit(1)
|
||||
)
|
||||
obj = result.scalar_one_or_none()
|
||||
if not obj:
|
||||
await _send_no_sla(bot, user_data, db, max_api)
|
||||
return
|
||||
|
||||
result = await db.execute(
|
||||
select(SLAContract).where(
|
||||
SLAContract.object_id == obj.id,
|
||||
SLAContract.status == "active"
|
||||
).order_by(SLAContract.created_at.desc()).limit(1)
|
||||
)
|
||||
sla = result.scalar_one_or_none()
|
||||
if not sla:
|
||||
await _send_no_sla(bot, user_data, db, max_api)
|
||||
return
|
||||
|
||||
level_map = {"start": "Базовый", "business": "Оптимальный", "enterprise": "Максимальный"}
|
||||
text = await get_template_rendered(db, "sla_ticket_found",
|
||||
object_name=obj.name,
|
||||
service_level=level_map.get(sla.service_level, sla.service_level))
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
return
|
||||
|
||||
if not text:
|
||||
await max_api.send_message(max_user_id, "Опишите неисправность. Вы можете прикрепить фото или документ.")
|
||||
return
|
||||
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from app.models.models import Customer, Object, SLAContract, BotTicket
|
||||
result = await db.execute(
|
||||
select(Customer).where(Customer.contact_phone.ilike(f"%{user.phone[-10:]}%")).limit(1)
|
||||
)
|
||||
customer = result.scalar_one_or_none()
|
||||
obj = None
|
||||
sla = None
|
||||
if customer:
|
||||
result = await db.execute(
|
||||
select(Object).where(Object.customer_id == customer.id, Object.status == "active").limit(1)
|
||||
)
|
||||
obj = result.scalar_one_or_none()
|
||||
if obj:
|
||||
result = await db.execute(
|
||||
select(SLAContract).where(
|
||||
SLAContract.object_id == obj.id, SLAContract.status == "active"
|
||||
).order_by(SLAContract.created_at.desc()).limit(1)
|
||||
)
|
||||
sla = result.scalar_one_or_none()
|
||||
|
||||
priority_map = {"start": "low", "business": "normal", "enterprise": "high"}
|
||||
response_map = {"start": "4 часа", "business": "2 часа", "enterprise": "30 минут"}
|
||||
sla_level = sla.service_level if sla else "start"
|
||||
priority = priority_map.get(sla_level, "normal")
|
||||
|
||||
result = await db.execute(select(func.count(BotTicket.id)))
|
||||
ticket_num = f"T-{result.scalar() + 1:04d}"
|
||||
|
||||
from app.models.models import BotTicket, BotTicketMessage, BotTicketStatus
|
||||
ticket = BotTicket(
|
||||
ticket_number=ticket_num,
|
||||
user_id=user.id,
|
||||
object_id=obj.id if obj else None,
|
||||
customer_id=customer.id if customer else None,
|
||||
title=text[:100],
|
||||
description=text,
|
||||
priority=priority,
|
||||
status="open",
|
||||
created_by="bot",
|
||||
sla_contract_id=sla.id if sla else None,
|
||||
has_attachment=user_data.get("has_attachment", False),
|
||||
attachment_type=user_data.get("attachment_type", ""),
|
||||
attachment_path=user_data.get("attachment_path", ""),
|
||||
)
|
||||
db.add(ticket)
|
||||
await db.flush()
|
||||
|
||||
db.add(BotTicketMessage(ticket_id=ticket.id, direction="in", text=text, sender="client"))
|
||||
db.add(BotTicketStatus(ticket_id=ticket.id, old_status="", new_status="open", changed_by="bot"))
|
||||
await db.flush()
|
||||
|
||||
has_attachment = user_data.get("has_attachment", False)
|
||||
attachment_type = user_data.get("attachment_type", "")
|
||||
attachment_path = user_data.get("attachment_path", "")
|
||||
if has_attachment:
|
||||
ticket.has_attachment = True
|
||||
ticket.attachment_type = attachment_type
|
||||
ticket.attachment_path = attachment_path
|
||||
await db.flush()
|
||||
|
||||
text = await get_template_rendered(db, "sla_ticket_created",
|
||||
ticket_number=ticket_num,
|
||||
priority=priority,
|
||||
response_time=response_map.get(sla_level, "2 часа"))
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
|
||||
from app.max_notifications import notify_ticket_created as nt
|
||||
await nt(db, ticket, max_api)
|
||||
|
||||
FSM.transition(max_user_id, "done")
|
||||
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)
|
||||
|
||||
|
||||
async def _send_no_sla(bot, user_data, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
await SettingsCache.refresh(db)
|
||||
phone_1 = SettingsCache.get("phone_1")
|
||||
phone_2 = SettingsCache.get("phone_2")
|
||||
email = SettingsCache.get("support_email")
|
||||
text = await get_template_rendered(db, "sla_ticket_not_found",
|
||||
phone_1=phone_1, phone_2=phone_2,
|
||||
support_email=email)
|
||||
from app.keyboards.inline import build_phone_email_buttons
|
||||
subject = SettingsCache.get("email_subject")
|
||||
buttons = build_phone_email_buttons(phone_1, phone_2, email, subject)
|
||||
await max_api.send_message_with_keyboard(max_user_id, text, buttons, format="markdown")
|
||||
FSM.transition(max_user_id, "done")
|
||||
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)
|
||||
@@ -0,0 +1,33 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_open_conversation, get_template_rendered
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
from app.fsm import BotState, FSM
|
||||
|
||||
|
||||
async def handle_start(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
first_name = user_data.get("first_name", "")
|
||||
last_name = user_data.get("last_name", "")
|
||||
username = user_data.get("username", "")
|
||||
|
||||
user = await get_or_create_user(db, max_user_id, first_name, last_name, username)
|
||||
await db.flush()
|
||||
|
||||
await SettingsCache.refresh(db)
|
||||
|
||||
open_conv = await get_open_conversation(db, user.id)
|
||||
name = SettingsCache.get_assistant_name()
|
||||
|
||||
if open_conv:
|
||||
text = await get_template_rendered(db, "greeting_returning",
|
||||
first_name=user.first_name or first_name,
|
||||
conv_id=open_conv.id)
|
||||
buttons = build_main_menu_buttons()
|
||||
await max_api.send_message_with_keyboard(max_user_id, text, buttons, format="markdown")
|
||||
FSM.set_state(max_user_id, BotState.IDLE)
|
||||
return
|
||||
|
||||
text = await get_template_rendered(db, "greeting", name=name)
|
||||
buttons = build_main_menu_buttons()
|
||||
await max_api.send_message_with_keyboard(max_user_id, text, buttons, format="markdown")
|
||||
FSM.set_state(max_user_id, BotState.IDLE)
|
||||
@@ -0,0 +1,32 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_open_conversation, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
|
||||
|
||||
async def handle_handoff_request(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
|
||||
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
|
||||
|
||||
text = await get_template_rendered(db, "handoff")
|
||||
await max_api.send_message(max_user_id, text)
|
||||
|
||||
conv = await get_open_conversation(db, user.id)
|
||||
if conv:
|
||||
conv.status = "handoff"
|
||||
conv.priority = "high"
|
||||
await db.flush()
|
||||
|
||||
from app.integrations import _1c_unf as ic_integration
|
||||
await ic_integration.send_to_1c(db, conv)
|
||||
|
||||
FSM.set_state(max_user_id, BotState.HANDOFF_REQUEST)
|
||||
|
||||
|
||||
async def handle_inquiry_fallback(bot, user_data, db, max_api):
|
||||
from app.handlers.inquiry import handle_inquiry
|
||||
await handle_inquiry(bot, user_data, db, max_api)
|
||||
@@ -0,0 +1,122 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import (
|
||||
get_or_create_user, get_open_conversation, create_conversation,
|
||||
add_message, auto_categorize_text, get_template_rendered
|
||||
)
|
||||
from app.integrations.yandex_gpt import categorize_with_gpt
|
||||
from app.keyboards.inline import build_category_buttons
|
||||
from app.fsm import BotState, FSM
|
||||
|
||||
|
||||
async def handle_inquiry(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
|
||||
if not text:
|
||||
await max_api.send_message(max_user_id, "Пожалуйста, опишите ваш вопрос.")
|
||||
return
|
||||
|
||||
conv = await get_open_conversation(db, user.id)
|
||||
if not conv:
|
||||
conv = await create_conversation(db, user.id)
|
||||
|
||||
conv.inquiry_text = text
|
||||
|
||||
has_attachment = user_data.get("has_attachment", False)
|
||||
attachment_type = user_data.get("attachment_type", "")
|
||||
attachment_path_str = user_data.get("attachment_path", "")
|
||||
if has_attachment:
|
||||
conv.has_attachment = True
|
||||
conv.attachment_type = attachment_type
|
||||
conv.attachment_path = attachment_path_str
|
||||
|
||||
await add_message(db, conv.id, "in", text, has_attachment, attachment_type, attachment_path_str)
|
||||
|
||||
if has_attachment and attachment_path_str and max_api:
|
||||
try:
|
||||
file_url = await max_api.get_file_url(attachment_path_str)
|
||||
if file_url:
|
||||
import os
|
||||
import aiohttp
|
||||
from app.config import get_settings
|
||||
from app.models.models import FileSyncQueue
|
||||
settings = get_settings()
|
||||
local_dir = settings.UPLOADS_DIR
|
||||
os.makedirs(local_dir, exist_ok=True)
|
||||
local_filename = f"conv_{conv.id}_{int(datetime.now().timestamp())}_{attachment_type}"
|
||||
local_path = os.path.join(local_dir, local_filename)
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(file_url) as resp:
|
||||
if resp.status == 200:
|
||||
with open(local_path, "wb") as f:
|
||||
f.write(await resp.read())
|
||||
sync_entry = FileSyncQueue(
|
||||
local_path=local_path,
|
||||
remote_path=f"/AegisOne_Service/bot/{local_filename}",
|
||||
entity_type="conversation",
|
||||
entity_id=conv.id,
|
||||
status="pending",
|
||||
)
|
||||
db.add(sync_entry)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
gpt_key = SettingsCache.get("yandex_gpt_key")
|
||||
if gpt_key:
|
||||
cat_name = await categorize_with_gpt(text)
|
||||
if cat_name:
|
||||
from sqlalchemy import select
|
||||
from app.models.models import BotCategory
|
||||
result = await db.execute(
|
||||
select(BotCategory).where(
|
||||
BotCategory.name.ilike(f"%{cat_name}%"),
|
||||
BotCategory.active == True
|
||||
).limit(1)
|
||||
)
|
||||
cat = result.scalar_one_or_none()
|
||||
if cat:
|
||||
conv.inquiry_type = cat.id
|
||||
await db.flush()
|
||||
confirm_text = await get_template_rendered(db, "auto_categorize_confirm", category=cat.name)
|
||||
from app.keyboards.inline import build_category_buttons
|
||||
buttons = build_category_buttons()
|
||||
await max_api.send_message_with_keyboard(max_user_id, confirm_text, buttons)
|
||||
FSM.set_state(max_user_id, BotState.CATEGORY_SELECT)
|
||||
return
|
||||
else:
|
||||
cat = await auto_categorize_text(db, text)
|
||||
if cat:
|
||||
conv.inquiry_type = cat.id
|
||||
await db.flush()
|
||||
|
||||
await db.flush()
|
||||
|
||||
ooh = not SettingsCache.is_work_hours()
|
||||
if ooh:
|
||||
start = SettingsCache.get_int("work_hours_start", 9)
|
||||
end = SettingsCache.get_int("work_hours_end", 18)
|
||||
ooh_text = await get_template_rendered(db, "out_of_hours",
|
||||
work_hours_start=start, work_hours_end=end)
|
||||
await max_api.send_message(max_user_id, ooh_text)
|
||||
|
||||
await _confirm_inquiry(bot, user_data, db, max_api, conv, ooh)
|
||||
|
||||
|
||||
async def _confirm_inquiry(bot, user_data, db, max_api, conv, is_ooh: bool):
|
||||
max_user_id = user_data["user_id"]
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
|
||||
if is_ooh:
|
||||
text = await get_template_rendered(db, "confirmation_ooh",
|
||||
first_name=user.first_name, conv_id=conv.id)
|
||||
else:
|
||||
text = await get_template_rendered(db, "confirmation",
|
||||
first_name=user.first_name, conv_id=conv.id)
|
||||
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
|
||||
from app.integrations import _1c_unf as ic_integration
|
||||
await ic_integration.send_to_1c(db, conv)
|
||||
|
||||
FSM.set_state(max_user_id, BotState.CONFIRMATION)
|
||||
@@ -0,0 +1,103 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
from app.fsm import BotState, FSM
|
||||
|
||||
|
||||
async def handle_main_menu(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
payload = user_data.get("payload", "")
|
||||
|
||||
if payload == "new_inquiry":
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
if user.consent_given:
|
||||
from app.handlers.contact import handle_contact_provided
|
||||
await handle_contact_provided(bot, user_data, db, max_api)
|
||||
else:
|
||||
from app.handlers.consent import handle_consent_request
|
||||
await handle_consent_request(bot, user_data, db, max_api)
|
||||
return
|
||||
|
||||
if payload == "handoff":
|
||||
from app.handlers.handoff import handle_handoff_request
|
||||
await handle_handoff_request(bot, user_data, db, max_api)
|
||||
return
|
||||
|
||||
if payload == "kb_search":
|
||||
FSM.set_state(max_user_id, BotState.KB_SEARCH)
|
||||
await max_api.send_message(max_user_id, "Задайте ваш вопрос, и я постараюсь найти ответ.")
|
||||
return
|
||||
|
||||
if payload.startswith("feature_"):
|
||||
feature_key = payload.replace("feature_", "")
|
||||
if SettingsCache.is_feature_enabled(feature_key):
|
||||
await _handle_feature(bot, user_data, db, max_api, feature_key)
|
||||
else:
|
||||
await max_api.send_message(max_user_id, "Этот функционал временно недоступен.")
|
||||
return
|
||||
|
||||
if payload.startswith("cat_"):
|
||||
await _handle_category_select(bot, user_data, db, max_api, payload)
|
||||
return
|
||||
|
||||
if payload == "cancel":
|
||||
FSM.reset(max_user_id)
|
||||
text = await get_template_rendered(db, "main_menu")
|
||||
buttons = build_main_menu_buttons()
|
||||
await max_api.send_message_with_keyboard(max_user_id, text, buttons)
|
||||
return
|
||||
|
||||
FSM.reset(max_user_id)
|
||||
text = await get_template_rendered(db, "main_menu")
|
||||
buttons = build_main_menu_buttons()
|
||||
await max_api.send_message_with_keyboard(max_user_id, text, buttons)
|
||||
|
||||
|
||||
async def _handle_feature(bot, user_data, db, max_api, feature_key: str):
|
||||
feature_handlers = {
|
||||
"risk_score": ("app.handlers.features.risk_score", "handle_risk_score"),
|
||||
"sla_status": ("app.handlers.features.sla_status", "handle_sla_status"),
|
||||
"photo_report": ("app.handlers.features.photo_report", "handle_photo_report"),
|
||||
"appointment": ("app.handlers.features.appointment", "handle_appointment"),
|
||||
"quality_rate": ("app.handlers.features.quality_rate", "handle_quality_rate"),
|
||||
"commercial_offer": ("app.handlers.features.commercial", "handle_commercial"),
|
||||
"my_objects": ("app.handlers.features.my_objects", "handle_my_objects"),
|
||||
"reminders": ("app.handlers.features.reminders", "handle_reminders"),
|
||||
"sla_ticket": ("app.handlers.features.sla_ticket", "handle_sla_ticket"),
|
||||
"notifications": ("app.handlers.features.notifications", "handle_notifications"),
|
||||
"broadcasts": ("app.handlers.features.broadcasts", "handle_broadcast_send"),
|
||||
}
|
||||
handler = feature_handlers.get(feature_key)
|
||||
if handler:
|
||||
import importlib
|
||||
mod = importlib.import_module(handler[0])
|
||||
func = getattr(mod, handler[1])
|
||||
await func(bot, user_data, db, max_api)
|
||||
else:
|
||||
await max_api.send_message(max_user_id, "Функция в разработке.")
|
||||
|
||||
|
||||
async def _handle_category_select(bot, user_data, db, max_api, payload: str):
|
||||
max_user_id = user_data["user_id"]
|
||||
try:
|
||||
cat_id = int(payload.replace("cat_", ""))
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
from sqlalchemy import select
|
||||
from app.models.models import BotCategory
|
||||
result = await db.execute(select(BotCategory).where(BotCategory.id == cat_id))
|
||||
cat = result.scalar_one_or_none()
|
||||
if not cat:
|
||||
return
|
||||
|
||||
from app.conversation import get_open_conversation, get_or_create_user
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
conv = await get_open_conversation(db, user.id)
|
||||
if conv:
|
||||
conv.inquiry_type = cat.id
|
||||
await db.flush()
|
||||
|
||||
ooh = not SettingsCache.is_work_hours()
|
||||
from app.handlers.inquiry import _confirm_inquiry
|
||||
await _confirm_inquiry(bot, user_data, db, max_api, conv, ooh)
|
||||
@@ -0,0 +1,43 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_template_rendered
|
||||
from app.keyboards.inline import build_cancel_button
|
||||
from app.fsm import BotState, FSM
|
||||
from app.handlers.main_menu import handle_main_menu
|
||||
|
||||
|
||||
async def handle_out_of_hours(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
await SettingsCache.refresh(db)
|
||||
|
||||
start = SettingsCache.get_int("work_hours_start", 9)
|
||||
end = SettingsCache.get_int("work_hours_end", 18)
|
||||
hours_str = SettingsCache.get_work_hours_str()
|
||||
|
||||
text = (
|
||||
f"Сейчас мы не работаем. Наше рабочее время: Пн-Пт {hours_str} (МСК).\n\n"
|
||||
f"Оставьте сообщение — мы ответим как можно быстрее."
|
||||
)
|
||||
buttons = [
|
||||
[{"type": "message", "text": "✉️ Оставить сообщение", "payload": "leave_message"}],
|
||||
[{"type": "message", "text": "📞 Заказать звонок", "payload": "callback"}],
|
||||
]
|
||||
await max_api.send_message_with_keyboard(max_user_id, text, buttons)
|
||||
FSM.set_state(max_user_id, BotState.OUT_OF_HOURS)
|
||||
|
||||
|
||||
async def handle_out_of_hours_callback(bot, user_data: dict, db, max_api, payload: str):
|
||||
max_user_id = user_data["user_id"]
|
||||
|
||||
if payload == "leave_message":
|
||||
FSM.set_state(max_user_id, BotState.INQUIRY_REQUEST)
|
||||
from app.handlers.inquiry import handle_inquiry
|
||||
await handle_inquiry(bot, user_data, db, max_api)
|
||||
return
|
||||
|
||||
if payload == "callback":
|
||||
from app.handlers.consent import handle_consent_request
|
||||
FSM.set_state(max_user_id, BotState.NAME_REQUEST)
|
||||
await max_api.send_message(max_user_id, "Как к вам обращаться?")
|
||||
return
|
||||
|
||||
await handle_main_menu(bot, user_data, db, max_api)
|
||||
Reference in New Issue
Block a user