rubyの最近のブログ記事

昨日はすっごいがんばって自前で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が動かないのはしょうがないのかなぁ。
で、そういう時に、「ここにユーザー名とパスワードを入れてね」という危険なやり方じゃなくて、ちゃんとユーザーの同意をとりつつ、アプリケーションにはパスワードを渡さなくていいようにするための認証APIが、GoogleAuthSubです。
実際に使っている例としては、携帯電話でGoogleカレンダーを読み書きできるGoogle Calendar Mobile Gatewayが有名です。
で、そのGoogleAuthSubをRails(というかRuby)で実現するためのgoogle_auth_sub.rbというのを作ってみました。
手元では動いていますが、いろいろ自信がないところがあるので、動いたとか動かなかったとか、このコードはまずいだろ、とか、いろいろフィードバックいただけると嬉しいです。
使い方
GoogleAuthSubについての情報は、この辺にまとまっています。- AuthSub Authentication for Web Applications - Account Authentication API - Google Code(公式ドキュメント)
- API Developer's Guide: The Protocol - Google Calendar APIs and Tools - Google Code(GoogleカレンダーのAPIの説明の一部)
- AuthSub & ruby on rails - Google Accounts API | Google グループ(Google グループでの議論。railsで使うのにどうやったらいいか)
- Google Calendar APIの基礎 : Under Construction, Baby(日本語で説明してくれている人のページ)
流れを見るのであれば最後のサイトを見るのがわかりやすいと思います。
ここでは、google_auth_sub.rbを使うことを前提に説明してみます。
まず、Googleの認証ページ用のURLを生成して、ユーザーさんに踏んでいただきます。
google_auth_sub.rbでは、こんな感じで生成します。
@uri = GoogleAuthSub.getURLForAuthSubRequest(最初の引数はscope、Googleのどのサービスにアクセスしたいかを示すURLです。
"https://www.google.com/calendar/feeds/",
"http://www.example.com/responce"
)
二つ目は、next、ユーザーさんがGoogleのサイトでアクセスを許可した後戻ってくるページのURLです。三番目以下を使いたい人は、Googleのドキュメント見てください:-)
ともあれ、こうやって生成したURLをWEBサイトに表示して、ユーザーさんに踏んでいただきます。
そうするとユーザーさんは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 )って書くのがいいみたいです。
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のような。
バージョンアップして時刻も対応していただけることを期待しております(^^)
/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)要するに、GoogleCalendarにポストしたら "Invalid date/time value"といわれました、ということらしい。
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
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がポストしたデータを見る方法ってないものでしょうか。パケットキャプチャ以外で。
なんではまるかというと、Rubyのパッケージが複数に分かれていて、yum install rubyとしただけだと、意図するものが入らない。
- ruby
- ruby-devel
- ruby-irb
- ruby-libs
- ruby-rdoc
- ruby-ri
普通にRubyを使おうと思ったら、上記全部入れておく必要がある。
でないと、こんなことになる。
例1:railsを入れたときにwarning
(rdocがないから)
[furukawa src]$ sudo gem install rails --remote
Bulk updating Gem source index for: http://gems.rubyforge.org
Successfully installed rake-0.8.1
Successfully installed activesupport-2.1.0
Successfully installed activerecord-2.1.0
Successfully installed actionpack-2.1.0
Successfully installed actionmailer-2.1.0
Successfully installed activeresource-2.1.0
Successfully installed rails-2.1.0
7 gems installed
/usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require': no such file to load -- rdoc/rdoc (LoadError)
from /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
from /usr/lib/ruby/site_ruby/1.8/rubygems/doc_manager.rb:68:in `load_rdoc'
from /usr/lib/ruby/site_ruby/1.8/rubygems/doc_manager.rb:38:in `generate
例2:sqlite-rubyを入れたとき、ヘッダがないといわれる
(ヘッダファイルがないから)
[furukawa src]$ sudo gem install sqlite-ruby
Building native extensions. This could take a while...
ERROR: Error installing sqlite-ruby:
ERROR: Failed to build gem native extension.
/usr/bin/ruby extconf.rb install sqlite-ruby
can't find header files for ruby.
:
例2:sqlite-rubyを入れたとき、irbがないといわれる
(irbがないから)
[furukawa src]$ sudo gem install sqlite-ruby
Building native extensions. This could take a while...
Successfully installed sqlite-ruby-2.2.3
1 gem installed
/usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require': no such file to load -- irb/slex (LoadError)
from /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
:
Rubygem-develでも、「それはまずかったと思う」と指摘されています。
(Re: error when irb not installed: msg#00092)Maybe it should more gracefully fail. I was using CentOS at the time, and had installed ruby with 'yum install ruby'. It is an additional install 'yum install irb' to get irb. :(
メンテナの方がパッケージを分けたのにはそれなりの理由があるのだと思いますし、
#サーバ上だと、Rubyで書いたプログラムさえ動けばいいからかなぁ?
とはいえ、Rubyはirbも含めてRubyだと思っている人にとって見ればびっくりする状況だと思うので、とりあえず情報提供としてブログに書いておきます。
(追記)
yum一発でなんでも入る状況が空気のように当然になっていて、逆に動かないときだけ非難されちゃうメンテナの方はとても大変だと思います。この記事も、別にメンテナの方を非難したいわけではなくて、当面困らないための情報提供として書かせていただいた次第。日本語で書いて通じるかどうか知りませんが、メンテナの方々にはとても感謝していることを、ここに明記しておきます。
なるほど、と思ったので自分の勉強メモ。rubyにおけるハッシュのソートについての考え方。
rubyでハッシュにいろいろデータを入れて取り出すとき、ソートして取り出したいよね、ということがよくある。
そうすると、ハッシュをソートしたいという発想が当然出てくるのだけれど、RubyにはHash::sortというメソッドそのものがない。
なぜか。そもそもハッシュというのは順序が保存されないもの([ruby-list:43857] Hashへの生成順は保障されないのか?)。なので、順番を並べ替えるソートという考え方に意味がない。(ソートしたってその順番通りに並ぶ保証がないのだから)
じゃあ、ハッシュのデータを順序どおりに取り出すときどうするか?そういうときは、順序が保存される配列に変換してからソートを行う。
irb(main):018:0> var = {
irb(main):019:1* 2=>"2",
irb(main):020:1* 1=>"1",
irb(main):021:1* 3=>"3"
irb(main):022:1> }
=> {1=>"1", 2=>"2", 3=>"3"}
#ね、順序が変わったでしょ?
irb(main):023:0> var_sorted_array = var.to_a.sort{|a,b|
irb(main):024:1* (a[0] <=> b[0])}
=> [[1, "1"], [2, "2"], [3, "3"]]
いったん配列にしたものをソートすることで、順序を決めて取り出すことができる。
使うときはこんなふうに。
irb(main):030:0> var_sorted_array.each{|var|
irb(main):031:1* p var[0]
irb(main):032:1> p var[1]
irb(main):033:1> }
1
"1"
2
"2"
3
"3"
たのしいRubyの人とかヽ( ・∀・)ノくまくまーの人が来ていました。
yuum3の人に、初心者向けセッションをしていただきました。どうもありがとうございます!
coLinuxでの環境構築
- 資料:
- 参考:
- WEB+DB PRESS Vol.40 「特集2 coLinux,Emacs,GNU screen,zsh[定番]Linux開発環境」
- ↑今回の講習はこれが元になっている。でも、このままだと結構はまった。
- なにがうれしいのか
- Windows上のrailsはとても遅い。coLinux上のrailsのほうが早い。
- インストール
- パーソナルファイアーウォールetcがあるとはまるのではずしておく
- コア部分とFS部分のインストールがある
- FSにはubutu/Fedora7/Debianがある。Yuumi3の人はubutuを使っている
- 起動
- ネットワークの設定をすませるまではcoLinuxコンソールで。英語キーボード扱いになるので注意。
- いろいろ設定。Windowsのインターネット接続の共有機能を使ってネットワークに出られるようにする
- インストール
- rubyとかgemとかsqliteとか
- sambaでファイル共有すると、Windowsのエディタで編集できる
Scaffoldの半歩先へ
- 参考資料
- railsによるアジャイルアプリケーション開発 <いい本だけど、読みにくい
- 舞波さんの本 <開発し出すと役に立つ
- リファレンス
実習
- railsコマンドで todo プロジェクトを作成。
rails todo -d sqlite3
- config/database.yml を変更。
- script/generate で todos テーブルに対応する
- Modelを作成。
ruby script/generate model todo due:date task:string
- rake db:migrate でDBにテーブルを作る。
rake db:migrate
- script/generate scaffold でひな形を作成。
ruby script/generate scaffold todo
- script/server でサーバーを起動。
- http://localhost:3000/todosでアクセスできる
コードを見る
- 重要なのはAPP
todo\app>dir controllers helpers models views
- いわゆるMVCモデル
- 例えばapp/views/todos.vhtml
- railsであつかうhtmlテンプレート。rubyのコードっぽい物が書かれている。erbとかeRubyとか。
- <HTML>がないじゃん?:layoutsに書かれている。
- app/controller
- todo_controller.rb
- URLに応じた処理が書かれている
- 例えばlist。pagenateというのが使われている。ヘルプ見る。
- per_pageで1ページあたりの個数とか、orderでソート順とか指定できる
- todo_controller.rbのlistメソッドでorderを指定すると、ソート順が指定できる。
- debugモードだと、書いたコードがそのまま動く。
名前の規約
一つのコントローラーで何でもすると大変なので、普通、いくつかのコントローラーに分ける。
実習2
実習3
このへんは、テキストにある話をザーッと流した感じ。おまけ:ログの見方
ターミナルで見ると色つきで見える <設定すればplainTextにも出来る。ブログに記事があったはず。
感想
動かしながらやると終わらなさそうだということでしたので、ざ~っと説明しながらやって見せていただく、という感じになりました。 当然ながらこれだけでは頭に入らないので、早速やってみないと。
やっちゃった....orz
いいめもで、「間違ったときどうするの?」というユーザーさんがたくさんいたので、暫定対策として、マイナスの金額の入力を認めよう、ということになりました。
で、このとき、ユーザーさんにマイナスを入力してもらうと、-(マイナス記号)じゃなくて、「-」とか「―」とか、そういう文字をマイナスのつもりで使っちゃう人もいるよね、と思ったので、
正規表現で
/([-‐-─━―]?[0-90-9,.,.]+)/を探して価格とする、というふうにしました。
起こったこと:「コーヒー100円」と書いたら「コーヒ」が「-100円」になった。
...そりゃあそうだよねorz
「コーヒー」の危険性に気づかなかった自分の責任です。
こういうの、会社だとコード/デザインレビューでつぶせるのだけれど。できなくなって始めて分かるありがたみですね。勉強になります。