d2ca750a2d
1) Кнопки меню: раздел «Кнопки меню 🧩» в админке — показать/скрыть любую кнопку главного меню (Подобрать/Интенсив/Цены/Вопрос/Вебинар). Флаги в БД (bot_flags), дефолты в config.MENU_FEATURES (вебинар по умолчанию скрыт). Скрытый раздел не реагирует и на кодовые слова (интенсив выкл → «лето» игнорируется). Вебинар получил кнопку в меню (для теста ветки) + вход по callback из поста работает всегда. 2) Экстренное отключение (только супер-админ): /favi-stop → запрос пароля → бот уходит в аварийный режим и молчит для всех; /favi-start возвращает. Пароль хранится как PBKDF2-хэш в .env (FAVI_STOP_HASH), не в коде; сравнение константное, 3 попытки. pytest 43 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
200 lines
12 KiB
Python
200 lines
12 KiB
Python
import os
|
|
|
|
# Load environment variables from .env in the project root
|
|
def _load_env():
|
|
# Find the project root (the directory containing config.py)
|
|
root_dir = os.path.dirname(os.path.abspath(__file__))
|
|
dotenv_path = os.path.join(root_dir, ".env")
|
|
if os.path.exists(dotenv_path):
|
|
try:
|
|
with open(dotenv_path, "r", encoding="utf-8") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
if "=" in line:
|
|
key, val = line.split("=", 1)
|
|
val = val.strip().strip('"').strip("'")
|
|
# Не перетираем переменные, уже заданные в реальном окружении
|
|
# (на проде токены приходят из env, .env — лишь фолбэк).
|
|
os.environ.setdefault(key.strip(), val)
|
|
except Exception as e:
|
|
print(f"Warning: Failed to load .env file: {e}")
|
|
|
|
_load_env()
|
|
|
|
# VK API Configuration
|
|
# In production, these should be loaded from environment variables or a secure .env file.
|
|
VK_TOKEN = os.getenv("VK_TOKEN", "your_vk_community_token_here")
|
|
GROUP_ID = int(os.getenv("VK_GROUP_ID", "0")) # Community ID (must be integer)
|
|
|
|
# Operator / Admin configuration
|
|
# ID of the user or chat where notifications about new leads and operator requests will be sent
|
|
OPERATOR_PEER_ID = int(os.getenv("OPERATOR_PEER_ID", "12345678")) # Change to actual VK user ID of operator
|
|
|
|
# Parse ADMIN_IDS as a list of integers from comma-separated env values
|
|
admin_ids_raw = os.getenv("ADMIN_IDS")
|
|
if admin_ids_raw:
|
|
try:
|
|
ADMIN_IDS = [int(x.strip()) for x in admin_ids_raw.split(",") if x.strip().isdigit()]
|
|
except Exception:
|
|
ADMIN_IDS = [OPERATOR_PEER_ID]
|
|
else:
|
|
ADMIN_IDS = [OPERATOR_PEER_ID]
|
|
|
|
# ============================================================
|
|
# СУПЕР-АДМИН. Меняется ТОЛЬКО ЗДЕСЬ, в коде (не через .env, не через бота).
|
|
# Супер-админ может добавлять/удалять обычных администраторов прямо из бота.
|
|
# Чтобы сменить/добавить супер-админа — впишите сюда VK ID (числом):
|
|
SUPER_ADMIN_IDS = [350793273]
|
|
# ============================================================
|
|
|
|
# Экстренное отключение бота супер-админом командой /favi-stop.
|
|
# Хранится ТОЛЬКО хэш пароля (PBKDF2), не сам пароль. Задаётся в .env: FAVI_STOP_HASH.
|
|
# Сгенерировать новый хэш: см. README/USER_GUIDE. Пусто — команда отключена.
|
|
FAVI_STOP_HASH = os.getenv("FAVI_STOP_HASH", "")
|
|
|
|
# Ссылки на юридические документы (инлайн-кнопки в боте)
|
|
OFFER_URL = os.getenv("OFFER_URL", "https://infocyber.pro/form/Dogovor_oferty_kibersport_versia_izmenennaya.pdf")
|
|
POLICY_URL = os.getenv("POLICY_URL", "https://infocyber.pro/form/Политика%20ПД.pdf")
|
|
|
|
# Несколько операторов: уведомления о заявках/откликах/вызовах уходят ВСЕМ из списка.
|
|
# По умолчанию = один OPERATOR_PEER_ID. Задать через .env: OPERATOR_PEER_IDS=111,222,333
|
|
_op_ids_raw = os.getenv("OPERATOR_PEER_IDS")
|
|
if _op_ids_raw:
|
|
OPERATOR_PEER_IDS = [int(x.strip()) for x in _op_ids_raw.split(",") if x.strip().lstrip("-").isdigit()]
|
|
if not OPERATOR_PEER_IDS:
|
|
OPERATOR_PEER_IDS = [OPERATOR_PEER_ID]
|
|
else:
|
|
OPERATOR_PEER_IDS = [OPERATOR_PEER_ID]
|
|
|
|
# Часы работы (для статуса оператора). Вне этого интервала — «ответим утром».
|
|
WORK_HOUR_START = int(os.getenv("WORK_HOUR_START", "9"))
|
|
WORK_HOUR_END = int(os.getenv("WORK_HOUR_END", "21"))
|
|
|
|
# Разрешённый источник (Origin) для CORS веб-формы. '*' допустим только для локальной отладки.
|
|
ALLOWED_ORIGIN = os.getenv("ALLOWED_ORIGIN", "https://infocyber.pro")
|
|
|
|
# Промокоды на скидку. Пока ВЫКЛЮЧЕНЫ — включить можно, задав PROMO_ENABLED=true в .env.
|
|
# Когда выключено: бот и форма не показывают промокод, в уведомлениях/письмах его нет.
|
|
PROMO_ENABLED = os.getenv("PROMO_ENABLED", "false").strip().lower() in ("1", "true", "yes")
|
|
|
|
# Email (SMTP) configuration для уведомлений о заявках
|
|
SMTP_HOST = os.getenv("SMTP_HOST", "")
|
|
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
|
|
SMTP_USER = os.getenv("SMTP_USER", "")
|
|
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "")
|
|
MAIL_FROM = os.getenv("MAIL_FROM", "") or SMTP_USER
|
|
# Куда отправлять уведомления о новых заявках (можно несколько через запятую)
|
|
MAIL_TO = os.getenv("MAIL_TO", "group-zakaz@infocyber.pro")
|
|
# Куда отправлять отклики на вакансии / набор в состав.
|
|
# TODO: пока совпадает с MAIL_TO; возможно вынести на отдельный HR-адрес (напр. hr@infocyber.pro) — задать HR_MAIL_TO в .env.
|
|
HR_MAIL_TO = os.getenv("HR_MAIL_TO", "") or MAIL_TO
|
|
# True — порт 465 (SSL), False — STARTTLS (обычно порт 587)
|
|
SMTP_USE_SSL = os.getenv("SMTP_USE_SSL", "true").strip().lower() in ("1", "true", "yes")
|
|
|
|
# ============================================================
|
|
# КИБЕРШКОЛА БАЗА — бренд, цены, ссылки, триггеры (по ТЗ)
|
|
# Всё редактируемое собрано здесь, чтобы менять без правки логики.
|
|
# ============================================================
|
|
BRAND = "Кибершкола БАЗА"
|
|
|
|
# --- Ссылки на оплату/запись (ПЛЕЙСХОЛДЕРЫ — заменить на реальные) ---
|
|
# UTM добавляются автоматически там, где ссылка ведёт наружу.
|
|
UTM_MAIN = "utm_source=vk&utm_medium=bot&utm_campaign=main_bot"
|
|
UTM_SUMMER = "utm_source=vk&utm_medium=bot&utm_campaign=summer_intensive"
|
|
LINKS = {
|
|
"trial": os.getenv("LINK_TRIAL", "https://infocyber.pro/"), # запись на пробное
|
|
"tariffs": os.getenv("LINK_TARIFFS", "https://infocyber.pro/"), # страница с тарифами
|
|
"group_4": os.getenv("LINK_GROUP_4", "https://infocyber.pro/"), # групповой абонемент 4
|
|
"group_8": os.getenv("LINK_GROUP_8", "https://infocyber.pro/"), # групповой абонемент 8
|
|
"individual_1": os.getenv("LINK_IND_1", "https://infocyber.pro/"), # индивидуальная 1
|
|
"individual_4": os.getenv("LINK_IND_4", "https://infocyber.pro/"), # индивидуальные 4
|
|
"individual_8": os.getenv("LINK_IND_8", "https://infocyber.pro/"), # индивидуальные 8
|
|
"summer": os.getenv("LINK_SUMMER", "https://infocyber.pro/"), # оплата летнего интенсива
|
|
}
|
|
|
|
# Человеческие названия ссылок (для админки смены ссылок). Порядок = порядок показа.
|
|
LINK_LABELS = {
|
|
"trial": "Запись на пробное",
|
|
"tariffs": "Страница тарифов",
|
|
"group_4": "Групповой абонемент — 4 занятия",
|
|
"group_8": "Групповой абонемент — 8 занятий",
|
|
"individual_1": "Индивидуальное — 1 занятие",
|
|
"individual_4": "Индивидуальные — 4 занятия",
|
|
"individual_8": "Индивидуальные — 8 занятий",
|
|
"summer": "Летний интенсив CS2",
|
|
}
|
|
|
|
# --- Кнопки/разделы главного меню: показать/скрыть (админка) ---
|
|
# key: (человеческое название, показывать ли по умолчанию)
|
|
MENU_FEATURES = {
|
|
"pickup": ("Подобрать тренировку 🎯", True),
|
|
"intensive": ("Летний интенсив CS2 🔥", True),
|
|
"prices": ("Посмотреть цены 💰", True),
|
|
"question": ("Задать вопрос 💬", True),
|
|
"webinar": ("Записаться на вебинар 🎓", False),
|
|
}
|
|
|
|
# --- Триггеры запуска (регистронезависимо) ---
|
|
MAIN_TRIGGERS = ["начать", "тренировки", "занятия", "цена", "стоимость", "записаться",
|
|
"привет", "здравствуйте", "старт", "меню"]
|
|
SUMMER_TRIGGERS = ["лето", "интенсив", "летний интенсив", "интенсив cs2", "хочу на интенсив"]
|
|
|
|
# --- Вебинар «Точка входа в киберспорт» ---
|
|
WEBINAR_TITLE = "Точка входа в киберспорт"
|
|
WEBINAR_DATE = "30 июля"
|
|
WEBINAR_TIME = "19:00"
|
|
WEBINAR_PAYLOAD = "webinar_register"
|
|
|
|
# --- Тексты (по ТЗ, редактируемые) ---
|
|
GREETING_BAZA = (
|
|
"Привет! Это Кибершкола БАЗА 🎮\n\n"
|
|
"Мы проводим онлайн-тренировки по Counter-Strike 2, Dota 2 и World of Tanks для подростков 12+.\n"
|
|
"Помогу подобрать подходящий формат, посмотреть цены или записаться на тренировку."
|
|
)
|
|
|
|
PRICES_TEXT = (
|
|
"Стоимость тренировок в Кибершколе БАЗА:\n\n"
|
|
"🧑🤝🧑 Групповые тренировки:\n"
|
|
"• 4 занятия — 2 790 ₽\n"
|
|
"• 8 занятий — 4 990 ₽\n\n"
|
|
"👤 Индивидуальные тренировки:\n"
|
|
"• 1 занятие — 870 ₽\n"
|
|
"• 4 занятия — 3 290 ₽\n"
|
|
"• 8 занятий — 5 990 ₽\n\n"
|
|
"Все занятия проходят онлайн с тренером."
|
|
)
|
|
|
|
INTENSIVE_PRICE = "1 990 ₽"
|
|
INTENSIVE_INFO = (
|
|
"Привет! Вы написали кодовое слово «лето» — рассказываю про летний интенсив по Counter-Strike 2 🔥\n\n"
|
|
"Это 4 онлайн-тренировки по 90 минут с тренером.\n"
|
|
"На интенсиве участники будут:\n"
|
|
"— разбирать роли и командную игру;\n"
|
|
"— тренировать аим, дуэли и контрстрейфы;\n"
|
|
"— разбирать игровые моменты и демозаписи;\n"
|
|
"— изучать гранаты, мувмент и полезные игровые приёмы.\n\n"
|
|
f"Стоимость участия — {INTENSIVE_PRICE} за все 4 тренировки."
|
|
)
|
|
|
|
# Доступные услуги (направления) — используются для сидирования БД (services)
|
|
SERVICES = {
|
|
"1": {
|
|
"name": "Counter-Strike 2 🔫",
|
|
"price": "от 870 ₽ / занятие",
|
|
"description": "Тренировки по Counter-Strike 2: aim, раскидки гранат, командная тактика. Разовое занятие 870 ₽, абонементы от 2 790 ₽."
|
|
},
|
|
"2": {
|
|
"name": "Dota 2 🎮",
|
|
"price": "от 870 ₽ / занятие",
|
|
"description": "Тренировки по Dota 2: разбор реплеев, позиционка, фарм и тимплей. Разовое занятие 870 ₽, абонементы от 2 790 ₽."
|
|
},
|
|
"3": {
|
|
"name": "World of Tanks 🛡️",
|
|
"price": "от 870 ₽ / занятие",
|
|
"description": "Тренировки по World of Tanks: позиционирование, сведение, тактика боёв. Разовое занятие 870 ₽, абонементы от 2 790 ₽."
|
|
}
|
|
}
|
|
|