OAuthコンシューマの仕組みと実装 〜 Ruby編

前置き

前回の記事でOAuthを使ってTwitter APIにアクセスすることができるようになりましたが、ruby-oauthは内部でNet::HTTPを呼び出しているため、そのままではGoogle App Engine for Java上のJRuby(以下JRuby for GAE/J)で利用できません。

JRuby for GAE/JでもNet::HTTPが使えるようになる」というrb-gae-supportと組み合わせればOKなのかもしれませんが*1、OAuthの仕様自体はシンプルなものですし、せっかくなので勉強がてら自分で実装してみることにします。

車輪の再発明おいしいです!*2

ちなみにタイトルにRuby編と付いていますが、他の言語編を作成する予定は特にありません。

OAuthの仕様

実装の前にOAuthの仕様や、そもそもの成り立ちについて調べました。既にわかりやすいまとめ記事がいくつも書かれていますので、ここではそれらを貼り付けるだけにとどめておきます。

OAuthの成り立ちや概要を理解するのならこちら。

「認証」と「認可」の違い、OpenIDとの比較などについてはこちら。

オフィシャルな資料にも目を通しておきます。

2009年8月現在、OAuthの仕様は"1.0"と"1.0 Revision A"がありますが、この記事ではより新しい"1.0 Revision A"を元に実装しています。

実装する範囲

シンプルな仕様とは言え、仕様に載っているものを全て実装しようとするとそれなりに大変です。*3

ですので、あくまでも最終的な目標である「JRuby for GAE/Jで動作するTwitterbot」を作るのに最低限必要な機能だけに絞って実装してみたいと思います。

まず、クライアントを作るわけですから、当然“コンシューマ”を実装することになりますね。

また、前回の記事でも触れたように、現時点ではTwitterのアクセストークンに期限切れはありませんので、そこは手動で頑張ることにすると、アクセストークンを取得するまでの処理の実装は必要ありません。

更に、TwitterはHMAC-SHA1形式の署名しかサポートしていませんので、それ以外の署名を実装する必要もありません。

OAuthプロトコルパラメータの渡し方もAuthorizationヘッダを使った方法だけ用意しておけば十分でしょう。

このように切り詰めていくと、最終的には

  • 既に取得済みのアクセストークンを使い
  • HMAC-SHA1で署名されたOAuthプロトコルパラメータを
  • Authorizationヘッダに格納し
  • サービスプロバイダから保護されたリソースを取得する

という一連の手順が実行できるだけの機能があればOK、ということになります。かなりシンプルなライブラリにできそうです。

実装の前提

最終的にはJRuby for GAE/Jで動かしたいのですが、色々試行錯誤して作っていくのならば、やはり素のRubyのほうが何かとお手軽です。

というわけで、まずは普通にNet::HTTPを使って通信するようなライブラリを書くことにします。それができれば、あとはNet::HTTP部分をGAEのURLFetchに置き換えるのはそれほど難しい作業ではないからです。

一応、1.8.7と1.9.1両方で動くようにはしているつもりです。

想定する使い方

OAuthによる認証に必要なコンシューマキー、コンシューマ秘密鍵、アクセストークン、アクセストーク秘密鍵を引数に与えてインスタンスを生成し、Net::HTTPのようにgetやpostなどのHTTPメソッド名でそのままHTTP通信を実行できるような形にします。

こんな感じのコードになりそうです。

simple_oauth = SimpleOAuth.new(CONSUMER_KEY, CONSUMER_SECRET, TOKEN, TOKEN_SECRET)
response = simple_oauth.get('http://twitter.com/statuses/friends_timeline.json')
response = simple_oauth.post('http://twitter.com/statuses/update.json', {
  :status => 'This is a test tweet.'
})

各メソッドの戻り値はNet::HTTPと同じくNet::HTTPResponseのインスタンスとします。

実装

では、仕様とにらめっこしつつ、少しずつ実装を進めていってみます。

インターフェース

インターフェース部分の実装はこんな感じになります。

class SimpleOAuth
  def initialize(consumer_key, consumer_secret, token, token_secret)
    @consumer_key = consumer_key
    @consumer_secret = consumer_secret
    @token = token
    @token_secret = token_secret
    # This class supports only 'HMAC-SHA1' as signature method at present.
    @signature_method = 'HMAC-SHA1'
  end

  def get(url, headers = {})
    request(:GET, url, nil, headers)
  end

  def head(url, headers = {})
    request(:HEAD, url, nil, headers)
  end

  def post(url, body = nil, headers = {})
    request(:POST, url, body, headers)
  end

  def put(url, body = nil, headers = {})
    request(:PUT, url, body, headers)
  end

  def delete(url, headers = {})
    request(:DELETE, url, nil, headers)
  end
end

特に言及すべき部分はありません。

HTTPリクエストの実行

getやpostなどのメソッドから呼び出されるrequestメソッドの実装です。

  def request(method, url, body = nil, headers = {})
    method = method.to_s
    url = URI.parse(url)
    request = create_http_request(method, url.request_uri, body, headers)
    request['Authorization'] = auth_header(method, url, request.body)
    Net::HTTP.new(url.host, url.port).request(request)
  end

HTTPリクエストオブジェクトの作成は少々手がかかるので別メソッドにしています。

auth_headerメソッドでOAuth用の認証ヘッダを生成し、Authorizationヘッダにセットしています。

あとはそのままHTTPリクエストを実行しているだけです。

汎用メソッド

次のメソッドの実装に移る前に、これ以降パラメータのエンコードや連結などの処理が頻繁に必要になってきますので、共通に使うために汎用メソッドとして実装しておきます。

まずパラメータのエンコードから。関連する仕様は5.1. Parameter Encodingです。

  RESERVED_CHARACTERS = /[^a-zA-Z0-9\-\.\_\~]/

  def escape(value)
    URI.escape(value.to_s, RESERVED_CHARACTERS)
  end

RESERVED_CHARACTERSは仕様に明記されているものをそのまま持ってきただけです。

次にパラメータの連結を。この処理は各パラメータのエスケープも含みます。

  def encode_parameters(params, delimiter = '&', quote = nil)
    if params.is_a?(Hash)
      params = params.map do |key, value|
        "#{escape(key)}=#{quote}#{escape(value)}#{quote}"
      end
    else
      params = params.map { |value| escape(value) }
    end
    delimiter ? params.join(delimiter) : params
  end

場所によって値を引用符で囲う必要があったり、連結する際の区切り文字が異なったりするので、全てに対応できるように柔軟な実装にしています。

HTTPリクエストオブジェクトの作成

では、先程のrequestメソッドから呼び出されていたメソッドの実装に戻ります。

  VERSION = '0.1'
  USER_AGENT = "SimpleOAuth/#{VERSION}"

  def create_http_request(method, path, body, headers)
    method = method.capitalize.to_sym
    request = Net::HTTP.const_get(method).new(path, headers)
    request['User-Agent'] = USER_AGENT
    if method == :Post || method == :Put
      request.body = body.is_a?(Hash) ? encode_parameters(body) : body.to_s
      request.content_type = 'application/x-www-form-urlencoded'
      request.content_length = (request.body || '').length
    end
    request
  end

引数で受け取ったメソッド名に対応するクラスをconst_getで取得し、インスタンスを生成します。

リクエストボディの形式は"application/x-www-form-urlencoded"しか想定していません。Twitter APIで使うなら他は特に必要ないかな、と思いますので…。

せっかくなのでUser-Agentも入れるようにしました。

OAuth認証用ヘッダの生成

もう一つ、requestメソッドから呼び出されていたメソッドの実装です。Authorizationヘッダにセットする認証用データを生成します。関連する仕様は5.4. OAuth HTTP Authorization Schemeです。

  def auth_header(method, url, body)
    parameters = oauth_parameters
    parameters[:oauth_signature] = signature(method, url, body, parameters)
    'OAuth ' + encode_parameters(parameters, ', ', '"')
  end

oauth_parametersメソッドでOAuthプロトコルパラメータを取得し、そこにメソッド名、URL、リクエストボディなどの情報を混ぜて、signatureメソッドで“署名”を生成します。署名をoauth_signatureパラメータにセットしたら、まとめてエンコードしてOAuth認証用ヘッダの完成です。

OAuthプロトコルパラメータの用意

では、そのoauth_parametersメソッドの実装です。OAuth認証に必要な一連のパラメータを用意します。関連する仕様は7. Accessing Protected Resourcesです。

  OAUTH_VERSION = '1.0'

  def oauth_parameters
    {
      :oauth_consumer_key => @consumer_key,
      :oauth_token => @token,
      :oauth_signature_method => @signature_method,
      :oauth_timestamp => timestamp,
      :oauth_nonce => nonce,
      :oauth_version => OAUTH_VERSION
    }
  end

必要なパラメータはコンシューマキー、アクセストークン、署名の形式、タイムスタンプ、ランダムな一意の文字列、そしてOAuthのバージョンです。

前者3つは既にインスタンス変数としてセット済みです。timestampとnonceメソッドは次に。OAuthのバージョンは現状'1.0'固定です。

タイムスタンプとNonce

timestampとnonceメソッドの実装です。関連する仕様は8. Nonce and Timestampです。

  def timestamp
    Time.now.to_i.to_s
  end

  def nonce
    OpenSSL::Digest::Digest.hexdigest('MD5', "#{Time.now.to_f}#{rand}")
  end

タイムスタンプはそのまま、現在時刻の基準時からの経過秒です。

Nonceはランダムな一意の文字列であれば、他に細かい指定はありません。リクエストのユニーク性を維持するためだけの値です。ここではとりあえず現在時刻にランダム数値を混ぜたもののMD5ハッシュ値としています。

MD5ハッシュ値の生成にDigest::MD5ではなくOpenSSLモジュールを利用しているのは、どちらにせよ後でOpenSSLモジュールが必要になるためです。

  def nonce
    Digest::MD5.hexdigest("#{Time.now.to_f}#{rand}")
  end

でも同じです。

署名

署名メソッドの実装に移ります。関連する仕様は9. Signing Requestsです。

  def signature(*args)
    base64(digest_hmac_sha1(signature_base_string(*args)))
  end

前述のように、今回はHMAC-SHA1による署名しかサポートしませんので、実装もHMAC-SHA1限定となっています。

署名の手順としては、

  1. 受け取ったパラメータ(メソッド名、URL、リクエストボディ、OAuthプロトコルパラメータ)からSignature Base String(署名基準文字列)を作成する。(signature_base_stringメソッド)
  2. Signature Base Stringと秘密鍵を組み合わせてHMAC-SHA1のダイジェスト値を生成する。(digest_hmac_sha1メソッド)
  3. ダイジェスト値をBase64エンコードする。(base64メソッド)

となります。

HMAC-SHA1ダイジェストとBase64エンコード

この二つは非常にシンプルですので、先に実装を済ませてしまいます。関連する仕様は9.2. HMAC-SHA1です。

  def base64(value)
    [ value ].pack('m').gsub(/\n/, '')
  end

  def digest_hmac_sha1(value)
    OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, secret, value)
  end

  def secret
    escape(@consumer_secret) + '&' + escape(@token_secret)
  end

base64メソッドは単にBase64エンコーディングを施しているだけです。値中の改行は不要なので全部削っています。

HMACの仕組みは以下のページが参考になります。ここではOpenSSLモジュールの力を借りてサクっと。

secretメソッドは秘密鍵の生成です。秘密鍵はコンシューマ秘密鍵とアクセストーク秘密鍵をそれぞれエスケープして&記号で連結したものになります。

Signature Base Stringの作成

ではsignature_base_stringメソッドの実装に移ります。関連する仕様は9.1. Signature Base Stringです。

  def signature_base_string(method, url, body, parameters)
    method = method.upcase
    base_url = signature_base_url(url)
    parameters = normalize_parameters(parameters, body, url.query)
    encode_parameters([ method, base_url, parameters ])
  end

メソッド名は大文字にします。

URLは次のような処理で必要な部分だけに削ぎ落とします。

  def signature_base_url(url)
    URI::HTTP.new(url.scheme, url.userinfo, url.host, nil, nil, url.path,
                  nil, nil, nil)
  end

OAuthプロトコルパラメータ、リクエストボディ、URL中のクエリデータは次のような処理で“正規化”を施します。

  def normalize_parameters(parameters, body, query)
    parameters = encode_parameters(parameters, nil)
    parameters += body.split('&') if body
    parameters += query.split('&') if query
    parameters.sort.join('&')
  end

これらのデータに含まれる“名前=値”のペア(それぞれエスケープ済み)をごちゃ混ぜにしてソートし、ひとかたまりに連結します。

以上のようにして得られた大文字メソッド名・基本URL・正規化パラメータをエスケープ&連結すればSignature Base Stringの完成です。

完成!

上から下へ実装していったので、どこまで実装が終わっているのかがわかりにくかったかもしれませんが、これで全ての実装が完了です。ここまでのコードをコピーしてまとめるだけで動くようになります。*4

使い方のサンプル

では実際に、今作ったライブラリ…simple-oauth.rbという名前で保存したとします、を使ってTwitterAPIへアクセスするサンプルコードを書いてみます。

#!/usr/bin/env ruby
# coding: utf-8

require 'simple-oauth'
require 'rubygems'
require 'json'

# ここを置き換える
CONSUMER_KEY = 'CONSUMER-KEY'
CONSUMER_SECRET = 'CONSUMER-SECRET'
TOKEN = 'ACCESS-TOKEN'
TOKEN_SECRET = 'ACCESS-TOKEN-SECRET'

simple_oauth = SimpleOAuth.new(CONSUMER_KEY, CONSUMER_SECRET, TOKEN, TOKEN_SECRET)

# Tweetの投稿
response = simple_oauth.post('http://twitter.com/statuses/update.json', {
  :status => "こんにちは!この投稿はテストです。 : #{Time.now}"
})
raise "Request failed: #{response.code}" unless response.code.to_i == 200

# TimeLineの取得
response = simple_oauth.get('http://twitter.com/statuses/friends_timeline.json?count=5')
raise "Request failed: #{response.code}" unless response.code.to_i == 200
JSON.parse(response.body).each do |status|
  puts "#{status['user']['screen_name']}: #{status['text']}"
end

コンシューマキーやアクセストークンは事前に取得しておいたものと置き換えてください。

これを実行すると、Tweetが一つ投稿され、またタイムライン中の最新5件のつぶやきが表示されます。

疑問点

とりあえず動くようにはなったものの、実はまだちょっと仕様の理解にあやふやな部分があるんですよね…。

それはURL中のクエリデータとリクエストボディの扱いです。

これまで見てきてわかる通り、これらはSignature Base Stringの生成のために一旦分解する必要があるのですが、この際「値の再エスケープまでする必要があるのかどうか」がわかっていません。

OAuthの仕様で定められているunreservedな文字は、Rubyで一般的にクエリデータのエスケープに使われているCGI#escapeのそれとは微妙に異なります。そのため、値をアンエスケープして再度エスケープした場合とそうでない場合とで、生成されるダイジェストが一致しないパターンが出てきます。

もちろん、再エスケープする必要があるのであれば、ダイジェストと実データに差異が出ないようにするため、リクエスト中のクエリデータとリクエストボディも再エスケープしたものに置き換える必要があります。

ダイジェストの生成に使ったデータと実際のクエリデータ・リクエストボディに差異さえなければ問題ないだろう(=再エスケープまでは要らないだろう)、とは思うのですが。この辺りご存知の方がいらっしゃいましたら是非ご教示ください。

まとめ

以上で「TwitterAPIを使うために最低限必要な機能を持ったOAuthライブラリ」*5はひとまず完成です。

今回書いたコードはGitHubに置いておきました。

中身はこの記事に書いてあるものと同一です。「分割されたコードをいちいちコピペするのが面倒臭い!」という場合はこちらから取得してください。

後はこれをJRuby for GAE/J用に書き直したいわけですが、それはまた長くなりそうなので次の機会に。

*1:実際動くかどうかは未確認です。

*2:ただし趣味の範囲に限る。

*3:ruby-oauthのコードサイズを見れば大体どの程度の規模かわかりますね。

*4:いくつかのrequireは追加する必要がありますが。

*5:なげぇ!

TwitterのbotをOAuthに対応させる

Twitterの認証方式について

Twitterの認証方式にはOAuthとBasic認証の二通りの仕組みがあります。

Basic認証はお手軽なので、世に存在するTwitter APIを利用する一般的なクライアントは、大抵がこちらの方法を使っているのではないかと思います。

ですが、実はこのBasic認証はセキュリティ上の理由から、将来的に非推奨になることが予告されています。

既にTwitter API Wikiでは「今後作成するクライアントはOAuthを利用することを強く推奨する」と表明されており、それに応じてOAuthを使うクライアントも増えてきています。

…が、現状Basic認証を利用しているクライアントが既に大量に存在していることを考えると、今日明日でBasic認証が使えなくなる可能性はそう高くはないでしょう。先程のFAQでも、将来的に非推奨にすることは考えているが、日程は全くの未定である…と書いてあります。

何故OAuthを使いたいか

しかし実は、そういうセキュリティ的な事情以外にも、bot製作者的に「OAuthに移行したい!」と思わせる嬉しい利点がOAuthには用意されているのです。

それは投稿したTweetに表示される“クライアント名”を任意に設定できるということです。Web上でタイムラインを見たときに「webで」とか「Tweenで」とか表示されている部分ですね。

昔はBasic認証を利用するクライアントでもクライアント名を登録できたのですが、現在はOAuthへの移行を促すためか、OAuthを利用するクライアントしか登録することができません。その代わり、一人で複数のクライアントを登録できたり、いつでも好きなときに名前を変えることができたりと、かなり自由にクライアント名を弄ることができます。

これを使えば、例えばネタ系のbotなんかは、無機質な「APIで」という表示の変わりに、そのbot専用のお茶目な名前なんかを設定できたりしちゃうわけです。実際にこれを利用してユニークなクライアント名を表示しているbotもちらほら見かけますね。

ということで、セキュリティの向上というよりも*1むしろ、クライアント名を好きに弄ることを主目的として、botをOAuthに対応させる手順を書いていこうと思います。使用する言語はRubyです。

OAuthの仕組み

OAuthの仕組みについては以下のサイトなどの解説を参照してください。

認証の仕組みに馴染みが薄いと理解が難しい部分ですので、この記事では「使い方」以上のことは言及しません。

以下の記述でも、「クライアント=コンシューマ+ユーザ」くらいの大雑把な概念で用語を使用しています。

RubyでOAuthを利用する準備を整える

実際の手順に取りかかる前に、RubyでOAuthを利用する準備をします。

幸いruby-oauthというライブラリが存在していますので、ありがたく使わせてもらうことにしましょう。

$ gem install oauth

必要ならroot権限で実行してください。

これであとは

require 'rubygems'
require 'oauth'

すれば使えるわけですが、ここで一点注意しなければならないことがあります。

どうやらこのruby-oauthはRuby1.9系に完全には対応していないようで*2、そのまま使おうとすると例外が発生してしまいます。

とりあえずRuby1.9系でも最低限の機能だけは使えるようにするパッチを書いてみました。完全なものではありませんが、この記事に書いてあるレベルのことなら実行できるようになります。

oauth-patch.rb
if RUBY_VERSION >= '1.9.0'
  # エンコーディングの違いのせいで、
  # 日本語の文字列をpostパラメータに含めようとするとエラーが出ます。
  # 無理矢理エンコーディングをUTF-8に変えて再試行することで回避。
  module OAuth
    module Helper
      def escape(value)
        begin
          URI::escape(value.to_s, OAuth::RESERVED_CHARACTERS)
        rescue ArgumentError
          URI::escape(
            value.to_s.force_encoding(Encoding::UTF_8),
            OAuth::RESERVED_CHARACTERS
          )
        end
      end
    end
  end

  # 1.9から文字列がEnumerableでなくなりましたので、
  # その対応をしています。
  module HMAC
    class Base
      def set_key(key)
        key = @algorithm.digest(key) if key.size > @block_size
        key_xor_ipad = Array.new(@block_size, 0x36)
        key_xor_opad = Array.new(@block_size, 0x5c)
        key.bytes.each_with_index do |value, index|
          key_xor_ipad[index] ^= value
          key_xor_opad[index] ^= value
        end
        @key_xor_ipad = key_xor_ipad.pack('c*')
        @key_xor_opad = key_xor_opad.pack('c*')
        @md = @algorithm.new
        @initialized = true
      end
    end
  end
end

以降のサンプルプログラムでも、この"oauth-patch.rb"をrequireして利用しています。

OAuthクライアントを登録する

まず、以下のページにアクセスしてクライアントを登録します。

このとき、クライアント製作者のアカウントでWebからTwitterにログインした状態でアクセスしてください。作成したクライアントはTwitterのアカウントと紐付けられるためです。

"Register a new application"のリンクをクリックして登録画面へ進み、必要な情報を入力して「保存する」をクリックしてください。最低限"Application Name"、"Description"、"Application Website"が入力されていれば良いようです。

"Application Name"や"Description"には日本語も使えます。"Application Website"にはbotの説明ページなどがあればそのURLを、無ければ自分のサイトなど、製作者がわかるようなURLを入れておいたほうが良いでしょう。

それから、botから利用することを考えると

  • "Application Type"は"Client"に
  • "Default Access type"は"Read & Write"に

設定しておくことも必要でしょう。特に後者は"Read-only"のままにしておくと、そのクライアントを介したTweetの投稿などができなくなってしまうようです。

登録に成功すると"Application Details"の画面に移動します。そこに表示されている"Consumer key"と"Consumer secret"の値を使用しますので、メモって(コピペして)おいてください。

この"Application Details"画面には、先程の"Applications Using Twitter"画面からいつでもアクセスすることができます。また、クライアント名の変更などもこの画面から行えます。*3

アクセストークンを取得する

次に、このOAuthクライアントにbotアカウントでのアクセス許可を与え、アクセストークンを取得します。

この辺の話はOAuthの仕組みを知らないと少々ややこしいかもしれません。気になる方は先程のサイトなどでおさらいしてください。気にならない方は以下書いてある通りに実行すればOKです。

このアクセストークンを取得する手順も全自動で行えると良いのかもしれませんが、幸いTwitterはアクセストークンに有効期限を設定していないようですので、この手順を実行するのは最初の一回だけということになります。

ということであまり頑張らずに、一部手動を介して実行することにします。

次のような半自動スクリプトを書いてみました。

twitter-oauth.rb
#!/usr/bin/env ruby
# coding: utf-8

require 'rubygems'
require 'oauth'
require 'oauth-patch'

CONSUMER_KEY = 'CONSUMER-KEY' # ←ここを書き換える
CONSUMER_SECRET = 'CONSUMER-SECRET' # ←ここを書き換える

consumer = OAuth::Consumer.new(
  CONSUMER_KEY,
  CONSUMER_SECRET,
  :site => 'http://twitter.com'
)

request_token = consumer.get_request_token

puts "Access this URL and approve => #{request_token.authorize_url}"

print "Input OAuth Verifier: "
oauth_verifier = gets.chomp.strip

access_token = request_token.get_access_token(
  :oauth_verifier => oauth_verifier
)

puts "Access token: #{access_token.token}"
puts "Access token secret: #{access_token.secret}"

ソース中のCONSUMER-KEYとCONSUMER-SECRETを先程メモった値に書き換えて保存してください。

スクリプトを実行すると、以下のような出力が表示されます。REQUEST-TOKENの部分には実際にはもっと複雑な文字列が入っています。

$ ruby twitter-oauth.rb
Access this URL and approve => http://twitter.com/oauth/authorize?oauth_token=REQUEST-TOKEN
Input OAuth Verifier: 

'Access this URL and approve =>'の先に示されるURLに、botのアカウントでWebからTwitterにログインした状態でアクセスしてください。

"Allow (クライアント名) access?"、"拒否する"、"Allow"と表示されたページに移動すると思います。ページ内にクライアント名やその製作者名が表示されていますので、間違いが無いことを確認して"Allow"をクリックしてください。*4

"Allow"すると"You've successfully granted access to (クライアント名)!"と表示された画面に移動し、でかでかと7桁の*5数字が表示されると思います。

先程のスクリプトが入力待ちの状態で止まっていますので、この数字をそこに入力してください。

すると、最終的に以下のような出力が表示されてスクリプトが終了します。

Access token: ACCESS-TOKEN
Access token secret: ACCESS-TOKEN-SECRET

ACCESS-TOKEN、ACCESS-TOKEN-SECRETには実際にはもっと複雑な文字列が表示されます。この二つの値をメモって(コピペして)おいてください。

ここまでで準備は完了です。

アクセストークンを使ってTwitter APIを利用する

では、取得したアクセストークンを使ってTwitter APIを利用し、タイムラインの取得やTweetの投稿をするサンプルスクリプトを書いてみます。

twitter-oauth-access.rb
#!/usr/bin/env ruby
# coding: utf-8

require 'rubygems'
require 'oauth'
require 'oauth-patch'
require 'json'

CONSUMER_KEY = 'CONSUMER-KEY' # ←ここを書き換える
CONSUMER_SECRET = 'CONSUMER-SECRET' # ←ここを書き換える
ACCESS_TOKEN = 'ACCESS-TOKEN' # ←ここを書き換える
ACCESS_TOKEN_SECRET = 'ACCESS-TOKEN-SECRET' # ←ここを書き換える

# 下準備
consumer = OAuth::Consumer.new(
  CONSUMER_KEY,
  CONSUMER_SECRET,
  :site => 'http://twitter.com'
)
access_token = OAuth::AccessToken.new(
  consumer,
  ACCESS_TOKEN,
  ACCESS_TOKEN_SECRET
)

# タイムラインを取得して時系列順に表示
response = access_token.get('http://twitter.com/statuses/friends_timeline.json')
JSON.parse(response.body).reverse_each do |status|
  user = status['user']
  puts "#{user['name']}(#{user['screen_name']}): #{status['text']}"
end

# Tweetの投稿
response = access_token.post(
  'http://twitter.com/statuses/update.json',
  'status'=> 'このメッセージはOAuth認証を通して投稿しています。'
)

CONSUMER-KEY、CONSUMER-SECRET、ACCESS-TOKEN、ACCESS-TOKEN-SECRETを先程メモった値に書き換えて保存してください。日本語が含まれていますので、文字コードUTF-8にすることを忘れないでください。

スクリプトを実行すると、タイムラインの直近20件が表示され、また「このメッセージはOAuth認証を通して投稿しています。」というTweetが投稿されます。

$ ruby twitter-oauth-access.rb

投稿した内容は実際にWebにアクセスするなどして確認してみてください。「APIで」の部分が、先程登録したクライアント名で表示されていたら成功です。また、"Application Details"の画面からクライアント名を変更すると、タイムライン上の表示にも即座に反映されることがわかると思います。

上のソースを見てもらえればわかりますが、ユーザ名やパスワードなどの情報は一切保持していません。

アクセストークンなどの情報は保持していますが、このアクセストークンに対するアクセス許可は、ユーザが任意に取り消すことができます。

botのアカウントでTwitterの「設定」ページを開いてみてください。一番右に「このユーザーに対する操作」というタブが増え、そこにアクセスを許可したクライアントがリストアップされています。ここからいつでも「許可を取り消す」ことができます。

この辺りがOAuthがBasic認証よりもセキュリティ的に優れていると言われる理由なわけですね。*6

botスクリプトをアクセストークンを利用するように書き換える

さて、アクセストークンを使ったTwitter APIの利用方法がわかりましたので、あとはbotスクリプトを書き換えてやれば、いつでも好きなときにクライアント名を変更できるbotの完成です。

実際にどこをどう書き換えればいいのかは、利用しているライブラリに依存しますので一概には言えません。

一例を挙げますと、jugyo氏作のRubyTwitterライブラリ"Rubytter"は、予めOAuthを使ったアクセス方法が組み込まれていますので、上のサンプルスクリプトでも作成したOAuth::AccessTokenクラスのインスタンスさえあれば、すぐにでもOAuth対応させることができます。

OAuthに対応していないライブラリの場合は、Net::HTTPを呼び出している部分をOAuth::AccessTokenクラスのメソッドに書き換える必要があります。Net::HTTPとOAuth::AccessTokenのメソッド間対応は下のページなどを参考にしてください。シンプルに1対1対応していますので、あまり迷うこともないかと思います。

他に、コンシューマキーやアクセストークンキーなどの情報を渡す仕組みも用意してやる必要があるかもしれません。

クラッチから書き起こしている場合も、やはりNet::HTTP部分をOAuth::AccessTokenに置き換えてやればOKです。既存のライブラリを書き換えるよりは、こちらのほうが融通が利きますので楽かもしれませんね。

まとめ

長くなりましたが、要点としては

  1. クライアントを登録し
  2. 半自動スクリプトを使ってアクセストークンを取得し
  3. botスクリプトのNet::HTTP部分をOAuth::AccessTokenに置き換える

という手順でOK、ということになります。

スクリプトの作りによっては若干手間がかかるかもしれませんが、一回手順を覚えてしまえば、あとは流れ作業なのでカンタンです。手順1と手順2でログインするTwitterアカウントを切り替えないといけないのが注意点といえば注意点でしょうか。*7

以上です。OAuthを使って、是非愛するbotにユーモア溢れるクライアント名を付けてあげてください!*8

*1:もちろんそれも大事なのですが

*2:あるいは私の使い方が悪いだけなのかもしれませんが…

*3:"Edit Application Settings"ボタン

*4:余談ですが、他のクライアントに対してあまり考えずに"Allow"することは、セキュリティ上の重大な過失となり得ます。十分に注意してください。詳しくはOAuthを悪用したTwitter DMスパムが登場 - まちゅダイアリー(2009-08-01)などを。

*5:桁数は変動するかも

*6:Twitter以外のサービスにIDやパスワードを教える必要が無い、という意味で。botのように自分で管理しているものなら、その辺りはさほど関係ないかもしれません。

*7:クライアントとbotが1対1対応するなら、両方botのアカウントでも良いのかもしれません。

*8:あまり頻繁に設定を変更するとTwitterから怒られるかもしれませんので、ほどほどに!

JRuby on Google App Engineでのログ出力

前置き

Google App Engine for Java(以下GAEJ)でJRubyスクリプトを書くにあたって、ログ出力をどうしたらいいのか、少し迷ってしまいませんか?

標準出力や標準エラー出力に書き出したデータは単に無視されるだけですし、書き込みモードでファイルを開けませんので、RubyLoggerクラスをそのまま使うことも出来ません。かといって、ログ出力無しでは何かと不便です。

ということで、GAEJ上のJRubyスクリプトでログ出力する方法を調べてみました。

Javaでのログ出力

JRubyJava上で動作しており、Javaのクラスを利用することができますので、Javaでどのようにログを出力するのかがわかれば解決したも同然です。

そこでGAEのデベロッパーガイドをざっと眺めてみました、が…。

どうにもそれっぽい項目が見当たりません。

よく探してみたら、全然関係なさそうな以下のページの下の方でさらっとログ出力について触れられていました。

ログ出力って結構重要な要素だと思うんですが、そうでもないんでしょうか…?

ともあれ、上のページによると、GAEJではjava.util.logging.Loggerクラスを利用してログ出力すれば良いそうです。つまり、JRubyからこのクラスを使わせてもらえばいいわけですね。

JRubyからjava.util.logging.Loggerを使う

インスタンス作成

java.util.logging.Loggerクラスのリファレンスを見てみると、どうやらLoggerクラスのインスタンスを取得するには複数の手段があるようです。

Rubyコードで書くと以下のような感じになります。

import java.util.logging.Logger

global_logger = Logger.getLogger(Logger::GLOBAL_LOGGER_NAME)
anonymous_logger = Logger.anonymous_logger
my_logger = Logger.getLogger('MyName')

上から順にグローバルロガー、匿名ロガー、名前付きロガー…らしいですが、Javaに明るくない私にはどう使い分けたらいいのかよくわかりません。正しく使い分けておけば、ログのネームスペースによってフィルタリングできる…とかそんな感じでしょうか。

ちょっと調べてみた範囲では、「とりあえず使うだけ」ならどれを使ってもあまり差は無さそうです。

ログ出力メソッド

Loggerクラスにはたくさんのメソッドがありますが、まず必要なのはレベル別のログ出力メソッドですね。

ログレベルを定義しているjava.util.logging.Levelクラスでは、レベルは低い順にFINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVEREとなっています。メソッドもそれぞれ同名のものが用意されているようです。

しかし、GAEダッシュボードのログ表示画面での表記はPython用のloggingモジュールで定義されているログレベルに合わせてあり、java.util.logging.Levelのそれとは名称が異なっています。

GAEJでは以下のような対応になっているようです。

my_logger.finest('hello') # = Debug
my_logger.finer('hello') # = Debug
my_logger.fine('hello') # = Debug
my_logger.config('hello') # = Debug
my_logger.info('hello') # = Info
my_logger.warning('hello') # = Warning
my_logger.severe('hello') # = Error

Criticalに対応するレベルは無いみたいです。とりあえずsevere, warning, info, あとはfine*1メソッド辺りがあれば十分、といったところでしょうか。

ログ出力レベルの変更

GAEではデフォルトのログ出力レベルはWARNING以上となっていますので、そのままではINFO以下のログは無視されてしまいます。*2

java.util.logging.Loggerクラスにはログ出力レベルを設定するsetLevelメソッドなどもありますので、それを利用しても良いのですが、設定ファイルによってアプリケーションのデフォルトログ出力レベルを指定できますので、今回はその方法をとることにします。

設定ファイルの書き方などは、先程も参照した以下のページに記載されています。

まずWEB-INF/appengine-web.xmlファイルを編集し、ログ設定ファイルの在り処をアプリケーションに教えてあげる記述を追加します。

$ vim WEB-INF/appengine-web.xml
<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
 …(略)…
  <system-properties>
    <property name="java.util.logging.config.file" value="WEB-INF/logging.properties" />
  </system-properties>
</appengine-web-app>

通常は既にsystem-properties要素が存在すると思いますので、その中に追加してやればOKです。

次に、上で指定したログ設定ファイルを作成します。1から書いてもいいのですが、SDK中にサンプルが用意されているようですので、そいつを持ってきます。

$ cp ../appengine-java-sdk-1.2.1/config/user/logging.properties WEB-INF

サンプルのままだとログ出力レベルがWARNINGのままですので、ここはひとまずFINEにでも変えておくとします。*3

$ vim WEB-INF/logging.properties
# Set the default logging level for all loggers to WARNING
#.level = WARNING
.level = FINE

これで全てのログが出力されるようになります。*4

その他のログ出力方法

実は、JRubyからログ出力するには、java.util.logging.Loggerクラスを使う方法の他に、もっと手軽な方法もあるみたいです。

$servlet_context.log('hello')

$servlet_contextというのはJRuby-Rackによって定義されるグローバル変数のようですね。名前からなんとなく役割は推測できますが、あまり詳しく調べてはいません。

この$servlet_contextを使ったログ出力は、logメソッド一つのみで、レベルの指定などもできません。このメソッドで出力したログは常にINFOレベルとして扱われるようです。

レベルに拘らず、ちょっとログに出したい…というようなときは、こちらのほうがお手軽で良いかもしれませんね。

GAEKit::Loggerを使う

あとはもう好きなようにログ出力すればいいだけなのですが、せっかくなのでこれもラッパークラスを書いてみました。前回紹介したGAE Toolkitに追加してあります。

GAEKitモジュール内のLoggerクラスがそれです。

ライブラリのインストール方法などは前回の記事を参照してください。

使い方

java.util.logging.Loggerクラスの三種類のインスタンスのうち、どれを使うようにすればいいのかよくわからなかったので、とりあえず全部呼び出せるようにしました。

引数を与えてGAEKit::Logger.newした場合は、通常の名前付きロガーが返ります。引数には自分のクラス名でも渡しておけばOKなんじゃないかと思います。*5

logger = GAEKit::Logger.new(self.class.name)
logger.info('This is a named logger.')

引数を与えずにGAEKit::Logger.newした場合は、匿名ロガーが返ります。

logger = GAEKit::Logger.new
logger.info('This is an anonymous logger.')

インスタンスを作らず、クラスメソッドでログ出力することもできます。この場合はグローバルロガーが使われます。

GAEKit::Logger.info('This is a global logger.')

また、レベル別ログ出力の各メソッド名は、RubyのLoggerクラスのそれに合わせてあります。java.util.logging.Levelとの対応は以下のようになります。

logger.debug('hello') # = FINE
logger.info('hello') # = INFO
logger.warn('hello') # = WARNING
logger.error('hello') # = SEVERE
logger.fatal('hello') # = SEVERE

基本的にfatal以外はGAEダッシュボードでの表記そのままと思ってもらって構いません。

java.util.logging.Level側に対応するレベルが無いために、fatalをCriticalとして扱えないのが残念ですが…。自分で勝手に定義できるものなのでしょうか。

そうそうそれと、java.util.logging.Loggerはメッセージに続く引数として例外オブジェクトを渡すと、そのスタックトレースをログに出力することができるようなので、それっぽいこともできるようにしました。

begin
  raise 'This is a dummy error.'
rescue
  logger.error('Caught error', $!)
end

ログを読みやすくする

ダウンロード

これで手軽にログ出力できるようになったわけですが、個人的にGAEダッシュボードのログ表示画面は若干使いにくいような気がします。

それに、やっぱりログはテキスト形式で扱えたほうが、色々加工しやすくて便利ですよね。

GAEドキュメントの以下のページによると、ログをテキスト形式でダウンロードする手段も用意されているようです。

こんな感じでコマンドを実行すると、全てのログを"mylogs.txt"としてダウンロードできます。

$ ../appengine-java-sdk-1.2.1/bin/appcfg.sh --num_days=0 --severity=0 request_logs . mylogs.txt

オプションの説明は上のページの"Command-Line Arguments"あたりを参照してください。

フォーマットの加工

ダウンロードできたはいいんですが、なんか妙に見難いフォーマットなんですよね…。リクエスト毎に、出力されたログが1行にまとめられているみたいです。

60.32.80.93 - - [16/Jul/2009:02:35:50 -0700] "GET / HTTP/1.1" 200 148 "http://appengine.g
oogle.com/" "Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.1) Gecko/20090624 Firefo
x/3.5 (.NET CLR 3.5.30729) AutoPager/0.5.2.2 (http://www.teesoft.info/),gzip(gfe)"^@0:124
7736950.569000 sun.reflect.NativeMethodAccessorImpl invoke0: hello^@: ^@1:1247736950.5720
00 sun.reflect.NativeMethodAccessorImpl invoke0: hello^@: ^@2:1247736950.573000 sun.refle
ct.NativeMethodAccessorImpl invoke0: hello^@: ^@3:1247736950.573000 sun.reflect.NativeMet
hodAccessorImpl invoke0: hello^@: ^@3:1247736950.573000 sun.reflect.NativeMethodAccessorI
mpl invoke0: hello^@: 

さすがにこのままでは読めないので、RubyのLoggerっぽいフォーマットに加工するスクリプトを書いてみました。*6

#!/usr/bin/env ruby
# coding: utf-8
require 'time'

class Formatter
  LEVELS = {
    '0' => [ 'D', 'DEBUG' ],
    '1' => [ 'I', ' INFO' ],
    '2' => [ 'W', ' WARN' ],
    '3' => [ 'E', 'ERROR' ],
    '4' => [ 'F', 'FATAL' ],
  }

  def filter(src = STDIN, dst = STDOUT)
    src.readlines.reverse_each do |line|
      @index = (@index || 0).succ
      @session = sprintf("#%05d", @index)
      line.split("\0")[1..-1].each do |log|
        dst.puts log if log = format(log)
      end
    end
  end

  def format(log)
    return nil unless log.match(/^(\d?): ?(.*)$/)
    message = $2
    return nil if message.strip.empty?
    @level = LEVELS[$1] || @level
    if message.match(/^(\d+)\.(\d+)\ (?:javax\.servlet\.ServletContext\ log|
            sun\.reflect\.NativeMethodAccessorImpl\ invoke0|
            sun\.reflect\.GeneratedMethodAccessor11\ invoke):\ (.*)$/x)
      message = $3
      time = Time.at($1.to_i, $2.to_i)
      @date = time.strftime('%Y-%m-%dT%H:%M:%S') + sprintf('.%06d', time.usec)
    end
    "#{@level[0]}, [#{@date} #{@session}] #{@level[1]} -- : #{message}"
  end
end

Formatter.new.filter(ARGF)

ログファイルを引数に渡すか、もしくは標準入力に流し込みます。

$ ./format_gaelog.rb mylogs.txt
D, [2009-07-16T18:35:50.569000 #00020] DEBUG -- : hello
I, [2009-07-16T18:35:50.572000 #00020]  INFO -- : hello
W, [2009-07-16T18:35:50.573000 #00020]  WARN -- : hello
E, [2009-07-16T18:35:50.573000 #00020] ERROR -- : hello
E, [2009-07-16T18:35:50.573000 #00020] ERROR -- : hello

こんな感じにフォーマットして出力されます。一応、ある程度は読み易くなるのではないかと。

ちなみに、日付・時刻の後の#から始まる数値は、プロセスIDではなくリクエスト番号(そのログ中に出現した何番目のリクエストか)を表しています。この数値が同じであれば、同一のリクエスト内で出力されたログだということになります。

*1:config〜finestの間ならなんでも

*2:ここでいうログ出力レベルは、GAEダッシュボードのログ表示画面における"Minimum Severity"とは別のものです。

*3:DEBUGレベルをFINESTに割り当てるのならFINEST、もしくはALLにします。

*4:今回はグローバルなログレベルを編集してしまいましたが、名前付きロガーで正しくログを分類している場合は、個別にデフォルトログレベルを指定したほうが良いのかもしれません。

*5:Javaではそんな感じのルールになってるみたい?

*6:sun.reflect.〜などの出力が邪魔なので無理矢理削ってますが、これって消せないんでしょうか。

Google App EngineデータストアをRubyから簡単に使う

前置き

前回前々回あたりでとりあえずGoogle App Engine for Java(以下GAEJ*1)上でRubyスクリプトを動作させることができるようになりました。

が、どうしてもいつものRubyスクリプトとは勝手が違って、いまいちやりにくい気がします。

例えばデータの記録なんかも、両手で数え足りるような数のデータを記録するだけであれば、O/Rマッパー的な仕組みを使ってモデルを定義して…という作業も面倒です。もっと手軽に行いたいものですよね。

大人しくフレームワークでも使っておけ、という話もありますが、まぁそれはそれ…。

GAE Toolkit

そんなこんなで、あまり「GAEのデータストア」という存在を意識せずに手軽にデータの保存・取得を行えるように、簡単なラッパーを書いてみました。GitHubリポジトリに置いてあります。

"Tiny toolkit for JRuby on Google App Engine"などと銘打っていますが、まだデータストアにアクセスするためのクラスしかありません。このくらいのライブラリなら探せば他にいくらでもありそうですが、あともういくつか個人的に用意したい機能もある*2ので、後々追加していけたらな…と思います。

簡単な使い方

例えば次のようにしてGAEデータストア上にデータを保存できます。

GAEKit::Store.put(:name, 'Tanaka Tarou')
GAEKit::Store.put('age', 20) # キーは文字列でもシンボルでも同じ扱い
GAEKit::Store.put(:friends, [ 'Ichiro', 'Hanako', 'John' ])
GAEKit::Store[:location] = 'Tokyo/Japan' # putメソッドの別名

データの取り出しは次のような感じです。

GAEKit::Store.get(:name)
GAEKit::Store.get('age')
GAEKit::Store.get(:friends)
GAEKit::Store[:location] # getメソッドの別名

GAEデータストア上に記録されていますので、もちろんセッションをまたいで永続的に利用できます。

GAEKitモジュールをincludeしてしまえば、もっと手軽にアクセスできます。

include GAEKit
Store[:foo] = 'bar'
value = Store[:name]

その他のメソッドをさらっと。

Store.has_key?(:name) # 引数で指定したプロパティの存在確認
Store.include?(:name) # has_key?メソッドの別名
Store.delete(:name) # 引数で指定したプロパティの削除
Store.keys # 全プロパティの名前を配列で取得
Store.values # 全プロパティの値を配列で取得

インストール方法

gitをインストール済みの場合は

$ git clone git://github.com/shibason/rb-gaekit.git

でチェックアウトできます。

gitがインストールされていない場合は、下のページの"download"ボタンからダウンロードして適当な場所に展開してください。

必要なのはrb-gaekit/gaekit.rbファイル1つだけですので、これをWEB-INFディレクトリ以下の適当な場所に放り込みます。

$ cp rb-gaekit/gaekit.rb sample-application/WEB-INF

あとは上記のファイルをrequireすれば使えるようになります。

require 'gaekit'

もう少し詳しい説明

格納できるデータ

数値や文字列、nil/true/falseなどの基本的な値はもちろん、配列も格納できます。

ただしネストした配列は格納できません。また、ハッシュを始めとしたその他のオブジェクトも格納不可です。*3

サブクラスの活用

GAEKit::Storeクラスはそれ自身がGAEデータストアの一つのエンティティと対応しており、エンティティの識別子として自身のクラス名を使っています。

つまり、GAEKit::Storeを継承したサブクラスを作成すると、それをもう一つの別のエンティティとして利用することができます。

include GAEKit

class MyStore < Store; end

Store[:name] = 'Tanaka Tarou'
MyStore[:name] = 'Suzuki Ichiro'

[ Store[:name], MyStore[:name] ] # => ["Tanaka Tarou", "Suzuki Ichiro"]

なお、GAEKit::Storeクラス及びそのサブクラスに対応するエンティティのkind値は"GAEKit::Datastore"となっています。

GAEデータストアの制限

データの実体はGAEデータストアに保存されますので、GAEKit::Storeに格納可能なデータについても、当然GAEデータストアが持つ各種制限をそのまま受け継ぎます。

GAEデータストアの制限は以下のページなどを参考にしてください。

GAEKit::Storeでは一つのエンティティに全てのデータを突っ込みますので、大量のデータ、もしくはサイズの大きなデータを保存する場合は、きちんとモデルを定義して使うタイプの他のライブラリを利用することをお勧めします。

また、APIの呼び出し回数制限にも注意が必要です。GAEKit::Store#[]=やGAEKit::Store#deleteなどの書き込みメソッドは、その呼出し毎に内部でGAEデータストアのAPIを呼んでいますので、例えばループの中でこれらのメソッドを呼んでいると、APIの呼び出し回数が嵩むばかりでなく、処理速度にも大きな影響を及ぼします。

ループで処理する際は一時変数に退避して、全ての処理が完了してからデータストアに反映する、といった処理を行うようにした方が安全です。

参考資料

GAEデータストアの基本的な使い方はGoogleのドキュメントを参照しました。

JRubyからJavaクラスを呼ぶときの約束事や、オブジェクトの型変換などについては以下のサイトを参考にさせて頂きました。

GitHubの使い方については以下のサイトを参考にさせて頂きました。

*1:GAEjのほうが一般的な表記でしょうか?

*2:むしろそっちが本命だったりもするのですが

*3:Marshal.dumpとMarshal.loadを使えばある程度はいけるかも。

Google App Engineで作るTwitter bot 〜 JRuby編

前置き

続きです。前回Google App Engine(以下GAE)上でRubyスクリプトを動作させるところまでできました。次のサンプルとして、簡単なTwitterbotを作成してみようと思います。

…どうして普通のWebアプリケーションじゃないのかって?それはまぁ…色々ありますが、元々私がGAEに触ってみようと思ったきっかけが「これを自作botの動作プラットフォームにできないかなぁ*1」と思ったから、ということもありまして。

それに、丁度URLフェッチやらGAEデータストアタスクのスケジューリングなど、GAE独自の機能がいくつか必要となってきますので、手軽なサンプルとしても悪くはないかな、と。

サンプルコードだらけで少々長い記事となってしまいましたが、どうぞお付き合い下さい。

作成するbotのタイプ

一言でTwitterbotと言っても、様々なタイプが存在します。今回はシンプルな挨拶系botを作ってみます。タイムラインから他者の発言を取得し、「起きた」「おはよう」などのキーワードが含まれていたら「おはようございます!」とリプライを飛ばす…といった感じのものです。

この系統のbotを作成する場合に必要となる機能を考えると、

  1. Basic認証付きのGETリクエストでタイムラインを取得できること。
  2. 取得したデータ(XML or JSON)を解析できること。
  3. 取得した発言の内容によって処理を分岐できること。
  4. Basic認証付きのPOSTリクエストで発言を投稿できること。
  5. 最後に取得したステータスIDを保存及び取得できること。
  6. 一定間隔で自動実行できること。

といったところでしょうか。3番はRubyのcase式と正規表現でなんとでもなりそうですので、それ以外の機能が実現可能かどうかがカギとなります。

なお、基本的にこの記事ではTwitterAPIについては一通りご存知であるという前提で話を進めていきます。ご存知でない方は、以下のドキュメントにざっと目を通しておくと良いかもしれません。*2

それでは、一つずつ条件をクリアしていきます。

雛形の準備

前回作成した雛形をそのまま流用します。

ダウンロードして展開し、一応わかりやすくディレクトリ名を変更します。

$ wget http://shiba.rdy.jp/souko/sample-jruby-on-gaej.tgz
$ tar xvzf sample-jruby-on-gaej.tgz 
$ mv sample-jruby-on-gaej sample-twitterbot-for-jruby-on-gaej

アプリケーション本体のファイル名(hello.rb)も変更しても良いのですが、今回は面倒なのでそのまま使っています。

ちなみに、GAEでは現状最大10個のアプリケーションしかデプロイできず、しかも削除することができません。なので、今回のようなサンプルは、枠を一つだけ確保し、そこをどんどん上書きする形で使っていった方が得策です。バージョン履歴は全部残りますので、いつでも昔のサンプルを動かすことができます。

friends_timelineを取得してみる

friends_timelineを取得するには、Basic認証付きのGETメソッドが使える必要があります。以下のページを眺めてみると、どうやらurlfetch-gaeというライブラリがこの要件を満たしてくれそうです。*3

簡単な使い方の説明などはこちら。

では、早速ダウンロードしてサンプルアプリに組み込みます。ライブラリの実体は一つのrbファイルですので、それをWEB-INFディレクトリに放り込むだけでOKです。gitをインストールしていない場合は、上のサイトの"download"ボタンからダウンロードしてください。

$ git clone git://github.com/Basaah/urlfetch-gae.git
$ cp urlfetch-gae/lib/urlfetch.rb sample-twitterbot-for-jruby-on-gaej/WEB-INF

ちなみにこのurlfetch.rb、わずか63行のシンプルなRubyスクリプトとなっていますので、RubyからどうやってJavaのクラスを利用するのかを学ぶのに良い題材となるかもしれません。

ではfriends_timelineを取得してみます。

$ cd sample-twitterbot-for-jruby-on-gaej
$ vim WEB-INF/hello.rb
require 'rubygems'
require 'rack'
require 'urlfetch'

class HelloWorld
  include Rack::Utils

  def call(env)
    res = Rack::Response.new

    res.write '<html><head>'
    res.write '<title>Sample Twitter bot for JRuby on GAEJ</title>'
    res.write '</head><body>'

    url = 'https://twitter.com/statuses/friends_timeline.xml'
    username = 'username' # ここをbotのユーザ名に書き換える
    password = 'password' # ここをbotのパスワードに書き換える
    auth_token = [ "#{username}:#{password}" ].pack('m').chomp
    request_header = { 'Authorization' => "Basic #{auth_token}" }
    response = URLFetch.get(url, :header => request_header)

    res.write '<h2>Response code</h2>'
    res.write "<p>#{response.code}</p>"

    res.write '<h2>Response headers</h2>'
    res.write '<dl>'
    response.header_hash.each_pair do |key, value|
      res.write "<dt>#{escape_html(key)}</dt><dd>#{escape_html(value)}</dd>"
    end
    res.write '</dl>'

    res.write '<h2>Response body</h2>'
    res.write '<p>' + escape_html(response.content) + '</p>'

    res.write '</body></html>'
    res.finish
  end
end

今回は取得したレスポンスボディは特に解析せずそのまま出力します。また、Basic認証の仕組みは用意されていないようですので、自力でリクエストヘッダにセットしています。

開発サーバを起動して8080番にアクセスし、レスポンスヘッダや取得したXMLが表示されれば成功です。

$ ../appengine-java-sdk-1.2.1/bin/dev_appserver.sh --address=0.0.0.0 .

取得したXMLを解析してみる

次はこのXMLを解析して必要な情報だけを取り出してみます。

JRubyではNative extensionが必要なgemは使えないようですので、JSONライブラリは使えません。Javaのクラスを呼び出して使ってもいいのですが、幸いRuby標準添付のREXMLがJRubyでも使えるようですので、こちらを利用します。つまり、フォーマットは常にXMLでリクエストすることになります。

では先ほどのソースを少々改造して、取得したメッセージをいくつかの情報に分け、テーブルで表示するようにしてみます。

$ vim WEB-INF/hello.rb
require 'rubygems'
require 'rack'
require 'urlfetch'
require 'rexml/document'
require 'time'

class HelloWorld
  include Rack::Utils

  def call(env)
    res = Rack::Response.new

    res.write '<html><head>'
    res.write '<title>Sample Twitter bot for JRuby on GAEJ</title>'
    res.write '</head><body>'

    url = 'https://twitter.com/statuses/friends_timeline.xml'
    username = 'username' # ここをbotのユーザ名に書き換える
    password = 'password' # ここをbotのパスワードに書き換える
    auth_token = [ "#{username}:#{password}" ].pack('m').chomp
    request_header = { 'Authorization' => "Basic #{auth_token}" }
    response = URLFetch.get(url, :header => request_header)

    res.write <<-HTML
      <table>
        <tr>
          <th>ID</th>
          <th>text</th>
          <th>from</th>
          <th>posted at</th>
        </tr>
    HTML
    doc = REXML::Document.new(response.content)
    doc.each_element('/statuses/status') do |status|
      id = status.elements['id'].text
      created_at = Time.parse(status.elements['created_at'].text)
      text = status.elements['text'].text
      user = status.elements['user']
      screen_name = user.elements['screen_name'].text
      res.write <<-HTML
        <tr>
          <td>#{id}</td>
          <td>#{escape_html(text)}</td>
          <td>#{screen_name}</td>
          <td>#{created_at}</td>
        </tr>
      HTML
    end
    res.write '</table>'

    res.write '</body></html>'
    res.finish
  end
end
$ ../appengine-java-sdk-1.2.1/bin/dev_appserver.sh --address=0.0.0.0 .

ID、発言内容、ユーザ名、投稿時間が表組みで表示されれば成功です。

発言を投稿してみる

取得は十分いけそうですので、次は発言の投稿を試してみます。urlfetch-gaeにはPOSTメソッドもサポートされており、ほぼ同じ感覚で使えるようですので、これも問題無さそうです。

とりあえずサンプルとして、アクセスする度に現在時刻をTwitterに投稿するスクリプトを書いてみます。

$ vim WEB-INF/hello.rb
require 'rubygems'
require 'rack'
require 'urlfetch'

class HelloWorld
  include Rack::Utils

  def call(env)
    res = Rack::Response.new

    res.write '<html><head>'
    res.write '<title>Sample Twitter bot for JRuby on GAEJ</title>'
    res.write '</head><body>'

    url = 'https://twitter.com/statuses/update.xml'
    username = 'username' # ここをbotのユーザ名に書き換える
    password = 'password' # ここをbotのパスワードに書き換える
    auth_token = [ "#{username}:#{password}" ].pack('m').chomp
    request_header = { 'Authorization' => "Basic #{auth_token}" }

    now = Time.now.strftime("%Y/%m/%d %H:%M:%S")
    message = "現在時刻は #{now} です。"
    query_string = 'status=' + escape(message)

    response = URLFetch.post(url, query_string, :header => request_header)

    if response.code == 200
      res.write '<p>Success!</p>'
    else
      res.write '<p>Failure</p>'
    end

    res.write '</body></html>'
    res.finish
  end
end

ソース中にマルチバイト文字が出てきましたが、基本的に全てUTF-8で保存しています。TwitterAPIUTF-8なので、そこを揃えないと文字化けしてしまいます。

$ ../appengine-java-sdk-1.2.1/bin/dev_appserver.sh --address=0.0.0.0 .

今回はアクセスしてみても'Success!'か'Failure'しか表示されませんので、Twitterのほうで実際に発言が投稿されているか確認する必要があります。

最後に取得したステータスIDを保存してみる

ここまででタイムラインの取得と発言まではできましたが、定期的に稼動させるbotとするためにはもう一つ重要な要素があります。

それは「前回取得したタイムラインの最後のステータスIDを保存できること」です。これがないと、次のタイムライン取得時に「どこまでが前回処理済なのか」を判別できず、同じ発言に対して2回リプライを飛ばしてしまう状態に陥ってしまいます。

前の記事で言及した通り、GAEではローカルファイルへの書き込みアクセスが禁止されていますので、他の手段をとる必要があります。

再度先ほどのページを眺めてみると、どうやらbumbleというライブラリがこの目的に使えそうです。

簡単な使い方の説明などは以下のページの真ん中少し下あたりが参考になります。

たった一つのデータを保存するためだけに使うには少々大げさですが、他に手段もないようですし*4、これを使うことにします。

インストールは先ほどのurlfetch-gaeと同じで、ダウンロード後、ライブラリの実体である一つのrbファイルをWEB-INFディレクトリに放り込むだけです。

$ cd ..
$ git clone git://github.com/olabini/bumble.git
$ cp bumble/bumble/bumble.rb sample-twitterbot-for-jruby-on-gaej/WEB-INF

もう一つ上の階層にもbumble.rbがありますが、これは無くても構いません。

では、先ほどのfriends_timelineをテーブルで出力するスクリプトにこれを組み込んで、前回実行時以降の発言だけ表示するようにしてみます。

$ cd sample-twitterbot-for-jruby-on-gaej
$ vim WEB-INF/hello.rb
require 'rubygems'
require 'rack'
require 'urlfetch'
require 'rexml/document'
require 'time'
require 'bumble'

class HelloWorld
  include Rack::Utils

  class StoredData
    include Bumble
    ds :last_id
  end

  def call(env)
    res = Rack::Response.new

    res.write '<html><head>'
    res.write '<title>Sample Twitter bot for JRuby on GAEJ</title>'
    res.write '</head><body>'

    data_set = StoredData.all({}, :limit => 1)
    if data_set.empty?
      data = StoredData.create
    else
      data = data_set.first
    end
    last_id = data.last_id || 1

    url = 'https://twitter.com/statuses/friends_timeline.xml'
    query_params = { :since_id => last_id, :count => 200 }
    query_string = query_params.map do |key, value|
      escape(key.to_s) + '=' + escape(value.to_s)
    end.join('&')
    url += '?' + query_string

    username = 'username' # ここをbotのユーザ名に書き換える
    password = 'password' # ここをbotのパスワードに書き換える
    auth_token = [ "#{username}:#{password}" ].pack('m').chomp
    request_header = { 'Authorization' => "Basic #{auth_token}" }

    response = URLFetch.get(url, :header => request_header)

    res.write <<-HTML
      <table>
        <tr>
          <th>ID</th>
          <th>text</th>
          <th>from</th>
          <th>posted at</th>
        </tr>
    HTML
    doc = REXML::Document.new(response.content)
    doc.elements.to_a('/statuses/status').reverse_each do |status|
      last_id = id = status.elements['id'].text.to_i
      created_at = Time.parse(status.elements['created_at'].text)
      text = status.elements['text'].text
      user = status.elements['user']
      screen_name = user.elements['screen_name'].text
      res.write <<-HTML
        <tr>
          <td>#{id}</td>
          <td>#{escape_html(text)}</td>
          <td>#{screen_name}</td>
          <td>#{created_at}</td>
        </tr>
      HTML
    end
    res.write '</table>'

    data.last_id = last_id
    data.save!

    res.write '</body></html>'
    res.finish
  end
end

GETリクエストにsince_idとcountパラメータを含めていること、また最後に処理する発言を最新のものにするために、ステータスの配列をreverse_eachで処理していることあたりがポイントです。

$ ../appengine-java-sdk-1.2.1/bin/dev_appserver.sh --address=0.0.0.0 .

間を空けつつ何度かアクセスしてみると、確かに新着メッセージしか表示されないことがわかります。

ちなみに、開発サーバでGAEデータストアを利用すると、WEB-INFディレクトリの中にappengine-generatedというディレクトリが作成されます。この中に書き込んだデータが保存されるため、開発サーバを立ち上げなおしても引き続きデータを利用することができます。逆にこのディレクトリを消してしまえばデータを初期化できます。

このディレクトリはGAEにアップロードする際は無視されますので、気にする必要はありません。

挨拶botを作ってみる

ここまでの材料で、手動で動かす挨拶botは作れるようになりました。ではhello.rbを整理しつつ少々改造して、簡単な挨拶botに仕立て上げてみます。

$ vim WEB-INF/hello.rb
require 'rubygems'
require 'rack'
require 'urlfetch'
require 'rexml/document'
require 'time'
require 'bumble'

class Hash
  def to_query_string
    map do |key, value|
      Rack::Utils.escape(key.to_s) + '=' + Rack::Utils.escape(value.to_s)
    end.join('&')
  end
end

class HelloWorld
  include Rack::Utils

  class StoredData
    include Bumble
    ds :last_id
  end

  URLS = {
    :friends_timeline => 'https://twitter.com/statuses/friends_timeline.xml',
    :status_update => 'https://twitter.com/statuses/update.xml',
  }

  USERNAME = 'username' # ここをbotのユーザ名に書き換える
  PASSWORD = 'password' # ここをbotのパスワードに書き換える
  def request_header
    auth_token = [ "#{USERNAME}:#{PASSWORD}" ].pack('m').chomp
    { 'Authorization' => "Basic #{auth_token}" }
  end

  def get_timeline(options = {})
    url = URLS[:friends_timeline] + '?' + options.to_query_string
    response = URLFetch.get(url, :header => request_header)
    raise "Get timeline failed: #{response.code}" unless response.code == 200
    doc = REXML::Document.new(response.content)
    doc.elements.to_a('/statuses/status').reverse
  end

  def post(text, options = {})
    options[:status] = text
    response = URLFetch.post(URLS[:status_update], options.to_query_string,
                             :header => request_header)
    raise "Post status failed: #{response.code}" unless response.code == 200
    doc = REXML::Document.new(response.content)
    doc.elements['/status']
  end

  def call(env)
    res = Rack::Response.new

    res.write '<html><head>'
    res.write '<title>Sample Twitter bot for JRuby on GAEJ</title>'
    res.write '</head><body>'

    data_set = StoredData.all({}, :limit => 1)
    if data_set.empty?
      data = StoredData.create
    else
      data = data_set.first
    end
    last_id = data.last_id || 1
    statuses = get_timeline( :since_id => last_id, :count => 200 )

    res.write <<-HTML
      <table>
        <tr>
          <th>ID</th>
          <th>text</th>
          <th>from</th>
          <th>posted at</th>
          <th>status</th>
        </tr>
    HTML
    statuses.each do |status|
      last_id = id = status.elements['id'].text.to_i
      created_at = Time.parse(status.elements['created_at'].text)
      text = status.elements['text'].text
      screen_name = status.elements['user/screen_name'].text
      next if screen_name == USERNAME   # 自分の発言はスキップ

      reply_message = nil
      unless text.include?('@') # 誰かへの返信ではない場合に限り
        # ここの条件分岐を発展させていけば、リプライのパターンを充実させられる
        case text
        when /(?:起きた|おきた|おはよ|オハヨ)/
          reply_message = 'おはようございます!いい朝ですね。'
        when /(?:こんにち(?:|)|コンニチ(?:|))/
          reply_message = 'こんにちは。ご機嫌はいかがですか?'
        when /(?:こんばん(?:|)|コンバン(?:|))/
          reply_message = 'こんばんは。今日も一日お疲れ様でした。'
        end
      end
      if reply_message
        post("@#{screen_name} #{reply_message}", :in_reply_to_status_id => id )
      end

      res.write <<-HTML
        <tr>
          <td>#{id}</td>
          <td>#{escape_html(text)}</td>
          <td>#{screen_name}</td>
          <td>#{created_at}</td>
          <td>#{reply_message ? 'replied' : 'ignored'}</td>
        </tr>
      HTML
    end
    res.write '</table>'

    data.last_id = last_id
    data.save!

    res.write '</body></html>'
    res.finish
  end
end

少々長くなりました。細部がかなり適当な実装となっていますが、最低限の挨拶botとしては稼動できる状態です。

$ ../appengine-java-sdk-1.2.1/bin/dev_appserver.sh --address=0.0.0.0 .

Twitter上で他のアカウントから発言してみるなどして動作を確認します。

自動的に定期実行させてみる

さて、ここまででbot自体の機能実装は一通り完了しましたが、botならばやはり自動的に定期実行させておきたいものです。

幸い、GAEにはcronの仕組みが用意されており、しかも最短で1分毎に指定したURLを呼び出すことができます。

上のページを読んで、cronの実行に必要な設定ファイルを記述します。

$ vim WEB-INF/cron.xml
<?xml version="1.0" encoding="UTF-8"?>
<cronentries>
  <cron>
    <url>/</url>
    <description>Auto-reply of sample twitter bot every 3 minutes</description>
    <schedule>every 3 minutes</schedule>
  </cron>
</cronentries>

そんなに急ぐようなbotでもありませんので、3分間隔であれば十分でしょう。

このままですとURLを手動で呼び出した場合もbotが起動してしまい、下手をすればTwitterAPI制限に引っかかってしまいますので、このURLを管理者以外アクセスできないような設定にし、cron以外では実行できないようにしてしまいます。

…と思いましたが、どうも現状その辺りにバグがあるらしく、アクセス制限をかけるとcronが機能しなくなる模様。仕方ないので今回はあくまでもサンプルということでスルーしておきます。

では開発サーバで動作確認…といきたいところですが、あいにく開発サーバはcronの実行まではサポートしていませんので、実際にGAEにアップロードして確認する必要があります。

上のページにはアプリケーション本体はアップロードせず、cronの設定だけを書き換える方法も書いてありますので、何度か試行錯誤してみても良いでしょう。

アップロードする前にWEB-INF/appengine-web.xmlのapplicationとversionの値を適切に書き換えておいてください。

$ ../appengine-java-sdk-1.2.1/bin/appcfg.sh --enable_jar_splitting update .

versionを上げた場合、アップロードが完了したらGAEのダッシュボードからアプリケーションのデフォルトのバージョンを変更するのを忘れないようにしてください。古いバージョンがデフォルトのままになっています。

最初の数回は上手く動かない場合もあるかもしれませんが、しばらく様子を見ていると安定してくるはずです。*5

他のアカウントからキーワードにかかるような発言をしてみて、手動でURLにアクセスすることなく正しくリプライが返ってくれば、めでたくbotの完成です。

次のステップへ

後はこのスクリプトのメッセージ分岐の部分を追加修正していけば、それなりにしっかりとしたbotにできる…のですが、試行錯誤しながら継ぎ足し継ぎ足し書いてきた関係で、だいぶごちゃごちゃしたソースになってしまっています。

Twitter APIを呼び出す部分なんかも、少し整理すれば別ファイルに切り出してライブラリにまとめられそうです。

ということで、次回*6は、ソースを整理しつつ、botとしてもう少しまともなスクリプトに発展させていきます。

ある程度整理が付いたら、また前回のように「GAE上のTwitter botのテンプレート」になるパッケージでも用意してみようと思います。

*1:現在はさくらのレンタルサーバ上でいくつか稼動させています

*2:読まなくても作れますが。

*3:rb-gae-supportを使えば巷のTwitterライブラリがそのまま使えるかもしれませんが、今回は独自で実装してみます。

*4:memcacheはいつデータが消えるかわかりませんし

*5:最初に200件ステータスを取得してしまうために30秒制限にひっかかるのかもしれません。最初の取得だけ件数を制限すれば問題ないかも。

*6:もしくは近い未来…あるいは遠い未来

Google App Engine上でRubyアプリを動かす手順

前置き

Google App Engine(以下GAE)では、公式には現在のところPythonJavaしかサポートされていませんが、JRubyという素晴らしいプロダクトのおかげでJava VM上でRubyスクリプトを実行できるため、考えようによってはRubyも既にサポート対象になっていると言えなくもありません。

実際にググってみても既に結構な量の情報が存在するのですが、どうもJRuby on Railsを対象とした情報が多く、素のRubyアプリケーションを動かすための情報があまり無いように感じました。

Railsももちろん優れたフレームワークなのですが、ちょっとしたアプリケーションを作るのには少々重過ぎますよね…。

ということで、非Railsな、もっとシンプルなRubyアプリをGAE上で動かすための手順を調べてみました。

ポイント

今回は以下の前提で作業を進めていきます。

環境は、手元にたまたまインストールしたてのものがあったという理由で、CentOS 5.3を選択しました。

Google App Engine for Java環境の構築

GAE上でJRubyを動かすには、当然ながらGAE上でJavaアプリが動作する環境(Google App Engine for Java、以下GAEJ)が必要ですので、まずは以下のページを参考に、Google App Engine SDK for Java(後述)に付いているJavaのデモアプリを動かせる環境を作ります。

GAE上で動くJavaアプリを開発するために最低限必要なものは以下の二つです。

JDKのインストール

以下のページから、最新版のLinuxJDK(jdk-6u14-linux-i586-rpm.bin)をダウンロードし、インストールします。

ダウンロードしたファイルはシェルスクリプトになっていますので、

$ sudo sh jdk-6u14-linux-i586-rpm.bin

というように実行します。利用許諾にyesと答えてしばらく待っているとインストールが完了します。

インストールが終わったら、以下の環境変数を設定しておきます。Bashを使っているのであれば.bashrcあたりに追加しておくと良いです。

export JAVA_HOME=/usr/java/default
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/jre/lib:$JAVA_HOME/lib:$JAVA_HOME/lib/tools.jar

軽くバージョンの確認をしておきます。

$ java -version
java version "1.6.0_14"
Java(TM) SE Runtime Environment (build 1.6.0_14-b08)
Java HotSpot(TM) Client VM (build 14.0-b16, mixed mode, sharing)
$ javac -version
javac 1.6.0_14

問題無さそうです。

Google App Engine SDK for Javaのダウンロード

以下のページからGoogle App Engine SDK for Javaをダウンロードし、適当な場所に展開しておきます。*1

$ wget http://googleappengine.googlecode.com/files/appengine-java-sdk-1.2.1.zip
$ unzip appengine-java-sdk-1.2.1.zip

この時点で、既にローカルでGAEJアプリケーションを動作させる環境は整いました。試しに今展開したSDK同梱のデモアプリを動かしてみます。

$ appengine-java-sdk-1.2.1/bin/dev_appserver.sh --address=0.0.0.0 appengine-java-sdk-1.2.1/demos/guestbook/war
The server is running at http://localhost:8080/

8080ポートでアクセスして、guestbookアプリの画面が表示されればOKです。*2

JRuby on GAEJ環境の準備

無事GAEJ環境が出来ましたので、次はこの上でJRubyを動作させるために必要なものを準備していきます。

GAEJ上でJRubyを動かすのに必要なものは以下の二つです。

jruby-complete.jarの作成

jruby-complete.jarはJRubyの動作のために必要な機能が全て詰め込まれたライブラリです。普通にパッケージからJRubyをインストールした場合は付いてきませんので、ソースから自分でコンパイルする必要があります。

JRubyをソースからコンパイルするためにgitとantが必要になりますので、まずはそれらを用意します。

gitのインストール

以下のページを参考に、yumからパッケージインストールします。

リポジトリを二つ追加します。

$ sudo vim /etc/yum.repos.d/git.repo
[git]
name=Base git repository
baseurl=http://www.kernel.org/pub/software/scm/git/RPMS/$basearch
enabled=1
gpgcheck=0
$ sudo vim /etc/yum.repos.d/rpmforge.repo
[rpmforge]
name = Red Hat Enterprise $releasever - RPMforge.net - dag
mirrorlist = http://apt.sw.be/redhat/el5/en/mirrors-rpmforge
enabled = 0
gpgcheck = 0

以下のコマンドでインストールできます。最新版だと依存性のエラーが出ますので、バージョンを指定しています。

$ sudo yum install git-1.5.6.1-1 --enablerepo=rpmforge
antのダウンロード

JRuby1.1以上ではAnt1.7以上が要求されるため、yumからパッケージインストールしたものは使えません。なので、公式サイトからダウンロードしたものを使います。

適当な場所に展開しておけばOKです。特にインストール作業などは必要ありません。

$ wget http://ftp.riken.jp/net/apache/ant/binaries/apache-ant-1.7.1-bin.tar.gz
$ tar xvzf apache-ant-1.7.1-bin.tar.gz
JRubyソースの取得とコンパイル

ではJRubyのソースをコンパイルし、jruby-complete.jarを作成します。

以下のようにコマンドを実行します。少々時間がかかるかもしれません。

$ git clone git://kenai.com/jruby~main jruby
$ cd jruby
$ ../apache-ant-1.7.1/bin/ant jar-complete

これでjruby/libディレクトリ内にjruby-complete.jarができます。*3

JRuby-Rackのダウンロード

次にJRuby-Rackをダウンロードしますが、その前に、JRuby-Rackがどんな物かを説明しておきます。

GAEJではJava サーブレット標準という仕組みを利用してWebアプリケーションを作成します。GAEJ上でJRubyを動かそうと思った場合、サーブレットJRubyの橋渡しをする部分のJavaプログラムを自作しなくてはいけません。

しかし、ありがたいことにJRuby-Rackがその面倒な部分を全て請け負ってくれます。

JRuby-Rackというのは、Ruby用に元々存在しているRackというソフトウェアの拡張版です。Rackに関する説明は以下のページが参考になります。

一言で説明すると、WebサーバとWebアプリケーションフレームワークの間に挟まって、各々の非互換性を吸収してくれるミドルウェアです。

JRuby-Rackは、RackがサポートしているWebサーバに加えてJavaサーブレットもサポートしているため、それを利用することで、サーブレットを意識することなく楽にWebアプリケーションが書けるようになる…というわけです。

今回はWebアプリケーションフレームワークを使いませんので、アプリケーションから直接Rackを触ることになります。少々癖がありますが、覚えること自体は少ないですので、先ほどのページや以下のページにざっと目を通して感覚を掴んでおくと良いかもしれません。

ではJRuby-Rackをダウンロードします。特にインストール作業などをする必要はなく、適当な場所に置いておけばOKです。

$ wget http://kenai.com/projects/jruby-rack/downloads/download/jruby-rack-0.9.4.jar

サンプルアプリケーションの作成

さて、ここまででJRuby on GAEJアプリに必要な材料は全て揃いました。とりあえず、簡単なHello worldアプリを作成してみます。

まずはアプリケーションのファイルを格納するディレクトリを作ります。GAEJでは、Javaサーブレット規格で定められた通りの配置でファイルが存在している必要があります。

$ mkdir sample-jruby-on-gaej
$ cd sample-jruby-on-gaej
$ mkdir -p WEB-INF/lib

アプリケーションのトップディレクトリにWEB-INFという名前のディレクトリが存在し、その中にアプリケーションに必要なファイルが全て収まっていなければいけません。

今作ったlibディレクトリには、これまで集めてきたJRuby on GAEJの動作に必要な3つのファイルを格納します。

$ cp ../appengine-java-sdk-1.2.1/lib/user/appengine-api-1.0-sdk-1.2.1.jar WEB-INF/lib
$ cp ../jruby/lib/jruby-complete.jar WEB-INF/lib
$ cp ../jruby-rack-0.9.4.jar WEB-INF/lib

アプリケーション本体は、以下のページを参考に、"Hello world!"というプレーンテキストを出力するだけのシンプルなものを書きます。配置場所はWEB-INFディレクトリ直下です。

$ vim WEB-INF/hello.rb
require 'rubygems'
require 'rack'

class HelloWorld
  def call(env)
    [ 200, { 'Content-Type' => 'text/plain' }, [ 'Hello world!' ] ]
  end
end

設定ファイルの作成

更に、GAEJの動作には二つの設定ファイルが必須となります。以下のページを参考に、最低限必要な設定を書いていきます。

$ vim WEB-INF/appengine-web.xml
<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
  <application>sample-jruby-on-gaej</application>
  <version>1</version>
  <system-properties>
    <property name="jruby.management.enabled" value="false" />
    <property name="os.arch" value="" />
  </system-properties>
</appengine-web-app>
$ vim WEB-INF/web.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE web-app PUBLIC
  "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
  "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">
  <context-param>
    <param-name>public.root</param-name>
    <param-value>/</param-value>
  </context-param>

  <context-param>
    <param-name>rackup</param-name>
    <param-value>
      require 'hello.rb'
      run HelloWorld.new
    </param-value>
  </context-param>

  <filter>
    <filter-name>RackFilter</filter-name>
    <filter-class>org.jruby.rack.RackFilter</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>RackFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <listener>
    <listener-class>org.jruby.rack.RackServletContextListener</listener-class>
  </listener>
</web-app>

サンプルアプリケーションの動作確認

ここまででアプリケーションの動作に必要なファイルは一通り揃いました。

試しにローカルで起動して、きちんと動くことを確かめてみます。

$ ../appengine-java-sdk-1.2.1/bin/dev_appserver.sh --address=0.0.0.0 .
The server is running at http://localhost:8080/

dev_appserver.shの最後の引数にはアプリケーションのトップディレクトリ(WEB-INFの親ディレクトリ)を指定します。

8080番ポートでアクセスして"Hello world!"が表示されたら成功です。

ついでですので、先ほどのhello.rbを色々書き換えてみて、確かにRubyスクリプトが動いているのだということを確認してみます。*4

require 'rubygems'
require 'rack'

class HelloWorld
  include Rack::Utils   # Rack::Utils.escape_htmlを簡単に使うため

  def call(env)
    req = Rack::Request.new(env)
    res = Rack::Response.new

    res.write '<html><head><title>Sample of JRuby on GAEJ</title></head><body>'

    # いくつか基本的な情報を表示
    res.write '<h2>Ruby version</h2>'
    res.write "<p>#{RUBY_VERSION}</p>"

    res.write '<h2>Platform</h2>'
    res.write "<p>#{RUBY_PLATFORM}</p>"

    res.write '<h2>Load paths</h2>'
    res.write '<ul>'
    res.write $:.map { |path| "<li>#{escape_html(path)}</li>" }.join
    res.write '</ul>'

    # POSTパラメータの一覧を表示
    res.write '<h2>Posted parameters</h2>'
    res.write '<dl>'
    req.params.each_pair do |key, value|
      res.write "<dt>#{escape_html(key)}</dt><dd>#{escape_html(value)}</dd>"
    end
    res.write '</dl>'

    # 環境変数の一覧を表示
    res.write '<h2>Environment variables</h2>'
    res.write '<dl>'
    req.env.each_pair do |key, value|
      res.write "<dt>#{escape_html(key)}</dt><dd>#{escape_html(value)}</dd>"
    end
    res.write '</dl>'

    res.write '</body></html>'
    res.finish
  end
end

今のところRubyスクリプトを書き換えたらサーバを再起動する必要がある?ようですので、先ほど立ち上げたdev_appserver.shをCtrl+Cで一旦終了し、再度実行します。

同じようにローカルの8080番にアクセスしてみると、色々と情報が列挙されるはずです。

アプリケーションのアップロード

あとはこのままこのアプリケーションをGAE上にアップロードすれば、世界中からアクセス可能なWebアプリの完成です。

当然GAEのアカウントを取得して、かつGAEJが使用可能な状態になっていなければなりません。*5

アップロードは以下のコマンドを実行するだけです。*6途中でGoogleアカウントとパスワードを聞かれてきますので入力します。

$ ../appengine-java-sdk-1.2.1/bin/appcfg.sh --enable_jar_splitting update .

最後の引数には先ほどと同様にアプリケーションのトップディレクトリを指定します。

GAEのダッシュボードで正しくアップロードされていることを確認し、表示されているURLにアクセスしてみます。先ほどのローカルでのテストと比較して、$LOAD_PATHなどの値が異なっていることが見てとれると思います。

JRuby on GAEJアプリのテンプレート

ここまででJRuby on GAEJアプリの作り方は一通り完了です。思ったよりも作業量が多くて疲れてしまいますね…。

しかし、よくよく手順を見返してみると、大変だったのは主に必要なライブラリの準備でしたし、またアプリケーション自体も本体のファイル(今回の場合は"hello.rb")以外はほぼ定型であり、そのまま使いまわすことができそうです。

ということで、JRuby on GAEJアプリの雛形として使えるパッケージを作成しました。

といっても、上のHello worldアプリをtarボールにしただけです。JRubyJRuby-Rackは同梱されていますので改めて準備する必要はありませんが、JDKGoogle App Engine SDK for Javaは別途インストールしておいてください。

また、将来的にGAEやSDKのアップデートによって使えなくなる可能性もありますので、その点もご了承下さい。

使い方としては、

  1. 展開して
  2. WEB-INF/hello.rbを好きなように書き換えて
  3. WEB-INF/appengine-web.xmlのapplicationタグとversionタグの値を適切に設定して
  4. GAEへアップロード(またはローカルでテスト起動)

するだけです。

ごく簡単なWebアプリなら、これでとりあえずは書けるかも?

もう少し本格的なアプリケーションの作成

とは言え、現状のままではできることも貧弱です。

JRuby on GAEJアプリは、GAEのサンドボックス内で動作する関係上、様々な機能的制限をかけられています。

例えばファイルへのアクセスも禁止されていますし、net/httpも使えませんのでHTTP経由で他のリソースを取得することもままなりません。

しかし、それらの制限された機能を補うために、GAE独自のインターフェースが用意されていますので、これを使うことでパワフルなWebアプリケーションを作成することも可能です。

GAEJで提供されているのは当然Javaへのインターフェースとなりますが、JRubyからそれらを使えるようにしてくれるラッパーライブラリもいくつか作成されています。

以下のページによると、DBへのアクセス、アカウント情報の取得、画像処理、メールサービスの利用、URLフェッチなど、めぼしい機能は一通りJRubyからも利用できるようです。

次回*7は、これらのライブラリを利用して、JRuby on GAEJ上で動作する簡単なTwitter BOT作りに挑戦してみようと思います。*8

*1:英語版のほうがバージョンが新しい場合がありますので、一度英語版のページをチェックしたほうが手間がかからなくて済みます。

*2:端末と開発環境が同一筐体の場合は"--address=0.0.0.0"オプションは不要です。

*3:少し古い資料では、このあと更にライブラリを分割する手順が説明されていますが、現在のGAEではその作業は必要ありません。

*4:Rack::Response#finishは本来ブロックを使った書き方もできるのですが、何故か手元の環境ではブロックを渡して使うと正しく本文データを返してくれないので、仕方なく少々格好悪い書き方をしています。

*5:その辺はばっさり省略します。

*6:この--enable_jar_splittingオプションのおかげで、jruby-complete.jarファイルの分割が不要になっているようです。

*7:もしくは近い未来…あるいは遠い未来

*8:Webアプリじゃない?こまけぇこたぁ(略)

GDBデバッギング覚え書き

使い方をよく忘れるので…。

リファレンス的なものではなく、要点のみの覚え書きです。

以下の記述は

を前提としています。

コンパイル

  • "-g"オプションは必須
  • "-O"系オプション(最適化)は付けない*1
  • stripしちゃダメ

プログラムの実行

$ gdb (progoram)

で起動して

(gdb) run

プログラムに引数を与えることもできます。また、標準入力、標準出力もリダイレクトで指定できます。

(gdb) run (arguments) < (input file) > (output file)

以下のコマンドで環境変数も設定できるので、この辺を組み合わせればCGIデバッグも可能です。

(gdb) set env LANG=C

環境変数の確認は以下の通り。

(gdb) show env

CGIデバッグをする場合はこんな感じになるのかな?

(gdb) set env PATH_INFO=/path/info
(gdb) set env QUERY_STRING=param1=value1&param2=value2
 …(略)…
(gdb) run < (POSTデータを記述したファイル)

2回目以降のrun時に明示的に引数を指定しない場合、前回の引数がそのまま使用されます。

現在設定されている引数を確認するのは

(gdb) show args

以下のように引数だけ先に指定しておくこともできます。

(gdb) set args foo bar baz

ブレイクポイント

関数の入り口に設定する場合は

(gdb) break hoge_function

ソース中の指定した行に設定する場合は

(gdb) break 30
(gdb) break hoge.c:60

条件付で設定する場合は

(gdb) break 100 if counter == 3
(gdb) break 200 if strlen(string) < 32

現在のブレイクポイント一覧を得るには

(gdb) info break

ブレイクポイントを指定した回数以上通過するまで無効化、という条件の指定もできます。ループの中にブレイクポイントを置く際なんかに重宝します。

(gdb) ignore 3 30

上の例では3番のブレイクポイントに対して、はじめの30回は無視するように指示しています。

ブレイクポイントの削除は次の通り。

(gdb) delete 5

全てのブレイクポイントを消したい場合は次のようにします。

(gdb) delete

フレームの表示と移動

現在どのフレームにいるかを知るには

(gdb) where

指定したフレームに移動するには

(gdb) frame 3

一つ上のフレームに移動するには

(gdb) up

一つ下のフレームに移動するには

(gdb) down

ソースの表示

単に

(gdb) list

と打つと、ブレイクポイントで停止した直後の場合はその前後の行が10行ほど、そうでない場合は最後に表示した行の次の行から10行表示されます。

行数や関数名を指定すると、その行を中心に前後10行表示されます。

(gdb) list 100
(gdb) list sample_function

開始行と終了行を指定することもできます。

(gdb) list 200,230

ステップ実行

1行処理を進めます。次の行が関数呼び出しの場合、その関数の中に入って停止します。引数を渡すと、その行数分進みます。

(gdb) step
(gdb) s
(gdb) s 10

1行処理を進めます。次の行が関数呼び出しの場合でも、「その行の処理が完了してから」停止します。つまり関数の中に入りません。引数を渡すと、その行数分進みます。

(gdb) next
(gdb) n
(gdb) n 10

次のブレイクポイント、もしくはプログラムの終了まで処理を続行します。

(gdb) continue
(gdb) c

変数の表示

ある変数の値を知りたい場合は以下のようにします。

(gdb) print param
(gdb) p param

現在のフレームから参照できるローカル変数の一覧は以下のように取得します。

(gdb) info locals

現在のフレームの引数の一覧も取得できます。

(gdb) info args

変数の値を変えてしまうこともできます。

(gdb) set var param = "Hello world!"

変数の自動表示

ある変数の値を、ブレイクポイントで停止する度に自動で表示させることができます。ループ中の値の変遷をチェックする場合などに便利です。

(gdb) display hoge

自動表示中の変数の一覧は

(gdb) display

自動表示を取りやめるには

(gdb) undisplay hoge

全ての自動表示を取りやめるには

(gdb) undisplay

変数の監視

あるいはもっと踏み込んで、ある変数の値が変更されたときに自動で停止させることもできます。「いつどこで変数が書き換えられたのかわからない」といった場合に便利です。

(gdb) watch moge

監視中の変数の一覧は

(gdb) info watch

これはブレイクポイントの一覧と同じ結果が出力されます。つまり、「ある変数が変更されたとき」という条件が設定されたブレイクポイントの一種なわけです。

そのため、削除などの操作は基本的にブレイクポイントのそれに準じます。

(gdb) delete 3

以下、そのうち追記…しないかも?

とりあえず困ったときは

ヘルプを見ればなんとかなります。

(gdb) help

*1:付けてもデバッグできるけど、付けない方がなにかと面倒なことがないので望ましい

*2:ちょっとバージョンが古いですが、基本的な使い方の範囲では問題ないと思います