PerlでGoogle Calendar APIを勉強
アラートのメール通知がきたらGoogle Calendarに記入しておくとあとで振り返るのが簡単なんじゃないかなーと思ったので、まずはお勉強から。
要素
できるようになってから振り返ると、以下の要素を理解する必要があった。
- REST
- OAuth 2.0
- Application Credential
- Authorization Code
- Scope
- Access Token
- Refresh Token
- Google Calendar API 3.0
これからそれぞれの要素について書いていこうと思う。
コードはほぼコピペだが、エラー処理(status=200でないとき)の処理は簡単のため省いている。
REST
REST APIはここ数年では至るところで使われているので、どうということはないと思っている。
HTTPのGETやPOSTやPUTなどのメソッドを使い分けて、サーバにリクエストをすることでサーバから情報を得たり、サーバ側の情報を更新させるものだ。
REST APIを提供しているところでは普通APIの仕様が示されているので、それに沿ってHTTPリクエストを投げていけばいい。
OAuth 2.0
REST APIはサーバにリクエストをするだけでサーバとのやりとりができるが、勝手に読み書きできては困るようなデータも存在する。
そこで認証(Authentication)が必要となる。
また、あるアプリケーションを使ってサーバとのやりとりをさせてよいかどうか、ユーザに許可をとる認可(Authorization)が必要となる。
これらをHTTP上でやりとりする仕組みとしてOAuthが存在する。最近は2.0が使われているらしい。そういえばTwitterもOAuth 1.0はもうやめて2.0に移った。
OAuthの仕組みについては検索すればいくらでも出てくるが、手順を以下に書いていく。
Google側にも場所さえ分かれば親切なドキュメントが用意されている。
それぞれの手順で見るべきドキュメントの場所をメモしておく(コンソールで動くものは"installed apps"らしいので、それのもの)。
https://developers.google.com/accounts/docs/OAuth2InstalledApp
アプリケーションをOAuth提供者(ここではGoogle Developers Console)に登録する
コードを書く以前の問題。
https://console.developers.google.com/project で登録する。
ここでOAuth提供者からcredentialをもらう。Googleの場合はGoogle Developers Console上からJSON形式でダウンロードできる。特に重要な中身は
- client_id: アプリケーション固有のID
- client_secret: アプリケーションの内部に保持する認証キー。秘密にするほどのものかはあまり自信がない。
- auth_uri: authn, authzのときにリクエストを投げる先となるURI。Googleの場合は https://accounts.google.com/o/oauth2/auth だった。
- token_uri: Access Tokenを取得するときにリクエストを投げる先となるURI。Googleの場合は https://accounts.google.com/o/oauth2/token だった。
今回作成するのはCLIアプリなので、Google Developers Console上ではデフォルトで生成されているものは使えない。新しい client ID を発行し、リダイレクトURIが []urn:ietf:wg:oauth:2.0:oob[]
となっているようなものを手に入れ、そっちを使う。
また、APIでやりとりしたい機能の範囲をscopeというものを使って制御する。ユーザから認可をもらう際、「このアプリケーションは以下にアクセスします」的な画面を表示して認可をもらう。
Google Calendar APIの場合は https://www.googleapis.com/auth/calendar など(Read/Write)。Readonlyなものもある。提供されているscopeはGoogle Developers ConsoleのAPI一覧から見られる。
GoogleのAPIはscopeがURIっぽくなっているが、別にそうである意味はないようだ。
Authorization Codeの入手
https://developers.google.com/accounts/docs/OAuth2InstalledApp#formingtheurl
ユーザ認証とユーザの認可が済んだことを示すのがAuthorization Code。
client_id => $client_id response_type => 'code' redirect_uri => 'urn:ietf:wg:oauth:2.0:oob' scope => 'https://www.googleapis.com/auth/calendar'
- できあがるのは https://accounts.google.com/o/oauth2/auth?client_id=$client_id&response_type=code&...... といったURLになるはず。これをユーザに示し、ブラウザで開いてもらう。アプリ側はこのあと標準入力からの入力を待つことになる。
- Googleの認証を通過すると認可が求められ、ユーザがそれを認可するとブラウザ側にAuthorization Codeが発行される。ユーザはこれをアプリケーションに(コピペで)渡す。
- これでアプリケーションはAuthorization Codeを入手できた。persistentな形式で保管しておく。
- ここまでCLIアプリケーションとGoogleとの直接通信はない。あくまでURLを生成し、ユーザに踏んでもらうことになる。Webアプリケーションを作った場合は帰ってくるためのredirect_uriを指定し、作ったURLにredirectすると、認可後にそこにredirectし返してくれるようだ。試してないけど。
Access Tokenの入手
https://developers.google.com/accounts/docs/OAuth2InstalledApp#handlingtheresponse
引き続いてGoogleにリクエストを投げる。今度はアプリケーションがtoken_uriに向かってPOSTリクエストを投げることになる。
およそ以下のように投げる。
# $secretには[](Googleから取ってきたcredentialのJSONをdecodeしたもの)->{installed}[]がそのまま入っている。 # uaはFurl->newしたstateオブジェクトを返す my $api_param = { code => $authorization_code, client_id => $secret->{client_id}, client_secret => $secret->{client_secret}, redirect_uri => CLI_REDIRECT_URI, grant_type => 'authorization_code', }; my $res = ua->post( $secret->{token_uri}, [], $api_param );
これが以下の要素をもつJSON文字列を返してくれば成功。
- access_token: 今後のリクエストで使うtoken
- token_type: 上記access_tokenのtypeを示すものらしい。現在は常に'Bearer'らしい。
- expires_in: 上記access_tokenの有効期限(秒)
- refresh_token: access_tokenの有効期限が切れてしまったとき、これを使ってaccess_tokenを新しく発行してもらうためのもの。
特にrefresh_tokenは取得したらすぐに保存しておく。どうせJSONをもらうので丸ごと保存しておけばいいと思う。
Access Tokenの有効期限が切れたときどうするか
https://developers.google.com/accounts/docs/OAuth2InstalledApp#refresh
expireしてしまったaccess_tokenを使ってAPIを叩くと、401が返ってくる。
refresh_token を使って新しいaccess_tokenを要求する。
投げ先はtoken_uriで、以下のように投げる。
my $api_param = { client_id => $secret->{client_id}, client_secret => $secret->{client_secret}, grant_type => 'refresh_token', refresh_token => $access_token->{refresh_token}, }; infof("Renew access token"); debugf("token_uri:%s param: %s", $secret->{token_uri}, ddf($api_param)); my $res = ua->post( $secret->{token_uri}, [], $api_param );
これでstatus=200が返ってくると、access_tokenの最初の取得のレスポンスからrefresh_tokenを除いたJSONが返される。
これでaccess_tokenを更新すればよい。refresh_tokenがない状態でファイルを上書いてしまわないように注意。自分は1度やった。
以下のようにした。
my $obj = decode_json($res->body); $obj->{refresh_token} = $access_token->{refresh_token}; $self->save_access_token(encode_json($obj));
Google Calendar API
https://developers.google.com/google-apps/calendar/v3/reference/
access_tokenは取得できたので、あとはこれを使ってGoogleのエンドポイントにリクエストを投げていける。
投げ先は
https://www.googleapis.com/$api_name/$api_version/$resource_path?$parameters
となる。
Google Calendar APIの場合、
が固定になり、あとは使う機能次第。
access_tokenは以下の形式でリクエストのヘッダに入れる。
Authorization: $token_type $access_token
Perlで書くときはこんな感じ(GETの場合)。
my $res = ua->get( $url, ['Authorization', $access_token->{token_type} . " " . $access_token->{access_token} ], );
以下ではいくつか例を示しておく。
いずれのAPIもGoogle Developers Consoleで試すことができる。
ここで返ってくるJSONの形式を見て、コードを適宜書けば好きなように処理ができるはず。
カレンダーの一覧を取得する
ユーザのカレンダーの一覧を取得するときは resource_path=>'users/me/calendarList' としてGETリクエストを投げるとJSONが返ってくる。
特定のカレンダーに登録されているスケジュールの一覧を取得する
「スケジュール」と日本語で言うのと英語ではちょっと意味合いが違うようで、とりあえずGoogle Calendar APIではカレンダーに登録されている予定を"event"と呼ぶ。
resource_path => "calendars/$calendar_id/events"としてGETリクエストを投げることでeventの一覧を取得することができる。
eventのタイムゾーンを揃える
今回はeventの時刻(startとend)をもとに処理したかったが、カレンダー側ではtimezoneを指定してeventを登録することができるため、これを統一して取得できた方が都合がよかった。
デフォルトでは実際に登録されたときのtimezoneで時刻が出てくる (2014-03-08T03:25:40-05:00 とか)が、GETパラメータのtimeZoneに'Asia/Tokyo'などを入れることで全て+09:00に揃った形で取得することができて非常に楽。
特にTime::Piece::strptimeがtimezoneの処理に失敗し(理由は見てない)たので、後ろが揃っていることが保証されればそのまま切り捨ててstrptimeできることが大きかった。
今回はここまででひとまずやりたいことができた感じ。
今後は自動event作成とかもやってみたいので、POST APIも触っていってみたい。
ひととおり自分でリクエストを生成して仕組みがわかったので、今後はおとなしくNet::OAuth2を使おう。