Django+HerokuでLINE Messaging APIのおそ松botを作るまで

# 概要
おそ松のセリフをランダムに返すLINE BOTを作ります。
環境はDjango+Heroku。細かいトラップを回避しながら頑張って作ります。  

# 友だち追加
<img src="http://qr-official.line.me/L/kL14wys_rb.png" width="150">
<a href="https://line.me/R/ti/p/%40ayf0252v"><img height="36" border="0" alt="友だち追加" src="https://scdn.line-apps.com/n/line_add_friends/btn/ja.png"></a>

 * あまり会話は成立しないです (セリフをランダムに返すだけなので)
 * 当然ですが非公式です

# ソースコード
https://github.com/yakan10/LINE_osomatsu_bot

# 開発環境
 * Mac OS X 10.11.6
 * python 2.7.11
 * Django 1.10.3

# おおまかな手順
[1. LINE Messaging API登録](#1-line-messaging-api登録)
[2. Djangoインストール](#2djangoインストール)
[3. Djangoプロジェクト作成+動作確認](#3-djangoプロジェクト作成動作確認)
[4. Djangoでbotアプリケーション作成+動作確認](#4-djangoでbotアプリケーション作成動作確認)
[5. おそ松のセリフをランダムで返す部分を作成](#5-おそ松のセリフをランダムで返す部分を作成)
[6. Herokuにデプロイ](#6-Herokuにデプロイ)
[7. LINE側にHerokuのアドレスをセット](#7-line側にherokuのアドレスをセット)
[8. 会話してみる](#8-会話してみる)

# 詳細な手順
## 0. 前提
* python, pipはインストール済み
* Herokuアカウント作成済み & Heroku Toolbeltインストール & SSH鍵設定済み
  Herokuことはじめは[こちら](http://developers.mobage.jp/blog/how-to-use-for-beginners-heroku)など参考になります。

## 1. LINE Messaging API登録
[こちらのページ](https://business.line.me/ja/services/bot)から登録できます。
名前を「おそ松bot」、画像をおそ松のアイコンにして作成します。
[登録手順の説明(動画つき)はこちら](https://developers.line.me/messaging-api/getting-started)
システム構築時に「Channel access token」が必要になるのでコピペしておきます。

ちなみに、[LINE BUSINESS CENTER](https://business.line.me/ja/) -> アカウントリスト と選択すると
作成したアカウント一覧が見られます。
1つのアカウントに「LINE@」と「Messaging API」の2つの機能がくっついていて、
LINE@ が、 LINEのアカウント全般の設定、
Meesaging API が、これから構築する自動応答システムの設定になります。
Channel Access TokenはMessaging APIの方の設定から確認できます。

##2.Djangoインストール
```
$ pip install --upgrade pip # 自分の環境ではpip 8.1.2だったので、9.0.1にupgrade
$ pip install django-toolbelt
```
もし
```
Error: pg_config executable not found.
```
と出てインストールできない場合はpostgresqlをインストールします。
```
$ brew install postgresql
```

## 3. Djangoプロジェクト作成＋動作確認
プロジェクト作成
```
$ django-admin startproject osomatsu_bot
```
osomatsu_botというディレクトリが作成され、その中にdjangoのファイルが配置されます。

動作確認
```
$ cd osomatsu_bot
$ python manage.py runserver
```

```
Performing system checks...

System check identified no issues (0 silenced).

You have 13 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

November 12, 2016 - 09:11:06
Django version 1.10.3, using settings 'osomatsu_bot.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
```

http://127.0.0.1:8000 にアクセスして「It worked!」が表示されればOK。
「You have 13 ...」とメッセージが出ますが、今回の場合、'python manage.py migrate'はやらなくても問題ないです。

## 4. Djangoでbotアプリケーション作成+動作確認
botアプリケーション作成
```
$ python manage.py startapp bot
```
botというディレクトリが作成され、その中にアプリケーションに必要なファイルが配置されます。

indexを作成
LINEのBOTとは直接関係ありませんが、動作確認のためにindexを作成します。
bot/views.pyを以下のように編集します。

```py:bot/views.py
from django.shortcuts import render
from django.http import HttpResponse

def index(request):
    return HttpResponse("This is bot api.")
```

ルーティング設定
http://localhost:8000/bot にアクセスすると、bot/views.pyのindex関数を実行するようにルーティングします。
osomatsu_bot/urls.pyの16行目以降を以下のように編集します。

```py:osomatsu_bot/urls.py
from django.conf.urls import url, include  # includeを追加
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^bot/', include('bot.urls')),  # bot/ディレクトリへのルーティングを追加
]
```

日本語のコメントごと追加する場合は、osomatsu_bot/urls.pyの一行目に以下を忘れずに追加。

```py:osomatsu_bot/urls.py
# -*- encoding: utf-8 -*-
```

次に、bot/の下にurls.pyというファイルを新規作成し、以下のように編集します。

```py:bot/urls.py
from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^$', views.index, name="index"),
]
```

動作確認
http://127.0.0.1:8000/bot/ にアクセス。
「This is bot api.」と表示されていれば成功。

## 5. おそ松のセリフをランダムで返す部分を作成
以下、bot/ディレクトリで作業していきます。
セリフの準備
1行1セリフが書かれたテキストファイルを用意します。  
※ 今回は[@osomatsu_bot](https://twitter.com/osomatsu_bot)からクロールしたものを用いました

```text:bot/osomatsu_serif.txt
ねぇ君、ちょっと聞いて〜
俺、松野家長男、松野おそ松でーす！
リーダーシップにあふれたアイドル！
夢はビッグなカリスマレジェンド！！
```

また、これをあらかじめ読み込んでおくためのpythonスクリプト bot/load_serif.py を作ります。

```bot/load_serif.py
osomatsu_serif = open("bot/osomatsu_serif.txt").read().split("\n")
```

osomatsu_serif.txtのパスにはディレクトリ名(bot/)を含めます。

### views.pyに,LINEのrequest(json形式)を受信して解析する処理を追加

LINEのrequestを受信して結果を返すcallback関数を作ります。

```py:bot/views.py

import json
def callback(request):
    reply = ""
    request_json = json.loads(request.body.decode('utf-8')) # requestの情報をdict形式で取得
    for e in request_json['events']:
        reply_token = e['replyToken']  # 返信先トークンの取得
        message_type = e['message']['type']   # typeの取得

        if message_type == 'text':
            text = e['message']['text']    # 受信メッセージの取得
            reply += reply_text(reply_token, text)   # LINEにセリフを送信する関数
    return HttpResponse(reply)  # テスト用
```

セリフをランダムに返す処理を追加
views.pyにセリフをランダムに選択してLINEにpostするreply_text関数を追加します。

```py:bot/views.py

import random
import requests
from load_serif import osomatsu_serif  # 先ほどのおそ松のセリフ一覧をimport

REPLY_ENDPOINT = 'https://api.line.me/v2/bot/message/reply'
ACCESS_TOKEN = 'ENTER YOUR Channel Access Token'
HEADER = {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + ACCESS_TOKEN
}

def reply_text(reply_token, text):
    reply = random.choice(osomatsu_serif)
    payload = {
          "replyToken":reply_token,
          "messages":[
                {
                    "type":"text",
                    "text": reply
                }
            ]
    }
    
    requests.post(REPLY_ENDPOINT, headers=HEADER, data=json.dumps(payload)) # LINEにデータを送信
    return reply
```

views.pyの全体はgithubのソースコードをご参考ください。

ルーティング設定
bot/urls.pyにcallback関数へのルーティングを追加します。

```py
:bot/urls.py
from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^$', views.index, name="index"),
    url(r'^callback', views.callback),
]
```

CSRFでPOSTが弾かれる対策
このままjsonをPOSTしてもCSRFで403 Forbiddenとなります。
osomatsu_bot/settings.pyの以下の行をコメントアウトします。

```py
:osomatsu_bot/settings.py
MIDDLEWARE = [
    ...
    # 'django.middleware.csrf.CsrfViewMiddleware',
     ...
]

```

動作確認
jsonをPOSTしてエラーが出ないか確かめます。
jsonをPOSTする方法はいろいろあると思いますが、ここではChromeのAdvanced REST clientを使ってPOSTしてみます。
Advanced REST clientをインストール後、以下のようにデータを入力します。  

 * URL : http://localhost:8000/bot/callback
 * 「POST」 を選択
 * methods 「application/json」を選択
 * Raw Payloadに以下入力
 
```
{
  "events" : [
    {
    "replyToken": "012345",
    "message": {
      "type": "text",
      "text": "こんにちは"
      }
    }
  ]
}
```

※本物のLINEはもっといろいろな情報を送りますが、ここではひとまず、使う情報だけを書いたダミーデータでテスト。

 * SENDボタンをクリック
 -> bot/views.py の reply関数に上記jsonがpostされるはず。

## 6. Herokuにデプロイ
Procfileの作成
manage.pyと同じディレクトリにProcfile(拡張子なし)を作成し、以下のように記入します。

```Procfile
web: gunicorn osomatsu_bot.wsgi bot:app --log-file -
```

runtime.txtの作成
manage.pyと同じディレクトリにruntime.txtを作成し、以下のように記入します。(python2.X系だと不要?)

```runtime.txt
python-2.7.11
```

requirements.txtを作成
manage.pyと同じディレクトリに、pythonのライブラリを記載したrequirements.txtを配置します。
```
$ pip freeze > requirements.txt
```

いろいろインストールしてる人は以下のツールあたりを残すとよさげです。

```requirements.txt
Django==1.10.3
django-toolbelt==0.0.1
gunicorn==19.6.0
psycopg2==2.6.2
requests==2.10.0
```

git初期化

```
$ git init
$ git add .
$ git commit -m 'first commit'
```

herokuにリポジトリを作成
```
$ heroku create
```

自動作成されたアプリ名を覚えておく。

```
Creating app... done, ⬢ evening-depths-59364
https://evening-depths-59364.herokuapp.com/ | https://git.heroku.com/evening-depths-59364.git
```

Herokuへのデプロイ前に、settings.pyを追記
* デプロイ時の"Error while running '$ python manage.py collectstatic --noinput'." 対策
settings.pyに以下を追記。(どこでもよい)

```py:osomatsu_bot/settings.py
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))

STATIC_ROOT = os.path.join(PROJECT_ROOT, 'staticfiles')
STATIC_URL = '/static/'

STATICFILES_DIRS = (
    os.path.join(PROJECT_ROOT, 'static'),
)
```
そして、以下の設定を追加。

```
$ heroku config:set DISABLE_COLLECTSTATIC=1
```

* デプロイ後のアクセス時の"Invalid HTTP_HOST header" 対策
ALLOWED_HOSTSに先ほどcreateしたherokuのアプリ名を追加する。

```py:osomatsu_bot/setings.py
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', 'evening-depths-59364.herokuapp.com']
```

settings.pyをコミット

```
$ git add osomatsu_bot/settings.py
$ git commit -m "edit settings.py"
```

herokuにデプロイ
```
$ git push heroku master
```

アクセスしてみる
https://evening-depths-59364.herokuapp.com/bot
-> 「This is bot api.」と表示されるはず

## 7. LINE側にherokuのアドレスをセット
LINE Developersの方の「Webhook URL」に、herokuのbot/callbackのアドレスを追加。
![スクリーンショット 2016-11-12 23.01.31.png](https://qiita-image-store.s3.amazonaws.com/0/123125/16b7ff47-7ace-47be-8e9e-a4807256d43c.png)


## 8. 会話してみる
こんな感じ
![スクリーンショット 2016-11-12 23.36.18.png](https://qiita-image-store.s3.amazonaws.com/0/123125/f2f6f578-5420-acb8-20e9-9ad9b53cc75d.png)

```
$ heroku logs --tail
```
でログ監視しながらテストするといいです。

自動応答メッセージをオフにする
初期設定だとLINE@の自動応答メッセージが返ってしまうので、オフにする。
![スクリーンショット 2016-11-12 23.32.31.png](https://qiita-image-store.s3.amazonaws.com/0/123125/448eb652-05da-50fd-aac0-c4a3ec8e93e1.png)


LINE@Managerにアクセス->メッセージ->自動応答メッセージ
で、初期設定の自動応答メッセージが表示されるので、deleteします。

お疲れ様でした。

# 雑感
* Djangoじゃなくてよかったな...wsgiとかflaskのがお手軽だな...
* 公式で作ってくれえ

# 参考サイト
### 全体的に参考にしました
* [LINE Messaging APIとPythonを使ってChatbotを作ってみた](http://qiita.com/Kosuke-Szk/items/eea6457616b6180c82d3)
* [LINE Messaging APIとPythonを使ってChatbotを作ってみた(2)~サーバー編~](http://qiita.com/Kosuke-Szk/items/e31df8665f2a83406362)

### herokuデプロイ時のエラー対策
* [Django製apiをherokuでデプロイする（自分用メモ）](http://qiita.com/Kajiyu/items/446169b1a0da1e562199)  
"Error while running '$ python manage.py collectstatic --noinput'." 対策
* [django 1.5 から ALLOWED_HOSTSが追加されたぞ！DEBUG=Falseのときは気をつけろ！](http://qiita.com/esehara@github/items/27c0d1f76bea8ac9169e)  
Herokuにブラウザでアクセスしたときに出る"Invalid HTTP_HOST header"対策

### botのセリフ
* [@osomatsu_bot](https://twitter.com/osomatsu_bot)