Lineでやり取りした画像を自動でGoogleDriveにアップしたいからBotを作ることにしました。Part.1
困っていたこと
LINEを使っていて、よくあるのが、 イベントのときに、なんかのグループ作って、 情報をやり取りし、イベント後に写真をLINEに投稿し合うってやつ、ありますよね。 正直、写真の保存がマジでしんどいのです。
保存が面倒なら、自動でグーグルドライブに保存してくれるBot作ったらいいじゃない!
やりたいこと
LINEでやり取りしている写真を、グーグルドライブに放り込みたい。 自動で。
アーキテクチャ
まずはアーキテクチャをざっくり考えます。 絵で書くとこんな感じです。
実際に使うときの流れは以下の通り
- 写真をユーザーがトークに送信
- botAPIに通知
- botAPIがLINEから画像をDL
- 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がオーソライゼーションサーバー、リソースサーバーを提供しているので、一緒に サービス・プロバイダとして記載しました。
Googleでログインとか、Facebookでログインとかで使われていますね。 あれです。ちょっと怖いけど、非常に便利な仕組みですね。これからは安心して、「〇〇でログイン」できます。
oAuthの理解には こちらの説明が参考になりました。 こちらの絵も参考になりました。 ありがとうございます。
OAuthで認証してGoogle Driveを使うために
まずは、この赤枠をやるための準備をします。 クライアントになるには、クライアントID が必要になります。
Googleのアクセストークンの理解には、こちらを参考にしました。
ClientIDの作成
https://console.developers.google.com/apis/credentialsこちらから作成します。
認証情報を作成をクリックします。
OAuthクライアントIDを選択します。
ここで、本来はウェブアプリケーションを選択するのですが、理解のためにまずは「その他」を選択します。作成をクリック。
クライアントIDができました。
このままでは使わないので、一番右のDLアイコンをクリックしてCredentialファイルをDLしましょう。 ←これ
DLすると、こんな感じのデータが入っています。アプリケーションはこの情報をもって、クライアントとなることができます。 コールバックページを示すのは、redirect_urisです。今回のクレデンシャルファイルはlocalhostを指しています。
7. 同意画面の設定 クライアント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()
実行すると、こんな画面が開くと思います。 さきほど同意画面で、記述したものが表示されています。
私はブラウザにすでにログインしていたので、ログインしているアカウントが表示されています。
ちなみに「Qiitaのサンプル」リンクをクリックすると、このように表示されます。
認証すると、以下のような文字が。 「The authentication flow has completed.」 Authフローが完了と出てますね。
GoogleDriveにもファイルがアップロードされています。
これで、OAuthによる認可とアップロードは完了してしまいました。 認可コードやアクセストークンが出てこなかったので、もう少し詳しく見てみることにします。
認可コードとコールバックページ
実は、先程表示された「The authentication flow has completed.」が 「認可コードを含むコールバックページ」に該当します。
「The authentication flow has completed.」が表示されるページの、 クエリストリングとして渡されるcodeが認可コードになります。
http://localhost:8080/?code=4/AAAjqBdam8TJPC33jLr0eo_PQ119ZQeoyhi8t7aPZNhQHYxdtLA7Wzp3sTV7SQNevQbd1TU0j3mF7Fe7IEteroA#
PyDriveに認可コードを渡すと、 クライアントIDと認可コードをもってアクセストークンを取得し、 APIをコールしてファイルアップロードをしてくれているのでした。
ところで今回使っているのは、 Googleで作成したのは「その他」のクライアントIDです。 コールバックページはlocalhostを指していました。
今回のサンプルコードのようにローカルで動かすときは良いのですが、 Linebotで動かしたときは、localhostではないところにリダイレクトして、 codeを取得しないといけません。
コールバックページをlocalhostではない所に設定する。
再度、https://console.developers.google.com/apis/credentialsで作成します。
今度は、「ウェブアプリケーション」を選択します。
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
こちらをクリックしてみます。
先程、設定したドメイン「sample.com」が表示されます。 認証すると、「sample.com」ドメインは存在していないため、認証してリダイレクトしてもエラーになります。
しかし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を追加する
$ 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 はこちら