limitusus’s diary

主に技術のことを書きます

PerlでGoogle Calendar APIを勉強

アラートのメール通知がきたらGoogle Calendarに記入しておくとあとで振り返るのが簡単なんじゃないかなーと思ったので、まずはお勉強から。

要素

できるようになってから振り返ると、以下の要素を理解する必要があった。

これからそれぞれの要素について書いていこうと思う。
コードはほぼコピペだが、エラー処理(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のときにリクエストを投げる先となるURIGoogleの場合は https://accounts.google.com/o/oauth2/auth だった。
  • token_uri: Access Tokenを取得するときにリクエストを投げる先となるURIGoogleの場合は 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一覧から見られる。
GoogleAPIは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の場合、

  • api_name => 'calendar'
  • api_version => 'v3' (全API共通)

が固定になり、あとは使う機能次第。

access_tokenは以下の形式でリクエストのヘッダに入れる。

Authorization: $token_type $access_token

Perlで書くときはこんな感じ(GETの場合)。

my $res = ua->get(
    $url,
    ['Authorization', $access_token->{token_type} . " " . $access_token->{access_token} ],
);

以下ではいくつか例を示しておく。
いずれのAPIGoogle 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を使おう。