Lineでやり取りした画像を自動でGoogleDriveにアップしたいからBotを作ることにしました。Part.1

# 困っていたこと
LINEを使っていて、よくあるのが、
イベントのときに、なんかのグループ作って、
情報をやり取りし、イベント後に写真をLINEに投稿し合うってやつ、ありますよね。
正直、写真の保存がマジでしんどいのです。

保存が面倒なら、自動でグーグルドライブに保存してくれるBot作ったらいいじゃない！

# やりたいこと
LINEでやり取りしている写真を、グーグルドライブに放り込みたい。
自動で。

# 今回書くこと
* アーキテクチャ
* OAuth
* サーバーレス（Chalice）
* Chaliceに外部のSDKを取り込む

# アーキテクチャ
まずはアーキテクチャをざっくり考えます。
絵で書くとこんな感じです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/c3c82432-2c43-28c5-a512-e4a60be89a26.png)

### 実際に使うときの流れは以下の通り
1. 写真をユーザーがトークに送信
2. botAPIに通知
3. botAPIがLINEから画像をDL
4. Driveにアップロード

### システム面の仕様はこんな感じです
botはサーバーレスでやることにします。
慣れているので、aws のサーバーレスフレームワーク、chaliceを使いましよう。
chaliceの記事もあるので、[こちら](https://qiita.com/yutako0217/items/0ecbee22cb6b3731827f)も参考になれば幸いです。
なので、言語はPythonになりました。

グーグルドライブの認証はoAauthを使います。
使ったことないけど、せっかくなので勉強します。

LINEの各チャットルームと、認証したドライブは紐付いている必要があります。
それを紐づけるものは、Dynamoでやることにしましょう。

# 開発環境
PC:MacbookPro
OS：MacOS High Sierra
言語：Python3.6
使うフレームワーク、ツール：Chalice, LineSDK, localstack, ngrok

# OAuthとはなんぞや
よく聞く認可の仕組みですが、今までよくわかってませんでした。
OAuthは、アプリケーションに許可された特定のリソース（データ）にアクセスを許可するものです。

# OAuthの認可フロー
OAuthの認可フローでは、以下の４つの登場人物がいます。

* リソースオーナー
* クライアント
* オーソライゼーションサーバー
* リソースサーバー

厳密な絵ではないのですが、このような流れかと思っています。
LINEさんは透過的に存在しています。LINEの上とか下を矢印通っているのは特には理由はないです。

今回、Goolｇeがオーソライゼーションサーバー、リソースサーバーを提供しているので、一緒に
サービス・プロバイダとして記載しました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/878adccc-169c-b6f3-cfb9-84b378614859.png)

Googleでログインとか、Facebookでログインとかで使われていますね。
あれです。ちょっと怖いけど、非常に便利な仕組みですね。これからは安心して、「〇〇でログイン」できます。

oAuthの理解には
[こちら](https://murashun.jp/blog/20150920-01.html)の説明が参考になりました。
[こちら](https://www.buildinsider.net/enterprise/openid/oauth20)の絵も参考になりました。
ありがとうございます。

# OAuthで認証してGoogle Driveを使うために

まずは、この赤枠をやるための準備をします。
クライアントになるには、**クライアントID** が必要になります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/e6c9c923-e748-17e4-364e-2384e65ae465.png)

Googleのアクセストークンの理解には、[こちら](https://qiita.com/shin1ogawa/items/49a076f62e5f17f18fe5)を参考にしました。

## ClientIDの作成
[https://console.developers.google.com/apis/credentials](https://console.developers.google.com/apis/credentials)こちらから作成します。

1. 認証情報を作成をクリックします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/eaa2a579-4dd1-d8bf-8e9e-7b5cd02492f4.png)

2. OAuthクライアントIDを選択します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/78c87e97-7065-c8b1-7aab-2fdf1e96ff3c.png)

3. ここで、本来はウェブアプリケーションを選択するのですが、理解のためにまずは「その他」を選択します。作成をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/ef7f872b-f8f9-228e-73c4-bba053e69a8e.png)

4. クライアントIDができました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/941b3b3f-e872-dba6-629a-2164b6211936.png)

5. このままでは使わないので、一番右のDLアイコンをクリックしてCredentialファイルをDLしましょう。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/1a9eafbc-da83-cd23-f401-5ea5516fded1.png)←これ

6. DLすると、こんな感じのデータが入っています。アプリケーションはこの情報をもって、クライアントとなることができます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/d561b11b-12e9-da41-3579-0d4284c67c08.png)
コールバックページを示すのは、redirect_urisです。今回のクレデンシャルファイルはlocalhostを指しています。

7． 同意画面の設定
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/01ee97a6-df44-6d11-a6f7-632774dc145d.png)
クライアントIDを設定すると、「同意画面」の設定をすることができます。
認可を求めるアプリケーションの提供者は誰か、を提供するものです。
あとで出てくるので、ここで設定しておきます。

これでクライアントの準備ができました。

## OAuthでGoogleDriveにアクセスしてみる
### ライブラリ
GoogleDriveを利用するにはAPIを使用するのですが、
GoogleDriveのAPIラッパーである[PyDrive](https://github.com/gsuitedevs/PyDrive)を使います。
OAuthの認証も行ってくれる素晴らしいライブラリです。

インストールはpipでできます。

```bash
$ pip install PyDrive
```

### サンプルコード
サンプルコードを実行してみましょう。
先程DLしたクレデンシャルファイルを、 `client_secrets.json` に変更し、
サンプルプログラムと同じディレクトリに置きましょう。

```
sample_dir
 ├── pydrive_sample.py
 └── client_secrets.json
```

サンプルコードはこちらです。

```python:pydrive_sample.py
#!/usr/bin/env python

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive

if __name__ == '__main__':

    gauth = GoogleAuth()
    gauth.LocalWebserverAuth()

    drive = GoogleDrive(gauth)
    file1 = drive.CreateFile({'title': 'Hello.txt'})
    file1.SetContentString('Hello')
    file1.Upload()

```

実行すると、こんな画面が開くと思います。
さきほど同意画面で、記述したものが表示されています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/dbeae46d-95a5-f24d-788d-200d21946106.png)

私はブラウザにすでにログインしていたので、ログインしているアカウントが表示されています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/d27572c7-62ea-c8c2-5429-2426affefb1d.png)

ちなみに「Qiitaのサンプル」リンクをクリックすると、このように表示されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/be054493-f2ed-7dc8-f627-b85c071a64ea.png)

認証すると、以下のような文字が。
「The authentication flow has completed.」
Authフローが完了と出てますね。

GoogleDriveにもファイルがアップロードされています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/5de4cdbb-b72a-6218-d184-80ab5e386cd6.png)

これで、OAuthによる認可とアップロードは完了してしまいました。
認可コードやアクセストークンが出てこなかったので、もう少し詳しく見てみることにします。

### 認可コードとコールバックページ
実は、先程表示された「The authentication flow has completed.」が
「認可コードを含むコールバックページ」に該当します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/69a10926-980b-27a7-6ac9-4ad41359189e.png)

「The authentication flow has completed.」が表示されるページの、
クエリストリングとして渡されるcodeが認可コードになります。

```
http://localhost:8080/?code=4/AAAjqBdam8TJPC33jLr0eo_PQ119ZQeoyhi8t7aPZNhQHYxdtLA7Wzp3sTV7SQNevQbd1TU0j3mF7Fe7IEteroA#
```

PyDriveに認可コードを渡すと、
クライアントIDと認可コードをもってアクセストークンを取得し、
APIをコールしてファイルアップロードをしてくれているのでした。

ところで今回使っているのは、
Googleで作成したのは「その他」のクライアントIDです。
コールバックページはlocalhostを指していました。

今回のサンプルコードのようにローカルで動かすときは良いのですが、
Linebotで動かしたときは、localhostではないところにリダイレクトして、
codeを取得しないといけません。

### コールバックページをlocalhostではない所に設定する。
1. 再度、[https://console.developers.google.com/apis/credentials](https://console.developers.google.com/apis/credentials)で作成します。

2. 今度は、「ウェブアプリケーション」を選択します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/3c9baacd-1885-7037-0616-580a9d8a38c4.png)

３． ここで、承認済みのリダイレクトURIを設定する事ができます。
GoogleのOAuthでは設定したURL以外にはりダイレクトできません。
あとで変更できるので、localhostではないところにする、という目的と矛盾するのですが、
とりあえず「 http://qiita.sample.com/ 」にしておきましょう。


４． クレデンシャルファイルをDLして、再度実行してみましょう。
今度は以下のようなコードを実行します。

```python:pydrive_sample2.py
#!/usr/bin/env python

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive

if __name__ == '__main__':

    gauth = GoogleAuth()
    print(gauth.GetAuthUrl())

```

表示された、長いURL

```
https://accounts.google.com/o/oauth2/auth?client_id=koko-ha-clientid-nanode-chotto-kakushimasu.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Fqiita.sample.com%2F&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive&access_type=offline&response_type=code
```

こちらをクリックしてみます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/1434d0ba-525a-3fcb-f0b9-ea13a2795ad0.png)

先程、設定したドメイン「sample.com」が表示されます。
認証すると、「sample.com」ドメインは存在していないため、認証してリダイレクトしてもエラーになります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/63125/56081581-fdb4-39b8-e9c1-130945050d66.png)

しかしURLが以下のようになっています。

```
http://qiita.sample.com/?code=4/AAD6PZwJESjjqqVbobH8XiLkqOIBTzf7MCix9_QYAdylEC23zORKxrAKl0z5C-t2Nm5mbYji1QNgxhDKCxZWeYo#
```

これで認証したら、
任意のURLにリダイレクトし、code取得できるのだということがわかります。


# chaliceプロジェクトを開始する。
OAuthで認証して、GoogleDriveにアップロードすることができそうだということがわかりました。
さっそくchaliceを使って、サーバーレスアプリケーションを作って行きましょう。

[こちら](https://qiita.com/yutako0217/items/0ecbee22cb6b3731827f)に入門用に書いたものがあるので、
ここでは、細かく説明しません。

```bash
$ chalice new-project python-line-gdrive
$ chalice deploy
reating role: python-line-gdrive-dev
Creating deployment package.
Creating lambda function: python-line-gdrive-dev
Initiating first time deployment.
Deploying to API Gateway stage: api
https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/
$ curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/
{"hello": "world"}
```

APIが作成できました。

# chaliceにline-botsdkを追加する
まずはline-bot-sdkをインストールしましょう。

```
$ pip install line-bot-sdk
```

chalice のベースプログラムに、[line-bot-sdk](https://github.com/line/line-bot-sdk-python)を追加しましょう。
`CHANNEL_ACCESS_TOKEN` , `CHANNEL_SECRET` はデプロイ後に、Lambdaのコンソールで設定するか、
ここのNoneのところに設定しましょう。

```
CHANNEL_ACCESS_TOKEN = os.getenv('CHANNEL_ACCESS_TOKEN', None)
CHANNEL_SECRET = os.getenv('CHANNEL_SECRET', None)
```

今回はこちらを実行します。
オウム返しするbotになります。

```python:line-bot-v0.1
import sys

import json
import os

from chalice import Chalice
from flask import Flask, request, abort, render_template

from linebot import (
    LineBotApi, WebhookParser
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage,
    ImageMessage
)
from linebot.models.events import (
    SourceGroup, SourceRoom, SourceUser
)

app = Chalice(app_name='python-line-gdrive')

"""
logging
"""
import logging
from logging import getLogger

logger = getLogger(__name__)
logger.setLevel(logging.DEBUG)

"""
Line TOKEN
"""
CHANNEL_ACCESS_TOKEN = os.getenv('CHANNEL_ACCESS_TOKEN', None)
CHANNEL_SECRET = os.getenv('CHANNEL_SECRET', None)

line_bot_api = LineBotApi(CHANNEL_ACCESS_TOKEN)
parser = WebhookParser(CHANNEL_SECRET)

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

@app.route("/bot", methods=['POST'])
def bot():
    signature = app.current_request.headers['X-Line-Signature']
    body = app.current_request.raw_body.decode('utf-8')
    
    # print('signature:{}'.format(signature))
    print('request body:{}'.format(body))

    try:
        events = parser.parse(body, signature)
    except InvalidSignatureError:
        print("InvalidSignatureError")
        abort(400)

    for event in events:
        # return if callback test request
        if event.reply_token == "00000000000000000000000000000000":
            print('request is callback test')
            return "ok"
        # skip if not MessageEvent
        if not isinstance(event, MessageEvent):
            print("not Message Event")
            continue

        # skip if not TextMessage
        if not isinstance(event.message, TextMessage) and not isinstance(event.message, ImageMessage):
            print("not TextMessage and ImageMessage")
            continue

        message_type = event.message
        print("message_type = {}".format(message_type))
        if isinstance(message_type, TextMessage):
            handle_message(event)

    return 'OK'
```

chaliceでは、依存関係は
vendorディレクトリにダウンロードしておく必要があります。

```
$ mkdir vendor
$ pip install -U line-bot-sdk==1.5.0 -t ./vendor/
$ pip install -U Flask==0.12.2 -t ./vendor/
```
requirementにも追加しておきましょう。

```text:requirement.txt
chalice==1.0.4
Flask==0.12.2
line-bot-sdk==1.5.0
```

そしてデプロイ。

```bash
$ chalice deploy
```

ちなみにですが、pipのバージョンが１０系の人は、
どこかで、こんなエラーがでるかもしれません。

```
AttributeError: module 'pip' has no attribute 'main'
```

その場合は、これで解決できるかと思います。

```
sudo python -m pip install --upgrade pip==9.0.3
```

なんか長くなりそうなので、いったんここまでにします。