ruby: 2008年9月アーカイブ

photo by Martin LaBar

 昨日はすっごいがんばって自前でGoogleAuthSub認証を通すコードを書いたのですが。
今日、続きでカレンダーを扱おうとしたら、gcalapiに同じ内容のコードがあるのを発見してしまいました。
しかもこっちの方が明らかにきれいです。だったら最初からこっちでよかったんじゃん。

再発明しちゃった車輪をもてあそびつつ、gcalapiでAuthSub認証を使う方法を記述しておきます。

Googleの認証ページ用のURLを生成する

require 'gcalapi'
require 'googlecalendar/auth_sub_util'
@uri = GoogleCalendar::AuthSubUtil.build_request_url(
'http://www.example.com/responce',
'http://www.google.com/calendar/feeds/',
false, #use_secure
true #use_session
)

Googleから戻ってきたトークンをつかってセッショントークンをもらう

authsub_token = ''
one_time_token = params[:token]
session_token = GoogleCalendar::AuthSubUtil.exchange_session_token(one_time_token)

戻ってきたページのURLからワンタイムトークンを取り出すためにget_one_time_tokenというメソッドも用意されているのですが、railsであればparams[:token]でとる方がずっと早いですね。

セッショントークンをつかってカレンダーに予定を書き込む

require 'gcalapi'
require 'googlecalendar/auth_sub_util'
require 'googlecalendar/service_auth_sub'

server = GoogleCalendar::ServiceAuthSub.new(session_token)
calendar = GoogleCalendar::Calendar.new(server, 'http://www.google.com/calendar/feeds/default/private/full')
event = calendar.create_event
event.st = Time.parse("2008-09-19 20:00:01")
event.en = Time.parse("2008-09-19 22:00:01")
event.title = "実験!"
event.desc = "こんにちはこんにちは! "
event.save!

あとは基本的に、GoogleCalendar::Service の代わりにGoogleCalendar::ServiceAuthSubを使えばいいらしい。calendar_listが動かないのはしょうがないのかなぁ。



GoogleカレンダーやGoogleDocの情報は、ユーザーの同意の下に他のアプリケーションからも読み書きができるようにAPIが定義されています。
で、そういう時に、「ここにユーザー名とパスワードを入れてね」という危険なやり方じゃなくて、ちゃんとユーザーの同意をとりつつ、アプリケーションにはパスワードを渡さなくていいようにするための認証APIが、GoogleAuthSubです。

実際に使っている例としては、携帯電話でGoogleカレンダーを読み書きできるGoogle Calendar Mobile Gatewayが有名です。

で、そのGoogleAuthSubをRails(というかRuby)で実現するためのgoogle_auth_sub.rbというのを作ってみました。
手元では動いていますが、いろいろ自信がないところがあるので、動いたとか動かなかったとか、このコードはまずいだろ、とか、いろいろフィードバックいただけると嬉しいです。

google_auth_sub.rb

使い方

 GoogleAuthSubについての情報は、この辺にまとまっています。

流れを見るのであれば最後のサイトを見るのがわかりやすいと思います。
ここでは、google_auth_sub.rbを使うことを前提に説明してみます。

まず、Googleの認証ページ用のURLを生成して、ユーザーさんに踏んでいただきます。
google_auth_sub.rbでは、こんな感じで生成します。

@uri = GoogleAuthSub.getURLForAuthSubRequest(
"https://www.google.com/calendar/feeds/",
"http://www.example.com/responce"
)
最初の引数はscope、Googleのどのサービスにアクセスしたいかを示すURLです。
二つ目は、next、ユーザーさんがGoogleのサイトでアクセスを許可した後戻ってくるページのURLです。三番目以下を使いたい人は、Googleのドキュメント見てください:-)

ともあれ、こうやって生成したURLをWEBサイトに表示して、ユーザーさんに踏んでいただきます。

そうするとユーザーさんはGoogleの認証ページに飛んで、
サイト xxxx は次のサービスで使用するため Google アカウントへのアクセスをリクエストしています。 というようなメッセージで、アプリケーションがGoogleカレンダーにアクセスすることへの確認を求められます。

ユーザーさんが「アクセスを許可」を押すと、ユーザーさんは先ほどnextで指定したURLに帰ってきます。この際、URIにToken=XXXX という形でトークンがついてきます。

これは一時トークンといって、一回しか使えませんが、Googleにお願いすると、当分使えるセッショントークンと交換してもらうことができます。

single_use_token = params[:token]
gas = GoogleAuthSub.new()
token = gas.getSessionToken(single_use_token)

セッショントークンがとれたら、あとはGoogleの各種APIにアクセスすることで、情報を取り出すことができます。このさい、セッショントークンをヘッダに入れてアクセスしないといけませんが

gas = GoogleAuthSub.new()
res = gas.googleHttpGet('https://www.google.com/calendar/feeds/default/private/full',token)
とすることで、その辺をラップして情報をとってくることができるようになります。

とってきた情報はGCal namespace element referenceというフォーマットで格納されていますが、ここから先はSubAuthじゃなくて各アプリケーションの処理なので、google_auth_sub.rbが面倒見るのはここまでです。

そのほか

GoogleAuthSub.new()の最初の引数で、CA 証明書ファイルのパスを指定します。nilで省略した場合には、HTTPSのサーバ証明書をチェックしなくなるので、セキュリティ上まずいような気がしますが、ライブラリとしては一応動作します。
CA 証明書ファイルを渡すと、VERIFY_PEERモードで証明書を検証します。でも、これで安全なのかどうなのかあまり分かっていないので、このあたりつっこみいただけるととても嬉しいです。

GoogleAuthSub.new()の二番目の引数にloggerを渡すと、Googleとのやりとりを見ることができます。

getAuthSubTokenInfo()をつかうと、トークンが有効かどうかを確認することができます。セッショントークンであっても、ユーザーが有効性を取り消すことができるので、時々チェックした方がいいのかも。
ちなみに一時トークンをこの関数に渡して有効性をチェックすることもできますが、チェックしたことでトークンを使い終わってしまうので、あんまり役にも立ちません:-)



まとめ

 res= http.get( url.path )
って書いていると、30x系のリダイレクトでパラメータ付きのURLを指示された時にはまります。url.request_uriを使いましょう。


内容

Google Calendar APIなんかを使ってWEBサービスからデータをとってくる時、net/httpを使うことはよくあるかと思います。

とってきたデータが30x系のリダイレクトであった場合まで考慮に入れたとすると、こんなコードになりますよね?

 max_retry_count = 5
 max_retry_count.times {|retry_count|
  http = Net::HTTP.new(url.host, url.port)
  http.use_ssl = true if (443==url.port)
  http.ca_file = '/var/hogehoge/www.google.com.cer'
  http.verify_mode = OpenSSL::SSL::VERIFY_PEER
  http.verify_depth = 5
  res= http.get( url.path )
  
  case res
  when Net::HTTPSuccess
   break
  when Net::HTTPRedirection
   url = URI.parse(res['Location'])
   next
  else
   break
  end
 }
 resを使ってあれこれ。

 ところが、これでGoogleカレンダーのデータをとってこようとしたらひどい目に遭います。とりあえずとってくると...

Moved Temporarily
The document has moved https://www.google.com/xxxx?gsessionid=m6Kxxxx

想定の範囲内の302 Moved Temporarilyなので、プログラムはいわれたとおりにリトライして、データをとってきます。

Moved Temporarily
The document has moved https://www.google.com/xxxx?gsessionid=1rJxxxx

え?と思いつつ、プログラムはいわれたとおりにリトライして、データをとってきます。

Moved Temporarily
The document has moved https://www.google.com/xxxx?gsessionid=Cmvxxxx

....たらい回し状態です。いっこうに目的のデータに行き着きません。

やり方が間違っているのかなぁ、それともドキュメントに読み落としがあるかなぁ、とさんざん悩んだ末、やっと原因に気がつきました。

rubyのURIライブラリは、URIを渡すと部品に分解してくれます。host/port/path/query....

そこで、http.get( url.path )を使うと? queryとして指示されている「?gsessionid=Cmvxxxx」が吹っ飛んじゃいますよね。

Googleさんから見ると、
・"xxxx"にクライアントが来たので、"xxxx?gsessionid=...."にリダイレクトを指示しました。
・なぜかクライアントはgsessionidを外して、再び"xxxx"にアクセスしてきました
・仕方がない(というか、別クライアントに見えるので)、"xxxx?gsessionid=...."にリダイレクトを指示しました。
・なぜかクライアントはgsessionidを外して"xxxx"にアクセスしてきたので....
ということになっていたわけです。

....だって みんな"http.get( url.path )" って書くじゃーん!

 res= http.get( url.request_uri )
って書くのがいいみたいです。



 文章を渡すとそこから地名や日付を抽出してくれる、Mextractr WebAPIというAPIがあります。
WEBにある文章から、そこに書かれている日付や場所、金額などが抽出できると、その内容をカレンダーに転記するとか、金額だったらうまい棒○本分に換算して表示するとか、いろいろとワクワクする展開が考えられますよね。

実は、自分も過去に同じようなことを考えて、未踏ソフトウェア創造事業に提案させていただいたことがあります。このときは、やり方にあまり斬新さがなかったので未踏的じゃない、ということでNGだったのですけど。

解析機能を提供していただけるのであれば、実現したいアプリケーションのアイデアはいくつもあるので、早速使ってみました。

使ってみた

 サンプルコードまで出していただいているので、使うのは難しくありません。
Mextractr_api_url = 'http://api.emetadata.net/mextractr?text=[[text]]&out=[[out]]&apikey=[[apikey]]'
#解析して欲しい文字列をエンコードして
text_encoded = CGI.escape( CGI.escapeHTML(text.toutf8) )
#Mextractr APIを呼び出す
uri = URI.parse( Mextractr_api_url.gsub('[[text]]',text_encoded).gsub('[[out]]','atom').gsub('[[apikey]]',@apikey) )
response = nil
Net::HTTP.start(uri.host,uri.port){|http|
	response = http.post(uri.path,uri.query)
}
で、帰ってきた結果をrexmlで解析してあげればOKです。

サンプルコードでは、結果をGETでとってきていたのですけど、URLに解析対象の文字列を丸ごと含むのはきついと思うので、POSTするようにしました。試しに投げてみたら値が帰ってきているので、きっとこれでOKです(笑)

ということで、結果のxmlをハッシュに変換するところまでで一つのクラスにまとめてみました。クラスを作って、parseメソッドにテキストを渡したら結果が帰ってきます。
mextractrAPI.rb
[名前] MextractrAPI
[概要] Mextractrサービス(http://api.emetadata.net/)をrubyから使うためのラッパークラス
[作成] 2008-08-27 古川大輔(mogya at mogya.com)
[用法] http://api.emetadata.net/からAPIキーを取得しておく必要があります。
       #取得したAPIキーをつかってMextractrAPIクラスを生成
       api = MextractrAPI.new('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
       #解析したいテキストを渡す
       result = api.parse(text)
       結果がどのように格納されているかは、 pp result して見てください。
Mashup Awards 4に応募しようと思っている方もそれ以外の方も、自由にお使いください(^^)

気づいたこと

複数の要素を検知したときに、重要度とか確からしさをつけて欲しい。
 文章中に日付とか場所が複数あった場合、それらは並列されて帰ってきます。
"where"=>
 [{"georss:point"=>"42 -104.1", "valueString"=>"沖縄"},
  {"georss:point"=>"13.15 144.5", "valueString"=>"グアム"},
  {"georss:point"=>"13.15 144.5", "valueString"=>"グアム"}],
"when"=>
 [{"valueString"=>"27日", "startTime"=>"2008-08-27"},
  {"valueString"=>"2009年度", "startTime"=>"2009-01-01"}],
 個人情報フィルタではこれでいいのだと思いますけど、文章からメタデータを取り出す上では、どれか一個、代表値が欲しいです。
たとえば、「日時:」で始まるデータは重要度が高いとか、日付の後ろに時刻まで書いてあったら確からしさが高いとか、そういう情報を付加していただけると嬉しいです。

whenで時刻は検知してくれないの?
 今のところ、whenとして帰ってくるのは日付だけみたいです。
「来る5月23日10時から、定時株主総会を墨田区の弊社本店A会議室にて開催いたします。」と渡しても、2009/5/23までしか見ていただけません。
日付しか返さないのだったらstartTimeじゃなくてstartDateのような。

バージョンアップして時刻も対応していただけることを期待しております(^^)



rubyからGoogleCalendarにアクセスできるgcalapiを使ったアプリケーションを書いているのですが、こんなエラーが出て苦労しました。

/usr/lib/ruby/gems/1.8/gems/gcalapi-0.1.1/lib/googlecalendar/event.rb:130:in `save!': [Line 8, Column 106, element gd:when] Invalid date/time value. (GoogleCalendar::EventInsertFailed)
from mail2gcal.rb:87:in `from_mail'
from mail2gcal.rb:102
from /usr/lib/ruby/1.8/net/pop.rb:528:in `each'
from /usr/lib/ruby/1.8/net/pop.rb:528:in `each_mail'
from mail2gcal.rb:97
要するに、GoogleCalendarにポストしたら "Invalid date/time value"といわれました、ということらしい。
Googleのドキュメントをあさると、
Invalid date/time value
The value you entered is not a valid date/time. Please enter a date and time in the correct format: YYYY-MM-DDThh:mm:ss. For example, "1975-09-25T06:20:00" is a valid date/time value, but "June 2005 5pm" is not.
(Invalid date/time value | Google Base Help Centre)
というのを発見。

「DateTimeは"1975-09-25T06:20:00"のフォーマットで渡してね」ということ。
そんなこと言われてもgcalapiにはDate型を渡しているし、うまくいく場合もあるからgcalapi側が変な形式のデータを渡しているとは考えにくい。ソース見ても、dt.iso8601 ってやっているし。


 試行錯誤した結果、 event.enを渡さないとエラーになるらしいことが判明。
event.st = Time.parse(yaml["st"].to_s) if yaml.has_key?("st")
# event.en = Time.parse(yaml["en"].to_s) if yaml.has_key?("en")

 gcalapiのサンプルについているmail2gcalに対して、
st: 2008-09-06 19:20:36
title: title of an event
desc: description of an event
where: location of an event
というメールを投げると再現します。

gcalapiとしては、開発者が渡したデータを適切に変換してGoogleCalendarに渡して、その結果帰ってきたエラーを適切に報告していただけているのだから、別にgcalapiが悪いわけじゃないと思うのですが。はまったので一応メモということで。

こういうのを調べるとき、原因がgcalapiや自分のアプリにあるのか、それともGoogleCalendarにあるのか切り分けるために、gcalapiがポストしたデータを見る方法ってないものでしょうか。パケットキャプチャ以外で。



このアーカイブについて

このページには、 2008年9月 以降に書かれたブログ記事のうち ruby カテゴリに属しているものが含まれています。

前のアーカイブは ruby: 2008年7月 です。

次のアーカイブは ruby: 2008年11月 です。

最近のコンテンツは インデックスページ で見られます。過去に書かれたものは アーカイブのページ で見られます。

Powered by
Movable Type