Slack メッセージ・アクション API を使ってディスカバラブルなアプリを作ろう

> Published: 2019-02-26
> Updated: 2020-02-11

> 1. OAuth 権限設定についての変更がありましたので、チュートリアルのその設定部分を追加しました。
> 2. 廃止予定の Dialog の代わりとなる Modals に変更しました。

Slack には、ユーザーがメッセージに対して[絵文字リアクション](https://get.slack.help/hc/en-us/articles/206870317-Emoji-reactions)や[シェア](https://get.slack.help/hc/en-us/articles/203274767-share-messages-in-slack#share-a-message)を行う標準機能がありますが、**Actions** (アクション) 機能を使うとユーザーがメッセージを送信したときにアプリを起動することもできます。

例えば、メッセージから直接プロジェクトマネージメントアプリのタスクを作成したり、バグトラッカーアプリにバグを送ったり、メッセージ内容をヘルプデスクにコピーして送信したりなど、いろいろなことが可能になるのです。この機能をうまく自分のアプリに取り込めば、より多くのユーザーにあなたのアプリを知ってもらうことにもなるでしょう。

というわけで、このチュートリアルでは、この API を使ってアクショナブルなアプリを作る説明をしていきたいと思います。

#"ClipIt! for Slack" を作る

これから作るアプリは「ClipIt! for Slack」というアプリです。あなたが架空の ClipIt! （クリップ・イット）というサービスをすでに運営していると仮定し、そのサービスに対応するスラックアプリを作るという設定でいってみましょう。このウェブサービスは、ユーザーがインターネット上で「クリップ」（保存）したテキストをデータベースに保存、同期していきます。さて、あなたはこのサービスを拡張してスラックのメッセージも保存できるようにしたいと思っています。と、いうことで今からこのアクション機能を使ってユーザーがスラック上のメッセージをクリッピングできるようにしてみましょう。

このスラックアプリは下のように動作します。
![ClipIt! Slack](https://cdn-images-1.medium.com/max/1600/1*MV4ZmSoumELV8Wyc56wcbA.gif)

ユーザーインタラクションは次のような感じになります。

1. まずユーザーがメッセージにマウスポインタを合わせ、表示された … メニューから Clip the message を選択
2. ダイアログが表示されるのでユーザーはその中のフォームに、必要な情報を編集または追加
3. ユーザーが送信 (Clipit) ボタンを押す
4. ClipIt! for Slack がそのメッセージを ClipIt データベースに追加し同期（ここは架空の過程）
5. ClipIt! for Slack が そのユーザーに DM で完了したことを通知

このチュートリアルは、プログラミング言語にかかわらず Slack API について学びたい誰もが理解できるようにと、あえて SDK を使用しないで API を直接 HTTP で呼び出しています。便宜上このチュートリアルでは Node.js を使っていますので、サンプルコードをそのまま使いたい方は、お使いのマシンやサーバーに [Node.js](https://nodejs.org) がインストールされていることを確認してください。この先も Node でスラックアプリをどんどん書いていきたい、という方は公式の [Node SDK](https://github.com/slackapi/node-slack-sdk) も参考にしてみてください。

🐙🐱 ソースコードは [GitHub]
(https://github.com/slackapi/template-action-and-dialog
) にありますが、このチュートリアルでは説明しやすいようにさらに簡略化したコードを使っています。そちらのソースは  [Glitch](https://glitch.com/~slack-clipit-simplified) という、Node アプリをウェブブラウザで書いて実行させることができるウェブ IDE 上に置いてありますので、そのコードをこのリンク https://glitch.com/edit/#!/remix/slack-clipit-simplified から "remix" してください。Glitch の remix とは、GitHub の fork のような機能で、リミックスしたコードは自分のリポジトリとなりますので、好きなように書き換える事ができます。

# ⚙️ アプリの作成と設定

まずは、開発用に使える Slack ワークスペースにサインインしてください。今からそのワークスペース上で新規のアプリを作成、インストールしていきます。[**Slack アプリ新規作成ページ**](https://api.slack.com/apps?new_app=1&ref=tutorial_message_actions_qiita) でアプリ名を入力し、開発用ワークスペースを選択してください。

![config_app_create.png](https://qiita-image-store.s3.amazonaws.com/0/339293/b7f62765-a037-c05d-3206-e10bccdbce6e.png)

**Create App** と書かれた緑のボタンをクリックしてください。

次に **Basic Information** の **App Credentials** セクションまでスクロールします。ここにはアプリの API 認証キーがいくつかがありますが、このチュートリアルでは Signing Secret を使います。

![config_app_credentials.png](https://qiita-image-store.s3.amazonaws.com/0/339293/e8daf8be-4fbf-4331-f8f5-0108ccab5cc7.png)

この Signing Secret は隠されている状態ですが、まずそれを表示させ、その値を Node アプリのルートにある **.env** ファイルに 環境変数 `SLACK_SIGNING_SECRET` として保存します。このシークレットキーの使い方についてはのちの 「リクエスト情報の認証 」セクションで説明します。

```
SLACK_SIGNING_SECRET=15770a…
```

もう少し下にスクロールしていくと **Display Information** がありますのでそこでアプリのアイコンや詳細など編集することができます。
次に、左のメニューから **Interactive Components** をクリックして、そのページトップになる *Interactivity* をオンにしてください。するとフィールドが表示されます。

![interactivity](https://cdn-images-1.medium.com/max/1600/1*1j55gupPm62srzooTrBPXg.png)


ここでは *Request URL* を入力します。このリクエスト URL とは、ユーザーがアクション機能を呼び出した際にスラックの API サーバーから送信されるペイロードデータの受け取り場所となる URL と考えてください。

注：この URL はあなたのアプリが稼働しているサーバーの URL となりますが、このチュートリアルでは Glitch を使っていますので、リクエスト URL は *https://my-project.glitch.me/actions* のようになります。この *my-project* 部分は各自異なる文字列になっています。確認してみてください。もし ngrok などの他のサービスでローカルホストをトンネルする場合はこのサービスの URL を使用してください。ngrok については [Using ngrok to develop locally for Slack](https://api.slack.com/tutorials/tunneling-with-ngrok) （英語）を参考に。

Request URL を設定し終わったら、**Actions** までスクロールし、**Create New Action** ボタンをクリックして、次のように入力します。

![config_request_url.png](https://qiita-image-store.s3.amazonaws.com/0/339293/41c3ddc0-ea4c-afda-f7af-fdce19707894.png)

**Create** ボタンを押し、次に場面で **Save Changes** をクリックしてください。

次は **OAuth & Permissions** へ行き **Install App to Workspace** をクリックして一旦このアプリをインストールします。インストール画面が表示されますのでそのまま続行し、ワークスペースにインストールしてください。し終わってから OAuth & Permission ページに戻ると **access tokens** が表示されていますのでそれを取得します。トークンは他のキー同様、 **.env** ファイルに保存します。

```
SLACK_ACCESS_TOKEN=xoxb-214…
```

次に、同じページ内でパーミションスコープを有効にする必要があります。下にスクロールして **Scopes** セクションまで行き、必要な bot スコープを追加します。ここでは次の二つの権限設定が必要です：

- `commands` メッセージ・アクションに必要な権限
- `chat:write`　メッセージを送信するのに必要な権限

![config scopes](https://api.slack.com/img/api/articles/message_action/config_scopes.png)

さて、アプリの設定がようやく終わりました！次は早速アプリのコーディングです。

#☕️ アプリの構築

冒頭でも述べたように、このチュートリアルでは Slack API そのものの使い方を説明していますので、Node.js の Express.js モジュールなどを使用して直接 API を呼んでいます。

さて、まず依存するモジュールをインストールしていきましょう。 POST リクエスト実行のための `Express` とミドルウェアの `body-parser`、そして HTTP リクエストクライアントの `axios`と、クエリストリングのパーサーである、`qs` をインストールします。

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

次はコード部分をみていきましょう。少しづつコード・スニペットで説明していきますので後からどんどんこのコードに追加・編集していきます。
まず、 *index.js* を作成し、Express アプリのインスタンス化し適当なポートナンバーでサーバを接続します。

```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
```

次に進む前に、まず今から構築していくスラックアプリがどう作動するかの説明を示したダイアグラムをみてみましょう。

![diagram slack app](https://api.slack.com/img/api/articles/message_action/diagram_app_flow.png)

各フローは、ユーザーがメッセージメニューからアクションを起こした時に始動します。ここで `message_action` イベントがトリガーされ、スラック側がそのイベントのペイロードを、指定されたエンドポイント（前のステップで設定した、Request URL）に送信します。

![Slack API Actions](https://cdn-images-1.medium.com/max/1600/1*UkcbMGPn7gDkerM4nwyP0w.png)

受け取りのエンドポイント側では、下のように書くことができます。この時、先に設定したパスである `/actions` を使っています。

```js
/* Snippet 2 */

app.post('/actions', (req, res) => {
  const payload = JSON.parse(req.body.payload);
  const { type, user, view } = 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 modal here
  } else if (type === 'view_submission') {
    // the modal is submitted
  }
});
```
`if(type === 'message_action')` で、イベントタイプが `message_action` か確認しています。これはユーザーがアクションを実行した際に送られるイベントタイプです。`true` である場合にはモーダルを開きます。

コード・スニペット 2 の `// open a modal here` とコメントのある部分に次のコード (Snippet 2.1) を追加します。ここではダイアログの内容の定義をし、[`views.open`](https://api.slack.com/methods/views.open) メソッドでスラッククライアント上でモーダルボックスを表示しています。

```js
/* Snippet 2.1 */

const viewData = {
  token: process.env.SLACK_ACCESS_TOKEN,
  trigger_id: payload.trigger_id,
  view: JSON.stringify({
    type: 'modal',
    title: {
      type: 'plain_text',
      text: 'Save it to ClipIt!'
    },
    callback_id: 'clipit',
    submit: {
      type: 'plain_text',
      text: 'ClipIt'
    },
    blocks: [ // Block Kit
      {
        block_id: 'message',
        type: 'input',
        element: {
          action_id: 'message_id',
          type: 'plain_text_input',
          multiline: true,
          initial_value: payload.message.text
        },
        label: {
          type: 'plain_text',
          text: 'Message Text'
        }
      },
      {
        block_id: 'importance',
        type: 'input',
        element: {
          action_id: 'importance_id',
          type: 'static_select',
          placeholder: {
            type: 'plain_text',
            text: 'Select importance',
            emoji: true
          },
          options: [
            {
              text: {
                type: 'plain_text',
                text: 'High 💎💎✨',
                emoji: true
              },
              value: 'high'
            },
            {
              text: {
                type: 'plain_text',
                text: 'Medium 💎',
                emoji: true
              },
              value: 'medium'
            },
            {
              text: {
                type: 'plain_text',
                text: 'Low ⚪️',
                emoji: true
              },
              value: 'low'
            }
          ]
        },
        label: {
          type: 'plain_text',
          text: 'Importance'
        }
      }
    ]
  });
};

axios.post('https://slack.com/api/views.open', qs.stringify(viewData))
  .then((result) => {
    res.sendStatus(200);
  });
```

最後の５行は、*axios* モジュールを使って POST リクエストをスラックに送信しています。`views.open` メソッドでダイアログ表示に成功したら即座に HTTP status 200 を送り返す必要があります。

![Slack modal](https://cdn-images-1.medium.com/max/1600/1*KXqEbHDmjurOrcZrOf6f9Q.png)

同様に、このダイアログがユーザーによって送信された際も先ほどと同じエンドポイントが呼び出されます。少し上の *code snippet 2* の `// dialog is submitted` というコメント部分に次のコード (snipet 2.2) を追加します。

```js
/* Snippet 2.2 */

} else if(type === 'view_submission') {
  res.send(''); // Make sure to respond immediately to the Slack server to avoid an error

  // Save the data in DB 
  db.set(user.id, payload); // this is a pseudo-code! 

  // DM the user a confirmation message
  let values = view.state.values;

  let blocks = [
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: 'Message clipped!\n\n'
      }
    },
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*Message*\n${values.message.message_id.value}`
      }
    },
    {
      type: 'section',
      fields: [
        {
          type: 'mrkdwn',
          text: `*Importance:*\n${values.importance.importance_id.selected_option.text.text}`
        },
        {
          type: 'mrkdwn',
          text: `*Link:*\nhttp://example.com/${user.id}/clip`
        }
      ]
    }
  ];

  let message = {
    token: process.env.SLACK_ACCESS_TOKEN,
    channel: userId,
    blocks: JSON.stringify(blocks)
  };

  axios.post(`${apiUrl}/chat.postMessage`, qs.stringify(message));
} 
```

この時も、ダイアログが無事に返信されました、とサーバーに伝える必要があるので、ここでは空の HTTP 200 リスポンスをまず送り返します。

次に、クリップされたメッセージをデータベースに保存するという仮定で進めます。（コードの `db.set` 部分はデータベース部分は擬似コードで省略してありますのでこのまま使うとエラーになります。）保存を同期がおわった時点で `chat.postMessage` メソッドを使ってユーザーに確認メッセージを送りましょう。

この最後の確認メッセージの過程は、アプリのユーザー・エクスペリエンスのためには非常に重要ですので、この先新しいアプリを作っていく際にもぜひ、ユーザー視点に立って使いやすさについて考えてみましょう。

さて、では一旦このコードを実行してみましょう。これでメッセージメニューに、このアプリのアクションが追加されているはずなので、クリックしてみてください。架空のデータベースパート以外はきちんと動作していることを確認してください。

では最後に、アプリのセキュリティ面を改善しましょう。

## 🔐 リクエスト情報の認証

ここまでのコードでも動作はしますが脆弱性があります。エンドポイントで受け取ったリクエストが本当にスラックから来たものなのかがわからないからです。それをリクエスト毎に確認をする必要があるので、今から [signing secrets](https://api.slack.com/docs/verifying-requests-from-slack)を使って認証してみましょう。

この Signing secrets （サインイング・シークレット、サイン認証）は、今まで Slack API で使われてきた verification tokens に代わるもので、セキュリティ面をさらに強化するために、各リクエストごとに HTTP ヘッダーに `X-Slack-Signature` を追加しています。

ここでの `X-Slack-Signature` は、 HMAC SHA256 でハッシュ化されたリクエスト・ペイロードで、 Signing Secret を使ってキー化します。

この例ではすでに  Express と body-parser を使っていますので、body-parser の `verify` ファンクションオプションを使ってこのペイロードを取得してみましょう。

始めのコード・スニペット1 に戻りましょう。`//The next two lines will be modified later` というコメント部の body-parser ミドルウェアの設定部分（12–13行目）を次のコードに置き換えてください。

```js
/* 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**](https://glitch.com/edit/#!/slack-clipit-simplified?path=src/verifySignature.js:1:1) のほうですでに用意しましたので、あとはこのファンクションを **index.js** の冒頭部に追加するだけです。

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

そして、*Snippet 2* の、イベント・エンドポイントでこのサインイング・シークレットをハッシュを比較することによってリクエストの認証をします。

```js
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;
}
```

この例では、イベント発生時のエンドポイントでのみに認証をしていますが、この先スラックアプリを構築する際はペイロードを受け取る時に逐一、認証をする必要があります。詳しくは [Verifying requests from Slack](https://api.slack.com/docs/verifying-requests-from-slack) をお読みください。

さて、Node コードをもう一度実行してみましょう。おめでとう、Action-able なアプリの完成です！

# 📄Related Slack API Documentation
- [Actions](https://api.slack.com/actions)
*Slack Platform documentation*
- [Dialogs](https://api.slack.com/dialogs)
*Slack Platform documentation*
- [chat.postMessage](https://api.slack.com/methods/chat.postMessage)
*API method documentation*
- [Signing Secret: Verifying requests from Slack](https://api.slack.com/docs/verifying-requests-from-slack) - 
*Slack Platform documentation*

---
Questions? Comments? コメントや質問はここ、もしくは Tomomi @girlie_mac か Slack Platform @SlackAPI へツイートしてくださいね。

---

原文 [*Tutorial: Developing an Action-able App*](https://medium.com/slack-developer-blog/tutorial-developing-an-action-able-app-4d5455d585b6) by [Tomomi Imura](https://qiita.com/girlie_mac) (Slack)