Как создать Telegram-бота на grammY

Как создать Telegram-бота с помощью библиотеки grammY

Рассказываем, как создать Telegram-бота с помощью библиотеки grammY и задеплоить его на облачный сервер.

Эту обучающую инструкцию подготовил фронтенд-разработчик и блогер Арсений Помазков.

Создаем бота в Telegram

Первым делом нам необходимо создать своего бота:

  1. открываем Telegram и в поиске вбиваем @BotFather;
  2. отправляем ему команду /newbot;
  3. получаем ответ, в котором нам предлагают выбрать для будущего бота имя и уникальный username;
  4. получаем ключ — токен для управления ботом. Он имеет вид 1234567890:UIEaeSx_YsRXdD-C39M0t1PzcdnZZ4HgsKq (советую не показывать и не публиковать нигде свои токены).

Готово, теперь можно разворачивать проект.

Разворачиваем проект и пишем стартовый код

Структура проекта

Для разработки нам понадобятся node.js и npm. При установке node.js автоматически должен установиться и npm, но чтобы убедиться в этом, запустите команды:


    node -v

    npm -v

Если в ответ увидите числовые версии, то все установлено. Если видите ошибку, вам нужно установить node.js с официального сайта: выбирайте LTS-версию и следуйте инструкциям установщика.

Следующим шагом откроем в редакторе кода папку, в которой будем создавать проект (в данном случае это grammyjs-bot). Затем в терминале инициализируем его с помощью npm. Для этого в командной строке редактора вводим команду:


    npm init -y

В нашей папке создался файл package.json с информацией об npm-пакете по умолчанию — его содержимое мы видим в консоли сразу после ввода команды:

Теперь нужно подключить три библиотеки. Сначала главную — Grammy JS (на самом деле, это фреймворк, как говорится в документации), затем две побочные. Библиотека dotenv нам понадобится, чтобы хранить токен как переменную окружения (env variable), nodemon, чтобы код бота автоматически перезапускался после внесения изменений. Все их можно установить одной командой:


    npm i grammy dotenv nodemon

После этого в папке появится новый файл package-lock.json с подробным описанием библиотек и зависимостей, а также папка node_modules с самими зависимостями. Кроме того, в package.json будут добавлены версии двух наших библиотек в поле dependencies.

Осталось создать файл index.js, в котором будем писать код бота, и файл .env, в котором будет храниться токен. Если решите использовать другое название, на забудьте также внести его в package.json, в поле main.

Код проекта

Итак, структура проекта готова, переходим к написанию кода. Для этого создадим переменную окружения в отдельном файле .env:


    // Обратите внимание, что кавычек нигде нет
BOT_API_KEY=1234567890:UIEaeSx_YsRXdD-C39M0t1PzcdnZZ4HgsKq

Сразу после знака «=» укажите свой уникальный ключ от BotFather (без кавычек).

Теперь открываем index.js (в нем будет наш основной код) и вставляем строки:


    // Подключаем библиотеку dotenv
require('dotenv').config();
// Обращаемся к библиотеке grammy и импортируем из него класс Bot const { Bot } = require('grammy');
// Создаём своего бота на основе импортированного класса, передавая
// в качестве аргумента ссылку на полученный ранее токен (чуть дальше мы
// его создадим)
const bot = new Bot (process.env.BOT_API_KEY);
// Запускаем созданного бота
bot.start();

process.env.BOT_API_KEY — это и есть получение переменной окружения BOT_API_KEY.

Бота уже можно запустить, однако он никак не будет реагировать на сообщения пользователя. Чтобы это исправить, добавим реакции на команду /start и любое другое сообщение:


    require('dotenv').config();
const { Bot } = require('grammy');


const bot = new Bot (process.env.BOT_API_KEY);


bot.command('start', async (ctx) => {
 await ctx.reply(
  'Привет! Я - Бот 🤖',
 );
});


bot.on("message", async (ctx) => {
 await ctx.reply('Надо подумать...');
});


bot.start();

Теперь можно запустить бота и провести первый тест. В первую очередь удаляем скрипт test, который создался в package.json по умолчанию, прописываем скрипт start и запускаем бота командой в терминале:


    "scripts": {
"start": "nodemon index.js"
},

    npm start

Готово — при сохранении изменений бот будет автоматически перезагружаться с помощью библиотеки nodemon.

Обработка ошибок

Чтобы бот не падал при возникновении ошибок, добавим их обработку, как советуют создатели Grammy JS в документации.

Сначала добавим импорт нужных классов:


    const { Bot, GrammyError, HttpError } = require('grammy');

А далее — сам обработчик ошибок:


    bot.catch((err) => { const ctx = err.ctx;
console.error(`Error while handling update ${ctx.update.update_id}:`); const e = err.error;
if (e instanceof GrammyError) {
 console.error('Error in request:', e.description);
} else if (e instanceof HttpError) { console.error('Could not contact Telegram:', e);
} else {
console.error('Unknown error:', e);
}
});
Обратите внимание: все обработчики (слушатели) событий и ошибок должны быть добавлены до его запуска в коде (bot.start()).

Об ошибках и их типах можно подробнее прочитать по ссылке.

Обработка данных от пользователя

Главная функция бота — отвечать на сообщения и команды пользователя. В Grammy это реализовано с помощью обработчиков определенных событий. Мы уже добавили боту два таких слушателя с помощью bot.command и bot.on. Теперь посмотрим, какие есть виды обработчиков в Grammy и как с ними работать.

Ответ на команды — bot.command

Команда — любая последовательность знаков без пробелов (/start, /help, /say_something). Чтобы бот мог на них отвечать, нужно добавить обработчик command и передать первым аргументом строку с названием команды (без слеша). Второй аргумент — функция (коллбэк), которая будет вызвана, если бот получит соответствующую команду. Коллбэк автоматически принимает объект context (он же ctx), который мы используем для отправки ответа.


    bot.command('say_hello', async (ctx) => { 
await ctx.reply('Hello');
});

Если необходимо, чтобы бот одинаково отвечал на несколько команд, передаем первым аргументом массив:


    bot.command(['say_hello', 'hello', 'say_hi'], async (ctx) => {
 await ctx.reply('Hello');
});
Эта запись работает как оператор ИЛИ — «вызвать указанный коллбэк, если получена команда /say_hello ИЛИ /hello ИЛИ /say_hi».

Получаем результат:

Кроме этого, в Grammy мы можем показать пользователю меню команд, которые есть у нашего бота. Чтобы это сделать, вызываем метод setMyCommands:


    bot.api.setMyCommands([
{ command: 'start', description: 'Запуск бота' },
{ command: 'say_hello', description: 'Получить приветствие' },
{ command: 'hello', description: 'Получить приветствие' },
{ command: 'say_hi', description: 'Получить приветствие' },
]);

У бота появится синяя кнопка с тремя белыми полосками в левом нижнем углу. Если пользователь нажмет на нее, он увидит команды с описанием:

Кстати, вы можете использовать кэмел-кейс (sayHi) вместо снейк-кейса (say_hi), когда добавляете слушатель bot.command, однако в bot.api.setMyCommands передать такую команду не получится. Если вы попытаетесь сделать это, получите ошибку 400 Bad Request: BOT_COMMAND_INVALID.

Судя по всему, проблема во внутренних механизмах самого Telegram, а не библиотеки: та же ошибка будет и в telegraf, и в aiogram (библиотеке для Python). При этом, если не использовать заглавные буквы, ошибок не возникнет. А вот кебаб-кейс вообще не будет работать.

Ответ на типы сообщений

Бот может отвечать на сообщения с текстом, картинкой, стикером, вложением и так далее. В этом заключается одна из основных фичей Grammy. Другие фреймворки для разработки ботов имеют примитивную систему фильтрации, из-за чего код обрастает кучей if-ов. У grammY же есть продвинутый язык фильтрации, чтобы реагировать именно на те сообщения, которые нужны.

Чтобы познакомиться с этим языком, нам понадобится метод bot.on. Ему первым аргументом мы передаем строку с фильтром (на какой тип сообщений хотим реагировать), а вторым — коллбэк.

Кстати, такой обработчик мы тоже уже использовали, передав первым аргументом строку «message», что позволило отвечать на абсолютно любой тип сообщения:


    bot.on('message:text', async (ctx) => {
await ctx.reply('Получил сообщение с текстом');
});

Вот, какие еще фильтры встроены в Grammy:

  • «message:text» — сообщение с текстом,
  • «message:photo» — сообщение с фотографией,
  • «message:voice» — голосовое сообщение и так далее.

Вариантов сообщений много, с полным списком можно ознакомиться в документации. Но и это еще не все.

Лайфхаки по работе с фильтрами

Слово «message» писать не обязательно, можно поставить двоеточие и более узкий фильтр:


    bot.on(':voice', async (ctx) => {
await ctx.reply('Получил голосовое сообщение');
});

Фильтр может быть еще более специфичным. Например, «message:entities:url» — тут мы обращаемся к механизму парсинга сообщения и отвечаем только на те, в которых есть ссылки. Кстати, первые два слова можно пропустить, сократив запись до «::url».

Так нам становятся доступны сообщения с упоминаниями («::mention»), ссылками, письмами («::email») и другими данными.


    bot.on('::email', async (ctx) => {
await ctx.reply('Ваше сообщение содержит email');
});

Но это еще не все лайфхаки.

  • Есть шорткаты. Запись «msg» заменяет одновременно «message» и «channel_post», «edit» — «edited_message» и «edited_channel_post», а «:media» – «:photo» и «:video».
  • Можно использовать массив и передавать несколько команд. Например, массив [«::url», «::email»] будет триггерить коллбэк в ответ на сообщения как со ссылкой, так и с письмом.
  • Фильтры можно комбинировать. Например, bot.on(«:photo»).on(«::hashtag») вызовет коллбэк, если в сообщении есть и фото, и хештег. Полезная фича для составления сложных фильтров.
  • Можно создавать свои фильтры, используя метод .filter. Первым аргументом он принимает функцию для фильтрации, вторым (в случае положительного ответа) — коллбэк для реагирования:

    bot.on('message').filter(
  (ctx) => ctx.from.id === 255162448, 
 async (ctx) => {
await ctx.reply('Привет, админ!');
},
);

Иными словами, если функция-фильтр возвращает true, выполняется коллбэк.

В примере выше я проверяю id пользователя, который отправил сообщение, на совпадение с моим личным идентификатором. Если он совпал, бот ответит личным приветствием. О том, как узнать свой id, мы поговорим позже.

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

Ответ на определенный текст

Конечно, мы можем сузить фильтр до максимума, заставив бота отвечать только на конкретный текст. Для этого нужен слушатель bot.hears:


    bot.hears('пинг', async (ctx) => {
 await ctx.reply('понг'); 
});

Сообщение пользователя должно один-в-один соответствовать строке «пинг», иначе коллбэк вызван не будет. Например, бот отреагирует на текст «пинг», а на «Пинг» — нет. А вот пробелы до или после слова в тексте сообщения не играют роли, так как они будут автоматически обрезаны. Как и ранее, мы можем передавать массив строк, если хотим отвечать одинаково на несколько разных сообщений:


    bot.hears(['пинг', 'еще пинг'], async (ctx) => { 
await ctx.reply('понг');
});

Кроме того, слушатель bot.hears может принимать не только строку, но и регулярные выражения. Это полезно, если мы хотим, чтобы бот отвечал на определенный текст пользователя. Чтобы все работало, заключим определенное слово в «/ /». Так мы сможем выцеплять его из сообщения любого размера, нивелировать значимость регистра, не учитывать остальной текст и так далее. Простой пример:


    bot.hears(/пипец/, async (ctx) => {
await ctx.reply('Ругаемся?');
});

Последовательность слушателей в коде

Последовательность добавления обработчиков важна. При получении сообщения проверка происходит последовательно — вызывается только коллбэк первого совпадения.

Это значит, что в примере ниже бот никогда не пришлет нам «Привет! Я — бот», так как наше сообщение с текстом «/start» подойдет под условия первого слушателя, вызовется коллбэк и выполнение завершится.


    bot.on('message', async (ctx) => await ctx.reply('Привет'));

bot.command('start', async (ctx) => {       await ctx.reply('Привет! Я - Бот🤖');
});

Получается, в нашем коде более специфические фильтры всегда должны располагаться выше, чем общие. Если поменять в этом примере слушателей местами, все будет работать как надо — на сообщение «/start» мы получим ответ «Привет! Я — бот», а на любые другие — просто «Привет».

Что такое Context (ctx)

Напомню, как работает система: пользователь отправляет сообщение, оно попадает на серверы Telegram, а после — перенаправляется на машину, где запущен сам бот.

Так вот, когда бот получает сообщение от Telegram, он оборачивает его в объект обновления, который и называется Context (контекст). Объект Context содержит не только текст и вложения сообщения, но и информацию о чате, пользователе и, конечно, самом сообщении.

Этот объект помогает удобно взаимодействовать с нужной информацией и использовать полезные методы. Context мы автоматически получаем первым аргументом в коллбэк слушателя. Он отображается как ctx.

Context: данные и информация

Объект Context содержит много разной информации, среди которой:

  • ctx.from — информация об отправителе сообщения (id, имя, юзернейм, язык и др.);
  • ctx.msg — объект сообщения (внутри есть id сообщения, информация об отправителе, дата отправки, текст и др.);
  • ctx.me — объект с информацией о самом боте.

Помните, мы добавили в фильтр одного из слушателей проверку id пользователя? Там я использовал свой идентификатор, который получил из объекта ctx.from, выведя его в консоль. Можем сделать так, что бот будет отправлять пользователю его id по запросу:


    bot.hears('ID', async (ctx) => {
 await ctx.reply(`Ваш telegram ID: ${ctx.from.id}`);
})

Context: методы — reply

Ранее мы уже применяли метод reply у контекста, когда отвечали на сообщения пользователя. Давайте подробнее его рассмотрим.

Во-первых, так как этот метод асинхронный, нам стоит дожидаться окончания его работы, используя await перед вызовом. Во-вторых, помимо текста ответа (первый аргумент), он может принимать второй параметр — объект. Он необязательный, но через него можно указать разные модификаторы для отправки.

Например, указав reply_parameters и передав туда id сообщения, мы заставим бота ответить на конкретное сообщение:


    bot.on(  'message', async (ctx) =>
await ctx.reply('Привет', {
reply_parameters: { message_id: ctx.msg.message_id },
}),
);

Также мы можем применить один из видов форматирования сообщений, передав параметр parse_mode. У него есть три варианта значения: HTML, Markdown, MarkdownV2.

Со значением HTML в тексте можно использовать привычные тэги: ссылку <a>, жирный текст <b>, курсив <i>, спойлер <span class=»tg-spoiler»>spoiler</span> (или <tg-spoiler>spoiler</tg-spoiler>, это то же самое) и другие. Пример:


    bot.on( 'message', async (ctx) =>
await ctx.reply(
'Привет! Подпишись на <a href="https://t.me/pomazkovjs">Telegram-канал</a> pomazkov.js',
{
parse_mode: 'HTML',
},
 ),
);

Причем превью ссылки можно отключить, добавив disable_web_page_preview: true


    {
parse_mode: 'HTML', disable_web_page_preview: true,
}

Второй популярный вариант форматирования — MarkdownV2. Возможности форматирования у него те же, просто отличается способ: необходимо добавлять символы справа и слева от текста. Вот так:

  • *жирный шрифт*
  • _курсив_
  • __подчеркнутый__
  • ||текст под спойлером||
  • [тексты ссылки](http://www.example.com/)

Context: методы — react

Еще один полезный метод у ctx — react. Он позволяет реагировать на сообщения пользователя от лица бота. Доработаем ответ на команду start так, чтобы бот еще и ставил лайк на сообщение с командой:


    bot.command('start', async (ctx) => { await ctx.react('👍');
await ctx.reply('Привет! Я - Бот 🤖');
});

Клавиатуры

Клавиатура — удобный элемент бота, который упрощает взаимодействие с пользователем. В Telegram-ботах можно использовать две клавиатуры: обычную, или custom, и inline. У обеих есть ограничения:

  • клавиатуру можно отправить только вместе с каким-либо сообщением;
  • одно сообщение — одна клавиатура, сразу две отправить нельзя.

Эти ограничения установлены разработчиками Telegram, так что ничего не поделаешь.

Custom Keyboard

Сперва рассмотрим Custom Keyboard. Это «обычная» клавиатура, которая появляется внизу экрана и заменяет встроенную клавиатуру с буквами.

Импортируем класс Keyboard из Grammy:


    const { Bot, GrammyError, HttpError, Keyboard } = require('grammy');

Далее создаем клавиатуру так же, как создавали экземпляр бота, через new Keyboard(). Чтобы добавить кнопку к клавиатуре, нужно вызвать метод text, передав строку с текстом в качестве аргумента. Последнее играет роль надписи на кнопке.

По умолчанию все кнопки встают в один ряд. Чтобы этого избежать и сделать текст читаемым, нужен метод row(), который прерывает ряд и переносит следующие элементы на новую строку. Кроме того, по умолчанию кнопки растягиваются так, чтобы клавиатура была обычного размера. Чтобы кнопки были по размеру содержимого, нужно вызвать метод resized.

Примеры работы с Custom Keyboard

Спросим, как у пользователя настроение, а варианты ответа передадим в клавиатуру. Сохраним ее в переменную moodKeyboard и добавим новый обработчик, который будет отправлять клавиатуру пользователю по команде /keyboard:


    const moodKeyboard = new Keyboard()
.text('Хорошо')
.row()
.text('Нормально')
.row()
.text('Плохо')
.resized();

Для отправки клавиатуры в методе reply используем уже известный второй аргумент:


    bot.command('keyboard', async (ctx) => { await ctx.reply('Как настроение?', {
reply_markup: moodKeyboard,
});
});

Как только пользователь нажмет кнопку с текстом, бот получит ответ в виде обычного текстового сообщения. Чтобы отвечать на такие нажатия, можно использовать обычный слушатель bot.hears:


    bot.hears("Хорошо", async (ctx) => { await ctx.reply('Рад слышать!');
})

Нажимаем на кнопку Хорошо и видим ответ:

Особенность этой клавиатуры в том, что по умолчанию она не пропадает самостоятельно. Пользователь может переключаться между ней и встроенными кнопками, нажав на иконку клавиатуры возле значка микрофона. Если мы хотим, чтобы она исчезала после нажатия пользователя (то есть была одноразовой), в момент создания необходимо добавить метод oneTime:


    const moodKeyboard = new Keyboard()
.text('Хорошо')
.row()
.text('Нормально')
.row()
.text('Плохо')
.resized()
  .oneTime();

Теперь клавиатура пропадет сразу после первого нажатия.

Второй вариант — вручную убрать такую клавиатуру. Для этого нужно будет отправить дополнительный параметр remove_keyboard:


    bot.hears('Хорошо', async (ctx) => { await ctx.reply('Рад слышать!', {
reply_markup: { remove_keyboard: true },
});
});

Если у вас уже есть массив строк, который нужно превратить в клавиатуру, можно использовать альтернативный способ создания ее экземпляров. Класс Keyboard имеет статический метод Keyboard.text, который позволяют создавать объекты кнопок. Далее можно создать экземпляр клавиатуры из массива объектов, используя Keyboard.from:


    const moodLabels = ['Хорошо', "Нормально", 'Плохо'];
const rows = moodLabels.map((label) => [Keyboard.text(label)]); const moodKeyboard2 = Keyboard.from(rows).resized();

Наконец, кроме обычных кнопок можно использовать встроенный механизм Telegram, чтобы запросить у пользователя геолокацию (requestLocation), номер телефона (requestContact) или даже опрос (requestPoll):


    const shareKeyboard = new Keyboard()
.requestLocation('Геолокация')
.requestContact('Контакт')
.requestPoll('Опрос')
.placeholder('Я хочу поделиться...')
.resized();
bot.command('share', async (ctx) => {
 await ctx.reply('Какими данными хочешь поделиться?', {
   reply_markup: shareKeyboard,
 });
});

Чтобы пользователь знал, что у нас есть такие команды, не забывайте добавить их в меню, как мы обсуждали ранее:


    bot.api.setMyCommands([
{ command: 'share', description: 'Поделиться данными' },
]);

Обратите внимание, что в этот раз мы также добавили плейсхолдер, который будет отображаться в поле ввода текста:

Эти кнопки мы можем обрабатывать, используя уже известный нам метод фильтрации входящих уведомлений — bot.on:


    bot.on(':location', async (ctx) => {
await ctx.reply('Спасибо за геолокацию!');
});
bot.on(':contact', async (ctx) => { await ctx.reply('Спасибо за контакт!');
});

Inline keyboard

Второй тип клавиатуры — inline-клавиатура. Отличие от обычной в том, что inline визуально прикрепляется к конкретному сообщению и находится прямо под ним. Кроме того, способ обработки нажатия достаточно сильно отличается. Создается она похожим образом — через импорт класса и инициализацию объекта:


    const { Bot, GrammyError, HttpError, Keyboard, InlineKeyboard } = require('grammy');

    const inlineKeyboard = new InlineKeyboard()
.text('1', 'button-1')
.text('2', 'button-2')
.text('3', 'button-3');

Примеры работы с Inline Keyboard

Как видите, в этот раз мы передаем в метод text два аргумента. Первый — это текст кнопки, который увидит пользователь. Второй — данные для нас, которые мы получим в качестве callback query payload. Чтобы слушать события от inline-клавиатуры, нам понадобится новый обработчик — bot.callbackQuery. Давайте сперва добавим команду для получения этой клавиатуры:


    bot.command('inline_keyboard', async
 (ctx) => { await ctx.reply('Выберите цифру', {
reply_markup: inlineKeyboard,
});
});

Вот, как это выглядит:

Теперь обработаем нажатия:


    bot.callbackQuery('button-1', async (ctx) => { await ctx.reply('Выбрана цифра 1');
});

Обратите внимание: после нажатия нам мгновенно приходит ответ от бота, но наверху какое-то время держится «Загрузка…». Это происходит потому, что нажатия на кнопки inline-клавиатуры требуют специального ответа через метод ctx.answerCallbackQuery:


    bot.callbackQuery('button-1', async
 (ctx) => { await
 ctx.answerCallbackQuery({
text: 'Выбрана цифра 1',
});
await ctx.answerCallbackQuery();
});

Теперь загрузка появляется и сразу исчезает. Кроме того, мы можем использовать этот метод для показа информации в верхнем окошке (где написано «Загрузка…»):


    bot.callbackQuery('button-1', async (ctx) => { await ctx.reply('Выбрана цифра 1');
await ctx.answerCallbackQuery({ text: "Ответ принят!",
});
});

Давайте улучшим этот код, переписав обработчик. Сейчас он реагирует только на одну кнопку, однако мы можем это изменить. Заменим bot.callbackQuery на bot.on и передадим первым аргументом не фиксированное значение, а фильтр, который будет реагировать только на те callbackQuery (коллбэк-запросы), которые содержат данные (payload):


    bot.on("callback_query:data", async (ctx) => {
await ctx.reply('Нажата кнопка inline-клавиатуры'); await ctx.answerCallbackQuery();
});

На любое нажатие inline-кнопки наш бот будет отвечать единым образом. Внутри этого обработчика мы можем получать данные из кнопки, которую нажал пользователь:


    bot.on('callback_query:data', async (ctx) => {
await ctx.reply(`Нажата кнопка ${ctx.callbackQuery.data}`); await ctx.answerCallbackQuery();
});

Что еще нужно знать про inline-клавиатуру

  • В метод text можно не передавать второй аргумент, но тогда реагировать на нажатия будет сложнее.
  • В inline-клавиатурах есть знакомый метод row, который переносит следующие кнопки на новый ряд.
  • Кроме методов text и row есть login, pay, url и другие.

    const inlineKeyboard = new InlineKeyboard()
.url('Перейти в тг-канал', 'https://t.me/pomazkovjs');

Плагины в клавиатурах

Это очень мощные инструменты для разных задач, которые местами упрощают то, что и так работает, а местами дают совершенно новую функциональность.

Сейчас я покажу вам один прием, суть которого узнал от подписчика. Для него нам понадобится плагин hydrate.

Установим hydrate командой в терминале:


    npm i @grammyjs/hydrate

И сразу импортируем его в коде наверху файла:


    import { hydrate } from "@grammyjs/hydrate";

А после строки с созданием экземпляра бота добавим в него плагин через bot.use:


    const bot = new Bot(process.env.BOT_API_KEY);
bot.use(hydrate());

Плагин готов к использованию. Так зачем же он нужен? Он поможет нам использовать inline-клавиатуры в качестве интерактивного меню. В некоторых случаях это очень удобно. До этого мы отвечали на нажатия, отправляя какие-то новые сообщения. Но иногда полезнее скрыть старые данные и показать новые, не захламляя переписку.

Для этого после нажатия пользователем на кнопку мы будем редактировать изначальное сообщение, заменяя текст и клавиатуру.

Сымитируем такую ситуацию, создав меню с двумя пунктами, из которых можно вернуться назад.

Сперва создадим две клавиатуры:


    const menuKeyboard = new InlineKeyboard()
.text('Узнать статус заказа', 'order-status')
.text('Обратиться в поддержку', 'support');
const backKeyboard = new InlineKeyboard().text('< Назад в меню', 'back');

Затем создадим обработчики на все случаи (включая первый, который по команде /menu будет отправлять меню):


    bot.command('menu', async (ctx) => {
 await ctx.reply('Выберите пункт меню', {
reply_markup: menuKeyboard,
});
});
 
 
bot.callbackQuery('order-status', async (ctx) => {
await ctx.callbackQuery.message.editText('Статус заказа: В пути', {
reply_markup: backKeyboard,
});
await ctx.api.editMessageText await ctx.answerCallbackQuery();
});

bot.callbackQuery('support', async (ctx) => {
await ctx.callbackQuery.message.editText('Напишите Ваш вопрос', { reply_markup: backKeyboard,
});
await ctx.answerCallbackQuery();
});

bot.callbackQuery('back', async (ctx) => {
await ctx.callbackQuery.message.editText('Выберите пункт меню', { reply_markup: menuKeyboard,
});
await ctx.answerCallbackQuery();
});

Плагин hydrate в данном случае помог нам тем, что добавил метод editText, которого нет по умолчанию. Точнее, у нас есть метод ctx.api.editMessageText, однако в него необходимо передавать id сообщения и id чата, что лишний раз удлиняет код. То есть вместо нашего краткого варианта (await ctx.callbackQuery.message.editText(‘Статус заказа: В пути’, { reply_markup: backKeyboard,});) пришлось бы написать такой:


    await ctx.api.editMessageText( ctx.chat.id,
ctx.update.callback_query.message.message_id, 'Статус заказа: В пути',
{
reply_markup: backKeyboard,
},
);

Удобно? Удобно.

Плагины — очень обширная тема, достойная отдельной статьи. Они помогут вам прокачать навык создания ботов до еще более высокого уровня.

Деплой бота на сервер Selectel

Естественно, от нашего бота не будет толку, если мы так и будем запускать его на компьютере. Чтобы он работал без перебоев, необходимо опубликовать его на удаленном сервере. Делается это просто.

Создание сервера

Сперва загружаем проект на GitHub. Затем заходим в панель управления Selectel, идем в раздел Облачная платформа, выбираем регион и пул, затем нажимаем Создать сервер.

Вводим имя сервера, далее источник — тут нам доступны все свежие дистрибутивы Linux: Ubuntu, Debian, CentOS, Fedora и т. д. Выбираем удобный для нас, пусть останется по умолчанию Ubuntu 22.04 LTS 64-bit 512 МБ.

Конфигурация: выбираем Shared. Она стоит всего от 10 рублей в день — идеально для небольших pet-проектов типа Telegram-ботов, личных VPN и тому подобного. Суть в том, что мы арендуем не весь сервер, а его часть — 10, 20 или 50%. Выбираем 20% и наименьшую конфигурацию оперативки. При этом, кстати, если на сервере не будет других арендаторов, нам автоматически будут доступны 100% его мощности без доплат.

Диск — базовый SSD на 5 ГБ, сеть — новая публичная подсеть, /29.

SSH ключи настраивать не будем, пароль от сервера можем скопировать и сохранить, но не обязательно, далее при входе в консоль он нам снова будет показан.

С конфигурацией все, нажимаем Создать внизу страницы.

Настройка сервера

После завершения откроется список серверов. Теперь нажимаем на название только что созданного и выбираем вкладку Консоль. В ней логинимся: логин — root, пароль написан сразу над открывшейся консолью.

Вводим эти данные и выполняем по очереди все нужные команды для настройки сервера:


    sudo apt update
sudo apt install git

Клонируем репозиторий с ботом на сервер:


    git clone git clone https://github.com/arseniypom/grammy-js.git

После предыдущей команды будет автоматически создана папка с названием репозитория (у меня это grammy-js), переходим в нее:


    cd grammy-js

Следующим шагом устанавливаем Node.js и пакетный менеджер npm:


    sudo apt install nodejs

    sudo apt install npm

Убедимся, что все установилось корректно:


    node -v

    npm -v

Версии этих пакетов по умолчанию ставятся старые, обновим их, используя пакет n:


    sudo npm install -g n

Далее установим последнюю стабильную версию:


    sudo n stable

Заново проверяем версию node. Если у вас, как и у меня, показывается старая версия, перезагрузите сервер кнопкой в правом верхнем углу:

После перезагрузки заново логинимся и перепроверяем версии — теперь они должны быть новыми.

Теперь идем в папку проекта и устанавливаем все зависимости:


    cd grammy-js
npm i

Запуск бота

Осталось запустить бота. Для этого будем использовать менеджер процессов pm2. Установим его глобально (флаг -g):


    npm i pm2 -g

Наконец, запустим бота (не забудьте перед этим убедиться, что остановили его локально, иначе возникнут конфликты):


    pm2 start index.js

Вот такой результат говорит о том, что бот успешно запущен:

Заключение

Мы не только научились создавать Telegram-ботов на grammY, обрабатывать команды и сообщения пользователя, реагировать на нажатия кнопок и коллбэки, но и подробно разобрали деплой на виртуальный сервер. Инструкции из этой статьи можно смело использовать для создания проектов разных масштабов. Библиотека grammY обеспечит штатную работу с Telegram API, а облако Selectel — гибкое масштабирование ресурсов.