Особенности разработки Telegram бота с Google API в Docker

Коротко о боте: получает список YouTube-каналов пользователя и уведомляет о новых видео с возможностью напомнить о нем позже.

В статье расскажу об особенностях написания этого бота и взаимодействия с Google API. Я люблю краткость, поэтому в статье будет мало «воды».
На какие вопросы ответит статья:

  • Где взять внешний адрес сайта для Webhook
  • Где взять HTTPS-сертификат как его использовать, чтобы Telegram ему доверял
  • Как передавать данные и обрабатывать нажатия на Inline-кнопки
  • Как получить вечный OAuth токен для Google API
  • Как передать данные пользователя через OAuth callback url
  • Как получить бесплатный домен 3 уровня

Стэк:
  1. Back-end: Node.js + Express.js
  2. БД: Mongo.js + mongoose
  3. Пакетный менеджер: Yarn (он действительно быстрый)
  4. Telegram-бот фреймворк: Telegraf
  5. Продакшн: 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


Боту нужно получать список каналов пользователя, а для этого нужен токен. Вот так вы сможете его получить:
  1. Создаем проект через Google Developers Console
  2. Выбираем нужный API. В моем случае это Youtube Data API
  3. Создаем идентификатор клиента OAuth 2.0. В поле «Разрешенные URI перенаправления» можно указать localhost с нужным портом
  4. В результате нам дадут ClientId и ClientSecret
  5. Ставим Google APIs Node.js Client
  6. Генерируем на основе этой пары ссылку для пользователя с помощью метода googleapis.auth.OAuth2.generateAuthUrl. Об этом подробно и с примерами написано в описании пакета
  7. После того как пользователь перейдет по ссылке и даст разрешение, мы получим 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

Комментарии (1)

  • 8 января 2017 в 02:27

    0

    Спасибо, хорошее саммари.

    Пока делал похожий сетап (pm2 с гитхуком на своём сервере вместо докера) столкнулся с очень странной проблемой: бот работает, если запускать HTTPS-сервер с сертификатом letsencrypt, но НЕ передавать сертификат самому боту или же если использовать самоподписанный сертификат и передавать его и HTTPS-серверу и боту.

© Habrahabr.ru