(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:
- Alice identifies another Bob whom she wants to befriend and sends him an invitation
- This creates a Request class which is then attached to Bob
- 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