Прием уведомлений от внешних сервисов, или зачем был сделан Hooksler

от автора

В последнее время большую популярность приобрел сервис для командной коммуникации Slack. Из коробки он имеет немалое количество интеграций с различными сервисами + довольно удобное внешнее API. Но при всем при этом на бесплатных аккаунтах есть ограничение в 5 интеграций. Прицепили мы github, newrelic + пару досок с trello и все, количество их закончилось. Можно использовать универсальный Incoming WebHook, но он само собой имеет свой формат и никак не совместим с другими сервисами. Но программист не был бы программистом, если бы не решил эту задачу.

Решение простое как молоток. Принимаем хуки от сервисов на себя, обрабатываем и кидаем в Slack в том виде, в котором нам нужно.
На ряду с интеграциями, в списке был обнаружен hammock, который написан на PHP и имеет некоторый набор плагинов, но данное решение не особо понравилось. Хоть и есть готовые интеграции, но увы, тех что нужно нет, а так как я знаком с PHP на уровне чтения кода и «что-то поправить по справочнику», то писать свои не было желания.

Потому принял решения написать свой сервис. Строить решил полностью на модульной основе: ядро и отдельно модули для «ввода» и «вывода». В качестве языка использовал Ruby, его динамическая природа очень помогла в реализации задуманного.

И так, встречайте,

Hooksler!

Позволяет с минимумом кода собрать сервис для приема уведомлений и дальнейшей их отправки. Для конфигурирования используется свой DSL:

require 'hooksler/slack' require 'hooksler/newrelic' require 'hooksler/trello' require 'dotenv'  Dotenv.load  Hooksler::Router.config do   secret_code 'very_secret_code'   host_name 'http://example.com'    endpoints do     input  'simple',        type: :simple     input  'newrelic',      type: :newrelic     input  'trello',        type: :trello,            create: false,            public_key: ENV['TRELLO_KEY'],            member_token: ENV['TRELLO_TOKEN'],            board_id: ENV['TRELLO_ID1']      output 'black_hole', type: :dummy     output 'slack_out', type: :slack, url: ENV['SLACK_WEBHOOK_URL'], channel: '#test'   end    route 'simple'       => 'slack_out'   route 'trello'         => ['black_hole', 'slack_out']   route 'newrelic'    => ['black_hole', 'slack_out'] end 

В начале объявляются точки ввода вывода, каждая имеет свое имя и тип, а так же может содержать дополнительные параметры для инициализации. Далее указываются маршруты. Можно указывать в разном виде: один к одному, один ко многим и наоборот.
Так же на каждый маршрут можно повесить фильтры, которые могут как модифицировать сообщение, так и фильтровать его. Таким образом получаем достаточно гибкое ядро для маршрутизации сообщений из точки A в точку B.
Сообщения внутри передаются во внутреннем представлении, при этом известно из какого сервиса (его тип) оно было получено + исходное сообщение. При получении заполняются типичные поля: пользователь, текст, заголовок, ссылка, уровень. В дальнейшем они могут использоваться для формирования уведомления.
На текущий момент полностью реализовано, проверено и покрыто тестами ядро. Так же реализовано несколько интеграций: trello, newrelic, slack. Свои интеграции написать очень просто.

Немного практики

Прием сообщений

Для примера сделаем модуль, который позволит помещать тело POST запроса в поле message.

class DummyInput   extend Hooksler::Channel::Input   register :dummy    def initialize(params)     @params = params   end    def load(request)     build_message({}) do |msg|       msg.message = request.body.read     end   end end 

Объявим класс и расширим его соответствующим модулем. После чего зарегистрируем его имя. Все, после этого мы готовы принимать и обрабатывать входящие данные. Обработка запросов выполняется в методе load, принимающий лишь один параметр — объект класса Rack::Request. Никакой сложной обработки нам не требуется, поэтому сразу создаем сообщение и заполняем поле. После этого оно пойдет далее по описанным в конфигурации маршрутам. Для отправки может быть создано несколько сообщений сразу, т.е. метод load вернет массив. В дальнейшем каждый объект обрабатывается отдельно.

Отправка сообщений

Не менее просто сделать модуль для отправки, который позволит нам видеть полученные сообщения в консоли

class DummyOutput   extend Hooksler::Channel::Output   register :dummy    def initialize(params)     @params = params   end    def dump(message)     puts "-- #{message.title} : #{message.level} --"     puts message.user     puts message.message     puts message.url   end end 

Выполняем аналогичные действия что и для входящего, только выбираем соответствующий модуль расширения. Отправка, в нашем случае вывод в консоль, выполняется в методе dump. Имя метода спорное, но send уже было занято, переопределять не хотелось.

Теперь соберем это все и опишем маршруты:

 Hooksler::Router.config do   secret_code 'very_secret_code'   host_name 'http://example.com'    endpoints do     input  'in',        type: :dummy     output 'out',    type: :dummy   end    route 'in'  => 'out' end 

Указываем код, который используется для генерации путей и хост, на котором будет висеть наш сервис. Запускаем и готово. Конечные пути можно глянуть обратившись по адресу http://example.com/_endpoints_, в ответе будет JSON. Более развернутый пример можно посмотреть в DEMO приложении: github.com/hooksler/hooksler-demo

Таким образом, без больших усилий можно настроить пересылку сообщений одновременно в разные точки: получать изменения из Trello, пересылать их в Slack, либо особо важные (например содержащие ключевые слова или метки) отправлять через push на телефон. Можно придумать кучу схем, благо основа гибкая.

Более практичный пример

На днях встала задача автоматизировать процесс приглашения пользователей в Slack. Добавлять каждого вручную — долго и нудно, а сделать отрытую регистрацию из коробки нельзя. В интернете есть готовая форма на nodejs. Но т.к. у себя уже держу работающий hooksler решил сделать на нем. Для начала, нужно как-то получить корректную почту, для этого воспользовался возможностью Mandrill заворачивать входящие сообщения в Webhook (прям то что доктор прописал). Далее, создаем входящий ящик, настраиваем Webhook и пишем наш приемник:

require 'hashie'  module Hooksler   module Mandrill     class Input       extend Hooksler::Channel::Input        register :mandrill        def initialize(params)         @params = Hashie::Mash.new(params)       end        def load(request)         return unless request.content_type == 'application/x-www-form-urlencoded'         action, payload = request.POST.first         return unless action == 'mandrill_events'         payload = MultiJson.load(payload)          payload.map do |event|           build_message(event) do |msg|             begin               method_name = "for_#{event['event']}"               self.send method_name, msg, event if respond_to? method_name             rescue             end           end         end       end        def for_inbound(msg, event)         msg.message = event['msg']['text'] || event['msg']['html']         msg.title = event['msg']['subject']         msg.user = event['msg']['headers']['From']       end     end   end end  

Принимаем события, заворачиваем в Message и шлем дальше. Теперь нам нужен код, который будет выполнять приглашение пользователей

class SlackInviteOutbound   extend Hooksler::Channel::Output   register :slack_invite    def initialize(params)     @params = params   end    def dump(message)     return unless message.source == :mandrill      email = message.raw['msg']['from_email']     url = "https://#{@params[:team]}.slack.com/api/users.admin.invite"     HTTParty.post url, body: { email: email, token:  @params[:token], set_active: true }   end end 

Принимаем сообщение, проверяем что оно пришло из Mandrill, получаем email, запрос и пользователь приглашен. При этом, мы точно уверены что ящик валидный.

В качестве последнего штриха настройка маршрутизации:

endpoints do   input  'slack_invite',  type: :mandrill   output 'slack_invite', type: :slack_invite, team: 'myteam', token: 'mysupersecrettoken' end route 'slack_invite' => 'slack_invite' 

Запускаем и наслаждаемся процессом.

В заключение

Данное решение использую у себя уже некоторое время, пока проблем не возникало — все ходит стабильно. Единственное, для trello не все случаи обработаны, уж больно много у них различных типов уведомлений. Так же, для Slack были сделаны свои модули форматирования, кому интересно могут посмотреть пример здесь.

В дальнейшем, в планах расширять количество адаптеров как для приема, так и для отправки сообщений. Надеюсь, данное решение будет ещё кому-то полезным.
Критика и предложения приветствуются, сообщения об ошибках в тексте в личку.

Сам Hooksler и адаптеры доступны на Github: github.com/hooksler

ссылка на оригинал статьи http://habrahabr.ru/post/261799/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *