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