【Python】オウム返しするLINE BOT作成


今回はPython(FastAPI)とLINE messaging APIを用いてオウム返しをするLINE BOTを開発します。

FastAPI は pythonのAPI用フレームワークです。
私はこれを使ったことなかったですが、ドキュメントを見た感じとっかかりやすそうだったのと、高速ということでレス時間がクオリティをかなり左右するボット開発においては有用かなと思い使ってみました。

なるべくLINE BOT SDKのメソッドの働き等についても踏み込んで説明します。
python-sdkに関してはドキュメントが存在しないので、参考になれば良いなと思います。

入門者の方はこちらを先に目をとしていただければFastAPIに関しての知識は十分得れるかと思います。
また、事前準備のセクションで記載していますが、今回はこの記事の環境構築部分を用いているのでついでにdocker環境を構築すると良いかなと思います。
[FastAPI入門](https://zenn.dev/sh0nk/books/537bb028709ab9)

FastAPI日本語ドキュメント
[FastAPI](https://fastapi.tiangolo.com/ja/)

## 事前準備
オウム返しをするLINE BOTをFastAPI (Python)を用いて作るにあたって主に以下のような下準備が必要になります。

今回はdockerで開発しますが、コードは環境に問わず参考になるかなと思います。

環境も同じにしたい場合は以下の4つのサイトをもとに構築してください。
### **ngrokインストール**
外部にlocalhostを公開するために使います。LINE botの動作確認に使用します。
[ngrokとは？インストール〜公開までの手順まとめ | Miyachi Labo](https://labo.kon-ruri.co.jp/ngrok/)

### **FastAPI(python) + Docker環境の構築**
以下のサイトを参考に、FastAPIインストールの章まで実行します。
[FastAPI入門](https://zenn.dev/sh0nk/books/537bb028709ab9)

### **LINE BOTの作成**
以下サイトの1番のセクションを実施して、アカウントの作成までを行う。
[【python】LINE botの作り方(Messaging API) | プログラミングLab](https://junpage.com/line-bot-development/)

### **Docker環境内でのパス設定**
初期状態ではPythonの参照元がdockerコンテナ内のものではない可能性が高いです。そのため、ファイル内でライブラリをインポートしてもvs codeのエラーが出てしまう場合があります。
Visual Studio Code のDev Containersという拡張機能で解消できるので試してみてください。（import時のウジウジ下線が気にならない場合は特にしなくても大丈夫です。僕は気になります。）
[Visual Studio Code を使用して Docker コンテナーを開発環境として使用する - Training](https://learn.microsoft.com/ja-jp/training/modules/use-docker-container-dev-env-vs-code/)

## 実装

### 初期設定
#### **.envファイルを作り環境変数を設定**

.envファイルをapiフォルダ内に作成します。
LINE Developersからチャネルシークレットとチャネルアクセストークンを確認し、環境変数として設定します。

```python
CHANNEL_SECRET=~~~~~~~~~~~~~~~~~~~
CHANNEL_ACCESS_TOKEN=~~~~~~~~~~~~~~~~~~~
```

.envファイルをプロジェクト内で読み込めるようにするために以下のコマンドを実行し、dotenvライブラリをインストールします。

```python
poetry add python-dotenv
```

#### **main.pyを編集しテスト用エンドポイントを作成**

以下のコードをapiは以下のmain.pyに記述します。

```python
from fastapi import FastAPI

app = FastAPI()

@app.get("/hello")
async def hello():
    return {"message": "hello world!"}
```

/docsをブラウザで開き、helloエンドポイントを実行してhello worldが帰ってきていたら成功です。Fast APIではswagger UIが自動生成され、/docsにアクセスするとそれを確認することができます。
![スクリーンショット 2023-10-14 15.11.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470041/c864b782-ed6b-38c5-0972-a9bd98400f86.png)

#### **line-bot-sdkをインストール**

Line messaging APIを用いた開発を行うにはあらかじめ用意されているLINE-BOT-SDKを使用します。
そこでこちらも同様にインストールしておきます。

```python
poetry add line-bot-sdk
```

以下がインストールしたsdkのgithubリンクになります。
詳細な仕様等についてreadmeに書いてありますので、今後使い方も説明しますが詳しく使い方が知りたい人は目を通してみてください。

https://github.com/line/line-bot-sdk-python

### Webhook用のエンドポイント定義

LINEのチャットボットをpython等の自作バックエンドで実行させるには、webhook用のエンドポイントの設定が必要です。

役割の説明を要約すると、ここではLINEプラットフォームからのリクエストであるということを確かめる動作をするということです。

その処理をこのセクションでは実装していきます。

#### **LINE用のファイルを用意**

これからスケールすることを考えてファイルを分割していこうと思います。
apiフォルダの配下にroutersフォルダを作成します。
また、作成したroutersフォルダ内に__init__.pyとline.pyファイルを追加します。

#### **Webhook用のエンドポイント作成**

line.pyを編集してWebhook用のエンドポイントを作成していきます。
```python
from fastapi import (
    APIRouter,
    Header, 
    Request
)
import os
from dotenv import load_dotenv
from linebot.v3 import WebhookHandler
from linebot.v3.exceptions import InvalidSignatureError
from starlette.exceptions import HTTPException

load_dotenv()
router = APIRouter()

handler = WebhookHandler(os.environ.get('CHANNEL_SECRET'))

@router.post(
    '/api/callback',
    summary='LINE Message APIからのコールバック',
    description='ユーザーからメッセージを受信した際、LINE Message APIからこちらにリクエストが送られます。',
)
async def callback(request: Request, x_line_signature=Header(None)):
    body = await request.body()

    try:
		# リクエストがLINEプラットフォームからのものかを検証する
        handler.handle(body.decode("utf-8"), x_line_signature)

    except InvalidSignatureError:
        raise HTTPException(status_code=400, detail="InvalidSignatureError")

    return "OK"
```

また、これらのルートファイルをapi/main.pyに認識させるために、main.pyに以下を追記します。

```python
from fastapi import FastAPI
# 追加
# lineファイルをインポート
from api.routers import line

app = FastAPI()

# 追加
# lineファイルで定義したルートをアプリケーションに適用
app.include_router(line.router)

@app.get("/hello")
async def hello():
    return {"message": "hello world!"}
```
##### (使用したLINE-BOT-SDK部分の解説)

- **WebhookHandler**

後ほど他のメソッド等も出てきますが、こちらで出てきたコードについて説明します。
このオブジェクトはこのコードで使用したように、lineプラットフォームから何らかのリクエストがあった時のハンドリングを行うためのものです。
    
**インスタンス化**
```python
handler = linebot.WebhookHandler('YOUR_CHANNEL_SECRET')
```
    
**handleメソッド**
    
以下のようにhandlerオブジェクトのhandleメソッドを使用し、リクエストボディとリクエストヘッダーに含まれるsignatureを検証してLINEプラットフォームからのアクセスであるかどうかを検証しています。
    
厳密に言えばもう少し複雑なことが行われていますが、イメージとしてこんな感じで理解できていれば十分かと思います。
    
```python
handler.handle(body, signature)
```


#### **検証**

ngrokで起動しているlocalhostを外部公開し、Line messaging APIのwebhook用URLに設定していきます。

```python
ngrok http 8000
```

以下のような記載が出てくるのでForwardingにあるURLをコピーして、webhook用のURLとして使います。

```python
ngrok                                                                                                                                                                                       (Ctrl+C to quit)

Introducing Always-On Global Server Load Balancer: https://ngrok.com/r/gslb

Session Status                online
Account                       〇〇 (Plan: Free)
Update                        update available (version 3.3.5, Ctrl-U to update)
Version                       3.0.7
Region                        Asia Pacific (ap)
Latency                       37ms
Web Interface                 http://127.0.0.1:4040
Forwarding                    https://c52e-203-205-52-152.ngrok-free.app -> http://localhost:8000

Connections                   ttl     opn     rt1     rt5     p50     p90
                              3       0       0.00    0.00    1.97    4.51

HTTP Requests
-------------
```

以下のように設定して検証を押します。
![スクリーンショット 2023-10-14 15.29.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470041/7c917093-086e-a9fa-a0d3-65057729a714.png)


以下のように表示されたら疎通成功です。
![スクリーンショット 2023-10-14 15.30.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470041/69a5d457-b299-daa5-0210-093b85ea9c40.png)


### オウム返し
前のセクションでWebhookでの疎通に関する部分が終わりました。
これから実際の処理(オウム返し)を実装していきます。

#### **設定**
アカウントの設定から以下のように挨拶メッセージとwebhookのみにしましょう。
応答メッセージ等がオンになっていると、自動的に余計な返信が行われてしまいますのでこのようにオフにしておきましょう。
![スクリーンショット 2023-10-14 16.18.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470041/02b74349-179b-7972-38a0-662a73992b49.png)


#### **オウム返し実装**

line.pyの記述を以下のように変更します。

```python
from fastapi import (
    APIRouter,
    Header, 
    Request
)
import os
from dotenv import load_dotenv
from linebot.v3 import WebhookHandler

# 以下のimportを追加
from linebot.v3.exceptions import InvalidSignatureError
from linebot.v3.messaging import (
    Configuration,
    ApiClient,
    MessagingApi,
    ReplyMessageRequest,
    TextMessage
)
from linebot.v3.webhooks import (
    MessageEvent,
    TextMessageContent
)
# ここまで
from starlette.exceptions import HTTPException

load_dotenv()
router = APIRouter()

handler = WebhookHandler(os.environ.get('CHANNEL_SECRET'))

# 以下の記述を追加
configuration = Configuration(access_token=os.environ.get('CHANNEL_ACCESS_TOKEN'))

@router.post(
    '/api/callback',
    summary='LINE Message APIからのコールバック',
    description='ユーザーからメッセージを受信した際、LINE Message APIからこちらにリクエストが送られます。',
)
async def callback(request: Request, background_tasks: BackgroundTasks, x_line_signature=Header(None)):
    body = await request.body()

    try:
        background_tasks.add_task(
            handler.handle(body.decode("utf-8"), x_line_signature)
        )

    except InvalidSignatureError:
        raise HTTPException(status_code=400, detail="InvalidSignatureError")

    return "OK"

# 以下を追加
@handler.add(MessageEvent, message=TextMessageContent)
def handle_message(event: MessageEvent):
    with ApiClient(configuration) as api_client:
        line_bot_api = MessagingApi(api_client)
        line_bot_api.reply_message_with_http_info(
            ReplyMessageRequest(
                reply_token=event.reply_token,
                messages=[TextMessage(text=event.message.text)]
            )
        )
```

##### (使用したLINE-BOT-SDK部分の解説)
- **MessagingApi, ApiClient, Configuration**
    
以下の流れでbotのインスタンスを作成し、MessagingApiインスタンスの持つメソッドによってユーザーへのメッセージ送信などを行うことができます。
    
この手順は深い理解というより、テンプレのように思っていれば大丈夫だと個人的には思います。
    
```python
configuration = Configuration(access_token=os.environ.get('CHANNEL_ACCESS_TOKEN'))
api_client = ApiClient(configuration)
line_bot_api = MessagingApi(api_client)
```
    
MessagingApiオブジェクトのメソッドは以下から詳しく見ることができます。
[公式ドキュメント](https://github.com/line/line-bot-sdk-python/blob/master/linebot/v3/messaging/docs/MessagingApi.md)


- **WebhookHandler**  
 
**addメソッド**
addメソッドでは特定のイベントがあった場合の処理内容を定義することができるメソッドです。
具体的には以下のように使うことができます。
    
```python
@handler.add(MessageEvent, message=TextMessage)
# メッセージイベントかつテキストメッセージを受け取った際の処理を記述

@handler.add(MessageEvent)
# メッセージイベントがあった際の処理を記述

@handler.add(FollowEvent)
# フォローイベントがあった際の処理を記述
```

## まとめ

Fast APIとLine messaging APIを用いてオウム返しするbotを開発してみました。
Fast APIはほぼ初めて使いますが、かなり簡単で使い勝手が良いなと思いました。（もしこうした方が良い等ありましたらご指摘ください）

今回はただのオウム返しでしたが、LINE messaging APIではおしゃれなUIを作れたりするのでそういったことにも挑戦してみようかなと思います。
あと折角なのでこれからこのコードを拡張してより応用的なアプリケーションを次回以降作っていこうと思います！