ぼくの技術日誌

日誌って銘打っていますが、更新頻度が…

HerokuとDjango(Python)、LINE Messaging APIでBotを作ってみる

はじめに

前回、HerokuにDjangoアプリをデプロイまでを書きました。
目的はLINE Messaging APIBotを作成することだったので、続いてLINE Botの作成手順をまとめておきます。
(ネットで見つけたサンプルを解析しながら作業をしました。先駆者のKosuke-Szk氏、itdadao氏、他皆様に感謝!)

また、LINE Messaging APIのアカウント作成・設定に関しては省略します。


この記事の実行環境は下記のとおりです。

Botアプリのベース作成

前回作成したDjangoアプリhellodjangoにhellobotという名前でアプリを追加します。
下記のコマンドを実行するとアプリの必要なファイルが自動生成されます。

(venv) yosuke@yosuke-vm:~/hellodjango$ python manage.py startapp hellobot
(venv) yosuke@yosuke-vm:~/hellodjango$ 

生成されたファイルの一覧を見てみます。

(venv) yosuke@yosuke-vm:~/hellodjango$ cd hellobot/
(venv) yosuke@yosuke-vm:~/hellodjango/hellobot$ ls -l
total 24
-rw-rw-r-- 1 yosuke yosuke   63 11月  2 14:46 admin.py
-rw-rw-r-- 1 yosuke yosuke  132 11月  2 14:46 apps.py
-rw-rw-r-- 1 yosuke yosuke    0 11月  2 14:46 __init__.py
drwxrwxr-x 2 yosuke yosuke 4096 11月  2 14:46 migrations
-rw-rw-r-- 1 yosuke yosuke   98 11月  2 14:46 models.py
-rw-rw-r-- 1 yosuke yosuke   60 11月  2 14:46 tests.py
-rw-rw-r-- 1 yosuke yosuke   63 11月  2 14:46 views.py
(venv) yosuke@yosuke-vm:~/hellodjango/hellobot$ 

サンプルコードをみると、views.pyにcallbackという関数が追加されており、ここでLINEからの受信メッセージの解析と応答メッセージの処理を行っているようです。

また、Djangoのリファレンスにも、
「ビュー関数、あるいは単に ビュー とは、簡単にいえばウェブリクエストを引数 にとり、ウェブレスポンスを返す関数です。」
という記載があったので、views.pyはリクエストを受ける処理を追加する場所のようです。

ルーティング(GET/POSTリクエスト受信時の呼び出し先設定)

処理を書く場所はわかりましたが、リクエスト受信時にこの処理を呼び出すために呼び出し先を設定(ルーティング)する必要があります。
これは、hellodjangoディレクトリにあるurls.pyに対して追記することで可能なようです。
hellodjango/urls.pyだけでルーティングを完結させることもできるようですが、先ほど作成したhellobotにも別にurls.pyを作って、hellobotだけのルーティング設定を書くこともできるようだったので、この分けて書く方法を試してみました。

hellodjango/urls.py(追記)

"""hellodjango URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  url(r'^$', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  url(r'^$', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.conf.urls import url, include
    2. Add a URL to urlpatterns:  url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^hellobot/', include('hellobot.urls')), #この行を追加
]

hellobot/urls.py(新規作成)

from django.conf.urls import include, url
from . import views

urlpatterns = [
    url('^callback/', views.callback),
]

ここまで書いたら、ローカルサーバを起動(foreman start)します。
localhost:5000/callback/を表示してみます。

問題がなければ、gitリポジトリへcommit後、Herokuにpushします。


リクエスト受信時処理の追加

見つけたサンプル実装(echoback)から、処理を抜き出します。

views.py

#from django.shortcuts import render
#
## Create your views here.
#
from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt

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

line_bot_api = LineBotApi(settings.LINE_CHANNEL_ACCESS_TOKEN)
parser = WebhookParser(settings.LINE_CHANNEL_SECRET)

@csrf_exempt
def callback(request):

    if request.method == 'POST':
        signature = request.META['HTTP_X_LINE_SIGNATURE']
        body = request.body.decode('utf-8')

        try:
            events = parser.parse(body, signature)
        except InvalidSignatureError:
            return HttpResponseForbidden()
        except LineBotApiError:
            return HttpResponseBadRequest()

        for event in events:
            if isinstance(event, MessageEvent):
                line_bot_api.reply_message(
                    event.reply_token,
                   TextSendMessage(text=event.message.text)
                )
        return HttpResponse()
    else:
        return HttpResponseBadRequest()

requestを分解してeventsを取得し、イベント(event)の種類がMessageEventなら、返信を作成するというようなことをしているようです。
eventはmessageを持ち、このmessageが持つtextが実際にLINEアプリに表示される本文のようです。

CHANNEL_ACCESS_TOKENとLINE_CHANNEL_SECRET

LINEのSDKで通信を行うために必要なCHANNEL_ACCESS_TOKENとLINE_CHANNEL_SECRETを定義します。
サンプルコードでは、settings.pyにHerokuサーバから定義値を取得するための関数が記載されていましたが、
Herokuへの定義方法がわからなかったので、一先ずsettings.pyに即値代入する形で追記しました。


さて、最終的なgitリポジトリに対する変更は下記のようになります。

(venv) yosuke@yosuke-vm:~/hellodjango$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   hellobot/__init__.py
	new file:   hellobot/admin.py
	new file:   hellobot/apps.py
	new file:   hellobot/migrations/__init__.py
	new file:   hellobot/models.py
	new file:   hellobot/tests.py
	new file:   hellobot/urls.py
	new file:   hellobot/views.py
	modified:   hellodjango/settings.py
	modified:   hellodjango/urls.py
	modified:   requirements.txt

(venv) yosuke@yosuke-vm:~/hellodjango$

new file:となっているもので処理を追加したファイルとその内容は
・views.py:リクエスト受信時に実行する処理
・urls.py:リクエスト受信時のルーティング設定
です。
modifled:となっているファイルとその内容は
・settings.py:LINE_CHANNEL_ACCESS_TOKENとLINE_CHANNEL_SECRETの定義を追加
・urls.py:リクエスト受信内容のうち、botアプリ向けのルーティング追加
・requirements.txt:LINE SDKの使用を宣言
です。

以上の変更をpushした後、LINE Botに対してメッセージを送ってみます。

f:id:yosuke_kirihata:20161107003156p:plain

自分の書いた内容がそのまま返ってきました!

SDKサンプルの動作確認(メッセージタイプ)

LINE Messaging APIについて調べていると、メッセージタイプというものが存在することがわかりました。

また、このあたりでLINEがサンプルコードを公開していることと、サンプルはDjangoとは違うFlaskというフレームワークを使用していることを知りました…
SDKサンプルコードから「Confirm」「Buttons」を貼り付けます。

        for event in events:
            if isinstance(event, MessageEvent):
                text = event.message.text
                if text == 'confirm':
                    confirm_template = ConfirmTemplate(text='Do it?', actions=[
                        MessageTemplateAction(label='Yes', text='Yes!'),
                        MessageTemplateAction(label='No', text='No!'),
                    ])
                    template_message = TemplateSendMessage(
                       alt_text='Confirm alt text', template=confirm_template)
                    line_bot_api.reply_message(
                       event.reply_token,
                       template_message
                    )
                elif text == 'buttons':
                    buttons_template = ButtonsTemplate(
                        title='My buttons sample', text='Hello, my buttons', actions=[
                            URITemplateAction(
                                label='Go to line.me', uri='https://line.me'),
                            PostbackTemplateAction(label='ping', data='ping'),
                            PostbackTemplateAction(
                                label='ping with text', data='ping',
                                text='ping'),
                            MessageTemplateAction(label='Translate Rice', text='米')
                        ])
                    template_message = TemplateSendMessage(
                        alt_text='Buttons alt text', template=buttons_template)
                    line_bot_api.reply_message(event.reply_token, template_message)
                else:
                    line_bot_api.reply_message(
                        event.reply_token,
                       TextSendMessage(text=event.message.text)
                    )
        return HttpResponse()

foreman startで実行してみて動作していれば、gitリポジトリへコードの変更を反映し、Herokuへpushします。
LINE Botに対して「confirm」を送信すると、選択肢が表示されます。

f:id:yosuke_kirihata:20161107003345p:plain

Yesボタンを押すと、「Yes!」というメッセージを自動で送信し、Noボタンを押すと、「No!」とメッセージを自動送信しました。
これは、コードに記載のあったtextに対応しています。

次に、LINE Botに対して「buttons」と送信します。今度も選択肢が表示されます。
このうち、「ping」を押してみますが、特に変化はありません。
ping with text」を押せば、「ping」とメッセージを自動送信します。
コード上、該当しそうな箇所を見てみると、「 ping」ボタンを押した際にdataを送信していそうだとわかります。

herokuのログを表示します。

yosuke@yosuke-vm:~/hellodjango/hellobot$ heroku logs --tail
<略>
2016-11-06T13:37:09.926367+00:00 heroku[router]: at=info method=POST path="/hellobot/callback/" host=shrouded-mesa-25264.herokuapp.com request_id=6fbbb303-3cb9-42b8-8d5f-fc89301bbb97 fwd="203.104.146.154" dyno=web.1 connect=1ms service=223ms status=200 bytes=202
2016-11-06T13:37:11.151169+00:00 heroku[router]: at=info method=POST path="/hellobot/callback/" host=shrouded-mesa-25264.herokuapp.com request_id=3a246a48-be31-4245-b5f7-4f1b529c82f0 fwd="203.104.146.154" dyno=web.1 connect=1ms service=229ms status=200 bytes=202
2016-11-06T13:37:12.465064+00:00 heroku[router]: at=info method=POST path="/hellobot/callback/" host=shrouded-mesa-25264.herokuapp.com request_id=2ff2b34c-8bff-44b5-b80e-a3cec794e9be fwd="203.104.146.154" dyno=web.1 connect=1ms service=237ms status=200 bytes=202


ping」ボタンを押してみます。

<略>
2016-11-06T13:37:09.926367+00:00 heroku[router]: at=info method=POST path="/hellobot/callback/" host=shrouded-mesa-25264.herokuapp.com request_id=6fbbb303-3cb9-42b8-8d5f-fc89301bbb97 fwd="203.104.146.154" dyno=web.1 connect=1ms service=223ms status=200 bytes=202
2016-11-06T13:37:11.151169+00:00 heroku[router]: at=info method=POST path="/hellobot/callback/" host=shrouded-mesa-25264.herokuapp.com request_id=3a246a48-be31-4245-b5f7-4f1b529c82f0 fwd="203.104.146.154" dyno=web.1 connect=1ms service=229ms status=200 bytes=202
2016-11-06T13:37:12.465064+00:00 heroku[router]: at=info method=POST path="/hellobot/callback/" host=shrouded-mesa-25264.herokuapp.com request_id=2ff2b34c-8bff-44b5-b80e-a3cec794e9be fwd="203.104.146.154" dyno=web.1 connect=1ms service=237ms status=200 bytes=202
2016-11-06T13:38:23.200287+00:00 heroku[router]: at=info method=POST path="/hellobot/callback/" host=shrouded-mesa-25264.herokuapp.com request_id=cbc249ac-d7ca-45d2-82aa-c9e0352651e7 fwd="203.104.146.154" dyno=web.1 connect=1ms service=217ms status=200 bytes=202

ログが増えたので、通信は行われているようです。
コード内部のdataが怪しいと思い、調べていると、pipのページにPostbackEventの階層構造が示されていました。

PostbackEvent
type

timestamp

source: Source

reply_token

postback: Postback
data


これを元に、pushbackのdataを取得できるか確認してみます。
取得eventを解析するfor文では、eventがMessageEventであれば、返信するような処理をしていました。
ここに取得したのがPostbackEventの場合の処理を追加します。
postbackのdataが'ping'であれば、「ping postback received!」というメッセージを表示させ、それ以外の場合はechobackと同様です。

        for event in events:
            if isinstance(event, MessageEvent):
               <略>
            elif isinstance(event, PostbackEvent):
                data = event.postback.data
                if data == 'ping':
                    line_bot_api.reply_message(
                        event.reply_token,
                        TextSendMessage(text='ping postback received!')
                    )
                else:
                    line_bot_api.reply_message(
                        event.reply_token,
                        TextSendMessage(text=data)
                    )
        return HttpResponse()

f:id:yosuke_kirihata:20161107003057p:plain

結果、pushbackのdataを取得することができました!

さいごに

ネットのサンプルコードから、Botの作成方法とLINE Messaging APIの使い方がわかりました!
特にpostbackを使うことで、LINE Bot越しに色々なモノを制御するということが僕でもできそうだと感じました。
次はArduinoRaspberry Piと連携させてみたいと思います!!

参考Webページ

DjangoでLINE Botのサンプル:ものすごく参考にした!
LINE Messaging APIとPythonを使ってChatbotを作ってみた - Qiita

echobackするBotDjangoサンプルプロジェクト:ものすごく参考にした!
line_echobot: A django implementation of Line Bot-IT大道

HerokuとLINE Messaging APIの設定:ものすごく参考にした!
HerokuとGoでLINEの Messaging API環境を作ってみた - Qiita

Djangoチュートリアル
はじめての Django アプリ作成、その 1 | Django documentation | Django

urlsの仕組み
Django urlsってなに? · workshop_tutorialJP

pipのLINE SDKリファレンス:EventとMessageの構造が参考になった!!
line-bot-sdk 1.0.2 : Python Package Index

メッセージタイプに関する簡単な紹介
LINE、 「Messaging API」正式提供開始-Botアプリケーションの開発が可能に - Feedmaticブログ

メッセージタイプの実装例:コードはPHPだけれど、confirmの理解に役立った!
PHP版の公式SDKを使ってLINE Messaging APIベースのBotを作ってみた - Qiita