Рефакторинг (этап 1): валидаторы и клавиатуры вынесены в модули
bot.py разгружен: чистые функции вынесены без изменения поведения: - validators.py — регэкспы, normalize_phone, format_phone_display, is_valid_name - keyboards.py — все get_*_keyboard Поведение проверено регрессионным прогоном (полный флоу заявки, админка, меню). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -41,355 +41,11 @@ BAN_STRIKES = 3
|
|||||||
BAN_WINDOW_SEC = 60
|
BAN_WINDOW_SEC = 60
|
||||||
BAN_SEC = 600
|
BAN_SEC = 600
|
||||||
|
|
||||||
# Регулярные выражения для валидации
|
from validators import (
|
||||||
# Российский номер для нормализации 8XXX / 7XXX -> +7XXX
|
RU_PHONE_REGEX, INTL_PHONE_REGEX, NAME_REGEX,
|
||||||
RU_PHONE_REGEX = re.compile(r"^\+?[78]\d{10}$")
|
normalize_phone, format_phone_display, is_valid_name,
|
||||||
# Международный формат (E.164): + и 10–15 цифр. Принимаем и иностранные номера.
|
)
|
||||||
INTL_PHONE_REGEX = re.compile(r"^\+?\d{10,15}$")
|
from keyboards import * # все get_*_keyboard
|
||||||
# Имя: буквы (кириллица/латиница), пробел, дефис, апостроф; длина 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). Кнопка отправляет '<id>. <label>'."""
|
|
||||||
keyboard = VkKeyboard(one_time=False)
|
|
||||||
count = 0
|
|
||||||
for _id, label in items[:10]:
|
|
||||||
if count and count % 2 == 0:
|
|
||||||
keyboard.add_line()
|
|
||||||
short = label if len(label) <= 30 else label[:29] + "…"
|
|
||||||
keyboard.add_button(f"{_id}. {short}", color=VkKeyboardColor.SECONDARY)
|
|
||||||
count += 1
|
|
||||||
keyboard.add_line()
|
|
||||||
keyboard.add_button("Отмена ❌", color=VkKeyboardColor.NEGATIVE)
|
|
||||||
return keyboard.get_keyboard()
|
|
||||||
|
|
||||||
|
|
||||||
def get_cancel_admin_keyboard() -> str:
|
|
||||||
"""Кнопка отмены для админских мастеров ввода."""
|
|
||||||
keyboard = VkKeyboard(one_time=False)
|
|
||||||
keyboard.add_button("Отмена ❌", color=VkKeyboardColor.NEGATIVE)
|
|
||||||
return keyboard.get_keyboard()
|
|
||||||
|
|
||||||
|
|
||||||
def get_vacancy_choice_keyboard() -> str:
|
|
||||||
"""Выбор направления в разделе 'Карьера в ИКП'."""
|
|
||||||
keyboard = VkKeyboard(one_time=False)
|
|
||||||
keyboard.add_button("Игроком в состав 🎮", color=VkKeyboardColor.PRIMARY)
|
|
||||||
keyboard.add_line()
|
|
||||||
keyboard.add_button("Сотрудником 💼", color=VkKeyboardColor.PRIMARY)
|
|
||||||
keyboard.add_line()
|
|
||||||
keyboard.add_button("Назад в меню ↩️", color=VkKeyboardColor.SECONDARY)
|
|
||||||
return keyboard.get_keyboard()
|
|
||||||
|
|
||||||
|
|
||||||
def get_vacancy_discipline_keyboard() -> str:
|
|
||||||
"""Выбор дисциплины для набора в состав."""
|
|
||||||
keyboard = VkKeyboard(one_time=False)
|
|
||||||
keyboard.add_button("Counter-Strike 2 🔫", color=VkKeyboardColor.SECONDARY)
|
|
||||||
keyboard.add_button("Dota 2 🎮", color=VkKeyboardColor.SECONDARY)
|
|
||||||
keyboard.add_line()
|
|
||||||
keyboard.add_button("World of Tanks 🛡️", color=VkKeyboardColor.SECONDARY)
|
|
||||||
keyboard.add_line()
|
|
||||||
keyboard.add_button("Отменить ❌", color=VkKeyboardColor.NEGATIVE)
|
|
||||||
return keyboard.get_keyboard()
|
|
||||||
|
|
||||||
|
|
||||||
def get_vacancy_position_keyboard() -> str:
|
|
||||||
"""Выбор должности для найма сотрудника."""
|
|
||||||
keyboard = VkKeyboard(one_time=False)
|
|
||||||
keyboard.add_button("SMM / контент", 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.NEGATIVE)
|
|
||||||
return keyboard.get_keyboard()
|
|
||||||
|
|
||||||
|
|
||||||
def get_vacancy_cancel_keyboard() -> str:
|
|
||||||
"""Клавиатура отмены отклика на свободном текстовом шаге."""
|
|
||||||
keyboard = VkKeyboard(one_time=False)
|
|
||||||
keyboard.add_button("Отменить ❌", color=VkKeyboardColor.NEGATIVE)
|
|
||||||
return keyboard.get_keyboard()
|
|
||||||
|
|
||||||
|
|
||||||
def get_vacancy_back_keyboard() -> str:
|
|
||||||
"""Клавиатура с одной кнопкой «Назад в меню» — под карусель выбора в «Карьере»."""
|
|
||||||
keyboard = VkKeyboard(one_time=False)
|
|
||||||
keyboard.add_button("Назад в меню ↩️", color=VkKeyboardColor.SECONDARY)
|
|
||||||
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.SECONDARY)
|
|
||||||
keyboard.add_line()
|
|
||||||
keyboard.add_button("Назад в меню ↩️", color=VkKeyboardColor.PRIMARY)
|
|
||||||
return keyboard.get_keyboard()
|
|
||||||
|
|
||||||
|
|
||||||
def get_consent_docs_keyboard() -> str:
|
|
||||||
"""Инлайн-кнопки со ссылками на оферту и политику обработки ПД."""
|
|
||||||
keyboard = VkKeyboard(inline=True)
|
|
||||||
keyboard.add_openlink_button("📄 Оферта", getattr(config, "OFFER_URL", "https://infocyber.pro/"))
|
|
||||||
keyboard.add_line()
|
|
||||||
keyboard.add_openlink_button("🔐 Политика обработки ПД", getattr(config, "POLICY_URL", "https://infocyber.pro/"))
|
|
||||||
return keyboard.get_keyboard()
|
|
||||||
|
|
||||||
|
|
||||||
def get_socials_keyboard() -> str:
|
|
||||||
"""Клавиатура с кнопками-ссылками на площадки ИКП (открываются в один тап)."""
|
|
||||||
keyboard = VkKeyboard(one_time=False)
|
|
||||||
keyboard.add_openlink_button("VK Live 📺", "https://live.vkvideo.ru/ikp")
|
|
||||||
keyboard.add_line()
|
|
||||||
keyboard.add_openlink_button("Telegram ✈️", "https://t.me/ikp_team")
|
|
||||||
keyboard.add_openlink_button("Discord 🎧", "https://discord.gg/z92mAw7KEe")
|
|
||||||
keyboard.add_line()
|
|
||||||
keyboard.add_openlink_button("ВКонтакте 🌐", "https://vk.com/ikpro")
|
|
||||||
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_returning_keyboard() -> str:
|
|
||||||
"""Клавиатура для возвращающегося клиента, чьи данные уже известны."""
|
|
||||||
keyboard = VkKeyboard(one_time=False)
|
|
||||||
keyboard.add_button("Да, всё верно ✅", color=VkKeyboardColor.PRIMARY)
|
|
||||||
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.NEGATIVE)
|
|
||||||
return keyboard.get_keyboard()
|
|
||||||
|
|
||||||
|
|
||||||
def get_calldate_keyboard() -> str:
|
|
||||||
"""Клавиатура выбора ДНЯ для звонка (сегодня/завтра + ближайшие даты)."""
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
wd = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
|
|
||||||
today = datetime.now()
|
|
||||||
keyboard = VkKeyboard(one_time=False)
|
|
||||||
keyboard.add_button("Сегодня", color=VkKeyboardColor.PRIMARY)
|
|
||||||
keyboard.add_button("Завтра", color=VkKeyboardColor.PRIMARY)
|
|
||||||
keyboard.add_line()
|
|
||||||
for i in (2, 3, 4):
|
|
||||||
d = today + timedelta(days=i)
|
|
||||||
keyboard.add_button(f"{wd[d.weekday()]} {d.strftime('%d.%m')}", color=VkKeyboardColor.SECONDARY)
|
|
||||||
keyboard.add_line()
|
|
||||||
keyboard.add_button("Отменить заполнение ❌", color=VkKeyboardColor.NEGATIVE)
|
|
||||||
return keyboard.get_keyboard()
|
|
||||||
|
|
||||||
|
|
||||||
def get_calltime_keyboard() -> str:
|
|
||||||
"""Клавиатура выбора ВРЕМЕНИ (слоты в рабочие часы)."""
|
|
||||||
keyboard = VkKeyboard(one_time=False)
|
|
||||||
keyboard.add_button("10:00", color=VkKeyboardColor.SECONDARY)
|
|
||||||
keyboard.add_button("12:00", color=VkKeyboardColor.SECONDARY)
|
|
||||||
keyboard.add_button("14:00", color=VkKeyboardColor.SECONDARY)
|
|
||||||
keyboard.add_line()
|
|
||||||
keyboard.add_button("16:00", color=VkKeyboardColor.SECONDARY)
|
|
||||||
keyboard.add_button("18:00", color=VkKeyboardColor.SECONDARY)
|
|
||||||
keyboard.add_button("20:00", color=VkKeyboardColor.SECONDARY)
|
|
||||||
keyboard.add_line()
|
|
||||||
keyboard.add_button("Любое время", color=VkKeyboardColor.PRIMARY)
|
|
||||||
keyboard.add_button("Отменить заполнение ❌", color=VkKeyboardColor.NEGATIVE)
|
|
||||||
return keyboard.get_keyboard()
|
|
||||||
|
|
||||||
|
|
||||||
def get_marketing_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_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.SECONDARY)
|
|
||||||
keyboard.add_line()
|
|
||||||
keyboard.add_button("Назад к проверке ↩️", color=VkKeyboardColor.PRIMARY)
|
|
||||||
return keyboard.get_keyboard()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|||||||
+314
@@ -0,0 +1,314 @@
|
|||||||
|
"""Клавиатуры интерфейса бота (VK)."""
|
||||||
|
from vk_api.keyboard import VkKeyboard, VkKeyboardColor
|
||||||
|
import config
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# КЛАВИАТУРЫ ДЛЯ ИНТЕРФЕЙСА (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). Кнопка отправляет '<id>. <label>'."""
|
||||||
|
keyboard = VkKeyboard(one_time=False)
|
||||||
|
count = 0
|
||||||
|
for _id, label in items[:10]:
|
||||||
|
if count and count % 2 == 0:
|
||||||
|
keyboard.add_line()
|
||||||
|
short = label if len(label) <= 30 else label[:29] + "…"
|
||||||
|
keyboard.add_button(f"{_id}. {short}", color=VkKeyboardColor.SECONDARY)
|
||||||
|
count += 1
|
||||||
|
keyboard.add_line()
|
||||||
|
keyboard.add_button("Отмена ❌", color=VkKeyboardColor.NEGATIVE)
|
||||||
|
return keyboard.get_keyboard()
|
||||||
|
|
||||||
|
|
||||||
|
def get_cancel_admin_keyboard() -> str:
|
||||||
|
"""Кнопка отмены для админских мастеров ввода."""
|
||||||
|
keyboard = VkKeyboard(one_time=False)
|
||||||
|
keyboard.add_button("Отмена ❌", color=VkKeyboardColor.NEGATIVE)
|
||||||
|
return keyboard.get_keyboard()
|
||||||
|
|
||||||
|
|
||||||
|
def get_vacancy_choice_keyboard() -> str:
|
||||||
|
"""Выбор направления в разделе 'Карьера в ИКП'."""
|
||||||
|
keyboard = VkKeyboard(one_time=False)
|
||||||
|
keyboard.add_button("Игроком в состав 🎮", color=VkKeyboardColor.PRIMARY)
|
||||||
|
keyboard.add_line()
|
||||||
|
keyboard.add_button("Сотрудником 💼", color=VkKeyboardColor.PRIMARY)
|
||||||
|
keyboard.add_line()
|
||||||
|
keyboard.add_button("Назад в меню ↩️", color=VkKeyboardColor.SECONDARY)
|
||||||
|
return keyboard.get_keyboard()
|
||||||
|
|
||||||
|
|
||||||
|
def get_vacancy_discipline_keyboard() -> str:
|
||||||
|
"""Выбор дисциплины для набора в состав."""
|
||||||
|
keyboard = VkKeyboard(one_time=False)
|
||||||
|
keyboard.add_button("Counter-Strike 2 🔫", color=VkKeyboardColor.SECONDARY)
|
||||||
|
keyboard.add_button("Dota 2 🎮", color=VkKeyboardColor.SECONDARY)
|
||||||
|
keyboard.add_line()
|
||||||
|
keyboard.add_button("World of Tanks 🛡️", color=VkKeyboardColor.SECONDARY)
|
||||||
|
keyboard.add_line()
|
||||||
|
keyboard.add_button("Отменить ❌", color=VkKeyboardColor.NEGATIVE)
|
||||||
|
return keyboard.get_keyboard()
|
||||||
|
|
||||||
|
|
||||||
|
def get_vacancy_position_keyboard() -> str:
|
||||||
|
"""Выбор должности для найма сотрудника."""
|
||||||
|
keyboard = VkKeyboard(one_time=False)
|
||||||
|
keyboard.add_button("SMM / контент", 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.NEGATIVE)
|
||||||
|
return keyboard.get_keyboard()
|
||||||
|
|
||||||
|
|
||||||
|
def get_vacancy_cancel_keyboard() -> str:
|
||||||
|
"""Клавиатура отмены отклика на свободном текстовом шаге."""
|
||||||
|
keyboard = VkKeyboard(one_time=False)
|
||||||
|
keyboard.add_button("Отменить ❌", color=VkKeyboardColor.NEGATIVE)
|
||||||
|
return keyboard.get_keyboard()
|
||||||
|
|
||||||
|
|
||||||
|
def get_vacancy_back_keyboard() -> str:
|
||||||
|
"""Клавиатура с одной кнопкой «Назад в меню» — под карусель выбора в «Карьере»."""
|
||||||
|
keyboard = VkKeyboard(one_time=False)
|
||||||
|
keyboard.add_button("Назад в меню ↩️", color=VkKeyboardColor.SECONDARY)
|
||||||
|
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.SECONDARY)
|
||||||
|
keyboard.add_line()
|
||||||
|
keyboard.add_button("Назад в меню ↩️", color=VkKeyboardColor.PRIMARY)
|
||||||
|
return keyboard.get_keyboard()
|
||||||
|
|
||||||
|
|
||||||
|
def get_consent_docs_keyboard() -> str:
|
||||||
|
"""Инлайн-кнопки со ссылками на оферту и политику обработки ПД."""
|
||||||
|
keyboard = VkKeyboard(inline=True)
|
||||||
|
keyboard.add_openlink_button("📄 Оферта", getattr(config, "OFFER_URL", "https://infocyber.pro/"))
|
||||||
|
keyboard.add_line()
|
||||||
|
keyboard.add_openlink_button("🔐 Политика обработки ПД", getattr(config, "POLICY_URL", "https://infocyber.pro/"))
|
||||||
|
return keyboard.get_keyboard()
|
||||||
|
|
||||||
|
|
||||||
|
def get_socials_keyboard() -> str:
|
||||||
|
"""Клавиатура с кнопками-ссылками на площадки ИКП (открываются в один тап)."""
|
||||||
|
keyboard = VkKeyboard(one_time=False)
|
||||||
|
keyboard.add_openlink_button("VK Live 📺", "https://live.vkvideo.ru/ikp")
|
||||||
|
keyboard.add_line()
|
||||||
|
keyboard.add_openlink_button("Telegram ✈️", "https://t.me/ikp_team")
|
||||||
|
keyboard.add_openlink_button("Discord 🎧", "https://discord.gg/z92mAw7KEe")
|
||||||
|
keyboard.add_line()
|
||||||
|
keyboard.add_openlink_button("ВКонтакте 🌐", "https://vk.com/ikpro")
|
||||||
|
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_returning_keyboard() -> str:
|
||||||
|
"""Клавиатура для возвращающегося клиента, чьи данные уже известны."""
|
||||||
|
keyboard = VkKeyboard(one_time=False)
|
||||||
|
keyboard.add_button("Да, всё верно ✅", color=VkKeyboardColor.PRIMARY)
|
||||||
|
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.NEGATIVE)
|
||||||
|
return keyboard.get_keyboard()
|
||||||
|
|
||||||
|
|
||||||
|
def get_calldate_keyboard() -> str:
|
||||||
|
"""Клавиатура выбора ДНЯ для звонка (сегодня/завтра + ближайшие даты)."""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
wd = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
|
||||||
|
today = datetime.now()
|
||||||
|
keyboard = VkKeyboard(one_time=False)
|
||||||
|
keyboard.add_button("Сегодня", color=VkKeyboardColor.PRIMARY)
|
||||||
|
keyboard.add_button("Завтра", color=VkKeyboardColor.PRIMARY)
|
||||||
|
keyboard.add_line()
|
||||||
|
for i in (2, 3, 4):
|
||||||
|
d = today + timedelta(days=i)
|
||||||
|
keyboard.add_button(f"{wd[d.weekday()]} {d.strftime('%d.%m')}", color=VkKeyboardColor.SECONDARY)
|
||||||
|
keyboard.add_line()
|
||||||
|
keyboard.add_button("Отменить заполнение ❌", color=VkKeyboardColor.NEGATIVE)
|
||||||
|
return keyboard.get_keyboard()
|
||||||
|
|
||||||
|
|
||||||
|
def get_calltime_keyboard() -> str:
|
||||||
|
"""Клавиатура выбора ВРЕМЕНИ (слоты в рабочие часы)."""
|
||||||
|
keyboard = VkKeyboard(one_time=False)
|
||||||
|
keyboard.add_button("10:00", color=VkKeyboardColor.SECONDARY)
|
||||||
|
keyboard.add_button("12:00", color=VkKeyboardColor.SECONDARY)
|
||||||
|
keyboard.add_button("14:00", color=VkKeyboardColor.SECONDARY)
|
||||||
|
keyboard.add_line()
|
||||||
|
keyboard.add_button("16:00", color=VkKeyboardColor.SECONDARY)
|
||||||
|
keyboard.add_button("18:00", color=VkKeyboardColor.SECONDARY)
|
||||||
|
keyboard.add_button("20:00", color=VkKeyboardColor.SECONDARY)
|
||||||
|
keyboard.add_line()
|
||||||
|
keyboard.add_button("Любое время", color=VkKeyboardColor.PRIMARY)
|
||||||
|
keyboard.add_button("Отменить заполнение ❌", color=VkKeyboardColor.NEGATIVE)
|
||||||
|
return keyboard.get_keyboard()
|
||||||
|
|
||||||
|
|
||||||
|
def get_marketing_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_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.SECONDARY)
|
||||||
|
keyboard.add_line()
|
||||||
|
keyboard.add_button("Назад к проверке ↩️", color=VkKeyboardColor.PRIMARY)
|
||||||
|
return keyboard.get_keyboard()
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""Валидация и форматирование пользовательского ввода."""
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
# Регулярные выражения для валидации
|
||||||
|
# Российский номер для нормализации 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()))
|
||||||
Reference in New Issue
Block a user