Activity Mapper
A framework for aggregating (public) social activity into a single polymorphic persistent structure. Using a unified map, it uses service modules to map XML/JSON data onto a unified object space.
Included service modules:
- Delicious
- Flickr
- Youtube
- Wakoopa
Step 1: Implement your Models
# For this example, we're using OpenStruct as a storage system.
# Normally one would use DataMapper or ActiveRecord
require 'ostruct'
class PersistentObject < OpenStruct
attr_accessor :id
def self.create(*arg)
self.new(*arg)
end
def update_attributes(attrs)
attrs.each do |name, value|
self.send("#{name}=", value)
end
end
end
# -- All objects below need to be defined in order for activity_mapper to function
# Your base user object
class User < PersistentObject; end
# Profile information: username, native_id, url
class ServiceProfile < PersistentObject
# belongs_to :user, implement me!
# has_many :activities, implement me!
def initialize(*arg); super(*arg); @activities = []; end
attr_accessor :activities
# -- All below is code necessary for routing to the proper service module (based on URL)
def create_or_update_summary!(*arg); service_module ? service_module.create_or_update_summary!(*arg) : nil; end
def aggregate_activity!(*arg); service_module ? service_module.aggregate_activity!(*arg) : nil; end
def analyze_this(*arg); service_module ? service_module.analyze_this(*arg) : nil; end
def service_module
return @service_module if @service_module
if (service_module_klass = ActivityMapper::ServiceModule.klass_for(url))
@service_module = service_module_klass.new(self)
else
nil
end
end
end
# The actual event that happened: caption, occurred_at, url, reference to ActivityObject
class Activity < PersistentObject
# has_one :object, implement me!
# Implement anti-duplication mechanisms here
def self.exists?(user_id, entry)
false
end
end
# The object that's referenced by the event: title, body, url, created_at
class ActivityObject < PersistentObject
def self.fetch(content_identifier, activity_object_type_id)
nil
end
def self.content_identifier(url)
MD5.hexdigest(url)
end
# To support space separated tags from APIs
def spaced_tags=(value)
self.tag_list = value.to_s.split(' ')
end
end
# Optional object that holds ranking/stats information
class RatingSummary < PersistentObject; end
# Hold media information
class Media < PersistentObject; end
# See activitystrea.ms for more verbs
class ActivityVerb < PersistentObject
# Constant Cache
POST = ActivityVerb.new(:id => 1)
FAVORITE = ActivityVerb.new(:id => 2)
RECENTLY_USED = ActivityVerb.new(:id => 3)
NEWLY_USED = ActivityVerb.new(:id => 4)
end
# See activitystrea.ms for more types
class ActivityObjectType < PersistentObject
# Constant Cache
STATUS = ActivityObjectType.new(:id => 1)
BOOKMARK = ActivityObjectType.new(:id => 2)
PHOTO = ActivityObjectType.new(:id => 3)
VIDEO = ActivityObjectType.new(:id => 4)
SONG = ActivityObjectType.new(:id => 5)
MIXED = ActivityObjectType.new(:id => 6)
SOFTWARE = ActivityObjectType.new(:id => 7)
SLIDESHOW = ActivityObjectType.new(:id => 8)
end
Step 2: Map that activity!
require 'rubygems'
require 'activity_mapper'
profile = ServiceProfile.create(:url => 'http://twitter.com/dominiek')
# Gather the most basic credentials:
profile.create_or_update_summary!
# => profile.username
# => profile.native_id
# => profile.url
profile.aggregate_activity!
activity = profile.activities.first
# => activity.caption
# => activity.occured_at
# => activity.native_id
# => activity.object.title
# => activity.object.body
# => activity.object.native_id
Example Service Module
module ActivityMapper
class TwitterServiceModule < ServiceModule
ACTIVITY_MAP = {
nil => {
'activity.occurred_at' => 'created_at',
'activity.native_id' => 'id',
'activity_object.native_id' => 'id',
'activity.caption' => 'text',
'activity_object.title' => 'text',
'activity_object.body' => 'text'
}
}
ACCEPTED_HOSTS = [/twitter\.com/]
def create_or_update_summary!(options = {})
attributes = {}
attributes[:username] = @profile.username || self.class.username_from_url(@profile.url)
mapper = ActivityDataMapper.new(ACTIVITY_MAP)
mapper.fetch!("http://twitter.com/statuses/user_timeline/#{attributes[:username]}.json", :format => :json)
tweets = mapper.data
attributes[:avatar_url] = tweets.blank? ? nil : tweets.first['user']['profile_image_url']
attributes[:native_user_id] = tweets.first['user']['id'].to_i
@profile.update_attributes(attributes)
end
def aggregate_activity!(options = {})
mapper = ActivityDataMapper.new(ACTIVITY_MAP)
mapper.fetch!("http://twitter.com/statuses/user_timeline/#{@profile.username}.json", :format => :json)
mapper.map!
mapper.entries.sort! { |e2,e1|
e1['activity.occurred_at'] <=> e2['activity.occurred_at']
}
mapper.entries.each do |entry|
entry['activity_object.url'] = entry['activity.url'] = "http://twitter.com/#{@profile.username}/status/#{entry['activity.native_id']}"
break if Activity.exists?(@profile.user_id, entry)
create_activity(entry, ActivityObjectType::STATUS, ActivityVerb::POST) do |activity, activity_object|
end
end
end
end
end
Author
Dominiek ter Heide
http://dominiek.com/
(Note: I wrote this a while back and thought this could be useful to some developers)