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

By Tomomi Imura

Published: 2019-03-05
Updated: 2020-02-11

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

Slack には、ユーザーがメッセージに対して絵文字リアクションシェアを行う標準機能がありますが、Actions (アクション) 機能を使うとユーザーがメッセージを送信したときにアプリを起動することもできます。

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

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

#"ClipIt! for Slack" を作る

これから作るアプリは「ClipIt! for Slack」というアプリです。あなたが架空の ClipIt! (クリップ・イット)というサービスをすでに運営していると仮定し、そのサービスに対応する Slack アプリを作るという設定でいってみましょう。このウェブサービスは、ユーザーがインターネット上で「クリップ」(保存)したテキストをデータベースに保存、同期していきます。

さて、あなたはこのサービスを拡張して Slack のメッセージも保存できるようにしたいと思っています。と、いうことで今からこのアクション機能を使ってユーザーが Slack 上のメッセージをクリッピングできるようにしてみましょう。

このアプリは下のように動作します。

demo 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 がインストールされていることを確認してください。この先も Node で Slack アプリをどんどん書いていきたい、という方は公式の Node SDK も参考にしてみてください。

🐙🐱 ソースコードは [GitHub] (https://github.com/slackapi/template-action-and-dialog ) にありますが、このチュートリアルでは説明しやすいようにさらに簡略化したコードを使っています。そちらのソースは Glitch という、Node アプリをウェブブラウザで書いて実行させることができるウェブ IDE 上に置いてあります。

🎏🍴そのコードをこのリンク https://glitch.com/edit/#!/remix/slack-clipit-simplified から "remix" してください。Glitch の remix とは、GitHub の fork のような機能で、リミックスしたコードは自分のリポジトリとなりますので、好きなように書き換える事ができます。

⚙️ アプリの作成と設定

まずは、開発用に使える Slack ワークスペースにサインインしてください。今からそのワークスペース上で新規のアプリを作成、インストールしていきます。Slack App マネージメント でアプリ名を入力し、開発用ワークスペースを選択してください。

新規アプリを作成

screenshot - create an app

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

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

screenshot - app credentials

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

SLACK_SIGNING_SECRET=15770a…

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

screenshot - actions

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

注:この URL はあなたのアプリが稼働しているサーバーの URL となりますが、このチュートリアルでは Glitch を使っていますので、リクエスト URL は https://my-project.glitch.me/actions のようになります。

この my-project 部分は各自異なる文字列になっています。確認してみてください。もし ngrok などの他のサービスでローカルホストをトンネルする場合はこのサービスの URL を使用してください。ngrok については Using ngrok to develop locally for Slack (英語)を参考に。

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

screenshot - actions

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

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

SLACK_ACCESS_TOKEN=xoxb-214…

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

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

screenshot - scopes

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

#☕️ アプリの構築

冒頭でも述べたように、このチュートリアルでは 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 アプリのインスタンス化し適当なポートナンバーでサーバを接続します。


/* 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

次に進む前に、まず今から構築していく Slack アプリがどう作動するかの説明を示した図をみてみましょう。

app flow diagram

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

app flow diagram

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

/* 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 メソッドで Slack クライアント上でモーダルボックスを表示しています。

/* 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);
  });

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

app

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

/* 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 メソッドを使ってユーザーに確認メッセージを送りましょう。

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

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

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

🔐 リクエスト情報の認証

ここまでのコードでも動作はしますが脆弱性があります。エンドポイントで受け取ったリクエストが本当に Slack から来たものなのかがわからないからです。それをリクエスト毎に確認をする必要があるので、今から signing secretsを使って認証してみましょう。

この 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行目)を次のコードに置き換えてください。

/* 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'); 

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

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 アプリを構築する際はペイロードを受け取る時に逐一、認証をする必要があります。詳しくは Verifying requests from Slack をお読みください。

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


ご質問やコメントがありましたら、開発者サポート (@SlackAPI または support@slack.com) までご連絡ください。

原文 Tutorial: Developing an Action-able App by Tomomi Imura (Slack)


おすすめの関連記事 🦄