Python+responderでLine BotをDockerで動作させる

※当ブログではアフィリエイト広告を利用しています

Line BotをPython+responderで実装し、Dockerコンテナで動作させる方法を解説しています。herokuへのDeployもできるようにしてあるので、これからLine BotをPythonで実装しようと思っている人は参考にしてください。

はじめに

Line BotをPythonで作る場合はflaskを使った解説がほとんどだと思います。公式もflask使っていますし、私も最近まで使っていました。

https://github.com/line/line-bot-sdk-python

開発している中で、「返信を5分後に返したい」という要件が出てきました。

reply tokenを使うと30秒ぐらいで返さないとトークンの有効期限が切れてしまうため、このような場合はPush通知で対応するのが一般的です。

しかし、herokuだとフロントエンドのタイムアウトが1分程度のようで、同期処理で書くと普通にタイムアウトとなってしまいます。

そのため、応答としてはすぐに200を返し、バックグラウンドで指定した時間までsleepした後でPush通知することが必要でした。

Pythonの非同期処理のモジュールを使うとflaskでも実現できそうでしたが、responderを使うと非常に簡易に実現できることが分かり、この機会に全部responderに書き換えることにしました。

また、heroku上でDockerコンテナとして動作させることができるよう、responder入りのイメージをDockerHubに用意しました。これはこれで長くなるので別記事にすると思います。

responderとは?

「a familiar HTTP Service Framework for Python」ということで、非常に簡単にAPIやWebサーバーを作ることができるようになっています。

以前はFlaskを使っていましたが、難なく乗り換えることができました。

GitHubリポジトリはこちら。

https://github.com/taoufik07/responder

バックグラウンド処理

responderの特徴的なところは、バックグラウンド処理が簡単に書けることだと思います。

以下はドキュメントの内容そのままですが、これをそのまま応用してBotに数分後に返信させることができました。


@api.route("/")
def hello(req, resp):

    @api.background.task
    def sleep(s=10):
        time.sleep(s)
        print("slept!")

    sleep()
    resp.content = "processing"

コード

公式のsdkのサンプルにある、単純に受け取った内容をそのまま返信するBotをresponderで書き直したコードです。


from __future__ import unicode_literals

import os
import sys
import responder

from argparse import ArgumentParser

from linebot import LineBotApi, WebhookParser
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage

api = responder.API(debug=True)

# get channel_secret and channel_access_token from your environment variable
channel_secret = os.getenv('LINE_CHANNEL_SECRET', None)
channel_access_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN', None)
if channel_secret is None:
    print('Specify LINE_CHANNEL_SECRET as environment variable.')
    sys.exit(1)
if channel_access_token is None:
    print('Specify LINE_CHANNEL_ACCESS_TOKEN as environment variable.')
    sys.exit(1)

line_bot_api = LineBotApi(channel_access_token)
parser = WebhookParser(channel_secret)


@api.route("/")
def nopage(req, resp):
    resp.text = "This is not a webpage you are looking for."
    resp.status_code = api.status_codes.HTTP_404


@api.route("/healthz")
def healthcheck(req, resp):
    resp.text = "OK"


@api.route("/callback")
async def callback(req, resp):
    if req.method == "post":
        signature = req.headers['X-Line-Signature']

        # get request body as text
        body = await req.text
        print("Request body: " + body)
        sys.stdout.flush()

        # parse webhook body
        try:
            events = parser.parse(body, signature)
        except InvalidSignatureError:
            resp.status_code = api.status_codes.HTTP_503
            return

        # if event is MessageEvent and message is TextMessage, then echo text
        for event in events:
            if not isinstance(event, MessageEvent):
                continue
            if not isinstance(event.message, TextMessage):
                continue

            line_bot_api.reply_message(
                event.reply_token,
                TextSendMessage(text=event.message.text)
            )
        resp.text = "OK"

    else:
        resp.text = "404: This is not a webpage you are looking for."
        resp.status_code = api.status_codes.HTTP_404


if __name__ == "__main__":
    arg_parser = ArgumentParser(
        usage='Usage: python ' + __file__ + ' [--port ] [--help]'
    )
    options = arg_parser.parse_args()
    port = int(os.environ.get('PORT', 5000))
    api.run(address='0.0.0.0', port=port, debug=True)

Flaskと比べて多少の違いはありますが、違和感なく移植することができました。

コードはこちらに置いてあります。

https://github.com/k8sinfo/linebot-responder

実際にはリポジトリにあるDockerfileとheroku.ymlを使ってherokuへデプロイして使っているのですが、別記事で解説しようと思います。

まとめ

flask使うのはその日にやめました。

皆さんもresponder使っていきましょう。

responder入りのdockerイメージも作ったのでよければ使ってください。

https://hub.docker.com/r/k8sinfo/alpine-python-responder