LINE Botでスプラトゥーン2のステージを答えてくれるBotを作ってみた

# はじめに
最近チャットボットが人気ですね。
2016年12月頃に私もLINEでチャットボットを作ってみようと思ったのですが、まだ文献が少なく当時の私ではなかなか難しかったのですが、最近は色んな人がQiitaを始めと色々記事を書いてくれているのでハードルが下がったかな？と思い再チャレンジしてみました。
そんなレベルの人間が作ったLINE Botです。

# 最終的な環境
今回のBot制作にあたり、以下の環境、サービスを利用しました。

- Python3
- Flask
- AWS(Amazon Linux)
- Let's Encrypt

# 選定理由
## Python
- 自分がよく使ってる言語だから
- Bot製作とかに向いてるから（多分）

## Flask
LINE Botを開発するうえでWebhookを受け取れるWebサーバが必要なので、Python製のWebフレームワークである[Flask](http://flask.pocoo.org/)を利用しました。
業務ではDjangoを使うことが多いのではじめはDjangoで作ろうかと思いましたが、よりシンプルなものでよさそうだったのでFlaskにしました。
しかし最終的にもっと簡素な[Bottle](https://bottlepy.org/docs/dev/)でもよかったかなと思いました。

## AWS(Amazon Linux)
Webサーバを立てる上で最初はherokuにデプロイを考えていたのですが、デプロイに挫折したので…。
Amazon LinuxにFlaskをインストールして使っています。インスタンスタイプは最小。

## Let's Encrypt
LINEのMessaging APIを利用するにはhttpsリクエストを受けられるサーバが必要です。
herokuであればその辺りも全く考慮する必要がないのですが、前述の通りherokuでのデプロイに挫折したのでAmazon Linuxをhttps化する必要がありました。
そこで無料でSSL証明書を発行できるLet's Encryptを利用しました。

書いていて思いましたが、herokuを使わない場合はドメインも必要ですね。
※私は個人でドメインを持っているのでそれを利用しました。

前置きはこれくらいにしてここから本題です。

# LINE側の準備
## LINEアカウントの用意
LINEのアカウントを利用します。
個人の情報が公開されることはないので普段使っている個人のLINEアカウントでも問題ありません。

## LINE Developersへの登録
https://developers.line.me/ja/ にアクセスして「Messaging API（ボット）を始める」をクリックします。
![2018-01-19_11h54_58.png](https://qiita-image-store.s3.amazonaws.com/0/73844/9ede35e8-e108-634e-e62e-ed8f67a0cc56.png)



チャンネルを開設するにあたり必要な情報を入力していきます。
プランは「Developer Trial」を選択します。
![2018-01-19_11h59_04.png](https://qiita-image-store.s3.amazonaws.com/0/73844/f4ab7fc0-2a39-2072-c348-4609bfda4120.png)



チャンネルが開設出来たら必要な情報をメモしたりWebhookの設定をします。
以下の項目を利用します。

- Channel Secret
- アクセストークン
- Webhook送信：利用する
- Webhook URL：httpsのURLを指定
- 自動応答メッセージ：利用しない

![2018-01-19_13h02_25.png](https://qiita-image-store.s3.amazonaws.com/0/73844/d99eb00e-3e6e-9b90-b880-fd8d1301d782.png)

これでLINE側の準備はOKです。

# サーバ側の準備
## サーバの初期セットアップ
Amazon Linuxの構築で行ったことは以下の4点です。

- **Apache2.4系のインストール**
    - デフォルトの2.2系でもよかったんですが、とりあえず新しいの使っていこうの精神
    - 【参考記事】[Amazon LinuxのApache 2.4にSSL証明書を設定してみた](https://dev.classmethod.jp/cloud/aws/amazonlinux-apache24-ssl/)
- **Python3.6系のインストール**
    - Amazon LinuxではPython2.7がデフォルトのため
    - 【参考記事】[Amazon Linux (EC2)上でPython3とDjangoをインストールして、Webサーバを動かす](https://qiita.com/KeijiYONEDA/items/f9cf37cfc359aa893797)
- **Flaskのインストール**
    - 【参考記事】[ウェブアプリケーションフレームワーク Flask を使ってみる](https://qiita.com/ynakayama/items/2cc0b1d3cf1a2da612e4)
    - 【参考記事】[PythonのFlaskを初めて触ってから３０分で本番環境にデプロイ+ HTTPS通信まで完成](https://qiita.com/yoshizaki_kkgk/items/3cd785b4a670deec0685)
    - 【参考記事】[AWS(EC2) で Python3 + Flask を Apache で動かす（mod_wsgi?）](https://qiita.com/yabekenzo/items/346142e29db36b77e42d)
- **Let's EncryptによるSSL証明書の発行と設定**
    - [公式ドキュメント](https://letsencrypt.jp/usage/install-certbot.html#CentOS6)に従ってインストールしました。

## Flaskの設定
Flaskを動作させるための設定をしていきます。

|ファイル名|説明|
|:----|:----|
|/var/www/flask/app.py|プログラム本体|
|/var/www/flask/adapter.wsgi|FlaskとApacheを繋げるためのファイル|
|/etc/httpd/conf.d/flask.conf|FlaskをApacheから実行するためのApache側の設定ファイル|

`app.py`については後述します。

```python:/var/www/flask/adapter.wsgi	
# coding: utf-8
import sys
sys.path.insert(0, '/var/www/flask')

from app import app as application
```

wsgiファイルの設定をします。
Flaskのドキュメントルートを指定します。

```conf:/etc/httpd/conf.d/flask.conf	
LoadModule wsgi_module /usr/local/lib64/python3.6/site-packages/mod_wsgi/server/mod_wsgi-py36.cpython-36m-x86_64-linux-gnu.so

<VirtualHost *:443>
 SSLEngine on
 SSLProtocol all -SSLv2
 SSLCertificateFile /etc/letsencrypt/live/YOUR_DOMAIN/cert.pem
 SSLCertificateKeyFile /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem
 SSLCertificateChainFile /etc/letsencrypt/live/YOUR_DOMAIN/chain.pem
 DocumentRoot /var/www/flask
 ServerName YOUR_DOMAIN:443
 CustomLog /var/www/flask/access.log common
 ErrorLog  /var/www/flask/error.log
 AddDefaultCharset UTF-8
 WSGIScriptAlias / /var/www/flask/adapter.wsgi
 <Directory "/var/www/flask/">
   Options -Indexes +FollowSymLinks +ExecCGI
 </Directory>
</VirtualHost>
```

Apacheの設定ファイルとしてのFlaskの設定です。
443ポートに対してのアクセスの設定で、Let's Encryptで生成した証明書や秘密鍵へのパスを指定します。
また、 `WSGIScriptAlias / /var/www/flask/adapter.wsgi` を記載してFlaskを動作させることを指示します。

## Bot本体の製作
本題の本題です。まずは何よりも公式ドキュメントを読みましょう。
かなり充実していてやりたいことは大抵ここを読めば何とかなりました。
https://developers.line.me/ja/docs/messaging-api/building-bot/
https://developers.line.me/ja/docs/messaging-api/reference/

## 構成
まず作るものの構成をイメージします。
まぁ今回の場合そんなに複雑ではありませんが、処理の流れをイメージすることは重要です。
![2018-03-13_10h41_34.png](https://qiita-image-store.s3.amazonaws.com/0/73844/4782690b-acbf-b723-4f9e-151f911fb241.png)

1. LINEアプリから特定のワードでBotに話しかける
2. Messaging APIからサーバにWebhookが送信される
3. サーバからスプラトゥーン2のステージ情報を公開しているAPIにアクセス
4. 1.で送信したワードから現在のステージなどの情報を返す
5. 受け取った情報を整形してMessaging APIに渡す
6. LINEで表示させる

とまぁこんなような流れでしょうかね。

ソースコードについてはまずは以下に全体を示します。
メソッド単位で（拙いですが）Messaging APIとの連携なども含めて解説していきます。
## ソースコード全体
冒頭の必要なライブラリのインポートやLINEのパラメータなどの設定の説明は見ればわかると思うので割愛します。

```python:/var/www/flask/app.py
# coding: utf-8
from flask import Flask
from flask import request

import requests
import json
import re

import hmac
import hashlib
import base64

"""LINE BOT用各種パラメータ"""
REPLY_ENDPOINT = 'https://api.line.me/v2/bot/message/reply'
CHANNEL_SECRET = 'YOUR_CHANNEL_SECRET'
ACCESS_TOKEN = 'YOUR_ACCESS_TOKEN'
HEADER = {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + ACCESS_TOKEN
}

"""スプラトゥーン2のステージ情報API"""
URL = "https://spla2.yuu26.com/"


def post(reply_token, messages):
    """
    LINEに投稿する本文を生成してセットする
    """
    payload = {
        "replyToken": reply_token,
        "messages": messages,
    }
    requests.post(REPLY_ENDPOINT, headers=HEADER, data=json.dumps(payload))


def validation_signature(signature, body):
    """
    署名の検証
    """
    if isinstance(body, str) != True:
        body = body.encode()

    gen_signature = hmac.new(CHANNEL_SECRET.encode(), body.encode(), hashlib.sha256).digest()
    gen_signature = base64.b64encode(gen_signature).decode()

    if gen_signature == signature:
        return True
    else:
        return False


def get_rule(query):
    """
    ステージ情報APIから現在のルールとステージを取得してBOTが返答する本文を生成する
    """
    response = requests.get(URL + query + '/now')
    if response.status_code == 200:
        # JSONを取得
        data = response.json()

        results = data['result']
        body = ""

        for result in results:
            body = result['rule_ex']['name'] + "\n"
            for map_data in result['maps_ex']:
                body = body + map_data['name'] + "\n"

        body = body + "です。ファイト～！"

        return body

app = Flask(__name__)


@app.route("/callback", methods=['POST'])
def callback():
    """
    LINEからのPOSTを受ける
    """
    if validation_signature(request.headers.get('X-Line-Signature', ''), request.data.decode()) == False:
        return 'Error: Signature', 403
    app.logger.info('CALLBACK: {}'.format(request.data))

    for event in request.json['events']:
        if event['type'] == 'message':
            if event['message']['text'] == "レギュラーマッチ" or event['message']['text'] == "ナワバリ":
                query = 'regular'
                body = "現在のレギュラーマッチは\n" + get_rule(query)

            elif event['message']['text'] == "ガチマッチ" or event['message']['text'] == "ガチマ":
                query = 'gachi'
                body = "現在のガチマッチは\n" + get_rule(query)

            elif event['message']['text'] == "リーグマッチ" or event['message']['text'] == "リグマ":
                query = 'league'
                body = "現在のリーグマッチは\n" + get_rule(query)

            else:
                body = "「レギュラーマッチ」\n「ガチマッチ」\n「リーグマッチ」\nのいずれかの単語を送信してください"

            messages = [
                {
                    'type': 'text',
                    'text': body,
                }
            ]
            post(event['replyToken'], messages)
    return '{}', 200


if __name__ == "__main__":
    app.run(host='0.0.0.0')
```


以下のメソッドから。

```python
def post(reply_token, messages):
    """
    LINEに投稿する本文を生成してセットする
    """
    payload = {
        "replyToken": reply_token,
        "messages": messages,
    }
    requests.post(REPLY_ENDPOINT, headers=HEADER, data=json.dumps(payload))
```

最終的に生成したメッセージをセットしてMessaging APIに返すメソッドです。
```REPLY_ENDPOINT``` に設定したAPIに対してデータを送ることでLINEに投稿することが出来ます。
https://developers.line.me/ja/docs/messaging-api/reference/#anchor-36ddabf319927434df30f0a74e21ad2cc69f0013

続いてはこちら。

```python
def validation_signature(signature, body):
    """
    署名の検証
    """
    if isinstance(body, str) != True:
        body = body.encode()

    gen_signature = hmac.new(CHANNEL_SECRET.encode(), body.encode(), hashlib.sha256).digest()
    gen_signature = base64.b64encode(gen_signature).decode()

    if gen_signature == signature:
        return True
    else:
        return False
```

Messaging APIを利用する上で、そのリクエストがLINEプラットフォームから送信されたことを確認する必要がある、と公式ドキュメントに書かれているため、その検証を行うメソッドです。
https://developers.line.me/ja/docs/messaging-api/reference/#anchor-b408ce0662937dae3ed576f3fbb12da6d7024ad9
>X-Line-Signatureリクエストヘッダーに含まれる署名を検証して、リクエストがLINEプラットフォームから送信されたことを確認する必要があります。
>
>検証の手順は以下のとおりです。
>
>チャネルシークレットを秘密鍵として、HMAC-SHA256アルゴリズムを使用してリクエストボディのダイジェスト値を取得します。
>ダイジェスト値をBase64エンコードした値とリクエストヘッダーにある署名が一致することを確認します。

【参考URL】[1時間でLINE BOTを作ってみた](https://blog.ultica.jp/archives/6)

このメソッドの引数として指定している `sigunature`はHTTPリクエストの`X-Line-Signature`にセットされています。
この値と、`gen_signature`が等しいかをチェックしています。


```python
def get_rule(query):
    """
    ステージ情報APIから現在のルールとステージを取得してBOTが返答する本文を生成する
    """
    response = requests.get(URL + query + '/now')
    if response.status_code == 200:
        # JSONを取得
        data = response.json()

        results = data['result']
        body = ""

        for result in results:
            body = result['rule_ex']['name'] + "\n"
            for map_data in result['maps_ex']:
                body = body + map_data['name'] + "\n"

        body = body + "です。ファイト～！"

        return body
```

このメソッドは[Spla2 API](https://spla2.yuu26.com/)というスプラトゥーン2のステージ情報を公開してくださっているサイトから、現在のステージとルールを取得するメソッドです。
単純にHTTPリクエストを送り、返ってきたJSONの結果をパースして本文（`body`）を生成しています。
**※ご利用される際はSpla2 API様の利用方法や注意事項に則った上でご利用ください。**

最後です。

```python
app = Flask(__name__)


@app.route("/callback", methods=['POST'])
def callback():
    """
    LINEからのPOSTを受ける
    """
    if validation_signature(request.headers.get('X-Line-Signature', ''), request.data.decode()) == False:
        return 'Error: Signature', 403
    app.logger.info('CALLBACK: {}'.format(request.data))

    for event in request.json['events']:
        if event['type'] == 'message':
            if event['message']['text'] == "レギュラーマッチ" or event['message']['text'] == "ナワバリ":
                query = 'regular'
                body = "現在のレギュラーマッチは\n" + get_rule(query)

            elif event['message']['text'] == "ガチマッチ" or event['message']['text'] == "ガチマ":
                query = 'gachi'
                body = "現在のガチマッチは\n" + get_rule(query)

            elif event['message']['text'] == "リーグマッチ" or event['message']['text'] == "リグマ":
                query = 'league'
                body = "現在のリーグマッチは\n" + get_rule(query)

            else:
                body = "「レギュラーマッチ」\n「ガチマッチ」\n「リーグマッチ」\nのいずれかの単語を送信してください"

            messages = [
                {
                    'type': 'text',
                    'text': body,
                }
            ]
            post(event['replyToken'], messages)
    return '{}', 200


if __name__ == "__main__":
    app.run(host='0.0.0.0')
```

最後にこちらは、Flaskを`app`という名前で起動させ、各種メソッドを呼び出します。
LINEからのPOSTリクエストは`/callback`というパスで受け取る必要があるので、`@app.route("/callback", methods=['POST'])`と記載しています。

公式ドキュメントによるとWebhookイベントオブジェクトは以下の形式になっています。
よって、以下の例で言うところの★マークを付けた箇所にLINEに投稿した本文が格納されることになります。
それをfor文で取得して、入力された文字列によってSpla2 APIに投げるリクエストを決定しています。

```json
{
  "events": [
    {
      "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
      "type": "message",
      "timestamp": 1462629479859,
      "source": {
        "type": "user",
        "userId": "U4af4980629..."
      },
      "message": {
        "id": "325708",
        "type": "text",
        "text": "Hello, world"  ★
      }
    },
    {
      "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
      "type": "follow",
      "timestamp": 1462629479859,
      "source": {
        "type": "user",
        "userId": "U4af4980629..."
      }
    }
  ]
}
```


そして返ってきた本文（`body`）を上記と同じ形式の`messages`にセットして`replyToken`を付けてpostメソッドを呼び出し、LINEに応答させています。

https://developers.line.me/ja/docs/messaging-api/reference/#anchor-6640e4a392930e46edb1c15c1d6817ee2356f75e

# 実際の動作
これでめでたく実装できましたので早速LINEから話しかけてみるとこんな感じでBotが応答してくれます！

<img width="375" alt="Image uploaded from iOS.png" src="https://qiita-image-store.s3.amazonaws.com/0/73844/1846e2c9-dd88-e7a6-ddd8-fda038b257e9.png">

~~公式アプリでいいじゃん~~

# 終わりに
今回は自分のPythonのコーディングスキルの向上と、外部APIと連携する際の基本（下記）を理解することが目的でした。

- 外部APIにGETリクエストを送る
- JSON形式の応答データをパースする
- パースしたデータをいい感じに処理する

結果的にherokuを使わなかった（使えなかった）ことでWebサーバも自分で構築したり、割と包括的に勉強できたのでLINEのBot開発はコードを書いたり処理の流れを勉強するのに結構有用なのではないでしょうか。

こんな拙い記事で参考文献に頼りまくりの記事ですが、どなたかのLINE Bot開発に役立てば幸いです。
ちなみにスプラトゥーン2のフレンドも募集しています！