Поэтому мы создаем этот сервис, который позволит пользователям создавать отчеты из своих отчетов Google Analytics. Основная функциональность заключается в доступе к данным путем авторизации в Google, отправке серии или запросов в Analytics API, создании графиков, их объединении в один файл и представлении пользователю.

Помимо отображения файла на веб-странице, мы рассматриваем различные другие способы доставки отчета. Отправка отчета по электронной почте — это, конечно, один из способов. Одной из экспериментальных функций, с которой мы экспериментируем, может быть интеграция приложения со Slack — системой обмена мгновенными сообщениями и совместной работы, которую, кажется, используют в наши дни на каждом рабочем месте или в школьной группе. Это определенно один из основных каналов коммуникации Craft Academy.

Итак, идея состоит в том, чтобы добавить функциональность, которая отправляет уведомление в экземпляр Slack пользователя с прикрепленным отчетом, созданным визуализатором.

Как пользователь
Чтобы распространять отчет GA Visualizer среди широкой публики
Я хочу, чтобы отчет был опубликован в моем канале Slack

После некоторых первоначальных исследований я понял, что есть два основных способа отправки уведомлений в Slack.

  1. с помощью вебхука
  2. с помощью пользователя-бота

Ради своего Спайка я хотел изучить оба этих метода, прежде чем решить, какой из них использовать в своей реализации.

Пользователь бота

Я начал искать метод API и нашел обёртку ruby, которая выглядела многообещающе — Slack Ruby Client.

Я добавил зависимость к своему Gemfile и связал.

source 'https://rubygems.org'

ruby '2.3.1'

gem 'rails', '~> 5.0.0', '>= 5.0.0.1'
# other gems...
gem 'slack-ruby-client'

Следующим шагом было создание интерфейса для запроса учетных данных пользователя и присоединения некоторого действия javascript для отправки данных в контроллер приложений.

[снимок экрана]

Разметка HAML для этого представления выглядит следующим образом:

.large-4.columns{style: 'margin-top: 50px;'}
  %h4 Share report
  %p Looking good. Why not sharing this report with people in your organization?
  #api
    %h5 Slack - using a Bot-user
    %input#api-input{type: 'text', placeholder: 'Slack API Token'}
    = button_tag 'Check API', id: 'slack-api-button', class: 'button', disabled: true
  .api_response
  .api_form

И JavaScript, связанный с этим методом?

$('#slack-api-button').on('click', function () {
    var input = $('#api-input');
    $.ajax({
            url: '/check_api',
            type: 'post',
            dataType: 'json',
            data: {api_token: input.val()}
        })
        .done(function (data) {
            $('#api').hide();
            $('.api_response').html('<img src="' + data.icon + '">');
            $('.api_response').append('<p><strong>' + data.team_name + '</strong></p>');
            $(function () {
                $('.api_form')
                    .append('<form id="notify-api-form" data-remote="true"></form>');
                $('#notify-api-form')
                    .attr('action', '/notify_with_api').attr('method', 'post')
                    .append('<input type="hidden" name="image_path" value="' + $('#report-img').attr('src') + '" />')
                    .append(appendSelect(data.channels))
                    .append($('<input type="submit" value="Post notification" name="submit" class="button" />'));
            });

        })
        .fail(function (data) {
            $('.api_response').html('<strong>' + data.responseJSON.message + '</strong>');
        });
})

Функция appendSelect, вызываемая в приведенном выше коде, выглядит так:

function appendSelect(channels) {
    var select = $("<select name='channel'></select>");
    channels.forEach(function (value, i) {
        select.append($("<option></option>")
            .attr("value", value)
            .text('#' + value));
    });
    return select;
}

Действия контроллера:

class SubscriptionsController < ApplicationController
  skip_before_action :verify_authenticity_token, only: [:notify_with_api]

  def check_api
    begin
      session[:api_token] = params[:api_token]
      client = Slack::Web::Client.new(token: params[:api_token])
      team = client.team_info
      channels = client.channels_list.channels
      render json: {team_name: team[:team][:name], icon: team[:team][:icon][:image_88], channels: channels.map { |c| c[:name] }}
    rescue => ex
      render json: {message: 'An unknown error occurred'}, status: 400
    end
  end

  def notify_with_api
    SlackNotifier.api_notify(session[:api_token], params[:channel], params[:image_path], 'Visualizer report')
    @message = {message: "Notification sent to ##{params[:channel]}"}
    respond_to do |format|
      format.js
    end
  end
end

Вы заметили, что я вызываю SlackNotifier, чтобы фактически выполнить вызов API. Давайте посмотрим на этот модуль:

module SlackNotifier
  extend ActiveSupport::Concern

  def self.api_notify(token, channel, file, message)
    client = Slack::Web::Client.new(token: token)
    client.files_upload(
        channels: channel,
        as_user: true,
        file: Faraday::UploadIO.new("public/#{file}", 'image/png'),
        title: 'Report'
    )
  end

end

Я также добавил шаблон js, который будет отображаться после отправки уведомления.

// app/views/subscriptions/notify_with_api.js.erb
$(".api_form").html("<%=j render html: @message[:message] %>");

Итак, с точки зрения UX это выглядит так.

Пользователь вводит свой токен API для бота и нажимает кнопку «Проверить API». В этот момент вызывается действие check_api, и происходит следующее:

  1. Токен хранится в файле cookie сеанса (он понадобится нам для дальнейших действий)
  2. клиент Slack открывается с использованием токена API
  3. Получена информация о команде
  4. Получена информация о доступных каналах
  5. Объект json с именем команды, значком и доступными каналами создается и отправляется обратно в представление.

И у нас есть сообщение в нашем канале Slack.

Веб-перехватчики

Второй подход заключается в настройке веб-перехватчика с помощью панели инструментов Slack (см. инструкции по ссылке выше) и реализации способа отправки сообщения с отчетом в виде вложения.

Для этого метода я буду использовать гем под названием slack-notifier. Как обычно, я удостоверяюсь, что он добавлен в мой Gemfile и включен приложением.

source 'https://rubygems.org'

ruby '2.3.1'

# other gemsgem 'slack-notifier', require: 'slack-notifier'

Я изменяю свой интерфейс с полем ввода, заключенным в div с идентификатором webhook:

.large-4.columns{style: 'margin-top: 50px;'}
  %h4 Share report
  %p Looking good. Why not sharing this report with people in your organization?
  #webhook
    %h5 Slack - using a webhook
    %small Experimental
    %input#webhook-input{type: 'text', placeholder: 'Slack webhook URL'}
    = button_tag 'Post with Webhook', id: 'slack-webhook-button', class: 'button', disabled: true
  #api
    %h5 Slack - using a Bot-user
    %input#api-input{type: 'text', placeholder: 'Slack API Token'}
    = button_tag 'Check API', id: 'slack-api-button', class: 'button', disabled: true
  .api_response
  .api_form

И добавьте некоторые действия JavaScript для обработки отправки:

$('#slack-webhook-button').on('click', function () {
    var input = $('#webhook-input');
    var message = '<p><strong>Not a valid Slack Webhook</strong></p>'
    if (input.val().length && parseUrl(input.val()).authority !== "hooks.slack.com") {
        if (input.parent().has('p').text().length == 0) {
            input.parent().append(message);
        }
    } else {
        input.parent().find('p').remove();
        $.ajax({
                url: '/notify_with_webhook',
                type: 'post',
                dataType: 'json',
                data: {hook: input.val(), image_path: $('#report-img').attr('src')}
            })
            .done(function (data) {
                $('#webhook').hide();
                $('#api').hide();

                $('.api_response').html('<strong>' + data.message + '</strong>');
            })
            .fail(function (data) {
                // TODO: There is no error handler in the controller atm.
                $('.api_response').append('<strong>' + data.responseJSON.message + '</strong>');
            });

    }
});

Что я хочу сделать здесь, так это добавить простую проверку того, что веб-перехватчик указывает на Slack, прежде чем я отправлю это действие контроллера (отсюда и проверка parseUrl(input.val()).authority !=="hooks.slack.com"), чтобы у нас не было проблем.

Функция parseUrl() — это пользовательская функция, которая выглядит так:

function parseUrl(url) {
    var pattern = new RegExp("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?");
    var matches = url.match(pattern);
    return {
        scheme: matches[2],
        authority: matches[4],
        path: matches[5],
        query: matches[7],
        fragment: matches[9]
    };
}

Новый метод в SubscriptionsController обрабатывает запрос:

class SubscriptionsController < ApplicationController
 
# omitted code

  def notify_with_webhook
    image_path = "#{request.protocol}#{request.host_with_port}#{params[:image_path]}"
    SlackNotifier.notify_hook(params[:hook], image_path, 'Visualizer report ')
    render json: {message: 'Notification sent'}
  end
end

А добавление к SlackNotifier` делает запрос к URL вебхука:

module SlackNotifier
  extend ActiveSupport::Concern

  def self.notify_hook(hook, image_path, message)
    client = Slack::Notifier.new hook
    client.ping 'Google Analytics Visualizer report',
                attachments: [{
                    image_url: image_path,
                    initial_comment: message
                }]
  end

# omitted code

end

И результат в Slack — это новое сообщение с отчетом в виде вложения.

Это поток, который мы сейчас используем в прототипе VISUALIZER. Есть много возможностей для рефакторинга, и мы отойдем от выполнения ajax-вызовов с использованием jQuery и перейдем к более рельсовому способу. Но это доказательство концепции уже есть, и у нас есть два способа интеграции Slack в наше приложение.

Хороший материал.