API GatewayとLambda(Python)でLINE BOT(Messaging API)開発 [後編]

# API GatewayとLambda(Python)でLINE BOT(Messaging API)開発 [後編]

## はじめに
[前編](https://qiita.com/w2or3w/items/1b80bfbae59fe19e2015)では主に環境構築と、「こんにちは！」と返すだけの簡単なサンプルを載せました。
後編となる本記事では、line-bot-sdkを利用して以下のことを実装してみたいと思います。
・文字列の応答
・ユーザー情報の取得
・選択肢の提示、選ばれたものの取得
・位置情報の受信、位置情報の送信

## line-bot-sdkのインストール
Lambdaでデプロイする前提ですので、lambda_function.pyと同じ場所にインストールする必要があります。
インストールしたライブラリ類をまとめてzip圧縮できるよう、適当なフォルダ(ここではsource)の中で行いましょう。

```
|- source
   |- lambda_function.py
```

ライブラリはpipでインストールします。

```bash
$ cd source
$ pip install line-bot-sdk -t .
```

## Lambdaへのデプロイ
sourceフォルダの中身をzip圧縮して、そのzipファイルをlambdaへデプロイします。

```bash
$ cd source
$ zip -r ../lambda-package.zip *
$ aws lambda update-function-code --function-name sample-map-function-scraping --zip-file fileb://../lambda-package.zip
```
(※ sample-map-function-scrapingは目的のLambda関数名に変えてください。)

Lambdaコンソールの関数コードを確認すると、ライブラリも含まれた状態でデプロイされていることが確認できますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/a0a77889-94af-6a32-e7f2-bb9f1cb411ab.png)

### 環境変数にチャンネルシークレットを設定する
前編では使いませんでしたが、LINE Developpersで作成したチャンネルの「チャンネル基本設定」にある**チャネルシークレット** を使うことになります。環境変数に`LINE_CHANNEL_SECRET`というキーの値として登録しておいてください。

## 文字列を応答する
前編では line-bot-sdk を用いず「こんにちは！」を応答しました。
line-bot-sk を利用した場合にどうなるか、比較のために書いてみます。


```lambda_function.py
import os
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage
)
LINE_CHANNEL_ACCESS_TOKEN   = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
LINE_CHANNEL_SECRET         = os.environ['LINE_CHANNEL_SECRET']
LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
LINE_HANDLER = WebhookHandler(LINE_CHANNEL_SECRET)

def lambda_handler(event, context):
    logger.info(event)
    signature = event["headers"]["x-line-signature"]
    body = event["body"]
    
    @LINE_HANDLER.add(MessageEvent, message=TextMessage)
    def on_message(line_event):
        LINE_BOT_API.reply_message(line_event.reply_token, TextSendMessage("こんにちは！"))
    
    LINE_HANDLER.handle(body, signature)
    return 0
```
少しすっきりした感じがしますね。
ライブラリを利用した書き方に慣れていないと(知っていないと) 難しく感じるかもしれませんが、使っていくうちに慣れると思います。

## ユーザー情報
LINEのメッセージから取得可能なユーザー情報はどんなものがあるのでしょうか。
それを確認してみたいと思います。

```py
    @LINE_HANDLER.add(MessageEvent, message=TextMessage)
    def on_message(line_event):
        profile = LINE_BOT_API.get_profile(line_event.source.user_id)
        logger.info(profile)
        LINE_BOT_API.reply_message(line_event.reply_token, TextSendMessage("こんにちは！"))
```

profileの内容をログ出力して確認してみると、以下のようなJSONとなっています。

```json
{
    "displayName": "あなたのLINE名",
    "language": "ja",
    "pictureUrl": "https://profile.line-scdn.net/ch/v2/p/uxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/0000000000000",
    "userId": "U00000xxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
```

LINEに登録している名前、プロフィール画像のURL、ユーザーIDなどを取得することができました。

## 選択肢
選択肢を提示したり、選ばれたものを取得するための実装サンプルです。
・「選択肢」というメッセージを受け取ったら、いくつかの選択肢を応答する。
・選ばれたら「{選ばれたタイプ} + を 選択しましたね！」と応答する

```lambda_function.py
import os
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage,
    TemplateSendMessage, ButtonsTemplate,
    PostbackEvent
)
LINE_CHANNEL_ACCESS_TOKEN   = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
LINE_CHANNEL_SECRET         = os.environ['LINE_CHANNEL_SECRET']
LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
LINE_HANDLER = WebhookHandler(LINE_CHANNEL_SECRET)

def lambda_handler(event, context):
    logger.info(event)
    signature = event["headers"]["x-line-signature"]
    body = event["body"]

    # テキストメッセージを受け取った時に呼ばれるイベント
    @LINE_HANDLER.add(MessageEvent, message=TextMessage)
    def on_message(line_event):
        # ユーザー情報を取得する
        profile = LINE_BOT_API.get_profile(line_event.source.user_id)
        logger.info(profile)

        message = line_event.message.text.lower()
        if message == '選択肢':
            LINE_BOT_API.reply_message(line_event.reply_token, make_select_message())
        else:
            LINE_BOT_API.reply_message(line_event.reply_token, TextSendMessage("こんにちは！"))

    # 選択肢から選ばれた時(postback)に呼ばれるイベント    
    @LINE_HANDLER.add(PostbackEvent)
    def on_postback(line_event):
        data = line_event.postback.data
        LINE_BOT_API.reply_message(line_event.reply_token, TextSendMessage("{0}を選択しましたね！".format(data)))
    
    LINE_HANDLER.handle(body, signature)
    return 0

def make_select_message():
    return TemplateSendMessage(
        alt_text="選択肢",
        template=ButtonsTemplate(
            title="選択肢のテスト",
            text="下から1つ選んでね！",
            actions=[
                {
                    "type": "postback",
                    "data": "morning",
                    "label": "朝"
                },
                {
                    "type": "postback",
                    "data": "noon",
                    "label": "昼"
                },
                {
                    "type": "postback",
                    "data": "night",
                    "label": "夜"
                }
            ]
        )
    )
```
動作確認した結果は以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/955b1064-1d4a-aea4-d332-a6b7ef843ac6.png)


## 位置情報
位置情報を受け取ったり、応答するための実装です。
・「現在位置」を受け取り「その場所の緯度経度は ({緯度}, {経度}) ですね！」と応答する。
・「佐鳴湖」というメッセージを受け取ったら、佐鳴湖の場所を応答する。

```lambda_function.py
import os
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage,
    TemplateSendMessage, ButtonsTemplate,
    PostbackEvent,
    LocationMessage, LocationSendMessage
)
LINE_CHANNEL_ACCESS_TOKEN   = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
LINE_CHANNEL_SECRET         = os.environ['LINE_CHANNEL_SECRET']
LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
LINE_HANDLER = WebhookHandler(LINE_CHANNEL_SECRET)

def lambda_handler(event, context):
    logger.info(event)
    signature = event["headers"]["x-line-signature"]
    body = event["body"]
    
    # テキストメッセージを受け取った時に呼ばれるイベント
    @LINE_HANDLER.add(MessageEvent, message=TextMessage)
    def on_message(line_event):
        # ユーザー情報を取得する
        profile = LINE_BOT_API.get_profile(line_event.source.user_id)
        logger.info(profile)

        message = line_event.message.text.lower()
        if message == '選択肢':
            LINE_BOT_API.reply_message(line_event.reply_token, make_select_message())
        elif message == '佐鳴湖':
            LINE_BOT_API.reply_message(line_event.reply_token, make_sanaruko_location_message())
        else:
            LINE_BOT_API.reply_message(line_event.reply_token, TextSendMessage("こんにちは！"))

    # 選択肢から選ばれた時(postback)に呼ばれるイベント    
    @LINE_HANDLER.add(PostbackEvent)
    def on_postback(line_event):
        data = line_event.postback.data
        LINE_BOT_API.reply_message(line_event.reply_token, TextSendMessage("{0}を選択しましたね！".format(data)))
        
    # 位置情報を受け取った時に呼ばれるイベント
    @LINE_HANDLER.add(MessageEvent, message=LocationMessage)
    def on_location(line_event):
        latlon = "({0}, {1})".format(line_event.message.latitude, line_event.message.longitude)
        LINE_BOT_API.reply_message(line_event.reply_token, TextSendMessage("その場所の緯度経度は {0} ですね！".format(latlon)))

    LINE_HANDLER.handle(body, signature)
    return 0

def make_select_message():
    return TemplateSendMessage(
        alt_text="選択肢",
        template=ButtonsTemplate(
            title="選択肢のテスト",
            text="下から1つ選んでね！",
            actions=[
                {
                    "type": "postback",
                    "data": "morning",
                    "label": "朝"
                },
                {
                    "type": "postback",
                    "data": "noon",
                    "label": "昼"
                },
                {
                    "type": "postback",
                    "data": "night",
                    "label": "夜"
                }
            ]
        )
    )
    
def make_sanaruko_location_message():
    title = '佐鳴湖'
    address = '〒432-8002 静岡県浜松市中区富塚町５１９５'
    lat = 34.707433242045255
    lng = 137.68702025092614
    return LocationSendMessage(title=title, address=address, latitude=lat, longitude=lng)
```
「位置情報」は、LINEのメッセージ入力欄の横にカメラや画像のアイコンがありますが、その横の「＋」アイコンを選択すると表示されるメニューの中にあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/cadabfe7-5681-58e2-f302-492416826d0c.png)

動作確認した結果は以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/f4bfdbb6-a8d3-f6a1-2a5b-004224e475c4.png)

## あとがき
本当は、画像を受信して画像処理して返す、みたいなものも載せようと思ってたのですが辞めました。
バイナリデータのまま返せるかと思っていたらそうではなく、どこかに公開してそのURLを返してあげる必要があるようで、ちょっと長くなりそうだなと思ったためです。
この記事に載せるのは辞めましたが、いつかこれをテーマにした記事を書こうと思います。

1年前に、顔や文字を検出してモザイクをかけるサーバーレスWebアプリ「[モザイク](https://mosaic.w2or3w.com/)」を作りましたが、これのLINE BOT版を作ってみようかな～。って思ってます。