В статье расскажу об особенностях написания этого бота и взаимодействия с 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
Комментарии (29)
aaaler
08.01.2017 13:09+3Docker логгер
у вас очень олдскульный метод описан, коллега.
докер уже умеет писать логи почти куда угодно :
json-file Default logging driver for Docker. Writes JSON messages to file.
syslog Syslog logging driver for Docker. Writes log messages to syslog.
journald Journald logging driver for Docker. Writes log messages to journald.
gelf Graylog Extended Log Format (GELF) logging driver for Docker. Writes log messages to a GELF endpoint like Graylog or Logstash.
fluentd Fluentd logging driver for Docker. Writes log messages to fluentd (forward input).
awslogs Amazon CloudWatch Logs logging driver for Docker. Writes log messages to Amazon CloudWatch Logs.
splunk Splunk logging driver for Docker. Writes log messages to splunk using HTTP Event Collector.
etwlogs ETW logging driver for Docker on Windows. Writes log messages as ETW events.
gcplogs Google Cloud Logging driver for Docker. Writes log messages to Google Cloud Logging.
Carburn
08.01.2017 13:31Какой хостинг вы использовали для бота?
lomaster
08.01.2017 13:31+1Vscale.io
Carburn
08.01.2017 14:10Почему не бесплатный?
Dreyk
08.01.2017 16:20а есть бесплатный хостинг с поддержкой докера?
ingrysty
08.01.2017 17:34+1- AWS with free tier
- https://arukas.io/en/ (free only for beta)
Dreyk
08.01.2017 19:08+1AWS бесплатен только на год
так что постоянного решения нет.
поэтому я и удивляюсь вопросу "почему не на бесплатном хостинге?"
Dreyk
08.01.2017 21:38хотя Arukas заявляют, что
No worries, we'll keep the free-plan below as it is even after Beta phase is ended.
так что это может считаться
el777
08.01.2017 22:09+1Heroku.
Правда поддержка Docker у них пока в бете. Но все остально работает давно, хорошо и надежно.
Проблема с ними только 1 — как только выйдете за бесплатные лимиты, ценник станет конский.Carburn
09.01.2017 08:38По-моему в Heroku лимит есть только по ОЗУ 512*2 МБ…
el777
09.01.2017 16:33+1Это PaaS — там лимит со всех сторон.
PostgreSQL — Бесплатно, но только 10К строк, если хочешь нормальный PG — то +$9 в месяц.
Такая ж история с Redis.
Бесплатно 1 воркер с ограничением по CPU.
Для отладки, теста и обкатки идеи — решение отличное. Хотя есть компании, которые в нем живут и радуются.
el777
08.01.2017 22:11+1У хероку есть один плюс: они сами дают тебе рабочий домен в *.herokuapp.com и https с валидным сертификатом. То есть париться насчет SSL для подключения к телеграму вообще не надо.
lomaster
09.01.2017 09:09Еще кстати OpenShift с докером начинает работать, но там как то всё сложно.
Carburn
09.01.2017 16:03Если PaaS и так поддерживает Node.js, то докер не нужен.
Dreyk
10.01.2017 13:29докер нужен, как сказано в статье, для бістрого развертівания. Например, если хочешь переехать с openshift на heroku, то без докера — это займет какое-то время на настройку окружения и сервисов, а вот с докером это одна команда
lomaster
09.01.2017 09:11Не нашел нормального. Позже поищу еще или в комментах подскажут.
AWS free tier у меня уже кончился.
feverqwe
08.01.2017 16:50+1Оффтоп, а почему тематика бота такая выбрана? Вас не волнует что может набежать много человек и вам придется это все для них поддерживать? Это я к тому, что тоже имею бота с подпиской на ютуб, и там 1k человек, и теперь приходится с этим жить.
lomaster
09.01.2017 09:06У меня подписок на ~30 каналов. Изначально, я хотел просто получать уведомления в Telegram. Вашего бота я тоже начинал использовать, но чет вручную забивать эти 30 каналов мне стало влом. Да и откладывать просмотр хотелось. Вот и написал своего, попрактиковался в node.js.
И поддерживать согласен, по мере возможностей. даже развивать. Долго искал чтобы было еще и не накладно (финансово), но пока не получилось.
Chilace
09.01.2017 08:13+1Изначально бот не видит подписки с ограниченным доступом, в настройках канала надо временно включить показ информации о подписках. Вопрос — после закрытия информации о подписках увидит ли бот новые подписки?
При выводе списка подписок он ограничен 50 пунктами. Вопрос — бот отслеживает все подписки или только те, что в списке?lomaster
09.01.2017 09:08+11. Бот обновляет подписки пользователей раз в 2 часа, если вы подписались или отписались бот это увидит.
2. Бот обрабатывает пока что только первые 50 подписок.Chilace
13.01.2017 09:51- Почему же бот не увидел подписки с ограниченным доступом в первый раз?
- -
- Было бы неплохо добавить команду с функционалом
/logout
, чтобы он сам удалялся отсюда: Приложения, связанные с аккаунтом. - Каким образом на данный момент можно отказаться от уведомлений бота?
Devgru
Спасибо, хорошее саммари.
Пока делал похожий сетап (pm2 с гитхуком на своём сервере вместо докера) столкнулся с очень странной проблемой: бот работает, если запускать HTTPS-сервер с сертификатом letsencrypt но НЕ передавать сертификат самому боту или же если использовать самоподписанный сертификат и передавать его и HTTPS-серверу и боту.