軽い気持ちでLINE BOTに手を出してみた～おみくじBOT その１～

あけましておめでとうございます、みやもとです。

早くも新年明けて半月が経ってしまいました。時が経つの早すぎませんかね。
ともあれ、気持ちも新たになんかやりたいな、と気がはやった結果軽率にLINE BOTの作成に手を出したので、その成り行きと内容を記事にしておこうと思います。

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

## おみくじBOTを作ろう～３択編～
今回LINE BOTを作るにあたり、やってみたかったのは
* メッセージに選択肢を含ませる
* 選択肢から何を選んだかで分岐する
* テキストとスタンプを返す

という処理です。


何を作るかちょっと考えた結果、３つの選択肢からひとつ選んでもらい、おみくじを返すBOTになりました。
何かメッセージを受け取ったタイミングで内容に関係なく選択肢を出す押しつけ型になっています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/e983069f-e298-fb31-6d1d-f02b4377e4d7.png" width="50%">


## コーディング前の準備部分
LINE BOTについてはQiitaやZenn、noteあたりを探すとかなり記事が豊富です。
私はこちらの記事を参考にしました。

https://note.com/nekoniii3/n/n0a01ec6731b5

https://qiita.com/spre55/items/da2ded18ac4652abb936

上記記事を参考に
* LINE Developersの登録、LINE Messaging APIチャネルの作成
* Google Cloud Platformの登録、Google Cloud Functionsの作成
* requirements.txtの作成

までを実施します。

## 処理本体を作る
先の記事を参考に下の画面までたどり着いたらコードを書きます。
![Funciton作成手順01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/89667de4-4a43-4795-2564-3ddbfe7438f6.jpeg)

準備部分の項でご紹介したふたつ目の記事に出てくるオウム返しBOTのソースと、

https://qiita.com/shimayu22/items/c599a94dfa39c6466dfa

こちらの記事のソースを参考につぎはぎしてあれこれいじくってみました。
（ちょっと長いので折りたたんであります。）
<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, PostbackEvent, TextMessage, TextSendMessage, 
    StickerMessage, StickerSendMessage,
    TemplateSendMessage,ButtonsTemplate,PostbackAction
)

omikuji = {'0':[6325,10979924,'大吉！良いことたくさんありますように'],
            '1':[11537,52002754,'中吉。いつも通りがいちばん'],
            '2':[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):
                line_bot_api.reply_message(
                    event.reply_token,
                    make_button_template()
                )
            else:
                continue
        if isinstance(event, PostbackEvent):
            line_bot_api.reply_message(
                event.reply_token,
                get_omikuji(event.postback.data)
            )
        else:
            continue

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


def make_button_template():
    omikuji_key = ['0','1','2']
    message_template = TemplateSendMessage(
        alt_text='おみくじ',
        template=ButtonsTemplate(
            text='どれにする？',
            title='おみくじ',
            actions=[
                PostbackAction(
                    data=omikuji_key.pop(random.randint(0,2)),
                    label='これ'
                ),
                PostbackAction(
                    data=omikuji_key.pop(random.randint(0,1)),
                    label='それ'
                ),
                PostbackAction(
                    data=omikuji_key.pop(0),
                    label='あれ'
                )
            ]
        )
    )
    return message_template

def get_omikuji(key):
    result = omikuji[key]
    sticker_message = StickerSendMessage(
        package_id=result[0], sticker_id=result[1]
    )
    text_message = TextSendMessage(text=result[2])
    return [sticker_message, text_message]
```
</details>

先に挙げた参照記事の内容と重複するところもありますが、以下少し内容の解説をしていきます。

#### 冒頭のimport群
必要なモジュールのインポートをしています。特に「from linebot～」の部分は公式提供のline-bot-sdkからのインポート部分ですね。

#### dict定義「omikuji」
おみくじの結果をdictに定義しています。リスト内は順にLINEスタンプのパッケージID、LINEスタンプのステッカーID、おみくじの結果文言です。
スタンプ関連の各IDについては公式ドキュメントをご参照ください。

https://developers.line.biz/ja/docs/messaging-api/sticker-list/

ここからはコード内容詳細になるのでもう少し小分けに書きますね。

#### 関数定義「main」
LINE BOTで実際に呼び出す関数になります。この項の最初の画像に出てくる「エントリポイント」の名前で定義してください。

まず注意するポイントとしてはこちらの部分
```python:main.py
    channel_secret = os.environ.get('LINE_CHANNEL_SECRET')
    channel_access_token = os.environ.get('LINE_CHANNEL_ACCESS_TOKEN')
```
事前準備のところで出てきた環境変数を取得しています。
LINE Messaging APIで準備したチャネルシークレットとアクセストークンを呼び出している箇所です。
後から定義する場合はコード内で書いた変数名と不一致にならないようにご注意ください。

アクセス元がLINEアカウントかどうかのチェックの後からが今回のBOTのメイン部分。
受信したメッセージに対する応答判定の処理が入ります。
```Python:main.py
    for event in events:
        if isinstance(event, MessageEvent):             # メッセージイベント受信
            if isinstance(event.message, TextMessage):  # テキストメッセージの場合
                line_bot_api.reply_message(
                    event.reply_token,
                    make_button_template()
                )
            else:
                continue
        if isinstance(event, PostbackEvent):            # ポストバックイベント受信
            line_bot_api.reply_message(
                event.reply_token,
                get_omikuji(event.postback.data)
            )
        else:
            continue
```
発生したイベントを受け取って、それがどんなイベントかで応答するかどうか判定しています。今回は何かしらメッセージを受け取ったらおみくじを出す、おみくじの３択ボタンのうちひとつ選ばれたタイミングで結果を返す、というもの。
テキストメッセージが送られてきたらおみくじを選ばせるメッセージを返し、ボタンをタップしたことで発生するポストバックイベントであればおみくじを返す、という動作をしています。
おみくじを選ばせるメッセージを作る部分とおみくじの結果を返す部分は後述します。

#### 関数定義「make_button_template」
```Python:main.py
def make_button_template():
    omikuji_key = ['0','1','2']                  # おみくじ取得用のキーリスト
    message_template = TemplateSendMessage(      # テンプレートメッセージ
        alt_text='おみくじ',
        template=ButtonsTemplate(                # ボタンテンプレート
            text='どれにする？',
            title='おみくじ',
            actions=[                            # メッセージで表示する各ボタンの定義
                PostbackAction(                  # 1つめのボタン動作
                    data=omikuji_key.pop(random.randint(0,2)),
                    label='これ'
                ),
                PostbackAction(                  # 2つめのボタン動作
                    data=omikuji_key.pop(random.randint(0,1)),
                    label='それ'
                ),
                PostbackAction(                  # 3つめのボタン動作
                    data=omikuji_key.pop(0),
                    label='あれ'
                )
            ]
        )
    )
    return message_template                      # テンプレートメッセージを返す
```
最初のリストはおみくじの結果を格納したdictから結果を取得するためのキーです。
わざわざ文字列型でひとつずつキーをリスト内に格納しているのは、
* PostbackActionで戻す値を数値型に変換せずにキーとして使いたい
* 3つのボタンそれぞれに重複しないように結果をつけたい

という理由です。とくに後者、整数値をランダム発生させるとめちゃくちゃ結果が偏りそうなので……。3択全部凶のおみくじとか発生しかねない。

で、おみくじ結果のキーリスト定義の後にあるのがボタンテンプレートのメッセージ作成部分です。
1つめから3つめまでのボタンがタップされた場合に返すアクションを定義していて、今回はPostbackAction（画面上は動作が見えないアクション）を使います。
ちなみにここでMessageActionを使うと、ボタンをタップした際にあらかじめ定義しておいた文字列が自分のメッセージとして送信されます。

PostbackAction内のプロパティはそれぞれ
* data：ボタンがタップされた際に返す文字列
* label：ボタンに表示するラベル文字

となります。dataのところでキーリストの内容をランダムに取り出して設定することで、「1番目のボタンを押せば必ず大吉」みたいな固定にならないようにしています（それでも数が少ないので、連続して同じ場所に同じ結果が割り当てられることも多いですが……）

#### 関数定義「get_omikuji」
```Python:main.py
def get_omikuji(key):
    result = omikuji[key]                            # おみくじの結果を取得
    sticker_message = StickerSendMessage(
        package_id=result[0], sticker_id=result[1]   # 送信するスタンプの定義
    )
    text_message = TextSendMessage(text=result[2])   # 送信するテキストメッセージの定義
    return [sticker_message, text_message]           # スタンプとテキストをリストにして返す
```
おみくじの結果として返信するスタンプとメッセージを設定する箇所です。
まずメッセージの内容を設定するため、引数で受け取ったキーでdictからおみくじの結果を取得します。
結果を取得したらそれぞれStickerSendMessageとTextSendMessageでスタンプとテキストのメッセージを作って、複数メッセージを返すためにリストに入れた形で呼び出し元に返しています。

そしてこれをデプロイして、エラーが出てないのを確認したら動作確認。
適当にテキストメッセージを送るとこんな感じでおみくじをくれます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477580/79fd7f92-0c9f-aeef-0185-1d33dab9d533.jpeg" width="50%">

いまいちよろしくないおみくじ結果ですが動いたので良しとします。
オフラインで引いたおみくじはちゃんと大吉だったので気にしません。

#### LINEBOTの反応がない場合
Google Cloud Functionのログにエラーが出力されるので、そちらでエラー内容を確認して適宜対応してください。

ちなみに私がひっかかったのは
* コード内の環境変数が実際の定義と一致しない
* 環境変数に定義したチャネルシークレットかアクセストークンが不完全
（コピペ間違いとか途中で切れたとか）
* コード内で定義した変数名と実際に変数を使う箇所に書いた変数名が一致しない
* linebotからインポートしてないクラスを使っている

ぐらいです。

## 感想と改善点
とりあえずちゃんと動くところを確認できたのと、スタンプ付きで結果を返したことで思ったよりかわいい感じに仕上がったのでまずは満足です。

今回は「メッセージに選択肢を含ませる」という機能を優先したため結果が3種類しかない[^1]おみくじになりましたが、ボタンの機能を「おみくじをひくorひかない」の2種類にして「ひく」を選んだ時にランダムで返す形にすれば結果の種類は増やせそうです。
また、ボタンをタップした場合のアクションをポストバックにしたため、BOTからの応答が遅いとタップしたかどうかわかりにくくなってしまいました。これはメッセージアクションにすることで改善できそうです。

次回はこのへんを改良したおみくじBOT改良版を記事にしたいと思います。
コードの解説だけになるので今回よりは手軽に読める分量の記事になるはずです。

[^1]:ボタンテンプレートで作れるボタンは最大4個

## 参考：公式ドキュメント
LINE BOTを作るのに使用したLINE Messaging APIのリファレンスとGoogle Cloud Functionsのドキュメントへのリンクを置いて終わりにします。

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

https://cloud.google.com/functions/docs?hl=ja