В меню Telegram появились команды с русскими подписями
Telegram не принимает кириллицу в именах команд, поэтому в меню теперь латинские алиасы (/help, /info, /nick, /list, /who, /purify, /eggs, /shop, /top, /sub, /unsub) с русскими описаниями, а русские команды продолжают работать при вводе вручную. При каждом старте бот сверяет набор с ожидаемым и обновляет его, если меню меняли вручную или оно пустое.
This commit is contained in:
parent
0ae337711e
commit
64cf24d949
|
|
@ -158,7 +158,7 @@ make backup E=test # на домашнем сервере: ./scripts/clickho
|
|||
|
||||
## Версионирование
|
||||
|
||||
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.5.1`.
|
||||
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.5.2`.
|
||||
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
|
||||
- Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3).
|
||||
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.
|
||||
|
|
@ -207,6 +207,11 @@ git push origin master --tags
|
|||
`/ник`, `/список`, `/кто`, `/инфо`, `/помощь`, `/очищение`, `/яички`,
|
||||
`/магазин`, `/рейтинг`, `/отписка`, `/подписка`.
|
||||
|
||||
В меню Telegram показываются латинские алиасы (`/help`, `/info`, `/nick`, `/list`,
|
||||
`/who`, `/purify`, `/eggs`, `/shop`, `/top`, `/sub`, `/unsub`) с русскими описаниями:
|
||||
Telegram не принимает кириллицу в именах команд. Русские команды работают при вводе
|
||||
вручную. Набор сверяется с ожидаемым при каждом старте бота (`ensure_commands`).
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
- общие — `.env.example` (включая `CLICKHOUSE_*` и параметры батчинга `ANALYTICS_*`);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import asyncio
|
|||
import logging
|
||||
|
||||
from bot.bot_factory import create_bot, create_dispatcher
|
||||
from bot.commands_menu import ensure_commands
|
||||
from bot.services.heartbeat import run_heartbeat
|
||||
from bot.services.notifications import run_notifications
|
||||
from bot.services.runtime import uptime_seconds
|
||||
|
|
@ -30,6 +31,9 @@ async def main() -> None:
|
|||
bot = create_bot()
|
||||
dp = create_dispatcher()
|
||||
|
||||
# Сверяем меню команд при каждом старте (Telegram разрешает только ASCII-имена).
|
||||
await ensure_commands(bot)
|
||||
|
||||
notifications_task = asyncio.create_task(run_notifications(bot))
|
||||
heartbeat_task = asyncio.create_task(run_heartbeat(bot))
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
"""Меню команд бота: латинские имена + русские описания.
|
||||
|
||||
Telegram разрешает в меню только ``a-z``, ``0-9`` и ``_``, поэтому русские
|
||||
команды остаются доступны при вводе вручную, а в меню показываются алиасы.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.types import BotCommand
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#: Ожидаемый набор команд в меню (порядок = порядок в Telegram).
|
||||
BOT_COMMANDS: tuple[BotCommand, ...] = (
|
||||
BotCommand(command="help", description="Помощь"),
|
||||
BotCommand(command="info", description="Инфо"),
|
||||
BotCommand(command="nick", description="Сменить ник"),
|
||||
BotCommand(command="list", description="Список игроков"),
|
||||
BotCommand(command="who", description="Кто это"),
|
||||
BotCommand(command="purify", description="Очищение"),
|
||||
BotCommand(command="eggs", description="Яички"),
|
||||
BotCommand(command="shop", description="Магазин"),
|
||||
BotCommand(command="top", description="Рейтинг"),
|
||||
BotCommand(command="sub", description="Подписка"),
|
||||
BotCommand(command="unsub", description="Отписка"),
|
||||
)
|
||||
|
||||
|
||||
def _key(commands: list[BotCommand]) -> list[tuple[str, str]]:
|
||||
return [(command.command, command.description) for command in commands]
|
||||
|
||||
|
||||
async def ensure_commands(bot: Bot) -> bool:
|
||||
"""Сверить меню команд с ожидаемым и обновить при расхождении.
|
||||
|
||||
Возвращает ``True``, если набор пришлось обновить. Ошибки Telegram не
|
||||
прерывают запуск бота — только пишутся в лог.
|
||||
"""
|
||||
try:
|
||||
current = await bot.get_my_commands()
|
||||
except Exception:
|
||||
logger.warning("Не удалось прочитать меню команд Telegram", exc_info=True)
|
||||
return False
|
||||
|
||||
if _key(current) == _key(list(BOT_COMMANDS)):
|
||||
logger.info("Меню команд актуально (%s команд)", len(BOT_COMMANDS))
|
||||
return False
|
||||
|
||||
try:
|
||||
await bot.set_my_commands(list(BOT_COMMANDS))
|
||||
except Exception:
|
||||
logger.warning("Не удалось обновить меню команд Telegram", exc_info=True)
|
||||
return False
|
||||
|
||||
logger.info("Меню команд обновлено (%s команд)", len(BOT_COMMANDS))
|
||||
return True
|
||||
|
|
@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
|
|||
router = Router(name="commands")
|
||||
|
||||
|
||||
@router.message(Command("ник", ignore_case=True))
|
||||
@router.message(Command("ник", "nick", ignore_case=True))
|
||||
async def set_nick_handler(message: Message) -> None:
|
||||
if not await access.check_user(message):
|
||||
return
|
||||
|
|
@ -41,7 +41,7 @@ async def set_nick_handler(message: Message) -> None:
|
|||
await message.reply(system.MSG_NICK_ERROR)
|
||||
|
||||
|
||||
@router.message(Command("список", ignore_case=True))
|
||||
@router.message(Command("список", "list", ignore_case=True))
|
||||
async def list_nicks_handler(message: Message) -> None:
|
||||
if not await access.check_user(message):
|
||||
return
|
||||
|
|
@ -66,7 +66,7 @@ async def list_nicks_handler(message: Message) -> None:
|
|||
await message.answer(render(await get_text("economy.nick_list"), context))
|
||||
|
||||
|
||||
@router.message(Command("кто", ignore_case=True))
|
||||
@router.message(Command("кто", "who", ignore_case=True))
|
||||
async def get_nick_handler(message: Message) -> None:
|
||||
if not await access.check_user(message):
|
||||
return
|
||||
|
|
@ -81,14 +81,14 @@ async def get_nick_handler(message: Message) -> None:
|
|||
await message.reply(user_info.nick)
|
||||
|
||||
|
||||
@router.message(Command("помощь", ignore_case=True))
|
||||
@router.message(Command("помощь", "help", ignore_case=True))
|
||||
async def help_handler(message: Message) -> None:
|
||||
if not await access.check_user(message):
|
||||
return
|
||||
await message.answer(await get_text("info.help"))
|
||||
|
||||
|
||||
@router.message(Command("инфо", ignore_case=True))
|
||||
@router.message(Command("инфо", "info", ignore_case=True))
|
||||
async def info_handler(message: Message) -> None:
|
||||
if not await access.check_user(message):
|
||||
return
|
||||
|
|
@ -116,7 +116,7 @@ async def info_handler(message: Message) -> None:
|
|||
await message.answer(render(await get_text("info.profile"), context))
|
||||
|
||||
|
||||
@router.message(Command("рейтинг", ignore_case=True))
|
||||
@router.message(Command("рейтинг", "top", ignore_case=True))
|
||||
async def top_handler(message: Message) -> None:
|
||||
if not await access.check_user(message):
|
||||
return
|
||||
|
|
@ -125,7 +125,7 @@ async def top_handler(message: Message) -> None:
|
|||
await message.answer(render(await get_text("economy.top"), context))
|
||||
|
||||
|
||||
@router.message(Command("отписка", ignore_case=True))
|
||||
@router.message(Command("отписка", "unsub", ignore_case=True))
|
||||
async def unsubscribe_handler(message: Message) -> None:
|
||||
if not await access.check_user(message):
|
||||
return
|
||||
|
|
@ -133,7 +133,7 @@ async def unsubscribe_handler(message: Message) -> None:
|
|||
await message.reply(system.MSG_UNSUBSCRIBED)
|
||||
|
||||
|
||||
@router.message(Command("подписка", ignore_case=True))
|
||||
@router.message(Command("подписка", "sub", ignore_case=True))
|
||||
async def subscribe_handler(message: Message) -> None:
|
||||
if not await access.check_user(message):
|
||||
return
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ router = Router(name="economy")
|
|||
COIN_CHANCE = 27 # из 100
|
||||
|
||||
|
||||
@router.message(Command("очищение", ignore_case=True))
|
||||
@router.message(Command("очищение", "purify", ignore_case=True))
|
||||
async def purification_handler(message: Message) -> None:
|
||||
if not await access.check_user(message):
|
||||
return
|
||||
|
|
@ -43,7 +43,7 @@ async def purification_handler(message: Message) -> None:
|
|||
)
|
||||
|
||||
|
||||
@router.message(Command("яички", ignore_case=True))
|
||||
@router.message(Command("яички", "eggs", ignore_case=True))
|
||||
async def eggs_handler(message: Message) -> None:
|
||||
settings = get_settings()
|
||||
if not is_egg_event_active(settings):
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
|
|||
router = Router(name="shop")
|
||||
|
||||
|
||||
@router.message(Command("магазин", ignore_case=True))
|
||||
@router.message(Command("магазин", "shop", ignore_case=True))
|
||||
async def shop_handler(message: Message) -> None:
|
||||
if not await access.check_user(message):
|
||||
return
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "0.5.1"
|
||||
__version__ = "0.5.2"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
"""Тесты меню команд: допустимые имена и синхронизация при старте."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from bot.commands_menu import BOT_COMMANDS, ensure_commands
|
||||
|
||||
|
||||
class FakeBot:
|
||||
def __init__(self, current: list) -> None:
|
||||
self.current = current
|
||||
self.set_calls: list[list] = []
|
||||
|
||||
async def get_my_commands(self) -> list:
|
||||
return self.current
|
||||
|
||||
async def set_my_commands(self, commands: list) -> None:
|
||||
self.set_calls.append(commands)
|
||||
|
||||
|
||||
class BrokenBot:
|
||||
async def get_my_commands(self) -> list:
|
||||
raise RuntimeError("telegram unavailable")
|
||||
|
||||
|
||||
def test_command_names_are_telegram_compatible() -> None:
|
||||
names = [command.command for command in BOT_COMMANDS]
|
||||
|
||||
assert len(names) == len(set(names))
|
||||
for name in names:
|
||||
assert re.fullmatch(r"[a-z0-9_]{1,32}", name), name
|
||||
for command in BOT_COMMANDS:
|
||||
assert command.description
|
||||
|
||||
|
||||
async def test_menu_is_set_when_empty() -> None:
|
||||
bot = FakeBot([])
|
||||
|
||||
assert await ensure_commands(bot) is True
|
||||
assert len(bot.set_calls) == 1
|
||||
assert [command.command for command in bot.set_calls[0]] == [command.command for command in BOT_COMMANDS]
|
||||
|
||||
|
||||
async def test_menu_is_not_touched_when_actual() -> None:
|
||||
bot = FakeBot(list(BOT_COMMANDS))
|
||||
|
||||
assert await ensure_commands(bot) is False
|
||||
assert bot.set_calls == []
|
||||
|
||||
|
||||
async def test_menu_updates_when_different() -> None:
|
||||
bot = FakeBot([BOT_COMMANDS[0]])
|
||||
|
||||
assert await ensure_commands(bot) is True
|
||||
assert len(bot.set_calls[0]) == len(BOT_COMMANDS)
|
||||
|
||||
|
||||
async def test_telegram_error_does_not_crash_startup() -> None:
|
||||
assert await ensure_commands(BrokenBot()) is False # type: ignore[arg-type]
|
||||
|
|
@ -7,4 +7,4 @@ from common import __version__
|
|||
|
||||
def test_version_is_set():
|
||||
assert __version__
|
||||
assert __version__ == "0.5.1"
|
||||
assert __version__ == "0.5.2"
|
||||
|
|
|
|||
Loading…
Reference in New Issue