Предыстория
Несколько месяцев назад как-то больше по приколу написал телеграм-бота с интеграцией GPT. Это было, кстати, ещё до того, как весь телеграм утонул в этих ботах. После этого решил, что можно попробовать эту область на фрилансе. За эти месяцы сделал миллион всяких телеграм-ботов с GPT, другими нейронками с доступным API (и даже недоступным в случае с Midjourney), всякие магазины и тому прочих ботов. Этот опыт позволил прошариться немного за телеграм-ботов и в этом материале расскажу об основных моментах, с которыми Вы скорее всего столкнётесь при написании телеграм-ботов на NodeJS. Если есть чем меня дополнить или, возможно, поправить, то буду рад обратной связи.
Начало работы
Для начала создадим NodeJS проект и установим туда пакет для работы с телеграм-ботом через npm:
npm init
npm i node-telegram-bot-api
И инициализируем библиотеку для работы с телеграм-ботом в проект:
const TelegramBot = require('node-telegram-bot-api');
Далее нам нужно создать экземпляр класса TelegramBot. В конструктор нам необходимо передать токен нашего бота (создать бота и получить токен для него можно в BotFather):
const bot = new TelegramBot(process.env.API_KEY_BOT, { polling: true });
Я для хранения таких переменных, как токен бота, использую модуль dotenv, однако, это можно представить в следующем виде:
const API_KEY_BOT = 'Токен от Вашего бота'; const bot = new TelegramBot(API_KEY_BOT, { polling: true });
Polling
Обратите внимание, что вместе с токеном бота я передаю объект, в котором включаю polling - это клиент-серверная технология, которая позволяет нам получать обновления с серверов телеграма. Если пользователь что-то написал боту, мы должны об этом как-то узнать и для этого мы будем с определенной периодичностью опрашивать сервер на предмет наличия новых действий пользователя с ботом. Polling можно просто включить указав ему значение true, но его можно настроить передав в значение объект с настройками, например:
const API_KEY_BOT = 'Токен от Вашего бота'; const bot = new TelegramBot(API_KEY_BOT, { polling: { interval: 300, autoStart: true } });
В данном примере я установил интервал между запросами с клиента на сервер в миллисекундах. autoStart отвечает за то, что наш бот отвечает на те сообщения, которые он пропустил за то время, когда был выключен. Однако, у polling есть ещё настройки, например, можно передать в значение params объект с параметрами такими, как timeout.
Также давайте добавим наш первый слушатель боту - обработаем ошибку polling'а, выведем в ко��соль сообщение ошибки, если она вообще будет:
bot.on("polling_error", err => console.log(err.data.error.message));
Стоит отметить, что получать наш бот информацию о действиях с ним может не только с помощью технологии polling, но и с помощью webhook.
Обработка и отправка текстового сообщения
Далее обработаем сообщения от пользователей. Для этого сначала добавим слушатель текстового сообщения:
bot.on('text', async msg => { console.log(msg); })
В данном примере мы добавили слушатель типа 'text'. Он возвращает нам следующий объект:
{ message_id: ID_СООБЩЕНИЯ, from: { id: ID_ПОЛЬЗОВАТЕЛЯ, is_bot: false, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, language_code: 'ru' }, chat: { id: ID_ЧАТА, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, type: 'private' }, date: 1686255759, text: ТЕКСТ_СООБЩЕНИЯ, }
Данный объект мы получаем в переменную msg и выводим в консоль. Расскажу немного подробнее про то, какие данные возвращает этот объект и на что стоит обратить внимание. В переменной text содержится само сообщение, которое нам написал пользователь. message_id определяет id сообщения, благодаря чему мы сможем далее обратиться к этому сообщению в обработчике слушателя, а объекты from и chat содержат информацию о том, какой пользователь написал это сообщение и в каком чате - чаще всего, например, id чата и id пользователя будут совпадать, однако, пользователи могут добавить бота в чат и писать ему туда - это тоже стоит учесть. Также бывают случаи, когда имя и ник пользователя могут до нас не дойти через слушатель, например, если пользователь закрыл это дело настройками конфиденциальности в телеграме - это тоже стоит учесть, например, в случае если мы хотим как-то обращаться к пользователю по нику или имени от лица бота.
Теперь мы принимаем какую-то информацию, когда пользователь пишет сообщение боту. Давайте с помощью этой информации построим примитивного эхо-бота - наш бот будет отвечать тем же текстом, что и написал нам пользователь. Для этого используем метод sendMessage, в который передадим id чата, в который мы будем отправлять сообщение, и содержание этого сообщения:
bot.on('text', async msg => { await bot.sendMessage(msg.chat.id, msg.text); })
Вот так это дело работает:

Давайте немного усложним нашу конструкцию. Практически это смысла иметь не будет, но позволит нам разобрать ещё несколько методов для работы с ботом. Попробуем сначала выводить пользователю сообщение о том, что бот генерирует ответ на сообщение, а затем через 5 секунд бот будет удалять сообщение о генерации ответа и будет скидывать ответ, в нашем случае то же сообщение, что и скинул нам пользователь:
bot.on('text', async msg => { const msgWait = await bot.sendMessage(msg.chat.id, `Бот генерирует ответ...`); setTimeout(async () => { await bot.deleteMessage(msgWait.chat.id, msgWait.message_id); await bot.sendMessage(msg.chat.id, msg.text); }, 5000); })
В данном примере, мы скидываем пользователю сообщение о генерации и записываем ответ сервера на запрос отправки сообщения в переменную msgWait. Ответ сервера будет объектом того же вида, что и объект msg. Далее, через пять секунд, используем метод deleteMessage для удаления сообщения о генерации и скидываем сам ответ.


Попробуем немного изменить это дело. Вместо удаления сообщения и отправки нового сделаем отправку сообщения о генерации, а затем через 5 секунд отредактируем это сообщение на наш ответ:
bot.on('text', async msg => { const msgWait = await bot.sendMessage(msg.chat.id, `Бот генерирует ответ...`); setTimeout(async () => { await bot.editMessageText(msg.text, { chat_id: msgWait.chat.id, message_id: msgWait.message_id }); }, 5000); })
Для этого мы используем метод editMessageText, в который передаём строку на которую изменим наше сообщение, а также объект с данными о том, в каком чате и какое именно сообщение надо изменить.
Также хочу обратить Ваше внимание на то, что по-хорошему необходи��о все наши методы обернуть в конструкцию try/catch, потому что возможен вариант, при котором наш код отлично отрабатывает, всё хорошо написано, однако, пользователь заблокировал бота, и если бот попытается ему написать сообщение, то сервер нам вернёт ошибку и весь бот может крашнуться.
bot.on('text', async msg => { try { await bot.sendMessage(msg.chat.id, msg.text); } catch(error) { console.log(error); } })
Обрабатываем запуск бота
Далее поговорим про обработку запуска бота. Каждый раз, когда новый пользователь заходит в бота перед ним появляется кнопка "Запустить", вот так это выглядит:

Нажав эту кнопку пользователь отправит боту текстовую команду "/start". Сразу встаёт вопрос о том, как обработать это дело, чтобы поприветствовать нового пользователя и возможно записать какую-то информацию о пользователе в базу данных. Конечно, это можно сделать банальным способом и в слушателе текстового сообщения проверять, является ли сообщение пользователя текстовой командой "/start". Выглядеть это будет так:
bot.on('text', async msg => { try { if(msg.text == '/start') { await bot.sendMessage(msg.chat.id, `Вы запустили бота!`); } else { await bot.sendMessage(msg.chat.id, msg.text); } } catch(error) { console.log(error); } })
Однако, у этого способа есть один минус, с которым лично я столкнулся. В ссылку на запуск бота мы можем передавать параметры и потом читать их. Это полезно, если мы, допустим, захотим сделать реферальную систему. Давайте я покажу, как это будет выглядеть, и в чем проблема. Для этого сделаем обработчик ещё одной команды, назовём её "/ref" и будем по этой команде отдавать пользователю уникальную ссылку на запуск бота:
bot.on('text', async msg => { try { if(msg.text == '/start') { await bot.sendMessage(msg.chat.id, `Вы запустили бота!`); } else if(msg.text == '/ref') { await bot.sendMessage(msg.chat.id, `${process.env.URL_TO_BOT}?start=${msg.from.id}`); } else { await bot.sendMessage(msg.chat.id, msg.text); } } catch(error) { console.log(error); } })
Обратите внимание, для того чтобы передать какую-то информацию при запуске бота, я передаю в ссылку на запуск бота get-параметр с id пользователя, который получил эту ссылку.
Теперь, когда если мы перейдем по ссылке, у нас будет та же самая кнопка "Запустить", которая отправит "/start" боту. Однако, пользователь видит, что он отправил команду "/start", но нам в боте возвращается уже немного другой объект, выглядеть он будет так:
{ message_id: ID_СООБЩЕНИЯ, from: { id: ID_ПОЛЬЗОВАТЕЛЯ, is_bot: false, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, language_code: 'ru' }, chat: { id: ID_ЧАТА, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, type: 'private' }, date: ДАТА, text: '/start ID_ПОЛЬЗОВАТЕЛЯ_ИЗ_РЕФЕРАЛЬНОЙ_ССЫЛКИ', entities: [ { offset: 0, length: 6, type: 'bot_command' } ] }
В текст сообщения нам возвращается уже не просто "/start", а вместе с ним то, что мы передали в get-параметр в ссылке на запуск. Следовательно, нам надо кое-что изменить в обработчике команды "/start".
bot.on('text', async msg => { try { if(msg.text.startsWith('/start')) { await bot.sendMessage(msg.chat.id, `Вы запустили бота!`); if(msg.text.length > 6) { const refID = msg.text.slice(7); await bot.sendMessage(msg.chat.id, `Вы зашли по ссылке пользователя с ID ${refID}`); } } else if(msg.text == '/ref') { await bot.sendMessage(msg.chat.id, `${process.env.URL_TO_BOT}?start=${msg.from.id}`); } else { await bot.sendMessage(msg.chat.id, msg.text); } } catch(error) { console.log(error); } })
Как видите, сначала мы изменили проверку с равенства текста сообщения команде "/start", на проверку, начинается ли текст сообщения с команды "/start". Затем проверяем есть ли ещё какие-то параметры в команде запуска, проверяя длину сообщения (6 в данном случае это длина строки "/start"), а затем вырезаем из текста сообщения команду "/start" вместе с пробелом после неё методом slice и записываем то, что мы передаём в ссылке на запуск бота в переменную.
Сделать это можно и другим способом, используя слушатель onText и регулярные выражения:
bot.onText(/\/start/, async msg => { try { await bot.sendMessage(msg.chat.id, `Вы запустили бота!`); if(msg.text.length > 6) { const refID = msg.text.slice(7); await bot.sendMessage(msg.chat.id, `Вы зашли по ссылке пользователя с ID ${refID}`); } } catch(error) { console.log(error); } })
Слушатель принимает регулярное выражение, по которому будет проверять сообщение. Однако, если Вы будете применять данный способ, учтите, что если пользователь запустит бота, то сработает и слушатель onText с регулярным выражением, и слушатель on с типом 'text'.
Меню команд для бота
Поговорим о том, как создать меню команду для бота. Выглядит меню команд следующим образом:

Создать это меню можно в BotFather или с помощью метода setMyCommands. Второй способ мне кажется удобнее и быстрее. В этот метод нам нужно передать массив объектов, в которых указаны сами команды и их описания в меню.
const commands = [ { command: "start", description: "Запуск бота" }, { command: "ref", description: "Получить реферальную ссылку" }, { command: "help", description: "Раздел помощи" }, ] bot.setMyCommands(commands);
Сначала мы задаём массив объектов с нашими командами, а затем передаём его в метод setMyCommands. Я создал команды, которые мы уже использовали до этого, а также создал новую команду help, давайте обработаем её в слушателе:
bot.on('text', async msg => { try { if(msg.text.startsWith('/start')) { await bot.sendMessage(msg.chat.id, `Вы запустили бота!`); if(msg.text.length > 6) { const refID = msg.text.slice(7); await bot.sendMessage(msg.chat.id, `Вы зашли по ссылке пользователя с ID ${refID}`); } } else if(msg.text == '/ref') { await bot.sendMessage(msg.chat.id, `${process.env.URL_TO_BOT}?start=${msg.from.id}`); } else if(msg.text == '/help') { await bot.sendMessage(msg.chat.id, `Раздел помощи`); } else { await bot.sendMessage(msg.chat.id, msg.text); } } catch(error) { console.log(error); } })
Форматирование текста
Далее разберемся в форматировании текста в сообщениях. Разберем на примере обработки команды "/help". Для форматирования, стилизации, текста можно использовать либо HTML-верстку, либо Markdown-верстку. Для этого необходимо передавать в строку сообщения текст с тегами, а также передать объект с параметром parse_mode в метод sendMessage. Выглядеть это будет примерно так:
else if(msg.text == '/help') { await bot.sendMessage(msg.chat.id, `Раздел помощи HTML\n\n<b>Жирный Текст</b>\n<i>Текст Курсивом</i>\n<code>Текст с Копированием</code>\n<s>Перечеркнутый текст</s>\n<u>Подчеркнутый текст</u>\n<pre language='c++'>код на c++</pre>\n<a href='t.me'>Гиперссылка</a>`, { parse_mode: "HTML" }); await bot.sendMessage(msg.chat.id, 'Раздел помощи Markdown\n\n*Жирный Текст*\n_Текст Курсивом_\n`Текст с Копированием`\n~Перечеркнутый текст~\n``` код ```\n||скрытый текст||\n[Гиперссылка](t.me)', { parse_mode: "MarkdownV2" }); }
Тогда команда "help" будет выводить нам следующее:

Вот список тегов с помощью которых Вы можете стилизовать текст в телеграм-ботах:
HTML:
<b> Текст </b> - Жирный текст
<i> Текст </i> - Текст курсивом
<code> Текст </code> - Текст, который можно скопировать нажатием на него
<s> Текст </s> - Перечеркнутый текст
<u> Текст </u> - Подчеркнутый текст
<pre language='язык'> Текст </pre> - Текст с оформлением кода
<a href='ссылка'> Текст </a> - Текст-гиперссылка
Markdown:
*Текст* - Жирный текст
_Текст_ - Текст курсивом
`Текст` - Текст, который можно скопировать нажатием на него
~Текст~ - Перечеркнутый текст
``` Текст ``` - Текст с оформлением кода
|| Текст || - Скрытый текст
[Текст](Ссылка) - Текст-гиперссылка
Стоит отметить, что все теги должны быть обязательно закрыты, иначе бот не отправит сообщение и вернёт ошибку. В этом плане, я советую делать стилизацию именно с помощью HTML-тегов, так как могут быть проблемы, если вы делаете админку или взаимодействие пользователей со стилизованным текстом. Например, если закинуть ссылку обычным текстом, а не гиперссылкой в сообщение, у которого parse_mode стоит на Markdown, то все нижние подчеркивания будут именно тегами, не отобразятся пользователю, и если их нечетное количество, то сообщение вообще не отправится. Также обратите внимание на то, что переход на следующую строку выполняется при помощи "\n" и в стилизации HTML, и в стилизации Markdown, здесь нельзя использовать тег <br> из HTML.
Также если мы хотим вставить эмодзи в наше сообщение, то можно просто скопировать эмодзи из телеграмма и вставить в нашу строку в коде, например:
await bot.sendMessage(msg.chat.id, `Вы запустили бота! 👋🏻`);

Также ещё подробнее поговорим про ссылки. Если мы укажем ссылку в сообщении, то сообщение придёт пользователю с превью ссылки. Добавлю для этого команду "/link" и её обработчик. Выглядит это так:
else if(msg.text == '/link') { await bot.sendMessage(msg.chat.id, `https://habr.com/`); }

Если мы хотим убрать превью в сообщении, то нам необходимо передать в метод sendMessage объект с параметром disable_web_page_preview со значением true:
else if(msg.text == '/link') { await bot.sendMessage(msg.chat.id, `https://habr.com/`, { disable_web_page_preview: true, }); }


Кстати, в этот же объект, помимо parse_mode и disable_web_page_preview, мы можем передать параметр disable_notification - это позволит отправить сообщение пользователю без уведомления:
else if(msg.text == '/link') { await bot.sendMessage(msg.chat.id, `https://habr.com/`, { disable_web_page_preview: true, disable_notification: true }); }
Меню-клавиатура

Далее обсудим меню-клавиатуру. Меню-клавиатуры делятся на два типа: то меню, которое находится рядом с вводом текста, и меню, которое привязано к сообщению. Между ними есть некоторая разница, о которой мы поговорим дальше. Давайте для начала попробуем создать меню, которое не привязано к сообщению - для удобства я буду его дальше называть просто клавиатура. Для этого создадим ещё одну команду "menu" и обработаем её:
else if(msg.text == '/menu') { await bot.sendMessage(msg.chat.id, `Меню бота`, { reply_markup: { keyboard: [ ['⭐️ Картинка', '⭐️ Видео'], ['⭐️ Аудио', '⭐️ Голосовое сообщение'] ] } }) }
В объект, в который мы раньше передавали параметры disable_web_page_preview, disable_notification, parse_mode теперь передаём reply_markup, который содержит массив массивов keyboard. Обратите внимание, что каждый массив в keyboard - это отдельная строка сверху вниз. То есть мы задаём массивами строки меню, а внутри строк задаём сами кнопки с помощью строк. Выглядеть то, что написано выше, в боте будет так:

Можем заметить, что кнопки получились какими-то большими. Для того, чтобы задать им адекватные размеры, можно в объект reply_markup передать параметр resize_keyboard со значением true. Давайте так и сделаем, и добавим в меню ещё несколько кнопок:
else if(msg.text == '/menu') { await bot.sendMessage(msg.chat.id, `Меню бота`, { reply_markup: { keyboard: [ ['⭐️ Картинка', '⭐️ Видео'], ['⭐️ Аудио', '⭐️ Голосовое сообщение'], ['⭐️ Контакт', '⭐️ Геолокация'], ['❌ Закрыть меню'] ], resize_keyboard: true } }) }

Далее встаёт вопрос о том, как обработать нажатие кнопки в нашей клавиатуре. Тут всё на самом деле просто. Когда пользователь нажимает на кнопку в меню, он скидывает боту текстовое сообщение с тем текстом, который написан на кнопке, который мы указывали в строках внутри массивов. Следовательно, будем обрабатывать нажатие на кнопку, как обычное текстовое сообщение:
else if(msg.text == '❌ Закрыть меню') { await bot.sendMessage(msg.chat.id, 'Меню закрыто', { reply_markup: { remove_keyboard: true } }) }
В данном примере, мы обработали нажатие кнопки закрытия меню. Обратите внимание на то, что если Вы просто отправите сообщение пользователю, клавиатура у него никуда не пропадёт. Для того, чтобы выключить клавиатуру, передаём параметр remove_keyboard со значением true в reply_markup. Как видим, клавиатура у нас пропала:

О том, как создать меню привязанное к сообщению поговорим чуть дальше.
Скидываем и обрабатываем изображение
Сейчас поговорим о том, как скинуть пользователю картинку, и как обработать сообщение с картинкой от пользователя.
Давайте будем скидывать картинку пользователю по нажатию кнопки "⭐️ Картинка" в меню. Саму кнопку мы уже создали, давайте обработаем её нажатие и с помощью метода sendPhoto, в который передадим ссылку на изображение, которое хотим скинуть пользователю:
else if(msg.text == '⭐️ Картинка') { await bot.sendPhoto(msg.chat.id, process.env.URL_TO_IMG); }
В данном случае, мы скидываем пользователю картинку с помощью ссылки на изображение, однако, мы можем скинуть картинку и просто указав путь до картинки:
else if(msg.text == '⭐️ Картинка') { await bot.sendPhoto(msg.chat.id, './image.jpg'); }
Также можно скинуть картинку используя модуль fs:
else if(msg.text == '⭐️ Картинка') { //Скидываем изображение ссылкой await bot.sendPhoto(msg.chat.id, process.env.URL_TO_IMG); //Скидываем изображение указав путь await bot.sendPhoto(msg.chat.id, './image.jpg'); //Скидываем изображение с помощью Readable Stream const imageStream = fs.createReadStream('./image.jpg'); await bot.sendPhoto(msg.chat.id, imageStream); //Скидываем изображение с помощью буфера const imageBuffer = fs.readFileSync('./image.jpg'); await bot.sendPhoto(msg.chat.id, imageBuffer); }
Теперь наш бот будет вести себя примерно так:

Бот скидывает только картинку. Однако, нам может понадобится добавить какую-то подпись к этой картинке. Для этого передаем в метод sendPhoto объект с опциями, надпись можно передать в параметр caption, также можем задать parse_mode, как и в обычном текстовом сообщении:
const imageStream = fs.createReadStream('./image.jpg'); await bot.sendPhoto(msg.chat.id, imageStream, { caption: '<b>⭐️ Картинка</b>', parse_mode: 'HTML' });
Далее разберемся с тем, как обработать сообщение с изображением от пользователя. Для этого используем слушатель с типом "photo":
bot.on('photo', async img => { console.log(img); })
Теперь, когда пользователь скинет сообщение с картинкой, телеграм вернёт нам объект следующего вида:
{ message_id: 500, from: { id: ID_ПОЛЬЗОВАТЕЛЯ, is_bot: false, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, language_code: 'ru' }, chat: { id: ID_ЧАТА, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, type: 'private' }, date: ДАТА, photo: [ { file_id: ID_ФАЙЛА, file_unique_id: УНИКАЛЬНЫЙ_ID_ФАЙЛА, file_size: РАЗМЕР_ФАЙЛА, width: ШИРИНА_КАРТИНКИ, height: ВЫСОТА_КАРТИНКИ }, { file_id: ID_ФАЙЛА, file_unique_id: УНИКАЛЬНЫЙ_ID_ФАЙЛА, file_size: РАЗМЕР_ФАЙЛА, width: ШИРИНА_КАРТИНКИ, height: ВЫСОТА_КАРТИНКИ }, { file_id: ID_ФАЙЛА, file_unique_id: УНИКАЛЬНЫЙ_ID_ФАЙЛА, file_size: РАЗМЕР_ФАЙЛА, width: ШИРИНА_КАРТИНКИ, height: ВЫСОТА_КАРТИНКИ }, { file_id: ID_ФАЙЛА, file_unique_id: УНИКАЛЬНЫЙ_ID_ФАЙЛА, file_size: РАЗМЕР_ФАЙЛА, width: ШИРИНА_КАРТИНКИ, height: ВЫСОТА_КАРТИНКИ } ] }
Мы похожее уже видели, когда обрабатывали текстовое сообщение. Только теперь мы имеем дело не со строкой text, а с массивом объектов photo. В этом массиве содержится наша картинка. Телеграм принял картинку, хранит у себя на сервере, и вернул нам несколько вариантов нашей картинки в разных размерах. Последний объект в этом массиве - это наш оригинал, а другие объекты содержат нашу картинку, только в сжатом виде. Но как же получить картинку, которую скидывал пользователь по информации содержащейся в объектах? Мы можем получить доступ к нашей картинке по file_id, который возвращает нам телеграм. Например, мы можем скачать файл по file_id с серверов телеграм используя метод downloadFile, в который передаём file_id, который нужно скачать, и директорию в которую будем скачивать файл. Выглядеть это будет так:
bot.on('photo', async img => { try { await bot.downloadFile(img.photo[img.photo.length-1].file_id, './image'); } catch(error) { console.log(error); } })
Обратите внимание, что для того, чтобы выбрать наш оригинал в индексе массива я указал длину массива минус один - не будем забывать, что мы, программисты, считаем с нуля.
Давайте теперь сделаем следующее: когда пользователь скидывает нам изображение, будем скидывать ему в ответ несколько изображений объединенных в одно сообщение. Эти несколько изображений - это наш оригинал и сжатые его варианты. Для этого воспользуемся методом sendMediaGroup:
bot.on('photo', async img => { try { const photoGroup = []; for(let index = 0; index < img.photo.length; index++) { const photoPath = await bot.downloadFile(img.photo[index].file_id, './image'); photoGroup.push({ type: 'photo', media: photoPath, caption: `Размер файла: ${img.photo[index].file_size} байт\nШирина: ${img.photo[index].width}\nВысота: ${img.photo[index].height}` }) } await bot.sendMediaGroup(img.chat.id, photoGroup); for(let index = 0; index < photoGroup.length; index++) { fs.unlink(photoGroup[index].media, error => { if(error) { console.log(error); } }) } } catch(error) { console.log(error); } })
Остановимся на этом поподробнее. В самом начале мы инициализируем массив. Он нам нужен для того, чтобы передать несколько картинок в метод sendMediaGroup и соответственно скинуть эти картинки пользователю. Метод sendMediaGroup принимает массив объектов, в которых мы указываем первым делом тип медиа-контента, в нашем случае это "photo". Также в объекте медиа-контента мы должны передать путь до нашего контента, этим путём могут служить, также как и в случае с методом sendPhoto: url, stream, buffer или путь до контента. Также мы можем указать file_id - его можно тоже указывать при отправке через sendPhoto или через sendMediaGroup, однако, в данном случае этот способ нам не подходит, так как какой бы file_id мы не указали, мы всегда будем скидывать оригинал картинки, а нас интересуют именно сжатые картинки. Дополнительно в объект медиа-контента мы можем передать caption - подпись под картинкой. В данном случае, я добавил в подпись под каждой картинкой её размер в байтах, ширину и высоту в пикселях - всё это нам возвращает телеграм. Теперь пройдясь циклом по всем вариантам нашей картинки, скачав все варианты и записав всю необходимую информацию, передаём наш массив объектов в метод sendMediaGroup, а затем удаляем все картинки, которые только что скачали. И теперь имеем следующее:


Скидываем и обрабатываем видео
Далее поговорим о том, как скинуть видео. Тут в общем-то всё аналогично фото, обработаем нажатие на кнопку на "⭐️ Видео" в нашем меню:
else if(msg.text == '⭐️ Видео') { await bot.sendVideo(msg.chat.id, './video.mp4'); }
Также мы туда можем добавить caption и parse_mode, и другие параметры, вроде disable_notification:
else if(msg.text == '⭐️ Видео') { await bot.sendVideo(msg.chat.id, './video.mp4', { caption: '<b>⭐️ Видео</b>', parse_mode: 'HTML' }); }
Получаем следующее:

Теперь давайте обработаем сообщение пользователя с видео. Делаем это также с помощью слушателя с типом "video":
bot.on("video", async video => { console.log(video); })
Телеграм присылает нам следующее, когда пользователь отправляет сообщение с видео:
{ message_id: ID_СООБЩЕНИЯ, from: { id: ID_СООБЩЕНИЯ, is_bot: false, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, language_code: 'ru' }, chat: { id: ID_ЧАТА, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, type: 'private' }, date: ДАТА, video: { duration: ДЛИТЕЛЬНОСТЬ_ВИДЕО, width: ВЫСОТА_ВИДЕО, height: ШИРИНА_ВИДЕО, file_name: ИМЯ_ФАЙЛА, mime_type: ТИП_ФАЙЛА, thumbnail: { file_id: ID_ИЗОБРАЖЕНИЯ, file_unique_id: УНИКАЛЬНОЕ_ID_ИЗОБРАЖЕНИЯ, file_size: РАЗМЕР_ИЗОБРАЖЕНИЯ, width: ШИРИНА_ИЗОБРАЖЕНИЯ, height: ВЫСОТА_ИЗОБРАЖЕНИЯ }, thumb: { file_id: ID_ИЗОБРАЖЕНИЯ, file_unique_id: УНИКАЛЬНОЕ_ID_ИЗОБРАЖЕНИЯ, file_size: РАЗМЕР_ИЗОБРАЖЕНИЯ, width: ШИРИНА_ИЗОБРАЖЕНИЯ, height: ВЫСОТА_ИЗОБРАЖЕНИЯ }, file_id: ID_ВИДЕО, file_unique_id: УНИКАЛЬНОЕ_ID_ВИДЕО, file_size: РАЗМЕР_ВИДЕО } }
Как видим, телеграм предоставляет нам информацию о видео и изображениях. Изображения - это миниатюры видео. Давайте сделаем следующее: когда пользователь скидывает видео, бот будет в ответ кидать само видео, миниатюру и информацию о видео. Выглядеть это будет следующим образом:
bot.on("video", async video => { try { const thumbPath = await bot.downloadFile(video.video.thumbnail.file_id, './image'); await bot.sendMediaGroup(video.chat.id, [ { type: 'video', media: video.video.file_id, caption: `Название файла: ${video.video.file_name}\nВес файла: ${video.video.file_size} байт\nДлительность видео: ${video.video.duration} секунд\nШирина кадра в видео: ${video.video.width}\nВысота кадра в видео: ${video.video.height}` }, { type: 'photo', media: thumbPath, } ]); fs.unlink(thumbPath, error => { if(error) { console.log(error); } }) } catch(error) { console.log(error); } })

Обратите внимание, что видео-файл я скидываю по его file_id, а миниатюру скачиваю, скидываю и затем удаляю, потому что телеграм не даёт скинуть миниатюру методом sendPhoto по file_id. Также важно, что в этот раз я добавил в методе sendMediaGroup параметр caption лишь к одному медиа-файлу и надпись отобразилась в сообщении.
Также есть важный момент, который нужно учесть. Иногда, когда пользователь скидывает видео, допустим в том же .mp4, телеграм может это сжать и скинуть в .gif.
Скидываем и обрабатываем аудио
Ситуация уже знакомая нам, скидываем аудио также, как видео и фото, обрабатывая кнопку "⭐️ Аудио":
else if(msg.text == '⭐️ Аудио') { await bot.sendAudio(msg.chat.id, './audio.mp3', { caption: '<b>⭐️ Аудио</b>', parse_mode: 'HTML' }); }
Обработка аудио выполняется тоже похожим образом, только используем слушатель с типом "audio":
bot.on('audio', async audio => { try { await bot.sendAudio(audio.chat.id, audio.audio.file_id, { caption: `Название файла: ${audio.audio.file_name}\nВес файла: ${audio.audio.file_size} байт\nДлительность аудио: ${audio.audio.duration} секунд` }) } catch(error) { console.log(error); } })

Телеграм-сервер возвращает похожий на предыдущие примеры объект:
{ message_id: 653, from: { id: 764548588, is_bot: false, first_name: 'shavrin', username: 'zloishavrin', language_code: 'ru' }, chat: { id: 764548588, first_name: 'shavrin', username: 'zloishavrin', type: 'private' }, date: 1686339341, audio: { duration: 1, file_name: 'audio.mp3', mime_type: 'audio/mpeg', file_id: 'CQACAgIAAxkBAAICjWSDfw0AAZdXcrZjG-2n840P-NqNIQACOzEAAi3wIUh2fGtPn59fBi8E', file_unique_id: 'AgADOzEAAi3wIUg', file_size: 19776 } }
Хочу обратить внимание на то, что если пользователь скинет несколько видео, фото или аудио одним сообщением, то они не вернутся Вам одним сообщением с массивом audio или video-объектов, просто сработают несколько слушателей и эти файлы будут считаться отдельными сообщениями и обрабатываться будут также отдельно.
С голосовыми ситуация аналогичная, ничего интересного:
else if(msg.text == '⭐️ Голосовое сообщение') { await bot.sendVoice(msg.chat.id, './audio.mp3', { caption: '<b>⭐️ Голосовое сообщение</b>', parse_mode: 'HTML' }); }
И также обрабатываем голосовые сообщения:
bot.on('voice', async voice => { try { await bot.sendAudio(voice.chat.id, voice.voice.file_id, { caption: `Вес файла: ${voice.voice.file_size} байт\nДлительность аудио: ${voice.voice.duration} секунд` }) } catch(error) { console.log(error); } })

Скидываем, запрашиваем и обрабатываем контакт
Дальше разберемся с тем, как скинуть пользователю контакт. Для этого обработаем кнопку "⭐️ Контакт", используя метод sendContact, в который передадим строку с номером телефона и именем контакта:
else if(msg.text == '⭐️ Контакт') { //Скидываем контакт await bot.sendContact(msg.chat.id, process.env.CONTACT, `Контакт`, { reply_to_message_id: msg.message_id }); }
Обратите внимание, что в телеграме стоит проверка на формат номера, и если мы укажем не номер, то телеграм вернёт нам ошибку.
Также я добавил параметр reply_to_message_id - в этот параметр мы можем передать message_id сообщения, на которое мы хотим ответить сообщением, в которое передаем message_id.

Теперь давайте немного изменим нашу кнопку. И вместо того, чтобы просто скидывать контакт, мы сначала будем его запрашивать, а затем обрабатывать. Для этого изменим обработку команды "/menu", в которой мы присылаем пользователю нашу клавиатуру:
else if(msg.text == '/menu') { await bot.sendMessage(msg.chat.id, `Меню бота`, { reply_markup: { keyboard: [ ['⭐️ Картинка', '⭐️ Видео'], ['⭐️ Аудио', '⭐️ Голосовое сообщение'], [{text: '⭐️ Контакт', request_contact: true}, '⭐️ Геолокация'], ['❌ Закрыть меню'] ], resize_keyboard: true } }) }
Как видите, теперь передаём вместо просто строки '⭐️ Контакт' объект с параметрами text и request_contact, тем самым запрашивая контакт пользователя при нажатии на кнопку:

Теперь обработаем контакт. Для этого добавляем слушатель с типом "contact". Этот слушатель вернёт нам следующий объект, если пользователь скинул контакт:
{ message_id: ID_СООБЩЕНИЯ, from: { id: ID_ПОЛЬЗОВАТЕЛЯ, is_bot: false, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, language_code: 'ru' }, chat: { id: ID_ЧАТА, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, type: 'private' }, date: ДАТА, reply_to_message: { message_id: ID_СООБЩЕНИЯ_В_КОТОРОМ_СКИДЫВАЛИ_КНОПКУ, from: { id: ID_БОТА, is_bot: true, first_name: ИМЯ_БОТА, username: НИК_БОТА }, chat: { id: ID_ЧАТА, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, type: 'private' }, date: ДАТА_СООБЩЕНИЯ_В_КОТОРОМ_СКИДЫВАЛИ_КНОПКУ, text: ТЕКСТ_СООБЩЕНИЯ_В_КОТОРОМ_СКИДЫВАЛИ_КНОПКУ }, contact: { phone_number: НОМЕР_КОНТАКТА, first_name: ИМЯ_КОНТАКТА, user_id: ID_ПОЛЬЗОВАТЕЛЯ } }
Давайте обработаем это следующим образом: когда боту приходит контакт, бот скидывает номер и имя контакта, отвечая на сообщение с контактом:
bot.on('contact', async contact => { try { await bot.sendMessage(contact.chat.id, `Номер контакта: ${contact.contact.phone_number}\nИмя контакта: ${contact.contact.first_name}`); } catch(error) { console.log(error); } })
Выглядеть это будет так:

Скидываем, запрашиваем и обрабатываем геолокацию
Обработаем нажатие кнопки "⭐️ Геолокация" и будем скидывать пользователю геолокацию Красной Площади, указав широту и долготу нужной нам координаты:
else if(msg.text == '⭐️ Геолокация') { const latitudeOfRedSquare = 55.753700; const longitudeOfReadSquare = 37.621250; await bot.sendLocation(msg.chat.id, latitudeOfRedSquare, longitudeOfReadSquare, { reply_to_message_id: msg.message_id }) }

Теперь провернём тоже самое, что мы делали с контактами, только для геолокации - сделаем запрос геолокации по кнопке и будем возвращать в ответ на сообщение с геолокацией координаты геолокации (широту и долготу). Начнём с того, что изменим кнопку "⭐️ Геолокация":
else if(msg.text == '/menu') { await bot.sendMessage(msg.chat.id, `Меню бота`, { reply_markup: { keyboard: [ ['⭐️ Картинка', '⭐️ Видео'], ['⭐️ Аудио', '⭐️ Голосовое сообщение'], [{text: '⭐️ Контакт', request_contact: true}, {text: '⭐️ Геолокация', request_location: true}], ['❌ Закрыть меню'] ], resize_keyboard: true } }) }

Далее обработаем сообщение пользователя с геолокацией. Сообщение с геолокацией возвращает следующий объект:
{ message_id: ID_СООБЩЕНИЯ, from: { id: ID_ПОЛЬЗОВАТЕЛЯ, is_bot: false, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, language_code: 'ru' }, chat: { id: ID_ЧАТА, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, type: 'private' }, date: ДАТА, reply_to_message: { message_id: ID_СООБЩЕНИЯ_В_КОТОРОМ_СКИДЫВАЛИ_КНОПКУ, from: { id: ID_БОТА, is_bot: true, first_name: ИМЯ_БОТА, username: НИК_БОТА }, chat: { id: ID_ЧАТА, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, type: 'private' }, date: ДАТА_СООБЩЕНИЯ_В_КОТОРОМ_СКИДЫВАЛИ_КНОПКУ, text: ТЕКСТ_СООБЩЕНИЯ_В_КОТОРОМ_СКИДЫВАЛИ_КНОПКУ }, location: { latitude: ШИРОТА_ГЕОЛОКАЦИИ, longitude: ДОЛГОТА_ГЕОЛОКАЦИИ } }
Сделаем следующий слушатель:
bot.on('location', async location => { try { await bot.sendMessage(location.chat.id, `Широта: ${location.location.latitude}\nДолгота: ${location.location.longitude}`); } catch(error) { console.log(error); } })

Делаем ещё одно меню
Выше я уже говорил, что меню можно создать нескольких видов. Выше мы уже создали меню-клавиатуру и меню команд. Теперь создадим меню, как я писал выше, привязанное к сообщению. Далее будем называть это инлайн-клавиатура. Для этого создадим новую команду и обработаем её:
else if(msg.text == '/second_menu') { await bot.sendMessage(msg.chat.id, `Второе меню`, { reply_markup: { inline_keyboard: [ [{text: 'Стикер', callback_data: 'sticker'}, {text: 'Круглое Видео', callback_data: 'circleVideo'}], [{text: 'Купить Файл', callback_data: 'buyFile'}], [{text: 'Проверить Подписку', callback_data: 'checkSubs'}], [{text: 'Закрыть Меню', callback_data: 'closeMenu'}] ] } }) }
Как видим мы также, как с обычной клавиатурой, создаём массив массивов в объекте reply_markup. Каждый массив в inline_keyboard - это новая строка в меню. В отличие от обычной клавиатуры, мы передаём в массив не строки, а объекты с текстом и callback_data. Обрабатывать мы будем нажатия кнопок не обычным текстовым сообщением, а уже коллбеками. Вот так будет выглядеть созданная нами инлайн-клавиатура:

Теперь давайте попробуем обработать кнопку "Закрыть Меню". Для этого будем использовать слушатель с типом "callback_query":
bot.on('callback_query', async ctx => { try { console.log(ctx); } catch(error) { console.log(error); } })
Коллбеки возвращают нам с серверов телеграм следующий объект:
{ id: ID_КОЛЛБЕКА, from: { id: ID_ПОЛЬЗОВАТЕЛЯ, is_bot: false, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, language_code: 'ru' }, message: { message_id: ID_СООБЩЕНИЯ, from: { id: ID_БОТА, is_bot: true, first_name: ИМЯ_БОТА, username: НИК_БОТА }, chat: { id: ID_ЧАТА, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, type: 'private' }, date: ДАТА, text: ТЕКСТ_СООБЩЕНИЯ, reply_markup: { inline_keyboard: [Array] } }, chat_instance: ЗАВИСИМЫЙ_ЧАТ, data: КОЛЛБЕК_ДАТА }
Как видим, в объекте возвращается информация о сообщении, к которому привязана кнопка, на которую нажали, информация о пользователе, который нажал кнопку, и самое важное - callback_data, которую мы указывали в кнопке. С помощью вот этого параметра callback_data будем обрабатывать нажатие на кнопку:
bot.on('callback_query', async ctx => { try { switch(ctx.data) { case "closeMenu": await bot.deleteMessage(ctx.message.chat.id, ctx.message.message_id); break; } } catch(error) { console.log(error); } })
В данном случае, мы удаляем сообщение с нашей инлайн-клавиатурой по нажатию кнопки "Закрыть Меню". Дальше в этом же слушателе будем обрабатывать нажатия других кнопок по параметру callback_data.
Теперь давайте немного модифицируем всё это дело. И наше меню будем скидывать не просто сообщением по команде, а в ответ на сообщение-команду:
else if(msg.text == '/second_menu') { await bot.sendMessage(msg.chat.id, `Второе меню`, { reply_markup: { inline_keyboard: [ [{text: 'Стикер', callback_data: 'sticker'}, {text: 'Круглое Видео', callback_data: 'circleVideo'}], [{text: 'Купить Файл', callback_data: 'buyFile'}], [{text: 'Проверить Подписку', callback_data: 'checkSubs'}], [{text: 'Закрыть Меню', callback_data: 'closeMenu'}] ] }, reply_to_message_id: msg.message_id }) }
Теперь мы можем сделать следующее: в обработчике закрытия меню удалять не только сообщение с самим меню, а ещё и удалять сообщение-команду, по которому было вызвано меню:
case "closeMenu": await bot.deleteMessage(ctx.message.chat.id, ctx.message.message_id); await bot.deleteMessage(ctx.message.reply_to_message.chat.id, ctx.message.reply_to_message.message_id); break;
Мы можем такое сделать, потому что тогда наш коллбек будет возвращать следующий объект:
{ id: ID_КОЛЛБЕКА, from: { id: ID_ПОЛЬЗОВАТЕЛЯ, is_bot: false, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, language_code: 'ru' }, message: { message_id: ID_СООБЩЕНИЯ, from: { id: ID_БОТА, is_bot: true, first_name: ИМЯ_БОТА, username: НИК_БОТА }, chat: { id: ID_ЧАТА, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, type: 'private' }, date: ДАТА, reply_to_message: { message_id: ID_СООБЩЕНИЯ_2, from: [Object], chat: [Object], date: ДАТА, text: КОМАНДА_ВЫЗОВА_МЕНЮ, entities: [Array] }, text: ТЕКСТ_СООБЩЕНИЯ, reply_markup: { inline_keyboard: [Array] } }, chat_instance: ЗАВИСИМЫЙ_ЧАТ, data: КОЛЛБЕК_ДАТА }
Скидываем и обрабатываем стикеры
Далее обработаем нажатие кнопки "Стикер" на нашей инлайн-клавиатуре. Для этого будем использовать метод sendSticker:
case "sticker": await bot.sendSticker(ctx.message.chat.id, `./image.jpg`); break;
Хочу обратить ваше внимание на то, что мы можем скинуть любое изображение стикером, аналогично тому, как мы это делали с методом sendPhoto.
Теперь обработаем сообщение пользователя со стикером с помощью слушателя с типом "sticker", который будет возвращать нам следующий объект:
{ message_id: ID_СООБЩЕНИЯ, from: { id: ID_ПОЛЬЗОВАТЕЛЯ, is_bot: false, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, language_code: 'ru' }, chat: { id: ID_ЧАТА, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, type: 'private' }, date: ДАТА, sticker: { width: ШИРИНА_СТИКЕРА, height: ДЛИНА_СТИКЕРА, emoji: '😊', //ЭМОДЗИ К КОТОРОМУ ПРИВЯЗАН СТИКЕР set_name: ИМЯ_СТИКЕРА, is_animated: false, //БУЛЕВА ПЕРЕМЕННАЯ, КОТОРАЯ ОТОБРАЖАЕТ АНИМИРОВАН СТИКЕР ИЛИ НЕТ is_video: false, //БУЛЕВА ПЕРЕМЕННАЯ, КОТОРАЯ ОТОБРАЖАЕТ СТИКЕР ВИДЕОФОРМАТА ИЛИ НЕТ type: 'regular', thumbnail: { file_id: ID_МИНИАТЮРЫ, file_unique_id: УНИКАЛЬНЫЙ_ID_МИНИАТЮРЫ, file_size: РАЗМЕР_МИНИАТЮРЫ, width: ШИРИНА_МИНИАТЮРЫ, height: ДЛИНА_МИНИАТЮРЫ }, thumb: { file_id: ID_МИНИАТЮРЫ, file_unique_id: УНИКАЛЬНЫЙ_ID_МИНИАТЮРЫ, file_size: РАЗМЕР_МИНИАТЮРЫ, width: ШИРИНА_МИНИАТЮРЫ, height: ДЛИНА_МИНИАТЮРЫ }, file_id: ID_ФАЙЛА_СТИКЕРА, file_unique_id: УНИКАЛЬНЫЙ_ID_ФАЙЛА_СТИКЕРА, file_size: РАЗМЕР_ФАЙЛА_СТИКЕРА } }
Давайте теперь сделаем следующее: когда пользователь скидывает стикер, будем проверять, какого вида стикер используя информацию из объекта (булевы переменные) и будем скидывать стикер пользователю картинкой, видео или анимацией:
bot.on('sticker', async sticker => { try { const stickerPath = await bot.downloadFile(sticker.sticker.file_id, './image'); if(sticker.sticker.is_video) { await bot.sendVideo(sticker.chat.id, stickerPath); } else if(sticker.sticker.is_animated) { await bot.sendAnimation(sticker.chat.id, stickerPath); } else { await bot.sendPhoto(sticker.chat.id, stickerPath); } fs.unlink(stickerPath, error => { if(error) { console.log(error); } }) } catch(error) { console.log(error); } })

Обратите внимание, что я сделал некрасиво: в любом случае, будь стикер анимацей, видео или картинкой скачиваю файл в папку с названием "image" - так лучше, конечно, не делать. Также я не отправляю файлы по file_id, а скачиваю и затем их отправляю, потому что если попробовать скинуть стикер-файл по file_id используя методы sendPhoto или sendVideo, телеграм вернёт ошибку о том, что нельзя скинуть файл типа "стикер", как "фото" или "видео", однако, метод sendAnimation позволяет отправлять стикеры анимацией по их file_id, тогда это будет выглядеть следующим образом:
else if(sticker.sticker.is_animated) { await bot.sendAnimation(sticker.chat.id, sticker.sticker.file_id); }
Круглое видео
Обработаем кнопку "Круглое Видео", по нажатию на которую будем скидывать пользователю видео круглого формата. Сделать это можно с помощью метода sendVideoNote:
case "circleVideo": await bot.sendVideoNote(ctx.message.chat.id, './video.mp4'); break;
В общем-то с круглыми видео ничего интересного нету - всё аналогично обычному видео. Кстати, в объект опций мы можем передать параметр protect_content со значением true и пользователь не сможет пересылать сообщение:
case "circleVideo": await bot.sendVideoNote(ctx.message.chat.id, './video.mp4', { protect_content: true }); break;
Проверка подписки на канал
Далее разберемся с тем, как сделать, чтобы бот проверял подписку на канал. Для этого необходимо поставить бота админом канала, на который он будет проверять подписку и использовать метод getChatMember:
case "checkSubs": const subscribe = await bot.getChatMember(process.env.ID_CHAT, ctx.from.id); console.log(subscribe); break;
В этот метод передаём ID нашего канала или чата - получить этот ID можно, например, скинув ссылку на канал или чат в специального бота, который возвращает ID, он будет в формате "-100XXXXXXXXXX", и также передаём в этот метод ID пользователя, подписку которого будем проверять. Данный метод вернёт объект такого вида:
{ user: { id: ID_ПОЛЬЗОВАТЕЛЯ, is_bot: false, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, language_code: 'ru' }, status: СТАТУС_ПОЛЬЗОВАТЕЛЯ, is_anonymous: false }
Будем обращаться к статусу пользователя. Статусы бывают следующие:
left - пользователь не подписан
kicked - пользователь заблокирован
member - пользователь подписан
administrator - пользователь является администратором
creator - пользователь является создателем
Обработаем статус пользователя и в зависимости от него будем выводить пользователю сообщение:
case "checkSubs": const subscribe = await bot.getChatMember(process.env.ID_CHAT, ctx.from.id); if(subscribe.status == 'left' || subscribe.status == 'kicked') { await bot.sendMessage(ctx.message.chat.id, `<b>Вы не являетесь подписчиком!</b>`, { parse_mode: 'HTML' }) } else { await bot.sendMessage(ctx.message.chat.id, '<b>Вы являетесь подписчиком!</b>', { parse_mode: 'HTML' }) } break;
Подключаем оплату
Для того, чтобы подключить оплату к своему бота для начала стоит определиться с платежной системой через которую будут совершаться платежи. У телеграма есть хороший выбор стандартных платежный систем, однако, никто не запрещает Вам подключить стороннюю платежную систему, но сейчас разберемся именно со стандартными телеграмовскими платежными системами. Для их подключения необходимо перейти в BotFather, выбрать своего бота, перейти в раздел Payments, а дальше в выбранной Вами платежной системе получить provider token - ключ, по которому мы будем подключать оплату.
Давайте отправим счёт на оплату пользователю по нажатию кнопки "Купить Файл". Для этого будем использовать метод sendInvoice:
case "buyFile": await bot.sendInvoice(ctx.message.chat.id, 'Купить Файл', 'Покупка файла', 'file', process.env.PROVIDER_TOKEN, 'RUB', [{ label: 'Файл', amount: 20000 }]); break;
В этот метод мы передаём название платежа, описание платежа, payload - это информация, которая передаётся в платеж, по ней мы будем отслеживать нужный платеж, а у пользователя она нигде не отобразится, провайдер-токен, валюта (в разных платежках поддерживаются разные валюты) и также массив объектов с товарами, которые будет оплачивать пользователь. Обратите внимание, что валютой я указал рубли (код валюты в ISO 4217), а цену на товар указал в копейках. Также можно в метод передать другие параметры, можно добавить миниатюру платежа, включить необходимость ввода некоторых данных для пользователя, а также данные для фискализации платежа.
Далее по кнопке "Купить Файл" пользователю отправится следующее сообщение:

Далее пользователь нажмёт кнопку оплаты, введёт свои данные, и когда подтвердит введённые данные, бот должен отправить окончательное подтверждение перед оформлением заказа. Сделать это можно при помощи обработки такого слушателя:
bot.on('pre_checkout_query', async ctx => { try { await bot.answerPreCheckoutQuery(ctx.id, true); } catch(error) { console.log(error); } })
Когда мы окончательно подтвердили оформление заказа, пользователь может совершить платеж и мы должны его как-то обработать. Для этого будем использовать слушатель с типом "successful_payment", который возвращает объект следующего вида:
{ message_id: ID_СООБЩЕНИЯ, from: { id: ID_ПОЛЬЗОВАТЕЛЯ, is_bot: false, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, language_code: 'ru' }, chat: { id: ID_ЧАТА, first_name: ИМЯ_ПОЛЬЗОВАТЕЛЯ, username: НИК_ПОЛЬЗОВАТЕЛЯ, type: 'private' }, date: ДАТА, successful_payment: { currency: КОД_ВАЛЮТЫ, total_amount: СУММА_ПЛАТЕЖА, invoice_payload: PAYLOAD, telegram_payment_charge_id: ID_ПЛАТЕЖА_ТЕЛЕГРАМ, provider_payment_charge_id: ID_ПЛАТЕЖА_ПЛАТЕЖНАЯ_СИСТЕМА } }
Исходя из этой информации мы можем обработать совершённый платеж, например:
bot.on('successful_payment', async ctx => { try { await bot.sendDocument(ctx.chat.id, `./${ctx.successful_payment.invoice_payload}.txt`, { caption: `Спасибо за оплату ${ctx.successful_payment.invoice_payload}!` }) } catch(error) { console.log(error); } })
Используем метод sendDocument для того, чтобы скинуть пользователю файл, который мы находим благодаря информации из payload, и в подписи к нему выводим информацию из payload, которую мы изначально передали в методе sendInvoice. Пользователь увидит следующее:

Заключение
В этом материале постарался максимально просто и с примерами рассказать об основных возможностях телеграм-ботов и как ими управлять с помощью NodeJS. Тут в принципе есть ещё о чем рассказать: веб-хуки для телеграм ботов, управление чатами, каналами, игры и веб-приложения в телеграм-ботах. Если кому-то будет интересна эта тема, то продолжу об этом писать.
Также выложил код всего, что было написано в этой статье на GitHub с комментариями, посмотреть и скачать можно здесь.
