SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Ruby on Rails + Curl(AD)

Curl+JRuby+Google App EngineでTwitter風アプリを作る
~ローカル環境構築編~

第4回

  • このエントリーをはてなブックマークに追加

 Twitterをご存じでしょうか。今話題のマイクロブログサービスですが、今回はTwitterを受信できるクライアントアプリをCurlで作成していきます。

  • このエントリーをはてなブックマークに追加

はじめに

 Twitterは140文字以下の短いメッセージをつぶやきあうシンプルなサービスです。新しいコミニュケーションの形として利用している人も増えてきました。

 Google App Engine(以下 GAE)についてはご存知の方が多いと思います、Googleが提供するクラウド環境で、ある規模までのアプリなら無料で使えます。また、GAEは単なるサーバ環境の提供ではなくスケーラビリティと信頼性が得られるアプリケーションの実行環境です。しかし使える機能やデータ管理の方式はGoogleの提供するSDKに従う必要があります。

 GAEは2009年4月からは、Pythonに加えJavaがサポートされるようになりました。本連載の第1回目でふれたように、Rubyの処理系にはまつもと氏の作った処理系(MRIと呼ばれます)以外にも、Javaで作られた JRuby という処理系があります。JRuby はJavaVMの上で動くソフトなので、JRuby を使うとGAE上でRuby on Railsのアプリを動かす事ができるようになります。

 またJRubyは、JavaVM上で動作するJavaで作られたアプリのクラスやメソッドを呼び出せるため、下の図のようにJavaで作られた既存のシステムと連携するアプリをRuby on Railsで構築できます。既に米国ではいくつかの実際のシステムで利用が始まっているようです。

 このJRubyを活用しながら、今回と次回の2回でCurlリッチクライントとGAEを使ったTwitter風アプリを作っていきます。今回は、GAE上でTwitter風アプリを構築するための準備として、まずローカル環境でJRuby on Railsを使ったTwitter風アプリを構築していきます。

サンプルソースについて

 こちらよりサンプルソースをダウンロードできます。なお、サンプルソース実行にあたってはライセンスファイル「curl-license-5.dat」が必要になります。Curl IDEなどに含まれているので、コピーして使用してください。

 サンプルソースを実行するには、zipファイルを展開したディレクトリで以下のコマンドを実行して下さい(添付されているテストデータも読み込まれます)。

> jruby -S rake db:migrate
> jruby -S rake db:fixtures:load
> jruby -S script/server
サンプルソースの訂正とお詫び(2009/09/24)

 これまで配布していたサンプルソースに誤りがありました。訂正してお詫びします。

 JRubyで動かすにはconfig/database.yml ファイルには「jdbcsqlite3」を指定する必要がありました。config/database.ymlファイルの4行目を以下のように修正してください(※現在公開されているサンプルソースは修正済みです)。

#adapter: sqlite3
adapter: jdbcsqlite3

過去の連載について

JRuby環境の構築

 JRubyの実行環境をローカルにインストールしてみましょう。JRubyの情報は「JRuby: Wiki: Home - Project Kenai」が詳しいです。Ruby on Railsについても詳しく書かれているので参考にしてください。ここではインストール方法を簡単に説明します。

1. Javaのインストール

 JRubyは Java5以降で動作しますが、GAEのJava環境がJava6相当なので、ここではJava6をインストールします。Java SE ダウンロード - Sun Developer Network (SDN) から最新のJDK 6をインストールします。また、環境変数JAVA_HOMEを作成してインストールされたJDKのパス(例えば C:¥Program Files¥Java¥jdk1.6.0_15)を設定し、環境変数PATHにJDKのbinのパス(例えば C:¥Program Files¥Java¥jdk1.6.0_15¥bin)を追加します。

2. JRubyのインストール

 JRuby: Wiki: Home - Project Kenaiから「Windows and Vista用 JRuby 1.3.1 Binary.zip」をダウンロードし、適当な場所に展開します(例えば C:¥)。また、環境変数PATHにJRubyのbinのパス(例えば C:¥jruby1.3.1¥bin)を追加します。

 コマンドプロンプトで jruby -v が実行できればJava、JRubyのインストールは成功です。

3. Ruby on Railsのインストール

 コマンドプロンプトで以下のコマンドを実行し、Railsや関連するソフトをインストールしてください。最新のRuby on Railsのバージョンは 2.3.3 ですが、GAEとの関連で 2.3.2 をインストールします。

> jruby -S gem install jruby-openssl --no-ri --no-rdoc
> jruby -S gem install rails -v 2.3.2 --no-ri --no-rdoc
> jruby -S gem install activerecord-jdbcsqlite3-adapter --no-ri --no-rdoc

 本連載第1回で行ったRuby on Rails開発環境の構築を行っていない方は sqlite3.exe、 sqlite3.dll をダウンロードし、PATHが通っているディレクトリにインストールしてください。

 上でgemコマンドを直接使うのではなく先頭にjruby -S を付けているのは、JRubyとRuby(MRI)の両方が入っている環境でコマンドやライブラリが混乱するのを回避するためです。

Curlで作るTwitterクライアント

 最初にCurlを使い、Twitterの簡単なクライアントを作ってみましょう。Curlクライアントはそのままではインターネット上のサービスにはアクセスできません。そこでCurlコントロールパネルで、Curlコードをダウンロードするサーバに特権を与える必要があります。手順はセキュリティの設定(特権設定)を参考にしてください。

 さらにpublicディレクトリに置くライセンスキー「curl-license-5.dat」は Curl IDEディレクトリ内 C:¥Program Files¥Curl Corporation¥Surge¥8¥ide¥etc¥localhost-pro¥curl-license-5.dat を使ってください(連載第1回目でコピーしたライセンスファイルでは外部のサーバにはアクセスできません)。

Twitter API

 TwitterはAPIを公開しており、Webページ以外からの利用ができます。これらのAPIを使い、Curl からTwitterサービスをアクセスすることにします。Twitter APIの仕様はTwitter API Wiki / Twitter API Documentationに公開されています。また非公式の日本語訳が観測気球 Twitter API 仕様書 (勝手に日本語訳シリーズ) にあります。

 TwitterのAPIはRESTに準拠しています。またデータ形式もXMLやJSONに対応しています。 Twitterは当初Ruby on Railsで作られたので、APIもとてもRailsらしいスタイルになっています(現在はユーザーの増加に対応するため、つぶやき処理はRuby on Railsではないようです)。

 さて、Twitterのホームページで表示される友人(フォローしている人)の最近のつぶやきは、「http://twitter.com/statuses/friends_timeline.json」URLをGETすると取得できます(データ形式がJSONの場合)。ただし、GET時にはBasic認証やOAuth認証が必要です。取得したデータはHashを持つ配列になっています。

{"favorited"=>false, "created_at"=>"Sat Aug 15 05:46:29 +0000 2009", "truncated"=>false, "text"=>"...発言....", 
"id"=>3323897846, "in_reply_to_user_id"=>nil, "source"=>"web", "in_reply_to_screen_name"=>nil, 
"user"=>{"profile_sidebar_border_color"=>"87bc44", "name"=>"Yuumi Yoshida", "profile_sidebar_fill_color"=>"e0ff92", 
"profile_background_tile"=>false, "location"=>"\350\207\252\347\224\261\343\201\214\344\270n230,Tokyo", 
"created_at"=>"Tue Aug 21 15:15:41 +0000 2007", 
"profile_image_url"=>"http://s3.amazonaws.com/twitter_production/profile_images/20404812/kurage_normal.jpg", 
"profile_link_color"=>"0000ff", "url"=>"http://d.hatena.ne.jp/yuum3/", "favourites_count"=>5, "id"=>8331722, 
"utc_offset"=>32400, "profile_text_color"=>"000000", "protected"=>false, "followers_count"=>98, "notifications"=>nil, 
"time_zone"=>"Tokyo", "verified"=>false, "profile_background_color"=>"9ae4e8", "description"=>"...自己紹介...", 
"profile_background_image_url"=>"http://static.twitter.com/images/themes/theme1/bg.gif", "statuses_count"=>76, 
"friends_count"=>83, "screen_name"=>"yuumi3", "following"=>nil}, "in_reply_to_status_id"=>nil}

 このHashには「text=>発言」「created_at=>発言日時」「user=>発言者プロフィール」などが入っています。発言者プロフィールのHashには「screen_name=>ユーザー名」「profile_image_url=>アイコンのURL」などが入っています。Twitterクライアントではこれらの情報を取得し、表示すればよさそうです。

Curlを使ったクライアント

 Curlで作った、簡単なTwitterクライアントのコードを説明します。

サーバ通信部

 http://twitter.com/statuses/friends_timeline.json をアクセスすることでJSON形式のデータが取得できます。また下のコードの set-http-authenticationプロシージャでBASIC認証のログイン名、パスワードを指定しています。ログイン名 XXXXXX、パスワード YYYYYY のところは、あなたのTwitterログイン名、パスワードに置き換えてください。

{curl 7.0 applet}
{curl-file-attributes character-encoding = "utf8"}

{import * from CURL.IO.JSON}

{let server_url = "http://twitter.com/"}
{let json_data_path = "statuses/friends_timeline.json"}
{let login = "XXXXXX"}
{let password = "YYYYYY"}

{set-http-authentication {url server_url}, login, password}

データ取得部分

 取得されるデータの形式はTwitterとほぼ同じですが項目は少なくなっています、ここでは以下の情報のみを取り出しています。

{text=>発言、user=>{screen_name=>ユーザー名、profile_image_url=>アイコンのURL}}

 コードは次のようになります。

{let twittersJsonValue: JsonValue =
    {JsonValue-parse
        {url server_url & "statuses.json"}
    }
}

{let twitters: RecordSet =
    {RecordSet
        {RecordFields
            {RecordField "screen_name", caption = "", domain = String},
            {RecordField "text", caption = "", domain = String},
            {RecordField "icon", caption = "", domain = String}
        }
    }
}

{define-proc public {add-rec
                        text: String,
                        screen_name: String,
                        profile_image_url: String
                    }: void
    let new-rec: Record = {twitters.new-record}
    {new-rec.set "text", text}
    {new-rec.set "screen_name", screen_name}
    {new-rec.set "icon", profile_image_url}
    {twitters.append new-rec}
}

{for twitter: JsonObject in twittersJsonValue do
    let found?: bool = false
    let text: String = {String}
    let screen_name: String = {String}
    let profile_image_url: String = {String}

    let user: JsonObject = {twitter.get "user"}
    set (text, found?) = {twitter.get-if-exists "text"}
    set (screen_name, found?) = {user.get-if-exists "screen_name"}
    set (profile_image_url, found?) = {user.get-if-exists "profile_image_url"}
    {if found? then
        {add-rec text, screen_name, profile_image_url}
    }
}

表示部分

 取得したデータをRecordGridを使って表示しています。発言者のアイコン画像を表示する部分は、Curl IDE付属ドキュメント「データ レコードとグリッド」のカスタムセルの作成を参考に、アイコンのURLから画像を読み込んでセルのバックグラウンドに設定しています。拡張子と実際の画像形式が異なる場合など、画像が表示できないこともあります。

{define-class public open IconCell
  {inherits StandardRecordGridCell}
  
  field private icon:Frame = 
      {Frame width={make-elastic}, height={make-elastic}}

  {constructor public {default}
    {construct-super}
    set self.height = 48px
    {self.add-internal self.icon}
    set self.cells-take-focus? = self.can-update?
  }
  
  {method public open {refresh-data}:void
    let (data:String, valid?:bool) = {self.get-formatted-data}
    {try
        set self.icon.background = {url data}
     catch e:Exception do
        {unset self.icon.background}
        {output "icon get error " & e.message & " " & data }        
    }
  }
}

{value
    {RecordGrid
        width=20cm,
        height=12.7cm,
        editable? = false,
        record-source = twitters,
        {RecordGridColumn  "icon", width = 48px, 
                column-resizable? = false, cell-spec = IconCell},
        {RecordGridColumn  width = 3cm, "screen_name"},
        {RecordGridColumn  width = 15cm, "text"}
    }
}

 これだけでTwitterのつぶやきを受信できるCurlクライアントは完成です。実行してみてください。うまくタイムラインを表示できたでしょうか?

Twitter風サービスの構築

 さて、Twitterクライアントが完成しました。今度は自分たちの手でTwitter風サービス(Twitterクローン)を作成してみましょう。Twitter風サービスを作るには、以下の3つのテーブル(=モデル)が必要になります。

  • 利用者(users)
    • 利用者の名前、画像、認証用情報
  • 友人、フォローしている人(friendships)
    • 利用者がフォローしている人
  • つぶやき(status)
    • つぶやきの内容、つぶやいた日時

 テーブルの関連は下のER図のようになります

 これから、このTwitter風サービスのコードを説明していきます。今回は紙面の都合もありますのでView(テンプレート)の説明は省略しました。

 まず、「tinytter」という名前でプロジェクトを作成し、テーブル情報を元にscaffoldを使ってひな形を作成します。

> jruby -S rails tinytter
> cd tinytter
>  jruby -S scrip/generate scaffold status text:string user_id:integer
>  jruby -S scrip/generate scaffold user screen_name:string password:string profile_image_path:string 
>  jruby -S scrip/generate scaffold friendship user_id:integer friend_id:integer

 JRuby はC言語で作られたRDBのドライバー等を直接呼び出せないので、sqlite3はJDBC経由でアクセスしています。そこで config/database.yml ファイルの adapter: を sqlite3 から jdbcsqite3 に変更して下さい。

applicationコントローラー

 最初に各コントローラーが継承しているapplicationコントローラーを簡単に説明します。

 layout で全View共通のレイアウトファイル app/view/layout/scaffold.html.erbを指定しています。

 before_filter でコントローラー内の各処理(アクション)の実行前に実行される処理を定義しています。ここではauthenticateメソッドでBasic認証を行っています。認証が失敗し、このメソッドが false を戻した場合はアクションは実行されません。またこのメソッド内で利用者のユーザーidをインスタンス変数 @user_id に設定しています。

class ApplicationController < ActionController::Base
  helper :all 
  protect_from_forgery 
  filter_parameter_logging  :password
  
  layout "scaffold"
  before_filter :authenticate

  private

  def authenticate
    authenticate_or_request_with_http_basic do |user_name, password|
      @user_id = User.authenticate(user_name, password)
    end
  end
end

利用者モデル

 usersテーブルには、friendshipsテーブル、statusesテーブルに一対多の関連がありますので has_manyで宣言しています。ActiveRecord(Ruby on RailsのO/Rマッパー)ではテーブルの関連をモデルに定義する事で、簡単に関連したテーブルの情報を扱えるようになります。

 authenticateメソッドはscreen_name、passwordを受け取り認証を行うクラスメソッドです。認証が成功した場合は利用者のidが戻ります。また、今回はパスワードを暗号化していません。アイコン画像はプログラムをシンプルにするため、ユーザー登録時に予め用意された画像から選択する方式にしました。 profile_image_selectorメソッドはアイコン画像のパス名一覧を戻します。

 after_create{...} ですがuserモデルのデータをテーブルに新規作成する際に実行される処理を定義しています。ここではユーザー登録時に自分自身を自分の友人としてfriendshipsに登録しています。このように自分自身を友人に登録する事でつぶやき表示に自分のつぶやきが表示されるようにしています。

 ActiveRecordでは、このようにデータの変更・作成の前後に独自の処理を追加できます。

class User < ActiveRecord::Base
  has_many :friendships
  has_many :statuses
  
  def self.authenticate(screen_name, password)
    user = self.find_by_screen_name(screen_name)
    (user && user.password == password) ? user.id : nil
  end

  IMAGES_DIR = "./public/images/"
  PROFILE_IMAGES_DIR = "profile_images"
  
  def self.profile_image_selector
    Dir.glob(IMAGES_DIR + PROFILE_IMAGES_DIR + "/*").map {|path| path.sub(IMAGES_DIR, '')}
  end
  
  after_create {|rec| Friendship.create_self(rec.id)}

end

利用者コントローラー

 ユーザー情報の登録、表示、変更、削除を行います。ほぼscaffoldのコードですが、XML関連出力を取り除きました。またパラメータでidを指定するのではなく、認証されたユーザーidに対して表示・変更・削除を行うようにしています。

 skip_before_filter ... でユーザーの登録、トップページの処理では認証を外しています。

class UsersController < ApplicationController
  
  skip_before_filter :authenticate, :only => [:top, :new, :create]

  def index
    @user = User.find(@user_id)
  end

  def new
    @user = User.new
  end

  def edit
    @user = User.find(@user_id)
  end

  def create
    @user = User.new(params[:user])

    if @user.save
      flash[:notice] = 'ユーザー登録しました'
      redirect_to(:action => :top)
    else
      render :action => "edit"
    end
  end

  def update
    @user = User.find(@user_id)

    if @user.update_attributes(params[:user])
      flash[:notice] = 'ユーザー情報を変更しました'
      redirect_to(users_url)
    else
      render :action => "edit"
    end
  end

  def destroy
    @user = User.find(@user_id)
    @user.destroy
    redirect_to(users_url)
  end
  
  def top
  end

end

Twitter風サービスの構築2

友人モデル

 friendshipsテーブルはusersテーブルに所属しているので、 所属をbelongs_toで宣言しています。また、friend_idカラム(Friendshipクラスのfriend属性)も usersテーブルに所属しているのでbelongs_toで宣言しています、ただし属性名、カラム名が規約通りではないので :class_name、foreign_keyで設定しています。

 user属性の方は friendshipsテーブルの user_id カラムに対応し、所属先は Userクラス(usersテーブル)という規約通りなのでclass_name、foreign_keyを省略しています。

 以下はいずれも小さなメソッドばかりですが、コントローラーにはロジックを含む処理は書かずに、モデルに集めるようにするとコードの重複が減り、テストもしやすいメンテナンス性の高いコードになります。

  • friendsクラスメソッドは、指定されたmy_idの友人の一覧を戻します。ただし、友人には自分自身も含まれるので、自分自身は含まないようにしています。
  • not_friend_users クラスメソッドは、指定されたmy_idの友人でないユーザーの一覧を戻します。
  • create_selfクラスメソッドは、指定されたmy_idの自分自身を友人として登録します。

 not_friend_userssで使われている find_all_by_user_id(id)メソッドですが、これは find(:all, :conditions=>["user_id = ?", id]) と同等で、 "find_all_by_カラム名" のメソッドが実行されるとActiveRecordが動的にメソッドを生成します。

class Friendship < ActiveRecord::Base
  belongs_to :user
  belongs_to :friend, :class_name => "User", :foreign_key => "friend_id"
  
  def self.friends(my_id)
    Friendship.all(:conditions => ["user_id = ? and friend_id <> ?", my_id, my_id])
  end

  def self.not_friend_users(my_id)
    User.all(:conditions => ["id not in (?)", Friendship.find_all_by_user_id(my_id).map(&:friend_id)])
  end
  
  def self.create_self(my_id)
    Friendship.new(:user_id => my_id, :friend_id => my_id).save!
  end
end

友人コントローラー

 友人コントローラーは、友人の一覧、追加、削除を行うだけの簡単なコントローラーです。

class FriendshipsController < ApplicationController

  def index
    @friendships = Friendship.friends(@user_id)
  end

  def new
    @friendship = Friendship.new
    @not_friend_users = Friendship.not_friend_users(@user_id)
  end

  def create
    @friendship = Friendship.new(params[:friendship])
    @friendship.user_id = @user_id

    if @friendship.save
      flash[:notice] = 'フォローに登録しました'
      redirect_to(friendships_url)
    else
      render :action => "new"
    end
  end

  def destroy
    @friendship = Friendship.find(params[:id])
    @friendship.destroy

    redirect_to(friendships_url)
  end
end

つぶやきモデル

 statusesテーブルはusersテーブルに所属しているので、belongs_toで宣言しています。

 all_frendsクラスメソッドは、指定されたmy_idユーザーの友人のつぶやきの一覧を取得しています。

 api_dataメソッドはCurl等のアクセスに対し、戻すデータを組み立てるメソッドです。データは statusesから所得したレコードをHashに変換したものです。ただし user_idの代わりにユーザー情報を含むHash、{id=>ユーザーid、 screen_name=>ユーザー名、profile_image_url=>アイコンのURL}を"user"をキーとして追加しています。

 また、画像のURLはモデルが関知すべき情報ではないので、image_url引数で画像URLを求める手続きオブジェクト(関数)を渡してもらい、それを image_url.call で呼び出しています。

class Status < ActiveRecord::Base
  belongs_to :user
  
  def self.all_frends(my_id)
    Status.all(:conditions => ["friendships.user_id = ?", my_id],
                    :joins => "JOIN friendships ON statuses.user_id = friendships.friend_id",
                    :order => "created_at DESC")
  end
  
  def api_data(image_url)
    status_attrs = self.attributes
    user_attrs = User.find(self.user_id, :select => "id,screen_name").attributes
    user_attrs["profile_image_url"] = image_url.call(self.user.profile_image_path)
    status_attrs["user"] = user_attrs
    status_attrs
  end
end

つぶやきコントローラー

 つぶやきコントローラーは、友人のつぶやき一覧と、つぶやきの新規作成が主な処理です。ただし連載2回目同様にCurlのコードをクライアントに送るstart処理もあります。この処理自体の認証は外してあります。

 つぶやき一覧(friends_timeline)処理では、つぶやき情報を取得し、HTMLやJSON形式でデータを戻しています。ただし、JSONデータに含まれるアイコン画像のURLを戻す手続きオブジェクト(関数)をto_image_url変数に代入し、api_dataメソッドに渡しています。 index は friends_timeline にリダイレクトするようにしました。

 つぶやきの新規作成(create)処理は Scaffoldの作ったコードのXMLをJSONに、user_idの設定を変更しただけです。

class StatusesController < ApplicationController

  skip_before_filter :authenticate, :only => :start
  
  def index
    redirect_to friends_timeline_statuses_url
  end
  
  def start
    render :layout => false, :content_type => 'text/vnd.curl'
  end
    
  def friends_timeline
    @statuses = Status.all_frends(@user_id)
    @status = Status.new
    to_image_url = lambda{|path| url_for.sub(/ #{friends_timeline_statuses_path}$/, '/images/') + path}
    
    respond_to do |format|
      format.html # index.html.erb
      format.json  { render :json => @statuses.map{|e| e.api_data(to_image_url)}.to_json }
    end
  end

  def create
    @status = Status.new(params[:status])
    @status.user_id = @user_id

    respond_to do |format|
      if @status.save
        format.html { redirect_to friends_timeline_statuses_url }
        format.json  { render :json => @status, :status => :created, :location => @status }
      else
        format.html { render :action => "new" }
        format.json  { render :json => @status.errors, :status => :unprocessable_entity }
      end
    end
  end
end

Curlクライアントの変更

 このTwitter風サービスにCurlクライアントからアクセスするにはサーバのURLを次のように変更します。

{let server_url = "http://localhost:3000/"}

まとめ

 今回は、Curlを使ったTwitterクライアントとRuby on Railsを使った Twitter風サービスを作ってみました。 Ruby on Railsのサーバ側は複数テーブルの関連や認証などを含み、本格的なシステムの作る際の参考になると思います。また、CurlもRuby on RailsもJSONデータなどを簡単に扱えるのでTwitterのような Web+APIのシステムが手軽に作れる事を感じていただけたかと思います。

この記事は参考になりましたか?

  • このエントリーをはてなブックマークに追加

【AD】本記事の内容は記事掲載開始時点のものです 企画・制作 株式会社翔泳社

この記事は参考になりましたか?

この記事をシェア

  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/4307 2009/09/24 15:08

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング