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()