軽い気持ちでLINE BOTに手を出してみた～おみくじBOT改良編～

お疲れさまです、みやもとです。
[先日投稿した記事](https://qiita.com/Mymt_aggw2208/items/d4a2bf81ae1c6ccf4cb2)で作ったLINE BOTについて、投稿時点で以下の改善点を挙げていました。

* ボタンをおみくじ自体ではなく「おみくじをひくかどうか」にする
* タップしたことがわかるようにPostbackではなくMessageで返す

せっかく思いついたからには反映させたいと思います。

ついでに、『前回記事のコードだとどんなメッセージを投げても同一のメッセージを投げてくる＝「応答している」感が薄い』ということで、メッセージを受けてリアクションを多少なりとも変えるようにしてみました。

:::note
この記事に出てくるコードはGoogle Cloud Functionsで動作を確認しています。
・環境：第2世代
・トリガー：HTTPS
・認証：未認証の呼び出しを許可
・割り当てメモリ：256MiB
・ランタイム：Python 3.8
:::

## おみくじBOTをいじろう～ランダム編～ 
とりあえずの仕様として決めたのは
* 受信したメッセージが「おみくじ」の場合、そのままおみくじの結果を返す
* 「おみくじ」以外のメッセージを受信した場合、受信したメッセージをオウム返しにした後におみくじをひくかどうかの確認をする
* 「おみくじをひく」選択肢を選んだ場合、おみくじの結果を返す
* 「おみくじをひかない」選択肢を選んだ場合、再度念押ししておみくじをひくかどうかの確認をする

この時点でお気づきかもしれませんが、この仕様で実装するとおみくじをひくまで選択肢を出し続けるおみくじの怨念みたいなBOTになります。
受信したメッセージとの対話っぽくしようとした結果かえって相手の話を聞かないモンスターになった感もありますがまあいいでしょう。

では実際のコードです。
今回も全体は折りたたんで、個別に切り出して解説します。
<details><summary>main.py</summary>

```Python:main.py
import os
import base64, hashlib, hmac
import logging
import random

from flask import abort, jsonify

from linebot import (
    LineBotApi, WebhookParser
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage, 
    StickerMessage, StickerSendMessage,
    TemplateSendMessage,ConfirmTemplate,MessageAction
)

omikuji = {0:[6325,10979924,'大吉！良いことあるかも？'],
            1:[11537,52002741,'中吉。いつも通りがいちばん'],
            2:[11537,52002745,'小吉。些細なことだってしあわせ'],
            3:[11537,52002754,'吉。平穏無事のありがたみ'],
            4:[6325,10979917,'末吉。ちょっとだけ気をつけて'],
            5:[11537,52002765,'凶。おとなしく過ごして']}


def main(request):
    channel_secret = os.environ.get('LINE_CHANNEL_SECRET')
    channel_access_token = os.environ.get('LINE_CHANNEL_ACCESS_TOKEN')

    line_bot_api = LineBotApi(channel_access_token)
    parser = WebhookParser(channel_secret)

    body = request.get_data(as_text=True)
    hash = hmac.new(channel_secret.encode('utf-8'),
        body.encode('utf-8'), hashlib.sha256).digest()
    signature = base64.b64encode(hash).decode()

    if signature != request.headers['X_LINE_SIGNATURE']:
        return abort(405)

    try:
        events = parser.parse(body, signature)
    except InvalidSignatureError:
        return abort(405)

    for event in events:
        if isinstance(event, MessageEvent):
            if isinstance(event.message, TextMessage):
                if event.message.text == 'おみくじ':
                    line_bot_api.reply_message(
                        event.reply_token,
                        get_omikuji()
                    )
                else:
                    reply_data = []
                    if event.message.text == 'やめとく':
                        reply_data.append(
                            StickerSendMessage(
                                package_id=6325, sticker_id=10979923
                            ))
                    
                    reply_data.append(
                        make_button_template(event.message.text))
                    line_bot_api.reply_message(
                        event.reply_token,
                        reply_data
                    )
            else:
                continue

    return jsonify({ 'message': 'ok'})


def make_button_template(message_text):
    if message_text == 'やめとく':
        base_text = 'ほんとにひかない？'
    else:
        base_text = '『' + message_text + '』' + '\n おみくじひかない？'
    message_template = TemplateSendMessage(
        alt_text='おみくじ',
        template=ConfirmTemplate(
            text = base_text,            
            actions=[
                MessageAction(
                    text='おみくじ',
                    label='ひく'
                ),
                MessageAction(
                    text='やめとく',
                    label='ひかない'
                )
            ]
        )
    )
    return message_template


def get_omikuji():
    result = omikuji[random.randint(0,5)]
    sticker_message = StickerSendMessage(
        package_id=result[0], sticker_id=result[1]
    )
    text_message = TextSendMessage(text=result[2])
    return [sticker_message, text_message]

```
</details>

とりあえずimport群や環境変数取得等については前回と同じなので省略して、今回変更したところをメインに解説をつけていきます。

#### dict定義「omikuji」

```Python:main.py
omikuji = {0:[6325,10979924,'大吉！良いことあるかも？'],
            1:[11537,52002741,'中吉。いつも通りがいちばん'],
            2:[11537,52002745,'小吉。些細なことだってしあわせ'],
            3:[11537,52002754,'吉。平穏無事のありがたみ'],
            4:[6325,10979917,'末吉。ちょっとだけ気をつけて'],
            5:[11537,52002765,'凶。おとなしく過ごして']}
```
前回と同じくLINEスタンプのパッケージID、LINEスタンプのステッカーID、おみくじの結果文言をリストに格納し、それをさらにキーと紐づけてdictに格納しています。
前回との違いはおみくじ結果の数と、地味にキーも文字列ではなく数値にしています。

#### 関数定義「get_omikuji」
前回記事の投稿後に「呼び出す処理の方を先に解説した方が流れがわかりやすいな」と思ったので、先におみくじ結果の取得処理を。
```Python:main.py
def get_omikuji():
    result = omikuji[random.randint(0,5)]
    sticker_message = StickerSendMessage(           # スタンプの設定
        package_id=result[0], sticker_id=result[1]
    )
    text_message = TextSendMessage(text=result[2])  # おみくじ結果
    return [sticker_message, text_message]
```
おみくじを取得する際のキーが引数ではなくランダム生成の整数になりました。
これはボタンで選ぶのがおみくじ自体ではなくなったからですね。
0～5のキーからランダムに取得した後は前回と同様にスタンプとメッセージを返しています。

#### 関数定義「make_button_template」
```Python:main.py
def make_button_template(message_text):
    if message_text == 'やめとく':            # 「ひかない」を選んだ場合
        base_text = 'ほんとにひかない？'
    else:
        base_text = '『' + message_text + '』' + '\n おみくじひかない？'
    message_template = TemplateSendMessage(
        alt_text='おみくじ',
        template=ConfirmTemplate(           # 確認テンプレートの作成
            text = base_text,            
            actions=[
                MessageAction(              #ひとつめのボタン定義
                    text='おみくじ',
                    label='ひく'
                ),
                MessageAction(              #ふたつめのボタン定義
                    text='やめとく',
                    label='ひかない'
                )
            ]
        )
    )
    return message_template
```
引数にメッセージを持たせて、受信メッセージがによって表示するメッセージのテキストを分岐させるようにしました。
「やめとく」というのはテンプレートメッセージで「（おみくじを）ひかない」をタップした場合に返ってくるメッセージなので、その場合は「ほんとにひかない？」と念を押すテキストを設定します。
それ以外の場合は送られてきたメッセージを『』で囲って返したうえで「おみくじひかない？」と尋ねるようになります。

メッセージのテンプレートについて、前回は最大４つのボタンをつけられるボタンテンプレートでしたが、今回はおみくじをひくかひかないかの二択なので確認テンプレートを使用しました。
それぞれのボタンにはPostbackActionではなくMessageActionを設定し、以下のプロパティを設定しています。
* text：返信として送るメッセージテキスト
* label：ボタンに表示するテキスト

実際の画面ではこんな感じで表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/ba063c73-8012-c3af-d5fd-631f80242e75.png" width="50%">


#### 関数定義「main」
最後に実際のBOTが呼び出すメイン関数部分です。
呼び出し元の判定等は前回と同じなので、イベント判定部分のみ抜き出します。

```Python:main.py
    for event in events:
        if isinstance(event, MessageEvent):
            if isinstance(event.message, TextMessage):
                if event.message.text == 'おみくじ':        # おみくじをひく場合
                    line_bot_api.reply_message(
                        event.reply_token,
                        get_omikuji()
                    )
                else:
                    reply_data = []
                    if event.message.text == 'やめとく':    # おみくじを断る場合
                        reply_data.append(
                            StickerSendMessage(
                                package_id=6325, sticker_id=10979923
                            ))
                    
                    reply_data.append(                      # 断る場合も含めておみくじ以外の場合共通
                        make_button_template(event.message.text))
                    line_bot_api.reply_message(
                        event.reply_token,
                        reply_data
                    )
            else:
                continue
```
今回はボタンをタップした場合の動作をMessageActionに変更したので、判定が必要なのはメッセージイベントのみになります。
「おみくじ」または「やめとく」という２つのメッセージの判定だけなのは、確認テンプレートで返されるテキストがその二つだけだからですね。

このコードだと直前におみくじメッセージが表示されているかどうかに関係なく「やめとく」と送れば確認メッセージから「いらない」を選択した場合と同じ動作になってしまいますが、そこは切り分けないことにしました。
これは私が「タップしたボタンによってメッセージを返す場合と返さない場合がある」という差異を嫌った結果なので、「いらない」の動作をPostbackActionにすれば明確に判別して処理を切り分けることは可能です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/45884554-7601-a680-1465-b9e7c5a3a880.jpeg" width="50%">


実際動かすとこんな感じになります。
なんとしてもおみくじをひかせるという執念めいたものがにじんでいる気がしますね。