Slack の Slash Commands で Interactive Messages を利用する

June 20, 2020

概要

この記事では、Bot がユーザーにボタン付きメッセージを送信し、ユーザーからのボタンの入力に応じてアクションする Slash Commands の作り方を説明します。

Slash Commands は、「/」(スラッシュ)から始まるコマンドを入力すると、特定のアクションを実行する機能です。 Slack 上で、ボタンやメニューなどのインターフェースを付け足メッセージを送信できます。 また、ユーザーからのアクションに応答するには、Slack Interactive Messages という機能を利用します。

Slash Commands や Interactive Messages の処理は、AWS Lambda(TypeScript)+ API Gateway で行います。 Lambda の設定やデプロイは手動が面倒なので、 Serverless Framework を利用します。

完成イメージ

こんなふうに Slash Comamnd を入力すると、 01

Bot がボタン付きのメッセージを返します。 02

ユーザーがボタンをクリックすると、 03

Bot のメッセージを書き換えます。

環境

  • Node.js v12.18.1
  • Serverless Framework: v1.73.1
  • AWS CLI v2.0.24

Slack App の作成

Slash Commands を作成します。

Slack App の作成

  1. https://api.slack.com/apps を開きます。
  2. [Create New App]ボタンをクリックします。
  3. モーダルが表示されるので、アプリケーション名と適用するSlack のワークスペースを選択します。
  4. [Create App]ボタンをクリックします。Slack App が作成されます。

OAuth スコープの設定

  1. 左サイドの「Features」から「OAuth & Permissions」を選択します。
  2. 「Bot Token Scopes」より、[Add an OAuth Scope]ボタンをクリックします。
  3. 「chat:write」を選択して追加します。
  4. 同様に、「commands」を選択して追加します。
  5. ページ上部の[Install App to Workspace]ボタンをクリックして、ワークスペースにこの Slack App をインストールします。
  6. アクセス権限をリクエストするページが表示されます。[許可する]ボタンをクリックします。

Signing Secret の確認

  1. 左サイドの「Features」から「Basic Information」を選択します。

  2. 「App Credentials」から「Signing Secret」の値を控えておきます。あとで、Lambda の環境変数の設定で利用します。

プロジェクト作成

  1. Serverless Framework を使って、プロジェクトを作成します。TypeScript で開発したいので、aws-nodejs-typescript テンプレートを利用します。

    serverless create -t aws-nodejs-typescript -p slack-interactive-command
    
    Serverless: Generating boilerplate...
    Serverless: Generating boilerplate in "/Users/chick-p/workspace/slack-interactive-command"
    _______                             __
    |   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
    |   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
    |____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
    |   |   |             The Serverless Application Framework
    |       |                           serverless.com, v1.73.1
    -------'
    
    Serverless: Successfully generated boilerplate for template: "aws-nodejs-typescript"
  2. 生成されたslack-interactive-command ディレクトリに移動します。

    cd slack-interactive-command
  3. Serverless Framework のデプロイ時に必要なモジュールをインストールしておきます。

    yarn install

Block Kit Builder での UI パーツ作成

Block Kit Builder を使うと、メッセージに利用できる UI パーツを簡単にデザインできます。

左側のメニューから利用したいパーツをドラッグ&ドロップで中央のイメージ表示部分に配置すると、右側の[Payload]タブに JSON が出力されます。 また右側の JSON を編集すると、中央のイメージ表示部分に反映されます。

04

今回は、「りんご」「バナナ」「オレンジ」というラベルのボタンを配置します。またそれぞれボタンがクリックされたときの値を value に設定しておきます。

メッセージを作成したら、[Payload]タブの JSON を控えておきます。

Slash Commands が実行されたときに呼び出される関数 command の実装

Slash Commands が実行されたときに呼び出される関数を作成します。

  1. handler.ts を開いて、次のコードに書き換えます。blocks 変数の値は、「Block Kit Builder での UI パーツ作成」で作成した JSON の blocks の値を指定します。

    import {
      APIGatewayProxyEvent,
      APIGatewayEventRequestContext,
      APIGatewayProxyResult,
    } from "aws-lambda";
    import "source-map-support/register";
    
    const buildErrorResponse = () => {
      return {
        statusCode: 200,
        body: "Sorry, that didn't work. Please try again.",
      };
    };
    
    export const command = async (
      event: APIGatewayProxyEvent,
      context_: APIGatewayEventRequestContext
    ): Promise<APIGatewayProxyResult> => {
      // リクエストが作成した App から送信されたものかを判定する
      // 詳細は後述
      if (!isVerifiedRequest(event)) {
        return buildErrorResponse();
      }
    
      // 「Block Kit Builder での UI パーツ作成」で作成した JSON の `blocks` の値
      const blocks = [
        {
          type: "actions",
          elements: [
            {
              type: "button",
              text: {
                type: "plain_text",
                text: "りんご",
                emoji: true,
              },
              value: "apple",
            },
            {
              type: "button",
              text: {
                type: "plain_text",
                text: "バナナ",
                emoji: true,
              },
              value: "banana",
            },
            {
              type: "button",
              text: {
                type: "plain_text",
                text: "オレンジ",
                emoji: true,
              },
              value: "orange",
            },
          ],
        },
      ];
    
      // メッセージの本文に Block Kit Builder で作った blocks を返す
      return {
        statusCode: 200,
        body: JSON.stringify({
          response_type: "ephemeral",
          blocks,
        }),
      };
    };
    • response_typeephemeral を指定すると、コマンドを実行したユーザーにのみ応答メッセージが表示されます。

Slack リクエストの検証

Slack から送られてくるリクエストが正しいかを検証する isVerifiedRequest() を実装します。

Slack から送られてくるリクエストは、次の2つを満たすとき正しいリクエストとして判定することを推奨しています。

  • リクエストヘッダー X-Slack-Request-Timestamp の値は5分以内か
  • リクエストヘッダー X-Slack-Signature が、リクエストボディの値とリクエストヘッダーのタイムスタンプの値をSigning Secret をキーにしてハッシュ化した値と正しいか

参考: https://api.slack.com/authentication/verifying-requests-from-slack

  1. cryptotsscmp を追加します。

    yarn add crypto tsscmp
  2. 以下を handler.ts に追記します。

    // ...略
    import "source-map-support/register";
    // 以下を追記
    import * as crypto from "crypto";
    import timingSafeCompare from "tsscmp";
    
    // ...略
    
    // 以下を追記
    const isVerifiedRequest = (event): boolean => {
      const signature = event.headers["X-Slack-Signature"];
      const timestamp = event.headers["X-Slack-Request-Timestamp"];
    
      // timestamp does not differ from local time by more than five minutes
      if (Math.floor(new Date().getTime() / 1000) - timestamp > 60 * 5) {
        return false;
      }
    
      // compare to equal the signatures
      const hmac = crypto.createHmac("sha256", process.env.SLACK_SIGNING_SECRET);
      const [version, hash] = signature.split("=");
      hmac.update(`${version}:${timestamp}:${event.body}`);
      return timingSafeCompare(hmac.digest("hex"), hash);
    };
    
    // ...略

ユーザーがボタンをクリックしたときに呼び出される関数 answer の実装

ユーザーがボタンをクリックしたときに呼び出される関数 answer を作成します。

  1. axiosqs を追加します。

    yarn add qs axios
    • axios は、ユーザーからのアクションの応答を送信するために利用します。
    • qs は、Slack から送られてくるレスポンスを parse するために利用します。
  2. 以下を handler.ts に追記します。

    // ...略
    import timingSafeCompare from "tsscmp";
    // 以下を追記
    import qs from "qs";
    import axios from "axios";
    
    // ...
    /// 略
    // ...
    
    // 以下を追記
    export const answer = async (
      event: APIGatewayProxyEvent,
      context_: APIGatewayEventRequestContext
    ): Promise<APIGatewayProxyResult> => {
      // リクエストが作成した App から送信されたものかを判定する
      if (!isVerifiedRequest(event)) {
        return buildErrorResponse();
      }
      const body = qs.parse(event.body);
      const payload = JSON.parse(body["payload"] as string);
      // ボタンクリックのアクションのときだけ返答する
      if (payload.type === "block_actions") {
        await axios.post(
          payload.response_url,
          JSON.stringify({
            text: `${payload.actions[0].value} is selected!`,
          }),
          {
            headers: {
              "Content-Type": "application/json; charset=UTF-8",
            },
          }
        );
      }
      return {
        statusCode: 200,
        body: JSON.stringify({
          response_type: "ephemeral",
        }),
      };
    };
    • payload.response_url で渡される URL にリクエストを送ると、元のメッセージ(ボタンを表示していたメッセージ)を書き換えできます。
      • response_url の URLは、30分以内に5回まで有効です。
      • 期限が切れた後にメッセージを書き換えたい場合は、Web API の chat.update を使って書き換えます。

serverless.yml の修正

AWS へデプロイするため、serverless.yml を修正します。

リージョンの設定

この記事では、東京リージョンを指定します。

provider:
  name: aws
  runtime: nodejs12.x
  apiGateway:
    minimumCompressionSize: 1024 # Enable gzip compression for responses > 1 KB
  environment:
    AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1
  region: ap-northeast-1 # 追記

作成した関数を定義

作成した関数 commandanswer をそれぞれ定義します。 Slack からは POST リクエストが送られてくるので、method は post にします。

functions:
  command:
    handler: handler.command
    events:
      - http:
          method: post
          path: command
  answer:
    handler: handler.answer
    events:
      - http:
          method: post
          path: answer

環境変数を定義

環境変数(SLACK_SIGNING_SECRET)は、デプロイするコマンドで与えるようにします。

provider:
  name: aws
  runtime: nodejs12.x
  apiGateway:
    minimumCompressionSize: 1024 # Enable gzip compression for responses > 1 KB
  environment:
    AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1
    SLACK_SIGNING_SECRET: ${env:SLACK_SIGNING_SECRET} # 追記
  region: ap-northeast-1

AWS へのデプロイ

AWS にデプロイします。secret の部分は、「Slack App の作成」の「Signing Secret の確認」で確認した Signing Secret に置き換えてください。

SLACK_SIGNING_SECRET=secret serverless deploy

ローカルで ts ファイルが js ファイルにトランスパイルされ、AWS にデプロイされます。

デプロイが完了すると、Lambda のエンドポイント URL が表示されるので、控えておきます。

...略...
endpoints:
  POST - https://example.execute-api.ap-northeast-1.amazonaws.com/dev/command
  POST - https://example.execute-api.ap-northeast-1.amazonaws.com/dev/answer
...略...

Slack App の設定

Slash Commands の追加

  1. https://api.slack.com/apps を開きます。

  2. 作成した Slack アプリケーションを開きます。

  3. 左サイドの「Features」から「Slash Commands」を選択します。

  4. [Create New Command]ボタンをクリックします。

  5. 次の内容を入力します。

    項目
    Commandコマンド名を入力します。この記事では「/fruits」にしています。
    Request URLLambda のエンドポイント URL(command のほう)を入力します。
    Short Descriptionコマンドの説明文を入力します。
  6. [Save]ボタンをクリックします。

Interavtive Messages の設定

  1. 左サイドの「Features」から「Interactivity & Shortcuts」を選択します。
  2. 「Interactivity」を「On」にします。
  3. 「Request URL」に、Lambda のエンドポイント URL answer のほう)を入力します。
  4. [Save Changes]ボタンをクリックします。

動作確認

  1. Slack のワークスペースを開きます。
  2. 任意のチャンネルを開きます。
  3. メッセージ欄に「/fruits」(「Slash Commands の追加」で追加したコマンド)を入力し、送信します。 01
  4. ボタン付きのメッセージが表示されます。 02
  5. いずれかのボタンをクリックします。
  6. ボタン付きのメッセージの内容が書き換わります。 03

以上で動作確認は終わりです。

参考