LINEで肌状態管理できるチャットボットを作る

スキンケアにハマって肌の水分量と油分量を計測するようになったので
LINEのチャットボットで気軽に計測結果を記録できるチャットボットを作りました。

##使い方
チャットボットに向かってユーザーが「登録」とメッセージを送ります。
「登録」という言葉に反応して、チャットボットが水分量と油分量を聞いてくれるので
それに沿って回答していくと登録が完了します。
![IMG_1526.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1019704/4f4689a3-3e89-5068-3cbc-48fe92253acb.jpeg)


##システム構成
![スクリーンショット 2021-06-19 19.46.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1019704/e37c9d71-1022-1413-3549-2c34590e4895.png)

1. Line Messaging APIからAPI GatewayへPOSTリクエストを送信
2. API Gatewayをトリガにして、Lambda関数が実行
3. Lambda関数からDynamoDBへの書き込みがされる

##作成手順
1.　DynamoDBでデータベースとテーブルを作成する
2.　Lambda関数の設定
3.　API Gatewayの設定
4.　Line MessagingAPIの設定

##1.　DynamoDBでデータベースとテーブルを作成する
今までRDBしか使ったことがなかったのでテーブルとカラムの概念に苦戦しました。
（今でもあんまり理解していない）
以下で解説しますが、テーブルは2つ作成していて、
①skinCheckテーブル…水分量と油分量を保持するテーブル
②userStateテーブル…ユーザーごとの登録ステータスを保持しているテーブル
という使い分けをしています。

####テーブル①skinCheckテーブル
このテーブルでは日付>ユーザーごとに水分量と油分量を保持させています。
dateの中にskinInfoをMapで、その中にuseIdをMapで持っているので、
skinInfoの中に複数ユーザーが水分量と油分量を持つことができます。
![スクリーンショット 2021-06-19 20.42.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1019704/3ec2ae18-284f-b2a8-836d-672808c2f300.png)
####テーブル②userStateテーブル
このテーブルではユーザーごとの登録ステータスを保持しています。
登録ステータスとは何かというと、現在の登録状態をここでステータスとして管理しておくことで
ユーザーがPOSTした値が水分量なのか、油分量なのかを判断して登録できるようにしています。

このチャットボットの中では、以下のようにステータス更新をしています。
・水分量を質問する際にステータスをMに更新
・油分量を質問する際にステータスをOに更新
・油分量を回答した際にステータスをIに更新

![スクリーンショット 2021-06-19 20.43.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1019704/abee0a56-16ba-a44b-8ef0-78da679538d1.png)

##2.　Lambda関数の設定
LambdaからFunctionを立ち上げ関数を設定してあげます。
（Lambdaの作成方法、DynamoDBとの繋げ方は初心者の私でもよくわからないながらにググったらできたので、省略します。私は[こちら](https://business.ntt-east.co.jp/content/cloudsolution/column-try-20.html)の記事を参考にしました。）
lambda_functuin.pyを編集します。
テーブル名とLINEMessagingAPIのトークンだけ書き換える必要があります。

```
import json
from decimal import Decimal
import boto3
import urllib.request
from datetime import datetime,timezone,timedelta
from boto3.dynamodb.conditions import Attr
from boto3.dynamodb.conditions import Key

#変数定義
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table("①テーブル名")
stateTable = dynamodb.Table("②テーブル名")

JST = timezone(timedelta(hours=+9), 'JST')
sysdate = datetime.now(JST)
strSysdate = sysdate.strftime('%Y/%m/%d')

def decimal_default_proc(obj):
    if isinstance(obj, Decimal):
        return float(obj)
    raise TypeError

# テーブルスキャン
def operation_scan():
    scanData = table.scan()
    items=scanData['Items']
    print(items)
    return scanData

def lambda_handler(event, context):
    print("Received event: " + json.dumps(event))
    scan_data = operation_scan()
    
    print(event)
    
    req_events = json.loads(event['body'])['events']
    print(req_events)
    
    for message_event in req_events : 
        url = 'https://api.line.me/v2/bot/message/reply'
        headers = {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + 'LINEMessagingAPIのチャンネルアクセストークン'
        }
        #textパスを定義
        text = message_event['message']['text']
        res_text = "test"
        #メッセージからユーザーIDを取得して変数に代入 userID
        userId = message_event['source']['userId']        
        
        #同一日付のレコードがない場合には作成する
        checkDate = table.get_item(
            Key={
                'date':strSysdate
            }
        )
        print(checkDate)

        #メッセージに「登録」が含まれる場合
        if "登録" in text:
            if sysdate not in checkDate:
                print("レコード作るよ")
                table.put_item(
                Item = {
                    'date': strSysdate,
                    'skinInfo':{}
                }
            )
            else:
                print("もうレコードあったにゃん")

            #水分量を質問する
            res_text = "水分量を入力してください。"
            #ステータスをMに更新する
            stateTable.put_item(
                Item = {
                    'userId': message_event['source']['userId'],
                    'state' : 'M'
                }
            )
            #テーブルにレコードを追加（dateとuserIdのみ）
            put_table = table.update_item(
                Key = {'date':strSysdate},
                UpdateExpression  = 'set skinInfo.'+userId+' = :userSkinInfo',
                ExpressionAttributeValues = {':userSkinInfo': {'moistureLevel':0,'oilLevel':0}},
                ReturnValues = 'NONE'
            )
        
            body = {
                'replyToken': json.loads(event['body'])['events'][0]['replyToken'],
                'messages': [
                    {
                        "type": "text",
                        "text": res_text
                    }
                ]
            }
            req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), method='POST', headers=headers)
            urllib.request.urlopen(req)
            
        #メッセージに「登録」がなかった場合   
        else :
            state = stateTable.get_item(
                Key={
                    'userId':message_event['source']['userId']
                }
                )
            #ステータスがMの場合
            if state['Item']['state']=="M":
                # print("Mだわね。")
                body = {
                'replyToken': json.loads(event['body'])['events'][0]['replyToken'],
                'messages': [
                    {
                        "type": "text",
                        "text": "油分量を入力してください。"
                    }
                ]
                }
                #ステータスをOに更新する
                stateTable.put_item(
                    Item = {
                        'userId': message_event['source']['userId'],
                        'state' : 'O'
                    }
                )
                #水分量を更新
                moistureLevel = message_event['message']['text']
                put_table = table.update_item(
                    Key = {'date':strSysdate},
                    UpdateExpression  = 'set skinInfo.'+userId+'.moistureLevel = :moistureLevel',
                    ExpressionAttributeValues = {':moistureLevel': moistureLevel},
                    ReturnValues = 'NONE'
                )
            #ステータスがMでなかった場合
            else :
                state = stateTable.get_item(
                    Key={
                        'userId':message_event['source']['userId']
                    }
                )
                #ステータスがOの場合
                if state['Item']['state']=="O":
                    print("Oだわね。")
                    body = {
                    'replyToken': json.loads(event['body'])['events'][0]['replyToken'],
                    'messages': [
                        {
                            "type": "text",
                            "text": "登録が完了しました。"
                        }
                    ]
                    }
                    #ステータスをIに更新する
                    stateTable.put_item(
                        Item = {
                            'userId': message_event['source']['userId'],
                            'state' : 'I'
                        }
                    )
                    #油分量を更新
                    oilLevel = message_event['message']['text']
                    put_table = table.update_item(
                        Key = {'date':strSysdate},
                        UpdateExpression  = 'set skinInfo.'+userId+'.oilLevel = :oilLevel',
                        ExpressionAttributeValues = {':oilLevel': oilLevel},
                        ReturnValues = 'NONE'
                    )

            req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), method='POST', headers=headers)
            urllib.request.urlopen(req)
    return {
        'statusCode': 200,
        'body': json.dumps(scan_data,default=decimal_default_proc)
    }
```
##3.　API Gatewayの設定
ここも[こちら](https://business.ntt-east.co.jp/content/cloudsolution/column-try-20.html)を参考にしています。

##4.　Line MessagingAPIの設定
[LINE developers](https://developers.line.biz/ja/)にログインし、新規プロバイダーを作成する。
アカウントがない場合、新規でアカウント作成する。アカウント作成する際は個人で普段使っているアカウントから作成しても問題ないです。

###・新規チャネルの作成
![スクリーンショット 2021-06-19 23.03.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1019704/6fe97e6c-5adc-6fd5-c847-1edf3dba9730.png)
新規でプロバイダを作成します。チャネル設定ではMessagingAPIを選択してあげましょう。
その後チャネル基本設定を入力しますが、基本的にはテキトーで問題ないです。
特段気にしてあげるところはないですが、今回はアプリタイプはボットにしています。
また、チャネル名＝チャットボットの名前になりますので愛着の湧く名前をつけてあげましょう。

###・Webhookの登録
APIをデプロイした際の呼び出しURLをWebhookに登録します。
Webhookの利用ラジオボタンもOKとしてあげてください。

Webhookを追加したら「検証」ボタンを押して、うまくいってるか確認しましょう。

![スクリーンショット 2021-06-19 23.22.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1019704/d291277c-a760-fd84-6e17-19f3ada7be29.png)

チャンネル基本設定→応答設定で友達追加時の応答方法や、自動応答の文章の設定、
Webhookを有効にするか等の設定ができるので、ここも確認しておくと良いです。
![スクリーンショット 2021-06-19 23.26.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1019704/9fb3bea2-5821-c01a-846f-62cc37e9bdb0.png)
![スクリーンショット 2021-06-19 23.30.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1019704/7dc44c2e-1036-a54a-32bd-0a1132734f41.png)


###・動作テスト
作成したチャットボットをLINEのお友達に追加して、検証してあげましょう。
今回の場合は、

1.【以下のように会話が止まらずに成立すること】

　「登録」→「水分量を入力してください。」
　 →「（ユーザーが入力）」→「油分量を入力してください。」
　 →「（ユーザーが入力）」→「登録が完了しました。」

2.【DynamoDBの①skinCheck②userStateそれぞれのテーブルが想定通り更新されていること】

　①skinCheckerテーブルに当日日付でユーザーID、水分量、油分量が登録されていて、
　②userStateテーブルがM→O→Iと変化している

を確認できたら十分ではないかと思います。

##終わりに
こんな感じでLINEのチャットボットで肌状態を管理できるようになりました。

肌状態に限らず、何かを管理したいけどいちいち入力めんどくさいな
みたいなものがあればチャットボット化できたら楽かもと思いました。

あと、後から調べましたが今回のシステム構成で個人利用チャットボットであればAWS費用もかなりお安いようです。
このチャットボットもこれから値の取得等できるよう追加開発したいです。