Создаём своего Telegram-бота на Node.JS

Создаём своего Telegram-бота на Node.JS

LOLZTEAM

Подробный гайд по созданию бота на Node.JS!

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

Больше интересных статей на нашем форуме: https://lolz.guru/articles/

Подписывайтесь на канал и делитесь ссылкой на статью с друзьями!

В этой теме мы напишем небольшого Telegram-бота на Node.JS с Inline-модом, который будет показывать текущий курс валют. Для взаимодействия с Bot API на Node.JS есть несколько модулей, например:

Но в этой статье мы будем рассматривать разработку бота именно на telegraf.

Шаг 0.1: Установка Node.JS

Для начала нам необходимо установить саму Ноду. Для этого переходим на сайт https://nodejs.org/ru/ и скачиваем LTS-версию.

Установка стандартная, как и в других программах. Просто нажимаем на "Далее". Проблем с этим возникнуть не должно.

Шаг 0.2: Установка IDE

Не советую использовать Блокнот или Notepad++, так как для такого типа проектов они не сильно эффективны. Использование специальных редакторов кода намного уменьшит вам головную боль и упразднит ненужные вопросы по синтаксическим ошибкам.

Для удобного редактирования кода существует несколько хороших программ:

  • Visual Studio Vode (VSC) - быстрый; поддерживает большое колличество языков; имеет встроенный терминал, что есть очень удобно; много плагинов. При написании кода на Ноде, я предпочитаю его.
  • Atom - четкий и понятный интерфейс, легко ориентироваться; открытый исходный код; большое количество плагинов.
  • Sublime Text - также популярный редактор, быстрый и понятный.

Шаг 0.3: Создание папки проекта

Как и любой проект, он должен находиться в своей папке. Для Ноды желательно называть проекты на латинице и без пробелов. (Неправильно: "мой бот", "my bot". Правильно: "mybot" или "my-bot").

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

Шаг 0.4: Использование терминала

Если вы используете VSC в качестве редактора кода, открыть терминал можно прямо в этой программе, используя сочетание клавиш Ctrl + ~ (Ctrl + Shift + ~) или через верхную панель Terminal -> New Terminal.

После того, как вы открыли терминал, необходимо проверить, что установка Node.JS прошла успешно. Написав команду node -v, мы можем убедиться, что все стоит правильно. Если вы получили в ответ что-то по типу v14.15.1 - значит все хорошо и можно приступать к следующему этапу. Если в ответ вы получаете ошибку, попробуйте перезапустить компьютер или попробовать переустановить Ноду.

Шаг 0.5: Инициализация NPM модуля

Далее нам необходимо создать файл package.json, где будет храниться информация о нашем проекте и список зависемостей для него. Сделать это можно таким образом:

Код:

npm init

# Если вы хотите пропустить вопросы
npm init --yes

После написания команды npm init, скрипт спросит вас информацию о проекте (название, ссылка на репозиторий, автор кода, версия). Вообще, это сделано для управлением модулей на npm, но т. к. мы не будем публиковать код в менеджере пакетов, это можно пропустить, используя аргумент --yes. Единственное, что нам надо будет изменить в будущем - это путь к главному файлу проекта, его мы будем хранить в папке src. Изменить это можно прямо в созданном package.json.

Шаг 0.5: Установка nodemon

Во время разработки бота, нужно его постоянно перезагружать вручную (если что, команда node .), чтобы видеть изменения. Но на самом деле, это не так удобно.. Что же люди придумали? - Программа, которая следит за изменением файлов и автоматически перезапускает скрипт. Она как раз nodemon и называется. Установить ее можно так:Код:

npm i nodemon --dev

Аргумент --dev означает, что эта зависимость создана только для удобства разработки и на работу бота не влияет. Когда мы заливаем код на продакшн, эти модули нам не нужны, по этому их нужно различать.

После этого заходим в package.json и создаем новый скрипт для запуска:

Код JavaScript:

"scripts": {
    "dev": "nodemon ."
  },

Теперь для запуска бота нам не нужно постоянно писать node ., а достаточно один раз написать npm run dev. Убить процесс - Ctrl + C.


Шаг 0.6: Установка линтера

Одна из существенных преимуществ специальных IDE перед Блокнотом или Notepad++ - возможность использования линтеров. Они помогают проверять синтаксис, не запуская код и писать всё в одном стиле, правила для которого можно указывать в специальных конфигурационных файлах. Это действительно удобно и при использовании вы сможете понять на сколько.

Для начала необходимо установить модули eslint и prettier (форматор кода), чтобы они были доступны в вашем проекте.

Код:

# Установка локально в проекте
npm i eslint prettier --dev

# Установка глобально
npm i eslint prettier --global

После этого необходимо установить плагины Prettier и ESLint в вашем редакторе.

Шаг 0.7: Настройка линтера

  1. Создайте файл в директории вашего проекта с названием .eslintrc.json
  2. Вставьте код ниже в созданный файл.

Код JavaScript:

{
  "extends": "eslint:recommended",
  "env": {
    "node": true,
    "es6": true
  },
  "parserOptions": {
    "ecmaVersion": 2020
  },
  "rules": {}
}

Так выглядит стандартный конфиг для ESLint. В объекте rules вы можете указывать правила для вашего кода. Полный список стандартных правил вы можете найти на официальном сайте ESLint.

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

Для правильной работы этого конфига необходимо установить дополнительные модули в проект:

Код:

npm i eslint-config-prettier eslint-plugin-import eslint-plugin-prettier --dev

После нужно создать файл под названием .prettierrc с таким содержанием:

Код JavaScript:

{
  "singleQuote": true,
  "printWidth": 120,
  "trailingComma": "all",
  "arrowParens": "avoid"
}

Шаг 1.1: Создание бота

  1. Переходим в бота BotFather
  2. Пишем команду /newbot и указываем спрашиваемые данные. Обратите внимание, что юзернейм бота должен заканчиваться на bot.
Мы получаем токен, с помощью которого мы будем авторизовываться.С помощью токена можно полностью управлять ботом. Храните его в безопасности: не делитесь им с недоверенными людьми.

Используя команду /mybots и выбрав только что созданного бота, мы можем установить ник, аватарку, описание и тд. и тп. Для работы inline-режима, который мы будем разглядывать в этой статье, необходимо включить эту опцию: Bot Settings -> Inline Mode -> Turn On


Шаг 1.2: Хранение токена в проекте

Как было сказано ранее, токен должен быть секретным и доступа к нему ни у кого, кроме доверенных лиц, быть не должно. Мы можем, конечно, хранить токен в специальном JSON-файле и доставать его оттуда, при этом добавив его в .gitignore. Но есть вариант куда удобней - модуль dotenv. С его помощью мы можем хранить токены в файле .env и иметь доступ к указанным переменным в коде без импорта. Например:

Код:

# .env
BOT_TOKEN=1771953780:AAHlcWwBkdEaFqlJLx4ORwiGqVIFJnvufTs

Код JavaScript:

// index.js
require('dotenv').config();
console.log(process.env.BOT_TOKEN);

Чтобы установить данный модуль, необходимо ввести команду:Код:

npm i dotenv

Создаем файл .env в корне проекта с токеном, который мы получили от BotFather в таком виде, который указан выше.

Перед тем, как вы зальете код на GitHub (или на любой другой сайт), хоть в приватный репозиторий, тем более в публичный, проверьте, что файл .env указан в .gitignore, чтобы он не попал в открытый доступ. В стандартном .gitignore от GitHub для Node файл .env уже указан.

Шаг 2.1: Создание главного файла

Главный файл должен находиться по тому пути, который вы указали в package.json в поле main. Здесь мы будем рассматривать бота, файлы которого находятся в папке src, по этому создаём такую папку и там файл index.js.

В самом начале кода мы должны подключить установленный dotenv, для того, чтобы он подгрузил значения. Далее мы импортируем главный класс Telegraf, который мы используем для создания нового бота. Перед этим я достал наш токен из process.env. В самом конце мы запускаем бота, используя метод launch().

Код JavaScript:

require('dotenv').config();

const { Telegraf } = require('telegraf');

const { BOT_TOKEN } = process.env;
const bot = new Telegraf(BOT_TOKEN);
bot.launch();

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

Шаг 2.2: Методы для реагирования на действия пользователя

Для того, чтобы бот реагировал на действия пользователя, в telegraf есть специальные методы:

Код JavaScript:

require('dotenv').config();

const { Telegraf } = require('telegraf');

const bot = new Telegraf(process.env.BOT_TOKEN);
bot.start(ctx => ctx.reply('Добро пожаловать!'));
bot.help(ctx => ctx.reply('Отправь мне стикер'));
bot.on('sticker', ctx => ctx.reply(''));
bot.hears('привет', ctx => ctx.reply('Привет!'));
bot.launch();

В каждом методе мы получаем контекст. Это объект, где хранятся все функции, которые могут быть нам полезны. Для ответа мы используем метод reply, но есть также и другие. Полный список вы можете найти в официальной документации Telegraf. Ниже вы можете видеть, как работает этот код.


Команды /start и /help мы указали специальной для них функцией, но основным методом является command. Ниже представлено две строки, результат будет один и тот же.

Код JavaScript:

bot.start(ctx => ctx.replyWithHTML('<b>Привет!</b>'));
bot.command('start', ctx => ctx.replyWithHTML('<b>Привет!</b>'));

Шаг 3.1: Структура проекта

Как уже было сказано, основной код будет находиться в папке src. Мы будем иметь один главный файл index.js, который будет подключать модули для работы бота. Папка helpers для утилит. Там есть:

  • keyboards.js - отдельный файл для клавиатур, чтобы не засорять обработчики. Тем более клавиатуры в боте могут использоваться не один раз
  • send.js - простая функция для упрощения отправки сообщений. Если обработка нажатия по кнопке - изменить сообщение, иначе отправить содержимое в новом сообщении
  • getExchange.js - функция на получение курса валют, которая обращается к API через axios и возвращает ответ сервера

Папка locales содержит в себе файлы локализации для разных языков. Они хранятся в формате yaml. В данном проекте мы будем иметь только русский язык, по этому будет только 1 файл - ru.yaml.

В директории handlers мы имеем index.js, который собирает все обработчики для удобного импорта. Также для каждого хендлера есть свой файл, который экспортирует функцию.

  • start.js - команда start и нажатие по кнопке с содержимым start
  • select.js - отображение информации о курсе валют. Нажатие по кнопке, которое передает значение выбранной валюты
  • update.js - обновление информации к уже отправленному сообщению о курсе валют. Кнопка "Обновить".
  • inline.js - обработчик для inline режима

Шаг 4.1: Установка telegraf-i18n

Для того, чтобы сильно не засорять сам код текстами для ответа, я использую модуль telegraf-i18n. С его помощью можно не только отделить фразы в отдельный файл, но еще и добавлять языки, помимо русского. Установка:Код:

npm i telegraf-i18n

Шаг 4.2: Конфигурация модуля

Импортируем этот модуль в файле index.js в папке src. Создаём новый инстанс класса с параметрами (все параметры можно посмотреть здесь):

  • язык по умолчанию - русский
  • папка, где находятся файлы с фразами - src/locales

После этого подключаем i18n, как middleware. Это такой промежуточный слой между получением сообщения и его обработкой. Он добавляет в контекст значение i18n, с помощью которого мы сможем быстро указывать фразы.

Код JavaScript:

require('dotenv').config();

const path = require('path');
const { Telegraf } = require('telegraf');
const TelegrafI18n = require('telegraf-i18n');

const { BOT_TOKEN } = process.env;
const bot = new Telegraf(BOT_TOKEN);
const i18n = new TelegrafI18n({
  defaultLanguage: 'ru',
  directory: path.resolve(__dirname, 'locales'),
});

bot.use(i18n.middleware());
bot.launch();

Шаг 4.3: Файл с фразами

В папке src/locales создаём файл ru.yaml. Пока он будет пустым, фразы будем добавлять по мере необходимости в следующих шагах.

Синтаксис очень простой: <name>: <content>. Если надо использовать более, чем одну строку, тогда надо добавить прямую линию ( | ):

Код:

<name>: |
  line one
  line two

В сами фразы можно также переносить значения из кода и использовать их, прямо как в коде, используя ${ }:Код:

start: Привет, ${from.first_name}

Шаг 5.1: Установка axios

Для того, чтобы получать информацию с API, нам необходимо использовать специальную библиотеку. В статье мы будем использовать axios, но есть также и node-fetch. Установка:Код:

npm i axios

Шаг 5.2: Метод для получения курса валют

Для получения курса, мы будем использовать API украинского ПриватБанка. Добавим ссылку на API в файл .env:

Код:

BOT_TOKEN=<TOKEN>
API_URL=https://api.privatbank.ua/p24api/pubinfo?json&exchange&coursid=5

В папке src/helpers создадим файл getExchange.js, в котором будем экспортировать асинхронную функцию, которая обращается к серверу по ссылке, которую указали в .env (await - ожидание ответа, т.к. он приходит не мгновенно). Если произошла ошибка при обработке запроса на сервере, и мы не получили нужную нам информацию, создаём ошибку, показывая, что всё плохо. Результат просто возвращаем.

Код JavaScript:

const axios = require('axios');
const { API_URL } = process.env;

module.exports = async () => {
  const res = await axios.get(API_URL);
  if (!res || res.status !== 200 || !res.data) throw new Error('Ошибка при запросе курса');
  return res.data;
};

Шаг 5.3: Запрос при старте бота

При старте бота будем делать запрос на API, используя только что созданную функцию, во-первых, чтобы проверить работоспособность сервера, во-вторых, чтобы получить список доступных валют и сохранить их в контексте бота. То есть, когда мы будем обрабатывать какое-либо событие, через ctx мы сможем легко получить этот список без дополнительных запросов на сервер. Добавим это в код index.js:

Код JavaScript:

// ...
const getExchange = require('./helpers/getExchange');
// ...
getExchange().then(data => {
  bot.context.currency = data.map(i => i.ccy);
});

Шаг 6.1: Утилита send

Так как, наш бот будет иметь навигацию по меню через кнопки в сообщениях (callback_button), будет удобно при нажатии на кнопку отправлять не новое сообщение, а изменять старое. Чтобы не делать одинаковые хендлеры для разных событий и не засорять код, выделим отдельную функцию для этого, которая будет проверять, если пользователь отправил команду - ответить в новом сообщении, если нажал на кнопку - изменить старое.

Код JavaScript:

module.exports = async (ctx, text, extra) => {
  try {
    if (ctx.updateType === 'message') {
      await ctx.reply(text, extra);
    } else if (ctx.updateType === 'callback_query') {
      await ctx.answerCbQuery();
      await ctx.editMessageText(text, extra);
    }
  } catch (err) {
    console.error(err);
  }
};

Шаг 7.1: Текст для отправки

Перед тем, как начать писать обработчик команды, надо бы создать клавиатуру и текст для ответа. В src/locales/ru.yaml добавим текст для отправки:Код:

start: <b>Выберите тип валюты:</b>

Кстати, можно также использовать HTML для форматирования текста.

Шаг 7.2: Обработчик

Создаём новый файл в папке src/handlers под названием start.js, который будет экспортировать функцию, которая принимает контекст события.

На случай, если случится неожиданная ошибка и бот не стал от нее, вставим наш код в конструкцию try { ... } catch (err) { ... }.

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

Сделаем каждую кнопку с таким содержимым select::<валюта>, например: select::USD. Также, для того, чтобы наши кнопки были не в одной строке, а как-бы "пирамидкой", используем функцию wrap, которую оглашаем после импортов.

Для отправки сообщения, используем ту функцию send, только не забываем ее импортировать. Вначале передаем сам контекст, потом текст, у нас будет тот, который мы указали в ru.yaml. Для того, чтобы его вставить, используем функцию ctx.i18n.t, далее идет клавиатура, с которой рядом режим парсинга сообщения (HTML).

Код JavaScript:

const { Markup, Extra } = require('telegraf');
const send = require('../helpers/send');

const wrap = (btn, index, currentRow) => currentRow.length >= index / 1;

module.exports = ctx => {
  try {
    const markup = Markup.inlineKeyboard(
      ctx.currency.map(c => Markup.callbackButton(c, `select::${c}`)),
      { wrap },
    );
    send(ctx, ctx.i18n.t('start'), Extra.HTML().markup(markup));
  } catch (err) {
    console.error(err);
  }
};

Итоговый результат

Шаг 7.3: Подключение команды

Для того, чтобы удобно импортировать хендлеры одной строкой, создадим src/handlers/index.js, где будем экспортировать хендлеры под нужными именами.

Код JavaScript:

module.exports = {
  startHandler: require('./start'),
};

В src/index.js импортируем этот файл и подключаем, используя методы start и action (чтобы бот реагировал не только на команду, но и нажатие на кнопку с содержимым start.

Код JavaScript:

// ...
const { startHandler } = require('./handlers');
// ...
bot.start(startHandler);

bot.action('start', startHandler);
// ...

Шаг 8.1: Клавиатура и текст для отправки

В файл с фразами добавим два названия кнопок для клавиатуры и сам текст ответа. Мы будем передавать объект course, который получаем с API.

Код:

update: Обновить
back: « Обратно
select: |
  <b>${course.base_ccy} -> ${course.ccy}:</b>

  Покупка: <code>${course.buy}</code>
  Продажа: <code>${course.sale}</code>

Теперь можно перейти к созданию клавиатуры. Создаем файл src/helpers/keyboards.js. Там - стрелочную функцию updateKeyboard, которая принимает контекст для перевода фраз и ответ сервера с курсом. У нас будет 2 кнопки: одна для возврата в главное меню, вторая для обновления информации (обработчик напишем позже). Содержание последней кнопки будет: update::<валюта>::<старая покупка>::<старая продажа>.

Шаг 8.2: Обработчик

Хендлер будет находится по пути src/handlers/select.js. Так же, как и в start.js, экспортируем асинхронную функцию, содержимое вставляем в конструкцию try ... catch, импортируем метод на получения курса валют. Запрашиваем информацию, ищем нужную нам валюту в ответе, и отправляем сообщение.

Итоговый результат:

Шаг 8.3: Подключение обработчика

Все по аналогии, как и с /start. Добавляем хендлер в src/handlers/index.js

Код JavaScript:

module.exports = {
  selectHandler: require('./select'),
  startHandler: require('./start'),
};

Теперь подключаем в главном файле проекта src/index.js. Используем метод action, где в параметрах передаем регулярное выражение:

Код JavaScript:

// ...
const { startHandler, selectHandler } = require('./handlers');
// ...
bot.action(/^select(?:::(\w+))$/, selectHandler);
// ...

Шаг 9.1: Текст для отправки

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

Код:

updated: |
  <b>${course.base_ccy} -> ${course.ccy}:</b>

  Покупка: <code>${course.buy} ${buyDiff ? buyDiff : ''}</code>
  Продажа: <code>${course.sale} ${saleDiff ? saleDiff : ''}</code>

  ${currentTime ? '<i>Последнее обновление в ' + currentTime + '</i>' : ''}

Шаг 9.2: Обработчик

Создаём файл src/handlers/update.js, который по структуре не отличается от прошлых команд. В самом начале мы получаем информацию из содержимого кнопки, которое уже спарсил telegraf, с помощью регулярного выражения. Опять запрашиваем новую информацию о курсе валют, сравниваем значения с помощью функций, которые оглашаем после импорта, после отправляем содержимое.

Код JavaScript:

const getExchange = require('../helpers/getExchange');
const { updateKeyboard } = require('../helpers/keyboards');
const send = require('../helpers/send');

const getCurrentTime = () => new Date(Date.now()).toISOString().substr(11, 8);
const getDiff = (a, b) => (a > b ? a - b : b - a);
const serializeDiff = diff => (diff > 0 ? `(${diff.toFixed(3)} )` : `(${Math.abs(diff.toFixed(3))} )`);

module.exports = async ctx => {
  try {
    const [currency, oldBuy, oldSale] = ctx.match.slice(1);

    const exchangeData = await getExchange();
    const course = exchangeData.find(c => c.ccy === currency);

    const buyDiff = serializeDiff(getDiff(oldBuy, course.buy));
    const saleDiff = serializeDiff(getDiff(oldSale, course.sale));

    send(
      ctx,
      ctx.i18n.t('updated', { course, currentTime: getCurrentTime(), buyDiff, saleDiff }),
      updateKeyboard(ctx, course),
    );
  } catch (err) {
    console.error(err);
  }
};

Итоговый результат:

Шаг 9.3: Подключение обработчика

Все абсолютно одно и тоже самое, что было ранее, отличается только регулярное выражение.

src/handlers/index.js

Код JavaScript:

module.exports = {
  selectHandler: require('./select'),
  startHandler: require('./start'),
  updateHandler: require('./update'),
};

src/index.js

Код JavaScript:

// ...
const { startHandler, selectHandler, updateHandler } = require('./handlers');
// ...
bot.action(/^update(?:::(\w+))(?:::([+-]?[0-9]*[.]?[0-9]+))(?:::([+-]?[0-9]*[.]?[0-9]+))$/, updateHandler);
// ...

Шаг 10.1: Разрешение Inline mode в настройках

Заходим в уже знакомого BotFather, пишем команду /mybots, выбираем нашего бота и включаем этот режим: Bot Settings -> Turn inline mode on. Изменить текст для плейсхолдера, при использовании этого режима (скрин ниже, что это такое), можно в этом же разделе - Edit inline placeholder.

Шаг 10.2: Обработчик

Для начала, нам необходимо добавить небольшую фразу для описания объекта. Добавляем в src/locales/ru.yaml такую строку:

Код:

exchange: Курс ${baseCcy} -> ${ccy}

Здесь мы будем обрабатывать событие inline_query. Нам надо ответить на запрос пользователя. Хотя мы можем получить то, что он вводит с помощью ctx.inlineQuery.query, нам это незачем. Ответ фильтровать будет уже клиент Telegram. По этому, итоговая задача состоит в том, что надо собрать ответы для всех валют и отправить с помощью функции answerInlineQuery:

Код JavaScript:

const getExchange = require('../helpers/getExchange');
const { updateKeyboard } = require('../helpers/keyboards');

module.exports = async ({ answerInlineQuery, i18n }) => {
  try {
    const exchangeData = await getExchange();
    const currencies = exchangeData.map(course => ({
      type: 'article',
      id: course.ccy,
      title: course.ccy,
      input_message_content: {
        message_text: i18n.t('select', { course }),
        parse_mode: 'HTML',
      },
      description: i18n.t('exchange', { ccy: course.ccy, baseCcy: course.base_ccy }),
      reply_markup: updateKeyboard({ i18n }, course).reply_markup,
    }));

    answerInlineQuery(currencies);
  } catch (err) {
    console.error(err);
  }
};

Шаг 10.3: Подключение обработчика

src/handlers/index.js

Код JavaScript:

module.exports = {
  selectHandler: require('./select'),
  startHandler: require('./start'),
  updateHandler: require('./update'),
  inlineHandler: require('./inline'),
};

src/index.js

Код JavaScript:

// ...
const { startHandler, selectHandler, updateHandler, inlineHandler } = require('./handlers');
// ...
bot.on('inline_query', inlineHandler);
// ...

На этом все! Всем спасибо за внимание!



Report Page