fist in your facebook

Jared Carroll

Here at t-bot, we just finished porting a part of one of our existing Rails applications to utilize the new Facebook platform. It took about a week, and despite the decent documentation for the Facebook platform, there was definitely a lot of trial and error. There also seems to not be a lot of resources on using Rails and the Facebook platform, so we thought we’d give back what we learned.

First, some basic info about the Facebook platform.

Basically Facebook acts like a middleman between the Facebook user and your Rails app. So when a Facebook user clicks on a link or submits a form in your Facebook application, Facebook POSTs that information, along with some of its own information, to your Rails application. When you configure your Facebook app settings, you tell Facebook that whenever it gets a request for, say, ‘https://apps.facebook.com/your-facebookapp-name’ to route that to ‘https://www.your-railsapp.com’.

EVERYTHING IS A POST.

Remember this.

It’s important because your RESTful routes aren’t going to work. You’ll need named routes in addition to your RESTful routes.

In routes.rb:

ActionController::Routing::Routes.draw do |map|

  map.resources :users

  map.with_options :controller => 'users' do |m|
    m.facebook_users 'your-facebookapp-name/users', :action => 'index'
  end

end

Say we configured our Facebook app settings to route every request from a Facebook user from ‘https://apps.facebook.com/your-facebookapp-name’ to ‘https://www.your-railsapp.com’.

Now when a Facebook user goes to ‘https://apps.facebook.com/your-facebookapp-name/users’ Facebook is going to do a POST to ‘https://your-railsapp.com/users’, which when using RESTful routes, will get routed to users#create. But you wanted ‘/users’ to be a list of all users available by a GET, just like a normal RESTful route. So you’ll need to add a named route; that way you can POST to ‘https://apps.facebook.com/your-facebookapp-name/users’ and get routed to users#index.

Controller structure

1) Keep your same controllers, and add conditional logic to determine if the request was from Facebook:

class UsersController < ApplicationController

  def show
    @user = User.find params[:id]
    if facebook?
      render :template => 'show_fbml', :layout => 'facebook'
    end
  end

 private

  def facebook?
    ! params[:fb_sig].nil?
  end

end

In every request from Facebook, which is always a POST, Facebook will send some of its own parameters. One of those parameters is fb_sig which means the request came from Facebook. Since our views in the Facebook portion of the app were different from the views in the non-Facebook portion of the app, we decided to create separate views to keep things separate. The above line:

render :template => 'show_fbml', :layout => 'facebook'

renders our Facebook ‘show’ page. We adopted the convention of suffixing all Facebook views with _fbml as well as using a separate layout specifically for Facebook. fbml stands for FaceBook Markup Language. It’s basically HTML plus a bunch of Facebook specific tags such as:

    <fb:action href="new.php">Create a new photo album</fb:action>

which renders to a link. We used mostly just HTML in our Facebook views with some FBML for the Facebook specific headers and navigation. We adopted the _fbml file name suffix because when executing the following in a view:

<%= render :partial => 'user', :object => @user %>

Rails will always look for a file in the current action’s controller’s views directory named _user.rhtml. The file it’s looking for will always be prefixed with an underscore and its file type will be ‘.rhtml’. It doesn’t matter if you specify the file extension or not in the #render call. Originally, the plan was to name all our Facebook views <view-name>.fbml.

You can take this a little farther to clean up your controllers some more and make them look more RESTful.

class UsersController < ApplicationController

  def show
    @user = User.find params[:id]
    respond_to do |wants|
      wants.html
      wants.fbml
    end
  end

end

And add the following in config/environment.rb or a file in lib:

Mime::Type.register 'text/html', :fbml
ActionController::MimeResponds::Responder::DEFAULT_BLOCKS[:fbml] = %(lambda {
  render :action => "\#{action_name}_fbml", :layout => 'facebook'
})

That adds a custom MIME type that we can then use in #respond_to blocks.

The lambda function in the string will be the default block that gets executed when you write the following in an action:

class UsersController < ApplicationController

  def new
    @user = User.new
    respond_to do |wants|
      wants.fbml
    end
  end

end

That’s going to try to render a file named new_fbml.rhtml in app/views/users. By using #respond_to, we can get rid of the conditional logic and #facebook? query method in our controller, which is nice.

In order for the #respond_to to work, you’ll need to make sure all your urls requested from Facebook end in .fbml or add a default :format parameter to your named routes. So you’ll have to update your routes file:

In routes.rb:

ActionController::Routing::Routes.draw do |map|
  map.with_options :controller => 'users' do |m|
    m.facebook_user 'your-facebookapp-name/user/:id.fbml',
      :action => 'show'
    # or
    m.facebook_user 'your-facebookapp-name/user/:id',
      :action => 'show',
      :format => 'fbml'
  end
end

You can write a functional test for the fbml MIME type like normal, but you’ll need to include the :format parameter on your POSTs:

def test_should_find_the_user_with_the_given_id_on_POST_to_show_from_facebook
  post :show, :id => users(:one).id, :format => 'fbml'

  assert_equal users(:one), assigns(:user)
  assert_response :success
  assert_template 'show_fbml'
end

2) Use namespaced controllers such as:

class Facebook::UsersController < ApplicationController
end

We started out this way in order to keep a nice separation between our Facebook and non-Facebook parts of the app. However, there was just too much duplication in the controllers so we decided to use method #1 and put Facebook specific conditional logic in our existing non-namespaced controllers.

Views

Always make sure to use :only_path => true in all your named route calls like so:

<% form_for :user, @user,
            :url => facebook_create_user_url(:only_path => true) do |form| -%>
<% end %>

All URLs should be relative, so Facebook can append ‘https://apps.facebook.com/’ to each of them. However, Facebook will not append your Facebook application’s unique path prefix (in the above examples: ‘your-facebookapp-name’), so your named routes will have to include it:

In routes.rb:

map.facebook_user 'your-facebookapp-name/user/:id', :action => 'show'

Now this works, but you’ll probably want multiple environments for your Facebook application. So you have to create multiple Facebook applications, each with a unique path prefix, and then externalize that path prefix in your Rails environment specific files. The above code then becomes:

In routes.rb:

map.facebook_user "#{FACEBOOK_PATH_PREFIX}/user/:id", :action => 'show'

And in config/environments/development.rb

FACEBOOK_PATH_PREFIX = 'your-facebookapp-name-development'

And in config/environments/production.rb

FACEBOOK_PATH_PREFIX = 'your-facebookapp-name'

Library

At first I’d thought to try write my own library to make all the Facebook API calls, but then reconsidered because of time constraints, to use the rFacebook API. It doesn’t feel very Rubyish because it looks like basically a straight port of the PHP Facebook client. It gives you a Facebook session object that you can use to make Facebook API calls pretty easily, like:

class UsersController < ApplicationController

  # mix in the library
  include RFacebook::RailsControllerExtensions

  def show
    doc = fbsession.users_getInfo :uids => [fbsession.session_user_id],
          :fields => %w(first_name last_name)
    # doc is an Hpricot XML document
    @user = User.find :first,
           :conditions => [name = ?',
           "#{doc.at('first_name').inner_html} #{doc.at('last_name').inner_html}"]
  end

end

rFacebook uses _why’s Hpricot, so make sure you got that gem unpacked in your ‘vendor/plugins’.

Exceptions

One exception we kept seeing in our logs was:

RFacebook::FacebookSession::NotActivatedException
  (You must activate the session before using it.)

The way to ‘activate’ your session is to log into Facebook, go to your application page, go to its ‘About’ page, click the ‘Add Application’ button and then add the application to your list of applications. This got rid of the exception every time.

Flash and Session

Since the Rails flash is associated with each Rails session, you can’t use it because Facebook does not pass the Rails cookie back on each request it proxies to your Rails app. Instead, each request from Facebook is seen as a brand new session to your Rails app. This also prevents you from using the session.

One solution here would be to write your own FacebookSession and somehow configure Rails to use that instead of its default whenever you reference session in a controller. Facebook specific session information is passed along from Facebook to your Rails app in every POST, which could be used as a session key.

update: Moved :layout parameter from the ‘fbml’ content type respond_to block to the ‘fbml’ custom MIME type’s default block.