This page looks best with JavaScript enabled

Responderを用いたLINEbot作成

 ·   ·  ☕ 4 min read  ·  ✍️ Reeve
    🏷️

Pythonの次世代Webフレームワークとして期待されているResponderを利用してLINEbotを作成してみた。
若干躓くポイントがあり、日本語での情報が少なかったので記事にまとめてみた。

この記事の内容

  • Responderを使ってユーザーが入力した文章をオウム返しするLINEbotの作成

そもそもResponderとは?

PythonのWebフレームワークとしてはDjangoやFlaskなどが有名だが、Responderはこのようなフレームワークの良い部分を取り入れた次世代Webフレームワーク。
私自身は今までFlaskを使ってきたが、Flaskでは非同期処理を実装するのが困難だった。
一方、Responderではasync/awaitを用いて簡単に非同期処理を実装できる。
今後使用する人が増えていくことが期待されている、非常にスマートなWebフレームワークである。

Responderの導入

ResponderはPyPIに登録されているので、pipで簡単にインストールできる。

1
pip install responder

Responderの詳細な使い方はここでは説明しない。
丁寧に説明しているページがあるので、そちらを参考にするのが良い。


また、実際に手を動かしながら覚える場合、この記事がおすすめ。
私もこれで勉強した。


オウム返しbotの作成

Flaskではサンプルとしてユーザーのメッセージをオウム返しするLINEbotのコードが公開されている。
このコードを改変することで様々なLINEbotを作成できる。


ちなみにLINE Messaging APIを利用する場合、サーバーがhttps接続でないと正常に動作しない。
自分で証明書を取得するのが面倒な場合はherokuなどを利用してサーバーを公開すると便利。

Responderを用いて先ほど述べたオウム返しbotと同様の挙動をするコードを以下に示す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import responder
import json

from linebot import ()
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage
)

api = responder.API()

# LINEの設定
line_bot_api = LineBotApi("LINE_ACCESS_TOKEN")   # ここにACCESS TOKENを入力
handler = WebhookHandler("LINE_CHANNEL_SECRET")  # ここにCHANNEL SECRETを入力

@api.route('/callback')
class LINEbot:
    async def on_post(self, req, resp):
        # get X-Line-Signature header value
        signature = req.headers['X-Line-Signature']

        # get request body as text
        body = await req.media()
        body = json.dumps(body, ensure_ascii=False).replace(' ', '')

        try:
            # handle webhook body
            handler.handle(body, signature)
            resp.status_code = 200
            resp.text = 'OK'
        except InvalidSignatureError as e:
            print(e)
            resp.status_code = 400
            resp.text = e

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=event.message.text)
    )

if __name__ == '__main__':
    api.run(address='0.0.0.0')

基本的にFlaskのものと大きく変わっている部分はない。
しかし、ハマるポイントが一点だけある。
それはsignatureを利用した認証の部分。

はじめ、何回やってもInvalidSignatureErrorが発生し、心が折れそうになった…
line-bot-sdkの認証部分のコードを探したところ、以下の処理を行っている模様。
(上記のコードのみで動作するため、ここから先は興味のある方のみ読めばOK)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
if hasattr(hmac, "compare_digest"):
    def compare_digest(val1, val2):
        """compare_digest function.
        If hmac module has compare_digest function, use it.
        Or not, use linebot.utils.safe_compare_digest.
        :param val1: string or bytes for compare
        :type val1: str | bytes
        :param val2: string or bytes for compare
        :type val2: str | bytes
        :rtype: bool
        :return: result
        """
        return hmac.compare_digest(val1, val2)
else:
    def compare_digest(val1, val2):
        """compare_digest function.
        If hmac module has compare_digest function, use it.
        Or not, use linebot.utils.safe_compare_digest.
        :param val1: string or bytes for compare
        :type val1: str | bytes
        :param val2: string or bytes for compare
        :type val2: str | bytes
        :rtype: bool
        :return: result
        """
        return safe_compare_digest(val1, val2)

class SignatureValidator(object):
    """Signature validator.
    https://developers.line.biz/en/reference/messaging-api/#signature-validation
    """

    def __init__(self, channel_secret):
        """__init__ method.
        :param str channel_secret: Channel secret (as text)
        """
        self.channel_secret = channel_secret.encode('utf-8')

    def validate(self, body, signature):
        """Check signature.
        :param str body: Request body (as text)
        :param str signature: X-Line-Signature value (as text)
        :rtype: bool
        """
        gen_signature = hmac.new(
            self.channel_secret,
            body.encode('utf-8'),
            hashlib.sha256
        ).digest()

        return compare_digest(
            signature.encode('utf-8'), base64.b64encode(gen_signature)
        )

簡単にいうとLINE Messaging APIでの認証は以下の流れで行っている。

  1. CHANNEL SECRET をkeyとしてbodyをSHA-256でハッシュ値に変換
  2. headerに含まれている signature と比較し、一致していない場合は InvalidSignatureError を例外としてraiseする

LINEのサーバー側でも同様の処理でハッシュ値に変換しているため、同じ CHANNEL SECRET を使用していない場合は認証エラーとなるということ。

実際にこの処理を行ってデバッグしながら調べたところ、 json.dumps() を利用してdictをstrに変換した場合、途中にスペースが入ってしまう。
どうやらLINEのサーバー側ではスペースが入っていない状態のbodyを使ってハッシュ値を計算しているようで、スペースを削除したらハッシュ値がheaderの signature と一致した。
これで無事オウム返しbotを動作させることができた。

まとめ

Responderを使ってオウム返しをする簡単なLINEbotを作成した。
非同期処理が簡単に実装できるのは魅力的なので、今後は積極的にResponderを使っていきたい。

Share on

Reeve
WRITTEN BY
Reeve
Researcher/Engineer