Прием уведомлений от внешних сервисов, или зачем был сделан 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

© Habrahabr.ru