Привет, читатели, сейчас не так много информации о новых фичах Slack, всё больше людей заходят в ИТ, пользуются Slack и он, в свою очередь, для многих становится основным приложением, куда регулярно хочется зайти и прочитать очередной прикол от менеджера.
Сам я прошёл путь создания приложения по англоязычным бредням, вырваным кускам из Stack Overflow, и очень многих проб, ошибок и исследований. Не имея понимания о том, что можно сделать, как то сложно хотеть что-то делать.
Было бы круто, если бы каждый умелый разраб (или не разраб) мог сделать приятно для коллектива или для себя любимого и добавить автоматизацию в свой один или несколько Slack Workspace.
Так что я опишу этапы создания своего Slack бота для многих Workspace!
Планирую розбить на 3 статьи, они будут такими:
Написания приложения локально через Sinatra и ngrok (Мы здесь).
Вступление
Почему Ruby - потому что это язык на котором лично я пишу пет проекты, он интересный, со своими приколами и удобствами.
Почему Slack - потому что для Telegram бота уже писал пост, ссылка, и потому что в рабочем workspace создал полезного бота в свободное от работы время, собственно описывать буду собственный опыт и проблемы/решения которые встретились. Была бы у меня такого рода статья, то сэкономил бы себе много времени :)
Оглавление этой части цикла :
Подготовка рабочего места
не пропускай этот этап
Для разработки тебе понадобится :
ngrok
PostgreSQL
Шаблон проекта который я уже подготовил, ссылка на Git репозиторий.
О настройке ngrok
Рассказывать не буду (не об этом туториал), в целом установка очень простая :)
Расскажу о создании БД на 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 заполнен ключами от нашего приложения, осталось запустить ngrok и вписать хост, который получим от ngrok в SLACK_REDIRECT_URI.
./ngrok http 9292
Выполнять стоит там, где у вас лежит файл ngrok.
На выходе будет так, как на Рисунке 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
Далее необходимо получить 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 указываем по формату 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.
Если вернутся к команде '/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.
Впечатляющий результат, мой читатель, всем мир :)
Vyacheslav_N
Отличная статья. Спасибо.