Разрабатываем Telegram-бота для отслеживания фильмов на NodeJS и TypeScript +10




У вас бывало такое, что вы приходите в кино и смотрите трейлеры перед началом фильма, при этом некоторые из них цепляют вас достаточно сильно и вы даете себе обещание посмотреть этот фильм если не в кинотеатре, то хотя бы позже, когда он станет доступен для скачивания? Смею предположить, что да. Но часто заканчивается это тем, что вы успешно забываете про это и, в лучшем случае, случайно находите этот фильм пролистывая список уже вышедших фильмов.

Я достаточно часто сталкивался с такой проблемой и решил ее созданием Eve — Telegram-бота, который помогает мне не забывать про релизы моих любимых фильмов. Что это, как оно работает и как разрабатывалось вы можете почитать под катом!



Предисловие


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

Зачем?


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

Следующей идеей было создание PWA. Вполне себе интересный вариант для того, чтобы познакомиться с технологией, но ее я решил оставить на будущее. В то же самое время я достаточно давно пользуюсь Telegram и за все это время у меня скопилось достаточное количество ботов, которые периодически делают мою жизнь немного проще. В конечном счете взвесив все плюсы и минусы, я решил, что данная платформа отлично подойдет для данной идеи.

Выбор технологий


Если прошерстить Github на предмет наличия Telegram-ботов, то можно увидеть, что большая часть из них написана на Python. Python действительно замечательный язык и отлично подходит для данной идеи, но мне хотелось реализовать данный проект именно на NodeJS + TypeScript. Применив соответствующие фильтры я наткнулся на 2 достаточно популярных инструмента: node-telegram-bot-api и Telegraf.js.

До этого опыта разработки ботов у меня не было, поэтому при выборе из этих двух было не совсем понятно на какие параметры следует смотреть. В итоге посмотрев документацию и полистав issues к каждой из библиотек, я остановился на Telegraf.js. Решающим фактором стало наличие механизма middlewares, который устроен таким же образом, как и в популярном фреймворке Express.js. Также Telegraf.js имеет более частую историю обновлений и хорошо структурированную документацию, из чего можно предположить, что разработчики прикладывают достаточно усилий для улучшения инструмента. Более того, он содержит тайпинги для TypeScript, что определенно является плюсом для выбранного стека.

Telegraf.js


Telegraf.js — основная библиотека, на которой был написан проект. Она использует несколько интересных подходов, которые позволяют создавать сложных ботов с сохранением простоты кода. Ниже представлены наиболее интересные механизмы:

  • Middlewares — да, это те же самые middlewares, которые доступны в Express.js. Когда пользователь что-то отправляет боту, сообщение вначале проходит через все зарегистрированные middlewares, а уже после попадает в контроллер. Middlewares могут модифицировать контекст, а также приостанавливать выполнение запроса, например, если обычный пользователь попытается попасть в область для администраторов. Telegraf.js предоставляет несколько крайне полезных middlewares «из коробки», но также можно использовать и разработанные лично. Именно middlewares считаются киллер-фичей Telegraf.js
  • Sessions — сессии, которые могут хранить в себе информацию, не привязанную к контроллеру. Можно привести аналогию с глобальными переменными, которые доступны из любого места в боте. Очень удобная вещь, которая используется, например, для локализации. Есть возможность хранить сессии в разных режимах — БД, redis, локальные файлы и т.д.
  • Webooks — бот может работать в двух режимах — long polling или Webhooks. И хотя работают они одинаково быстро, все же в production среде лучше использовать второй вариант. Таким образом, боту не нужно будет стучаться на сервер Telegram, вместо этого он сам будет получать все обновления.

    Важно: обязательным условием для Webhook'ов является поддержка сервером SSL/TLS. Также убедитесь, что порт, на котором запущен Webhook, открыт и доступен извне.
  • Markup — с помощью этого класса можно научить бота отвечать в markup/markdown разметке. Это важно если вы, например, делаете HTML-игру. Тем не менее в своем проекте я использовал его только для bold/italic текста.
  • Stage — крайне интересный модуль, который помог мне сохранить нервы и некоторое количество часов разработки. Здесь я расскажу подробнее.
    Бот умеет слушать сообщения от пользователя и, в зависимости от типа или содержимого сообщения, перенаправлять его в тот или иной контроллер, который выполнит свою функцию и отправит ответ пользователю. Выглядит это следующим образом:

    bot.hears('hello', async ctx => {
      await ctx.reply('Hello!');
    });
    

    Работает это просто: если написать боту hello, то он просто ответит Hello! Зная этот аспект работы, попробуем представить, как может работать бот для поиска фильмов:

    1. Пользователь пишет, что хочет перейти к поиску фильмов;
    2. Пользователь вбивает название фильма;
    3. Бот возвращает список фильмов и предлагает пользователю сохранить выбранный фильм в свою библиотеку;
    4. ...

    Внимание, вопрос! Как бот сможет различать, когда пользователь пишет ему, что хочет начать искать фильм, а когда он пишет само название фильма для поиска?

    Да, можно любой присланный текст принимать за название фильма, но мы же хотим сделать бота с блэкджеком и путанами хорошим интерфейсом.

    И тут в игру вступают сцены (Stages)! Легче всего будет рассказать на примере выше. Когда пользователь говорит, что хочет перейти к поиску фильмов, мы запускаем сцену. Внутри этой сцены стоят свои отдельные обработчики текста — bot.hears(...). Таким образом, все последующие сообщения будут относиться непосредственно к поиску фильмов до тех пор, пока пользователь не напишет сообщение для выхода и не покинет сцену. После этого обработчики внутри этой сцены деактивируются до тех пор, пока пользователь не вернется туда снова. Далее я расскажу, как я использовал сцены, чтобы сделать обработку событий максимально простой.


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


Есть подозрение, что статья получится достаточно объемной, а потому, чтобы не растягивать ее еще больше, я опущу некоторые моменты. Например, чтобы бот начал работать, необходимо начать с получения Telegram Bot API token. Прочитать о том, как это сделать, можно в официальной документации Telegram или же в документации к Telegraf.js.

Файловая структура проекта выглядит следующим образом:

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


Работает это следующим образом:

  • src/controllers — это и есть те самые сцены, про которые было написано выше. В данном проекте каждая сцена представляет из себя отдельный контроллер, который занимается обработкой соответствующих запросов.
  • src/locales — переводы для разных языков. Telegraf.js позволяет относительно просто добавить локализацию к боту с использованием middlewares.
  • src/models — модели для MongoDB.
  • src/types — тайпинги для TypeScript. К сожалению, не все используемые в проекте библиотеки имеют тайпинги по умолчанию.
  • src/util — различные функции-помощники, которые используются в разных частях проекта. Здесь можно посмотреть как работает проверка доступности фильма для скачивания, управление сессиями, создание клавиатур, обработчики ошибок и еще много всего интересного.
  • src/bot.ts — главный файл, где производится вся подготовка и запуск бота.
  • src/telegram.ts — в данном случае мы создаем объект класса Telegram из библиотеки Telegraf.js. С помощью этого объекта мы можем отправлять пользователям сообщения первыми, используя их ID, а не ждать, пока они что-то напишут. Например, в данном проекте мы отправляем пользователю сообщение о том, что фильм, за которым он следил, уже можно скачать. Конечно же, данный объект предоставляет гораздо больше возможностей.
  • Все остальное — конфигурации для разных частей проекта, которые не будут рассмотрены в этой статье

Инициализация и запуск


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

Начинается все с подключения к БД, где хранится информация о пользователях и отслеживаемых ими фильмах. После успешного подключения мы регистрируем все используемые в проекте сцены, задаем параметры для локализации и добавляем несколько middlewares — обработку сессий, локализацию, настройку сцен, а также несколько собственных. Одна из них, например, получает всю информацию о пользователе в соответствии с его ID и добавляет ее к контексту, который используется в контроллерах. Наконец, после всех основных приготовлений, мы запускаем бота либо в development (long polling), либо в production (Webhooks) режиме.

Важно: если вы используете разные методы получения обновлений (long polling и Webhooks), то при запуске бота в режиме long polling сперва удалите слушающий Webhook с помощью GET запроса на api.telegram.org/botYOUR_TOKEN/deleteWebhook. В противном случае бот может работать неправильно.

Обрабатываем пользовательский ввод


Ура! Бот работает, подключен к базе данных и готов принимать сообщения от пользователей. Но как правильно это сделать?

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

В файле bot.ts как раз стоят такие обработчики. Так как бот поддерживает два языка, то и кнопки могут содержать разный текст — на русском и английском языках. В telegraf-i18n есть функция match, которая умеет обрабатывать нажатие одной и той же кнопки с разными языками.

Большинство обработчиков в bot.ts выполняют единственную функцию — они запускают пользователя в соответствующую сцену. Так, у нас есть несколько разделов — поиск фильмов, моя коллекция, настройки и контакты. Для каждого из разделов есть своя сцена и своя кнопка, при нажатии на которую пользователь перемещается в соответствующую сцену.

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

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

Внутри сцены можно использовать свои middlewares, которые лежат в файле middlewares.ts. Например, используя middleware в сцене поиска, мы можем достаточно просто пробросить всю информацию о фильме в соответствующие методы, а не выполнять одну и ту же функцию каждый раз внутри них.

В Telegram также есть inline-клавиатура. Возможно вы встречали сообщения с голосованиями, под которыми есть несколько полупрозрачных кнопок и при нажатии на одну из них количество голосов меняется. Эти кнопки и есть inline-клавиатура

Вот как это выглядит у Eve


В каждой кнопке содержится информация, при нажатии на кнопку она будет передана в соответствующий обработчик. Размер передаваемой информации не должен превышать 64 байта! Чтобы научить бота слушать нажатия на кнопки, нам нужно зарегистрировать их с помощью bot.action(/trigger/, callback). В первый параметр попадают все данные, которые были привязаны к кнопке. Я решил использовать нечто вроде Actions из Redux, где к каждой кнопке привязан объект вида {a: actionName, p: payload}. При регистрации listeners мы можем использовать простой RegExp, например: bot.action(/actionName/, callBack). Все обработчики для inline-клавиатуры находятся в файлах actions.ts.

Помимо этого, в некоторых сценах лежат файлы helpers.ts, которые содержат в себе небольшие функции вынесенные туда с целью разгрузить остальные файлы. По большей части там лежат генераторы клавиатур для разных действий со стороны пользователя.

Локализация


Так как это важная тема, я думаю ее стоит упомянуть отдельно. Как я говорил ранее, Telegraf.js содержит довольно большое количество middlewares, одной из которых является telegraf-i18n. В репозитории присутствует подробная инструкция и особых проблем у меня с этим не возникло, но все же я добавлю пару слов о том, как это работает в данном проекте.

Есть папка locales, где лежат файлы для локализации, которые представляют из себя JSON объект вида { «ключ»: «перевод» }. Далее, везде, где нам нужно использовать разные языки, мы используем метод из этой библиотеки, куда передаем нужный нам перевод по ключу, а на выходе получаем соответствующий перевод. Для хранения информации о выбранном пользователем языке может использоваться сессия. Также еще раз стоит упомянуть про кнопки. В этой же библиотеке есть функция match, поэтому если текст на кнопке меняется в зависимости от языка, то эта функция поможет вам повесить правильный listener.

Важно: если вы собираетесь использовать локализацию и писать бота на TypeScript, не забудьте добавить папку с переводами в tsconfig.json, иначе код не скомпилируется. Например:

"include": ["src/locales/*.json"]

Заключение


Спасибо что дочитали до конца! В этой статье я постарался максимально подробно описать процесс создания Telegram-бота в целом, без сильной привязки к своему проекту. Я надеюсь, что после прочтения этой статьи, а также изучения исходного кода Eve, вы сможете с легкостью создать бота, который сможет помочь вам.

Как и обещал, исходный код вы можете посмотреть на GitHub, а попробовать Eve в деле вот здесь. Я буду безмерно благодарен за любую критику и предложения по улучшению.

Отдельно хочу отметить раздел в документации Telegraf.js с интересными open source проектами, на которые можно посмотреть и вдохновиться архитектурой и решениями. Я, в свою очередь, хочу отметить один из них — The Guard Bot. Поистине большой и хорошо сделанный бот, откуда я позаимствовал часть решений для своей разработки.

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




К сожалению, не доступен сервер mySQL