Building the Facebook Clone using Ruby

7 min read

(For more resources on Ruby, see here.)

This is the largest clone and has many components. Some of the less interesting parts of the code are not listed or described here. To get access to the full source code please go to http://github.com/sausheong/saushengine.

Configuring the clone

We use a few external APIs in Colony so we need to configure our access to these APIs. In a Colony all these API keys and settings are stored in a Ruby file called config.rb as below.

S3_CONFIG = {}
S3_CONFIG['AWS_ACCESS_KEY'] = '<AWS ACCESS KEY>'
S3_CONFIG['AWS_SECRET_KEY'] = '<AWS SECRET KEY>'
RPX_API_KEY = '<RPX API KEY>'

Modeling the data

You will find a large number of classes and relationships in this article.

The following diagram shows how the clone is modeled:

User

The first class we look at is the User class. There are more relationships with other classes and the relationship with other users follows that of a friends model rather than a followers model.

class User
include DataMapper::Resource

property :id, Serial
property :email, String, :length => 255
property :nickname, String, :length => 255
property :formatted_name, String, :length => 255
property :sex, String, :length => 6
property :relationship_status, String
property :provider, String, :length => 255
property :identifier, String, :length => 255
property :photo_url, String, :length => 255
property :location, String, :length => 255
property :description, String, :length => 255
property :interests, Text
property :education, Text
has n, :relationships
has n, :followers, :through => :relationships, :class_name =>
'User', :child_key => [:user_id]
has n, :follows, :through => :relationships, :class_name => 'User',
:remote_name => :user, :child_key => [:follower_id]
has n, :statuses
belongs_to :wall
has n, :groups, :through => Resource
has n, :sent_messages, :class_name => 'Message', :child_key =>
[:user_id]
has n, :received_messages, :class_name => 'Message', :child_key =>
[:recipient_id]
has n, :confirms
has n, :confirmed_events, :through => :confirms, :class_name =>
'Event', :child_key => [:user_id], :date.gte => Date.today
has n, :pendings
has n, :pending_events, :through => :pendings, :class_name =>
'Event', :child_key => [:user_id], :date.gte => Date.today
has n, :requests
has n, :albums
has n, :photos, :through => :albums
has n, :comments
has n, :activities
has n, :pages

validates_is_unique :nickname, :message => "Someone else has taken
up this nickname, try something else!"
after :create, :create_s3_bucket
after :create, :create_wall

def add_friend(user)
Relationship.create(:user => user, :follower => self)
end

def friends
(followers + follows).uniq
end

def self.find(identifier)
u = first(:identifier => identifier)
u = new(:identifier => identifier) if u.nil?
return u
end

def feed
feed = [] + activities
friends.each do |friend|
feed += friend.activities
end
return feed.sort {|x,y| y.created_at <=> x.created_at}
end

def possessive_pronoun
sex.downcase == 'male' ? 'his' : 'her'
end

def pronoun
sex.downcase == 'male' ? 'he' : 'she'
end

def create_s3_bucket
S3.create_bucket("fc.#{id}")
end

def create_wall
self.wall = Wall.create
self.save
end

def all_events
confirmed_events + pending_events
end

def friend_events
events = []
friends.each do |friend|
events += friend.confirmed_events
end
return events.sort {|x,y| y.time <=> x.time}
end

def friend_groups
groups = []
friends.each do |friend|
groups += friend.groups
end
groups - self.groups
end
end

As mentioned in the design section above, the data used in Colony is user-centric. All data in Colony eventually links up to a user. A user has following relationships with other models:

  • A user has none, one, or more status updates
  • A user is associated with a wall
  • A user belongs to none, one, or more groups
  • A user has none, one, or more sent and received messages
  • A user has none, one, or more confirmed and pending attendances at events
  • A user has none, one, or more user invitations
  • A user has none, one, or more albums and in each album there are none, one, or more photos
  • A user makes none, one, or more comments
  • A user has none, one, or more pages
  • A user has none, one, or more activities
  • Finally of course, a user has one or more friends

Once a user is created, there are two actions we need to take. Firstly, we need to create an Amazon S3 bucket for this user, to store his photos.

after :create, :create_s3_bucket

def create_s3_bucket
S3.create_bucket("fc.#{id}")
end

We also need to create a wall for the user where he or his friends can post to.

after :create, :create_wall
def create_wall
self.wall = Wall.create
self.save
end

Adding a friend means creating a relationship between the user and the friend.

def add_friend(user)
Relationship.create(:user => user, :follower => self)
end

Colony treats the following relationship as a friends relationship. The question here is who will initiate the request to join? This is why when we ask the User object to give us its friends, it will add both followers and follows together and return a unique array representing all the user’s friends.

def friends
(followers + follows).uniq
end

In the Relationship class, each time a new relationship is created, an Activity object is also created to indicate that both users are now friends.

class Relationship
include DataMapper::Resource

property :user_id, Integer, :key => true
property :follower_id, Integer, :key => true
belongs_to :user, :child_key => [:user_id]
belongs_to :follower, :class_name => 'User', :child_key =>
[:follower_id]
after :save, :add_activity

def add_activity
Activity.create(:user => user, :activity_type => 'relationship',
:text => "<a href='/user/#{user.nickname}'>#{user.formatted_name}</a>
and <a href='/user/#{follower.nickname}'>#{follower.formatted_name}</
a> are now friends.")
end
end

Finally we get the user’s news feed by taking the user’s activities and going through each of the user’s friends, their activities as well.

def feed
feed = [] + activities
friends.each do |friend|
feed += friend.activities
end
return feed.sort {|x,y| y.created_at <=> x.created_at}
end

Request

We use a simple mechanism for users to invite other users to be their friends. The mechanism goes like this:

  1. Alice identifies another Bob whom she wants to befriend and sends him an invitation
  2. This creates a Request class which is then attached to Bob
  3. When Bob approves the request to be a friend, Alice is added as a friend (which is essentially making Alice follow Bob, since the definition of a friend in Colony is either a follower or follows another user)

    class Request
    include DataMapper::Resource
    property :id, Serial
    property :text, Text
    property :created_at, DateTime

    belongs_to :from, :class_name => User, :child_key => [:from_id]
    belongs_to :user

    def approve
    self.user.add_friend(self.from)
    end
    end

Message

Messages in Colony are private messages that are sent between users of Colony. As a result, messages sent or received are not tracked as activities in the user’s activity feed.

class Message
include DataMapper::Resource
property :id, Serial
property :subject, String
property :text, Text
property :created_at, DateTime
property :read, Boolean, :default => false
property :thread, Integer

belongs_to :sender, :class_name => 'User', :child_key => [:user_id]
belongs_to :recipient, :class_name => 'User', :child_key =>
[:recipient_id]
end

A message must have a sender and a recipient, both of which are users.

has n, :sent_messages, :class_name => 'Message', :child_key => [:user_
id]
has n, :received_messages, :class_name => 'Message', :child_key =>
[:recipient_id]

The read property tells us if the message has been read by the recipient, while the thread property tells us how to group messages together for display.

Album

An activity is logged, each time an album is created.

class Album
include DataMapper::Resource
property :id, Serial
property :name, String, :length => 255
property :description, Text
property :created_at, DateTime

belongs_to :user
has n, :photos
belongs_to :cover_photo, :class_name => 'Photo', :child_key =>
[:cover_photo_id]
after :save, :add_activity

def add_activity
Activity.create(:user => user, :activity_type => 'album', :text =>
"<a href='/user/#{user.nickname}'>#{user.formatted_name}</a> created a
new album <a href='/album/#{self.id}'>#{self.name}</a>")
end
end

Packt

Share
Published by
Packt

Recent Posts

Top life hacks for prepping for your IT certification exam

I remember deciding to pursue my first IT certification, the CompTIA A+. I had signed…

3 years ago

Learn Transformers for Natural Language Processing with Denis Rothman

Key takeaways The transformer architecture has proved to be revolutionary in outperforming the classical RNN…

3 years ago

Learning Essential Linux Commands for Navigating the Shell Effectively

Once we learn how to deploy an Ubuntu server, how to manage users, and how…

3 years ago

Clean Coding in Python with Mariano Anaya

Key-takeaways:   Clean code isn’t just a nice thing to have or a luxury in software projects; it's a necessity. If we…

3 years ago

Exploring Forms in Angular – types, benefits and differences   

While developing a web application, or setting dynamic pages and meta tags we need to deal with…

3 years ago

Gain Practical Expertise with the Latest Edition of Software Architecture with C# 9 and .NET 5

Software architecture is one of the most discussed topics in the software industry today, and…

3 years ago