Админка: показ/скрытие кнопок меню + экстренный стоп бота (/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>
This commit is contained in:
Kenik
2026-07-26 10:58:54 +03:00
parent 24daec1c09
commit d2ca750a2d
6 changed files with 279 additions and 36 deletions
+49
View File
@@ -88,6 +88,7 @@ def test_baza_question_ticket(app):
def test_webinar_registration(app):
"""Ветка вебинара: регистрация, метка, остаётся в боте (меню доступно)."""
u = 70041
bot.db.set_flag("webinar", True) # вебинар по умолчанию скрыт — включаем
app.handle_message(u, "записаться на вебинар")
assert "webinar_registered" in bot.db.get_profile(u).get("metki", [])
assert _state(u) == "FREE_CHAT"
@@ -247,6 +248,54 @@ def test_resolve_vk_links(app):
assert app._resolve_vk_user_id("999") == 999
def test_menu_feature_toggle(app):
"""Скрытие кнопки меню: раздел «интенсив» выключается и не запускается по слову."""
import json
a = _admin()
# выключаем интенсив
app.handle_message(a, "Кнопки меню 🧩")
app.handle_message(a, "Летний интенсив CS2 🔥 — вкл ✅") # нажатие -> выкл
assert bot.db.get_flag("intensive", True) is False
# кнопки нет в меню
labels = [b["action"].get("label") for row in json.loads(app._menu_kb(a))["buttons"] for b in row]
assert not any("Летний интенсив" in x for x in labels)
# кодовое слово «лето» больше не запускает интенсив
u = 70050
app.handle_message(u, "лето")
assert _state(u) == "FREE_CHAT"
def test_favi_stop_start(app):
"""Экстренное отключение супер-админом по паролю и обратное включение."""
import config as cfg
# тестовый хэш пароля 'secret'
import hashlib, os
salt = os.urandom(16)
cfg.FAVI_STOP_HASH = "pbkdf2_sha256$240000$" + salt.hex() + "$" + \
hashlib.pbkdf2_hmac("sha256", b"secret", salt, 240000).hex()
a = bot.config.SUPER_ADMIN_IDS[0]
app.handle_message(a, "/favi-stop")
app.handle_message(a, "secret")
assert bot.db.get_flag("bot_disabled", False) is True
# обычный юзер не получает ответа
app._sent.clear(); app.handle_message(70051, "привет")
assert len(app._sent) == 0
# включаем обратно
app.handle_message(a, "/favi-start"); app.handle_message(a, "secret")
assert bot.db.get_flag("bot_disabled", False) is False
def test_favi_wrong_password(app):
import config as cfg, hashlib, os
salt = os.urandom(16)
cfg.FAVI_STOP_HASH = "pbkdf2_sha256$240000$" + salt.hex() + "$" + \
hashlib.pbkdf2_hmac("sha256", b"secret", salt, 240000).hex()
a = bot.config.SUPER_ADMIN_IDS[0]
app.handle_message(a, "/favi-stop")
app.handle_message(a, "wrong")
assert bot.db.get_flag("bot_disabled", False) is False # не отключился
def test_admin_mute_notifications(app):
"""Админ может отключить себе уведомления от клиентов — _notify_operators его пропускает."""
op = _admin()