Предыстория
Несколько месяцев назад как-то больше по приколу написал телеграм-бота с интеграцией 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 с комментариями, посмотреть и скачать можно здесь.
Комментарии (3)
justalike
10.06.2023 17:41Жаль что вы не описали корявость самой библиотеки)
А callback_query listener-ы при кажущейся удобности работы доставляют множество мигреней если сразу их адекватно не структурировать.
Как человек который знаком с этой библиотекой не по-наслышке рекомендую посмотреть альтернативы или обратиться к протоколу MTProto, который позволит сделать юзерботов.
alexchekmenev
10.06.2023 17:41Используемая в статье либа уже давным давно устарела. Лучше взять тот же Telegraf или новый grammY. Код в вашей статье конечно отдельное искусство :D
delphinpro
Вы просто переписали на русском часть документации. Запилите лучше отдельный ресурс с переводом документации. Полезнее будет.