linebotの作成をしてみた(実装編)

準備編：https://qiita.com/maihamada/items/2c4d5b4f6ae82db45970
準備が終わったので、実際に実装をしていく。

#(1)今回作りたいシステム
「執事診断」という入力がきたら、「執事診断」を行う。質問は以下のフロー。
<img width="1053" alt="スクリーンショット 2020-05-09 19.09.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631554/ed59a483-1571-d939-215c-2e0601d60d25.png">
完成した画像はこんな感じ。
![S__3842059.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631554/5ee2fd04-4ac9-6dfc-f57e-3e92b912c217.jpeg)

ボタンイベントの返答によってボタンイベントを返したり、メッセージイベントを返したり…なイメージ。

#(2)フォルダの作成
作業ディレクトリが作成されているので、そちらで作業していく。
以下のようなファイル構成で、フォルダの作成を行う。

```
situji-bot/ [作業ディレクトリ(なんでもいいです)]
 ├app.py 
 ├conf.json
 ├Procfile
 ├requirements.txt
 ├runtime.txt
 └template/
  └button_event.py
```

#(3)ファイルの説明
## app.py
アプリケーションのメインのものを書いている。

```python

import os
import sys
import json

# flaskのライブラリをimport
from flask import Flask, request, abort
# linebotのライブラリをimport
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import (
    MessageEvent, PostbackEvent, TextMessage, TextSendMessage
)

# 自作のライブラリをimport
from template import button_event

app = Flask(__name__)

# 設定ファイルの読み込み
ABS_PATH = os.path.dirname(os.path.abspath(__file__))
with open(ABS_PATH+'/conf.json', 'r') as f:
    CONF_DATA = json.load(f)
CHANNEL_SECRET = CONF_DATA['CHANNEL_SECRET']
CHANNEL_ACCESS_TOKEN = CONF_DATA['CHANNEL_ACCESS_TOKEN']

# クライアントライブラリのインスタンスを作成
line_bot_api = LineBotApi(CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(CHANNEL_SECRET)

# テスト用のコード
@app.route("/")
def test():
    app.logger.info("test")
    return('test OK')

# LINE APIのコード
@app.route("/callback", methods=['POST'])
def callback():
    signature = request.headers['X-Line-Signature']
    body = request.get_data(as_text=True)
    app.logger.info(f"Request body: {body}")
    try:
        handler.handle(body, signature)
    except InvalidSignatureError as e:
        print(e)
        abort(400)
    return 'OK'

# メッセージが来た時の反応
@handler.add(MessageEvent, message=TextMessage)
def message_text(event):
    message_text = event.message.text
    app.logger.info(message_text)

    if message_text == '執事診断':
        line_bot_api.reply_message(
            event.reply_token,
            button_event.SitujiSindan().question_a()
        )
    else:
        msg = '申し訳ありませんが、現在対応しておりません。'
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text=msg)
        )

# 値が帰ってきたときの反応
@handler.add(PostbackEvent)
def on_postback(event):
    reply_token = event.reply_token
    user_id = event.source.user_id

    # postback_msg : method名を文字列で
    postback_msg = event.postback.data
    # situji_sindan : classオブジェクト
    situji_sindan = button_event.SitujiSindan()
    # クラスオブジェクトと文字列で取得したメソッド名から、メソッドオブジェクトを作成
    question = getattr(situji_sindan, postback_msg)
    # 次の質問投げつける
    line_bot_api.reply_message(
        event.reply_token,
        question()
    )

if __name__ == "__main__":
    app.run(debug=True)
```

## conf.json
このファイルを見たら、どの設定を使用しているかわかるように、分離させている。
今は、line apiの設定しかしていないが、将来的にDBの設定とかも入れていきたい。
準備編で取得した、チャネルシークレット・チャネルアクセストークンはここで使う。

```json
{
"CHANNEL_SECRET": "[チャネルシークレット]",
"CHANNEL_ACCESS_TOKEN": "[チャネルアクセストークン]"
}
```

## Procfile
プロセスタイプなどの設定ファイル。
webサーバーとflaskフレームワークをつなぐ為に、gunicorn(WSGIサーバー)が必要となるそう。
書き方は以下の通りで、最後の--log-file -はログを出したい場合のみ記述する。
[process type]: [command] [api_name] : app --log-file -

```
web: gunicorn app:app --log-file -
```

## requirements.txt
使用したライブラリとバージョンを書く。

```text
Flask==1.1.2
gunicorn==20.0.4
line-bot-sdk==1.16.0
```
## runtime.txt
使用したpythonのバージョンを書く。

```text
python-3.8.1
```
## template/button_event.py 
今回、ボタンイベントを大量に作成したので、別モジュールにしてみた。

```python
from linebot.models import (
    PostbackEvent, TextSendMessage, TemplateSendMessage,
    ButtonsTemplate, PostbackTemplateAction
)

class SitujiSindan:
    def question_a(self):
        button_template = TemplateSendMessage(
            alt_text="執事診断",
            template=ButtonsTemplate(
                title="質問1",
                text="あなたがピンチに陥った時",
                actions=[
                  PostbackTemplateAction(
                    label='頭脳であなたを助けてくれる',
                    data='question_b'
                  ),
                  PostbackTemplateAction(
                    label='体を張ってあなたを助けてくれる',
                    data='question_c'
                  )
                ]
            )
        )
        return button_template

    def question_b(self):
        button_template = TemplateSendMessage(
            alt_text="執事診断",
            template=ButtonsTemplate(
                title="質問２",
                text="あなたが大変なことをやらかした時には…",
                actions=[
                  PostbackTemplateAction(
                    label='優しくフォローしてほしい',
                    data='answer_d'
                  ),
                  PostbackTemplateAction(
                    label='ちゃんと叱ってほしい',
                    data='answer_e'
                  )
                ]
            )
        )
        return button_template

    def question_c(self):
        button_template = TemplateSendMessage(
            alt_text="執事診断",
            template=ButtonsTemplate(
                title="質問2",
                text="どちらの方がタイプ？",
                actions=[
                  PostbackTemplateAction(
                    label='無愛想',
                    data='answer_f'
                  ),
                  PostbackTemplateAction(
                    label='フレンドリー',
                    data='answer_g'
                  )
                ]
            )
        )
        return button_template

    def answer_d(self):
        msg = '診断結果は「正統派執事」です。とにかく、お嬢様の為に尽くしています'
        return TextSendMessage(text=msg)

    def answer_e(self):
        msg = '診断結果は「お兄様系執事」です。お嬢様のことが大好きで、いつもお嬢様のことを気にしています。'
        return TextSendMessage(text=msg)

    def answer_f(self):
        msg = '診断結果は「あくまで執事」です。少し無愛想ですが、全力でお嬢様を守っています。口癖は、「あくまで執事ですから」です。'
        return TextSendMessage(text=msg)

    def answer_g(self):
        msg = '診断結果は「弟系執事」です。お嬢様のことが大好きで、弟のように接してきます。普段は頼りないですが、ピンチの時は全力で守ります。'
        return TextSendMessage(text=msg)
```

#(4)ローカルでテストを行う。
```
$ python app.py
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 897-298-226
```
chromeなどで、以下のurlを叩き、「test OK」の文字が出たらOK。
http://127.0.0.1:5000/
※Warnigは無視してOK。

#(5)デプロイする
Gitにpushしたら自動でデプロイされるように設定をしたので、pushを行う。
ステージングする。

```
$ git add .
```
ステージング状況を確認。

```
$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:   Procfile
	new file:   app.py
	new file:   conf.json
	new file:   requirements.txt
	new file:   runtime.txt
	new file:   template/button_event.py
```
commit & pushを行う。

```
$ git commit -m 'first push'
[master 04a377d] first push
 6 files changed, 198 insertions(+)
 create mode 100644 Procfile
 create mode 100644 app.py
 create mode 100644 conf.json
 create mode 100644 requirements.txt
 create mode 100644 runtime.txt
 create mode 100644 template/button_event.py
$ git push origin master
Enumerating objects: 10, done.
Counting objects: 100% (10/10), done.
Delta compression using up to 4 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (9/9), 2.80 KiB | 1.40 MiB/s, done.
Total 9 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:maihamada/situji-bot.git
   fc7af07..04a377d  master -> master
```

#(6)LINE Developersの設定をする
webhockの設定を行う。(Messaging API設定のところにある)
<img width="1152" alt="スクリーンショット 2020-05-09 19.52.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631554/d71603ed-6b98-aebd-d433-9fc2d46c532a.png">

応答設定を好きなようにアレンジする。
<img width="783" alt="スクリーンショット 2020-05-09 19.53.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631554/377c809d-602b-4396-fdf9-4a732fb24d5c.png">


以上で、LINE BOTの作成は完了。

# 感想
思ったより実装は簡単だった。
今後は、文字列による返答パターンを増やしたり、この診断の結果を使用して執事の口調を変更させるなどしていきたい。