Files
bot_vk_ikp_prodagi/config.py
T
Kenik d2ca750a2d Админка: показ/скрытие кнопок меню + экстренный стоп бота (/favi-stop)
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>
2026-07-26 10:58:54 +03:00

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 ₽."
}
}