Python+Flask+Herokuで作るLINE bot (具体的な操作: Postback, carouselなど)

# 想定対象
 - MySQL触れる人
 - Flaskは初めて触る人
 - LINE bot の環境を作ったが具体的なユーザーとのやりとりの仕方がわからない人
 - LINE bot のエコーサーバー的なものから先に進まない人
 - ドキュメントやライブラリのソースコードは<S>読めない</S>読みたくない人

 
LINE bot のpython+flask+herokuでの環境構築は
[FlaskでLINE botを実装し，herokuにdeployするまで](https://qiita.com/suigin/items/0deb9451f45e351acf92)を参照。

# 例えば
最近は仮想通貨のBotが流行っているみたいですが、一昔前のMT4のようにシグナルをスマホに配信したい場合にLINE botではユーザーIDが必要になります。
ユーザーIDを受け取る方法はいくつもありますが、ユーザーが友達追加したタイミングで取得しておくのが自然です。
取得したデータをどこに保存するかといえば、データベースです。herokuのDBでも良いけど、今回は汎用的に使えるように外部のデータベース使用を想定します

# 内容
 - [イベントの受け取り](イベントの受け取り)
 - [メッセージのプッシュ(通知)送信](プッシュ通知)
 - [ユーザーからの入力受け取り](ユーザーからの入力を受け取る(PostbackAction・ButtonsTemplate))
 - [Carousel(カルーセル)の使い方](Carousel(カルーセル)の使い方)

# 準備
先にライブラリをインストールします。

```shell:@terminal
>>> pip install flask
>>> pip install line-bot-sdk
>>> pip install MySQLdb || pip install pymysql
```
※MySQLdbは今回ダメだったのでpymysqlで代用(DBを使わない人は不要)


 - ディレクトリ構造

```shell:@terminal 
>>> tree
.
├── runtime.txt
├── Procfile
├── requirements.txt
├── conf.json
└── app.py
```

 - このファイルたちの中身

1.Pythonのバージョン

```txt:runtime.txt
python-3.6.4
```

2.heroku上で実行されるコマンド

```txt:Procfile
web: python main.py
```

3.Pythonライブラリ(`pip freeze > requirements.txt`で出力していらないものを消す)

```txt:requiremts.txt
line-bot-sdk==1.5.0
future==0.16.0
requests==2.18.4
Flask==0.12.2
click==6.7
itsdangerous==0.24
Jinja2==2.10
MarkupSafe==1.0
Werkzeug==0.14.1

mysqlclient==1.3.12
PyMySQL==0.8.0
```

4.設定ファイル (環境変数を使うならいらない・データベース関係は使う人だけ)

```json:conf.json
{
"CHANNEL_SECRET": "LINE APIのチャネルシークレット",
"CHANNEL_ACCESS_TOKEN": "LINE APIのアクセストークン",

"REMOTE_HOST": "データベースのホストIPもしくはホスト名",
"REMOTE_DB_NAME": "データベースの名前",
"REMOTE_DB_USER": "データベースのユーザ名",
"REMOTE_DB_PASS": "データベースのパスワード",
"REMOTE_DB_TB": "データベースのテーブル名",

"COMMENT": "パーミッションは600とかにしましょう"
}
```

5.メインのファイル

```python:app.py
# -*- coding: utf-8 -*-

import os
import sys
import json
from decimal import Decimal #金融系の計算で丸め誤差を排除するために必要なライブラリ

try:
    import MySQLdb
except:
    import pymysql
    pymysql.install_as_MySQLdb()
    import MySQLdb

from argparse import ArgumentParser

from flask import Flask, request, abort
from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import ( # 使用するモデル(イベント, メッセージ, アクションなど)を列挙
    FollowEvent, UnfollowEvent, MessageEvent, PostbackEvent,
    TextMessage, TextSendMessage, TemplateSendMessage,
    ButtonsTemplate, CarouselTemplate, CarouselColumn,
    PostbackTemplateAction
)

app = Flask(__name__)

ABS_PATH = os.path.dirname(os.path.abspath(sys.argv[0]))
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']
REMOTE_HOST = CONF_DATA['REMOTE_HOST']
REMOTE_DB_NAME = CONF_DATA['REMOTE_DB_NAME']
REMOTE_DB_USER = CONF_DATA['REMOTE_DB_USER']
REMOTE_DB_PASS = CONF_DATA['REMOTE_DB_PASS']
REMOTE_DB_TB = CONF_DATA['REMOTE_DB_TB']

if CHANNEL_SECRET is None:
    print('Specify LINE_CHANNEL_SECRET.')
    sys.exit(1)
if CHANNEL_ACCESS_TOKEN is None:
    print('Specify LINE_CHANNEL_ACCESS_TOKEN.')
    sys.exit(1)

line_bot_api = LineBotApi(CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(CHANNEL_SECRET)

# https://アプリ名.herokuapp.com/test にアクセスしてtest okが表示されればデプロイ自体は成功してる
# flaskは@app.route("/ディレクトリ名")でルーティングする
@app.route("/test")
def 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("Request body: " + body)
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)
    return 'OK'

# メッセージが来た時の反応
@handler.add(MessageEvent, message=TextMessage)
def message_text(event):
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=msg)
    )
```
この状態でLINE botにメッセージを送信すれば、送信した内容をエコーして返してくれます。

# イベントの受け取り
イベントにはFollowEvent、UnfollowEventなどがあります。

今回は「FollowEvent(友達追加・ブロック解除時に起動)を検知」
→「ユーザー情報のDB保存」「メッセージ送信」をします

```python:app.py
# Follow Event
@handler.add(FollowEvent)
def on_follow(event):
    reply_token = event.reply_token
    user_id = event.source.user_id
    profiles = line_bot_api.get_profile(user_id=user_id)
    display_name = profiles.display_name
    picture_url = profiles.picture_url
    status_message = profiles.status_message
    
    # DBへの保存
    try:
        conn = MySQLdb.connect(user=REMOTE_DB_USER, passwd=REMOTE_DB_PASS, host=REMOTE_HOST, db=REMOTE_DB_NAME)
        c = conn.cursor()
        sql = "SELECT `id` FROM`"+REMOTE_DB_TB+"` WHERE `user_id` = '"+user_id+"';"
        c.execute(sql)
        ret = c.fetchall()
        if len(ret) == 0:
            sql = "INSERT INTO `"+REMOTE_DB_TB+"` (`user_id`, `display_name`, `picture_url`, `status_message`, `status`)\
              VALUES ('"+user_id+"', '"+str(display_name)+"', '"+str(picture_url)+"', '"+str(status_message)+"', 1);"
        elif len(ret) == 1:
            sql = "UPDATE `"+REMOTE_DB_TB+"` SET `display_name` = '"+str(display_name)+"', `picture_url` = '"+str(picture_url)+"',\
            `status_message` = '"+str(status_message)+"', `status` = '1' WHERE `user_id` = '"+user_id+"';"
        c.execute(sql)
        conn.commit()
    finally:
        conn.close()
        c.close()

    # メッセージの送信
    line_bot_api.reply_message(
        reply_token=reply_token,
        messages=TextSendMessage(text='メッセージArigato!\nです')
    )

```

※他のイベントを受け取りたい場合は`@handler.add(他のイベント名)`にすればokです

# プッシュ通知

DBにユーザーIDを保存したことで、これを使ってプッシュ通知ができます。
プッシュ通知はイベントをトリガーとしないので`@handler.add`は不要です
任意のタイミングで呼び出して下さい

```python:app.py
def send_push_message(user_id=None, content=None):
    if user_id is None or content is None:
        return False
    line_bot_api.push_message(
        to=user_id,
        messages=TextSendMessage(text='メッセージがPushされたよ！')
    )

```

※実際の開発ではPush通知自体よりもuser_idを取得するところでつまづきがちです。

# ユーザーからの入力を受け取る(PostbackAction・ButtonsTemplate)
LINE APIでのユーザーからの入力は基本的にメッセージ送信です
しかし、メッセージ形式だと自由度が高すぎてユーザーは何を送信して良いのか分かりません。
そこでLINE APIではTemplate(テンプレート)が用意されています。
今回は「ON/OFF」が押せるボタンを作成します

```python:app.py

# ボタンの入力を受け取るPostbackEvent
@handler.add(PostbackEvent)
def on_postback(event):
    reply_token = event.reply_token
    user_id = event.source.user_id
    postback_msg = event.postback.data

    if postback_msg == 'is_show=1':
        line_bot_api.push_message(
            to=user_id,
            messages=TextSendMessage(text='is_showオプションは1だよ！')
        )
    elif postback_msg == 'is_show=0':
        line_bot_api.push_message(
            to=user_id,
            messages=TextSendMessage(text='is_showオプションは0だよ！')
        )

# ボタンを送信する
def send_button(event, user_id):
    message_template = ButtonsTemplate(
      text='BTC_JPYの通知',
      actions=[
          PostbackTemplateAction(
            label='ON',
            data='is_show=1'
          ),
          PostbackTemplateAction(
            label='OFF',
            data='is_show=0'
          )
      ]
    )
    line_bot_api.push_message(
        to=user_id,
        messages=TemplateSendMessage(
            alt_text='button template',
            template=message_template
        )
    )

```

こんな感じにボタンを出力できます（´ω`*）
![IMG_0586のコピー2.png](https://qiita-image-store.s3.amazonaws.com/0/206426/3d8b845e-ca69-e49d-735b-297b9934d9dd.png)

セレクトボックスやチェックボックスの入力受け取りは今のところ未対応な気がします
カスタマイズ可能な要素はTemplateやRichmenuで、ということのようです

# Carousel(カルーセル)の使い方
ここまでが出来ていれば他のメソッドも簡単に実装できると思いますがCarouselが少し厄介だったので紹介します。
carouselとは横にスクロルできるやつです

```python:app.py
def show_carousel(user_id):
    carousel_columns = [
        CarouselColumn(
            text=value,
            title=value+'の通知',
            actions=[
                PostbackTemplateAction(
                    label='ON',
                    data=value+'1'
                ),
                PostbackTemplateAction(
                    label='OFF',
                    data=value+'0'
                )
            ]
        ) for key, value in (
            zip(
                ('取引所', '取引所', '取引所', '取引所', '取引所'),
                ('Binance', 'KuCoin', 'Hupbipro', 'Poloniex', 'Bittrex')
            )
        )
    ]
    message_template = CarouselTemplate(columns=carousel_columns)
    line_bot_api.push_message(
        to=user_id,
        messages=TemplateSendMessage(alt_text='carousel template', template=message_template)
    )

```

内容をうまくいじればこんな感じに出力できますヽ(*･ω･)ﾉ
![IMG_0586.PNG](https://qiita-image-store.s3.amazonaws.com/0/206426/4619bee2-3396-0c71-fafa-0dfd6ffc4756.png)

# あとがき(言い訳)
業務に使ったものを切り貼りしてるのでおかしな部分があるかもしれません。。(´・ω・｀)