#361 Facebook Graph API pro
- Download:
- source codeProject Files in Zip (193 KB)
- mp4Full Size H.264 Video (36.5 MB)
- m4vSmaller H.264 Video (23 MB)
- webmFull Size VP8 Video (20.8 MB)
- ogvFull Size Theora Video (62 MB)
In the previous episode we modified a Rails application to allow signing in through Facebook. To do this we registered our application with Facebook and used the OmniAuth Facebook gem to handle authentication on the server, along with an optional client-side authentication process which used the Facebook JavaScript SDK. Now that we’re able to authenticate users through Facebook we have a variety of opportunities to integrate Facebook further into our app.
Using Koala To Access The Facebook API
We’ll use the Koala gem to communicate with Facebook’s API. The documentation for this gem is good and the gem is easy to use so we’ll install it then try it out to see what we can do with it. We’ll start by adding Koala to the gemfile and then running bundle
.
gem 'koala'
Next in our User
model we’ll make a facebook
method for interacting with the Facebook API with a user’s credentials. In this method we’ll create a new Koala::Facebook::API
instance, passing the user’s oauth_token
attribute, which is saved when the user authenticates. We’ll cache the result of this method in an instance variable so that it’s only created once.
def facebook @facebook ||= Koala::Facebook::API.new(oauth_token) end
In the console we can now fetch any user that’s signed in to our application through Facebook and use methods on the facebook
method to access the API through Koala. We’ll use it to get the current user’s profile.
>> u = User.first >> u.facebook.get_object("me") #<User id:2, provider: "facebook", uid: "1843562551", name: "Ryan Bates", # Rest of response omitted>
To do this Koala uses Facebook’s Graph API and it helps to understand that we’ll be interacting with two different types of things. We can interact with objects, such as users, pages, events and so on or we can interact with the connections that link objects together, for example a user’s friends, likes and so on. Each type of object has its own separate documentation page which lists the types of attributes that are returned when we make a request for that object, along with the permissions that are required to access that attribute. We’ll discuss permissions later.
When we call Koala’s get_object
method and pass in "me
" it fetches the information for the current user through the Graph API. To get an object’s connections we can call get_connection
. This takes two arguments: the object we want to get the connections for and the type of connection we want. We’ll use this to see what television shows the current user likes.
>> u.facebook.get_connection("me", "television") => [{"name"=>"The Big Bang Theory", "Category"=>"Tv show", "id" => "22934684677", "created_time"=>"2009-09-10T01:30:34+0000"}]
This returns an array of objects, although only one is listed here. Each object returned has an id attribute returned and we can pass this to get_object to get more information about what a given connection has.
console >> u.facebook.get_object("22934684677") # Response omitted.This provides us with a full description, a picture URL and so on. Some objects have ausername
attribute that we can use instead of an id
and we can use this to get the same information.
>> u.facebook.get_object("TheBigBangTheory") # Response omitted.
Another good way to experiment with the Graph API is to use the Graph API Explorer. This allows us to access the public API by default, but if we paste in an OAuth token from a User
record we can authorize with these credentials. We can use this go get information about objects and see the connections between them.
The id
of any object returned is clickable and so we can easily see further details of anything we’re interested in. This is a great way to learn more about the Graph API and is well-worth bookmarking if you’ll be working with the API a lot.
Modifying a User’s Data
If we want to do more than just fetch information about objects the Graph API section of the Koala wiki shows the methods we call call to create wall posts, delete objects, like things and more. The RDocs for Koala’s GraphAPIMethods module has documentation on each of the methods that we can call to access the Graph API. We’ll use one of these methods now to post to a user’s wall.
>> u.facebook.put_wall_post("testing") Koala::Facebook::APIError: OAuthException: (#200) The user hasn't authorized the application to perform this action
When we do this we get an exception telling us that we’re not authorized to perform this action. This is because the user hasn’t given our application permission to post to their wall. Each authentication token has a set of permissions assigned to it and there’s documentation on the Facebook Developers site which lists the various permissions and what they allow. One of these is publish_stream
and it’s this permission that is stopping us from posting content to the user’s wall. We can determine the permissions programatically like this:
>> u.facebook.get_connection("me", "permissions") => [{"installed"=>1}]
We only have the installed
permission which means that this user has installed this application into their Facebook account. To gain permissions we have to request them from the user when they authenticate. To do this with the Facebook OmniAuth gem we need to modify the initializer where we set up OmniAuth and use the scope
option to set a comma separated list of the permissions we want to request from the user. We’ll request the permissions for viewing their email address and posting to their stream.
OmniAuth.config.logger = Rails.logger Rails.application.config.middleware.use OmniAuth::Builder do provider :facebook, ENV['FACEBOOK_APP_ID'], ENV['FACEBOOK_SECRET'], scope: "email, publish_stream" end
As we’re also using the JavaScript SDK for authentication we’ll need to add this option there too.
window.fbAsyncInit = -> FB.init(appId: '<%= ENV["FACEBOOK_APP_ID"] %>', cookie: true) $('#sign_in').click (e) -> e.preventDefault() FB.login (response) -> window.location = '/auth/facebook/callback' if response.authResponse , scope: "email,publish_stream" $('#sign_out').click (e) -> FB.getLoginStatus (response) -> FB.logout() if response.authResponse true
When a user tries to sign in to our application for the first time now they’ll be told that our application is requesting their email address. When they log in they’ll be told that our app is requesting permission to post on their behalf.
The user has the ability to choose which of the requested permissions they want to allow, so they don’t have to let our application have the ability to post to their wall. If we deny this permission we’ll still be signed in to the app but it won’t have the ability to post to our wall. This means that our app will need to be aware that different users may have different sets of permissions and be able to handle that.
Setting up Test Accounts
When we have an application that has the ability to post content we don’t want to test this behaviour on a real Facebook account. We can set up some test accounts by going to the Roles section of our app’s Facebook page and adding some test users and we can add as many of these as we want for our app. We’ll add three users, each of which will behave like a full Facebook account. We can manage these users, make them friends with each other, if we want to test that behaviour, and even switch to one so that our app behaves as if we were signed in as that user. We’ll do this now and give our application permission to post to this test user’s wall.
Once we’ve logged in as this user we can test this functionality out in the console. If we fetch the test user we’ve just logged-in as and look their permissions we’ll see that we have much more permissions than we did when we checked our own.
>> u = User.last >> u.facebook.get_connection("me", "permissions") => [{"installed"=>1, "status_update"=>1, "photo_upload"=>1, "video_upload"=>1, "email"=>1, "create_note"=>1, "share_item"=>1, "publish_stream"=>1, "publish_actions"=>1}]
We now have permissions to write to the user’s stream, upload photos and video and change the user’s status as the publish stream permission includes all these. We can test this by writing to this user’s wall.
>> u.facebook.put_wall_post("testing")
This time we don’t get an exception so this has worked. We can check this by viewing that user’s wall.
FQL
One thing we haven’t discussed yet is Facebook Query Language, FQL. This allows us to write queries that look similar to SQL and which allow us to access data in a way that’s not really possible through the Graph API. We can run FQL queries in the Graph API Explorer If we want to get, say, the pictures for the Amazon and Google accounts we can run this query:
select pic from page where username=‘amazon’ or username=‘google’
This will return a JSON data hash contain each picture’s URL. We can also run these queries in our Rails application and we’ll demonstrate this in the console.
>> u.facebook.fql_query("select pic from page where username=‘amazon’ or username=‘google’") => [{"pic”=> "http://profile.aj.fbcdn.net/hprofile-ak-snc4/373031_9465008123_1931347389_s.jpg"}, {"pic”=> "http://profile.aj.fbcdn.net/hprofile-ak-snc4/373048_104958162837_1813677875_s.jpg"}]
There’s more information about FQL in the documentation and here you’ll find more information about the different queries we can perform and a reference to the tables that can be queried against and their columns.
Error Handling
We’ll finish off this episode by talking about error handling. We’ve logged out of our application and from Facebook and when this happens all the access tokens that were associated with our account are invalidated. If we try to access the Facebook API now to get an object for the current user a Koala::Facebook::APIError
exception will be raised telling us that the session is invalid as the user has logged out. We don’t want error like these throwing 500
errors in our application so how should we handle them?
We’ll illustrate this problem in the app rather than in the console. Let’s say that we want to display the number of friends that a user has on the page. To do this we’ll add a friends_count
method to the User
model.
def friends_count facebook.get_connection("me", "friends").size end
Now in the index
template we’ll display this value.
<h1>Movies</h1> <% if current_user %> Number of Friends: <%= current_user.friends_count %> <% end %> <%= render @movies %>
Now when a user signs in through Facebook the page should display the number of friends that user has. If a user visits Facebook and logs out then revisits our application we’ll have a problem, however. We still have a current_user
in our app but, because they’ve signed out of Facebook, we’ll see the same Koala::Facebook::APIError
we saw in the console. We could modify our friends_count
method to handle this error, like this:
def friends_count facebook.get_connection("me", "friends").size rescue Koala::Facebook::APIError logger.info e.to_s nil end
If we won’t want to just return nil
here we could make a custom null object to represent the idea that the API has returned an error. This would also have the advantage of being able to access the error from this object. Even if we don’t do this at least when we reload the page now it will load correctly, even if it displays nothing where the number of friends should be.
If we find that we’re doing this kind of thing a lot in our application we could move this rescue code in to the facebook
method and use a block when we call it.
def facebook @facebook ||= Koala::Facebook::API.new(oauth_token) block_given? ? yield(@facebook) : @facebook rescue Koala::Facebook::APIError logger.info e.to_s nil end def friends_count facebook { |fb| fb.get_connection("me", "friends").size } end
This way the block will be triggered from within the facebook
method and the exception will be caught. Our page still has the same functionality it had before but the code if now a little cleaner. Another thing we could do is automatically sign the user out from our application if they’re not currently signed in Facebook. We can do this with some JavaScript code, like this:
window.fbAsyncInit = -> FB.init(appId: '<%= ENV["FACEBOOK_APP_ID"] %>', cookie: true) $('#sign_in').click (e) -> e.preventDefault() FB.login (response) -> window.location = '/auth/facebook/callback' if response.authResponse , scope: "email,publish_stream" $('#sign_out').click (e) -> FB.getLoginStatus (response) -> FB.logout() if response.authResponse true if $('#sign_out').length > 0 FB.getLoginStatus (response) -> window.location = $('#sign_out').attr('href') if !response.authResponse
The new code is the three lines at the bottom. Here we check to see if there’s a sign out link on the page, which indicates that the current user is signed in to our app. if there is we get their Facebook login status. If they’re not logged in then we set the window’s location to the sign out page. If we try this, though, it doesn’t work and this is because our app is running in Sandbox Mode which makes it invisible to all public users so when we sign our of our app it doesn’t properly initialize with the JavaScript SDK. If we go to our app’s page on Facebook and disable sandbox mode, then go back to our application and reload the page the “Sign out” link will be triggered and we’ll be signed out.
That’s it for this episode but there’s more to the Facebook API than we’ve covered here. One thing we haven’t covered are realtime updates. There’s more information about this on the Koala gem’s README page but essentially this feature allows us to subscribe and listen to event changes and fire a callback information when an event fires.
We’ve covered a lot in this episode but there’s still more we can do to integrate Facebook into our application. For example when a user writes a review of a film it could notify other Facebook users about that review so that they can come to our application and read it. We could post to the user’s wall when they write a review but this is poor behaviour. There must be a better way to do this and there is: using the Open Graph protocol. We won’t cover this here, but it will be covered in a future episode.