Slack の Slash Command をサーバーレスで実装

Slash Command とは

こんな感じで Slack の入力フォームで何かコマンドを実行する仕組み。 自分で実装できる。

実際に構築したアプリ詳細はこちら

構成

  • API Gateway
  • Lambda
  • (その他、DB など)

Slash Command の場合、ユーザーが Slack 上でコマンドを発行したときに Slack API サーバーから予め設定した URL に対して HTTP リクエスト (POST) が発行される。 それを API Gateway で受け付けてあげれば良い。

要点

リクエスト検証

悪意あるユーザーによるアタックを防ぐために Slack API サーバーからの正当なリクエストであることを検証する必要がある。 検証方法はよくある HMAC 認証で、Slack のドキュメント にも親切な解説がある。

Python3 の Lambda だとこんな雰囲気かと。

import os
import hmac
import hashlib

def __generate_hmac_signature(timestamp, body):
    # Slack App - Basic Information - App Credentials に記載されている
    # Signing Secret
    secretkey = os.environ['SLACK_API_SIGNING_SECRET']
    secretkey_bytes = bytes(secretkey, 'UTF-8')

    message = "v0:{}:{}".format(timestamp, body)
    message_bytes = bytes(message, 'UTF-8')
    return hmac.new(secretkey_bytes, message_bytes, hashlib.sha256).hexdigest()


def is_valid_event(event):
    if "X-Slack-Request-Timestamp" not in event["headers"] \
        or "X-Slack-Signature" not in event["headers"]:
        return False

    request_timestamp = event["headers"]["X-Slack-Request-Timestamp"]
    now_timestamp = int(datetime.datetime.now().timestamp())

    if abs(request_timestamp - now_timestamp) > (60 * 5):
        return False

    expected_hash = __generate_hmac_signature(
        event["headers"]["X-Slack-Request-Timestamp"],
        event["body"]
    )

    expected = "v0={}".format(expected_hash)
    actual = event["headers"]["X-Slack-Signature"]

    logger.debug("Expected HMAC signature: {}".format(expected))
    logger.debug("Actual HMAC signature: {}".format(actual))

    return hmac.compare_digest(expected_hash, actual)

※ちなみに、API Gateway Lambda オーソライザー ではリクエストボディが取得できないらしいので無理。普通のLambda関数の先頭で検証するしかない。

同期処理か、非同期処理か

Slash Command のドキュメントによると、 Slack API からの HTTP リクエストに対して、3000 ミリ秒以内に 200 OK のレスポンスを返さないと失敗扱いになる。

さほど重い処理を動かさないのであれば同期的に処理してしまっても大丈夫そうだが、 そうでない場合や真面目に作る場合は非同期的に処理を実装する必要がある。

Slack API 的にも response_url という特別なURL により、そのような非同期的な処理方法をサポートしている。 この response_url と SNS (もしくは SQS)、Lambda 関数 2 つを使えば、非同期的な処理を実装できる。

こんな感じかと。

SQS からの Lambda 実行はつい最近サポートされました。

Lambda (API Gateway プロキシ統合) における HTTP リクエストの内容取得

参考: プロキシ統合のための Lambda 関数の入力形式

Lambda のハンドラ関数引数

  • HTTP リクエストヘッダ: event["headers"]
  • HTTP リクエストボディ: event["body"]

など。

Lambda (API Gateway プロキシ統合) における HTTP レスポンス指定方法

return {
    "statusCode": 200,
    "headers": { "headerName": "headerValue", ... },
    "body": "OK"
}

Lambda のハンドラ関数からこんなものを返せば良い。