AWS ChaliceでLINE Botにメッセージのオウム返しと、任意のメッセージをプッシュさせてみる

# これはなに
AWS Chaliceで簡単なLINE Botを作る手順をまとめました。

最終的に構成図にするとこんな感じになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96507/a3a88c72-dd42-3245-c4b6-8ff3e8637ecd.png)

# LINE Botの作成
LINE Developersに登録し、[LINE Developersコンソール](https://developers.line.biz/console/)で作業します。

## プロバイダーの作成
アプリ（Bot）を提供するプロバイダーを作成します。組織名や自分の名前を付けるといいようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96507/037f1dac-8bef-5549-358a-9796fd535728.png)

## チャネルの作成
次に自分の作ったサービスとプロバイダーを関連づけるチャネルを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96507/1bfda25b-66ec-469a-b973-d46a732e9e78.png)

今回はMessaging APIをチャネルとして利用するのでこちらを選択します。

アイコン、チャネル名（Bot名）、チャネル説明など項目を設定して作成します。

チャネルが作成できたら、「チャネル基本設定」から「チャネルシークレット」。
「Messaging API設定」から「チャネルアクセストークン（長期）」をそれぞれ取得して控えておきます。

ここまでできたら次はチャネルと連携するサービス（API）を準備します。

# AWS Chalice
続いて[Amazon API Gateway](https://aws.amazon.com/jp/api-gateway/)と[AWS Lambda](https://aws.amazon.com/jp/lambda/)を使ったAPIを簡単に作れるPythonフレームワークの[AWS Chalice](https://aws.amazon.com/jp/builders-flash/202003/chalice-api/)を使ってLINE Botと連携するAPIサービスを作っていきます。

## Chaliceのインストール・プロジェクトの作成

```
>mkdir chalice
>cd chalice
>virtualenv venv
>venv\Scripts\activate
>pip3 install chalice
>chalice --version
chalice 1.22.0, python 3.8.6, windows 10

>chalice new-project my_line_bot
Your project has been generated in ./my_line_bot
>cd my_line_bot
```

## APIの作成

### インストールライブラリの指定

Lambdaデプロイ時にインストールするライブラリを記述します。
[LINE BotのPython SDK](https://github.com/line/line-bot-sdk-python)を使うのでこれを指定。

```requirements.txt
line-bot-sdk==1.18.0
```

### 環境変数の設定

先ほど控えた「チャネルシークレット」と「チャネルアクセストークン（長期）」がLambdaの環境変数として展開されるよう設定します。

```json:.chalice\config.json
{
  "version": "2.0",
  "app_name": "my_line_bot",
  "stages": {
    "dev": {
      "api_gateway_stage": "api",
      "environment_variables": {
        "LINE_CHANNEL_SECRET": "チャネルシークレット",
        "LINE_CHANNEL_ACCESS_TOKEN": "チャネルアクセストークン"
      }
    }
  }
}
```

### API実装

チャネルの呼び出しに応じて行う処理を記述します。

```python:app.py
import os
import json
import logging
import random

from chalice import Chalice
from chalice import BadRequestError, ForbiddenError

from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import FollowEvent, MessageEvent, TextMessage, TextSendMessage, StickerMessage, StickerSendMessage

app = Chalice(app_name='my_line_bot')

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# 各クライアントライブラリのインスタンス作成
handler = WebhookHandler(os.environ.get('LINE_CHANNEL_SECRET'))
linebot = LineBotApi(os.environ.get('LINE_CHANNEL_ACCESS_TOKEN'))

# Webhook
@app.route('/callback', methods=['POST'])
def callback():
    try:
        request = app.current_request
        logger.info(json.dumps(request.json_body))

        # リクエストヘッダーから署名検証のための値を取得
        signature = request.headers['x-Line-Signature']
        logger.info(signature)

        # リクエストボディを取得
        body = request.raw_body.decode('utf8')

        # 署名を検証、問題なければhandleに定義されている関数を呼び出す
        handler.handle(body, signature)

    except InvalidSignatureError as err:
        logger.exception(err)
        raise ForbiddenError('Invalid signature.')

    except Exception as err:
        logger.exception(err)
        raise BadRequestError('Invalid Request.')

    return {}

# 友達追加（一度ブロックし解除しても発火する）
@handler.add(FollowEvent)
def handle_follow(event):

    linebot.reply_message(
        event.reply_token,
        TextSendMessage(text='はじめまして！よろしくお願いします！')
    )

# メッセージイベント
@handler.add(MessageEvent, message=TextMessage)
def handle_text_message(event):

    # メッセージをオウム返しする
    linebot.reply_message(
        event.reply_token,
        TextSendMessage(text=event.message.text)
    )

# スタンプメッセージ
@handler.add(MessageEvent, message=StickerMessage)
def handle_sticker_message(event):

    # https://developers.line.biz/media/messaging-api/sticker_list.pdf
    sticker_ids = [
        '52002734','52002735','52002736','52002737','52002738','52002739','52002740','52002741','52002742','52002743',
    ]
    random.shuffle(sticker_ids)

    sticker_message = StickerSendMessage(
        package_id='11537',
        sticker_id=sticker_ids[0]
    )

    linebot.reply_message(
        event.reply_token,
        sticker_message
    )

# 通知
@app.route('/push', methods=['POST'])
def push():
    try:
        request = app.current_request
        logger.info(json.dumps(request.json_body))

        user_id = request.json_body['user_id']
        message = request.json_body['message']

        # メッセージを通知
        linebot.push_message(
            user_id,
            TextSendMessage(text=message)
        )

    except Exception as err:
        logger.exception(err)
        raise BadRequestError('Invalid Request.')

    return {}
```

Botと対話するユーザーからのアクションに応じて発火するWebhook用のAPI（`/callback`）と、ユーザーを指定してメッセージをプッシュするAPI（`/push`）の二つのパスを用意しています。

これで、APIは完成なのでデプロイします。

## デプロイ

AWS CLIで適切な権限のあるconfigureが設定されているのが前提です。
IAMポリシーはこの辺があたってれば大丈夫そう。
https://github.com/aws/chalice/issues/59#issuecomment-603760552

```
>cd my_line_bot
>chalice deploy --profile chalice
```

これでAWSにデプロイが行われAPI GatewayとLambdaが構成されました。
得られたエンドポイントをチャネルに設定します。

# LINE Botの設定
「Messaging API設定」の「Webhook URL」にエンドポイントを設定。
「検証」を押して「成功」と表示されればOKです。
また「Webhookの利用」をオンにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96507/b33bc9e7-ed9e-88b8-d768-1f0ef482cdb6.png)

LINE Official Account Managerから「あいさつメッセージ」と「応答メッセージ」をオフにしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96507/9ef5e91c-ee4e-75b5-62cb-e18a52bea1e0.png)

これで完成です！
「Messaging API設定」にあるQRコードから友達追加しましょう。

# 使ってみる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96507/63439522-1bf9-85c4-b3f2-513d4d931dee.png)
LINEユーザーからのアクションに応じて、FollowEvent、MessageEventのTextMessage、MessageEventのStickerMessageがそれぞれ発火しているのがわかります。

プッシュの方もやってみます。
CloudWatch Logsに先ほどまでのリクエストボディがログ出力されているので、そこからユーザーID（LINEユーザーを識別するために、プロバイダーごとに割り当てられたID）を拾って、プッシュAPIを呼び出します。

```
$ curl -X POST -H "Content-Type: application/json" \
    -d '{"user_id":"************************", "message":"何か話しましょう"}' \
    https://*********.execute-api.ap-northeast-1.amazonaws.com/api/push
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96507/655cec62-3d36-e30e-6c80-55e2cb02139f.png)

これでサービスから能動的にLINEユーザーに通知することもできました。

# 参考
https://developers.line.biz/ja/docs/messaging-api/getting-started/
https://developers.line.biz/ja/docs/messaging-api/sending-messages/#methods-of-sending-message
https://developers.line.biz/ja/reference/messaging-api/#send-reply-message
https://developers.line.biz/ja/reference/messaging-api/#sticker-message
https://qiita.com/GlobeFish/items/dbf05e0f43b93ff7a6df
https://qiita.com/t-kigi/items/21073df2bfc27e2b4999
https://keinumata.hatenablog.com/entry/2018/05/08/122348