Slack で自動返信するサーバレスBOTを作りました

# 💡作ったもの
Slack の特定の Reaction に反応し、Reactionで反応し返してくれるサーバレスBOTです。

[![](https://user-images.githubusercontent.com/1152469/35902255-db0707e4-0c1d-11e8-882e-ca90d1e7a933.gif)](https://user-images.githubusercontent.com/1152469/35902255-db0707e4-0c1d-11e8-882e-ca90d1e7a933.gif)

　Slack に登録されたものから適当にキーワードを拾って、定型文を返信するだけのBOTです。APIを叩き叩かれるだけなので Lambda でやります。slackbot ライブラリや Hubot みたいな軟弱なものは使いません。

この処理を使いまわして、Slack BOT でなんかやる系のネタ記事をいくか作る予定です。

# :octocat: (追記) 成果物
Serverless Framework 設定と共に公開しました。構築手順はこちらのほうが簡単です。
[saitota/SlackServerlessReplyBot: It is a serverless BOT which receives Slack's Public Channel message and reply if there includes a specific keyword.](https://github.com/saitota/SlackServerlessReplyBot)

## ✋手順
### 必要なもの
- Slack アカウント
- AWS アカウント

### Slack/BOT作成
BOTから作っていきます。

- アプリケーション作成
    - https://api.slack.com/slack-apps
    - CreateApplicaionでアプリケーション作成

- Bot User
    - Display Name：適当に
    - Default Username：適当に


トークン発行とアクセス許可をします

- Permissions
    - OAuth & Permissions
        - OAuth Access Token：'xoxp-000000000000-000000000000-000000000000-0x0x0x0x0x0x0x0x0x0x0x0x0x0x0x0x'
        - Bot User OAuth Access Token：'xoxb-000000000000-0x0x0x0x0x0x0x'
    - OAuth & Permissions
        - Scopes
            - channels:history を追加
            - 参考：[Oauth Scopes | Slack](https://api.slack.com/docs/oauth-scopes)

### Lambda/API Gateway
前提として、チャレンジパラメータといういい感じの認証コードを返却するような Lambda Functrionを作成する必要があります。 
参考：[url_verification event | Slack](https://api.slack.com/events/url_verification)


```json:url_verification
{
    "token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
    "challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
    "type": "url_verification"
}
```

- API-Gateway
    - API名：CatchApi
    - メソッド：POST
    - 統合タイプ：Lambda関数
    - リージョン：us-east-1
    - Lambda プロキシ統合の使用：True
    - Lambda関数：CatchKeyword
    - APIのデプロイ、からURL発行：https://0x0x0x0x0x.execute-api.us-east-1.amazonaws.com/Prod
- Lambda
    - 名前：CatchKeyword
    - ランタイム：Python3.6
    - ロール：lambda_basic_execution
    - 環境変数
        - OAUTH_TOKEN: `xoxp-000000000000-000000000000-000000000000-0x0x0x0x0x0x0x0x0x0x0x0x0x0x0x0x`
        - BOT_TOKEN: `xoxb-000000000000-0x0x0x0x0x0x0x0x0x0x0x0x`
        - HOOK_KEYWORD: `poop`
        - REPLY_WORD: `:poop: :poop: :poop:`
        - BOT_NAME: `poopman`
    - トリガ：API-Gateway

Lambdaの標準ライブラリだけでいけるようにしました。
参考：[chat.postMessage method | Slack](https://api.slack.com/methods/chat.postMessage)

```main.py
import json
import logging
import urllib.request
import os

print('Loading function... ')
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handler(event, context):
    #getenv
    OAUTH_TOKEN = os.environ['OAUTH_TOKEN']
    BOT_TOKEN = os.environ['BOT_TOKEN']
    HOOK_KEYWORD = os.environ['HOOK_KEYWORD']
    REPLY_WORD = os.environ['REPLY_WORD']
    BOT_NAME = os.environ['BOT_NAME']

    #受信したjsonをLogsに出力
    logging.info(json.dumps(event))

    print (type(event))
    # json処理
    if 'body' in event:
        body = json.loads(event.get('body'))
    elif 'token' in event:
        body = event
    else:
        logger.error('unexpected event format')
        return {'statusCode': 500, 'body': 'error:unexpected event format'}

    #url_verificationイベントに返す
    if 'challenge' in body:
        challenge = body.get('challenge')
        logging.info('return challenge key %s:', challenge)
        return {
            'isBase64Encoded': 'true',
            'statusCode': 200,
            'headers': {},
            'body': challenge
        }
    #SlackMessageに特定のキーワードが入っていたときの処理（poopに反応して処理します）
    if body.get('event').get('text') == HOOK_KEYWORD:
        logger.info('hit!: %s', HOOK_KEYWORD)
        url = 'https://slack.com/api/chat.postMessage'
        headers = {
            'Content-Type': 'application/json; charset=UTF-8',
            'Authorization': 'Bearer {0}'.format(BOT_TOKEN)
        }
        data = {
            'token': OAUTH_TOKEN,
            'channel': body.get('event').get('channel'),
            'text': REPLY_WORD,
            'username': BOT_NAME
        }
        # POST処理　
        req = urllib.request.Request(url, data=json.dumps(data).encode('utf-8'), method='POST', headers=headers)
        res = urllib.request.urlopen(req)
        logger.info('post result: %s', res.msg)
        return {'statusCode': 200, 'body': 'ok'}
    return {'statusCode': 200, 'body': 'quit'}
```


### Slack/BOTの設定を追加

Event Subscriptions で呼び出すトリガを設定します。すべてのPublicチャネルの Message を受信します。

- Event Subscriptions
    - リクエストURL
        - https://0x0x0x0x0x.execute-api.us-east-1.amazonaws.com/Prod
    - Subscribe to Workspace Events
        - message.channels を追加

　以上で動くことを確認できました。

## ハマった所
　何度もSlackメッセージが届く事象が起き（数分間隔を置いて、合計4回届く）、Lambda側で処理が悪くてリトライされているのかと思ったけどログを見ると何度も API リクエストが届いているという事象がありました。2度め以降のリクエストを見たところ `headers` が`"X-Slack-Retry-Num": "1", "X-Slack-Retry-Reason": "http_error"` となっており、HTTP レスポンスで `"statusCode": 2xx` を返さないとリトライされてしまう
 Slack 側のリトライ仕様でした。~~API-Gateway できちんと 200 を返すよう作り直し、解消しました。~~
　Lambda は `return {'statusCode': 200, 'body': 'hogehoge'}` で返さないと、`502 Bad Gateway` を返してしまうので、きちんと HTTP リターンコードを設定して return する必要がありました。

# 📝あとがき
　Slack の Incoming Webhook で同じ実装をする場合、API-Gateway と連携させようとするとマッピングテンプレート作ったりで非常に面倒だったのですが、今回 Event Subscribe をつかったら楽ちんでした。なお、今回はすべてのパブリックチャンネルのメッセージを拾っているので、Lambda 料金がアレかもしれません。今回特に書いていませんが、Cloud9 がテストやデプロイでなかなか便利だったので使いこなしていきたいです。
　公開用に Serverless Framework も使ってみましたが簡単にAPI Gatewayやアプリケーションがデプロイで来て定義も正しくできるので、とてもいい感じでした。

# 📘参考にしました
[AWS Lambda + API Gateway で Slack Bot を動かす - Speee DEVELOPER BLOG](http://tech.speee.jp/entry/2018/01/23/105308)
[オウム返し slack bot をぱっとつくる - Qiita](https://qiita.com/takose/items/bc2b63426b9921604599)
[Slack Event APIを使ってサーバレスなSlackボットを作る - Qiita](https://qiita.com/omd/items/fcbbdb2bcce3edf0d3f5)