Коротко о боте: получает список YouTube-каналов пользователя и уведомляет о новых видео с возможностью напомнить о нем позже.
В статье расскажу об особенностях написания этого бота и взаимодействия с Google API. Я люблю краткость, поэтому в статье будет мало «воды».
На какие вопросы ответит статья:
- Где взять внешний адрес сайта для Webhook
- Где взять HTTPS-сертификат как его использовать, чтобы Telegram ему доверял
- Как передавать данные и обрабатывать нажатия на Inline-кнопки
- Как получить вечный OAuth токен для Google API
- Как передать данные пользователя через OAuth callback url
- Как получить бесплатный домен 3 уровня
Стэк:
- Back-end: Node.js + Express.js
- БД: Mongo.js + mongoose
- Пакетный менеджер: Yarn (он действительно быстрый)
- Telegram-бот фреймворк: Telegraf
- Продакшн: Docker + Docker Compose + Vscale.io
Особенности при разработке бота
Получать команды от Telegram можно с помощью Long-polling и Webhook. Судя по отзывам в интернете Long-polling через некоторое время перестает работать — Telegram возвращает 500 ошибку, поэтому я решил сразу делать через Webhook.
Нужен внешний адрес сайта для Webhook
Webhook — это адрес, на который Telegram будет отправлять команды и сообщения от пользователей, поэтому он должен быть внешний, а как тогда разрабатывать локально?
Тут приходят на помощь сервисы такие как: ngrok и Localtunnel (ссылка 1, ссылка 2).
Оба этих сервиса генерируют случайный домен 3 уровня. Если хочется статический, то в ngrok надо будет заплатить, а в Localtunnel — нет.
Мне нужно было формировать OAuth Callback Url, который привязывался к идентификатору клиента OAuth 2.0 в Google API, поэтому удобнее если он будет статический. По этой причине я использовал именно Localtunnel.
Оба этих сервиса предоставляют HTTPS с нормальным валидным сертификатом, поэтому проблем с Telegram не будет.
В продакшене нужен будет HTTPS
Telegram позволяет использовать самоподписанные сертификаты. Инструкция есть на их официальном сайте. Но тогда браузеры не будут доверять ему, а веть этот же сертификат будет использоваться для OAuth Callback Url, поэтому нужен был валидный сертификат.
На помощь приходит Let’s Encrypt.
Сгенерировать сертификат не проблема в интернете полно инструкций. Единственное, на сколько я понял, его надо генерировать на сервере где он будет использоваться (поправьте, если это не так).
На ubunte я воспользовался пакетом letsencrypt и выполнил команду.
letsencrypt certonly -n -d domain1.com -d domain2.ru --email admin@domain.ru --standalone --noninteractive --agree-tos
Какой сертификат и как его передать Telegram
Для работы Webhook Telegram нужно передать сертификат УЦ, чтобы Telegram начал ему доверять.
В случае с самоподписанным сертификатом — это нужно делать обязательно и передать нужно открытый ключ.
В случае с Let’s Encrypt ничего передавать не нужно, но нужно правильно настроить HTTPS на веб-сервере.
Let’s Encrypt сгенерирует 4 сертификата:
- cert.pem — открытый ключ
- chain.pem — сертификат УЦ
- fullchain.pem — открытый ключ + сертификат УЦ
- privkey.pem — закрытый ключ
Именно privkey.pem+fullchain.pem нужны для HTTPS, если вы используете HAProxy (скорее всего и для других нужно настраивать аналогично), чтобы Telegram начал доверять нашему боту.
Передать этот сертификат через Telegraf можно следующим образом:
let cert = { source: '/path/public.pem' };
app.telegram.setWebhook(config.webHookUrl + '/' + config.webHookSecretPath, cert);
Передача данных при нажатии Inline-кнопок
Отправить сообщение с кнопкой не проблема (можно использовать Telegraf Markup & Extra). Сложности начинаются с передачей данных и отловом нажатия на эту кнопку.
Согласно документации InlineKeyboardButton максимальных размер данных всего 64 байта. В большинстве случаев этого хватает, просто учтите при разработке своего бота.
Дальше нужно эти данные обработать, все callback приходят в одну функцию, поэтому разбирать на какой тип кнопки нажали приходится в ней. А значит вместе с данными нужно еще и тип этот передавать. Напрашивается роутер. В Telegraf этот роутер уже частично реализовали — это класс Router. Его можно создать, но парсить команду и параметры нужно будет самому (пример). Разделитель параметров тоже нужно придумывать самому. На мой взгляд это прошлый век, но с этим можно жить.
Взаимодействие с Google API
Боту нужно получать список каналов пользователя, а для этого нужен токен. Вот так вы сможете его получить:
- Создаем проект через Google Developers Console
- Выбираем нужный API. В моем случае это Youtube Data API
- Создаем идентификатор клиента OAuth 2.0. В поле «Разрешенные URI перенаправления» можно указать localhost с нужным портом
- В результате нам дадут ClientId и ClientSecret
- Ставим Google APIs Node.js Client
- Генерируем на основе этой пары ссылку для пользователя с помощью метода googleapis.auth.OAuth2.generateAuthUrl. Об этом подробно и с примерами написано в описании пакета
- После того как пользователь перейдет по ссылке и даст разрешение, мы получим access-токен
Почему access-токен живет только 1 час
По идее Google с access_type: «offline» должен предоставлять refresh-токен, но так просто он этого не сделает. Мне надо было обновлять список каналов пользователя в фоне, поэтому погуглив я нашел такой вариант: в метод generateAuthUrl передать опцию approval_prompt: 'force'. Тогда Google будет запрашивать у пользователя автономный доступ к аккаунту и, если пользователь согласится, то даст нам нужный refresh-токен. С помощью него мы в любой момент времени сможем обновлять access-токен и получать список каналов.
Как передать данные пользователя через callback url?
Для этого в метод generateAuthUrl можно передать опцию state. Передавать можно только строку, поэтому все объекты нужно сериализовать/кодировать/шифровать.
По переданным данным можно сохранить токен в БД и потом получать его уже по ИД пользователя.
Google не даст токен если callback url это IP
Тут есть 2 пути:
Сначала я искал бесплатный, поэтому поделюсь с вами ссылкой на один из таких сервисов 4nmv — он дает домен 3 уровня бесплатно и позволяет настроить для его любые ns-записи. Наверняка есть и другие сервисы (поделитесь ссылочками, пожалуйста), но меня устроил и этот.
Но потом я всё же купил домен 2 уровня .com за 69 руб в GoDaddy для солидности.
Продакшн
В продакшне я использую Docker и Docker Compose. Они позволяют быстро поднимать и обновлять бота на разных хостингах (если это будет необходимо).
Docker логгер
Разрабатывая на Node.js я писал ошибки и отладочные сообщения в консоль и когда развернул в docker их смотреть стало не удобно.
На помощь может прийти сервис Papertrail, он позволяет сохранять любые сообщения откуда угодно в том числе в формате syslog.
Чтобы передать все сообщения из всех контейнеров можно использовать образ gliderlabs/logspout. Настраивается он очень просто. Вот вырезка из docker-compose.yml
logger:
image: gliderlabs/logspout:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: 'syslog+tls://logsX.papertrailapp.com:PORT'
restart: always
Как запустить Yarn в Docker-контейнере
...
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
RUN $HOME/.yarn/bin/yarn install
...
Результат
Бот запущен в конце декабря 2016 года и доступен по имени @youtube_subs_watcher_bot
Исходники на GitHub
Автор: lomaster