LINEbot製作でやったこと

# 概要
この時期では送信したメッセージをオウム返しするだけの単純なLINEbotの製作手順を解説します。

# 作成の流れ
今回作成において参考にしたのは[こちらのサイト](https://dango-study.hatenablog.jp/entry/2023/03/18/144712)。

基本的にはこのサイトの手順に沿って進んでいきます。

# 製作環境
・Windows
・python3.9
・pip 23.3.2

# LINE Message APIを使うための環境を整える

まずは、LINE Developersに登録します。
その後、公式ドキュメントの記載に従って、チャネルを作成します。今回は、メッセージのやり取りをしたいので、Messaging APIチャネルを選択して、作成する。
作成が完了したら終了。ただ、後のことを考えて、"Messaging API設定"の一番下、"チャンネルアクセストークン"を押下し、メモ帳などにその値を控えておく。

![スクリーンショット 2024-02-07 110122.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/0a7a317c-e8c1-7c67-5de3-9940f403d1a6.png)

# AWS の環境を整える+LINEと連携する
### lambda関数の設定
AWSコンソールにログインして、Lambdaの画面を開き、関数->関数作成を押下する。
以下のような画面が出るので、関数名は適当に記入、ランタイムは"Python3.9"を設定する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/6b42cc3f-7706-4e4b-9b55-4f4ea5417696.png)

### タイムアウト設定
デフォルトは5秒に設定されていますが、API等 別のシステムとやり取りをする場合は、タイムアウトが起こり得るため、30秒程度まで伸ばした設定に変更する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/00dbf703-1cb7-4420-9b43-e76b13197b7a.png)

### AWS Lambdaレイヤー作成
今回は、LINE BOT開発用のline-bot-sdkをインストールして、レイヤーとして設定することで、モジュールを使用します。

まず、pipコマンドを使って必要モジュールファイルをダウンロードします。

以下コードでは、"python”フォルダを作成して、そのフォルダに必要モジュールファイルを格納、zip化する

```
mkdir python
$ cd C:\Users\user\python
$ pip install line-bot-sdk -t ./
$ cd ../
$ zip python.zip python/* #zip化する
```
ちなみに、zipファイルの構成は以下の通り
```
python.zip
　　-python
　　　　|-line-bot-sdk
```
ここでディレクトリ構成を間違えるとうまくいかない可能性がある。

その後は作成したzipファイルをレイヤに設定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/15c9fae8-cb9a-30f3-cf9c-fba7af313d48.png)

### 環境変数の登録
環境変数に特定パラメータ(今回はAPIキー)を設定してプログラムで利用する。

キーは任意に入力し、値にAPIキーを入力して、コード内で呼び出して(LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']という感じ)使う。
LINE Message APIのチャンネルアクセストークンはキー: LINE_CHANNEL_ACCESS_TOKENの値に入力して、今回は設定した。



![スクリーンショット 2024-02-07 111851.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/9eb4132f-f0f3-1cb6-9548-01372d203951.png)

### API Gatewayの作成/設定
Lambdaの関数画面で、以下画像では左下に"API Gateway"という文字とアイコンが存在するが、初期は存在しない。
"+トリガーを追加"個所をクリックして、ウィンドウを開く。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/dab34e98-24ec-0dd0-6681-e2570a509f48.png)

ウィンドウが開いたのち、"API Gateway"を選択すると以下画像のような設定画面が現れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/af7be11b-3742-b14e-f87a-4366df8b09b3.png)

API設定についてですが、これがベストというわけではないようなのですが、まだAPIの種類、具体的にHTTPとRESTでどう変わるのか理解していな部分が多いのでとりあえずはサイト通りの手順に従っておきます...(教えていただけると幸いです)

上記の通り作成すると、先程の関数ウィンドウのトリガー欄に、API Gatewayが作成される。

API Gatewayの名前をクリックして、詳細を設定する。
以下画像では、"Resources"欄に"/POST"という1つしか存在しないが、当初は"ANY"が存在している。

"ANY"メソッドは削除して"POST"メソッドを作成する。POSTメソッドは、HTTP POSTリクエストをサポートしており、HTTP POSTとは、クライアントからの入力内容をWebサーバに送信するために使用するメソッドのこと。

![スクリーンショット 2024-02-07 114111.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/726eee6b-dd2b-f30e-ec01-33a8abe3fb23.png)

POSTメソッド作成時の設定は以下の通り、"Lambda Function"の入力を求められるが、そこにはLambda 関数名を入力する。

![スクリーンショット 2024-02-07 115123.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/c49e4f08-da8a-7d88-63b4-b94c6e567ce0.png)

### リクエストヘッダの設定
作ろうとしているシステムでは、AWS API GatewayへのHTTP POSTリクエストはLINEから来ることを想定しているが、LINEから来たかどうかの検証をする方法として、署名の検証方法が推奨されているようなので搭載してみる。

やり方はPOSTメソッドのメソッドリクエストのタグを選択、編集をクリックして、
HTTPリクエストヘッダの欄にx-line-signatureと入力する。

![スクリーンショット 2024-02-07 115428.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/39334a28-b740-47d1-53e0-edf91ed1eb9c.png)

![スクリーンショット 2024-02-07 115624.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/8c18c382-178e-7eaa-4914-984d1808baa1.png)

### APIのデプロイ
"Resources"の横の"Actions"欄から、Deploy APIを選択することで、APIをデプロイ(設置)することができる。
これを忘れて、次工程がうまくいかず...
デプロイ時のStage名などは、任意に設定する。(ここではchatbotと設定した)

### webhookの設定
"Stages"選択し、デプロイ時のStage名を選択すると、以下のような画面が現れる。
Invoke URL(下の画像ではurlを呼び出すの所)がリクエストするURLになるので、ここにLINEの送信先が来るように設定する。

![スクリーンショット 2024-02-07 120004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/faa25e53-7cc4-ef25-ce63-3d57d4877b2a.png)

今回では、LINE上でメッセージを受けた際にそのメッセージと通知の連絡をAWS API Gatewayにリクエストする際に使う。以下の通り、LINE DevelopersのページでWebhook設定個所があるので、上記API GatewayのInvoke URLを入力し、"検証"ボタンを押下する。その結果、"成功"と記載されたウィンドウが出れば、問題なく設定できている。

ここで注意したいのは下の画像のようにルートリソースの下に直接POSTメソッドを生成しないと検証でエラーになることがある。

![スクリーンショット 2024-02-07 120335.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/65ae3cfa-61a3-3824-2193-154094c279df.png)



# AWS Lambdaでコード実行(動作確認)
### コード作成
AWS Lambdaでコード実行(動作確認)残る手順も少なく、無事最後までいけるかと思われたが、最も苦労したのがこの後の実際にlambdaでpythonコードを実行するところだった。

始めはそのままサイトに貼ってあったコードをそのままコピペし、実行。

```:python
import json
import urllib.request
import os
from linebot import LineBotApi
from linebot.models import TextSendMessage
from datetime import datetime 

#######APIキーを取得
# 環境変数からLINE Botのチャネルアクセストークンを取得
LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
# チャネルアクセストークンを使用して、LINE BotのAPIインスタンスを作成
LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)

########説明
#LINEで入力されたテキストは ['events’][0]['message’]['text’] に入っている
#受信したメッセージに対してリプライを行うには、replyTokenの値を使用する

# ログ出力関数
def logging(errorLv, lambdaName, errorMsg):
    loggingDateStr=(datetime.now()).strftime('%Y/%m/%d %H:%M:%S')
    print(loggingDateStr + " " + lambdaName + " [" + errorLv + "] " + errorMsg)
    return

def lambda_handler(event, context):
    logging("info", context.function_name, "実行開始")
    logging("error", context.function_name, "エラーログテスト")
    if json.loads(event["body"])["events"][0]["type"] == "message":
        if json.loads(event["body"])["events"][0]["message"]["type"] == "text":
            replyToken = json.loads(event["body"])["events"][0]['replyToken']# リプライ用トークン
            messageText = json.loads(event["body"])["events"][0]["message"]["text"]# 受信メッセージ
            LINE_BOT_API.reply_message(replyToken, TextSendMessage(text=messageText))# メッセージを返信（受信メッセージをそのまま返す）

    return {'statusCode': 200, 'body': json.dumps('Reply ended normally.')}
```

しかし、なにやらエラーが発生してしまったようだ。

![スクリーンショット 2024-02-06 181505.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/8d293891-667b-0b41-f685-80972a01314a.png)

実際に発生したエラーはこんな感じ。
どうやらjsonのデコードエラーのようだ。
ということでいろいろ検索したところ、event["body"]が有効なJSON文字列ではない可能性があるということだったので、他のコードも参考にして何とか修正。

```:python
import json
import logging

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

def lambda_handler(event, context):
    try:
        body = event.get("body", "")
        # デバッグログの追加
        logger.info(f"Received body: {body}")

        if body:
            parsed_body = json.loads(body)
            if parsed_body["events"][0]["type"] == "message":
                # ここで必要な処理を行います
                pass
            else:
                return {"statusCode": 400, "body": "Invalid message type"}
        else:
            return {"statusCode": 400, "body": "Empty body"}

    except json.JSONDecodeError as e:
        logger.error(f"JSON decoding error: {str(e)}")
        return {"statusCode": 400, "body": "Invalid JSON format"}

    except Exception as e:
        logger.error(f"Unexpected error: {str(e)}")
        return {"statusCode": 500, "body": "Internal server error"}

    return {"statusCode": 200, "body": "Success"}
```
json.load()を行う前に、event["body"]がから出ないか、有効なjson文字列かどうかを例外処理でチェックする処理を追加した。

ということで再度実行。

おや、またエラーが発生してしまった...

![スクリーンショット 2024-02-06 182222.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/be3edc29-8b9a-25cb-1a0d-02d1ca380c43.png)

今度はインデックス指定についてこんなエラーが。
原因がわからないので、コードを途中まで実行することを繰り返してみたところ、
body.get("events", [{}])[0]という部分がどうやらエラーの原因になっているようだ。

0指定でこのエラーが発生しているということは、eventsリストが空の可能性があるので再び調べながら修正。

```:python
import json
import os
from linebot import LineBotApi
from linebot.models import TextSendMessage
from datetime import datetime

# 環境変数からLINE Botのチャネルアクセストークンを取得
LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
# チャネルアクセストークンを使用して、LINE BotのAPIインスタンスを作成
LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)

# ログ出力関数
def logging(errorLv, lambdaName, errorMsg):
    loggingDateStr = (datetime.now()).strftime('%Y/%m/%d %H:%M:%S')
    print(loggingDateStr + " " + lambdaName + " [" + errorLv + "] " + errorMsg)
    return

def lambda_handler(event, context):
    logging("info", context.function_name, "実行開始")

    try:
        body = json.loads(event.get("body", "{}"))  # event["body"]が空の場合、空のdictを返す
        events = body.get("events", [])  # eventsキーがない場合は空のリストを返す

        if events:  # eventsリストが空でないことを確認
            first_event = events[0]
            if first_event.get("type") == "message":
                message_type = first_event["message"].get("type")
                if message_type == "text":
                    replyToken = first_event['replyToken']  # リプライ用トークン
                    messageText = first_event["message"]["text"]  # 受信メッセージ
                    LINE_BOT_API.reply_message(replyToken, TextSendMessage(text=messageText))  # メッセージを返信
    except json.JSONDecodeError as e:
        logging("error", context.function_name, f"JSON decoding error: {str(e)}")
        return {'statusCode': 400, 'body': json.dumps('Invalid JSON format.')}
    except Exception as e:
        logging("error", context.function_name, f"Unexpected error: {str(e)}")
        return {'statusCode': 500, 'body': json.dumps('Internal server error.')}

    return {'statusCode': 200, 'body': json.dumps('Reply ended normally.')}

```
最終的なコードはこんな感じ。
実行してみると、うまくいったようだ。

line api messageのwebhook検証も試してみる。
成功だ。

最後にLINEでLINE API messageのQRコードを読み込んでbotを追加。
そして会話をしてみると...

![スクリーンショット 2024-02-05 183904.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/9b60fb8b-bc85-761d-c90c-fd8d05aec226.png)

やった！うまくオウム返しできている。
しかし、
メッセージありがとうございます！このアカウントでは・・・
という部分が邪魔なので、この部分を削除するには、line developersのapi message設定から応答メッセージを無効にすることで解決できる。

![スクリーンショット 2024-02-07 123451.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538166/1927495c-381b-9555-85b3-69ba01c0981f.png)


# 今後の目標
今回は上述のサイトを参考にlinebot製作を行ったが、次はchatgptを利用して、lineで文章生成aiを利用できるようにしたい。