Slack Ruby App. Часть 1. Написания приложения локально через Sinatra и ngrock

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Привет читатели, на сейчас не так много информации о новых фичах Slack, всё больше людей заходят в ИТ и пользуются Slack и Slack для многих становится основным приложением куда регулярно хочется зайти и прочитать очередной прикол от менеджера.

Сам я прошёл путь создания приложения по англоязычным бредням, вырваным кускам из Stack Overflow, и очень многих проб, ошибок, и исследований. Не имея понимания о том что можно сделать, как то сложно хотеть что-то делать.

Было бы круто если бы каждый умелый разраб (или не разраб) мог сделать приятно для коллектива или для себя любимого и добавить автоматизацию в свой один или несколько Slack Workspace

Так что я опишу этапы создания своего Slack бота для многих Workspace!

Планирую розбить на 3 статьи, они будут такими:

  1. Написания приложения локально через Sinatra и ngrock (Мы здесь).

  2. Добавления чартов, или как делать рендер фронта на сервере.

  3. Тусовка приложения с таким гостем как Heroku.


Вступление

Почему Ruby - потому что это язык на котором лично я пишу пет проекты, он интересный, со своими приколами и удобствами.

Почему Slack - потому что для Telegram бота уже писал пост, ссылка, и потому что в рабочем workspace создал полезного бота в свободное от работы время, собственно описывать буду собственный опыт и проблемы/решения которые встретились. Была бы у меня такого рода статья то сэкономил бы себе много времени :)

Оглавление этой части цикла :

  1. Подготовка рабочего места

  2. Поговорим об архитектуре приложения

  3. Про гемы

  4. Добавляем приложения в workspace (Авторизация)

  5. Учимся настраивать Slash Commands

  6. Модальные окна в качестве альтернативы Slash Commands

  7. Обработка ивентов

  8. Заключение


Подготовка рабочего места

не пропускай этот этап

Для разработки тебе понадобится :

  • Ngrock

  • PostgreSQL

  • Шаблон проекта который я уже подготовил, ссылка на Git репозиторий.

О настройке ngrock

Рассказывать не буду (не об этом туториал), в целом установка очень простая :)

Расскажу о создании БД на PostgreSQL

Система MacOs, хз может на Линуксе такой же процесс.

Выбор PostgreSql в качестве базы данных не случаен, до выгрузки на Heroku я использовал SQLite 3 для этого проекта, но требования от Heroku не разрешают использовать SQLite и вообще форсят PostgreSql, так что пришлось ...

Все команды в консоле (терминале)

psql -l 

Результат примерно такой :

Рисунок 1. Список существующих БД + их владельцы
Рисунок 1. Список существующих БД + их владельцы
sudo psql -U oleksandrtutunnik -d postgres

На месте oleksandrtutunnik пишите своего owner с Рисунка 1, БД обязательно postgres указывать. Собственно откроется консоль для SQL скриптов в БД.

CREATE DATABASE habr_one_love;
CREATE USER admin WITH password 'admin';
GRANT ALL ON DATABASE habr_one_love TO admin;
\q

Первая строчка создаст БД habr_one_love, вторая строчка создаст своего юзера, третья для королевских удобств. `\q` для выхода с этой штуки (quit).

Проверим что у нас получилось командой ранее

Рисунок 2. Результат который достоин лайка.
Рисунок 2. Результат который достоин лайка.

Я пользуюсь RubyMine, БД в рамках обучения будет не большая, так что никаких DataGrip, Workbench не будет.

На Рисунке 3 - пример заполнения полей для добавления базы в RubyMine. У вас могут отличатся поля: name, comment, user, password, Database. Остальное по идеи должно быть таким же.

Рисунок 3. Добавления DB в RubyMine
Рисунок 3. Добавления DB в RubyMine

Клонируем GitHub репозиторий

Собственно переходим в терминале в репозиторий с проектом, пишем команду

git clone https://github.com/sorshireality/teamplate-slack-ruby-app

На этом этапе у нас есть все что нужно для разработки приложения для Slack (Slack должен быть :) )


Поговорим о архитектуре приложения

На Рисунке 4 тот проект, который у вас будет сразу после клонирования репозитория.

Рисунок 4. Базовая архитектура приложения
Рисунок 4. Базовая архитектура приложения

Файлы Git базовые: .gitignore, README.md, CHANGELOG.md
Файлы Ruby: Gemfile и файл с моим окружением Gemfile.lock
Файл auth.rb отвечает за авторизацию новых аккаунтов (Slack Workspace) и пользователей.
Файл env.rb отвечает за переменные окружения по типу токен доступа нашего приложения, секретный ключ приложения и другие.

Файлы в папке Listeners отвечают за обработку запросов к приложению от Slack пользователей.
Сейчас тут три файла для обработки Slack Commands, Events, Interactivity Actions. Попозже разберемся что к чему.

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


Про гемы

Я использую стандартный Bundler для работы с гемами. Как было упомянуто ранее, шаблон оснащен Gemfile.lock файлом который нужен для того что бы у вас случайно не установилась версия иная от моей и проект не пошел по пиз кривой дорожке.

Базово Gemfile оснащен такими гемами :

source 'http://rubygems.org'

gem 'pg' ---> Гем для PostgreSQL
gem 'sinatra', '~> 1.4.7' ---> Гем-сервер который будет обрабатывать запросы от SLACK API.
gem 'slack-ruby-client', '~> 0.17.0' ---> Очень-очень полезный гем для работы с Slack API, с помощью клиента упрощает обаботку ошибок, отправки запросов, авторизации.

Сейчас (на старте) и далее, когда проект будет пополнятся новыми гемами, команда для установка зависимостей :

bundle install

Выполнять всё так же через терминал в директории проекта.


Добавляем приложения в workspace (Авторизация)

Создание приложения

Переходим на api.slack.com, там клацаем создать приложение. Заполняем примерно как изображено на Рисунке 5.

Рисунок 5. Создание приложения
Рисунок 5. Создание приложения

Далее, переходим в панель управления приложением - Рисунок 6.

Рисунок 6. Панель управления приложением
Рисунок 6. Панель управления приложением

На менюшке выбираем Basic Information и ищем блок App Credentials, на Рисунок 6 его краешек виден. Там будут данные которыми необходимо заполнить файл env.rb с нашего проекта.

Запуск приложения

Теперь, когда файл env.rb заполнен ключами от нашего приложения осталось запустить ngrock и вписать хост который получим от ngrock в SLACK_REDIRECT_URI.

./ngrock http 9292

Выполнять стоит там, где у вас лежит файл ngrock.
На выходе будет так, как на Рисунок 7

Рисунок 7. Output от ngrock
Рисунок 7. Output от ngrock

Вписываем в env.rb именно адрес с приставкой https (это Slack так хочет, не я :) ), в нашем случае это : https://d69e-31-202-13-150.ngrok.io, полностью поле должно выглядеть так

ENV['SLACK_REDIRECT_URI'] = "https://d69e-31-202-13-150.ngrok.io/finish_auth"

И что бы запустить сервер который и будет обрабатывать запросы от нашего приложения. Результат на Рисунок 8 :

rackup
Рисунок 8. Результат команды rackup
Рисунок 8. Результат команды rackup

Деплой приложения

На панели управления приложением переходим по менюшке на вкладку OAuth & Permissions. На этой вкладке в блоке Scopes установите значения схожие с теми что на Рисунок 9.

Рисунок 9. Scopes нашего app
Рисунок 9. Scopes нашего app

Также, в блок Redirect URLs вписываем адрес с нашего env.rb файла.

Поскольку я не собираюсь тут игры играть с одним workspace, то сразу идём в Basic Information > Manage Distribution -> Distribute. Отмечаем все галочки (знаю что есть хардкод, но по другому на Heroku я не смог залить App). И в низу нажимает Active Public Distribution.

Добавления приложения в канал/workspace

Вверху страницы Manage Distribute есть кнопка для добавления приложения в Workspace, нажимаем эту кнопку и предоставляем те доступы которые сами и запросили Рисунок 10.

Рисунок 10. Примерно так выглядит процесс добавления приложения в любой Slack Workspace
Рисунок 10. Примерно так выглядит процесс добавления приложения в любой Slack Workspace
Рисунок 11. Успешно всё прошло :)
Рисунок 11. Успешно всё прошло :)

Теперь, если посмотреть в БД которую создали ранее, добавится таблица oauth_access и в ней новая запись для этого Workspace. Одна запись - один Workspace.


Учимся настраивать Slash Commands

Откройте панель управление приложением > Slash Commands и там нажмите кнопку Create New Command

Рисунок 12. Пример заполнения Slash Command
Рисунок 12. Пример заполнения Slash Command

Я заполнил настройки команды как показано на Рисунке 12. Теперь приступим к реализации. Я собираюсь просто запросить у Slack API информацию о пользователе и вывести её с помощью сообщения в чат.

В файле Listeners/commands.rb будет происходит обработка Slash Commands, о других файлах в этой же папке поговорим чуть позже.

Запишем обработку вызова для этой команды таким образом :

post '/who_am_i' do
    get_input
    pp self.input
    status 200
  end

Что бы применить изминения, так сказать, нужно остановить сервер комбинацией CTRL + C и снова запустить с помощью

rackup

К большому сожелению так нужно делать всегда после любой строчки кода что бы она приминилась на боте (сервере), можно сделать без этого но это не так просто и в Ruby будет работать не всегда.

Теперь, если зайти в чат Slack, к Workspace которого мы добавили нашего бота и начать сообщения с '/who_' то должна вылезти подсказка о Slash Command которая говорит о том что команда успешно добавлена (Рисунок 13)

Рисунок 13. Своя личная команда, прикол да?
Рисунок 13. Своя личная команда, прикол да?

Собстено если запустить эту команду то ничего в чате не произойдёт, зато если вернутся к серверу и посмотреть в консоль, то увидим payload запроса, примерно такой как на Рисунке 14.

Рисунок 14. P-p-p-payload
Рисунок 14. P-p-p-payload

Собственно давайте попытаемся найти в базе ключ доступа с team_id из payload и отправить сообщение в чат как ответ

Сразу скажу что в моем шаблоне это предусмотрено, для поиска access_token используем

Database.find_access_token input['team_id']

Для создания клиента

create_slack_client(access_token)

Для отправки чего то в чат нам так же нужен будет channel_id, он тоже есть в payload с Рисунка 14. Формируем вызов функции таким образом :

    message = 'Не мешай мне обрабатывать!'
    blocks =
        [
            'type' => 'section',
            'text' => {
                'type' => 'mrkdwn',
                'text' => message
            }
        ]
    client.chat_postMessage(
        channel: input['channel_id'],
        blocks: blocks.to_json
    )

Тут blocks, это язык разметки от Slack, подробнее и попрактиковатся с ним можно тут

Целиком сейчас обработка команды выглядит так :

  post '/who_am_i' do
    get_input
    pp self.input

    Database.init
    client = create_slack_client(Database.find_access_token input['team_id'])
    message = 'Не мешай мне обрабатывать!'
    blocks =
        [
            'type' => 'section',
            'text' => {
                'type' => 'mrkdwn',
                'text' => message
            }
        ]
    client.chat_postMessage(
        channel: input['channel_id'],
        blocks: blocks.to_json
    )
    status 200
  end

Ложим-поднимаем сервер, и пытаемся вызвать нашу команду снова. Видим сообщение об ошибке, лезем на сервер, а там ... Рисунок 14

Рисунок 14. Лютое фаталити для проекта
Рисунок 14. Лютое фаталити для проекта

Длинный стек вызовов которые привел к очень простой ошибке - нужно добавить приложение в канал (чат) для того что бы он имел доступ к сообщениям, участникам, и мог отправлять сообщения. Нажимаем на название канала (Рисунок 15)

Рисунок 15. Название канала
Рисунок 15. Название канала

Далее вкладка Интеграции > Добавить приложение - выбираем и нажимаем Добавить

Попытка номер 2 ... Результат на Рисунке 16.

Рисунок 16. Результат попытки номер 2
Рисунок 16. Результат попытки номер 2

Собственно теперь дописываем запрос на получения данных о пользователе. Для этого нужно знать маршрут по которому обращаться. Все маршруты Slack API, вы можете найти по этой ссылке https://api.slack.com/methods. Вангую нам нужен этот роут : https://api.slack.com/methods/users.profile.get.

Окей роут есть, аргументы есть, как теперь сделать вызов. Всё просто! Чудесный гем всё делает за нас, нужно лишь заменить точки на подчеркивания!!!

User_id можем взять из Payload с Рисунка 14, модифицируем текст сообщения с предыдущего примера таким образом

 message = client.users_profile_get(
        user: input['user_id']
    ).to_s

Убиваем сервер, возрождаем сервер. Попытка отправить команду в чат ... Рисунок 17.

Рисунок 17. Ошибка которую никто не ждал!
Рисунок 17. Ошибка которую никто не ждал!

Как видим по тексту ошибки - проблема в доступах, скоупах. Возвращаемся в панель управления приложением OAuth & Permissions > Добавляем скоуп users.profile:read.

После этого действия вверху страницы вылезет Warning Message, который говорит что необходимо переустановить приложение. Делаем это с помощью ссылки в этом Warning.

Попытка номер 2 ... Результат поражает ! (Продолжение на Рисунке 18)

Рисунок 18. Неужели всё так просто может быть :)
Рисунок 18. Неужели всё так просто может быть :)

Понимаю, выглядит как параш хлам и никакой полезной нагрузки. Но мы задачу выполнили ? Выполнили :) Потом пофиксим :)


Модальные окна в качестве альтернативы Slash Commands

В качестве альтернативы к slash command существуют в Slack модальные окна.

Лично я обратился к этому функционалу из-за неудобного способа ввода параметров для команд и тому, что сложно вообще понимать какие есть команды.

Давайте обратимся к панели управления приложением и создадим новую команду по адресу '/menu', по задумке для открытия меню.

Так же для вызова модального окна необходимо будет отправить особый запрос на Slack API с нашего приложения. https://api.slack.com/methods/views.open

Пройдемся по параметрам этого вызова :

trigger_id - это id вызова для которого мы покажем в качестве ответа от сервера наше модальное окно. Есть в каждом сообщении и посмотреть как он выглядить можно на Рисунке 14.

view - это и будет синтаксис нашего модального окна, по сути, те же blocks но с доп логической структурой.

Для моего примера будем использовать шаблон для Ruby, файлы которые называются erb. Создайте папку в директории Components для этих шаблонов. В моем случае это 'Components/View'.
Там создам файл menu.json (json потом поменяю на erb, сейчас это для удобства отображения в RubyMine)

Синтаксис для модальных окон выглядит так :

{
  "type": "modal",
  "title": {
    "type": "plain_text",
    "text": "Menu"
  },
  "blocks": [],
  "private_metadata": "<%= metadata %>",
  "callback_id": "menu"
}

Строка 8 содержит синтаксис шаблонов типа erb который позволяет подставить значения из вне, в данном случае из переменной metadata.

Строка 9 обозначает что это за модальное окно что бы мы могли его обрабатывать в соответствующем файле - interactivity.rb

На месте blocks стоит вставить ваши блоки для этого модального окна, да ладно, я знаю что вы будете сначала мои ставить :) Вот они

"blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "List of available functions"
      }
    },
    {
      "type": "divider"
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "Display information about this user"
      },
      "accessory": {
        "type": "button",
        "text": {
          "type": "plain_text",
          "text": "Display",
          "emoji": true
        },
        "value": "display_user_info",
        "action_id": "display_user_info"
      }
    }
  ],

Теперь можно файл переименовать в menu.erb и вернутся к commands.rb для обработки команды '/menu'.

Сначала получим токен по аналогии с вызовом команды /who_am_i

post '/menu' do
    get_input
    Database.init
    access_token = Database.find_access_token input['team_id']
    client = create_slack_client access_token
end

Далее необходимо получить trigger_id и в качестве metadata записать id канала (это пригодится нам позже)

triger_id = input['trigger_id']
metadata = input['channel_id']

Что бы получить наш шаблон menu.erb используем вызов библиотеки ERB от Ruby. Вверху файле допишите

require 'erb'

и получения самого шаблона

template = File.read './Components/View/menu.erb'
view = ERB.new(template).result(binding)

Теперь, когда у нас есть все необходимые данные вызываем запрос на Slack API с этими параметрами. Полная обработка команды выглядит так :

  post '/menu' do
    get_input
    access_token = DBHelper.new.find_access_token self.input['team_id']
    client = create_slack_client access_token
    
    triger_id = input['trigger_id']
    metadata = self.input['channel_id']
    
    template = File.read './Components/View/menu.erb'
    view = ERB.new(template).result(binding)
    
    client.views_open(
        trigger_id: triger_id,
        view: view
    )
    status 200
  end

Перезапускаем приложение и вызываем команду '/menu'. Результат на Рисунке 19.

Рисунок 19. Модальное окно-меню приложения
Рисунок 19. Модальное окно-меню приложения

Если нажать на Display то ничего не произойдёт, так как это уже следующий этап.

Обработка ивентов

Нажатие на кнопку это конечно ивент, но не совсем в терминологии Slack. Зайдите в панель управления приложением, там есть пункт "Interactivity & Shortcuts", по умолчению эта функция выключена - необходимо включить.
Request URL указываем по формату ngrock_host/interactivity, где ngrock_host - ваш адресс ngrock (такой же как и в Slash Command)

Теперь, в файле interactivity вы можете обрабатывать все действия с модальными окнами. Для действия с menu мы добавили callback_id в menu.erb файл - 'menu'. По нему можно отсеять запросы. Сначала получим payload. В случае с интерактивными командами это немного другой процесс

request_data = Rack::Utils.parse_nested_query(request.body.read)
payload = JSON.parse(request_data['payload'])

Далее создадим case оператор для callback_id и добавим кейс когда он равен menu

    case payload['view']['callback_id']
    when 'menu'
      status 200
    else
      status 404
    end

Осталось лишь заполнить функционал этого кейса. Для удобства и что бы проверить что мы делаем всё правильно давайте выведем наш payload после нажатия на кнопку

when 'menu'
  pp payload
  status 200  

Перезапускаем сервер, проверяем что произойдёт если вызвать команду меню и нажать на кнопку Display.

Рисунок 20. Результат в консоле (payload)
Рисунок 20. Результат в консоле (payload)

В консоле должно появится много данных, примерно как на Рисунке 20.

Если вернутся к команде '/who_am_i', то для её работы нужно три неизвестных :

  • team_id

  • user_id

  • channel_id

Везение в том, что такие данные есть в нашем payload. Понимаете к чему я веду? Давайте оформим '/who_am_i' как функцию и вызовем её в кейсе menu.

Поскольку эта функция не относится напрямую ни к одному из её вызовов то поместить её стоит в отдельный файл. Я хочу создать в папке Components, модуль Helper. Знаю, Helper название - для додиков не самое лучшее и не описывает всю глубину этого модуля, а главное это не описывает предназначение. Но если честно, нужно немного и самим креативить :)

Собственно без лишних слов, тело модуля :

module Helper
  def displayUserInfo(team_id, user_id, channel_id)
    Database.init
    client = create_slack_client(Database.find_access_token team_id)
    message = client.users_profile_get(
        user: user_id
    ).to_s
    blocks =
        [
            'type' => 'section',
            'text' => {
                'type' => 'mrkdwn',
                'text' => message
            }
        ]
    client.chat_postMessage(
        channel: channel_id,
        blocks: blocks.to_json
    )
  end

  module_function(
      :displayUserInfo
  )
end

НЕ ЗАБЫВАЕМ ПОДКЛЮЧИТЬ ФАЙЛ МОДУЛЯ В ФАЙЛЕ 'auth.rb' что бы он загрузился в приложение.

Давайте проверим работу этой функции сначала для команды '/who_am_i', для этого перепишем обработку команды таким образом :

  post '/who_am_i' do
    get_input
    Helper.displayUserInfo input['team_id'], input['user_id'], input['channel_id']
    status 200
  end

Перезапустим сервер и вызовем команду '/who_am_i'

В результате получим ответ аналогичный тому что был раньше для этой команды.

Теперь можем приступать к подключению функции displayUserInfo в interactivity.rb. Первые два параметра team_id и user_id, можно легко найти в payload на Рисунке 20. Но вот channel_id там нету, тут то нам и пригодится metadata которую мы заполняли во время создания модального окна. Если внимательно посмотреть на payload, в массив view, то там есть эта информация, нам лишь нужно указать к ней правильный путь

case payload['view']['callback_id']
    when 'menu'
      Helper.displayUserInfo payload['user']['team_id'], payload['user']['id'], payload['view']['private_metadata']
      status 200
    else
      status 404
    end

Перезапускаем приложение, открываем меню (команда '/menu') и нажимаем на Display и результатом станет сообщения в чат о нашем пользователе!

Результат отличный, но давайте не будем забывать что кнопок на модальном окне может быть много, особенно в случае с меню. Если взглянуть на файл menu.erb, то там есть для кнопи 'action_id'. Предлагаю использовать его для фильтрации действий во время обработки, таким образом :

case payload['view']['callback_id']
    when 'menu'
      case payload['actions'].first['action_id']
      when 'display_user_info'
        Helper.displayUserInfo payload['user']['team_id'], payload['user']['id'], payload['view']['private_metadata']
        status 200
      else
        status 404
      end
    else
      status 404
    end

Перезапускаем, вызываем, нажимаем, смотрим на результат. Всё без проблем вобщем :)


Результаты

По итогу что мы научились делать :

  • создать базу данных postgresql

  • создавать свой Slack App

  • добавлять свой Slack App в Workspace

  • получать токен доступа для своего Slack App

  • совмещать ngrock и Sinatra при разработке на Ruby

  • пользоваться гемом slack-ruby-client

  • Кросс-Workspace приложение

  • создавать Slash Commands

  • пользоватся Slack Block Kit

  • пользоваться Slack API Methods

  • писать в чат от имени бота

  • получать информацию о Slack User с помощью бота

  • создавать модальные окна

  • создавать кнопки на модальных окнах

  • обрабатывать действия с модальными окнами

Что мы по итогу получили :

Slack App который можно свободно распространять по Workspace'ам в котором есть команда для получения информации о пользователе и модальное меню со списком доступных функции которая работает по аналогии с slash commands.

Впечатляющий результат мой читатель, всем мир :)

Ссылка на шаблон приложения : https://github.com/sorshireality/teamplate-slack-ruby-app/tree/master

Ссылка на итоговое приложение после туториала : https://github.com/sorshireality/teamplate-slack-ruby-app/tree/my_app

Источник: https://habr.com/ru/post/576150/


Интересные статьи

Интересные статьи

Как правило, трудно бывает «продать» руководству чистую миграцию, без улучшения функциональности или каких-то других преимуществ для бизнеса. Вполне логично, что в русле долгосрочной модернизации хоте...
Сегодня с вами на связи отдел динамического ценообразования Ситимобил. И мы начинаем серию статей о том, как мы проводим и оцениваем ценовые эксперименты внутри наше...
Во второй части статьи мы начали знакомиться с основными блоками устройства для передачи данных по PLC. Это будет заключительная часть статьи, которая касается описания ж...
Материал, первую часть перевода которого мы публикуем сегодня, посвящён масштабной проблеме, которая возникла в gitlab.com. Здесь пойдёт речь о том, как её обнаружили, как с ней боролись, и как, ...
Как сделать стандарт за 10 дней, я рассказывал раньше. Сейчас я хотел бы рассказать о терминологии и названиях документов, их значении и разных подходах к составлению документации. Конечно, все з...