Команды, кнопки и красивые сообщения
Эхо-бот это весело ровно один раз. Реальный бот это: набор команд, кнопки чтобы не печатать, и сообщения с подсветкой важного. Хорошая новость — всё это в Telegram уже встроено, и в grammY это пара строк. Разберём три кита: команды (`/start`, `/help`), клавиатуры (обычные и inline), и форматирование (жирный, курсив, моноширинный).
Команды и подсказки
Команда — это сообщение, начинающееся с /. Telegram сам подсвечивает его и предлагает автокомплит. Чтобы пользователь видел список команд при нажатии «/», их надо зарегистрировать через bot.api.setMyCommands([...]). Один раз при запуске. После этого в чате с ботом появится кнопка-меню снизу с описанием каждой команды. Без этого пользователь не знает что бот умеет — ОБЯЗАТЕЛЬНО регистрируй команды.
Обычная клавиатура (Reply Keyboard)
Это кнопки которые появляются ВМЕСТО клавиатуры внизу экрана. Когда пользователь нажимает кнопку — её текст отправляется как обычное текстовое сообщение. Подходит для главного меню («Заказать», «Отмена», «Меню»). Создаётся через
Передаёшь в 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 или БД, не локальные переменные. Локальные пропадут при перезапуске.
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-кнопками