困っていたこと
LINEを使っていて、よくあるのが、
イベントのときに、なんかのグループ作って、
情報をやり取りし、イベント後に写真をLINEに投稿し合うってやつ、ありますよね。
正直、写真の保存がマジでしんどいのです。
保存が面倒なら、自動でグーグルドライブに保存してくれるBot作ったらいいじゃない!
やりたいこと
LINEでやり取りしている写真を、グーグルドライブに放り込みたい。
自動で。
今回書くこと
- アーキテクチャ
- OAuth
- サーバーレス(Chalice)
- Chaliceに外部のSDKを取り込む
アーキテクチャ
まずはアーキテクチャをざっくり考えます。
絵で書くとこんな感じです。
実際に使うときの流れは以下の通り
- 写真をユーザーがトークに送信
- 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こちらから作成します。
-
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ではない所に設定する。
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を追加する
まずはline-bot-sdkをインストールしましょう。
$ pip install line-bot-sdk
chalice のベースプログラムに、line-bot-sdkを追加しましょう。
CHANNEL_ACCESS_TOKEN
, CHANNEL_SECRET
はデプロイ後に、Lambdaのコンソールで設定するか、
ここのNoneのところに設定しましょう。
CHANNEL_ACCESS_TOKEN = os.getenv('CHANNEL_ACCESS_TOKEN', None)
CHANNEL_SECRET = os.getenv('CHANNEL_SECRET', None)
今回はこちらを実行します。
オウム返しするbotになります。
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
なんか長くなりそうなので、いったんここまでにします。