Files
bot_vk_ikp_prodagi/bot.py
T
a.gruzdev c8d769ee1a main
Начальный проект чат бота (Для тестирования и исправления)
2026-06-15 04:56:06 +00:00

1037 lines
55 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import re
import logging
from typing import Dict, Any, Optional
# Нам понадобятся библиотеки vk_api.
try:
import vk_api
from vk_api.bot_longpoll import VkBotLongPoll, VkBotEventType
from vk_api.keyboard import VkKeyboard, VkKeyboardColor
from vk_api.utils import get_random_id
except ImportError:
vk_api = None
from database import DatabaseManager
import config
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - [%(levelname)s] - %(name)s - %(message)s"
)
logger = logging.getLogger("VK_Bot_IKP")
# Инициализация базы данных сессий
db = DatabaseManager()
# Регулярное выражение для базовой валидации номера телефона
PHONE_REGEX = re.compile(r"^\+?[78]\d{10}$")
# ==========================================
# КЛАВИАТУРЫ ДЛЯ ИНТЕРФЕЙСА (UX/UI ВКонтакте)
# ==========================================
def get_main_keyboard() -> str:
"""Главное меню бота."""
keyboard = VkKeyboard(one_time=False)
# Кнопка начать опрос (зеленая)
keyboard.add_button("Заполнить заявку 📝", color=VkKeyboardColor.PRIMARY)
keyboard.add_line()
# Кнопки FAQ и оператора
keyboard.add_button("Частые вопросы ❓", color=VkKeyboardColor.SECONDARY)
keyboard.add_button("Позвать оператора 🧑‍💼", color=VkKeyboardColor.NEGATIVE)
return keyboard.get_keyboard()
def get_faq_keyboard() -> str:
"""Клавиатура раздела Частых Вопросов (FAQ)."""
keyboard = VkKeyboard(one_time=False)
keyboard.add_button("Цены 💰", color=VkKeyboardColor.SECONDARY)
keyboard.add_button("Услуги ⚙️", color=VkKeyboardColor.SECONDARY)
keyboard.add_line()
keyboard.add_button("Контакты 📞", color=VkKeyboardColor.SECONDARY)
keyboard.add_button("О компании 🏢", color=VkKeyboardColor.SECONDARY)
keyboard.add_line()
keyboard.add_button("Назад в меню ↩️", color=VkKeyboardColor.PRIMARY)
return keyboard.get_keyboard()
def get_cancel_keyboard() -> str:
"""Клавиатура отмены заполнения формы."""
keyboard = VkKeyboard(one_time=False)
keyboard.add_button("Отменить заполнение ❌", color=VkKeyboardColor.NEGATIVE)
return keyboard.get_keyboard()
def get_operator_keyboard() -> str:
"""Клавиатура при активном диалоге с оператором."""
keyboard = VkKeyboard(one_time=False)
keyboard.add_button("Завершить диалог 🚪", color=VkKeyboardColor.NEGATIVE)
return keyboard.get_keyboard()
def get_binary_keyboard(show_skip: bool = False) -> str:
"""Клавиатура с кнопками Согласен / Не согласен."""
keyboard = VkKeyboard(one_time=False)
keyboard.add_button("Согласен(а) и подтверждаю ✅", color=VkKeyboardColor.PRIMARY)
keyboard.add_button("Не согласен(а) ❌", color=VkKeyboardColor.NEGATIVE)
if show_skip:
keyboard.add_line()
keyboard.add_button("Пропустить ➡️", color=VkKeyboardColor.SECONDARY)
keyboard.add_line()
keyboard.add_button("Отменить заполнение ❌", color=VkKeyboardColor.NEGATIVE)
return keyboard.get_keyboard()
def get_services_keyboard() -> str:
"""Клавиатура со списком доступных услуг."""
keyboard = VkKeyboard(one_time=False)
first = True
for key, svc in config.SERVICES.items():
if not first:
keyboard.add_line()
keyboard.add_button(svc["name"], color=VkKeyboardColor.SECONDARY)
first = False
keyboard.add_line()
keyboard.add_button("Отменить заполнение ❌", color=VkKeyboardColor.NEGATIVE)
return keyboard.get_keyboard()
def get_confirm_service_keyboard() -> str:
"""Клавиатура для подтверждения выбранной услуги."""
keyboard = VkKeyboard(one_time=False)
keyboard.add_button("Да, подтверждаю ✅", color=VkKeyboardColor.PRIMARY)
keyboard.add_button("Нет, выбрать другую 🔄", color=VkKeyboardColor.NEGATIVE)
keyboard.add_line()
keyboard.add_button("Отменить заполнение ❌", color=VkKeyboardColor.NEGATIVE)
return keyboard.get_keyboard()
def get_review_keyboard() -> str:
"""Клавиатура для финальной проверки данных пользователем."""
keyboard = VkKeyboard(one_time=False)
keyboard.add_button("Да, всё верно! ✅", color=VkKeyboardColor.PRIMARY)
keyboard.add_button("Изменить данные 🔄", color=VkKeyboardColor.SECONDARY)
keyboard.add_line()
keyboard.add_button("Отменить заполнение ❌", color=VkKeyboardColor.NEGATIVE)
return keyboard.get_keyboard()
def get_edit_menu_keyboard() -> str:
"""Клавиатура меню изменения полей."""
keyboard = VkKeyboard(one_time=False)
keyboard.add_button("Изменить Имя 👤", color=VkKeyboardColor.SECONDARY)
keyboard.add_button("Изменить Телефон 📞", color=VkKeyboardColor.SECONDARY)
keyboard.add_line()
keyboard.add_button("Изменить Услугу 🎮", color=VkKeyboardColor.SECONDARY)
keyboard.add_button("Назад к проверке ↩️", color=VkKeyboardColor.PRIMARY)
return keyboard.get_keyboard()
# ==========================================
# КЛАСС БОТА И ЛОГИКА ОБРАБОТКИ
# ==========================================
class VKBotApplication:
def __init__(self):
self.vk_session = None
self.vk = None
self.longpoll = None
# Инициализируем динамические услуги из базы данных
config.SERVICES = db.get_services()
if vk_api and config.VK_TOKEN != "your_vk_community_token_here":
try:
self.vk_session = vk_api.VkApi(token=config.VK_TOKEN)
self.vk = self.vk_session.get_api()
self.longpoll = VkBotLongPoll(self.vk_session, config.GROUP_ID)
logger.info("Успешное подключение к VK API!")
except Exception as e:
logger.error(f"Ошибка авторизации в VK API: {e}")
def send_message(self, peer_id: int, message: str, keyboard: Optional[str] = None, attachment: Optional[str] = None):
"""Отправка сообщения пользователю с сохранением в лог."""
db.log_message(peer_id, "bot", message)
if self.vk:
try:
params = {
"peer_id": peer_id,
"message": message,
"random_id": get_random_id()
}
if keyboard:
params["keyboard"] = keyboard
if attachment:
params["attachment"] = attachment
self.vk.messages.send(**params)
except Exception as e:
logger.error(f"Не удалось отправить сообщение пользователю {peer_id}: {e}")
else:
attachment_str = f", Attachment: {attachment}" if attachment else ""
logger.info(f"[MOCK SEND] to {peer_id}: {message} (Keyboard: {'Да' if keyboard else 'Нет'}{attachment_str})")
def handle_message(self, user_id: int, text: str):
"""Маршрутизатор входящих сообщений на основе состояний пользователя."""
text_clean = text.strip()
text_lower = text_clean.lower()
# Логируем входящее сообщение от пользователя
db.log_message(user_id, "user", text_clean)
# Получаем текущее состояние и заполненные данные
user_data = db.get_user_data(user_id)
state = user_data["state"]
# Глобальная обработка админ-команд (доступна только пользователям из ADMIN_IDS)
if hasattr(config, "ADMIN_IDS") and user_id in config.ADMIN_IDS:
if text_lower in ["админка", "/admin", "услуги"]:
self._send_admin_menu(user_id)
return
elif text_lower.startswith("ред "):
self._handle_admin_edit(user_id, text_clean)
return
elif text_lower.startswith("доб "):
self._handle_admin_add(user_id, text_clean)
return
elif text_lower.startswith("удал "):
self._handle_admin_delete(user_id, text_clean)
return
logger.info(f"User {user_id} in state {state} sent: {text_clean}")
# Глобальная обработка отмены формы в любых состояниях заполнения опроса
if state.startswith("FORM_") and text_lower in ["отменить заполнение ❌", "отмена", "отменить"]:
db.reset_form(user_id)
self.send_message(
user_id,
"Заполнение заявки отменено. Мы вернулись в главное меню. Чем я могу помочь?",
keyboard=get_main_keyboard()
)
return
# Маршрутизация по состояниям (FSM)
if state == "FREE_CHAT":
self._handle_free_chat(user_id, text_clean, text_lower)
elif state == "FORM_NAME":
self._handle_form_name(user_id, text_clean)
elif state == "FORM_PHONE":
self._handle_form_phone(user_id, text_clean)
elif state == "FORM_QUESTION":
self._handle_form_question(user_id, text_clean, user_data)
elif state == "FORM_CONFIRM_QUESTION":
self._handle_form_confirm_question(user_id, text_clean, user_data)
elif state == "FORM_REVIEW":
self._handle_form_review(user_id, text_clean, user_data)
elif state == "FORM_EDIT_MENU":
self._handle_form_edit_menu(user_id, text_clean, user_data)
elif state == "FORM_EDIT_NAME":
self._handle_form_edit_name(user_id, text_clean)
elif state == "FORM_EDIT_PHONE":
self._handle_form_edit_phone(user_id, text_clean)
elif state == "FORM_EDIT_QUESTION":
self._handle_form_edit_question(user_id, text_clean)
elif state == "FORM_CONSENT_1":
self._handle_form_consent_1(user_id, text_clean, text_lower)
elif state == "FORM_CONSENT_2":
self._handle_form_consent_2(user_id, text_clean, text_lower)
elif state == "FORM_CONSENT_3":
self._handle_form_consent_3(user_id, text_clean, text_lower, user_data)
elif state == "OPERATOR_CHAT":
self._handle_operator_chat(user_id, text_clean, text_lower)
else:
# Предотвращение неопределенного состояния
db.update_user_state(user_id, "FREE_CHAT")
self.send_message(
user_id,
"Произошла непредвиденная ошибка в системе. Возвращаю вас в главное меню.",
keyboard=get_main_keyboard()
)
# ------------------------------------------
# ОБРАБОТЧИКИ СОСТОЯНИЙ (FSM)
# ------------------------------------------
def _handle_free_chat(self, user_id: int, text: str, text_lower: str):
"""Логика Свободного Чата (Главное меню, FAQ, переключение на форму)."""
# 1. Запуск формы опроса
if text_lower in ["заполнить заявку 📝", "заявка", "начать опрос", "опрос"]:
db.update_user_state(user_id, "FORM_NAME")
self.send_message(
user_id,
"📝 Начинаем заполнение заявки!\n\n"
"Шаг 1 из 3: Как к вам обращаться? Пожалуйста, введите ваше имя.",
keyboard=get_cancel_keyboard()
)
return
# 2. Переход к списку частых вопросов (FAQ)
if text_lower in ["частые вопросы ❓", "faq", "вопросы", "помощь"]:
self.send_message(
user_id,
"❓ Выберите тему, которая вас интересует, на клавиатуре ниже:",
keyboard=get_faq_keyboard()
)
return
# 3. Возврат в главное меню
if text_lower in ["назад в меню ↩️", "меню", "назад"]:
self.send_message(
user_id,
"🏠 Вы вернулись в главное меню. Выберите интересующее действие:",
keyboard=get_main_keyboard()
)
return
# 4. Вызов оператора
if text_lower in ["позвать оператора 🧑‍💼", "позвать оператора", "оператор", "менеджер", "человек", "позови оператора", "связь с оператором"]:
self._handoff_to_operator(user_id)
return
# 5. Обработка конкретных ответов FAQ
for key, answer in config.FAQ_ANSWERS.items():
if key in text_lower:
self.send_message(user_id, answer, keyboard=get_faq_keyboard())
return
# 6. Приветственные сообщения
if text_lower in ["начать", "привет", "здравствуйте", "ку", "hi", "hello"]:
self.send_message(
user_id,
"👋 Здравствуйте! Я умный чат-бот школы киберспорта 'Cyber Pro'.\n\n"
"Я могу помочь вам быстро оформить заявку на наши услуги или ответить на частые вопросы. "
"Выберите действие на кнопках ниже или просто напишите ваш вопрос!",
keyboard=get_main_keyboard()
)
return
# 7. Fallback (Бот не понял, но пытается помочь, предлагая клавиатуру)
self.send_message(
user_id,
"🤔 Я пока только учусь и не смог полностью распознать ваш запрос.\n\n"
"Пожалуйста, воспользуйтесь кнопками меню ниже или напишите 'оператор', чтобы я пригласил специалиста.",
keyboard=get_main_keyboard()
)
def _handle_form_name(self, user_id: int, text: str):
"""Сохранение имени и переход к номеру телефона."""
# Валидация имени: длина от 2 до 50 символов
if len(text) < 2 or len(text) > 50:
self.send_message(
user_id,
"⚠️ Имя выглядит слишком коротким или длинным. Пожалуйста, введите корректное имя.",
keyboard=get_cancel_keyboard()
)
return
# Сохраняем имя в БД
db.update_user_field(user_id, "name", text)
db.update_user_state(user_id, "FORM_PHONE")
self.send_message(
user_id,
f"👍 Приятно познакомиться, {text}!\n\n"
"Шаг 2 из 3: Введите ваш контактный номер телефона (например, +79991234567) для связи с менеджером.",
keyboard=get_cancel_keyboard()
)
def _handle_form_phone(self, user_id: int, text: str):
"""Сохранение телефона и переход к выбору услуги."""
# Очищаем телефон от пробелов, скобок и дефисов для валидации
phone_cleaned = re.sub(r"[\s\-\(\)]", "", text)
# Если телефон начинается с 8, заменяем на +7 для стандартизации
if phone_cleaned.startswith("8") and len(phone_cleaned) == 11:
phone_cleaned = "+7" + phone_cleaned[1:]
elif phone_cleaned.startswith("7") and len(phone_cleaned) == 11:
phone_cleaned = "+" + phone_cleaned
# Валидация регулярным выражением
if not PHONE_REGEX.match(phone_cleaned):
self.send_message(
user_id,
"⚠️ Некорректный формат телефона. Пожалуйста, отправьте номер в формате +79991234567.",
keyboard=get_cancel_keyboard()
)
return
# Сохраняем телефон в БД
db.update_user_field(user_id, "phone", phone_cleaned)
db.update_user_state(user_id, "FORM_QUESTION")
# Формируем список услуг с ценами
services_text = (
"✅ Телефон принят!\n\n"
"Шаг 3 из 3: Какая услуга вас интересует?\n\n"
"📋 Доступные услуги и расценки:\n\n"
)
for key, svc in config.SERVICES.items():
services_text += f"{key}️⃣ **{svc['name']}**\n"
services_text += f" 💰 Стоимость: {svc['price']}\n"
services_text += f" 📝 {svc['description']}\n\n"
services_text += "Пожалуйста, выберите нужную услугу, нажав на кнопку на клавиатуре или отправив её номер/название."
self.send_message(
user_id,
services_text,
keyboard=get_services_keyboard()
)
def _handle_form_question(self, user_id: int, text: str, user_data: Dict[str, Any]):
"""Обработка выбора услуги с прямым переходом к проверке данных."""
selected_service = None
text_lower = text.lower().strip()
# 1. Прямое сопоставление синонимов дисциплин для надежности
synonyms = {
"1": ["дота", "dota", "дотку", "дотка", "dota2"],
"2": ["кс", "cs", "ксго", "кс2", "csgo", "cs2", "контра", "кондра", "counter"]
}
for key_opt, words in synonyms.items():
if any(w in text_lower for w in words):
selected_service = config.SERVICES.get(key_opt)
break
# 2. Поиск по названию или ключу (если синонимы не сработали)
if not selected_service:
for key, svc in config.SERVICES.items():
name_clean = svc["name"].lower().strip()
# Убираем эмодзи и спецсимволы для гибкого сравнения
name_no_emoji = re.sub(r"[^\w\s\d]", "", name_clean).strip()
text_no_emoji = re.sub(r"[^\w\s\d]", "", text_lower).strip()
if (text_lower == key or
text_lower == f"{key}." or
text_lower == f"{key}️⃣" or
name_clean in text_lower or
text_lower in name_clean or
(name_no_emoji and text_no_emoji and (text_no_emoji in name_no_emoji or name_no_emoji in text_no_emoji))):
selected_service = svc
break
if selected_service:
# Сохраняем временный выбор в поле question
db.update_user_field(user_id, "question", f"{selected_service['name']} ({selected_service['price']})")
db.update_user_state(user_id, "FORM_REVIEW")
updated_data = db.get_user_data(user_id)
self._send_review_message(user_id, updated_data)
else:
# Формируем список услуг с ценами для повторного показа
services_text = (
"⚠️ К сожалению, я не смог распознать выбранную услугу.\n\n"
"Пожалуйста, выберите одну из доступных услуг на клавиатуре или отправьте её номер/название:\n\n"
)
for key, svc in config.SERVICES.items():
services_text += f"{key}️⃣ **{svc['name']}**\n"
services_text += f" 💰 Стоимость: {svc['price']}\n"
services_text += f" 📝 {svc['description']}\n\n"
self.send_message(
user_id,
services_text,
keyboard=get_services_keyboard()
)
def _handle_form_confirm_question(self, user_id: int, text: str, user_data: Dict[str, Any]):
"""Резервный обработчик для совместимости с устаревшим шагом подтверждения."""
db.update_user_state(user_id, "FORM_REVIEW")
self._send_review_message(user_id, user_data)
def _send_review_message(self, user_id: int, user_data: Dict[str, Any]):
"""Отправляет сводную карточку проверки данных перед финальным шагом."""
name = user_data.get("name", "Не указано")
phone = user_data.get("phone", "Не указано")
question = user_data.get("question", "Не указано")
review_text = (
"🔍 **Пожалуйста, проверьте правильность заполненных данных:**\n\n"
f"👤 **Имя:** {name}\n"
f"📞 **Телефон:** {phone}\n"
f"🎮 **Выбранная услуга:** {question}\n\n"
"Если всё верно, нажмите «Да, всё верно! ✅» для продолжения. Если хотите исправить — нажмите «Изменить данные 🔄»."
)
self.send_message(user_id, review_text, keyboard=get_review_keyboard())
def _handle_form_review(self, user_id: int, text: str, user_data: Dict[str, Any]):
"""Обработка решений на экране финальной проверки данных."""
text_lower = text.lower().strip()
if text_lower in ["да, всё верно! ✅", "да, всё верно", "все верно", "да", "верно"]:
# Переходим к шагу оферты и согласий
db.update_user_state(user_id, "FORM_CONSENT_1")
# Загружаем документ оферты для ознакомления
doc_attachment = self.get_offer_document_attachment(user_id)
# Отправляем значительно сокращенное и дружелюбное сообщение-согласие
self.send_message(
user_id,
"📄 **Согласие с условиями и офертой**\n\n"
"Для завершения отправки заявки подтвердите, пожалуйста, ваше согласие с условиями Публичной оферты, "
"Политикой обработки персональных данных и записью разговора.\n\n"
"«Я подтверждаю, что ознакомлен(а) с договором оферты (прикреплен ниже), принимаю его условия, "
"соглашаюсь на обработку персональных данных и запись консультации.»",
keyboard=get_binary_keyboard(show_skip=False),
attachment=doc_attachment
)
elif text_lower in ["изменить данные 🔄", "изменить данные", "изменить", "редактировать", "поменять"]:
# Переходим в меню изменения полей
db.update_user_state(user_id, "FORM_EDIT_MENU")
self.send_message(
user_id,
"🔄 **Меню изменения данных**\n\n"
"Пожалуйста, выберите на клавиатуре ниже, какое поле вы хотите изменить:",
keyboard=get_edit_menu_keyboard()
)
else:
self.send_message(
user_id,
"❓ Пожалуйста, подтвердите ваши данные кнопкой «Да, всё верно! ✅» или нажмите «Изменить данные 🔄» для редактирования.",
keyboard=get_review_keyboard()
)
def _handle_form_edit_menu(self, user_id: int, text: str, user_data: Dict[str, Any]):
"""Обработка меню выбора поля для редактирования."""
text_lower = text.lower().strip()
if text_lower in ["изменить имя 👤", "изменить имя", "имя", "1"]:
db.update_user_state(user_id, "FORM_EDIT_NAME")
self.send_message(
user_id,
"👤 Введите ваше новое имя:",
keyboard=get_cancel_keyboard()
)
elif text_lower in ["изменить телефон 📞", "изменить телефон", "телефон", "2"]:
db.update_user_state(user_id, "FORM_EDIT_PHONE")
self.send_message(
user_id,
"📞 Введите ваш новый номер телефона в формате +79991234567:",
keyboard=get_cancel_keyboard()
)
elif text_lower in ["изменить услугу 🎮", "изменить услугу", "услугу", "услуга", "3"]:
db.update_user_state(user_id, "FORM_EDIT_QUESTION")
services_text = (
"🎮 **Выберите новую услугу из списка:**\n\n"
)
for key, svc in config.SERVICES.items():
services_text += f"{key}️⃣ **{svc['name']}**\n"
services_text += f" 💰 Стоимость: {svc['price']}\n\n"
self.send_message(
user_id,
services_text,
keyboard=get_services_keyboard()
)
elif text_lower in ["назад к проверке ↩️", "назад к проверке", "назад", "вернуть", "4"]:
db.update_user_state(user_id, "FORM_REVIEW")
self._send_review_message(user_id, user_data)
else:
self.send_message(
user_id,
"❓ Выберите нужный пункт меню на клавиатуре или нажмите «Назад к проверке ↩️» для возврата к карточке данных.",
keyboard=get_edit_menu_keyboard()
)
def _handle_form_edit_name(self, user_id: int, text: str):
"""Сохранение нового имени и возврат к проверке."""
if len(text) < 2 or len(text) > 50:
self.send_message(
user_id,
"⚠️ Имя выглядит слишком коротким или длинным. Пожалуйста, введите корректное имя.",
keyboard=get_cancel_keyboard()
)
return
db.update_user_field(user_id, "name", text)
db.update_user_state(user_id, "FORM_REVIEW")
updated_data = db.get_user_data(user_id)
self.send_message(user_id, "✅ Имя успешно обновлено!")
self._send_review_message(user_id, updated_data)
def _handle_form_edit_phone(self, user_id: int, text: str):
"""Сохранение нового телефона и возврат к проверке."""
phone_cleaned = re.sub(r"[\s\-\(\)]", "", text)
if phone_cleaned.startswith("8") and len(phone_cleaned) == 11:
phone_cleaned = "+7" + phone_cleaned[1:]
elif phone_cleaned.startswith("7") and len(phone_cleaned) == 11:
phone_cleaned = "+" + phone_cleaned
if not PHONE_REGEX.match(phone_cleaned):
self.send_message(
user_id,
"⚠️ Некорректный формат телефона. Пожалуйста, отправьте номер в формате +79991234567.",
keyboard=get_cancel_keyboard()
)
return
db.update_user_field(user_id, "phone", phone_cleaned)
db.update_user_state(user_id, "FORM_REVIEW")
updated_data = db.get_user_data(user_id)
self.send_message(user_id, "✅ Номер телефона успешно обновлен!")
self._send_review_message(user_id, updated_data)
def _handle_form_edit_question(self, user_id: int, text: str):
"""Сохранение новой услуги и возврат к проверке."""
selected_service = None
text_lower = text.lower().strip()
# 1. Прямое сопоставление синонимов дисциплин для надежности
synonyms = {
"1": ["дота", "dota", "дотку", "дотка", "dota2"],
"2": ["кс", "cs", "ксго", "кс2", "csgo", "cs2", "контра", "кондра", "counter"]
}
for key_opt, words in synonyms.items():
if any(w in text_lower for w in words):
selected_service = config.SERVICES.get(key_opt)
break
# 2. Поиск по названию или ключу (если синонимы не сработали)
if not selected_service:
for key, svc in config.SERVICES.items():
name_clean = svc["name"].lower().strip()
name_no_emoji = re.sub(r"[^\w\s\d]", "", name_clean).strip()
text_no_emoji = re.sub(r"[^\w\s\d]", "", text_lower).strip()
if (text_lower == key or
text_lower == f"{key}." or
text_lower == f"{key}️⃣" or
name_clean in text_lower or
text_lower in name_clean or
(name_no_emoji and text_no_emoji and (text_no_emoji in name_no_emoji or name_no_emoji in text_no_emoji))):
selected_service = svc
break
if selected_service:
db.update_user_field(user_id, "question", f"{selected_service['name']} ({selected_service['price']})")
db.update_user_state(user_id, "FORM_REVIEW")
updated_data = db.get_user_data(user_id)
self.send_message(user_id, "✅ Услуга успешно обновлена!")
self._send_review_message(user_id, updated_data)
else:
services_text = (
"⚠️ К сожалению, я не смог распознать выбранную услугу.\n\n"
"Пожалуйста, выберите одну из доступных услуг на клавиатуре:\n\n"
)
for key, svc in config.SERVICES.items():
services_text += f"{key}️⃣ **{svc['name']}**\n"
services_text += f" 💰 Стоимость: {svc['price']}\n\n"
self.send_message(
user_id,
services_text,
keyboard=get_services_keyboard()
)
def get_offer_document_attachment(self, peer_id: int) -> Optional[str]:
"""Загружает документ оферты на сервер VK и возвращает attachment-строку."""
if hasattr(self, "_cached_doc_attachment") and self._cached_doc_attachment:
return self._cached_doc_attachment
import os
doc_path = os.path.join(os.path.dirname(__file__), "Dogovor_oferty_kibersport_versia_izmenennaya.docx")
if not os.path.exists(doc_path):
logger.error(f"Файл оферты не найден по пути: {doc_path}")
return None
if not self.vk:
# Режим эмуляции (консольный)
logger.info(f"[EMULATOR] Файл {os.path.basename(doc_path)} прикреплен к сообщению.")
return None
try:
import requests
logger.info("Загрузка документа оферты в VK API...")
upload_server = self.vk.docs.getMessagesUploadServer(peer_id=peer_id, type="doc")
upload_url = upload_server["upload_url"]
title = "Dogovor_oferty_kibersport.docx"
with open(doc_path, "rb") as f:
response = requests.post(upload_url, files={"file": (title, f)})
response_json = response.json()
if "file" not in response_json:
logger.error(f"Ошибка загрузки файла в VK: {response_json}")
return None
saved_doc = self.vk.docs.save(file=response_json["file"], title=title)
doc_info = saved_doc[0] if isinstance(saved_doc, list) else saved_doc
attachment = f"doc{doc_info['owner_id']}_{doc_info['id']}"
self._cached_doc_attachment = attachment
logger.info(f"Документ загружен успешно: {attachment}")
return attachment
except Exception as e:
logger.error(f"Ошибка при загрузке документа в VK API: {e}")
return None
def _handle_form_consent_1(self, user_id: int, text: str, text_lower: str):
"""Обработка объединенного шага согласий."""
is_agree = text_lower in ["согласен(а) и подтверждаю ✅", "согласен и подтверждаю", "согласен(а) и подтверждаю", "согласен", "согласна", "да", "принимаю", "согласен(а) ✅"]
is_disagree = text_lower in ["не согласен(а) ❌", "не согласен", "не согласна", "нет", "не принимаю"]
if is_agree:
# Записываем все согласия как "Согласен" в базу данных для полной совместимости
db.update_user_field(user_id, "consent_1", "Согласен")
db.update_user_field(user_id, "consent_2", "Согласен")
db.update_user_field(user_id, "consent_3", "Согласен")
# Получаем актуальные данные пользователя
updated_data = db.get_user_data(user_id)
phone = updated_data["phone"]
# Возвращаем пользователя в главное меню
db.update_user_state(user_id, "FREE_CHAT")
# Динамически определяем время ответа в зависимости от рабочего времени (с 9:00 до 21:00)
from datetime import datetime
now = datetime.now()
if 9 <= now.hour < 21:
callback_time = "в течение 15 минут"
else:
callback_time = "сегодня утром, начиная с 9:00"
# Отправляем подтверждение пользователю
self.send_message(
user_id,
"🎉 Заявка и все необходимые согласия успешно оформлены!\n\n"
f"Наш менеджер уже изучает детали и свяжется с вами {callback_time} по номеру "
f"{phone}. Спасибо за обращение!\n\n"
"Вы вернулись в главное меню бота.",
keyboard=get_main_keyboard()
)
# Уведомляем оператора о новой заявке со всеми согласиями
self._notify_operator_about_lead(user_id, updated_data)
elif is_disagree:
self.send_message(
user_id,
"⚠️ **Для продолжения оформления заявки необходимо принять условия Публичной оферты и дать согласие на обработку данных и запись разговора.**\n\n"
"Пожалуйста, подтвердите согласие кнопкой «Согласен(а) и подтверждаю ✅» или нажмите «Отменить заполнение ❌» для выхода.",
keyboard=get_binary_keyboard(show_skip=False)
)
else:
self.send_message(
user_id,
"❓ Пожалуйста, подтвердите согласие кнопкой «Согласен(а) и подтверждаю ✅» или выберите «Не согласен(а) ❌» / «Отменить заполнение ❌».",
keyboard=get_binary_keyboard(show_skip=False)
)
def _handle_form_consent_2(self, user_id: int, text: str, text_lower: str):
"""Резервный обработчик для совместимости с устаревшими состояниями в БД."""
db.update_user_field(user_id, "consent_2", "Согласен")
db.update_user_field(user_id, "consent_3", "Согласен")
updated_data = db.get_user_data(user_id)
phone = updated_data["phone"]
db.update_user_state(user_id, "FREE_CHAT")
self.send_message(
user_id,
"🎉 Заявка успешно оформлена!\n\n"
f"Наш менеджер свяжется с вами по номеру {phone}. Спасибо за обращение!",
keyboard=get_main_keyboard()
)
self._notify_operator_about_lead(user_id, updated_data)
def _handle_form_consent_3(self, user_id: int, text: str, text_lower: str, user_data: Dict[str, Any]):
"""Резервный обработчик для совместимости с устаревшими состояниями в БД."""
db.update_user_field(user_id, "consent_3", "Согласен")
updated_data = db.get_user_data(user_id)
phone = updated_data["phone"]
db.update_user_state(user_id, "FREE_CHAT")
self.send_message(
user_id,
"🎉 Заявка успешно оформлена!\n\n"
f"Наш менеджер свяжется с вами по номеру {phone}. Спасибо за обращение!",
keyboard=get_main_keyboard()
)
self._notify_operator_about_lead(user_id, updated_data)
def _handle_operator_chat(self, user_id: int, text: str, text_lower: str):
"""Поведение бота в режиме активного диалога с живым человеком."""
# Пользователь хочет выйти из диалога с оператором
if text_lower in ["завершить диалог 🚪", "завершить", "/exit", "выход"]:
db.update_user_state(user_id, "FREE_CHAT")
self.send_message(
user_id,
"🚪 Диалог с оператором завершен. Вы вернулись к автоматическому ассистенту.",
keyboard=get_main_keyboard()
)
# Уведомляем оператора о закрытии диалога
self.send_message(
config.OPERATOR_PEER_ID,
f"ℹ️ Пользователь vk.com/id{user_id} завершил сессию общения с оператором."
)
return
# В остальное время бот МОЛЧИТ в диалог с пользователем.
logger.info(f"[OPERATOR MODE] Message from {user_id} bypasses bot: {text}")
self._forward_to_operator(user_id, text)
# ------------------------------------------
# СЦЕНАРИИ ПЕРЕДАЧИ ОПЕРАТОРУ И СВЯЗИ
# ------------------------------------------
def _handoff_to_operator(self, user_id: int):
"""Переводит пользователя в режим оператора и уведомляет команду поддержки."""
db.update_user_state(user_id, "OPERATOR_CHAT")
self.send_message(
user_id,
"🧑‍💼 Переключаю вас на оператора...\n\n"
"Я отправил уведомление специалисту поддержки. Он ответит вам прямо в этом диалоге в ближайшее время.\n"
"Все ваши сообщения сейчас направляются напрямую человеку. Чтобы вернуться к боту, нажмите кнопку ниже.",
keyboard=get_operator_keyboard()
)
# Получаем историю последних 10 сообщений для контекста оператора
history = db.get_dialogue_history(user_id, limit=10)
formatted_history = ""
for sender, msg, ts in history:
role = "Вы" if sender == "user" else "Бот"
formatted_history += f"[{ts}] {role}: {msg}\n"
# Отправляем алерт оператору с историей диалога
operator_msg = (
f"🚨 **Запрос оператора!**\n"
f"Пользователь: vk.com/id{user_id}\n"
f"Тип запроса: Прямой вызов из меню\n\n"
f"📋 **Последние сообщения для контекста:**\n{formatted_history}"
)
self.send_message(config.OPERATOR_PEER_ID, operator_msg)
def _notify_operator_about_lead(self, user_id: int, user_data: Dict[str, Any]):
"""Отправляет структурированный лид оператору после заполнения формы с указанием всех согласий."""
name = user_data["name"]
phone = user_data["phone"]
question = user_data["question"]
consent_1 = user_data.get("consent_1", "Согласен")
consent_2 = user_data.get("consent_2", "Согласен")
consent_3 = user_data.get("consent_3", "Согласен")
operator_msg = (
f"⚡ **Получена новая заявка!**\n\n"
f"👤 **Имя:** {name}\n"
f"📞 **Телефон:** {phone}\n"
f"🎯 **Выбранная услуга:** {question}\n\n"
f"⚖️ **Согласия пользователя:**\n"
f"1. Оферта и обработка ПД (Обяз.): {consent_1}\n"
f"2. Маркетинговые рассылки (Необяз.): {consent_2}\n"
f"3. Запись консультации (Обяз.): {consent_3}\n\n"
f"🔗 Профиль клиента: vk.com/id{user_id}\n"
f"Переписка сохранена в базе данных."
)
self.send_message(config.OPERATOR_PEER_ID, operator_msg)
def _forward_to_operator(self, user_id: int, text: str):
"""Пересылка сообщений от клиента в чат оператора."""
forward_msg = (
f"💬 **Сообщение от vk.com/id{user_id}:**\n{text}"
)
self.send_message(config.OPERATOR_PEER_ID, forward_msg)
# ------------------------------------------
# ЗАПУСК И ОБРАБОТКА LONGPOLL
# ------------------------------------------
def _send_admin_menu(self, user_id: int):
"""Отправляет администратору панель управления услугами с текущим списком."""
services = db.get_services()
services_list_text = ""
for key, svc in services.items():
services_list_text += f"🔹 **[{key}] {svc['name']}**\n"
services_list_text += f" 💰 Стоимость: {svc['price']}\n"
services_list_text += f" 📝 Описание: {svc['description']}\n\n"
admin_text = (
"⚙️ **Панель управления услугами (Админ-меню)**\n\n"
"Здесь вы можете редактировать существующие услуги или добавлять новые.\n\n"
"📋 **Текущий список активных услуг:**\n\n"
f"{services_list_text}"
"🔧 **Доступные команды управления:**\n\n"
"1. **Редактирование услуги**:\n"
" 👉 Отправьте: `ред [номер] [поле] [новое значение]`\n"
" Доступные поля: `имя`, `цена`, `описание`.\n"
" *Пример:* `ред 1 цена 1 800 ₽/час`\n\n"
"2. **Добавление услуги**:\n"
" 👉 Отправьте: `доб [Имя] | [Цена] | [Описание]`\n"
" *Пример:* `доб Тренировка по Valorant 🎯 | 1 500 ₽/час | Разбор стрельбы и aim.`\n\n"
"3. **Удаление услуги**:\n"
" 👉 Отправьте: `удал [номер]`\n"
" *Пример:* `удал 4`"
)
self.send_message(user_id, admin_text)
def _handle_admin_edit(self, user_id: int, text: str):
"""Обрабатывает редактирование услуги администратором."""
parts = text.split(" ", 3)
if len(parts) < 4:
self.send_message(
user_id,
"⚠️ Некорректный формат команды редактирования.\n"
"Правильный формат: `ред [номер] [поле] [новое значение]`\n"
"Пример: `ред 1 цена 1 800 ₽/час`"
)
return
_, service_id, field_name, new_value = parts
field_name = field_name.lower().strip()
new_value = new_value.strip()
field_mapping = {
"имя": "name",
"название": "name",
"цена": "price",
"стоимость": "price",
"описание": "description"
}
if field_name not in field_mapping:
self.send_message(
user_id,
"⚠️ Неизвестное поле для редактирования. Допустимые поля: `имя`, `цена`, `описание`."
)
return
db_field = field_mapping[field_name]
services = db.get_services()
if service_id not in services:
self.send_message(
user_id,
f"⚠️ Услуга с номером '{service_id}' не найдена в списке активных услуг."
)
return
try:
db.update_service(service_id, db_field, new_value)
config.SERVICES = db.get_services()
self.send_message(
user_id,
f"✅ Услуга [{service_id}] успешно обновлена!\n"
f"Поле `{field_name}` изменено на: `{new_value}`."
)
self._send_admin_menu(user_id)
except Exception as e:
logger.error(f"Ошибка при редактировании услуги в БД: {e}")
self.send_message(user_id, f"⚠️ Произошла ошибка при обновлении базы данных: {e}")
def _handle_admin_add(self, user_id: int, text: str):
"""Обрабатывает добавление новой услуги администратором."""
content = text[4:].strip()
parts = [p.strip() for p in content.split("|")]
if len(parts) < 3:
self.send_message(
user_id,
"⚠️ Некорректный формат команды добавления.\n"
"Правильный формат: `доб [Имя] | [Цена] | [Описание]`\n"
"Пример: `доб Тренировка по Valorant 🎯 | 1 500 ₽/час | Разбор стрельбы и aim.`"
)
return
name, price, description = parts
if not name or not price or not description:
self.send_message(user_id, "⚠️ Все три поля (имя, цена, описание) должны быть заполнены!")
return
try:
new_id = db.add_service(name, price, description)
config.SERVICES = db.get_services()
self.send_message(
user_id,
f"✅ Новая услуга успешно добавлена с номером **[{new_id}]**!"
)
self._send_admin_menu(user_id)
except Exception as e:
logger.error(f"Ошибка при добавлении услуги в БД: {e}")
self.send_message(user_id, f"⚠️ Произошла ошибка при обновлении базы данных: {e}")
def _handle_admin_delete(self, user_id: int, text: str):
"""Обрабатывает мягкое удаление услуги администратором."""
parts = text.split(" ")
if len(parts) < 2:
self.send_message(
user_id,
"⚠️ Укажите номер услуги для удаления.\n"
"Пример: `удал 4`"
)
return
service_id = parts[1].strip()
services = db.get_services()
if service_id not in services:
self.send_message(
user_id,
f"⚠️ Услуга с номером '{service_id}' не найдена в списке активных услуг."
)
return
try:
db.delete_service(service_id)
config.SERVICES = db.get_services()
self.send_message(
user_id,
f"✅ Услуга [{service_id}] успешно удалена из активного списка!"
)
self._send_admin_menu(user_id)
except Exception as e:
logger.error(f"Ошибка при деактивации услуги в БД: {e}")
self.send_message(user_id, f"⚠️ Произошла ошибка при обновлении базы данных: {e}")
def run(self):
"""Запуск бесконечного цикла прослушивания событий VK LongPoll."""
if not self.longpoll:
logger.warning("Бот запущен в режиме ЭМУЛЯЦИИ (без валидных токенов VK API).")
print("Симуляция работы бота в консоли. Напишите 'выход' для завершения.")
# Эмулятор консольного ввода для тестирования на Windows локально
test_user_id = 99999
# Эмулируем приветствие при старте
self.handle_message(test_user_id, "начать")
while True:
try:
user_input = input("Вы: ")
if user_input.lower() == "выход":
break
self.handle_message(test_user_id, user_input)
except (KeyboardInterrupt, EOFError):
break
return
logger.info("Бот успешно запущен в режиме прослушивания LongPoll...")
for event in self.longpoll.listen():
if event.type == VkBotEventType.MESSAGE_NEW:
message_obj = event.obj.message
user_id = message_obj["from_id"]
text = message_obj["text"]
self.handle_message(user_id, text)
if __name__ == "__main__":
bot = VKBotApplication()
bot.run()