Меню команд и подсказки

Сегодня мы научим «Цыплёнка-помощника» показывать аккуратное меню команд — то самое, что выпадает по кнопке слева от поля ввода, — чтобы пользователю не приходилось гадать, что бот вообще умеет.
Меню команд — это список команд бота с понятными описаниями, который Telegram показывает рядом с полем ввода. Задаётся он один раз через метод set_my_commands, а дальше Telegram сам подсказывает его каждому пользователю.

Зачем боту меню команд

Представь: ты скинул друзьям в чат игрового клана своего бота-напоминалку про рейды. Первое, что делает друг, — открывает чат и… замирает. Пустое поле ввода, мигающий курсор и ноль подсказок. Что писать? /start? /help? /рейд? А может, просто «привет»? Девять из десяти человек в этот момент пишут «привет», бот молчит (он такого не ждал), и друг закрывает чат с мыслью «ну и фигня». Знакомая боль?

А теперь представь другой вход. Друг открывает чат, и слева от поля ввода уже горит кнопочка-менюшка. Жмёт — и видит готовый список: «/start — запустить бота», «/help — что я умею», «/weather — погода перед прогулкой», «/remind — поставить напоминание». Не надо ничего угадывать: тыкаешь нужную строку, и команда сама подставляется в поле. Разница как между запиской «разберись сам» и понятным меню в кафе, где у каждого блюда есть название и описание.

Вот к чему мы придём в этом уроке — после пары строк кода у нашего бота появится вот такое меню:

start    — Запустить бота
help     — Что я умею
weather  — Погода перед прогулкой
remind   — Поставить напоминание

Результат: в чате слева от поля ввода появится кнопка с иконкой меню; при нажатии Telegram покажет этот список, и тап по любой строке подставит команду в поле ввода.

Меню команд — это не украшение, а первый разговор бота с новым человеком. Хороший список команд отвечает на главный вопрос новичка «а что вообще тут можно?» ещё до того, как он его задал. Давай научимся его делать.

Как Telegram хранит меню: метафора стенда с кнопками

Сначала разберёмся, где это меню вообще живёт. Тут есть важная мысль, которая снимает половину будущих вопросов: список команд хранится не в твоём коде, а на серверах Telegram.

Представь школьный стенд с расписанием у входа. Ты один раз приходишь и вешаешь листок: «Понедельник — математика, вторник — физика…». После этого ты можешь уйти домой, выключить компьютер, уехать на дачу — а стенд продолжает висеть, и любой ученик, проходя мимо, читает расписание. Тебя рядом нет, но информация на месте.

С меню команд всё точно так же. Твоя программа один раз говорит Telegram: «вот мой список команд, повесь его» — и Telegram запоминает. Дальше каждому, кто открывает чат с ботом, Telegram сам показывает этот список из своей памяти. Даже если твой бот в этот момент выключен (например, ты остановил программу на ночь), меню всё равно видно — оно «висит на стенде» на стороне Telegram.

Отсюда два полезных вывода. Во-первых, меню задаётся один раз при запуске бота, а не на каждое сообщение — нет смысла перевешивать одно и то же расписание сто раз в минуту. Во-вторых, если ты поменял список команд в коде, старое меню не исчезнет само: его надо явно «перевесить», снова вызвав нужный метод. Telegram не читает твои мысли — он показывает то, что ты повесил в последний раз.

Есть и приятный бонус от такого устройства. Раз меню живёт на серверах Telegram, оно одинаково работает и на телефоне друга, и на его планшете, и в веб-версии Telegram в браузере — ты ничего не настраиваешь под каждое устройство. Повесил один раз — и список команд «доезжает» до всех клиентов сам. Тебе как автору бота вообще не надо думать, с чего человек открыл чат: меню просто есть везде, где есть Telegram.

И ещё момент, который часто пугает новичков: «а вдруг я повешу меню, а оно перезапишет то, что было?». Да, set_my_commands именно перезаписывает весь список целиком — это не «добавить ещё одну команду к старым», а «вот теперь меню выглядит вот так». Поэтому в коде мы всегда передаём полный список команд, который хотим видеть, а не только новые. Это удобно: ты смотришь на свой список в коде и точно знаешь, что увидит пользователь — никаких невидимых «остатков» от прошлых запусков.

Знакомимся с инструментами: BotCommand и set_my_commands

Чтобы повесить меню, в aiogram есть ровно два инструмента, и оба простые.

BotCommand — одна строчка меню

Каждый пункт меню — это объект BotCommand. У него всего два поля:

  • command — сама команда без слеша (пишем "help", а не "/help"; слеш Telegram добавит сам);
  • description — короткое человеческое описание, которое увидит пользователь.

Один BotCommand — это одна строчка на нашем «стенде». Чтобы получился список, мы складываем несколько таких объектов в обычный Python-список.

set_my_commands — гвоздь, которым вешаем стенд

А отправляет этот список в Telegram метод объекта botset_my_commands. Помнишь объект bot из прошлых уроков? Это он умеет слать запросы в Telegram. Вот и тут: передаём ему список BotCommand, и он «прибивает стенд гвоздём» — регистрирует меню на серверах.

Метод асинхронный (как почти всё в aiogram), поэтому вызывать его надо с await. Не пугайся слова «асинхронный»: тут оно значит лишь «эта операция ходит в интернет, к серверам Telegram, и мы вежливо ждём ответа». await — это и есть наше «подожди, пока сходит и вернётся».

Разбираемся на примерах

Пример 1. Самое первое меню

Возьмём наш файл bot.py, в котором уже живут bot и dp из прошлых уроков, и добавим меню. Я покажу только новый кусочек — функцию настройки и её вызов при старте.

from aiogram import Bot, Dispatcher
from aiogram.types import BotCommand

# bot и dp у нас уже созданы в прошлых уроках
# bot = Bot(token=TOKEN)
# dp = Dispatcher()

async def set_commands(bot: Bot):
    commands = [
        BotCommand(command="start", description="Запустить бота"),
        BotCommand(command="help", description="Что я умею"),
    ]
    await bot.set_my_commands(commands)

Что здесь происходит, по шагам:

  1. Импортируем BotCommand из aiogram.types — это «кирпичик» одной строки меню.
  2. Внутри функции set_commands собираем обычный список из двух BotCommand: команда start с описанием «Запустить бота» и команда help с описанием «Что я умею». Слеш нигде не пишем.
  3. Вызываем await bot.set_my_commands(commands) — это и есть «гвоздь», который вешает стенд на сервере Telegram.

Результат: в чате у пользователя появится кнопка меню; при нажатии он увидит две строки — «start — Запустить бота» и «help — Что я умею».

Пример 2. Где это вызвать, чтобы сработало

Функцию мы написали, но сама по себе она не выполнится — её надо позвать при запуске бота. В aiogram 3.x для этого есть удобное место: стартовый хук диспетчера dp.startup. Помнишь Dispatcher — объект, который принимает обновления и раздаёт их хэндлерам? У него есть событие «я только что запустился», и мы можем подписать на него нашу настройку меню.

from aiogram import Bot, Dispatcher
from aiogram.types import BotCommand

async def set_commands(bot: Bot):
    commands = [
        BotCommand(command="start", description="Запустить бота"),
        BotCommand(command="help", description="Что я умею"),
    ]
    await bot.set_my_commands(commands)

async def main():
    # регистрируем нашу настройку на момент запуска
    dp.startup.register(set_commands)
    # и запускаем привычный polling из прошлых уроков
    await dp.start_polling(bot)

Разберём новую строчку dp.startup.register(set_commands). Мы как будто говорим диспетчеру: «когда запустишься — не забудь сначала вызвать set_commands и передать туда bot». aiogram сам подставит объект bot в аргумент функции — поэтому в set_commands(bot: Bot) мы его и ждём. Дальше идёт уже знакомый по прошлым урокам start_polling — бот начинает слушать сообщения.

Результат: при каждом запуске программы бот сначала «перевешивает» меню на серверах Telegram, а потом начинает отвечать на сообщения. Меню всегда будет соответствовать тому, что в коде.

Пример 3. Меню «Цыплёнка-помощника»

Теперь соберём осмысленное меню под наш проект. К концу курса Цыплёнок будет уметь приветствовать, подсказывать, показывать погоду и ставить напоминания — заранее заложим эти команды в меню (даже если хэндлеры для части из них мы напишем в следующих модулях).

async def set_commands(bot: Bot):
    commands = [
        BotCommand(command="start",   description="Запустить бота"),
        BotCommand(command="help",    description="Что я умею"),
        BotCommand(command="weather", description="Погода перед прогулкой"),
        BotCommand(command="remind",  description="Поставить напоминание"),
    ]
    await bot.set_my_commands(commands)

Обрати внимание на описания. Они не «weather — weather» и не «команда номер 3», а написаны человеческим языком, от лица пользователя: «Погода перед прогулкой», «Поставить напоминание». Хорошее описание отвечает на вопрос «зачем мне жать эту кнопку?», а не повторяет название команды. Это та же вежливость, что и подписи к блюдам в меню кафе.

Результат: пользователь увидит четыре аккуратные строки меню; тап по «weather» подставит в поле ввода /weather, по «remind» — /remind, и так далее.

Кстати, добавлять команду в меню и писать для неё хэндлер — это два разных дела. Про хэндлеры — функции-обработчики, которые реагируют на команды и сообщения, — мы подробно говорили в уроке «Хэндлеры сообщений». Меню лишь показывает команду в списке и подставляет её в поле ввода. Если хэндлера для /weather пока нет, бот на неё просто не ответит — но в меню она уже будет красиво висеть. Поэтому удобно сначала продумать меню, а потом по одному дописывать обработчики.

Пример 4. Маленький помощник на чистом Python

Когда команд становится много, удобно хранить их парами «команда — описание» в обычном словаре, а список BotCommand собирать из него. Эту логику — превращение словаря в список пар — можно отработать на чистом Python прямо здесь, без всякого Telegram. Это самодостаточный сниппет на стандартной библиотеке, его можно запустить.

commands = {
    "start": "Запустить бота",
    "help": "Что я умею",
    "weather": "Погода перед прогулкой",
}

# превращаем словарь в список пар, как потом сделаем с BotCommand
for name, description in commands.items():
    print(f"/{name} — {description}")

Вывод:

/start — Запустить бота
/help — Что я умею
/weather — Погода перед прогулкой

Видишь идею? Мы прошлись по словарю методом .items() и для каждой пары собрали красивую строку. В реальном коде вместо print мы будем создавать BotCommand(command=name, description=description) и складывать в список. Логика та же, просто результат — не текст, а объекты для Telegram.

Частые ошибки и подводные камни

На меню команд новички спотыкаются предсказуемо. Разберём грабли заранее.

  1. Пишут слеш внутри команды. Очень частое: BotCommand(command="/help", ...). Слеш в поле command писать нельзя — Telegram добавит его сам. С лишним слешем Telegram ответит ошибкой, и меню не повесится. Правильно: command="help".
  2. Ждут, что меню обновится «само». Поменял описание в коде, перезапустил бота — а в чате старый текст. Почему? Скорее всего, Telegram-клиент показывает закешированное меню. Обычно достаточно переоткрыть чат или подождать минуту. Но бывает и другая причина: ты забыл, что set_commands вообще должна вызваться — без dp.startup.register(set_commands) функция просто лежит мёртвым грузом и ничего не вешает.
  3. Нарушают правила имени команды. Telegram строг: команда — это только латинские строчные буквы, цифры и подчёркивание, длиной от 1 до 32 символов. Никаких пробелов, дефисов, эмодзи и кириллицы. BotCommand(command="моя команда", ...) или command="my-cmd" приведут к ошибке. Если хочется «русское» меню — кириллицу пиши в описании (description), а саму команду оставляй латиницей: BotCommand(command="weather", description="Погода 🌤").
  4. Слишком длинное описание. Описание ограничено 256 символами, но это и так слишком много: длинный текст обрежется и будет выглядеть некрасиво. Держи описания короткими — несколько слов, как подпись к кнопке, а не абзац.
  5. Вызывают set_my_commands на каждое сообщение. Логика «на всякий случай перевешу меню в каждом хэндлере» — лишняя нагрузка и лишние запросы к Telegram. Меню не меняется от сообщения к сообщению, поэтому ставим его один раз при старте. Это ровно та же мысль про «стенд вешаем один раз».

Мини-практика: сделай меню своего бота

Теперь твоя очередь оживить меню. Открой свой bot.py (тот самый, с bot и dp) и доведи задание до конца:

  1. Добавь функцию set_commands(bot) с тремя командами: start, help и ещё одной своей — придумай команду под свою идею бота. Например, для бота-напоминалки про домашку это /homework — Список домашки на сегодня, а для бота-опросника друзей — /poll — Запустить опрос.
  2. Зарегистрируй функцию через dp.startup.register(set_commands) в main(), чтобы меню вешалось при запуске.
  3. Запусти бота, открой с ним чат и проверь: появилась ли кнопка меню? Видны ли все три команды с твоими описаниями?
  4. Прочитай свои описания глазами друга, который видит бота впервые. Понятно ли из них, зачем жать кнопку? Если где-то стоит description вроде «help — help», перепиши по-человечески.
  5. Со звёздочкой: вынеси команды в словарь, как в Примере 4, и собери список BotCommand циклом for. Так добавлять новые команды станет приятнее — правишь одну строчку словаря, а не копируешь длинный BotCommand(...).

Если меню не появилось — пройди по списку ошибок выше: чаще всего виноваты либо забытый dp.startup.register, либо слеш внутри command, либо кеш клиента (переоткрой чат).

Когда основное задание получится, попробуй маленький эксперимент, который хорошо укладывает в голове идею «стенда». Запусти бота, убедись, что меню появилось, а потом останови программу (закрой её в терминале) и снова открой чат с ботом. Заметь: меню по-прежнему на месте, хотя бот выключен и отвечать не может. Это и есть то самое «расписание висит на стенде, даже когда тебя нет рядом». А теперь добавь в код четвёртую команду, перезапусти бота и посмотри, как список в чате обновился — это работа set_my_commands, которая перезаписала меню целиком.

Итоги

Поздравляю — теперь любой, кто откроет чат с твоим ботом, сразу видит, что тот умеет. Давай закрепим главное:

  • Меню команд — это список команд с описаниями, который Telegram показывает рядом с полем ввода и хранит у себя на серверах (вспомни «стенд с расписанием»).
  • Одна строка меню — это объект BotCommand с полями command (без слеша!) и description (по-человечески).
  • Список BotCommand мы вешаем методом set_my_commands объекта bot — один раз при запуске, через dp.startup.register.
  • Команда в меню и хэндлер для неё — разные вещи: меню показывает команду, а отвечает на неё хэндлер.

А дальше мы научим «Цыплёнка-помощника» не просто показывать команды в меню, а вести пользователя по шагам — встретимся с reply-клавиатурой и inline-кнопками поближе, а затем перейдём к самому интересному: к диалогу-анкете через FSM, где бот будет спрашивать тебя по очереди, как заполнить поля в форме. Меню мы повесили — теперь сделаем бота по-настоящему удобным.

Проверьте себя
1. Где хранится меню команд бота после вызова set_my_commands?
AВ коде твоей программы, поэтому при выключении бота меню пропадает
BНа серверах Telegram — оно видно, даже если бот выключен
CВ оперативной памяти телефона пользователя
DВ файле bot.py рядом с токеном
2. Как правильно записать команду help в объекте BotCommand?
ABotCommand(command="/help", description="Что я умею")
BBotCommand(command="help", description="Что я умею")
CBotCommand("help", "Что я умею", slash=True)
DBotCommand(name="/help", text="Что я умею")
3. Куда удобнее всего поставить вызов set_commands, чтобы меню вешалось при запуске бота?
AВ каждый хэндлер сообщения, чтобы перестраховаться
BЗарегистрировать через dp.startup.register(set_commands) перед start_polling
CВообще не вызывать — aiogram повесит меню автоматически
DВ тело команды /start, чтобы меню появлялось после первого сообщения
4. Ты хочешь команду с русским смыслом «Погода перед прогулкой». Как это сделать правильно?
ABotCommand(command="погода", description="...") — команды можно писать кириллицей
BBotCommand(command="weather", description="Погода перед прогулкой") — команда латиницей, кириллица в описании
CBotCommand(command="weather прогулка", description="...") — через пробел
DНикак, русские смыслы в меню не поддерживаются
5. Команда добавлена в меню через BotCommand, но бот на неё не отвечает. В чём вероятная причина?
ATelegram ещё не успел повесить меню — надо подождать сутки
BДля этой команды не написан хэндлер: меню только показывает команду, а отвечает на неё обработчик
CBotCommand нельзя использовать вместе с polling
DНужно вызвать set_my_commands ещё раз внутри хэндлера
6. Почему не стоит вызывать set_my_commands в каждом хэндлере на каждое сообщение?
ATelegram заблокирует бота после трёх вызовов
BМеню не меняется от сообщения к сообщению, поэтому это лишняя нагрузка и лишние запросы — достаточно один раз при старте
Cset_my_commands вообще нельзя вызывать больше одного раза за всю жизнь бота
DОт этого меню начнёт показываться кириллицей