(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 endend
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_bucketdef 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_walldef create_wall self.wall = Wall.create self.saveend
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).uniqend
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) endend
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
Read more