Команды и меню при старте
Проверяю getMyCommands, обновляю список при расхождении и включаю MenuButtonCommands. Ошибки настройки не блокируют запуск бота.
This commit is contained in:
parent
37a954c9fc
commit
bb096dec47
|
|
@ -205,6 +205,7 @@ TELEGRAM_HEALTH_INTERVAL=60
|
||||||
- `/invite` — реферальная ссылка.
|
- `/invite` — реферальная ссылка.
|
||||||
- `/restart` — сбросить контекст и начать новую сцену (алиас `/reset`).
|
- `/restart` — сбросить контекст и начать новую сцену (алиас `/reset`).
|
||||||
- `/settings` — выбор модели Krea2: стандартная Q4 или улучшенная Q6.
|
- `/settings` — выбор модели Krea2: стандартная Q4 или улучшенная Q6.
|
||||||
|
- При старте бот проверяет `setMyCommands` и включает кнопку меню с командами.
|
||||||
- За первую оплату приглашённого друга начисляется +3 генерации, реферер получает уведомление.
|
- За первую оплату приглашённого друга начисляется +3 генерации, реферер получает уведомление.
|
||||||
|
|
||||||
Баланс и рефералы хранятся в таблице `users`. Пропущенные дни начисляются при следующем запуске/полуночи.
|
Баланс и рефералы хранятся в таблице `users`. Пропущенные дни начисляются при следующем запуске/полуночи.
|
||||||
|
|
|
||||||
50
bot.py
50
bot.py
|
|
@ -11,11 +11,13 @@ from pathlib import Path
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from telegram import (
|
from telegram import (
|
||||||
|
BotCommand,
|
||||||
CopyTextButton,
|
CopyTextButton,
|
||||||
InlineKeyboardButton,
|
InlineKeyboardButton,
|
||||||
InlineKeyboardMarkup,
|
InlineKeyboardMarkup,
|
||||||
InputFile,
|
InputFile,
|
||||||
LabeledPrice,
|
LabeledPrice,
|
||||||
|
MenuButtonCommands,
|
||||||
Update,
|
Update,
|
||||||
)
|
)
|
||||||
from telegram.constants import ChatAction, ChatType
|
from telegram.constants import ChatAction, ChatType
|
||||||
|
|
@ -47,6 +49,19 @@ RESTART_HINT = "/restart — сброс контекста"
|
||||||
CAPTION_LIMIT = 1024
|
CAPTION_LIMIT = 1024
|
||||||
|
|
||||||
|
|
||||||
|
def commands_need_update(
|
||||||
|
current: tuple[BotCommand, ...] | list[BotCommand],
|
||||||
|
desired: tuple[BotCommand, ...] | list[BotCommand],
|
||||||
|
) -> bool:
|
||||||
|
current_pairs = [
|
||||||
|
(command.command, command.description) for command in current
|
||||||
|
]
|
||||||
|
desired_pairs = [
|
||||||
|
(command.command, command.description) for command in desired
|
||||||
|
]
|
||||||
|
return current_pairs != desired_pairs
|
||||||
|
|
||||||
|
|
||||||
def result_caption(prompt: str, *, limit: int = CAPTION_LIMIT) -> str:
|
def result_caption(prompt: str, *, limit: int = CAPTION_LIMIT) -> str:
|
||||||
value = (prompt or "").strip()
|
value = (prompt or "").strip()
|
||||||
suffix = f"\n\n{RESTART_HINT}"
|
suffix = f"\n\n{RESTART_HINT}"
|
||||||
|
|
@ -80,6 +95,15 @@ START_TEXT = (
|
||||||
"/restart — сброс контекста.\n"
|
"/restart — сброс контекста.\n"
|
||||||
"/settings — качество Krea2."
|
"/settings — качество Krea2."
|
||||||
)
|
)
|
||||||
|
BOT_COMMANDS = (
|
||||||
|
BotCommand("start", "Запуск и баланс"),
|
||||||
|
BotCommand("help", "Инструкция"),
|
||||||
|
BotCommand("balance", "Баланс генераций"),
|
||||||
|
BotCommand("invite", "Пригласить друга"),
|
||||||
|
BotCommand("status", "Статус текущей задачи"),
|
||||||
|
BotCommand("settings", "Качество Krea2"),
|
||||||
|
BotCommand("restart", "Сброс контекста"),
|
||||||
|
)
|
||||||
BLOCKED_TEXT = "Запрос нарушает правила. Звёзды не списаны."
|
BLOCKED_TEXT = "Запрос нарушает правила. Звёзды не списаны."
|
||||||
AGE_TEXT = (
|
AGE_TEXT = (
|
||||||
"🔞 Бот доступен только пользователям 18+.\n\n"
|
"🔞 Бот доступен только пользователям 18+.\n\n"
|
||||||
|
|
@ -170,6 +194,7 @@ class MpakoBot:
|
||||||
|
|
||||||
async def _post_init(self, application: Application) -> None:
|
async def _post_init(self, application: Application) -> None:
|
||||||
self._bot = application.bot
|
self._bot = application.bot
|
||||||
|
await self._setup_commands()
|
||||||
self._settings.storage_dir.mkdir(parents=True, exist_ok=True)
|
self._settings.storage_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self._workflows = WorkflowBuilder(
|
self._workflows = WorkflowBuilder(
|
||||||
self._settings.workflow_t2i,
|
self._settings.workflow_t2i,
|
||||||
|
|
@ -228,6 +253,31 @@ class MpakoBot:
|
||||||
await self._db.close()
|
await self._db.close()
|
||||||
logger.info("Бот остановлен")
|
logger.info("Бот остановлен")
|
||||||
|
|
||||||
|
async def _setup_commands(self) -> None:
|
||||||
|
if self._bot is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
current = await self._retry(
|
||||||
|
self._bot.get_my_commands, attempts=3
|
||||||
|
)
|
||||||
|
if commands_need_update(current, BOT_COMMANDS):
|
||||||
|
await self._retry(
|
||||||
|
self._bot.set_my_commands, BOT_COMMANDS, attempts=3
|
||||||
|
)
|
||||||
|
logger.info("Команды бота обновлены")
|
||||||
|
menu = await self._retry(
|
||||||
|
self._bot.get_chat_menu_button, attempts=3
|
||||||
|
)
|
||||||
|
if not isinstance(menu, MenuButtonCommands):
|
||||||
|
await self._retry(
|
||||||
|
self._bot.set_chat_menu_button,
|
||||||
|
menu_button=MenuButtonCommands(),
|
||||||
|
attempts=3,
|
||||||
|
)
|
||||||
|
logger.info("Кнопка меню с командами включена")
|
||||||
|
except TelegramError:
|
||||||
|
logger.exception("Не удалось настроить команды бота")
|
||||||
|
|
||||||
async def _track_update(
|
async def _track_update(
|
||||||
self, update: Update, context: ContextTypes.DEFAULT_TYPE
|
self, update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
||||||
12
checklist.md
12
checklist.md
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
- Реализованы блоки 2–7, базовое хранилище (9), файлы Docker (10), тесты (11), README (12), веб-история (13), улучшение промптов (14), модерация (15), оплата Stars (16), баланс/рефералы (17), управление пользователями (18), возрастной gate (19) и надёжность Telegram (20).
|
- Реализованы блоки 2–7, базовое хранилище (9), файлы Docker (10), тесты (11), README (12), веб-история (13), улучшение промптов (14), модерация (15), оплата Stars (16), баланс/рефералы (17), управление пользователями (18), возрастной gate (19) и надёжность Telegram (20).
|
||||||
- Вживую проверены: T2I (Z-Image), I2I (Krea2), WebSocket-прогресс, улучшение промптов, Docker, CI и веб-история.
|
- Вживую проверены: T2I (Z-Image), I2I (Krea2), WebSocket-прогресс, улучшение промптов, Docker, CI и веб-история.
|
||||||
- 75 тестов: 47 проходят локально без БД, 28 тестов БД выполняются в CI на PostgreSQL через `TEST_DATABASE_URL`. Покрыты workflows, очередь, веб-история, enhancer, модерация, платежи, премиум-очередь, баланс/рефералы, управление пользователями, retry, миграция, watchdog, контекст, админ-панель, скрытие истории, настройки Krea2.
|
- 78 тестов: 50 проходят локально без БД, 28 тестов БД выполняются в CI на PostgreSQL через `TEST_DATABASE_URL`. Покрыты workflows, очередь, веб-история, enhancer, модерация, платежи, премиум-очередь, баланс/рефералы, управление пользователями, retry, миграция, watchdog, контекст, админ-панель, скрытие истории, настройки Krea2.
|
||||||
- Не проверено: реальная оплата Stars и реферальная выплата в Telegram, очередь из 3+ пользователей, падение ComfyUI, рестарт с `running`-задачей, детекция лица на реальном фото. PostgreSQL-миграция и тесты БД ещё не запускались вживую — это сделает CI и первый деплой.
|
- Не проверено: реальная оплата Stars и реферальная выплата в Telegram, очередь из 3+ пользователей, падение ComfyUI, рестарт с `running`-задачей, детекция лица на реальном фото. PostgreSQL-миграция и тесты БД ещё не запускались вживую — это сделает CI и первый деплой.
|
||||||
|
|
||||||
## 0. Допущения и решения по умолчанию
|
## 0. Допущения и решения по умолчанию
|
||||||
|
|
@ -416,6 +416,16 @@ ON requests(user_id) WHERE status IN ('queued', 'running');
|
||||||
|
|
||||||
**DoD:** выбор сохраняется между запросами, стандартная использует Q4, улучшенная — Q6.
|
**DoD:** выбор сохраняется между запросами, стандартная использует Q4, улучшенная — Q6.
|
||||||
|
|
||||||
|
## 24. Команды и меню
|
||||||
|
|
||||||
|
- [x] При старте бот получает `getMyCommands` и обновляет список при расхождении.
|
||||||
|
- [x] `setMyCommands` для `/start`, `/help`, `/balance`, `/invite`, `/status`, `/settings`, `/restart`.
|
||||||
|
- [x] `setChatMenuButton` включает `MenuButtonCommands` — кнопка меню слева снизу.
|
||||||
|
- [x] Ошибки настройки команд не блокируют запуск бота.
|
||||||
|
- [x] Тесты сравнения команд и идемпотентной настройки.
|
||||||
|
|
||||||
|
**DoD:** после старта в меню бота видны команды, повторный запуск не дублирует настройку.
|
||||||
|
|
||||||
## Порядок работ
|
## Порядок работ
|
||||||
|
|
||||||
1. Блок 1 (подготовка) — без него нельзя валидировать клиент и workflow.
|
1. Блок 1 (подготовка) — без него нельзя валидировать клиент и workflow.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,52 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from bot import MpakoBot
|
from telegram import BotCommand, MenuButtonCommands, MenuButtonDefault
|
||||||
|
|
||||||
|
from bot import BOT_COMMANDS, MpakoBot, commands_need_update
|
||||||
from config import Settings
|
from config import Settings
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCommandsBot:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.commands: tuple[BotCommand, ...] = ()
|
||||||
|
self.menu = MenuButtonDefault()
|
||||||
|
self.set_commands_calls = 0
|
||||||
|
|
||||||
|
async def get_my_commands(self):
|
||||||
|
return self.commands
|
||||||
|
|
||||||
|
async def set_my_commands(self, commands, **kwargs):
|
||||||
|
self.commands = tuple(commands)
|
||||||
|
self.set_commands_calls += 1
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def get_chat_menu_button(self):
|
||||||
|
return self.menu
|
||||||
|
|
||||||
|
async def set_chat_menu_button(self, menu_button=None, **kwargs):
|
||||||
|
self.menu = menu_button
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_commands_need_update() -> None:
|
||||||
|
assert commands_need_update(BOT_COMMANDS, BOT_COMMANDS) is False
|
||||||
|
assert commands_need_update((), BOT_COMMANDS) is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_commands_sets_commands_and_menu_once() -> None:
|
||||||
|
bot = MpakoBot(Settings(bot_token=""))
|
||||||
|
fake = FakeCommandsBot()
|
||||||
|
bot._bot = fake # type: ignore[assignment]
|
||||||
|
|
||||||
|
await bot._setup_commands()
|
||||||
|
assert fake.commands == BOT_COMMANDS
|
||||||
|
assert isinstance(fake.menu, MenuButtonCommands)
|
||||||
|
assert fake.set_commands_calls == 1
|
||||||
|
|
||||||
|
await bot._setup_commands()
|
||||||
|
assert fake.set_commands_calls == 1
|
||||||
|
|
||||||
|
|
||||||
def test_settings_command_is_not_shadowed_by_settings_attribute() -> None:
|
def test_settings_command_is_not_shadowed_by_settings_attribute() -> None:
|
||||||
bot = MpakoBot(Settings(bot_token=""))
|
bot = MpakoBot(Settings(bot_token=""))
|
||||||
assert callable(bot._settings_command)
|
assert callable(bot._settings_command)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue