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

LEAVE A REPLY

Please enter your comment!
Please enter your name here