対話型LINE BOTの作り方001 (さくらVPS ＆ Django)

#概要
WEBアプリの開発未経験者が無謀にも対話型LINE BOTを自作するまでの試行錯誤の記録。
本記事ではWEBサーバのセットアップから始まり、BOTのデプロイ、外部サービスを活用した対話機能の実装までを時系列にまとめる。
この記事をトレースすれば誰でもBOTが作成できるはず。

##インスピを受けたもの
最初のモチベーションとなったものはこれ↓
[LINE BOTでりんな風の自分専用女子高生BOTを作ってみた (Python & Heroku)](https://qiita.com/shiraco/items/58a33b878fa5f1180d75)

その他、基本的にはQiitaにUPされている作例で学習した。
(出典は覚えている範囲でリンクを貼っています。)

##採用したサービス
趣味で作るものだから家計に負担をかけないためにコストミニマムであることは重要である。

| Service | 用途 | 備考 | 維持コスト |
|:---|:---|:---|:---|
|さくらVPS|CentOS 6|IaaSとして安価に導入可能|698円/月|
|LINE@|Messaging API||なし|
|Google Domains|任意のドメイン取得||1700円/年|
|Let's Encrypt|中間CA局|無料プラン|なし|
|Userlocal|AIチャットボット|無料プラン|なし|

##BOTサーバ構成要素

| Software | 分類 | 備考 |
|:---|:---|:---|
|NGiNX|WEBフロントサーバ|LINE@との窓口|
|Gunicorn|WEBアプリケーションサーバ||
|Django|WEBアプリフレームワーク|BOT本体|

##システム構成

基本動作はとてもシンプル。LINEアプリからメッセージを発信すると、その内容を外部の対話APIまで中継し、応答メッセージを受信して再びLINEに返す。これだけだと単なるメッセージ中継BOTに過ぎないが、メッセージの内容に応じて処理をアドオンするなり自在にカスタマイズできるようになるので汎用性が高い。

![システム構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484220/5d12d710-768d-65ed-becf-1f0a07291f38.png)

フロントとアプリケーションを分離するメリットは、SSLのオフロードによる負荷軽減であったり、NGiNXをリバースプロキシとすることによる機能追加など拡張性が見込めるといったことが挙げられる。


#0.事前準備

最低限の準備は事前に終わらせておく。解説記事は検索すれば山ほど出てくるので割愛。

 - さくらVPSの契約、サーバログイン
 - LINE Developersのアカウント作成、プロバイダー作成、チャネル作成
 - UserLocalのアカウント作成

#1.Python環境構築

さくらVPSへのログインからスタート

```bash:カーネル確認
$ cat /etc/redhat-release
CentOS release 6.10 (Final)
```

```bash:リポジトリ更新
$ sudo yum install -y https://centos6.iuscommunity.org/ius-release.rpm
```

まずは手始めにpythonのインストール。

```bash:python3インストール
$ sudo yum install -y python36*
```

```bash:エイリアスを作成
$ sudo ln -s /usr/bin/python3.6 /usr/bin/python3
$ sudo ln -s /usr/bin/pip3.6 /usr/bin/pip3
```

```bash:pip最新化
$ sudo pip3 install --upgrade pip
```

>CentOS(7系含む）ではパッケージ管理のYumコマンドがデフォルトインストールのPython2.xで動いているのでデフォルトインストールされているpythonのパスを変えてしまうようなエイリアス設定はしない方が良い
>〈参考〉[CentOS6にPython3入れる](https://qiita.com/f-akazawa/items/5ec741f8a5cca999d7b3)
>〈参考〉[PythonでLine botを作ってみた](https://qiita.com/krocks96/items/67f7510b36945eb9689b)

```bash:ライブラリをインストール
$ sudo pip3 install flask
$ sudo pip3 install line-bot-sdk
```

#2.ドメイン取得
　ドメインはGoogle Domainsで取得することとした。
　ネームサーバも併せてGoogle Domainsでレコード登録。
　手順は記載するほどでもないので割愛する。というか驚くほど簡単だったため書くことがない・・・。


#3.NGiNX導入
　WEBフロントサーバとしてNGiNXを利用する。ここでは時系列に沿って説明するため詳しくは後述するが、最終的にはSSLオフロードおよびリバースプロキシの役割を担うこととなる。

```bash:nginxをインストール
$ sudo yum install -y nginx
```

```bash:SELinuxの無効化
$ sudo vi /etc/selinux/config
```

```bash:iptablesの設定
$ sudo iptables -L
Chain INPUT (policy DROP)
target     prot opt source               destination
ACCEPT     all  --  anywhere             anywhere
ACCEPT     icmp --  anywhere             anywhere            icmp any limit: avg 1/sec burst 4
ACCEPT     tcp  --  anywhere             anywhere            state RELATED,ESTABLISHED
SERVICES   tcp  --  anywhere             anywhere            state NEW
ACCEPT     udp  --  anywhere             anywhere            udp spt:domain
ACCEPT     udp  --  anywhere             anywhere            udp spt:ntp dpt:ntp

Chain FORWARD (policy DROP)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Chain SERVICES (1 references)
target     prot opt source               destination
ACCEPT     tcp  --  anywhere             anywhere            tcp dpt:http　★追加
ACCEPT     tcp  --  anywhere             anywhere            tcp dpt:https　★追加
ACCEPT     tcp  --  anywhere             anywhere            tcp dpt:domain
ACCEPT     udp  --  anywhere             anywhere            udp dpt:domain
ACCEPT     tcp  --  anywhere             anywhere            tcp dpt:ftp
ACCEPT     tcp  --  anywhere             anywhere            tcp dpt:wap-wsp
```

>　nginx 操作コマンド(CentOS7)

```
開始： systemctl start nginx
終了： systemctl stop nginx
再起動：systemctl restart nginx
設定の再読み込み：systemctl reload nginx
状態確認：systemctl status nginx.service -l
覚えておくと便利。
→CentOS6ならserviceコマンドに読み替えること！
```

```bash:自動起動設定
$ sudo chkconfig nginx on
$ chkconfig --list
```

```bash:サービス起動
$ sudo service nginx start
```

>〈参考〉[サーバ構築_サクラVPSのHttps有効化手順](https://qiita.com/__init__/items/cfd134b08017f85b1fad)


#4.サーバ証明書発行

NGiNXのHTTPS化を目論む。理由としては自己防衛の意味合いもあるが、ここではLINE Messaging APIがHTTPに対応していないため必須事項なのである。
今回は認証局として中間CA局であるLet's Encryptを利用する。

```none:クライアントソフトのインストール
$ sudo su -
# yum install -y epel-release
# curl https://dl.eff.org/certbot-auto -o /usr/bin/certbot-auto
# chmod a+x /usr/bin/certbot-auto
# certbot-auto --os-packages-only --non-interactive
```

```bash:証明書の新規発行
certbot-auto certonly --non-interactive --agree-tos --webroot -w /usr/share/nginx/html -d mydomain.com --email mymail@example.com
```

>オプション解説
`--non-interactive`
`--agree-tos`
`--webroot`  webrootモードで動作
`-w`  webrootのディレクトリを指定する。この中にファイルを書き込むためhttp(80)でアクセス可能にしておく必要がある
`-d`  ドメイン
`--email`  更新期限が近づいたときにメール通知されるメールアドレス

```noneh:発行された証明書
# ls /etc/letsencrypt/live/mydomain.com/
README  cert.pem  chain.pem  fullchain.pem  privkey.pe
```

>〈参考〉[CentOSにLet's EncryptのSSL証明書を導入する](https://kohkimakimoto.hatenablog.com/entry/2016/09/15/180119)
>〈参考〉[[サーバー] Let's EncryptとNginxで、https対応を行う](https://www.yoheim.net/blog.php?q=20171204)

証明書の更新

```bash:証明書の更新(有効期限内なら処理はスキップされる)
certbot-auto renew --webroot -w /usr/share/nginx/html --post-hook "service nginx reload"
```

>`--post-hook`  証明書の更新が行われた場合にのみ、1度だけ実行される。更新が行われなかった場合は実行されない。通常はWebサーバの再読み込み処理を記述する。

Let's Encryptの証明書は3ヶ月で有効期限が切れるので、これをcronで定期的に実行させるとよい。

```bash:cronで自動更新設定
crontab -e
50 3 * * * root certbot-auto renew --webroot -w /usr/share/nginx/html --post-hook "service nginx reload" > /dev/null 2>&1
```


#5.NGiNXのSSL化

ssl.confファイルを編集

```bash:/etc/nginx/ssl.conf
#
# HTTPS server configuration
#

server {
    listen       443 ssl http2 default_server;
#    listen       [::]:443 ssl;
#    server_name  _;
#    root         /usr/share/nginx/html;

    ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;
#    ssl_session_cache shared:SSL:1m;
#    ssl_session_timeout  10m;
#    ssl_ciphers HIGH:!aNULL:!MD5;
#    ssl_prefer_server_ciphers on;

    # Load configuration files for the default server block.
    include /etc/nginx/default.d/*.conf;

    location / {
    }

    error_page 404 /404.html;
        location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }
}
```

```none:nginxリロード
# service nginx restart
```

ブラウザからhttps接続してみてレスポンスが得られれば無事に疎通完了。

#中間まとめ１
ここまで
　・ドメイン取得、証明書発行
　・WEBフロントサーバとしてNGiNXをインストール
　・クライアントからのhttps接続を確立
が完了した。
続いて、WEBアプリケーションサーバの構築を進めていく。

>〈参考〉[サーバ構築_サクラVPSのWebアプリケーションサーバ構築](https://qiita.com/__init__/items/a8083530f3057fc0afd7)

#6.postgresqlをインストール
Djangoのデータベースとして PostgreSQLを利用する。

```none:PostgreSQLインストール
# yum -y install postgresql-server
```

```none:DB初期化
# service postgresql initdb
```

```none:起動
# service postgresql start
```

```bash:postgresというユーザでログインし詳細設定
$ sudo -u postgres psql
postgres=# create database myDB;
postgres-# create user mysqluser with password 'mysql'; #ユーザとパスワード作成
postgres-# alter role mysqluser set client_encoding to 'utf8'; #文字コード設定
postgres-# alter role mysqluser set default_transaction_isolation to 'read committed'; #実行された結果だけを見に行く
postgres-# alter role mysqluser set timezone to 'UTC+9'; #タイムゾーンを設定
postgres-# grant all privileges on database myDB to mysqluser; #ユーザがデータベースを参照できるよう権限付与
postgres=# \q #終了
```

```bash:状態確認
$ sudo ps aux | grep post
postgres 26941 0.1 1.2 220940 6124 ? S 10:40 0:00 /usr/bin/postmaster -p 5432 -D /var/lib/pgsql/data
postgres 26945 0.0 0.2 183952 1328 ? Ss 10:40 0:00 postgres: logger process
postgres 26947 0.0 0.4 220940 2112 ? Ss 10:40 0:00 postgres: writer process
postgres 26948 0.0 0.2 220940 1504 ? Ss 10:40 0:00 postgres: wal writer process
postgres 26949 0.0 0.3 221080 1832 ? Ss 10:40 0:00 postgres: autovacuum launcher process
postgres 26950 0.0 0.3 183948 1524 ? Ss 10:40 0:00 postgres: stats collector process
```


#7.virtualenvのインストール
virtualenv = pythonの仮想環境を構築してくれるツール。
例えば、python2とpython3は普通の開発環境だと共存できない。そういった場合にvirtualenvを活用するらしい。

```bash:virtualenvのインストール
$ sudo pip install virtualenv
$ which python3
/usr/bin/python3
```

```bash:仮想環境を構築する(python3インストパス、botenvが仮想環境名)
$ virtualenv -p /usr/bin/python3 botenv

Running virtualenv with interpreter /usr/bin/python3
Using base prefix '/usr'
New python executable in /home/my/botenv/bin/python3
Also creating executable in /home/my/botenv/bin/python
Installing setuptools, pip, wheel...
done.
```

```bash:内容確認
$ ls botenv/bin/
 #ずらずらとファイルが配置されている
```

```bash:仮想環境起動
$ source /home/my/botenv/bin/activate
(botenv) []$
```

django と gunicorn と postgresql に接続するためのモジュールをインポートする(仮想環境内で)

```bash:モジュール追加
(botenv) []$ pip install django gunicorn psycopg2
(botenv) []$ pip install django-toolbelt
```

```bash:動作確認
(botenv) [ ~]$ python3
Python 3.4.8 (default, Apr 9 2018, 11:43:18)
[GCC 4.4.7 20120313 (Red Hat 4.4.7-18)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import django
>>> django.get_version()
'2.0.10'
>>> exit()
```

```bash:仮想環境から抜ける
(botenv) [ ~]$ deactivate
```

>〈参考〉[Djangoで投稿アプリを作ってみる①(プロジェクト作成、http / https 通信許可) ](https://qiita.com/__init__/items/bb9d8f160747ead381d7)
>〈参考〉[はじめての Django アプリ作成、その 1](https://docs.djangoproject.com/ja/2.0/intro/tutorial01/)


#(チュートリアル) Djangoの使い方
BOTの開発に入る前に、Djangoの練習としてテストプロジェクトの作成やWEBアプリとして公開する手順などを試してみる。

```bash:事前準備
(botenv) []$ cd /home/my/botenv/

#gccインストール
(botenv) [botenv]$ sudo yum install -y gcc

#treeインストール
(botenv) [botenv]$ sudo wget ftp://mama.indstate.edu/linux/tree/tree-1.6.0.tgz
(botenv) [botenv]$ sudo tar zxvf tree-1.6.0.tgz
(botenv) [botenv]$ cd tree-1.6.0
(botenv) [tree-1.6.0]$ sudo make
(botenv) [tree-1.6.0]$ sudo make install
(botenv) [tree-1.6.0]$ cd ..
```

```bash:Djangoお試し
#Djangoプロジェクトを作成（project001）
(botenv) [botenv]$ django-admin startproject project001
(botenv) [botenv]$ tree project001/
project001
├── manage.py
└── project001
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

1 directory, 5 files

#Djangoサーバ起動
(botenv) [botenv]$ cd project001
(botenv) [project001]$ python3 manage.py runserver

#このままでは8000番ポートがコネクトできないためfirewallを設定追加
(botenv) [project001]$ sudo iptables -I SERVICES -p tcp --dport 8000 -j ACCEPT

#結果確認（nオプションでポート番号で表示される）
(botenv) [project001]$ sudo iptables -L -n

```

まだ外部からDjangoサーバにアクセスできない。
setting.pyにallowホストを記述する必要がある。

```python:project001/project001/setting.py
ALLOWED_HOSTS = ['サーバのIPアドレス','mydomain.com'] #自身のホストを許可する
```

```bash:Djangoサーバ実行コマンド(外部公開版)
# 全てのIP(0.0.0.0)からの8000番ポートへのアクセスを受け付ける
(botenv) [project001]$ python3 manage.py runserver 0.0.0.0:8000 
```

これでブラウザから`http:<IPアドレス>:8000`でアクセスしてロケットの絵が表示されれば無事成功。
しかしまだhttpsでのアクセスは拒否される。どうやらデフォルトのDjangoサーバではhttpsがサポートされていないらしい。
そこでhttps対応のDjangoサーバであるdjango-sslserverをインストールする。

```bash:django-sslserverのインストール
(botenv) [project001]$ pip install django-sslserver
```

あわせてsettings.pyも★の行を追記する。

```bash:project001/project001/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'sslserver',　★
]
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')　★
SECURE_SSL_REDIRECT = True　★
SESSION_COOKIE_SECURE = True　★
CSRF_COOKIE_SECURE = True　★
```

さらにローカルユーザへサーバ証明書アクセス権を付与。

```bash:証明書へのアクセス権設定
$ sudo chmod 755 /etc/letsencrypt/live/
$ sudo chmod 755 /etc/letsencrypt/archive/
$ sudo chmod 644 /etc/letsencrypt/archive/mydomain.com/privkey1.pem
(秘密鍵までreadableにするのはあんまり望ましくない？)
```

最終的に実行コマンドは以下の通りとなる

```bash:Djangoサーバ実行コマンド(外部公開版,SSL対応版)
(botenv) [project001]$ python3 manage.py runsslserver 0.0.0.0:8000 --certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem --key /etc/letsencrypt/live/mydomain.com/privkey.pem
```

実行コマンドの書式
`python3 manage.py runsslserver 0.0.0.0:8000 サーバ証明書 秘密鍵`

以上でブラウザから`https://<URL>:8000`でアクセスできれば成功。

>〈参考〉[Djangoで投稿アプリを作ってみる②(最初の Viewを表示)](https://qiita.com/__init__/items/2e3567cf85cf400f2e9e)

##NGiNXとgunicornとDjangoの連携

DjangoをWEBサービスとして提供するためにはWEBサーバとの連携が必要となる。

★`NGiNX` (グローバルIP or URL:443)
nginxがインターネットからの接続を受け付けるWEBフロントとなり
SSLの終端および静的コンテンツの提供を行うとともに、
gunicornへのリバースプロキシとして動作する。
gunicornの負荷軽減を目的としてHTTP接続を用いる。

★`Gunicorn` (127.0.0.1:8000)
gunicornはPythonアプリを動作させるWebアプリケーションサーバ。
　＝別名WSGI(Web Server Gateway Interface)サーバ。
単体でも動作するがnginxなど他のWebサーバとも連携できる。

★`Django`
WEBアプリケーションを実装するために用意されたpython用のモダンフレームワーク

連携イメージは下記の通り。
`クライアント` <--(HTTPS)--> `nginx` <--(HTTP)--> `gunicorn` <--(WSGI)--> `Django`

>〈参考〉[Django best practice (nginx)](https://django-best-practice-ja.readthedocs.io/ja/latest/deployment/servers.html)
〈参考〉[CentOS7 Nginx+GunicornでDjangoを動かす ](https://narito.ninja/blog/detail/21/)

###まずはNGiNXの設定

```bash:/etc/nginx/conf.d/mydomain.com.conf
server {
  listen 80;
  server_name サーバのIPアドレス;

  location /static {
    alias /usr/share/nginx/html/static;
  }

  location /media {
    alias /usr/share/nginx/html/media;
  }

  location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

server {
  listen 443 ssl;
  server_name mydomain.com;

  ssl on;
  ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;

  location /static {
    alias /usr/share/nginx/html/static;
  }

  location /media {
    alias /usr/share/nginx/html/media;
  }

  location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}
```

```bash:syntax_check
(botenv) [conf.d]$ sudo nginx -t 
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
```

###Gunicorn設定

```bash:Gunicornインストール
(botenv) [project001]$ pip install gunicorn
```

```bash:project001/settings.py　(★を追記)
INSTALLED_APPS = [
'gunicorn',　★
]
```

先ほどDjangoのSSLサーバ化を行った場合は`'sslserver'`を指定したが、
Gunicorn＆NGiNXとの連携を行う場合においては、NGiNXがSSLを終端する(NGiNXとGunicornの間はHTTPで接続される)ため、DjangoのSSLサーバ化は必要なくなる。

###GunicornからDjangoを起動

```bash:gunicornでDjango_projectを起動
(botenv) [project001]$ gunicorn --bind 127.0.0.1:8000 project001.wsgi:application
```

```bash:gunicornでDjango_projectを起動(デーモンモード)
(botenv) [project001]$ gunicorn --daemon --bind 127.0.0.1:8000 project001.wsgi:application

#終了するときはプロセスを探してkill
(botenv) [project001]$ sudo lsof -i:8000
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
gunicorn 29072 my 6u IPv4 2604917 0t0 TCP localhost:irdmi (LISTEN)
gunicorn 29075 my 6u IPv4 2604917 0t0 TCP localhost:irdmi (LISTEN)
(botenv) [project001]$ sudo kill -9 29072
(botenv) [project001]$ sudo kill -9 29075
```

以上でチュートリアル終了。

#8.BOTアプリケーション開発

いよいよBOT本体の開発に入っていく。
チュートリアルで確認した通り、NGiNXとGunicornとDjangoの連携を用いることとする。

>〈参考〉[Django+HerokuでLINE Messaging APIのおそ松botを作るまで](https://qiita.com/yakan10/items/b7ad35c2cbba5db81462)


```bash:
#Djangoプロジェクト(プロジェクト名：line_bot)作成
(botenv) [botenv]$ django-admin startproject line_bot
(botenv) [botenv]$ cd line_bot

#ファイル配置確認
(botenv) [line_bot]$ tree
.
├── db.sqlite3
├── line_bot
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-34.pyc
│   │   ├── settings.cpython-34.pyc
│   │   ├── urls.cpython-34.pyc
│   │   └── wsgi.cpython-34.pyc
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

#Allowホスト設定,Gunicorn連携設定
(botenv) [line_bot]$ vim line_bot/settings.py
ALLOWED_HOSTS = ['サーバのIPアドレス','mydomain.com'] #自身のホストを許可
INSTALLED_APPS = ['gunicorn'] #追記

#botアプリケーション（アプリ名：bot）作成
(botenv) [line_bot]$ python3 manage.py startapp bot
 # botディレクトリが作成され、必要なファイルが配置される。

#ファイル配置確認
(botenv) [line_bot]$ tree
.
├── bot
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── db.sqlite3
├── line_bot
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-34.pyc
│   │   ├── settings.cpython-34.pyc
│   │   ├── urls.cpython-34.pyc
│   │   └── wsgi.cpython-34.pyc
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
```

#8-2.テスト用関数の準備
簡易な応答関数を用意しておくことで、動作確認の際に何かと重宝する。
今回はbot/views.pyにindexおよびcallbackを配置する。
indexは単純な文字列を返す関数
callbackはPOSTされた文字列を返す関数

bot/views.pyまでのルーティングはline_bot/urls.pyおよびbot/urls.pyにて記述する。
イメージとしては
`line_bot/urls.py` → `bot/urls.py` → `bot/views.py `
という参照の流れ。


``` line_bot/urls.py
#from django.contrib import admin　★コメントアウト
from django.urls import path
from django.conf.urls import url,include　★追記

urlpatterns = [
#    path('admin/', admin.site.urls),　★コメントアウト
    path('bot/',include('bot.urls')),　★追記
]
```

>ついでにadminは当面使う予定がないのでコメントアウトしておく。

```bot/urls.py(★新規作成)
from django.conf.urls import url
from . import views 

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

```bot/views.py 
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt ##注釈1

# Create your views here. 
from django.http import HttpResponse
import json
import requests

REPLY_ENDPOINT = 'https://api.line.me/v2/bot/message/reply'
ACCESS_TOKEN = '' #アクセストークンはLINE Developers管理画面にて確認する
HEADER = {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + ACCESS_TOKEN
}

def reply_text(reply_token, text):
    reply = "You said "+text+" that's all."
    payload = {
      "replyToken":reply_token,
      "messages":[
        {
          "type":"text",
          "text": reply
        }
      ]
    }
    requests.post(REPLY_ENDPOINT, headers=HEADER, data=json.dumps(payload))
    return reply

@csrf_exempt  ##注釈1
def callback(request):
    reply = ""
    request_json = json.loads(request.body.decode('utf-8'))

    for e in request_json['events']:
      reply_token = e['replyToken']
      message_type = e['message']['type']

      if message_type == 'text':
        text = e['message']['text']
        reply += reply_text(reply_token, text)
    return HttpResponse(reply)

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

> 注釈1：CSRF検証の回避
CSRF(クロスサイトリクエストフォージェリ)対策としてDjangoにおいては
settings.pyの
    'django.middleware.csrf.CsrfViewMiddleware',
で検証が有効になっている。
Djangoでは、デフォルトで POST/PUT/DELETE のメソッドが禁止されており、GET のみ許可されている。このままではPOSTが使えないため、必要に応じてCSRF検証をスキップする必要がある。
 - 案1.setting.pyにて上記設定をコメントアウト(全てのCSRF検証を無効化)
 - 案2.@csrf_exemptで例外扱いとする(関数単位にCSRF検証を無効化)
 - 案3.Django REST Frameworkを利用する(恒久対策)
>老番ほど安全性は高くなるが、案3については別途学習コストが必要だったので今回は案2で対処した。今後余力があれば挑戦したい。


```bash:足りてないpythonモジュールは必要に応じて追加
#requestモジュールのインストール
(botenv) [line_bot]$ pip3 install requests
```

準備ができたところでBOTを起動してみる。以下のような出力が得られれば無事に待ち受け状態となる。

```bash:Gunicornからline_botを起動
(botenv) [line_bot]$ gunicorn --bind 127.0.0.1:8000 line_bot.wsgi:application
[2019-10-29 12:01:23 +0900] [19196] [INFO] Starting gunicorn 19.9.0
[2019-10-29 12:01:23 +0900] [19196] [INFO] Listening at: http://127.0.0.1:8000 (19196)
[2019-10-29 12:01:23 +0900] [19196] [INFO] Using worker: sync
[2019-10-29 12:01:23 +0900] [19199] [INFO] Booting worker with pid: 19199
```


###動作確認①
`https://mydomain.com/bot/にブラウザアクセス`
下記メッセージが表示されればOK
![0001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484220/35201110-3edc-2dc5-8052-6e87d65f20a0.png)


###動作確認②
`https://mydomain.com/bot/callbackに自作jsonをPOST`
chromeの拡張機能であるAdvanced REST clientを使用して手動でPOSTする。設定例は下記の通り。

 - Method : POST
 - Request URL : https://mydomain.com/bot/callback
 - Header name : content-type
 - Header value : application/json
 - Body :
 {
  "events": [
    {
      "replyToken": "000000",
      "message": {
        "type": "text",
        "text": "テスト送信"
      }
    }
  ]
}

SENDボタンを押下し、レスポンスが正しく得られたらOK
![0003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484220/d17fbab0-1950-0e6b-6ec3-a584d6e88cff.png)



###動作確認③
LINE Developers管理コンソールからWebhook URLに
`https://mydomain.com/bot/callback`を設定、接続確認ボタンを押下し成功すればOK

![0004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484220/7661fc39-1056-822c-90d6-b60fc77b5215.png)


どうやら問題なさそうである。

#中間まとめ２
前項までで以下の構築が完了した。

 - NGiNXをフロントサーバとしたSSLオフロードおよびGunicornへのリバースプロキシ化。
 - Djangoフレームワークを利用した簡単なreplyを返すBOTアプリを設置。
 
続けてloggingの準備をして、終わったらいよいよ実際にLINE BOTアプリの開発を始める。


#8-3.Logging設定

慣れている人にとっては完全に余談でしかないと思うので省略しても問題ない。
筆者はPythonのド素人なので、小まめにログを出すことが生産性に直結するためしっかり準備しておく。。
なおloggingの学習だけで５日くらい要した。奥が深すぎて気が遠くなりそう。
参考にしたサイトは忘れてしまったので申し訳ないが、出来上がったコードはほぼコピペに近く、新規性は無いのだが、いちおう備忘録として掲載しておく。

```bash:共通関数用ディレクトリ
(botenv) [bot]$mkdir utils
```

```python:utils/tools.py
# -*- coding: utf-8 -*-
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

import json
import logging
import requests

@csrf_exempt
def outputLog_line_request(request): # type=byte code
    if request.method == 'POST':
        linelogger = logging.getLogger('lineBotLogging')
        # byte を string型へデコード
        linelogger.debug(json.loads(request.body.decode('utf-8')))
    return '処理完了'

def outputLog_line_response(response): # type=json
    linelogger = logging.getLogger('lineBotLogging')
    linelogger.debug(response)
    return '処理完了'

def outputLog_ulocal_request(request): # type=json
    linelogger = logging.getLogger('ulocalBotLogging')
    linelogger.debug(request)
    return '処理完了'

def outputLog_ulocal_response(response): # type=byte code
    linelogger = logging.getLogger('ulocalBotLogging')
    # byte を string型へデコード
    linelogger.debug(response.json())
    return '処理完了'
```

```bash:logファイル用ディレクトリ
(botenv) [bot]$mkdir log
``` 

```python:log/logconfig.py
import logging.config
import os

logging.config.dictConfig({
    'version': 1,
    'formatters': {
        'botFormat': {
            'format': '%(asctime)s %(name)-12s %(message)s'
        },
        'customFormat': {
            'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
        },
    },
    'handlers': {
        'rootHandler': {
            'class': 'logging.FileHandler',
            'formatter': 'customFormat',
            'encoding': 'utf8',
            'level': logging.DEBUG,
            'filename': os.path.dirname(os.path.abspath(__file__)) + '/debug.txt'
        },
        'lineBotHandler': {
            'class': 'logging.FileHandler',
            'formatter': 'botFormat',
            'encoding': 'utf8',
            'level': logging.DEBUG,
            'filename': os.path.dirname(os.path.abspath(__file__)) + '/linelogger.txt'
        },
        'ulocalBotHandler': {
            'class': 'logging.FileHandler',
            'formatter': 'botFormat',
            'encoding': 'utf8',
            'level': logging.DEBUG,
            'filename': os.path.dirname(os.path.abspath(__file__)) + '/ulocallogger.txt'
        },
        'customFileHandler': {
            'class': 'logging.FileHandler',
            'formatter': 'customFormat',
            'encoding': 'utf8',
            'level': logging.DEBUG,
            'filename': os.path.dirname(os.path.abspath(__file__)) + '/debug.txt'
        }
    },
    'root': {
        'handlers': ['rootHandler'],
        'level': logging.DEBUG
    },
    'loggers': {
        'lineBotLogging': {
            'handlers': ['lineBotHandler'],
            'level': logging.DEBUG,
            'propagate': 0
        },
        'ulocalBotLogging': {
            'handlers': ['ulocalBotHandler'],
            'level': logging.DEBUG,
            'propagate': 0
        },
        'commonLogging': {
            'handlers': ['customFileHandler'],
            'level': logging.DEBUG,
            'propagate': 0
        },
    }
})
```

#8-4.LINE BOT(対話BOT)作成

LINEからメッセージを受け取り、本文をUserLocalの対話APIに中継し、
応答を受け取ると、再びLINEへ中継するというリレーアプリを作る。
ポイントとしてはLINEからのWebhookはJSON、UserLocalへの投稿はGET、LINEへの応答はJSONのPOSTという具合である。

LINEから遅れらてくるJSONフォーマットは下記に示す通り

```python:LINEメッセージ内容
{
  'destination': ,
  'events': [{
    'type': 'message',
    'timestamp': 1572579298365,
    'source': {
      'type': 'user',
      'userId': ユーザ固有
    },
    'replyToken': '13a9ed827c4341d1b1e118c682fd6840',
    'message': {
     'type': 'text',
     'id': '10840574616013',
     'text': '何してるの？'
    }
  }]
}
```


第三者からのアクセスを防ぎたければuserIdでユーザを識別するようホワイトリストを作ればよい。
重要なのはreplyTokenでこれは受信の毎に変わるが、LINE側のコンテクストを維持するために応答メッセージに含める必要がある。

出来上がったコードは以下の通り。
（デバッグが不要であればloggingは全て無視して問題ない。)

```bot/chatbot.py
# -*- Coding: utf-8 -*-

from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
from django.shortcuts import render
from datetime import datetime
from time import sleep
import requests
import json
import base64
import logging
import os
import random
import log.logconfig
from utils import tools

logger = logging.getLogger('commonLogging')

LINE_ENDPOINT = 'https://api.line.me/v2/bot/message/reply'
LINE_ACCESS_TOKEN = '' #Line developer管理画面で確認
LINE_ALLOW_USER='' #Line developer管理画面で確認

ULOCAL_ENDPOINT = 'https://chatbot-api.userlocal.jp/api/chat'
ULOCAL_API_KEY = '' #Userlocal登録時に確認

@csrf_exempt
def line_handler(request):

    #exception
    if not request.method == 'POST':
      return HttpResponse(status=200)

    logger.debug('line_handler message incoming') #logging
    out_log = tools.outputLog_line_request(request) #logging
    request_json = json.loads(request.body.decode('utf-8'))

    for event in request_json['events']:
      reply_token = event['replyToken']
      message_type = event['message']['type']
      user_id = event['source']['userId']

      #whitelist
      if not user_id == LINE_ALLOW_USER:
        logger.warning('invalid userID:' + user_id) #logging
        return HttpResponse(status=200)

      #action
      if message_type == 'text':
        #ulocal chat
        response_text(reply_token,ulocal_chatting(event))

    return HttpResponse(status=200)

def response_text(reply_token,text):
    payload = {
      "replyToken": reply_token,
      "messages":[
        {
          "type": 'text',
          "text": text
        }
      ]
    }
    line_post(payload)

def line_post(payload):
    url = LINE_ENDPOINT
    header = {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + LINE_ACCESS_TOKEN
    }
    requests.post(url, headers=header, data=json.dumps(payload))
    out_log = tools.outputLog_line_response(payload) #logging
    logger.debug('line_handler message -->reply') #logging

def ulocal_chatting(event):
    url = ULOCAL_ENDPOINT   
    payload={
      'key'    : ULOCAL_API_KEY,
      'message': event['message']['text']
    }  

    out_log = tools.outputLog_ulocal_request(payload) #logging
    logger.debug('ulocal_chatting send request') #logging
    ulocal_res = requests.get(url,payload)
    logger.debug('ulocal_chatting -->recv response') #logging
    out_log = tools.outputLog_ulocal_response(ulocal_res) #logging

    data = ulocal_res.json()
    response = data['result']
    return response
```

続いてルーティングの追加

```python:bot/urls.py(★追記)
from django.conf.urls import url
from . import views,chatbot　★

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

以上で完成

```bash:line_botを起動
(botenv) [line_bot]$ gunicorn --bind 127.0.0.1:8000 line_bot.wsgi:application
```

LINE Developers管理コンソールにてWebhook URLを
`https://mydomain.com/bot/chatbot`に変更して接続確認し成功することを確認。

LINEアプリからメッセージを投下してみると、問題なく応答が得られるだろう。

![s_Screenshot110.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484220/eed51d20-4a9f-9b7b-4687-89b8ec70abf1.jpeg)

実用性はさておき、これで最低限の機能を有する対話型BOTが完成した。
初回投稿はここまで。