Как стать автором
Обновить

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

Время на прочтение12 мин
Количество просмотров2.2K

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

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

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

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

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

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

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

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


Вступление

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

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

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

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

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

  3. Про гемы

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

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

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

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

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


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

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

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

О настройке ngrok

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

Расскажу о создании БД на 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 заполнен ключами от нашего приложения, осталось запустить ngrok и вписать хост, который получим от ngrok в SLACK_REDIRECT_URI.

./ngrok http 9292

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

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

Вписываем в 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

Далее необходимо получить 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 указываем по формату ngrok_host/interactivity, где ngrok_host - ваш адресс ngrok (такой же как и в 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ссылка на шаблон приложения

Ссылка на итоговое приложение после туториала

Теги:
Хабы:
Всего голосов 3: ↑3 и ↓0+3
Комментарии2

Публикации