Pippi Life

主に仕事に関連するITのことや、プライベートもちょいちょい書きます。

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

困っていたこと

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

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

やりたいこと

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

アーキテクチャ

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

image.png

実際に使うときの流れは以下の通り

  1. 写真をユーザーがトークに送信
  2. botAPIに通知
  3. botAPIがLINEから画像をDL
  4. Driveにアップロード

システム面の仕様はこんな感じです

botはサーバーレスでやることにします。 慣れているので、aws のサーバーレスフレームワークchaliceを使いましよう。 chaliceの記事もあるので、こちらも参考になれば幸いです。 なので、言語はPythonになりました。

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

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

開発環境

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

OAuthとはなんぞや

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

OAuthの認可フロー

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

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

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

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

image.png

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

oAuthの理解には こちらの説明が参考になりました。 こちらの絵も参考になりました。 ありがとうございます。

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

まずは、この赤枠をやるための準備をします。 クライアントになるには、クライアントID が必要になります。 image.png

Googleのアクセストークンの理解には、こちらを参考にしました。

ClientIDの作成

https://console.developers.google.com/apis/credentialsこちらから作成します。

  1. 認証情報を作成をクリックします。 image.png

  2. OAuthクライアントIDを選択します。 image.png

  3. ここで、本来はウェブアプリケーションを選択するのですが、理解のためにまずは「その他」を選択します。作成をクリック。 image.png

  4. クライアントIDができました。 image.png

  5. このままでは使わないので、一番右のDLアイコンをクリックしてCredentialファイルをDLしましょう。 image.png←これ

  6. DLすると、こんな感じのデータが入っています。アプリケーションはこの情報をもって、クライアントとなることができます。 image.png コールバックページを示すのは、redirect_urisです。今回のクレデンシャルファイルはlocalhostを指しています。

7. 同意画面の設定 image.png クライアントIDを設定すると、「同意画面」の設定をすることができます。 認可を求めるアプリケーションの提供者は誰か、を提供するものです。 あとで出てくるので、ここで設定しておきます。

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

OAuthでGoogleDriveにアクセスしてみる

ライブラリ

GoogleDriveを利用するにはAPIを使用するのですが、 GoogleDriveのAPIラッパーであるPyDriveを使います。 OAuthの認証も行ってくれる素晴らしいライブラリです。

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

$ pip install PyDrive

サンプルコード

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

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

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

#!/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

私はブラウザにすでにログインしていたので、ログインしているアカウントが表示されています。 image.png

ちなみに「Qiitaのサンプル」リンクをクリックすると、このように表示されます。 image.png

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

GoogleDriveにもファイルがアップロードされています。 image.png

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

認可コードとコールバックページ

実は、先程表示された「The authentication flow has completed.」が 「認可コードを含むコールバックページ」に該当します。 image.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で作成します。

  2. 今度は、「ウェブアプリケーション」を選択します。 image.png

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

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

#!/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

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

image.png

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

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

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

chaliceプロジェクトを開始する。

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

こちらに入門用に書いたものがあるので、 ここでは、細かく説明しません。

$ 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を追加しましょう。 これでオウム返しするbotになりました。 CHANNEL_ACCESS_TOKEN , CHANNEL_SECRET はデプロイ後に、Lambdaのコンソールで設定するか、 ここのNoneのところに設定しましょう。

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

今回はこちらを実行します。

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にも追加しておきましょう。

chalice==1.0.4
Flask==0.12.2
line-bot-sdk==1.5.0

そしてデプロイ。

$ chalice deploy

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

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

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

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

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

Qiita はこちら