Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет читатели, на сейчас не так много информации о новых фичах Slack, всё больше людей заходят в ИТ и пользуются Slack и Slack для многих становится основным приложением куда регулярно хочется зайти и прочитать очередной прикол от менеджера.
Сам я прошёл путь создания приложения по англоязычным бредням, вырваным кускам из Stack Overflow, и очень многих проб, ошибок, и исследований. Не имея понимания о том что можно сделать, как то сложно хотеть что-то делать.
Было бы круто если бы каждый умелый разраб (или не разраб) мог сделать приятно для коллектива или для себя любимого и добавить автоматизацию в свой один или несколько Slack Workspace
Так что я опишу этапы создания своего Slack бота для многих Workspace!
Планирую розбить на 3 статьи, они будут такими:
Написания приложения локально через Sinatra и ngrock (Мы здесь).
Добавления чартов, или как делать рендер фронта на сервере.
Тусовка приложения с таким гостем как Heroku.
Вступление
Почему Ruby - потому что это язык на котором лично я пишу пет проекты, он интересный, со своими приколами и удобствами.
Почему Slack - потому что для Telegram бота уже писал пост, ссылка, и потому что в рабочем workspace создал полезного бота в свободное от работы время, собственно описывать буду собственный опыт и проблемы/решения которые встретились. Была бы у меня такого рода статья то сэкономил бы себе много времени :)
Оглавление этой части цикла :
Подготовка рабочего места
Поговорим об архитектуре приложения
Про гемы
Добавляем приложения в workspace (Авторизация)
Учимся настраивать Slash Commands
Модальные окна в качестве альтернативы Slash Commands
Обработка ивентов
Заключение
Подготовка рабочего места
не пропускай этот этап
Для разработки тебе понадобится :
Ngrock
PostgreSQL
Шаблон проекта который я уже подготовил, ссылка на Git репозиторий.
О настройке ngrock
Рассказывать не буду (не об этом туториал), в целом установка очень простая :)
Расскажу о создании БД на PostgreSQL
Система MacOs, хз может на Линуксе такой же процесс.
Выбор PostgreSql в качестве базы данных не случаен, до выгрузки на Heroku я использовал SQLite 3 для этого проекта, но требования от Heroku не разрешают использовать SQLite и вообще форсят PostgreSql, так что пришлось ...
Все команды в консоле (терминале)
psql -l
Результат примерно такой :
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).
Проверим что у нас получилось командой ранее
Я пользуюсь RubyMine, БД в рамках обучения будет не большая, так что никаких DataGrip, Workbench не будет.
На Рисунке 3 - пример заполнения полей для добавления базы в RubyMine. У вас могут отличатся поля: name, comment, user, password, Database. Остальное по идеи должно быть таким же.
Клонируем GitHub репозиторий
Собственно переходим в терминале в репозиторий с проектом, пишем команду
git clone https://github.com/sorshireality/teamplate-slack-ruby-app
На этом этапе у нас есть все что нужно для разработки приложения для Slack (Slack должен быть :) )
Поговорим о архитектуре приложения
На Рисунке 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.
Далее, переходим в панель управления приложением - Рисунок 6.
На менюшке выбираем Basic Information и ищем блок App Credentials, на Рисунок 6 его краешек виден. Там будут данные которыми необходимо заполнить файл env.rb с нашего проекта.
Запуск приложения
Теперь, когда файл env.rb заполнен ключами от нашего приложения осталось запустить ngrock и вписать хост который получим от ngrock в SLACK_REDIRECT_URI.
./ngrock http 9292
Выполнять стоит там, где у вас лежит файл ngrock.
На выходе будет так, как на Рисунок 7
Вписываем в 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
Деплой приложения
На панели управления приложением переходим по менюшке на вкладку OAuth & Permissions. На этой вкладке в блоке Scopes установите значения схожие с теми что на Рисунок 9.
Также, в блок Redirect URLs вписываем адрес с нашего env.rb файла.
Поскольку я не собираюсь тут игры играть с одним workspace, то сразу идём в Basic Information > Manage Distribution -> Distribute. Отмечаем все галочки (знаю что есть хардкод, но по другому на Heroku я не смог залить App). И в низу нажимает Active Public Distribution.
Добавления приложения в канал/workspace
Вверху страницы Manage Distribute есть кнопка для добавления приложения в Workspace, нажимаем эту кнопку и предоставляем те доступы которые сами и запросили Рисунок 10.
Теперь, если посмотреть в БД которую создали ранее, добавится таблица oauth_access и в ней новая запись для этого Workspace. Одна запись - один Workspace.
Учимся настраивать Slash Commands
Откройте панель управление приложением > Slash Commands и там нажмите кнопку Create New 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)
Собстено если запустить эту команду то ничего в чате не произойдёт, зато если вернутся к серверу и посмотреть в консоль, то увидим payload запроса, примерно такой как на Рисунке 14.
Собственно давайте попытаемся найти в базе ключ доступа с 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
Длинный стек вызовов которые привел к очень простой ошибке - нужно добавить приложение в канал (чат) для того что бы он имел доступ к сообщениям, участникам, и мог отправлять сообщения. Нажимаем на название канала (Рисунок 15)
Далее вкладка Интеграции > Добавить приложение - выбираем и нажимаем Добавить
Попытка номер 2 ... Результат на Рисунке 16.
Собственно теперь дописываем запрос на получения данных о пользователе. Для этого нужно знать маршрут по которому обращаться. Все маршруты 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.
Как видим по тексту ошибки - проблема в доступах, скоупах. Возвращаемся в панель управления приложением OAuth & Permissions > Добавляем скоуп users.profile:read.
После этого действия вверху страницы вылезет Warning Message, который говорит что необходимо переустановить приложение. Делаем это с помощью ссылки в этом Warning.
Попытка номер 2 ... Результат поражает ! (Продолжение на Рисунке 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.
Если нажать на 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.
Если вернутся к команде '/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