AWS Lambda (Python3) + API Gateway で Slackのbotを作ろう

# echo bot
echoサーバという、送られてきたものをそのまんま返すサーバが、ネットワークを使ったプログラムの世界の中では、Hello World的存在として知られているようです。

基本的な部分はきっちり押さえつつ、それさえできれば用途に合わせてカスタマイズができるし、読者にとって興味ない部分はあまり読まなくていい、何かの解説には持って来いの題材です。

今回、echoサーバのように、何かを書き込むと同じものをそのまんま投稿するSlackのbotを作ってみました。

Web上に、すでにLambdaでSlackのbotを作る解説はありますが、UIが変わっていたり、node.jsで書いてあるものが多かったり、パラメータの渡され方などがあまりよく分からなかったり、個人的にしっくりこなかったので、記事にしました。

# Lambdaの作成
1. 「AWSマネジメントコンソール」から「Lambda」をクリックします。
1. 左のハンバーガーメニュー("三" のマーク)をクリックし、「関数」をクリックします。
1. 右のほうにある「関数の作成」をクリックします。
2. 「一から作成」が選ばれた状態で、以下のように項目を埋めます。
    - 名前「echobot」
    - ランタイム「Python 3.6」
    - ロール, 既存のロール→すでに使いたいロールがあれば、それを使用。今回は「テンプレートから新しいロールを作成」で、何もテンプレートを選ばなかった。(そうすると[「基本的な Lambda のアクセス権限」テンプレート](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/policy-templates.html)が選択されるっぽい)
1. 「関数の作成」をクリックします。
3. 「echobot」(作成時に指定した名前)が選択されていることを確認して、下にある「関数コード」のエディタを見ます。(ちなみに、アドブロック系ソフトを入れているとロードされない場合があるっぽい。このサイトでは外すように。)
4. 以下のようにコードを書きかえます。(先にネタばらしすると、次に紹介するAPI Gatewayのために、こんな感じの出力が必要)

```py3:HTTPステータス200(OK)と本文にeventをJSON形式にしたものを返す
import json

def lambda_handler(event, context):
    # TODO implement
    return { 'statusCode': 200, 'body': json.dumps(event) }
```

これで、Lambdaの作成が終わりました。
「トリガーの追加」からでもAPI Gatewayを追加できるんですが、ここで追加すると全部のHTTPメソッドで叩けるようになってしまい、気持ち悪いので、別のところから追加します。
また、コードを書かないといけないのですが、とりあえず後回しにします。
今の画面は開いたままにしておいてください。
(または、閉じてしまっても、手順2のハンバーガーメニュー→「関数」から作ったものを選ぶこともできます)

# API Gatewayの作成
1. 「AWSマネジメントコンソール」から「API Gateway」をクリックします。
2. 「APIの作成」をクリックします。
3. 「新しいAPI」を選択し、以下のように空欄を埋めます。
    - API名「echobot」
    - 説明「echobotのエンドポイント」
    - エンドポイントタイプ→デフォルトのままで問題ない
0. 右下の「API作成」をクリックします。
1. 「アクション」→「メソッドの作成」をクリック(APIを階層化したい場合は「リソースの作成」を行ってもいいが、今回はそんな豪華なAPIじゃないので、直下にメソッドを作成する)
2. 「POST」を選択し、選択後に出るチェックボタンをクリックします。
3. 以下のようにセットアップします。
     - 統合タイプ「Lambda関数」
     - Lambdaプロキシ統合の使用→チェックを入れる
     - Lambdaリージョン→さっき作ったLambdaのリージョンを指定。変な操作してなかったらデフォルトでいいはず。
     - Lambda関数→さっき作ったLambdaの名前「echobot」を入力
     - デフォルトタイムアウトの使用→チェックを入れたままでいい。(echobotの性質上、チェック外してもっと短くしてもいい気はするが、今回、面倒なのでいじらない)
0. 「保存」をクリックします。
1. 「Lambda関数に権限を追加する」とか出るので、「OK」をクリックします。
2. 「テスト」をクリックします。
3. とりあえずクエリ文字列とかは空のまま「テスト」をクリックしてみます。
    - 先ほどのLambdaの作成時のコードから大方予想がつくように、「ステータスコード」が200、「レスポンス本文」が`'body'`に指定したもの(先ほどのコードでは`event`をJSONにしたもの)になる
    - 暇な人は、Lambdaのコードをいじって、別のステータスコードを返してみたり、別の本文を返してみたりする
    - `event`はPython内で、辞書形式っぽいと分かる
    - `event['queryStringParameters']`は「クエリ文字列」が空だと、空文字列ではなく`None`になっている
    - 「クエリ文字列」に`foo=123&bar&hoge=567`と入力し、テストすると`'queryStringParameters': {'bar': None, 'hoge': '567', 'foo': '123'}`となった
    - 必要に応じて「ヘッダー」と`event['headers']`の関係も確認する
    - 「リクエスト本文」も空だと`event['body']`が`None`になる。空じゃない場合は、指定した文字列が`event['body']`に入ってくる。

これで、API Gatewayの作成が終わり、event変数の中身が分かってきました。
API Gatewayの画面も後々使いますので、開いたままにしておいてください。

#Slackの発信Webhook (Outgoing Webhooks)の準備
https://slack.com/apps/manage/custom-integrations にアクセスし、「発信Webhook」(英語UIだとOutgoing Webhooksかも)をクリック、「設定を追加」をクリックします。
(発信Webhookを使うのが初めてだと、手順が増えるかもしれないですが、私のチームでは誰かがすでに追加しているので詳細な手順が分かりません。すみません)

0. 「発信ペイロードとレスポンス」をクリックすると、どんなデータがやってくるのか分かるのでコピーし、メモ帳か何かに貼り付けます。
1. 貼り付けたデータの改行を`&`に置き換え、コピーします。
1. まだWebhookを作る途中ですが、一旦ここで放置し(後で使うので、画面は残しておいてください)、AWSのAPI Gatewayテスト画面に戻ります。
2. 「リクエスト本文」に、先ほどコピーしたのを貼り付けて、テストします。
3. テスト後、「レスポンス本文」に出てきた結果をコピーします。
4. 作成したLambdaの画面に戻ります。
5. Lambdaのテストの横にある、テストイベントの名前をクリックし、「テストイベントの設定」を行います。
6. テスト内容のテキストを全部消して、先ほどコピーした「レスポンス本文」の中身を貼り付けて「保存」します。今はまだLambdaのテストを再実行する必要はありません。

#SlackのリクエストをAPI Gatewayを通してLambdaで実行する際知っておくといいこと
先ほどの操作にどういう意味があるかというと、HTTPリクエストはevent変数に入れられてLambdaが呼ばれるわけですが、実際にやってくる形式に近いものをテストデータとして設定しました。
こんな感じで、ちょっとずつ試してみて分かったechobotを作るうえで重要なパラメータ等は以下の通りです。

- `event` は辞書型でやってくる
- `event['body']` にリクエストの本文がやってくる
- Slackからのパラメータは「発信ペイロードとレスポンス」のようなデータが`application/x-www-url-form-encoded`形式でやってきて、`event['body']` に入る
- 上記の形式のデータは、Pythonの`urllib.parse.parse_qs`で辞書型にパース出来る
- Slackからのパラメータで大切なのをいくつか
    - `text=...` が、投稿の本文
    - `token=...` の`token`は、外部からは推測が難しいので、この値を検査すれば、リクエストがSlackから送られてきたものであると確認できる
    - `user_name=...`は投稿ユーザの名前。Webhookなどから投稿した場合は`slackbot`が指定される
- 本文(`body`)をJSON形式で `{"text": "投稿したい文章"}` または `{"text": "投稿したい文章", "username": "表示させるユーザ名"}`のような文字列にすると、指定された内容がSlackに投稿される


#Lambdaのコーディング
上で分かったことに合わせて、Lambdaをコーディングします。
その際、Lambdaに無限ループを起こさせないよう気を付けます。つまり、
ユーザが何かを書く→Lambda呼び出し→Lambdaで何かを書く→Lambda呼び出し→Lambdaで何かを書く→Lambda呼び出し→...
とならないようにコードを書きます。

そのための方法として、以下の2つがパッと思いつきます。

1. Webhookの設定で、特定の文字列を含む場合のみLambdaを呼び出すようにする。Lambda側は、投稿する文字列に自分自身を呼び出す文字列を含まないよう気を付ける。
1. Lambda側で、投稿者をチェックし、`slackbot`による投稿だった場合は何も書き込まないようにする

どちらでもいいのですが、今回は後者の方法を取りました。
次の手順で、トークンをセットし、また、コードを記述します。

1. 「関数コード」の下の「環境変数」のところに、Slackのtokenとしてやってくる文字列を指定します。これは、エンドポイントが誰かに知られても、いたずらできなくするための措置です。
    - キー「SLACK_TOKEN」
    - 値: Slackの発信Webhookの「発信ペイロードとレスポンス」にあった`token=...`の行の`=`より後の文字列
2. 以下のように関数コードを書いて、保存します。

```py3:echobot完成版
import json
import os
from urllib.parse import parse_qs

def lambda_handler(event, context):
    token = os.environ['SLACK_TOKEN']
    query = parse_qs(event.get('body') or '')
    if query.get('token', [''])[0] != token:
        # 予期しない呼び出し。400 Bad Requestを返す
        return { 'statusCode': 400 }
    slackbot_name = 'slackbot'
    if query.get('user_name', [slackbot_name])[0] == slackbot_name:
        # Botによる書き込み。無限ループを避けるために、何も書き込まない
        return { 'statusCode': 200 }
    # textの内容をそのまま書き込む
    return {
        'statusCode': 200,
        'body': json.dumps({
            'text': query.get('text', [''])[0]
        }) }
```

「テスト」をして、`statusCode`が200、`body`がJSON形式で`text`にリクエストの本文と同じっぽいものが記述されていたらLambdaの方はOKです。(もしかしたら、エスケープされていてわかりづらいかもしれないですが)
API Gatewayの方でも再度テストをして、ステータスコードが200で、textがリクエストの本文と同じだったらOKです。

#API GatewayエンドポイントURLの作成
1. API Gatewayのページの「リソース」の右側、「アクション」をクリックし、「APIのデプロイ」をクリックします。
2. 以下のように入力し、「デプロイ」をクリックします。
   - デプロイされるステージ「新しいステージ」
   - ステージ名：適当。ここでは「echobot」
   - ステージの説明、デプロイメントの説明: 書きたければ書く
3. 「ステージエディター」に画面が切り替わり、「URLの呼び出し」の横にURLができました。このURLをコピーします。

#Slack Webhookの設定
1. 作りかけて放置していた、Slack Webhookの作成画面に戻ります。
2. 「インテグレーションの設定」までスクロールし、以下のように設定します。
    - チャンネル: echobotの性質上、全チャンネルだと迷惑極まりないので、echobotを入れるチャンネルを選ぶか作成します。今回、echobotチャンネルを新たに作成しました。
    - 引き金となる言葉: ここに指定した単語で始まる行があったときのみbotをAPIを叩きます。全チャンネルの場合は必須ですが、今回はチャンネルを指定したので不要です。何も指定しません。
    - URL: 先ほどAPI GatewayをデプロイしてできたURLを貼り付けます
    - トークン: 変えずにそのまま。Lambdaの方で設定した環境変数SLACK_TOKENと同じかどうか、念のため確認する。(違っていた場合はLambdaの方を変えて保存する)
    - それ以外の項目: 特にさわる必要はない。変えたいものがあったら変える。
3. 「設定を保存する」をクリック。

#完成
echobotを入れたチャンネルで、何か書き込んでみる。
書き込んだのと同じ文字列がやってくるはず。

![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/226504/b20d952c-129b-bd81-cf91-d8839e4d6c2c.png)

やってこなかったら、テストが通るか試す、設定を見直すなど。
無限ループが発生したら、WebhookのURLを消して保存しなおすとか、Lambdaの関数の一行目に`return`を書くとか、とにかく止める努力を。

そして、おそらく皆さんが本当に作りたいのはechobotじゃないと思うので、ご自由に改造してみてください。