[無料][2024年版] LINE Messaging API v3 + Python(Flask) でボットを作る [その2 - テストアプリ編]

## まずは最終目標

では準備も終わったところで、具体的なボットの中身に入っていきたいと思います。本格的なチャットボットの類は当然 ChatGPT などを使った方が良いので、分野限定的な（ニッチな）情報検索ツールとしてのボットを作ることにします。

今回は、ビートルズの楽曲情報を引き出すためのツールを作ろうかと思います。私がスクレイピングなどでまとめたデータは[こちらのレポジトリ](https://github.com/nozomiyamada/linebot/tree/main/data)にありますので、よければご自由にお使いください。

搭載予定機能は以下のようになります

・楽曲名からYouTube動画の検索
・楽曲名から歌詞の取得
・楽曲名から演奏動画や楽譜の検索
・楽曲名から曲情報の検索
・アルバム名から楽曲一覧の取得
・歌詞の単語から楽曲検索
・その他おまけ機能

などです。何回に分けて、少しずつ機能を増やしていきたいと思います。

また、あくまでこれは私の例です。私のコードを参考に、勝手にアレンジしてお好みのボットを作ってみて下さい。

ちなみにまだまだ現在進行形ですが、作成したボットはこちらになります。ぜひ友達登録して使ってみて下さい。

LINE BOT ID : `@711mvjit`

![711mvjit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/e5bb1e26-d1ce-8353-e1af-f274a15e2393.png)


## テストアプリの作成

各機能の具体的なプログラムは順次実装するとして、まずはLINEボットが動く状態にしなければなりません。テストプログラムで動作の確認をします。今回は Flask を使うので、以下のようなディレクトリ構成にします。

~~~
linebot/
　├ venv/  ## 仮想環境
　├ .env  ## チャンネルトークンなどの記載
　├ data/  ## そのうちデータを入れる
　├ templates/  ## そのうちウェブサイトを作る
　├ requirements.txt  ## ライブラリ一覧
　├ .gitignore  ## GitHub用
　└ app.py  ## メインプログラム
 ~~~

LINE messaging API では、ユーザーがボットに対して行った動作はWebhookイベントとして受信します。イベントには以下のようなものがあります。

https://developers.line.biz/ja/reference/messaging-api/#webhook-event-objects

おそらくよく使うのはこの3つでしょう

- **フォローイベント** : ユーザーがボットを友達追加（or ブロック解除）した場合に受け取る Webhook
- **メッセージイベント** : テキスト、動画、スタンプなど何らかの「メッセージ」をユーザーがボットに送信した場合に受け取る Webhook
- **ポストバックイベント** : ボタン・画像を押す、クイックリプライを選択するなどの動作を行ったときに受け取る Webhook

このうち、フォローイベントとメッセージイベントを使った、以下のようなテストプログラムを作ります

:::note
1. 友達追加した際に、"Thank You!" と送るe
2. メッセージを送った際に、その内容をユーザーネームと共にオウム返しする
:::

それでは具体的に `app.py` を編集していきます。コードは、[公式SDKのGitHubページ](https://github.com/line/line-bot-sdk-python/tree/master)から大半をコピペします。

#### インポート部分
`Flask`, `linebot`, `os` をインポートします。仮想環境の構築とライブラリのインストールが済んでいない方は前回の記事を参考にして下さい。

~~~python:app.py
# -*- coding: utf-8 -*-
from flask import Flask, request, abort
from linebot.v3 import WebhookHandler
from linebot.v3.exceptions import InvalidSignatureError
from linebot.v3.messaging import (
	ApiClient, Configuration, MessagingApi,
	ReplyMessageRequest, PushMessageRequest,
	TextMessage, PostbackAction
)
from linebot.v3.webhooks import (
	FollowEvent, MessageEvent, PostbackEvent, TextMessageContent
)
import os
~~~

#### 環境変数部分
LINE Developer アカウントで発行した2つのトークンをプログラムに渡す必要がありますが、直接コード内に書くのはオススメできません。環境変数として `.env` ファイルから別途読み込む形式が基本です。詳しくは[こちら](https://qiita.com/shown_it/items/2b85434e4e2658c484f4)などを参考にして下さい。その際に、仮想環境内で `python-dotenv` のインストールが必要になります。

~~~
(venv) linebot $ pip install python-dotenv
~~~

その上で、`.env` ファイルにチャンネルシークレットと、チャンネルアクセストークンを以下のように記載します。[LINE Developers](https://developers.line.biz/ja/) の「チャネル基本設定」「Messaging API設定」の項目からそれぞれ確認できます

~~~bash:.env
CHANNEL_ACCESS_TOKEN=4KJ......
CHANNEL_SECRET=549......
~~~

ここまで準備できたら、`app.py` 内でこれらの値を読み込み、以下のように割り当てます。

~~~python:app.py
## .env ファイル読み込み
from dotenv import load_dotenv
load_dotenv()

## 環境変数を変数に割り当て
CHANNEL_ACCESS_TOKEN = os.environ["CHANNEL_ACCESS_TOKEN"]
CHANNEL_SECRET = os.environ["CHANNEL_SECRET"]
~~~

また、`.env` を GitHub に間違ってアップしてしまわないよう、`.gitignore` ファイルに書きこんでおくのも忘れないで下さい。私は Mac なので、`.DS_Store` などを除外するためによくこう書きます。

~~~bash:.gitignore
**/.DS_Store  ## 不要な管理ファイルの除外
**/__pycache__  ## キャッシュファイルの除外
.env  ## 環境変数ファイル
venv  ## 仮想環境
~~~

#### おまじない部分
大半をサンプルコードからコピペして使います。Flask アプリの作成と、コールバックでの認証等に関するものです。

~~~python:app.py
## Flask アプリのインスタンス化
app = Flask(__name__)

## LINE のアクセストークン読み込み
configuration = Configuration(access_token=CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(CHANNEL_SECRET)

## コールバックのおまじない
@app.route("/callback", methods=['POST'])
def callback():
	# get X-Line-Signature header value
	signature = request.headers['X-Line-Signature']

	# get request body as text
	body = request.get_data(as_text=True)
	app.logger.info("Request body: " + body)

	# handle webhook body
	try:
		handler.handle(body, signature)
	except InvalidSignatureError:
		app.logger.info("Invalid signature. Please check your channel access token/channel secret.")
		abort(400)

	return 'OK'
~~~

#### イベントハンドラ部分

ここからメインプログラムになります。テストアプリに実装する機能を再掲します。

1. 友達追加した際に、"Thank You!" と送る
2. メッセージを送った際に、その内容をユーザーネームと共にオウム返しする

##### 1. 友達追加時のメッセージ
フォローイベントを受信した時の処理をします。関数名は `handle_follow()` 以外にしても構いません。`messages` の部分は、リストにするのを忘れないで下さい。 


~~~python:app.py
## 友達追加時のメッセージ送信
@handler.add(FollowEvent)
def handle_follow(event):
	## APIインスタンス化
	with ApiClient(configuration) as api_client:
		line_bot_api = MessagingApi(api_client)

	## 返信
	line_bot_api.reply_message(ReplyMessageRequest(
		replyToken=event.reply_token,
		messages=[TextMessage(text='Thank You!')]
	))
~~~

ちなみに、友達追加時のメッセージ (Greeting message) は [LINE Official Account Manager](https://manager.line.biz/) でも設定できてしまうので、動作確認のためにそちらはオフにしておきましょう。友達追加して、何もメッセージが送られてこないことを確認して下さい。

![Screenshot 2024-01-05 at 20.06.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/b65c0601-1750-5156-7cba-14f1faa5675b.png)

##### 2. オウム返しメッセージ

これは公式のサンプルコードほぼそのままですが、せっかくなので一工夫加えます。Webhook の情報には UserID はあるものの、ユーザーネームはわかりません。これを取得するために、APIを呼びます。

また、`handler.add` の部分で `TextMessageContent` を指定することで、「テキストメッセージの時のみ反応する」ようにします。これを指定しないと、動画やスタンプなどあらゆるメッセージで反応するようになります。

~~~python:app.py
## オウム返しメッセージ
@handler.add(MessageEvent, message=TextMessageContent)
def handle_message(event):
	## APIインスタンス化
	with ApiClient(configuration) as api_client:
		line_bot_api = MessagingApi(api_client)

	## 受信メッセージの中身を取得
	received_message = event.message.text

	## APIを呼んで送信者のプロフィール取得
	profile = line_bot_api.get_profile(event.source.user_id)
	display_name = profile.display_name

	## 返信メッセージ編集
	reply = f'{display_name}さんのメッセージ\n{received_message}'

	## オウム返し
	line_bot_api.reply_message(ReplyMessageRequest(
		replyToken=event.reply_token,
		messages=[TextMessage(text=reply)]
	))
~~~

#### プログラム起動部分

最後に、プログラムを起動するコードを付け加えます。ポート番号は `8000` にしていますが、これは他の数字でも構いません。コードを起動してアプリケーションサーバーを立ち上げるのに私は `gunicorn` というものを使うので、そのデフォルトポートであるこの数字を使っています。

~~~python:app.py
## ボット起動コード
if __name__ == "__main__":
	## ローカルでテストする時のために、`debug=True` にしておく
	app.run(host="0.0.0.0", port=8000, debug=True)
~~~

#### 起動確認用ウェブページ

ここまででLINEボットとしてのコードは十分なのですが、デプロイが完了してアプリが起動しているかどうかの確認ページも作っておきます。Flask はウェブ作成をするのためのツールでもありますので、そのルーティング機能で簡単に実装できます。先ほどの `if __name__...` の上にこれを追加します

~~~python:app.py
## 起動確認用ウェブサイトのトップページ
@app.route('/', methods=['GET'])
def toppage():
	return 'Hello world!'
~~~

デプロイが完了した後、Render で発行されたURLにジャンプして、"Hello world!" が表示されていればきちんと起動しているということになります。

ここまでの `app.py` のコードは、最下部に全てまとめてありますので使って下さい。

## デプロイ準備

Flask 単体でもアプリケーションサーバー立ち上げは可能なのですが、テスト用の簡易的なものなのでここはやはり `gunicorn` などを使うことをおすすめします。ということで、これもインストールしてしまいましょう。

~~~bash
(venv) linebot $ pip install gunicorn
~~~

これらインストールしたライブラリのリストは、`requirements.txt` として書き出しておく必要があります。これがないと、デプロイする時にどのライブラリをインストールすればいいかわからないからです。以下のコマンドを実行します。

~~~bash
(venv) linebot $ pip freeze > requirements.txt
~~~

書き出しが成功すると、以下のようになライブラリとバージョンが指定されたファイルができているはずです。

~~~bash:requirements.txt
aenum==3.1.15
aiohttp==3.9.1
...
Flask==3.0.0
...
gunicorn==21.2.0
...
line-bot-sdk==3.7.0
...
python-dotenv==1.0.0
...
~~~

ここに、`Flask`, `gunicorn`, `line-bot-sdk`, `python-dotenv` の4つがきちんとあることを確認して下さい。バージョンは同じでなくとも構いません（上記は Python3.11.6、pip23.3.1 の場合です）。もしなければ、**仮想環境が有効化されていることを確認**した上で `$ pip install line-bot-sdk flask python-dotenv gunicorn` しましょう。

ここまでで、ディレクトリ・ファイル構成は以下のようになっているはずです。

~~~
linebot/
　├ venv/
　├ .env
　├ requirements.txt
　├ .gitignore
　└ app.py
 ~~~

そして、この `app.py` と `requirements.txt` を GitHub にアップします。くれぐれも `.env` はアップしないようにして下さい。`.gitignore` がきちんと書かれているか確認して下さい。

## Render にデプロイ

前回の準備編でアカウントを作ってあれば、"New Web Service" から新しくアプリを作ります。

![Screenshot 2024-01-05 at 21.53.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/4403032a-4226-8e82-c1f8-008a525a211e.png)

GitHub の `linebot` レポジトリと連携します。リストにない場合は、右側の "Configure Account" からレポジトリを追加します。

![Screenshot 2024-01-05 at 21.57.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/07e6e517-76ee-84a4-7485-9577002995c7.png)

ボットの名前を設定します。Region は一番近い Singapore にしていますが、多分どこでもそこまで変わらないはずです。

![Screenshot 2024-01-05 at 21.59.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/d5d619ca-488e-e273-37c5-7ca0bb1bfecb.png)

起動時のコマンドを設定します。

- `gunicorn` をインストールしている
- ファイル名が `app.py` で、Flask のインスタンス名が `app`

ならいじる必要はありません。


次の Instance Type ですが、今回のテーマは **[無料]** なので、当然 Free を選択します。

![Screenshot 2024-01-05 at 22.06.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/0bf8bd10-73d6-30a5-9787-f2ed66e6839d.png)

最後の Environment Variables の項目に、`.env` に記載しておいた `CHANNEL_ACCESS_TOKEN` と `CHANNEL_SECRET` を入力します。ここで設定することで、`.env` ファイルがなくても環境変数として取り込むことができるようになります。 

![Screenshot 2024-01-05 at 22.08.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/d9621a07-e5b6-5a05-2c75-b45925d8b38d.png)

Create Web を押すと、自動でデプロイが始まり、ログが表示されます。

![Screenshot 2024-01-05 at 22.13.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/5f561585-2e0f-3b70-8508-72d7b0adbb27.png)

左側の "Event" にデプロイ履歴が表示されます。デプロイが成功すると、✅マークが付きます。失敗した場合は、ファイルが欠けているか、バージョンの問題がほとんどです。ログと睨めっこして修正が必要です。

![Screenshot 2024-01-05 at 22.16.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/82b22834-8b58-878b-4c27-f1e80eb515e5.png)

上部に表示されているWebページのリンクにアクセスします。きちんと `Hello world!` と表示されていますね。

![Screenshot 2024-01-05 at 22.18.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/84a3fe52-9635-3982-129e-a85ee0a5239f.png)

## Webhook 設定

デプロイが成功したら、ボットの Webhook の設定をします。[LINE Developers のコンソール](https://developers.line.biz/console/)にいき、チャネルを選択します。

"Messaging API設定" の項目から、"Webhook設定" にいき、ここに Render で割り当てられた URL に `/callback` を足したものを登録します。さらに検証ボタンを押して、「成功」と表示されればOKです。その下にある "Webhookの利用" にチェックを付けるのを忘れないで下さい。

![Screenshot 2024-01-05 at 22.30.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/5692bb6f-3578-1582-46b4-6ee38992e5aa.png)

## ボット動作確認

これで設定は終了です。実際にLINE内で動作を確認してみましょう。

LINE Developers 内に表示されているQRを読み込んで友達登録する（2回目以降は右上の三本線メニューから、ブロック→ブロック解除する）と、`Thank You!` のメッセージが送られてきます。さらにユーザーネーム付きのオウム返しも成功しています。

![IMG_65959FF5E5B4-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/a5a7d498-42d8-dbc5-f994-bc990c6769dc.jpeg)

ということでテストアプリ編は終了です。次回から本番ボットの実装に入ります。


## `app.py` 全コード

~~~app.py
# -*- coding: utf-8 -*-
from flask import Flask, request, abort
from linebot.v3 import WebhookHandler
from linebot.v3.exceptions import InvalidSignatureError
from linebot.v3.messaging import (
	ApiClient, Configuration, MessagingApi,
	ReplyMessageRequest, PushMessageRequest,
	TextMessage, PostbackAction
)
from linebot.v3.webhooks import (
	FollowEvent, MessageEvent, PostbackEvent, TextMessageContent
)
import os

## .env ファイル読み込み
from dotenv import load_dotenv
load_dotenv()

## 環境変数を変数に割り当て
CHANNEL_ACCESS_TOKEN = os.environ["CHANNEL_ACCESS_TOKEN"]
CHANNEL_SECRET = os.environ["CHANNEL_SECRET"]

## Flask アプリのインスタンス化
app = Flask(__name__)

## LINE のアクセストークン読み込み
configuration = Configuration(access_token=CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(CHANNEL_SECRET)

## コールバックのおまじない
@app.route("/callback", methods=['POST'])
def callback():
	# get X-Line-Signature header value
	signature = request.headers['X-Line-Signature']

	# get request body as text
	body = request.get_data(as_text=True)
	app.logger.info("Request body: " + body)

	# handle webhook body
	try:
		handler.handle(body, signature)
	except InvalidSignatureError:
		app.logger.info("Invalid signature. Please check your channel access token/channel secret.")
		abort(400)

	return 'OK'

## 友達追加時のメッセージ送信
@handler.add(FollowEvent)
def handle_follow(event):
	## APIインスタンス化
	with ApiClient(configuration) as api_client:
		line_bot_api = MessagingApi(api_client)

	## 返信
	line_bot_api.reply_message(ReplyMessageRequest(
		replyToken=event.reply_token,
		messages=[TextMessage(text='Thank You!')]
	))
	
## オウム返しメッセージ
@handler.add(MessageEvent, message=TextMessageContent)
def handle_message(event):
	## APIインスタンス化
	with ApiClient(configuration) as api_client:
		line_bot_api = MessagingApi(api_client)

	## 受信メッセージの中身を取得
	received_message = event.message.text

	## APIを呼んで送信者のプロフィール取得
	profile = line_bot_api.get_profile(event.source.user_id)
	display_name = profile.display_name

	## 返信メッセージ編集
	reply = f'{display_name}さんのメッセージ\n{received_message}'

	## オウム返し
	line_bot_api.reply_message(ReplyMessageRequest(
		replyToken=event.reply_token,
		messages=[TextMessage(text=reply)]
	))

## 起動確認用ウェブサイトのトップページ
@app.route('/', methods=['GET'])
def toppage():
	return 'Hello world!'

## ボット起動コード
if __name__ == "__main__":
	## ローカルでテストする時のために、`debug=True` にしておく
	app.run(host="0.0.0.0", port=8000, debug=True)
~~~


## 目次 : [無料][2024年版] LINE Messaging API v3 + Python(Flask) でボットを作る
[GitHub レポジトリ](https://github.com/nozomiyamada/linebot/tree/main)
`older_version` 内に、各回時点での `app.py` と `requirements.txt` があります。

- [その0 - v3 での変更点](https://qiita.com/nozomiyamada/items/dff5f240e8c936d731fd)
- [その1 - 準備編](https://qiita.com/nozomiyamada/items/f6c6816e02359e08a74e)
- その2 - テストアプリ編
- [その3 - 情報検索理論(前編)](https://qiita.com/nozomiyamada/items/d1bcedcc885c43794ff7)
- [その4 - 情報検索理論(後編)](https://qiita.com/nozomiyamada/items/6b0b30ebc36eeddc73d2)
- [その5 - tips 編](https://qiita.com/nozomiyamada/items/58918cd4024987ad1979)
- [その6 - モジュール化編](https://qiita.com/nozomiyamada/items/b80eaf5aef0c294c4acf)
- [その7 - クラス化編](https://qiita.com/nozomiyamada/items/c8315c8a0f0f7d19129b)
- [その8 - Flex Message 編](https://qiita.com/nozomiyamada/items/1be9ae292b01fb35273e)
- [その9 - ポストバック編](https://qiita.com/nozomiyamada/items/8a236a2d38f33827b4ce)
- [その10 - リッチメニュー & LIFF 編](https://qiita.com/nozomiyamada/items/ffbeeafbadfa6e98c711)