【Slack】Events APIとAWSでトリガー処理してみた

この記事は[ニフティグループ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nifty)の12/22分です。

# はじめに
Slackでユーザーの何かしらの操作をトリガーに、何かの処理が可能なEvents APIを触ってみましたのでそのメモをまとめておきます。



# Slack Events APIとは？
Slackで何かしらのアクションが発生したときに、事前に設定したエンドポイントに対して、通知をしてくれるAPI
[Slack API公式](https://api.slack.com/apis/connections/events-api)


例…
- メンバーがワークスペースに参加したとき
- チャンネルが作られたとき
- リアクションが付けられたとき
など

今回は、サンプルとしてメンバーがワークスペースに参加したときに、メッセージを#generalに投稿させてみる。
（処理部分をカスタマイズすればいろいろ応用が効きます）


# 構成図
![構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2326327/522203cc-558b-1cac-58e7-a5fa32b1bf15.png)

① Slack Events APIからメンバー参加時にデータが送られてくる
② 送られてきたデータをAPI Gatewayが受け取り、それをLambdaにわたす（以降、`Lambda A`）
③ 受け取ったデータを必要な部分だけ抽出してSQSに渡す
④ Slack Event APIに`200 OK`を返す
⑤ SQSからLambdaにデータが渡される（以降、`Lambda B`）
⑥ Slack APIにメッセージを送信する


### なんでSQSを挟むの？？

> __Your app should respond to the event request with an HTTP 2xx within three seconds.__ If it does not, we'll consider the event delivery attempt failed. After a failure, we'll retry three times, backing off exponentially.
> Maintain a response success rate of at least 5% of events per 60 minutes to prevent automatic disabling.
> Respond to events with a HTTP 200 OK as soon as you can. __Avoid actually processing and reacting to events within the same process.__ Implement a queue to handle inbound events after they are received.
> [Using the Slack Events API](https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events)


Slackからのリクエストに対して3秒以内に200番台で応答しないといけないから。
なので、受信してから処理を行いレスポンスを返しているとタイムアウトで再送され、2重で処理をしてしまう可能性あり
（一応、再送されたデータには`X-Slack-Retry-Num`というヘッダーが付与されるので判別できなくはないです…）

# 手順

## SQSでキューを作成する
Lambda同士をつなぐためのキューを作成する。
好きな名前をつけて、デフォルトで作成。
（ARNを後で利用するのでメモしておく）
![スクリーンショット 2021-12-07 19.41.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2326327/d4417483-ba38-f6a9-4210-b6f9e0eb81c5.png)



## Lambda Aを作成する
API Gatewayで受け取ったデータをSQSに入れて、`200 OK`を返すだけのLambdaを作る。

関数名に好きな名前をつけて、ランタイムに`Python3.9`を選択する。

### ソースコード
```lambda_function.py
import os
import boto3
import json

def lambda_handler(event, lambda_context):
    request_body = json.loads(event['body'])

    # Slack API設定時のURL検証に応答する
    if request_body['type'] == 'url_verification':
        return {
            'statusCode': 200,
            'body': json.dumps(
                {
                    'challenge': request_body['challenge']
                }
            )
        }

    # Slack APIから来たイベントか検証する
    if request_body['token'] != os.environ.get('VERIFICATION_TOKEN'):
        return {
            'statusCode': 400,
            'body': json.dumps({
                'msg': 'inviled token'
            })
        }

    # ゲストとボットは除く
    if not (request_body['event']['user']['is_restricted'] or request_body['event']['user']['is_bot']):
        QUEUE_NAME = os.environ.get('QUEUE_NAME')
        sqs = boto3.resource('sqs')
        queue = sqs.get_queue_by_name(QueueName=QUEUE_NAME)

        # 参加メンバーのIDと名前をSQSに送る
        msg_dict = {
            'id': {
                'StringValue': request_body['event']['user']['id'],
                'DataType': 'String',
            },
            'name': {
                'StringValue': request_body['event']['user']['profile']['real_name'],
                'DataType': 'String',
            },
        }
        queue.send_message(MessageBody='NewMemberJoin!', MessageAttributes=msg_dict)

    # Slack APIに200 OKを返却
    return {
        'statusCode': 200,
        'body': json.dumps({
            'msg': 'ok'
        })
    }
```

## Lambda Aの実行ロールにSQSの書き込み権限を適用する
作成したLambda Aの「設定」 → 「アクセス権限」 → 「ロール名」をクリックで、このLambdaに紐付いているIAM roleにアクセス。
![スクリーンショット 2021-12-07 19.47.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2326327/692056af-1a18-54cd-02b2-d3ed522aec21.png)

IAMの画面から「インラインポリシーを追加」 → 「JSON」タブをクリックし、下記の内容をコピー。
（`Resource`の値には、先程作成したSQSのARNを記入）

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sqs:SendMessage",
                "sqs:GetQueueUrl"
            ],
            "Resource": "[さっき作成したSQSのARN]"
        }
    ]
}
```
記入したら、「ポリシーの確認」をクリック。
好きな名前をつけて「ポリシーの作成」をクリックすれば、Lambda AにSQSの書き込み権限が付与完了。

## API GatewayでエンドポイントURLを払い出す

API Gatewayのページから、`HTTP API`を「構築」。
![スクリーンショット 2021-12-07 19.55.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2326327/bf53e0b6-16f0-e7be-122f-7aae9d9ab5f4.png)

「統合」では「Lambda」を選択し、先程作成したLambda Aを選択。
これで、このエンドポイントにデータが来ると、Lambda Aに渡され、実行されるようになる。

問題なければ「確認して作成」 → 「作成」をクリック。
これでエンドポイントが作成完了。

作成したAPIのURLと…
![スクリーンショット 2021-12-07 19.59.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2326327/a1e36e63-962d-95b9-bd84-a9e82a35384c.png)

「ルート」にあるパスを結合したものが、エンドポイントURL
![スクリーンショット 2021-12-07 20.00.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2326327/3114f230-01a1-9f4c-c469-f836af116771.png)
（画像の場合だと、`https://*****.amazonaws.com/SlackEventsApiHandler`）

あとで利用するのでメモしておく。


## Slack APIの設定をする
[Slack API](https://api.slack.com)のページに行き、「Create an app」をクリック

今回は最近登場したmanifestを利用して作成してみる。
![スクリーンショット 2021-12-07 20.08.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2326327/620303d1-c3fe-6db7-4898-6787b7be06f2.png)

インストール先のワークスペースを選択したら、次のページで以下のYAMLを貼り付ける。
`request_url`には、先程作成したエンドポイントURLを、`name`、`description`、`desplay_name`には好きな値を入力する。
（ここで入力エラーが出たら、パラメーターに日本語ではなく、アルファベットのみを入力してみると解決する…）

```manifest.yml
 _metadata:
  major_version: 1
  minor_version: 1
display_information:
  name: [アプリの名前]
  description: [アプリの説明]
features:
  app_home:
    home_tab_enabled: true
  bot_user:
    display_name: [Slack上での表示名]
    always_online: true
oauth_config:
  scopes:
    bot:
      - incoming-webhook
      - users:read
settings:
  event_subscriptions:
    request_url: [先程払い出したエンドポイントURL]
    bot_events:
      - team_join
  org_deploy_enabled: false
  socket_mode_enabled: false
  is_hosted: false
```

エラーが出なければ`next`で確認をした上で作成。

Slack Appの画面に遷移したら、「Install to Workspace」をクリックし、ワークスペースにインストールしておく。
![スクリーンショット 2021-12-07 20.13.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2326327/1c1ca31d-1668-bce2-e495-7aa0435ef3c5.png)

インストール時にメッセージの投稿先が聞かれるので、メンバーが参加したときにメッセージを送信したいチャンネルを指定。
![スクリーンショット 2021-12-07 20.13.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2326327/63385089-4992-d6f0-1a94-846630e6acef.png)

その後、Incoming Webhookの画面からWebhook URLをメモ
![スクリーンショット 2021-12-07 20.19.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2326327/d72c2262-ec13-edc1-b8f3-91d9b37183f0.png)

加えて、Event Subscriptionsの画面で、先ほどをエンドポイントURLを入力し、`Verified`が表示されることを確認する。これで、Slackでイベントが発生したら、API Gateway経由でLambda Aが実行されるように。
![スクリーンショット 2021-12-07 20.20.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2326327/54036cba-42da-ef32-93ab-520454623b26.png)

（manifestの`request_url`が適用されなかったのは謎…）


## Lambda Aに環境変数を設定する
今設定したアプリの認証用のTokenをLambda Aの環境変数に設定

### 環境変数に設定するもの

| Key | Value |
|:-:|:-:|
| `VERIFICATION_TOKEN` | 先程払い出したSlack APIのToken  |
| `QUEUE_NAME` | 作成したSQSのキュー名  |


## Lambda Bを作成する
SQSからデータを受け取り、Slackにメッセージを送信する処理。
（ここの処理を変えれば、自由にイベントに対応する処理をカスタマイズできる）

ただし、このソースではLambdaで利用できない`requests`ライブラリが必要のため、一旦ローカルでソースとライブラリを用意して、zipで固めてアップロードする。
### ソースコード
```lambda_handler.py
import requests
import os

def lambda_handler(event, hambda_context):

    for record in event['Records']:
        msg_json = record['messageAttributes']
        name = msg_json['name']['stringValue']
        id = msg_json['id']['stringValue']
        
        requests.post(os.environ.get('WEBHOOK_URL'), json={
            'text': f'{name}さん(<@{id}>)がメンバーとして参加しました:+1:'
        })
````

`requests`をローカルに保存する

```shell
% pip install requests -t .
```


ディレクトリはこんな感じに。これをzipにして、Lambda Bにアップロード
![スクリーンショット 2021-12-16 22.21.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2326327/8daaf49f-ce80-64f4-0ecb-c51e0e4f9b23.png)


### 環境変数に設定する

| Key | Value |
|:-:|:-:|
| `WEBHOOK_URL` | 先程払い出したIncoming Webhook URL  |


### Lambda Bの実行ロールに下記のポリシーをインラインで追加

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sqs:ReceiveMessage",
                "sqs:GetQueueAttributes",
                "sqs:DeleteMessage"
            ],
            "Resource": "[SQSのARN]"
        }
    ]
}
```

### SQSをトリガーとして起動するように設定
![スクリーンショット 2021-12-16 22.32.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2326327/b1f90570-78e6-50f7-00ea-2f8e203594e9.png)




# 動作確認
メンバーが参加すると、メッセージが指定したチャンネルに飛んでくる
![スクリーンショット 2021-12-19 22.16.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2326327/f7bd7f15-5393-7369-fe48-f2e6f9d36b07.png)

# その他
Lambda AのSlack APIから来たデータか確認する部分は、実は新しい方法がある…