Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / Ruby

Where in the World are my Facebook Friends?

Rate me:
Please Sign up or sign in to vote.
5.00/5 (12 votes)
8 Oct 2013CPOL17 min read 38K   15   6
Locate your Facebook friends on a Google Map - A Ruby on Rails web application
This article walks you through the process of creating your very own "Where Are My Friends" web application with Ruby on Rails.

Image 1

Introduction

I've been working on a project involving Facebook authentication and embedding "likes" and "posts", as well as mapping locations of crowdfunding projects onto Google Maps. A few days ago, I thought, gee, what if I could map my Facebook friends locations on Google Maps. Turns out this has already been done, but that doesn't stop me from wanting to learn how it's done! This article walks you through the process of creating your very own "Where Are My Friends" web application with Ruby on Rails.

Prerequisites for Ruby on Rails Development in Windows

Since we're mostly Windows developers on Code Project, let's stay in the Windows platform environment. To do this, you'll need to:

  1. Download and install RailsInstaller
  2. Download and install a decent IDE, namely RubyMine (there's a 30 day trial version, but it's quite affordable and quite excellent)

Prerequisites for Facebook Development

You'll need to create an app from your Facebook account. To do this (note that the UI might change over time), log in to Facebook and, from the "gear" pulldown, select "Create App":

Image 2

Create an application, providing:

  • a unique app namespace
  • set the Site Domain to "localhost"
  • set the Site URL to http://localhost:3000/
  • Sandbox mode should be enabled

Once you've completed the process, you will be provided with an App ID and App Secret. You'll need these for obtaining a user access token later on.

Prequisites for Querying Facebook For Testing Purposes

A user access token is needed to obtain the friend location and hometown, which we use to map where our friends are. For testing purposes, we can acquire this token manually, but beware that it expires every two hours, so you will have to repeat this step if you get a "token expired" error.

Go to https://developers.facebook.com/tools/explorer.

Then click on Get Access Token. A popup will appear, from which you should select "Friends Data Permissions" and then check "friends_hometown" and "friends_location":

Image 3

Click on Get Access Token, which closes the dialog and now your access token will be displayed in the Graph API Explorer. You can copy this access token into your code for temporary access when testing the application.

Test the Query

While we're here, we might as well test the query we'll be using. Click on "FQL Query" and enter:

SQL
SELECT uid, name, pic_square, current_address, current_location, 
hometown_location FROM user WHERE uid IN (SELECT uid2 FROM friend WHERE uid1 = 
me())

Then click the Submit button. You should see an array returned, and if your friends entered information as to where they are living and/or their hometown and make this information public, then you should see something along the lines of:

Image 4

We'll be parsing these records later in Ruby.

Prerequisites for Connecting to Facebook

Windows is lacking SSL information which will result in an SSL authentication failure. To correct this problem, follow exactly these instructions:

  1. Download http://curl.haxx.se/ca/cacert.pem into c:\railsinstaller\cacert.pem
  2. Go to your Computer -> Advanced Settings -> Environment Variables
  3. Create a new System Variable:
    • Variable: SSL_CERT_FILE
    • Value: C:\RailsInstaller\cacert.pem

For some reason, it originally took me three tries to get this right.

Prerequisites for GitHub

This part is optional. The source code for this project is available on GitHub at WhereAreMyFriends. If you want to set up your own GitHub project, this is what I did:

  • If you don't already have a GitHub account, create one
  • Create a new repository with a readme so you can immediately clone the repository
  • From the command line, clone the repository into the desired folder. I cloned mine using "git clone https://github.com/cliftonm/WhereAreMyFriends"
  • You can also simply clone my repository and use the code that I wrote, but you won't be able to commit any changes back to me. For that, you'd need to fork my repository and then issue "pull requests" if you want me to incorporate some changes in my project that you've made.

Prerequisites for Using Git

This is optional for two reasons: you can use the Git command line or you can use RubyMine's Git integration for working with the repository. Personally, I prefer to use a separate visual tool such as SmartGit/Hg, which I've found to be the best of the various visual tools for Git.

Getting Started

We've got a few housekeeping things to take care of before we get started with actual coding.

Creating the Initial Rails App

If you've cloned my repository, ignore this step, as you can simply open the directory in RubyMine.

If you're starting from a blank slate because you want to walk through how I wrote this app, then you'll need to create a Rails app. Again, from the command line, go to the parent directory into which you create the directory and contents for the project. If you've cloned a blank repository from GitHub, don't worry, just do this as well.

From the command line, type in "rails new WhereAreMyFriends" (or, if you gave your project a different name, use that name.) This will create all the pieces for a Ruby on Rails application.

In the RubyMine IDE, you should now see something like this when you open the directory:

Image 5

If you've cloned a Git repository, RubyMine should already be configured to use Git as the VCS. Personally, I much prefer using SmartGit/Hg, but you should know that RubyMine has built-in Git support.

We Want Git to Ignore RubyMine Files

We don't want all the RubyMine IDE files to be part of the repository, so open the ".gitignore" file (in the application's root folder) and add:

/.idea

which excludes the entire .idea folder that RubyMine creates.

Components (Gems) We'll be Using

We need to pull in a few components, so edit the Gemfile (in root of your application folder), adding:

gem 'gmaps4rails'
gem 'fql'
gem 'slim'
gem 'thin'

Once the Gemfile is updated, click on the Tools menu is RubyMine, then select "Bundler..." then "Install", then click on the Install button (leaving the optional arguments blank.) This installs the gems and any dependencies that they have.

What are all these gems?

gmaps4rails

This is the gem for interfacing to Google Maps (as well as others, such as OpenLayers, Bing, and Mapquest).

fql

This gem supports using the Facebook Query Language in Ruby, which I use to query the locations of my friends. There are other options as well and other techniques for querying Facebook, but this is the approach I've chosen.

The Facebook FQL reference documentation can be found here.

slim

This gem, from the website: "is a template language whose goal is [to] reduce the syntax of essential parts without becoming cryptic." I find it makes HTML a lot more readable, de-cluttering the angle brackets, closing tags, etc. There's a great online utility for converting HTML to slim for here.

thin

This gem is a much faster web server than the default, which is WEBrick.

Add All the Pieces Needed for Gmaps

The gmaps4rails gem includes an installer that adds all the JavaScript and CSS that you need for actually displaying a map. To do this, open a command line prompt and cd to your application folder. Then type:

rails generate gmaps4rails:install

Create the Page Controller

While we're on the command line, let's create the controller and view. Type:

rails generate controller map_my_friends index

This creates:

  • the file "map_my_friends_controller.rb" in the app\controllers folder.
  • the folder "map_my_friends" in the app\views folder.
  • the file "index.html.erb" in the app\views\map_my_friends folder.

Delete the "index.html.erb" file and create a new file called "index.html.slim", so that we're using the slim HTML syntax rather than straight HTML.

You should see something like this now in your project tree:

Image 6

Setting the Root Route

Finally, before we do anything else, let's set the root route to this page, so we can get to it simply from "localhost:3000". Edit the routes.rb file (in the config folder), adding:

root to: "map_my_friends#index"

Note that when we created the page controller, the route:

get "map_my_friends/index"

was automatically added for us.

Start Coding!

Now we're ready to do some coding. First, we're going to create a basic model, "Friend", to hold the information about our friends. We could do this with a generator similar to how we created the controller, but because it's not a vanilla solution, I prefer to simply create the file manually.

Create the 'Friend' Model

In RubyMine, under the app\models folder, create the file "friend.rb":

class Friend < ActiveRecord::Base
  acts_as_gmappable

  # Fields we get from FB
  attr_accessible :uid, :name, :pic, :address
  # Fields required by gmaps4rails (lat and long also come from FB)
  attr_accessible :gmaps, :latitude, :longitude

  # gmaps4rails methods
  def gmaps4rails_address
    address
  end

  def gmaps4rails_infowindow
    "#{name}"
  end
end

The line "acts_as_gmappable" is a hook that generates latitude and longitude data when an address is persisted. While it wasn't my intention to even have a persisting Friend model, the gmaps4rails gem is somewhat coupled with the Rails ActiveRecord and the five minutes I spent Googling and playing around trying to decouple it, without success, was five minutes more than I wanted to spend on the issue, so as a result, we have a persistable Friend model.

Create the 'Friend' Table

No model is usually complete without its associated table, so we will use a database migration to create the table. Since we're using sqlite3 as the database, there's no need to futz around with database authentication issues, database servers, etc.

In RubyMine, in the "db" folder, create a sub-folder called "migrate", and in that folder, create a file called "001_create_friends_table.rb":

class CreateFriendsTable < ActiveRecord::Migration
  def change
    create_table :friends do |t|
      t.string :uid
      t.string :name
      t.string :pic
      t.string :address
      t.float :latitude
      t.float :longitude
      t.boolean :gmaps
      t.timestamps
    end
  end
end

Your project tree should now reflect these two new files:

Image 7

Now, run the migration by pressing Ctrl+F9, or right-clicking on the migration and from the popup menu selecting "run db:migrate".

Create Our Facebook Library

A "standard" practice in Ruby on Rails code is to put directly into the controller all the code that's needed to render a page. So, typically, you would see the code that queries Facebook and populates the model either in the controller or in the model. Personally, I prefer to put this kind of code into the lib folder and provide helper methods to interface to whatever model supports the necessary fields. I've read some articles that disagree with my on this point, saying that all business logic should go in the model. The problem as I see it is that there is application-model-independent business logic (as in, agnostic business logic), such as how we interface with Facebook, that shouldn't go into the application's model because it is agnostic.

However, because Rails does not auto-load the code in the lib folder, we have to coerce it. Also note that files in the lib folder are don't automatically cause the server to reload the Ruby script, so you'll have to restart the server if you make changes in the lib folder's files.

First, edit the application.rb file found in the app\config folder, adding the line:

config.autoload_paths += %W(#{config.root}/lib/facebook_wrapper)

which tells Rails we specifically want to include files found in this folder.

Second, in the lib folder, create a sub-folder called "facebook_wrapper".

Third, create a file in that sub-folder called "facebook_wrapper.rb".

Your project structure should now look like this:

Image 8

Now we're going to wrap our class, FacebookWrapper, in a module called FacebookWrapperModule:

module FacebookWrapperModule
  class FacebookWrapper
    def ... my functions ...
  end
end

and implement the following functions.

get_fb_friends

This function returns an array of friends in Facebook structure:

def self.get_fb_friends
  options = {access_token: "[your access token goes here]"}
  friends = Fql.execute("SELECT uid, name, pic_square, current_address, 
     current_location, hometown_location FROM user WHERE uid IN (
     SELECT uid2 FROM friend WHERE uid1 = me())", options)

  friends
end

We will fix the hardcoded access token later - for the moment, we just want to get something up and running.

from_fb_friends

This function converts the Facebook friends array into an array of our model instances, which we callback to the application to create each model instance. Because Ruby is a duck-typing language, all the application needs to do is implement attributes (properties) or methods for the attributes we expect to initialize - we don't need to know the "type" or implement this as an interface, as we would in C#. Furthermore, by utilizing the callback capability of Ruby, we can request that the application instantiates its model instance itself:

def self.from_fb_friends(fb_friends)
  friends = []

  fb_friends.each do |fb_friend|
    location = get_location_or_hometown_address(fb_friend)

    if !location.nil?  # or: unless location.nil?
      friend = yield(fb_friend, location)
      friends << friend
    end
  end

  friends
end

Another Ruby-ism is to use the "unless" keyword rather than "if !" (if not), which I personally find reduces the readability of the code. I have no problem with negative logic, and saying "unless location.nil?" requires me to do mental gyrations back to "if locations does not equal nil."

get_location_or_hometown_address

Lastly, we have a private helper method for getting the address information from either the friend's location (preferable) or their hometown (a fallback):

private

def self.get_location_or_hometown_address(fb_friend)
  location = fb_friend["current_location"]

  if location.nil?
   location = fb_friend["hometown_location"]
  end

  location
end

Note that we never explicitly use the "return" keyword. There's a reason for that, which I'll illustrate next.

Update the Controller to Pass Along the Location Information

Next, we'll update the map_my_friends_controller.rb file, the controller for our index. The first thing we need to do is reference our facebook_wrapper library helper. This reveals some of the intricacies of Ruby's module and file handling.

First, we need to tell Ruby that we "require" the facebook_wrapper.rb file (which it knows how to get because we added lib\facebook_wrapper to the auto_load config paths):

require 'facebook_wrapper'

Then, we need to tell Ruby that we want to use the objects defined in the FacebookWrapperModule:

include FacebookWrapperModule

If we don't do this, we have to qualify the objects with "FacebookWrapperModule::". The include keyword is similar to the using keyword in C#, and the module keyword is similar to the namespace keyword. The only thing new here is the dynamic loading of a dependent file facebook_wrapper.rb.

The implementation for the index method gathers the arrays and provides the callback method for populating a model (Friend) instance for each instance of a Facebook structure, and finally that array is formatted as JSON and passed back to the client:

class MapMyFriendsController < ApplicationController
  def index
    fb_friends = FacebookWrapper.get_fb_friends
    @friends = FacebookWrapper.from_fb_friends(fb_friends) { |fb_friend, location|
        friend = Friend.new
        friend.uid = fb_friend["uid"]
        friend.name = fb_friend["name"]
        friend.pic = fb_friend["pic_square"]
        friend.address = location["name"]
        friend.latitude = location["latitude"]
        friend.longitude = location["longitude"]
        friend.gmaps = true

        friend
    }

    @json = @friends.to_gmaps4rails
    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @friends }
    end
  end
end

Note that we do not call return friend in the callback code - if we do this, it's treated as a return from the calling code and the @friends property is never initialized!

Getting the View to Render the Map

Edit the index.html.slim file (in the app\views\map_my_friends folder), adding this one line as the entire contents of the file:

= gmaps4rails(@json)

Test This

If you run the application (with a current user access token), you should see your friends mapped onto a Google map:

Image 9

Making the Map Bigger

However, what we'd to do is make the map bigger, so that it takes up most of a full-screen browser window (I do everything in full screen which is why I like this). To do this, replace the line that we created above with:

= gmaps( :map_options => { :container_class => "my_map_container" }, 
  "markers" => {"data" => @json, 
  "options" => {"auto_zoom" => false} })

and edit the map_my_friends.css.scss (found in the app\assets\stylesheets folder), adding:

CSS
div.my_map_container {
  margin-top: 30px;
  padding: 6px;
  border-width: 1px;
  border-style: solid;
  border-color: #ccc #ccc #999 #ccc;
  -webkit-box-shadow: rgba(64, 64, 64, 0.5) 0 2px 5px;
  -moz-box-shadow: rgba(64, 64, 64, 0.5) 0 2px 5px;
  box-shadow: rgba(64, 64, 64, 0.1) 0 2px 5px;
  width: 80%;
  height: 80%;
  margin-left:auto;
  margin-right:auto;
}

div.my_map_container #map {
  width: 100%;
  height: 100%;
}

Refresh the browser and you will get bigger map which sizes based on the browser window.

Adding Some Pizzazz to the Thumbnail Popup

First, we'll add profile_url to the FQL query that we're using, and adjust our model and controller accordingly. We also need to add a migration to add this field to the Friend table:

class AddProfileUrlField < ActiveRecord::Migration
  def change
    add_column :friends, :profile_url, :string
  end
end

Next, we provide some HTML to render in the Google Maps info window:

def gmaps4rails_infowindow
  "<p><a href = '#{profile_url}' target='_blank'>#{name}</a>
   <br>#{address}<br><img src = '#{pic}'/></p>"
end

and the result is:

Image 10

showing us

  • the person's name as a clickable link to their Facebook profile that opens in a new window,
  • their location/hometown, and
  • their Facebook picture.

Omniauth-Facebook

Now that we have a basic application running, let's deal with a different set of complexity which will also resolve the pesky user access token expiration problem. The issue is this - rather than gathering our friends, we need authorization to gather the friends of anyone that visits our site, which means that we'll need the ability for users to log in using their Facebook login and authorize us to query their data.

Add omniauth-facebook to the Gemfile

First, add:

gem 'omniauth-facebook'

to the Gemfile found in your root folder. The gems we've now added to the Gemfile for this project are:

gem 'gmaps4rails'
gem 'fql'
gem 'slim'
gem 'thin'
gem 'omniauth-facebook'

Create a User Model

This time, let's create the user model with the model generator. From RubyMine's Tool menu, select "Run Rails Generator" then double-click on "model". Enter the options for the rails generator:

User provider:string uid:string name:string email:string oauth_token:string

Image 11

This creates a new migration file, so find it under db\migrate, right click on it and select "Run 'db:migrate' "

In the newly create User model, add the following code:

def self.create_with_omniauth(auth)
  create! do |user|
    user.provider = auth.provider
    user.uid = auth.uid
    user.oauth_token = auth.credentials.token

    if auth.info
      user.name = auth.info.name || ""
      user.email = auth.info.email || ""
    end
  end
end

This code creates a user in the database with the provided authentication information.

Notice that we have access here to the user access token, which is saved in the field oauth_token.

Setup Authentication

In the config\initializers folder, create the file omniauth.rb with the contents:

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET'],
            :scope => 'friends_location, friends_hometown, user_friends, email',
           :display => 'popup'
end

This informs omniauth that we're authenticating with Facebook. Notice the "scope" key, whose values are friends_location and friends_hometown, which specifies that we're interested in the location and hometown of our friends, and we need user_friends so that we can get the friends of the Facebook user.

Setup Environment Variables

Personally, I don't like environment variables - I would rather use a file that isn't stored in the Git repository.  Previously, I've used a local_env.yml file to programmatically add items to the ENV collection:

Edit the application.rb file (located in the config folder) and add:

config.before_configuration do
  env_file = File.join(Rails.root, 'config', 'local_env.yml')
  YAML.load(File.open(env_file)).each do |key, value|
    ENV[key.to_s] = value
  end if File.exists?(env_file)
end

This code adds additional items to the ENV collection. Now we need to create the file. In the config folder, create the file local_env.yml, whose contents are:

FACEBOOK_KEY: '[your key]'
FACEBOOK_SECRET: '[your secret id]'

Make sure that when you put in your key and secret ID, that you preserve the single quotes.

Also, add config/local_env.yml to your .gitignore file -- this prevents the file from being added to your repository.

Create the Sessions Controller

In the app\controllers folder, create the file sessions_controller.rb, whose contents are:

class SessionsController < ApplicationController

  def new
    redirect_to '/auth/facebook'
  end

  def create
    auth = request.env["omniauth.auth"]
    user = User.where(:provider => auth['provider'], 
                      :uid => auth['uid']).first || User.create_with_omniauth(auth)
    session[:user_id] = user.id
    redirect_to root_url, :notice => "Signed in!"
    end

  def destroy
    session[:user_id] = nil
    redirect_to root_url, notice: 'Signed out!'
  end
end

This handles three routes:

  • new - simply redirects to the Facebook authentication page
  • create - performs the sign in, registering the user in the database if the uid is unique for this provider (being Facebook)
  • destroy - signs out from Facebook and removes the user's UID from the database, forcing the user to sign in again (useful for testing and getting a new authentication token)

Create the Necessary Routes

Add the following routes to the routes.rb file (in the config folder):

match '/auth/:provider/callback' => 'sessions#create'
match '/signout' => 'sessions#destroy'
match '/signin' => 'sessions#new'

Change the User Access Token to Use the oauth_token

In our map_my_friends_controller, we're going to pass in the access token, which we acquire from the database with the user's id stored when the session was created:

def index
  user_id = session[:user_id]
  @friends = []

  if !user_id.nil?
    oauth_token = User.find(user_id).oauth_token
    @friends = get_friends(oauth_token)
  end

  @json = @friends.to_gmaps4rails
  respond_to do |format|
    format.html # index.html.erb
    format.json { render json: @friends }
  end
end

Notice that I separated out the get_friends code into a separate function. Another thing you'll often see in a lot of Ruby on Rails code is very long functions with code blocks that really should be broken out. It's easy to write the code the "wrong" way because you're dealing with specific, isolated route handler functions, but it makes things a lot less readable and maintainable.

And in facebook_wrapper.rb:

def self.get_fb_friends(oauth_token)
  options = {access_token: oauth_token}
  friends = Fql.execute("SELECT uid, name, pic_square, current_address, 
    current_location, hometown_location, 
    profile_url FROM user WHERE uid IN (SELECT uid2 
    FROM friend WHERE uid1 = me())", options)

  friends
end

Update the Application View with Sign In / Sign Out

In our application view (common to all pages), we want to provide:

  • sign in / sign out notification messages
  • a sign in / sign out button
  • the name of the user signed in

We might as well convert this file to a "slim" file as well, so delete application.html.erb (in the app\views\layouts folder), and replace it with the slim file "application.html.slim":

Edit the application.html.erb file (in the app\views\map_my_friends folder), inserting at the top of the file:

doctype
html
  head
    title Where Are My Friends
    = stylesheet_link_tag "application", media: "all"
    = javascript_include_tag "application"
    = csrf_meta_tag
  body
    #container
      #user_nav
        - if current_user
          | Signed in as
          | &nbsp;
          strong= current_user.name
          | &nbsp;
          = link_to "Sign out", signout_path
        - else
          = link_to "Sign in with Facebook", signin_path
      - flash.each do |name, msg|
        = content_tag :div, msg, id: "flash_#{name}"
      = yield
      = yield :scripts

Access the User Record from our View

To access the current_user that we used above, we'll add a helper function to application_controller.rb (in the app\controllers folder):

class ApplicationController < ActionController::Base
  protect_from_forgery

  private

  def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
  end

  helper_method :current_user
end

There's also a bunch of CSS that I'm not showing. The result is now a usable website:

Image 12

Try It Out Live!

The application is hosted here: http://wherearemyfriends.herokuapp.com/ give it a try!

Caveats

If your friends haven't set a current location or hometown or if this information is blocked, they won't show up on the map. I also don't distinguish between current location and hometown - that would be something nice to do with a different marker. So there's a couple things I'll get around to at some point and update the article.

Also, it's interesting working with a Facebook app. For example, if I want my housemate to try out the site after I've signed in, while I can sign out from my site, I also need to sign out from Facebook (by going to Facebook!) and only then will I get the Facebook sign in so my housemate can sign in with her Facebook username and password.

Acknowledgements

I'm indebted specifically to the following people who have no idea that they helped me put all this together! This, of course, omits all the people that have put in countless hours writing Ruby, Rails, and all these amazing gems.

History

  • 7th October, 2013: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
GeneralGreat work! Pin
Afzaal Ahmad Zeeshan21-Aug-15 1:19
professionalAfzaal Ahmad Zeeshan21-Aug-15 1:19 
QuestionI can see CG in your screen shot! Pin
User 5838525-Dec-13 13:22
User 5838525-Dec-13 13:22 
He's a long, long way from the majority of your FB friends Smile | :)
AnswerRe: I can see CG in your screen shot! Pin
Marc Clifton5-Dec-13 14:09
mvaMarc Clifton5-Dec-13 14:09 
GeneralNice Article.. Pin
Pasan Eeriyagama7-Oct-13 19:36
Pasan Eeriyagama7-Oct-13 19:36 
QuestionWow Marc! Pin
Tom Clement7-Oct-13 10:45
professionalTom Clement7-Oct-13 10:45 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.