import json import re import time import random import string import logging import threading from collections import defaultdict 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 import mailer # Настройка логирования logging.basicConfig( level=logging.INFO, format="%(asctime)s - [%(levelname)s] - %(name)s - %(message)s" ) logger = logging.getLogger("VK_Bot_IKP") # Инициализация базы данных сессий db = DatabaseManager() # Анти-флуд: максимум сообщений от одного пользователя за окно времени FLOOD_MAX_MSGS = 12 FLOOD_WINDOW_SEC = 10 # Кулдаун между уведомлениями оператора о заявке/отклике от одного пользователя (сек) NOTIFY_COOLDOWN_SEC = 120 # Временный бан при повторном флуде: после BAN_STRIKES срабатываний за BAN_WINDOW_SEC — бан на BAN_SEC BAN_STRIKES = 3 BAN_WINDOW_SEC = 60 BAN_SEC = 600 # Регулярные выражения для валидации # Российский номер для нормализации 8XXX / 7XXX -> +7XXX RU_PHONE_REGEX = re.compile(r"^\+?[78]\d{10}$") # Международный формат (E.164): + и 10–15 цифр. Принимаем и иностранные номера. INTL_PHONE_REGEX = re.compile(r"^\+?\d{10,15}$") # Имя: буквы (кириллица/латиница), пробел, дефис, апостроф; длина 2–50 NAME_REGEX = re.compile(r"^[A-Za-zА-Яа-яЁё][A-Za-zА-Яа-яЁё \-']{1,49}$") def normalize_phone(text: str) -> Optional[str]: """Очищает и нормализует телефон. Возвращает нормализованный номер или None, если формат неверен.""" phone_cleaned = re.sub(r"[\s\-\(\)]", "", text) # Российские номера приводим к виду +7XXXXXXXXXX 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 # Принимаем как российский, так и любой международный номер в формате E.164 if RU_PHONE_REGEX.match(phone_cleaned) or INTL_PHONE_REGEX.match(phone_cleaned): return phone_cleaned return None def format_phone_display(phone: str) -> str: """Красивое отображение номера: +7 (999) 123-45-67 для РФ, иначе как есть.""" if not phone: return phone digits = re.sub(r"\D", "", phone) if len(digits) == 11 and digits[0] in ("7", "8"): d = digits return f"+7 ({d[1:4]}) {d[4:7]}-{d[7:9]}-{d[9:11]}" return phone def is_valid_name(text: str) -> bool: """Имя должно состоять только из букв, пробела, дефиса или апострофа (без цифр и спецсимволов).""" return bool(NAME_REGEX.match(text.strip())) # ========================================== # КЛАВИАТУРЫ ДЛЯ ИНТЕРФЕЙСА (UX/UI ВКонтакте) # ========================================== def get_main_keyboard(is_admin: bool = False) -> str: """Главное меню бота. Для админа добавляется кнопка «Админка ⚙️».""" keyboard = VkKeyboard(one_time=False) # Кнопка начать опрос (зеленая) keyboard.add_button("Заполнить заявку 📝", color=VkKeyboardColor.PRIMARY) keyboard.add_line() # Карьера в ИКП (набор игроков и сотрудников) keyboard.add_button("Карьера в ИКП 💼", color=VkKeyboardColor.POSITIVE) keyboard.add_line() # Кнопки FAQ и оператора keyboard.add_button("Частые вопросы ❓", color=VkKeyboardColor.SECONDARY) keyboard.add_button("Позвать оператора 🧑‍💼", color=VkKeyboardColor.NEGATIVE) # Кнопка админ-панели — только для администраторов (отдельной строкой) if is_admin: keyboard.add_line() keyboard.add_button("Админка ⚙️", color=VkKeyboardColor.PRIMARY) return keyboard.get_keyboard() def get_admin_keyboard(is_super: bool = False) -> 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.SECONDARY) keyboard.add_line() keyboard.add_button("Быстрые ответы 💬", color=VkKeyboardColor.SECONDARY) keyboard.add_button("Рассылка 📣", color=VkKeyboardColor.SECONDARY) keyboard.add_line() if is_super: keyboard.add_button("Администраторы 👑", color=VkKeyboardColor.NEGATIVE) keyboard.add_line() keyboard.add_button("В главное меню ↩️", color=VkKeyboardColor.PRIMARY) return keyboard.get_keyboard() def get_svc_admin_keyboard() -> str: """Клавиатура раздела управления услугами (направлениями).""" keyboard = VkKeyboard(one_time=False) keyboard.add_button("➕ Добавить услугу", color=VkKeyboardColor.POSITIVE) keyboard.add_line() keyboard.add_button("✏️ Изменить услугу", color=VkKeyboardColor.SECONDARY) keyboard.add_button("🖼 Сменить картинку", color=VkKeyboardColor.SECONDARY) keyboard.add_line() keyboard.add_button("🗑 Удалить услугу", color=VkKeyboardColor.NEGATIVE) keyboard.add_line() keyboard.add_button("В админку ↩️", color=VkKeyboardColor.PRIMARY) return keyboard.get_keyboard() def get_faq_admin_keyboard() -> str: """Клавиатура раздела управления быстрыми ответами.""" keyboard = VkKeyboard(one_time=False) keyboard.add_button("➕ Добавить ответ", color=VkKeyboardColor.POSITIVE) keyboard.add_line() keyboard.add_button("✏️ Изменить ответ", color=VkKeyboardColor.SECONDARY) keyboard.add_button("🗑 Удалить ответ", color=VkKeyboardColor.NEGATIVE) keyboard.add_line() keyboard.add_button("В админку ↩️", color=VkKeyboardColor.PRIMARY) return keyboard.get_keyboard() def get_pick_keyboard(items: list) -> str: """Клавиатура выбора из списка пар (id, label). Кнопка отправляет '.