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.