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

    Комментарии 2

      0
      Т.Е. взяли SaaS решение (Slack) и чтобы работать с ним захостили свой сервис для него. Отлично. Что мешало тупо развернуть irc или аналог слака.
        0
        С IRC ничего общего, Slack позволяет делать интеграции с Trello, Newrelic и другими сервисами. Но их количество ограничено, а чтобы снять его можно собственно и сделал данный сервис. Плюс как приятное дополнение можно сообщения в итоге маршрутизировать не только в Slack, но и в другие места если сделать соответсвующий адаптер. В том числе можно и с IRC интегрировать.

        Если коротко это мост между сервисом A и сервисом B.

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

      Самое читаемое