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

от автора

Привет, читатели, сейчас не так много информации о новых фичах 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.

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

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/576150/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *