Slack の bot を自作する（AWS Lambda + API Gateway）

無料でも十分使える Slack ですが、 bot を自作できたらもっと楽しいですよね。
自社に Slack を導入し、 bot を作って遊んでいるのですがせっかくなら記事にしよう、と思い立ち、今回執筆してみました。

# はじめに

bot を作成するにあたって、 Slack のワークスペースの管理者である必要があるかも。
自分はオーナーであり、またプライベート用にも Slack のワークスペースを作成しているのでそちらで説明を進めます。

# アプリの管理を開く

ブラウザの場合、以下のように左上のワークスペース名をクリックして表示されるメニューをたどり、「アプリを管理する」をクリックします。
![メニュー01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/bfaa210a-d688-9257-5b41-2716fa250f32.png)
すると、以下のような画面が開きます。
![メニュー02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/9512ae1e-40b4-d8c4-0d48-c098131891f9.png)
新しく Slack bot を作成しますので、右上の「ビルド」をクリックします。
すると、英語のページが開きます。
![メニュー03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/5731823a-c977-15d4-55f0-b2ad60f332f6.png)
画面中央の「Start Building」をクリックしましょう。

# Slack App を作成

これがいわゆる bot になります。
新規作成画面が勝手に開くと思うので、適当な名前と作成先のワークスペースを指定し、「Create App」をクリックします。
![メニュー04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/33eb30d8-1663-846c-1468-4cab8a95d3f9.png)
すると、以下のような画面になります。
![メニュー05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/524be6df-dfa8-718e-24f9-79e0ea2e971b.png)
これでベースは作成できたわけですが、どのようにして bot を呼び出すかによって、やり方がいくつかあります。
今回は、メンション機能（`@` を付けてアカウント名を記載すると、そのアカウントに対して通知を送れる機能）を使用し、 bot にメンションを送ると起動するようにします。
画面左側の「Event Subscriptions」をクリックしましょう。
設定は以下のように「Enable Events」を On に変更し、「Sbuscribe to bot events」の項目で「Add Bot User Event」から「app_mention」を選択するだけです。
![メニュー06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/64e01838-e269-a7b1-89cc-95eba6c78aba.png)
ここで、「Save Changes」が選択できないことに気づきました。
ここで作成しようとしている bot は、あくまで Slack への投稿をトリガーにして外部 API を実行するだけのものです。
そのため、ここで AWS Lambda を使用し、実際に処理することを実装します。

# AWS Lambda での実装

AWS のアカウントがある前提で進めます。
アカウントをお持ちでない方は、以下から作成しましょう。
https://aws.amazon.com/jp/register-flow/

AWS のマネジメントコンソールから、 Lambda の管理画面を開くと以下のようになります。
![メニュー07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/c464ba58-a298-c8bd-68c8-03dc5805cbbd.png)
今回はテスト用に新しく作成するため、「関数の作成」をします。

## 関数の作成

Slack から http 経由で API を実行する必要があります。
そのため、以下のように「設計図の使用」を選択し、キーワードに「http」を入力してください。
API Gateway を使用するものが Node.js と Python の 2 つあります。
（microservice-http-endpoint となっているものです）
お好きな方を選択してください。
ここでは、 Python の方を選択して進めます。
![メニュー08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/b8c3890b-15c7-9ed6-9953-f027a7ee0f8f.png)

作成後、設定項目が複数あります。
キャプチャしづらいので、文字で説明します。

### 関数名

適当に入れます。
ここでは、「slack-bot-test」としました。

### 実行ロール

「既存のロールを使用する」にします。
既存のロールは、「lambda_basic_execution」を選択します。

### API Gateway トリガー

「新規 API の作成」にし、「REST API」を選択します。
HTTP API は昔はなかったと思います。
使い方をまだ把握できていないので、今回は従来からある REST API にしました。
セキュリティは、本当は誰でも実行できてしまうのでよろしくないのですが、「オープン」にしちゃいます。


ここまでやったら、「関数の作成」をクリックします。
これでベースが作成できました。

# 実装の修正

Lambda がたたかれた際に、どういうパラメータを受け取るのかを把握しないといけないため、コードを書き換えます。
以下のようにします。

```python
import json
import logging

LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.INFO)

def respond(err, res=None):
    ret = {
        'statusCode': '200',
        'body': json.dumps(res, ensure_ascii=False),
        'headers': {
            'Content-Type': 'application/json',
        },
    }
    LOGGER.info(f"Return: {ret}")
    return ret


def lambda_handler(event, context):
    LOGGER.info(f"Received event: {json.dumps(event)}")
    payload = {}
    if event.get('body'):
        payload = event['body']
    return respond(None, payload)
```

DynamoDB は使用しないため、削っています。
コードを修正したら右上の「保存」をクリックし、保存します。
![メニュー09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/bfb2a775-b8af-0d2b-003d-17dee4e1b6f5.png)

「保存」の隣にある「テスト」から、コードのテストを実行できます。
画面ではすでに test という名前のテストイベントを作成してしまっているのですが、名前を付けただけで初回の作成画面から何も変更していません。
テストイベントを作成し、「テスト」をもう一度クリックすると、成功のログが表示されるかと思います。

# API Gateway の設定

API Gateway では、今回は POST メソッドのみ受け付けるように変更してみます。
左側の「API Gateway」をクリックし、下に表示される「slack-bot-test-API」をクリックします。
![メニュー10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/86608e35-32bc-b2b4-1b16-944b3a3de565.png)
すると、 API Gateway の設定画面が開きます。
![メニュー11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/f913f136-c077-de31-1cc8-8c6c1663e175.png)
メソッドは POST だけでいいので、まずは ANY を削除します。
「アクション」から、「メソッドの削除」を実行し、 ANY を削除したら今度は「メソッドの作成」を実行し、 POST を作成していきます。
「Lambda プロキシ統合の使用」にチェックを入れ、「Lambda 関数」に今回作成した「slack-bot-test」を入力して保存します。
![メニュー12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/a5eda844-48c7-19c6-affe-19be753c6d5f.png)
次に、アクションから「API のデプロイ」を選択し、「デプロイされるステージ」に「default」を選択してデプロイします。
これで、 POST のみ受け付けるようになったと思います。

ここで Lambda の設定画面に戻ってみると、なぜか API Gateway の設定が 2 つになっています。
この例では、上のものが古いやつなので、削除して保存します。
![メニュー13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/476e0015-dba5-56c5-769b-51138dd70786.png)

# API Gateway → AWS Lambda の連携確認

API Gateway から Lambda が実行できるか確認します。
Windows10 ではいつの間にやらコマンドプロンプトで curl コマンドが一応使えるようになりました。
ただし、癖があるので以下のように実行します。

```bash:Windows
curl -XPOST {API Gateway の URL} -H"content-type:application/json" -d"{\"test\": \"test2\"}"
```

VM 上に起動した CentOS などから実行する場合は、以下のようにします。

```bash:Linux
curl -XPOST {API Gateway の URL} -H"content-type:application/json" -d'{"test": "test2"}'
```

ほぼ一緒ですが、渡すデータでエスケープが必要かどうかだけですね。
API Gateway の URL は、 AWS Lambda の画面上で「API エンドポイント」として記載のあるものです。
たとえばコマンドプロンプトで実行すると、以下のように応答があるはずです。

```bash
C:\Users\hoge>curl -XPOST {API Gateway の URL} -H"content-type:application/json" -d"{\"test\": \"test2\"}"
"{\"test\": \"test2\"}"
```

この時の Lambda 上のログを確認します。
上部にある「モニタリング」をクリックし、「CloudWatch のログを表示」をクリックしましょう。
![メニュー14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/85361b01-42f2-a39c-ee40-91b845e1c85d.png)
そうすると、ログが確認できる画面になります。
![メニュー15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/857be682-aa37-a9e2-89b5-b11d0db546ac.png)
ここでは、「最終のイベント時刻」の降順になるように並び替えてあります。
直近のログストリームをクリックすると、ロギングされた内容が確認できます。

これでまずは API Gateway + AWS Lambda の環境が準備できました。

# Slack bot → API Gateway への連携

ようやく戻ります。
入力できていなかった「Request URL」の欄に、 API Gateway のエンドポイントを入力します。
カーソルを外すとチェックが走り、「Verified」と表示されたら正しく連携可能ということです。
![メニュー16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/3272b6f1-bc3f-bcf9-7b61-9d818170f93b.png)
ここでようやく、「Save Changes」を実行します。

# Slack bot への権限の付与

Slack への投稿を bot が読み取れるよう、権限を付与します。
左側の「Basic Information」を選択し、「Install your app to your workspace」を展開し、「Install App to Workspace」クリックします。
![メニュー17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/894d6e54-394c-2c65-2f06-b1306284d928.png)
次の画面では、そのまま「許可する」をクリックして大丈夫です。
![メニュー18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/ab1e415d-bda7-f965-a89f-2e0035c9674d.png)

# Slack 上での連携確認

まずは、適当にチャンネルを追加します。
ここでは「#test」を追加しました。
![メニュー19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/66570c73-237e-cff0-20f0-4283b1674b24.png)
画面上の「アプリを追加する」から、「slack-bot-test」を追加します。
上記キャプチャではすでに追加しています。
（どうやらハイフンは使えないようで、ハイフンがカットされた形で登録されているようですね）

さて、実際にメンションしてみます。
以下のように、メンションで bot へ「testtesttest」という文字列を渡してみます。
![メニュー20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/c6b0af50-67eb-e0ef-1ef2-fc5094adcf21.png)
連携されたかどうかは、 CloudWatch ログを確認します。
アクセスがあれば新しくログストリームができているか、あるいは直近のログストリーム内に追記されています。
自分の場合は以下のように新しいログストリームに来ていました。
![メニュー21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/a1e1f9cf-b6eb-e9f5-96ae-e2115826fbff.png)

# body の中を確認する

Slack から API Gateway を介して AWS Lambda は実行できたわけですが、このままでは Slack への投稿ができません。
Slack への投稿には、 Slack api の chat.postMessage というものを使用します。
https://api.slack.com/methods/chat.postMessage
これを使う前に、まずは Slack からどういったデータが受け取れるのかを把握する必要があるため、ログからデータを取得します。

ロギングした中の「Received event:」の下に、 AWS Lambda が取得したデータが確認できます。
この中に「`"body"`」というキーがあり、ここに実際に受け取ったデータが入っています。
JSON 形式の文字列なので、整形してあげると以下のようになっています。

```json
{
  "token": "********",
  "team_id": "********",
  "api_app_id": "********",
  "event": {
    "client_msg_id": "********",
    "type": "app_mention",
    "text": "<@********> testtesttest",
    "user": "********",
    "ts": "1583634642.001000",
    "team": "********",
    "blocks": [
      {
        "type": "rich_text",
        "block_id": "********",
        "elements": [
          {
            "type": "rich_text_section",
            "elements": [
              {
                "type": "user",
                "user_id": "********"
              },
              {
                "type": "text",
                "text": " testtesttest"
              }
            ]
          }
        ]
      }
    ],
    "channel": "{どのチャンネルで実行されたか}",
    "event_ts": "1583634642.001000"
  },
  "type": "event_callback",
  "event_id": "********",
  "event_time": 1583634642,
  "authed_users": [
    "********"
  ]
}
```

ここで重要なのは“何を受け取ったか”と、“どのチャンネルから受け取ったか”なので、それ以外は伏字にしました。
つまり、ここでほしいのは JSON.event.text と JSON.event.channel になります。
分かりやすく、不要な項目を全カットした JSON データは以下の通りです。

```json
{
  "event": {
    "text": "<@********> testtesttest",
    "channel": "{どのチャンネルで実行されたか}"
  }
}
```

これでデータは分かったので、いよいよ Slack への投稿ができるように準備していきます。

# Slack の認証情報の登録

Slack api の chat.postMessage を使用するにあたって、ヘッダーに指定する認証情報とチャンネルに投稿するためのトークンが必要です。

ヘッダーに指定する認証情報は、 Slack api の画面左側から「OAuth & Permissions」を選択した際に表示される「Bot User OAuth Access Token」を使用します。
![メニュー22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/47bab845-7517-85b6-ca63-edc020b2f164.png)
投稿する際に使用するトークンは、左側の「Basic Information」を選択し、下の方にある「Verification Token」を使用します。
![メニュー23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/0fac5677-b38a-8d34-491b-a07d39b141e1.png)


# AWS Lambda 上の環境変数として登録

先ほどの認証情報は変更しないため、ソースコード上ではなく AWS Lambda の環境変数に登録し、ソース上から見えないようにしましょう。
AWS Lambda の画面で少し下の方に行くと、「環境変数」の欄があります。
![メニュー24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/6396e390-ff5b-4f5a-074c-9c09d6343a57.png)
ここでは、以下のように「OAuthToken」と「PostToken」として登録しました。
![メニュー25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/480d11ce-98dc-35a2-a619-30d82cd3bc1a.png)

# Lambda ソース修正

以下のように書き換えます。

```python
import json
import logging
import os

import requests

LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.INFO)


def respond(res=None):
    ret = {
        'statusCode': '200',
        'body': json.dumps(res, ensure_ascii=False),
        'headers': {
            'Content-Type': 'application/json',
        },
    }
    LOGGER.info(f"Return: {ret}")
    return ret


def post_slack(channel, message):
    headers = {
        'Content-Type': 'application/json; charset=UTF-8',
        "Authorization": f"Bearer {os.environ.get('OAuthToken')}",
    }
    url = "https://slack.com/api/chat.postMessage"
    payload = {
        'text': message,
        "token": os.environ.get('PostToken'),
        "channel": channel,
    }
    LOGGER.info(f"Header: {headers} Payload: {payload}")
    res = requests.post(
        url, data=json.dumps(payload).encode("utf-8"), headers=headers)
    LOGGER.info(f"status: {res.status_code} content: {res.content}")


def lambda_handler(event, context):
    LOGGER.info(f"Received event: {json.dumps(event)}")
    body = {}
    if event.get('body'):
        body = json.loads(event['body'])
    if event.get('headers', {}).get('X-Slack-Signature'):
        # Slack からのアクセス時のみ Slack へ投稿
        channel = body['event']['channel']
        # テキストは先頭のメンション部分をカット
        text_args = body['event']['text'].split(' ')
        message = ' '.join(text_args[1:])
        post_slack(channel, message)
    return respond(body)
```

ここでは、 Slack から渡された文字列をそのまま Slack へ投稿するだけです。
ここで、 requests というものを使用して Slack api へ post しています。
これはサードパーティのパッケージであるため、インストールが必要です。
AWS Lambda 上にインストールはできないため、手元で用意して zip でアップロードする必要があります。
Windows上に Python が入っていることが前提となりますが、以下のように実行してフォルダ指定でインストールします。

```bash
C:\Users\hoge>py -3.7 -m pip install requests -t d:\program_src\slack-bot-test\
```

Windows 上で Python を使うためには、過去に記事を書いていますので参考にしていただければと思います。
[Windows で複数バージョンの Python を使う](https://qiita.com/landwarrior/items/1b5e0f9af5316a025fe0)

自分の環境をベースに話を進めることになりますが、以下のフォルダに requests がインストールされました。
`d:\program_src\slack-bot-test\`
ソースをまとめてアップロードする必要がありますので、 AWS Lambda の画面上で編集していたソースを、 `lambda_function.py` という名前で同じく上記フォルダに入れます。
Atom 上で恐縮ですが、イメージは以下の通りです。
![メニュー26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/dda8bc85-6e1f-b6fd-1353-7583408d1650.png)

色々と警告が出てたりしますが、気にしないでください。

ソースの準備ができたら、フォルダの中身をまるっと zip にします。
ここで、フォルダを指定して zip にしてしまうと zip を解凍した時にフォルダができてしまうため、あくまでファイルを全選択して zip にする必要がありますので、注意してください。
自分は 7-zip を使用して zip を作成しました。

その後、 AWS Lambda にアップロードします。
画面上の「関数コード」の欄で、「コード エントリ タイプ」の部分で「.zip ファイルをアップロード」にすればアップロード可能です。
![メニュー27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/f823b603-b674-3e0f-04be-933928b53867.png)
アップロードして保存すれば、以下のようになるはずです。
![メニュー28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/17b23031-41b2-aa31-4cca-b31329049af6.png)
これで AWS Lambda 側の準備は完了です。

# Slack bot への追加の権限付与

権限の仕様が変わったようで、追加の作業が必要でしたので記載します。
Slack api の画面左側の「OAuth & Permissions」を選択し、下の方に「Scopes」というものがあると思います。
初期状態では、 read 権限しかないようです。
![メニュー29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/5cd7c8ff-9131-1f6d-2c9d-4382728b3cb0.png)
ここに、チャットへの書き込み権限を付与します。
「Add an OAuth Scope」をクリックし、「chat:write」を追加します。
![メニュー30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/782d5e4e-baf1-15ca-cda5-54a8e62e2b89.png)
ここで、アプリの再インストールを求められます。
キャプチャをとる前に実行してしまったので、上記には記載されていませんが画面上部に黄色の警告メッセージのようなものが表示されますので、「reinstall ～」のようなリンクがあったと思いますので、そちらをクリックして許可設定をしましょう。
ようやく、準備完了です。

# Slack → AWS Lambda → Slack の動作確認

早速、 Slack へ投稿してみます。
![メニュー31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/a218429a-9196-43ce-50e8-d81c955e6e91.png)
成功しました。

# 終わり

ここまでお疲れさまでした。
なるべく画像を多めにして分かりやすく解説したつもりですが、分かりにくかったらすみません。
AWS Lambda を使用するにあたって、 API Gateway の課金を抑えるためにメンション方式にしています。
Slack ではほかにもスラッシュコマンドというものもありますので、そちらでやっても面白いかもしれないですね。
Slack bot を試してみたいけどどこからやっていいのか分からない、という方に向けて執筆してみました。

なお、連携方法は色々あるようですが、ここで Slack への投稿に使用している chat.postMessage の利点を挙げるとすれば。

- 必ずしも Slack からの投稿をトリガーにして実行されるとは限らない

この点に限ると思います。
API Gateway を叩けば Slack からである必要はなく、また API Gateway ではなく CloudWatch Events を使用すれば定期的に Slack へ投稿することも可能です。
参考がてらキャプチャを載せると、以下のような感じです。
![メニュー32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179043/c4557f93-6533-b63d-5a53-ce262a9466ea.png)
API Gateway を介しての投稿と、 CloudWatch Events を介しての定期実行ができるようにトリガーを組んでいます。
当然ながら、実装でどちらから受けたのかを判断する必要はあります。

それでは、よい Slack bot 生活をお過ごしください！

# 2020/05/25 追記

自分の Slack bot のソースコードを github で公開することにしました。
今まではトークンなどをべた書きにしているところがあったので、そのあたりを見直したので大丈夫なはず・・・
https://github.com/landwarrior/slackbot2