Телеграм-боты на NodeJS

от автора

Предыстория

Несколько месяцев назад как-то больше по приколу написал телеграм-бота с интеграцией 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 секунд и бот удалил сообщение о генерации, и ответил тем же текстом

Прошло 5 секунд и бот удалил сообщение о генерации, и ответил тем же текстом

Попробуем немного изменить это дело. Вместо удаления сообщения и отправки нового сделаем отправку сообщения о генерации, а затем через 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          }      })  }
Меню-клавиатура с параметром resize_keyboard

Меню-клавиатура с параметром resize_keyboard

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

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 с комментариями, посмотреть и скачать можно здесь.


ссылка на оригинал статьи https://habr.com/ru/articles/740796/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *