В последнее время большую популярность приобрел сервис для командной коммуникации 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/
Добавить комментарий