EvolCode
треки/Телеграм-боты·01 / 06
12 мин чтения

Команды, кнопки и красивые сообщения

Эхо-бот это весело ровно один раз. Реальный бот это: набор команд, кнопки чтобы не печатать, и сообщения с подсветкой важного. Хорошая новость — всё это в Telegram уже встроено, и в grammY это пара строк. Разберём три кита: команды (`/start`, `/help`), клавиатуры (обычные и inline), и форматирование (жирный, курсив, моноширинный).

Команды и подсказки

Команда — это сообщение, начинающееся с /. Telegram сам подсвечивает его и предлагает автокомплит. Чтобы пользователь видел список команд при нажатии «/», их надо зарегистрировать через bot.api.setMyCommands([...]). Один раз при запуске. После этого в чате с ботом появится кнопка-меню снизу с описанием каждой команды. Без этого пользователь не знает что бот умеет — ОБЯЗАТЕЛЬНО регистрируй команды.

Обычная клавиатура (Reply Keyboard)

Это кнопки которые появляются ВМЕСТО клавиатуры внизу экрана. Когда пользователь нажимает кнопку — её текст отправляется как обычное текстовое сообщение. Подходит для главного меню («Заказать», «Отмена», «Меню»). Создаётся через

$ Keyboard().text("Кнопка").row().text("Другая")

Передаёшь в reply через reply_markup. У клавиатуры есть свойства: resize_keyboard (компактный размер), one_time_keyboard (закроется после первого нажатия), is_persistent (постоянная).

Inline-клавиатура (Inline Keyboard)

Это кнопки ПОД сообщением, прямо в чате. Самый частый и мощный тип. У каждой кнопки есть text (что видно) и callback_data (что прилетит на сервер при нажатии — до 64 байт). Когда пользователь нажимает — приходит событие callback_query. На него надо ответить через ctx.answerCallbackQuery() (иначе у пользователя крутится спиннер). Можно редактировать сообщение прямо на месте, заменять кнопки, делать многошаговые меню. Inline-кнопки умеют ещё URL (просто переход), switch_inline_query, web_app (мини-приложения) — последнее открывает HTML-страницу прямо в Telegram.

Форматирование текста

Telegram поддерживает 2 синтаксиса: HTML и Markdown V2. HTML понятнее и более устойчивый к битым символам. Доступно: <b>жирный</b>, <i>курсив</i>, <u>подчёркнутый</u>, <s>зачёркнутый</s>, <code>моноширинный</code>, <pre>блок кода</pre>, <a href="url">ссылка</a>, <tg-spoiler>спойлер</tg-spoiler>. Передаёшь parse_mode: "HTML" в reply. Markdown V2 строже к экранированию символов: точки, скобки, дефисы надо экранировать \. Если нет особых причин — используй HTML.

Реальная схема меню

Типичный сценарий: на /start показываем приветствие + reply-keyboard с главным меню (3-4 кнопки). По нажатию любой кнопки — открываем подменю через inline-клавиатуру (кнопки под новым сообщением). Дальше навигация — по callback_data. Для возврата делаем кнопку «← назад» которая редактирует сообщение обратно в главное меню. Один источник правды о состоянии — callback_data или БД, не локальные переменные. Локальные пропадут при перезапуске.

примерtypescript
import { Bot, Keyboard, InlineKeyboard } from 'grammy';

const bot = new Bot(process.env.BOT_TOKEN!);

// Регистрируем команды чтобы Telegram показывал их в меню
await bot.api.setMyCommands([
  { command: 'start', description: 'Запустить бота' },
  { command: 'help', description: 'Что я умею' },
  { command: 'about', description: 'О боте' },
]);

// /start — приветствие + главная reply-клавиатура
bot.command('start', (ctx) => {
  const mainMenu = new Keyboard()
    .text('📚 Учиться').text('🎯 Челлендж')
    .row()
    .text('⚙️ Настройки')
    .resized();

  return ctx.reply('Привет! Выбери что хочешь:', {
    reply_markup: mainMenu,
  });
});

// /help — текст с HTML-форматированием
bot.command('help', (ctx) =>
  ctx.reply(
    '<b>Что я умею:</b>\n\n' +
    '• Учить тебя кодить\n' +
    '• Давать ежедневные челленджи\n' +
    '• Хранить твой прогресс\n\n' +
    '<i>Просто нажимай кнопки или пиши команды</i>',
    { parse_mode: 'HTML' }
  )
);

// Когда пользователь нажал «Учиться» из reply-клавиатуры —
// показываем inline-меню с уроками
bot.hears('📚 Учиться', (ctx) => {
  const lessons = new InlineKeyboard()
    .text('Урок 1: Переменные', 'lesson:1').row()
    .text('Урок 2: Функции', 'lesson:2').row()
    .text('Урок 3: Массивы', 'lesson:3');

  return ctx.reply('Выбери урок:', { reply_markup: lessons });
});

// Обработчик inline-кнопок с callback_data="lesson:N"
bot.callbackQuery(/^lesson:(\d+)$/, async (ctx) => {
  const lessonId = ctx.match[1];
  await ctx.answerCallbackQuery(); // убираем спиннер
  await ctx.editMessageText(
    `<b>Урок ${lessonId}</b>\n\nЗдесь будет содержание урока…`,
    {
      parse_mode: 'HTML',
      reply_markup: new InlineKeyboard().text('← назад', 'lessons:list'),
    }
  );
});

bot.callbackQuery('lessons:list', async (ctx) => {
  await ctx.answerCallbackQuery();
  const lessons = new InlineKeyboard()
    .text('Урок 1: Переменные', 'lesson:1').row()
    .text('Урок 2: Функции', 'lesson:2').row()
    .text('Урок 3: Массивы', 'lesson:3');
  await ctx.editMessageText('Выбери урок:', { reply_markup: lessons });
});

bot.start();

Бот с командами, главным меню и inline-кнопками

главное
  • 01Регистрируй команды через setMyCommands — иначе пользователь не знает что бот умеет.
  • 02Reply-клавиатура для главного меню (всегда видна). Inline — для меню под сообщением (контекстные).
  • 03Каждый callbackQuery нужно подтверждать через answerCallbackQuery — иначе у юзера крутится спиннер.
  • 04Используй HTML parse_mode — он не требует экранирования спецсимволов как Markdown V2.
  • 05callback_data ограничен 64 байтами. Для длинного состояния — храни в БД, в callback клади только id.
проверь себя
01Что обязательно сделать при ответе на callback_query?
02Какое максимальное количество байт можно положить в callback_data?
03Зачем нужна reply-клавиатура если есть inline?
следующий урок: Состояния и хранение пользователей