Go to Slack

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

By Tomomi Imura. Published March 5th, 2019

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

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

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

"ClipIt! for Slack" を作る

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

このスラックアプリは下のように動作します。 ClipIt! Slack

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

  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 でスラックアプリをどんどん書いていきたい、という方は公式の Node SDK も参考にしてみてください。

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

⚙️ アプリの作成と設定

まずは、開発用に使える Slack ワークスペースにサインインしてください。今からそのワークスペース上で新規のアプリを作成、インストールしていきます。新規アプリは、 /apps の Create New App ボタンから作成してください。ボタンをクリックすると表示されるダイアログ内でアプリ名を入力し、開発用ワークスペースを選択してください。

config_app_create.png

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

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

config_app_credentials.png

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

SLACK_SIGNING_SECRET=15770a...

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

interactivity

ここでは 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 (英語)を参考に。

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

config_request_url.png

Create ボタンを押し、次に場面で Save Changes をクリックしてください。 次に左のメニューから Bot Users へいきます。そこで Add a Bot User をクリックすると次にようにボットの名前の設定ができます。

add a bot

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

SLACK_ACCESS_TOKEN=xoxb-214...

次に、同じページ内でパーミションスコープを有効にする必要があります。下にスクロールして Scopes セクションまで行き、 botcommand スコープがすでに有効になっているのを確認してください。 さて、アプリの設定がようやく終わりました!次は早速アプリのコーディングです。

☕️ アプリの構築

冒頭でも述べたように、このチュートリアルでは 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

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

diagram slack app

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

Slack API Actions

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

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

13行目では、イベントタイプが message_action か確認しています。これはユーザーがアクションを実行した際に送られるイベントタイプです。true である場合にはダイアログを開きます。

先のコード・スニペットの // open a dialog! とコメントのある部分に次のコード (Snippet 2.1) を追加します。ここではダイアログの内容の定義をし、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);
  });

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

Slack dialog

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

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

  axios.post('https://slack.com/api/chat.postMessage', qs.stringify(message))
    .then((result => {
      console.log(result.data);
    }))
    .catch((err) => {
      console.log(err);
    });
}

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

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

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

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

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

🔐 リクエスト情報の認証

ここまでのコードでも動作はしますが脆弱性があります。エンドポイントで受け取ったリクエストが本当にスラックから来たものなのかがわからないからです。それをリクエスト毎に確認をする必要があるので、今から 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;
}

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

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

📄Related Slack API Documentation


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


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