Разработка Action-able приложения для Slack

ca085793d555da41e3362e86f4ac612a.png

От переводчика: публикуем для вас статью Томоми Имуры о том, как разработать приложение, интегрированное со Slack

Actions — то, что позволяет пользователям Slack взаимодействовать с вашим приложением при помощи сообщений: сообщить о проблеме с багом, отправить запрос в хелпдеск или сделать что-то еще. Эта функция похожа на встроенные возможности вроде комментирования или шеринга.

Этот туториал проведет вас через весь процесс создания действующего приложения.

Skillbox рекомендует: Двухлетний практический курс «Я — Веб-разработчик PRO».

Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».


Туториал будет полезен для всех, кто хотел бы изучить API Slack. При разработке приложения используется Node.js, так что, если вы хотите повторить то, о чем пойдет разговор, — установите его.

С готовым проектом можно ознакомиться на GitHub, с упрощенной его версией — на Glitch.

ClipIt! for Slack

Мы займемся разработкой Slack-app для воображаемого приложения ClipIt! Давайте представим, что мы управляем веб-сервисом, который позволяет «заложить» часть веб-страницы, сохранив ее в базу данных. Пользователи могут получать доступ к сохраненному контенту с мобильного устройства или ПК, сервис является мультиплатформенным. А еще ClipIt! позволяет прикреплять текст в среде сообщений Slack, используя Action.

Вот гифка, демонстрирующая принцип действия приложения.

8d58f70a20f5791bec6ff2efc6b921cc.gif

А вот алгоритм взаимодействия пользователя с ClipIt! из Slack:

  • Пользователь наводит курсор на сообщение и выбирает Clip the message в выпадающем меню.
  • Открывается диалог, который дает больше возможностей.
  • Пользователь подтверждает действие.
  • ClipIt! для Slack экспортирует сообщение в базу данных ClipIt!.
  • ClipIt! для Slack отправляет пользователю DM с подтверждением.


Настраиваем приложение


Зайдите в свой аккаунт Slack и создайте приложение при помощи этой ссылки. Введите его название и область применения.

7e6c949f8433d60da49b46ec74b7177a.png

Нажмите кнопку Create App, затем — Basic Information, прокрутите до App Credentials.

244fd0cd041582e3049ba33ffd597d73.png

После этого откройте Signing Secret и скопируйте код для использования в качестве переменной окружения SLACK_SIGNING_SECRET в вашем .env-файле в root Node. Я объясню, что это и как все это использовать, в разделе «Верификация запросов» чуть ниже.

SLACK_SIGNING_SECRET=15770a…

Прокрутите еще немного ниже для заполнения Display Information с иконкой вашего приложения и описанием.

Теперь включите Interactivity в Interactive Components. Сделав это, вы увидите больше полей на странице.

4c0bb87ac72c79d750a831049ce414e3.png

Пора ввести Request URL — это адрес, на который Slack отсылает соответствующие данные, когда пользователь запускает действие.

Это должен быть URL вашего сервера, на котором выполняется код приложения. Например, если вы разместили все это в Glitch, ваш URL будет выглядеть примерно так example.glitch.me/actions. Если вы используете туннелирование, работая с сервисами вроде ngrok, то используйте URL сервиса (например example.ngrok.io, а затем добавляйте /actions).

Как только вы ввели Request URL, прокрутите вниз до Actions и нажмите Create New Action. Заполните форму:

8007b0f06583bcbf20417943986ae42a.png

Нажмите Create, затем Save Changes.

Далее идем к Bot Users. Нажмите Add a Bot User и назовите бота приложения.

ad8034b2305374171c5059080eec8b41.png

Теперь кликаем по Add Bot User и сохраняемся.

Следующий шаг — идем в OAuth & Permissions и нажимаем Install App to Workspace. После завершения установки страница вернет вас к OAuth & Permission с токенами доступа. Скопируйте токен бота и сохраните все это в файле .env.

SLACK_ACCESS_TOKEN=xoxb-214…

Кроме того, на той же странице нужно будет активировать Scopes. Сделав это, удостоверьтесь, что и bot, и command выделены.

Теперь, когда все настройки готовы, начинаем творить — пишем приложение.

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


Как говорилось выше, при создании приложения используются Node.js и ExpressJS. Для того чтобы работать со всем этим, устанавливаем зависимости ExpressJS, bodyParser и еще кое-что. Так, я используют клиент HTTP-запросов axios вместе с qs.

$ npm install express body-parser axios qs dotenv --save

Давайте начнем с самого важного. Мы изменим код позже, когда добавим больше функций. Сначала создаем файл index.js. В этом файле прописываем прослушивание сервером соответствующего порта:

/* Snippet 1 */
 
require('dotenv').config(); // To grab env vers from the .env file
 
const express = require('express');
const bodyParser = require('body-parser');
const axios = require('axios');
const qs = require('qs');
const app = express();
 
// The next two lines will be modified later
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
 
const server = app.listen(5000); // port


Прежде чем продолжить, давайте взглянем на диаграмму с описанием принципов работы всего, о чем мы сейчас говорим.

873887d5c4df62c27b41b1d752eeb5ab.png

Каждый поток инициализируется, когда пользователь выполняет actions, расположенные в меню сообщений. Как только срабатывает событие message_action, Slack отправляет приложению payload по URL-адресу запроса, который был прописан ранее.

e83fa1941e9933d237af7e5a3f70589b.png

Эндпоинт можно прописать следующим образом:

/* Snippet 2 */
 
app.post('/actions', (req, res) => {
  const payload = JSON.parse(req.body.payload);
  const {type, user, submission} = payload;
 
  // Verifying the request. I'll explain this later in this tutorial!
  if (!signature.isVerified(req)) {
    res.sendStatus(404);
    return;
  }
 
  if(type === 'message_action') {
    // open a dialog!
  } else if (type === 'dialog_submission') {
    // dialog is submitted
  }
});


Если тип события — message_action, приложение открывает диалог.

Потом добавляем код, определяющий структуру контента диалога, с открытием его в клиенте Slack, используя метод dialog.open:

/* Snippet 2.1 */
 
const dialogData = {
  token: process.env.SLACK_ACCESS_TOKEN,
  trigger_id: payload.trigger_id,
  dialog: JSON.stringify({
    title: 'Save it to ClipIt!',
    callback_id: 'clipit',
    submit_label: 'ClipIt',
    elements: [
      {
        label: 'Message Text',
        type: 'textarea',
        name: 'message',
        value: payload.message.text
      },
      {
        label: 'Importance',
        type: 'select',
        name: 'importance',
        value: 'Medium ',
        options: [
          { label: 'High', value: 'High ' },
          { label: 'Medium', value: 'Medium ' },
          { label: 'Low', value: 'Low ️' }
        ],
      },
    ]
  })
};
 
// open the dialog by calling the dialogs.open method and sending the payload
axios.post('https://slack.com/api/dialog.open', qs.stringify(dialogData))
  .then((result) => {
      if(result.data.error) {
        res.sendStatus(500);
      } else {
        res.sendStatus(200);
      }
   })
  .catch((err) => {
      res.sendStatus(500);
});


Здесь мы используем модуль axios для выполнения запроса POST к Slack; после этого метод dialog.open открывает диалог, отправляя HTTP статус 200.

2784ac20848dbc55377795d9b4ab7f24.png

Эндпоинт задействуется также в том случае, если диалог инициируется пользователем. В коде snippet 2 нужно ответить пустым запросом HTTP 200, чтобы Slack знал, что представление получено.

Наконец, мы отправляем пользователю сообщение с подтверждением, используя метод chat.postMessage.

/* Snippet 2.2 */
 
else if (type === 'dialog_submission') {
  res.send('');
    
  // Save the data in DB
  db.set(user.id, submission); // this is a pseudo-code!
    
  // DM the user a confirmation message
 
  const attachments = [
    {
      title: 'Message clipped!',
      title_link: `http://example.com/${user.id}/clip`,
      fields: [
        {
          title: 'Message',
          value: submission.message
        },
        {
          title: 'Importance',
          value: submission.importance,
          short: true
        },
      ],
    },
  ];
 
  const message = {
    token: process.env.SLACK_ACCESS_TOKEN,
    channel: user.id,
    as_user: true, // DM will be sent by the bot
    attachments: JSON.stringify(attachments)
  };
 
}


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

Верификация запросов


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

Для этого возвращаемся в snippet 1, который расположен в самом верху. Заменяем то место, где есть комментарий //The next two lines will be modified later, на вот это:

/* Snippet 3 */
 
const rawBodyBuffer = (req, res, buf, encoding) => {
 if (buf && buf.length) {
   req.rawBody = buf.toString(encoding || 'utf8');
 }
};
 
app.use(bodyParser.urlencoded({verify: rawBodyBuffer, extended: true }));
app.use(bodyParser.json({ verify: rawBodyBuffer }));


Я уже подключила криптографию в verifySignature.js, поэтому просто добавляем функцию в начало index.js:

const signature = require('./verifySignature');


Теперь выполняем верификацию:

if(!signature.isVerified(req)) { // when the request is NOT coming from Slack!
   res.sendStatus(404); // a good idea to just make it "not found” to the potential attacker!
   return;
}


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

Запускаем код снова, и, если все работает, как нужно, можно праздновать успех! Поздравляю!

© Habrahabr.ru