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:

  • Twitter
  • 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)