[無料][2024年版] LINE Messaging API v3 + Python(Flask) でボットを作る [その9 - ポストバック編]

## ポストバックとは

一言でいうと、**文字列データを付加してサーバーに送信すること**です。購入ボタンを押した時に `buy` という文字列を送信したり、リッチメニューを開いた時に `open` という文字列を送信したり、ポストバックイベントは**何らかのアクション**に紐づいてサーバー側に送信されます。元々は Webアプリケーションの用語で、現在のウェブページ自身に POSTリクエストを送るところから来ているようです。

そして、このポストバックで送信するデータというのは、ユーザーからは見えません。ですので、普通のメッセージのやりとり以外で送りたいデータがあるときにはポストバックが重宝します。

わかりやすい例なんかでは、商品の購入ボタンをした時に「その商品IDを送信する」というものです。商品名が一意に決まるのであれば、ポストバックを使わずとも普通のメッセージで商品名を送っても処理できますが、データベースと連携しているなんかの場合は、IDで受け取れるというのは非常に楽でしょう。

また、アンケートなどのように連続してメッセージをやり取りする際、前の回答データを残しておきたいことがあります。この場合にもポストバックが有効です。

ポストバックは、**PostbackAction** としてボタンなどに配置し、Webhook受信時には **PostbackEvent** として処理します。

> ポストバックアクション

https://developers.line.biz/ja/reference/messaging-api/#postback-action

> ポストバックイベント

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

## ポストバック+クイックリプライを使ったクイズ例

早速ですが、ポストバックを利用した完成品の動画です。`Quiz` と入力すると歌詞の一部から曲名を当てるクイズモードに入ります。選択肢はクイックリプライを使って4つ表示されます。計5問が出題され、最後には合計得点をカウントして表示します。

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/1039dec7-2d3d-6f88-d729-8096023e7e0d.gif)

試してみたい方はぜひこちらからボット登録してください。
LINE BOT ID : `@711mvjit`

![711mvjit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/11af27e0-e364-8a8d-856f-1527993636d5.png)

このプログラムでは各クイックリプライボタンに **PostbackAction** が設定されています。ポストバックはその中で4つのパラメータを保持しています。

|パラメータ|説明|値|
|-|-|-|
|`question`|前問の問題番号|0 - 5|
|`score`|前問までの合計得点|0 - 5|
|`answer`|前問の正解の選択肢|曲名|
|`correct`|前問で正解したかどうか|true / false|

クイックリプライの選択肢を押して一問回答すると、これらの値が処理関数に渡されて動的に更新され、次の問題のポストバックのデータになるということです。

詳細なコードは長くなるので割愛します。興味のある方は、[リポジトリ](https://github.com/nozomiyamada/linebot)から `app.py` と `botfuncs_quiz.py` を見て下さい。ここでは使い方のみ説明します。

## `PostbackAction`

`linebot.v3.messaging` の中に存在するクラスです。ボタンや画像などに設定できる、アクションオブジェクトの一つです。基本的に以下の3つを指定します。

- `label` : 表示する文字列（**20文字以下**）。ボタンでは必須、画像やリッチメニューでは任意
- `displayText` : そのボタンを押した時に、メッセージとして表示される文字列。表示されるだけで、実際にそのメッセージが Webhook で送信されるわけではない
- `data` : 送信する不可視のデータ文字列

この `data` にどうやって必要な情報を入れるかは、完全にお好みです。単純に必要な値だけをそのまま入れても良いですが、一般的なのは `id=123&name=abc` などのように、GETリクエスト同様のクエリ文字列にする方法です。私の実装でも `'question=0&score=0&answer=NONE&correct=true'` のような文字列にして送信しています。`0,0,NONE,true` のように配列的にしてもいいですが、やはりキー・バリュー型にしておく方がコードの可読性が上がるという理由です。

このようなクエリ文字列をパースする方法として、組み込みライブラリの `urllib.parse.parse_qs()` を使う方法があります。 

https://qiita.com/yagays/items/e59731b3930252b5f0c4

これを用いれば、クエリ文字列を自動的に辞書型に変換してくれますが、バリューがリストになってしまうのと、型変換まではやってくれないので、自作関数を作った方が早いです。私は以下のように実装しました。

~~~python:utils.py
def parse_postback(postback:str) -> dict:
	"""
	"question=1&score=0&answer=Help!&correct=true"
	-> {'question': 1, 'score': 0, 'answer': 'Help!', 'correct': 'true'}
	"""
	result = {}
	for parameter in  postback.split('&'):
		key, value = parameter.split('=')
		try:
			result[key] = int(value)
		except:
			result[key] = value
	return result
~~~

これはコードそのままで、まず `&` で　`key=value` というパラメータの組に分割し、さらにそれぞれを `=` で分割して、整数型に変換しているだけです。今回の場合、曲名である `answer` の部分には `&` や `=` の文字が入らないので機能しますが、もしバリュー内にこのような文字が存在する可能性がある場合、別の仕切り文字を使う必要があります。
`"question@1|score@0|answer@Help!|correct@true"`
などでも良さそうですね。何なら、JSON文字列にしても構わないと思います。

逆に、これらのパラメータをポストバックとして送る際には一つの文字列にまとめる必要があります。`urllib.parse.urlencode` というメソッドがありますが、これは `don't` が `don%27t` のようにパーセントエンコードされてしまうので、オススメしません。自作関数でも以下のように一行で済みますので、これを使いましょう。

~~~python:utils.py
def encode_postback(postback_dict:dict) -> str:
	return '&'.join(f'{key}={value}' for key, value  in postback_dict.items())
~~~

## `PostbackEvent`

これは `MessageEvent` や `FollowEvent` 同様に Webhook受信のタイプを示すもので、`linebot.v3.webhooks` の中に入っています。`app.py` の中に、このように新しいイベントハンドラを作ります。

~~~python:app.py
@handler.add(PostbackEvent)
def handle_postback(event):
	with ApiClient(configuration) as api_client:
		line_bot_api = MessagingApi(api_client)
	
	## get the postback data
	postback_data = event.postback.data

	## get replay message - QuickReply(with postback) or TextMessage
	messages = create_postback_reply(postback=postback_data)

	## send reply message
	line_bot_api.reply_message(ReplyMessageRequest(
		replyToken=event.reply_token,
		messages=messages
	))
~~~

この `create_postback_reply(postback)` がポストバックを受け取って次のポストバック付きクイックリプライを作成する具体的な処理関数です。それ以外の部分は、他のイベントハンドラと大差ありません。

## クイズの設計

ではこれらを使って、全5問のクイズを作ってみましょう。大まかな処理の流れは以下のようになります。

1. ユーザーが `"Quiz"` というメッセージを送信
2. `MessageEvent` ハンドラ (`handle_message` 関数) 内でクイズモードに分岐、**実際にはポストバックはないが、パラメータ初期値を疑似ポストバックとして用意して** `create_postback_reply(postback)` に渡す
3. `create_postback_reply(postback)` が与えられたポストバックから1問目のポストバック付クイックリプライを作成し、`MessageEvent` ハンドラ内で返信
4. ユーザーがクイックリプライボタンを選択、ポストバック送信
5. `PostbackEvent` ハンドラ (`handle_postback` 関数) 内でポストバック受信、`create_postback_reply(postback)` に渡す
6. `create_postback_reply(postback)` が与えられたポストバックから2問目のポストバック付クイックリプライを作成し、`PostbackEvent` ハンドラ内で返信
7. 以下2問目の回答受信〜5問目の問題送信も同様
8. 5問目の回答のポストバックを受信して `create_postback_reply(postback)` に渡すと、関数は結果を表示するだけの `TextMessage` を作成し、送信して終了

ということです。つまり、 `create_postback_reply(postback)` 内で

- 開始 & 1問目作成
- 1問目受信 & 2問目作成 ~ 4問目受信 & 5問目作成
- 5問目受信 & 結果作成

という3パターンに分けて処理が必要ということですね。この判断のために、ポストバックに `question` というパラメータを用意し、分岐させています。ポストバックに格納しているパラメータを再掲しておきます。

|パラメータ|説明|値|
|-|-|-|
|`question`|前問の問題番号|0 - 5|
|`score`|前問までの合計得点|0 - 5|
|`answer`|前問の正解の選択肢|曲名|
|`correct`|前問で正解したかどうか|true / false|

`score` は、正解のボタンのポストバックのみ値が +1 されており、`correct` も正解のボタンのみ `true` になっています。`answer` は前問の正解を表示するためのものなので、各回ごとにどのボタンでも同じ値を保持しています。

クイズは Flex Message を使って作り、コードかなり長くなっていますのでここには載せませんが、[GitHub レポジトリ](https://github.com/nozomiyamada/linebot/tree/main) で公開していますから良ければ参考にして下さい。ちなみに、`TextMessage` を使って作るなら

~~~python
## インスタンス化
quick_reply = QuickReply(items=[])

## 各ボタンの作成
for ... :
    item = QuickReplyItem(action=PostbackAction(
        label=xxx,
        displayText=xxx,
        data=encode_postback(postback_dict)
    ))
    quick_reply.items.append(item)

## メッセージオブジェクト化
messages = [TextMessage(text=xxx, quickReply=quick_reply)]
~~~

のような感じでできるはずです。

## さらなる高みへ

実は今回のバージョンで、記事には書いていない様々な機能をこっそり追加しています。詳しくはリポジトリの README を参照して下さい。

LINE Messaging API の最後の重要な機能として**リッチメニュー**があるのですが、やれることはポストバックアクションだったり、URIアクションだったり、他の要素と結局は同じです。それはあくまで LINE Messaging API の範囲内なので仕方がないのですが、逆に言えば API の範囲外であればやれることはもっと増えるわけです。

そこで、次回は一気に **LIFF** の導入をしようと思います。これを使用することで、「入力フォームを使って検索」のようなこともできるようになり、ボットの UI/UX 向上にもなる(?)ことでしょう。その過程で、リッチメニューも導入します。


## 目次 : [無料][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 - テストアプリ編](https://qiita.com/nozomiyamada/items/87389aacb151b8418501)
- [その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 - ポストバック編
- [その10 - リッチメニュー & LIFF 編](https://qiita.com/nozomiyamada/items/ffbeeafbadfa6e98c711)