ging/social_stream

View on GitHub
base/app/models/activity.rb

Summary

Maintainability
C
1 day
Test Coverage
# Activities follow the {Activity Streams}[http://activitystrea.ms/] standard.
#
# Every {Activity} has an {#author}, {#user_author} and {#owner}
#
# author:: Is the {SocialStream::Models::Subject subject} that originated
#          the activity. The entity that posted something, liked, etc..
#
# user_author:: The {User} logged in when the {Activity} was created.
#               If the {User} has not changed the session to represent
#               other entity (a {Group} for example), the user_author
#               will be the same as the author.
#
# owner:: The {SocialStream::Models::Subject subject} whose wall was posted
#         or comment was liked, etc..
#
# == {Audience Audiences} and visibility
# Each activity is attached to one or more {Relation relations}, which define
# the {SocialStream::Models::Subject subject} that can reach the activity
#
# In the case of a {Relation::Public public relation} everyone will be
# able to see the activity.
#
# In the case of {Relation::Custom custom relations}, only the subjects
# that have a {Tie} with that relation (in other words, the contacts that
# have been added as friends with that relation} will be able to reach the {Activity}
#
class Activity < ActiveRecord::Base
  # FIXME: this does not follow the Rails way
  include NotificationsHelper

  # This has to be declared before 'has_ancestry' to work around rails issue #670
  # See: https://github.com/rails/rails/issues/670
  before_destroy :destroy_children_comments
  before_destroy :decrement_like_count, :delete_notifications

  has_ancestry

  paginates_per 10

  belongs_to :activity_verb

  belongs_to :author,
             :class_name => "Actor"
  belongs_to :owner,
             :class_name => "Actor"
  belongs_to :user_author,
             :class_name => "Actor"

  has_many :audiences, :dependent => :destroy
  has_many :relations, :through => :audiences

  has_many :activity_object_activities,
           :dependent => :destroy
  has_many :activity_objects,
           :through => :activity_object_activities

  scope :authored_by, lambda { |subject|
    where(:author_id => Actor.normalize_id(subject))
  }
  scope :owned_by, lambda { |subject|
    where(:owner_id => Actor.normalize_id(subject))
  }
  scope :authored_or_owned_by, lambda { |subjects|
    if subjects.present?
      ids = Actor.normalize_id(subjects)

      where(arel_table[:author_id].in(ids).or(arel_table[:owner_id].in(ids)))
    end
  }

  scope :shared_with, lambda { |subject|
    joins(:audiences).
      merge(Audience.where(:relation_id => Relation.ids_shared_with(subject)))
  }

  scope :timeline, lambda { |senders = nil, receivers = nil|
    if senders == :home
      senders = receivers.following_actor_and_self_ids
    end

    select("DISTINCT activities.*").
      roots.
      includes(:author, :user_author, :owner, :activity_objects, :activity_verb, :relations).
      authored_or_owned_by(senders).
      shared_with(receivers).
      order("created_at desc")
  }


  after_create  :increment_like_count

  validates_presence_of :author_id, :user_author_id, :owner_id, :relations
 
  #For now, it should be the last one
  #FIXME
  after_create :send_notifications

  # The name of the verb of this activity
  def verb
    activity_verb.name
  end

  # Set the name of the verb of this activity
  def verb=(name)
    self.activity_verb = ActivityVerb[name]
  end

  # The {SocialStream::Models::Subject subject} author
  def author_subject
    author.subject
  end

  # The {SocialStream::Models::Subject subject} owner
  def owner_subject
    owner.subject
  end

  # The {SocialStream::Models::Subject subject} user actor
  def user_author_subject
    user_author.subject
  end

  # Does this {Activity} have the same sender and receiver?
  def reflexive?
    author_id == owner_id
  end

  # Is the author represented in this {Activity}?
  def represented_author?
    author_id != user_author_id
  end

  # The {Actor} author of this activity
  #
  # This method provides the {Actor}. Use {#sender_subject} for the {SocialStream::Models::Subject Subject}
  # ({User}, {Group}, etc..)
  def sender
    author
  end

  # The {SocialStream::Models::Subject Subject} author of this activity
  #
  # This method provides the {SocialStream::Models::Subject Subject} ({User}, {Group}, etc...).
  # Use {#sender} for the {Actor}.
  def sender_subject
    author_subject
  end

  # The wall where the activity is shown belongs to receiver
  #
  # This method provides the {Actor}. Use {#receiver_subject} for the {SocialStream::Models::Subject Subject}
  # ({User}, {Group}, etc..)
  def receiver
    owner
  end

  # The wall where the activity is shown belongs to the receiver
  #
  # This method provides the {SocialStream::Models::Subject Subject} ({User}, {Group}, etc...).
  # Use {#receiver} for the {Actor}.
  def receiver_subject
    owner_subject
  end

  # The comments about this activity
  def comments
    children.includes(:activity_objects).where('activity_objects.object_type' => "Comment")
  end

  # The 'like' qualifications emmited to this activities
  def likes
    children.joins(:activity_verb).where('activity_verbs.name' => "like")
  end

  def liked_by(user) #:nodoc:
    likes.authored_by(user)
  end

  # Does user like this activity?
  def liked_by?(user)
    liked_by(user).present?
  end

  # Build a new children activity where subject like this
  def new_like(subject, user)
    a = children.new :verb           => "like",
                     :author_id      => Actor.normalize_id(subject),
                     :user_author_id => Actor.normalize_id(user),
                     :owner_id       => owner_id,
                     :relation_ids   => self.relation_ids

    if direct_activity_object.present?
      a.activity_objects << direct_activity_object
    end

    a
  end

  # The first activity object of this activity
  def direct_activity_object
    @direct_activity_object ||=
      activity_objects.first
  end

  # The first object of this activity
  def direct_object
    @direct_object ||=
      direct_activity_object.try(:object)
  end

  # The title for this activity in the stream
  def title view
    case verb
    when "follow", "make-friend", "like"
      I18n.t "activity.verb.#{ verb }.#{ receiver.subject_type }.title",
      :subject => view.link_name(sender_subject),
      :contact => view.link_name(receiver_subject)
    when "post", "update"
      if sender == receiver
        view.link_name sender_subject
      else
        I18n.t "activity.verb.post.title.other_wall",
               :sender => view.link_name(sender_subject),
               :receiver => view.link_name(receiver_subject)
      end
    when 'join'
      I18n.t('notification.join.one', 
            :sender => view.link_name(sender_subject),
            :thing => I18n.t(direct_object.class.to_s.underscore+'.one'),
            :title => title_of(direct_object))
    else
      "Must define activity title"
    end.html_safe
  end

  # Title for activity streams
  def stream_title
    # FIXMEEEEEEEEEEEEEEEEE
    object = ( direct_object.present? ? 
               ( direct_object.is_a?(SocialStream::Models::Subject) ? 
                 direct_object.name :
                 direct_object.title ) :
               receiver.name )

    I18n.t "activity.stream.title.#{ verb }",
           :author => sender_subject.name,
           :activity_object => object
  end

  # TODO: detailed description of activity
  def stream_content
    stream_title
  end
  
  def notificable?
    is_root? or ['post','update'].include?(root.verb)
  end

  def notify
    return true unless notificable?
    #Avaible verbs: follow, like, make-friend, post, update, join

    case
      when direct_object.is_a?(Comment)
        participants.each do |p|
          send_mail = p.subject.notification_settings[:someone_comments_on_my_post]
          p.notify(notification_subject, "Youre not supposed to see this", self, true, nil, send_mail) unless p == sender
        end
      when reflexive?
        return true
      when ['like'].include?(verb)
        send_mail = receiver.subject.notification_settings[:someone_likes_my_post]
        receiver.notify(notification_subject, "Youre not supposed to see this", self, true, nil, send_mail)
      when ['follow'].include?(verb)
        send_mail = receiver.subject.notification_settings[:someone_adds_me_as_a_contact]
        receiver.notify(notification_subject, "Youre not supposed to see this", self, true, nil, send_mail)
      when ['make-friend'].include?(verb)
        send_mail = receiver.subject.notification_settings[:someone_confirms_my_contact_request]
        receiver.notify(notification_subject, "Youre not supposed to see this", self, true, nil, send_mail)
      when ['post', 'update', 'join'].include?(verb)
        receiver.notify(notification_subject, "Youre not supposed to see this", self)
    end
    true
  end

  # A list of participants
  def participants
    parts=Set.new
    same_thread.map{|a| a.activity_objects.first}.each do |ao|
      parts << ao.author if ao.respond_to? :author and !ao.author.nil?
    end
    parts
  end

  # This and related activities
  def same_thread
    return [self] if is_root?
    [parent] + siblings
  end

  # Is this activity public?
  def public?
    relation_ids.include? Relation::Public.instance.id
  end

  # The {Actor Actors} this activity is shared with
  def audience
    raise "Cannot get the audience of a public activity!" if public?

    [ author, user_author, owner ].uniq |
      Actor.
        joins(:received_ties).
        merge(Tie.where(:relation_id => relation_ids))
  end

  # The {Relation} with which activity is shared
  def audience_in_words(subject, options = {})
    options[:details] ||= :full

    public_relation = relations.select{ |r| r.is_a?(Relation::Public) }

    visibility, audience =
      if public_relation.present?
        [ :public, nil ]
      else
        visible_relations =
          relations.select{ |r| r.actor_id == Actor.normalize_id(subject)}

        if visible_relations.present?
          [ :visible, visible_relations.map(&:name).uniq.join(", ") ]
        else
          [ :hidden, relations.map(&:actor).map(&:name).uniq.join(", ") ]
        end
      end

    I18n.t "activity.audience.#{ visibility }.#{ options[:details] }", :audience => audience
  end

  private

  #
  # Get the email subject for the activity's notification
  #
  def notification_subject
    sender_name= sender.name.truncate(30, :separator => ' ')
    receiver_name= receiver.name.truncate(30, :separator => ' ')
    case verb 
      when 'like'
        if direct_object.acts_as_actor?
          I18n.t('notification.fan', 
                :sender => sender_name,
                :whose => I18n.t('notification.whose.'+ receiver.subject.class.to_s.underscore,
                            :receiver => receiver_name))
        else
          I18n.t('notification.like.'+ receiver.subject.class.to_s.underscore, 
                :sender => sender_name,
                :whose => I18n.t('notification.whose.'+ receiver.subject.class.to_s.underscore,
                            :receiver => receiver_name),
                :thing => I18n.t(direct_object.class.to_s.underscore+'.name'))
        end
      when 'follow'
        I18n.t('notification.follow.'+ receiver.subject.class.to_s.underscore, 
              :sender => sender_name,
              :who => I18n.t('notification.who.'+ receiver.subject.class.to_s.underscore,
                             :name => receiver_name))
      when 'make-friend'
        I18n.t('notification.makefriend.'+ receiver.subject.class.to_s.underscore, 
              :sender => sender_name,
              :who => I18n.t('notification.who.'+ receiver.subject.class.to_s.underscore,
                              :name => receiver_name))
      when 'post'
        I18n.t('notification.post.'+ receiver.subject.class.to_s.underscore, 
            :sender => sender_name,
            :whose => I18n.t('notification.whose.'+ receiver.subject.class.to_s.underscore,
                              :receiver => receiver_name),
        :title => title_of(direct_object))
      when 'update'
        I18n.t('notification.update.'+ receiver.subject.class.to_s.underscore, 
              :sender => sender_name,
              :whose => I18n.t('notification.whose.'+ receiver.subject.class.to_s.underscore,
                               :receiver => receiver_name),
              :thing => I18n.t(direct_object.class.to_s.underscore+'.one'))
      when 'join'
        I18n.t('notification.join.one'  , 
            :sender => sender_name,
            :thing => I18n.t(direct_object.class.to_s.underscore+'.title.one'),
            :title => title_of(direct_object))
      
      else
        t('notification.default')
      end
  end
  
  #Send notifications to actors based on proximity, interest and permissions
  def send_notifications
    notify
  end

  # after_create callback
  #
  # Increment like counter in objects with a like activity
  def increment_like_count
    return if verb != "like" || direct_activity_object.blank?

    direct_activity_object.increment!(:like_count)
  end

  # before_destroy callback
  #
  # Destroy children comments when the activity is destroyed
  def destroy_children_comments
    comments.each do |c|
      c.direct_object.destroy
    end
  end

  # after_destroy callback
  #
  # Decrement like counter in objects when like activity is destroyed
  def decrement_like_count
    return if verb != "like" || direct_activity_object.blank?

    direct_activity_object.decrement!(:like_count)
  end
  
  # after_destroy callback
  #
  # Destroy any Notification linked with the activity
  def delete_notifications
    Notification.with_object(self).each do |notification|
      notification.destroy
    end
  end
end