Python
AWS
Flask
Splatoon
linebot

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を利用しました。
業務ではDjangoを使うことが多いのではじめはDjangoで作ろうかと思いましたが、よりシンプルなものでよさそうだったのでFlaskにしました。
しかし最終的にもっと簡素なBottleでもよかったかなと思いました。

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

チャンネルを開設するにあたり必要な情報を入力していきます。
プランは「Developer Trial」を選択します。
2018-01-19_11h59_04.png

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

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

2018-01-19_13h02_25.png

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

サーバ側の準備

サーバの初期セットアップ

Amazon Linuxの構築で行ったことは以下の4点です。

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については後述します。

/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のドキュメントルートを指定します。

/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

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

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

ソースコードについてはまずは以下に全体を示します。
メソッド単位で(拙いですが)Messaging APIとの連携なども含めて解説していきます。

ソースコード全体

冒頭の必要なライブラリのインポートやLINEのパラメータなどの設定の説明は見ればわかると思うので割愛します。

/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')

以下のメソッドから。

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

続いてはこちら。

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を作ってみた

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

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というスプラトゥーン2のステージ情報を公開してくださっているサイトから、現在のステージとルールを取得するメソッドです。
単純にHTTPリクエストを送り、返ってきたJSONの結果をパースして本文(body)を生成しています。
※ご利用される際はSpla2 API様の利用方法や注意事項に則った上でご利用ください。

最後です。

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に投げるリクエストを決定しています。

{
  "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が応答してくれます!

Image uploaded from iOS.png

公式アプリでいいじゃん

終わりに

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

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

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

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