base/app/models/actor.rb
# An {Actor} represents a social entity. This means {User individuals},
# but also {Group groups}, departments, organizations even nations or states.
#
# Actors are the nodes of a social network. Two actors are linked by {Tie Ties}. The
# type of a {Tie} is a {Relation}. Each actor can define and customize their relations own
# {Relation Relations}.
#
# Every {Actor} has an Avatar, a {Profile} with personal or group information, contact data, etc.
#
# {Actor Actors} perform {ActivityAction actions} (like, suscribe, etc.) on
# {ActivityObject activity objects} ({Post posts}, {Comment commments}, pictures, events..)
#
# = Actor subtypes
# An actor subtype is called a {SocialStream::Models::Subject Subject}.
# {SocialStream::Base} provides two actor subtypes, {User} and {Group}, but the
# application developer can define as many actor subtypes as required.
# Besides including the {SocialStream::Models::Subject} module, Actor subtypes
# must added to +config/initializers/social_stream.rb+
#
#
class Actor < ActiveRecord::Base
# Actor is a supertype of all subjects defined in SocialStream.subjects
supertype_of :subject
include SocialStream::Models::Object
acts_as_avatarable :default_url => "/assets/:attachment/:style/:subtype_class.png"
acts_as_messageable
acts_as_url :name, :url_attribute => :slug
serialize :notification_settings
has_one :profile,
dependent: :destroy,
inverse_of: :actor
has_many :sent_contacts,
:class_name => 'Contact',
:foreign_key => 'sender_id',
:dependent => :destroy
has_many :received_contacts,
:class_name => 'Contact',
:foreign_key => 'receiver_id',
:dependent => :destroy
has_many :sent_ties,
:through => :sent_contacts,
:source => :ties
has_many :received_ties,
:through => :received_contacts,
:source => :ties
has_many :sent_relations,
:through => :sent_ties,
:source => :relation
has_many :received_relations,
:through => :received_ties,
:source => :relation
has_many :sent_permissions,
:through => :sent_relations,
:source => :permissions
has_many :received_permissions,
:through => :received_relations,
:source => :permissions
has_many :senders,
:through => :received_contacts,
:uniq => true
has_many :receivers,
:through => :sent_contacts,
:uniq => true
has_many :relations,
:dependent => :destroy
has_many :sent_actions,
:class_name => "ActivityAction",
:dependent => :destroy
has_many :followings,
:through => :sent_actions,
:source => :activity_object,
:conditions => { 'activity_actions.follow' => true }
has_many :authored_activities,
:class_name => "Activity",
:foreign_key => :author_id,
:dependent => :destroy
has_many :user_authored_activities,
:class_name => "Activity",
:foreign_key => :user_author_id,
:dependent => :destroy
has_many :owned_activities,
:class_name => "Activity",
:foreign_key => :owner_id,
:dependent => :destroy
validates_presence_of :name, :message => ''
validates_presence_of :subject_type
scope :alphabetic, order('actors.name')
scope :letter, lambda { |param|
if param.present?
where('actors.name LIKE ?', "#{ param }%")
end
}
scope :name_search, lambda { |param|
if param.present?
where('actors.name LIKE ?', "%#{ param }%")
end
}
scope :tagged_with, lambda { |param|
if param.present?
joins(:activity_object).merge(ActivityObject.tagged_with(param))
end
}
scope :distinct_initials, select('DISTINCT SUBSTR(actors.name,1,1) as initial').order("initial ASC")
scope :contacted_to, lambda { |a|
joins(:sent_contacts).merge(Contact.active.received_by(a))
}
scope :contacted_from, lambda { |a|
joins(:received_contacts).merge(Contact.active.sent_by(a))
}
scope :not_contacted_from, lambda { |a|
if a.present?
where(arel_table[:id].not_in(a.sent_active_contact_ids + [a.id]))
end
}
scope :followed, joins(:activity_object).merge(ActivityObject.followed)
scope :followed_by, lambda { |a|
select("DISTINCT actors.*").
joins(:received_ties => { :relation => :permissions }).
merge(Contact.sent_by(a)).
merge(Permission.follow)
}
scope :subject_type, lambda { |t|
if t.present?
where(subject_type: t.split(',').map(&:classify))
end
}
scope :recent, -> { order('actors.updated_at DESC') }
after_create :create_initial_relations
after_create :save_or_create_profile
# FIXME SocialStream::ActivityStreams::Supertype should take precedence over
# SocialStream::ActivityStreams::Subtype
def as_object_type
subtype_instance.as_object_type
end
#Returning the email address of the model if an email should be sent for this object (Message or Notification).
#If the actor is a Group and has no email address, an array with the email of the highest rank members will be
#returned isntead.
#
#If no mail has to be sent, return nil.
def mailboxer_email(object)
#If actor has disabled the emails, return nil.
return nil if !notify_by_email
#If actor has enabled the emails and has email
return "#{name} <#{email}>" if email.present?
#If actor is a Group, has enabled emails but no mail we return the highest_rank ones.
if (group = self.subject).is_a? Group
emails = Array.new
group.relation_notifys.each do |relation|
receivers = group.contact_actors(:direction => :sent, :relations => relation)
receivers.each do |receiver|
next unless Actor.normalize(receiver).subject_type.eql?("User")
receiver_emails = receiver.mailboxer_email(object)
case receiver_emails
when String
emails << receiver_emails
when Array
receiver_emails.each do |receiver_email|
emails << receiver_email
end
end
end
end
return emails
end
end
# The subject instance for this actor
def subject
subtype_instance
end
# All the {Relation relations} defined by this {Actor}
def relation_customs
relations.where(:type => 'Relation::Custom')
end
# A given relation defined and managed by this actor
def relation_custom(name)
relation_customs.find_by_name(name)
end
# All {Relation relations} with the 'notify' permission
def relation_notifys
relations.joins(:relation_permissions => :permission).where('permissions.action' => 'notify')
end
# The relations that will appear in privacy forms
#
# They usually include {Relation::Custom} but may also include other system-defined
# relations that are not editable but appear in add contact buttons
def relations_list
Relation.system_list(subject) + relation_customs
end
# The relations offered in the "Add contact" button when subjects
# add new contacts
def relations_for_select
relations_list
end
# Return an object of choices suitable for the contact add button or modal
def options_for_contact_select
relations_for_select.map{ |r| [ r.name, r.id ] }
end
# Options for contact select are simple
def options_for_contact_select_simple?
relations_list.count == 1
end
# Returns the type for contact select, :simple or :multiple
def options_for_contact_select_type
options_for_contact_select_simple? ? :simple : :multiple
end
# All the {Actor Actors} this one has ties with:
#
# actor.contact_actors #=> array of actors that sent and receive ties from actor
#
#
#
# There are several options available to refine the query:
# type:: Filter by the class of the contacts ({User}, {Group}, etc.)
# actor.contact_actors(:type => :user) #=> array of user actors. Exclude groups, etc.
#
# direction:: +:sent+ leaves only the actors this one has ties to. +:received+ gets
# the actors sending ties to this actor, whether this actor added them or not
# actor.contact_actors(:direction => :sent) #=> all the receivers of ties from actor
# relations:: Restrict to ties made up with +relations+. In the case of both directions,
# only relations belonging to {Actor} are considered.
# It defaults to actor's {Relation::Custom custom relations}
# actor.contact_actors(:relations => [2]) #=> actors tied with relation #2
# include_self:: False by default, do not include this actor even they have ties with themselves.
# load_subjects:: True by default, make the queries for {http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Eager+loading+of+associations eager loading} of {SocialStream::Models::Subject Subject}
#
def contact_actors(options = {})
subject_types = Array(options[:type] || self.class.subtypes)
subject_classes = subject_types.map{ |s| s.to_s.classify }
as = Actor.select('actors.*').
# PostgreSQL requires that all the columns must be included in the GROUP BY
group((Actor.columns.map(&:name).map{ |c| "actors.#{ c }" } + [ "contacts.created_at" ]).join(", ")).
where('actors.subject_type' => subject_classes)
if options[:load_subjects].nil? || options[:load_subjects]
as = as.includes(subject_types)
end
# A blank :direction means reciprocate contacts, there must be ties in both directions
#
# This is achieved by getting the id of all the contacts that are sending ties
# Then, we filter the sent contacts query to only those contacts
if options[:direction].blank?
rcv_opts = options.dup
rcv_opts[:direction] = :received
rcv_opts[:load_subjects] = false
# Get the id of actors that are sending to this one
sender_ids = contact_actors(rcv_opts).map(&:id)
# Filter the sent query with these ids
as = as.where(:id => sender_ids)
options[:direction] = :sent
end
case options[:direction]
when :sent
as = as.joins(:received_ties => :relation).merge(Contact.sent_by(self))
when :received
as = as.joins(:sent_ties => :relation).merge(Contact.received_by(self))
else
raise "How do you get here?!"
end
if options[:include_self].blank?
as = as.where("actors.id != ?", self.id)
end
if options[:relations].present?
as = as.merge(Tie.related_by(options[:relations]))
else
as = as.merge(Relation.positive)
end
as
end
# All the {SocialStream::Models::Subject subjects} that send or receive at least one {Tie} to this {Actor}
#
# When passing a block, it will be evaluated for building the actors query, allowing to add
# options before the mapping to subjects
#
# See #contact_actors for options
def contact_subjects(options = {})
as = contact_actors(options)
if block_given?
as = yield(as)
end
as.map(&:subject)
end
# The actors this one has established positive ties with
def positive_sent_contact_actors
sent_contacts.joins(ties: :relation).merge(Relation.positive)
end
# Return a contact to subject.
def contact_to(subject)
sent_contacts.received_by(subject).first
end
# Return a contact to subject. Create it if it does not exist
def contact_to!(subject)
contact_to(subject) ||
sent_contacts.create!(receiver_id: Actor.normalize_id(subject))
end
# The {Contact} of this {Actor} to self (totally close!)
def self_contact
contact_to!(self)
end
alias_method :ego_contact, :self_contact
# The {ActivityObject ActivityObjects} followed by this {Actor}
# that are {Actor Actors}
def following_actor_objects
followings.
where('activity_objects.object_type' => "Actor")
end
# An array with the ids of {Actor Actors} followed by this {Actor}
def following_actor_ids
following_actor_objects.
includes(:actor).
map(&:actor).
map(&:id)
end
# An array with the ids of {Actor Actors} followed by this {Actor}
# plus the id from this {Actor}
def following_actor_and_self_ids
following_actor_ids + [ id ]
end
# Does this {Actor} allow subject to perform action on object?
def allow?(subject, action, object = nil)
ties_to(subject).with_permissions(action, object).any?
end
# Return the {ActivityAction} model to an {ActivityObject}
def action_to(activity_object)
sent_actions.received_by(activity_object).first
end
# Return the {ActivityAction} model to an {ActivityObject}. Create it if it does not exist
def action_to!(activity_object)
action_to(activity_object) ||
sent_actions.create!(:activity_object => ActivityObject.normalize(activity_object))
end
def sent_active_contact_count
sent_contacts.active.count
end
def sent_active_contact_ids
@sent_active_contact_ids ||=
load_sent_active_contact_ids
end
# By now, it returns a suggested {Contact} to another {Actor} without any current {Tie}
#
# Suggested actor types can be configured in SocialStream.suggestion_models in
# config/initializers/social_stream.rb
#
# @return [Contact]
def suggestions(size = 1)
candidates =
Actor.
where(subject_type: SocialStream.suggested_models.map{ |m| m.to_s.classify }).
where(Actor.arel_table[:id].not_in(sent_active_contact_ids + [id]))
size.times.map {
candidates.delete_at rand(candidates.size)
}.compact.map { |a|
contact_to! a
}
end
# Set of ties sent by this actor received by subject
def ties_to(subject)
sent_ties.merge(Contact.received_by(subject))
end
# Is there any {Tie} sent by this actor and received by subject
def ties_to?(subject)
ties_to(subject).present?
end
# The {Tie ties} sent by this actor, plus the second grade ties
def egocentric_ties
@egocentric_ties ||=
load_egocentric_ties
end
# Can this actor be represented by subject. Does she has permissions for it?
def represented_by?(subject)
return false if subject.blank?
self.class.normalize(subject) == self ||
sent_ties.
merge(Contact.received_by(subject)).
joins(:relation => :permissions).
merge(Permission.represent).
any?
end
# The default {Relation Relations} for sharing an {Activity} owned
# by this {Actor}
#
# Activities are shared with all the contacts by default.
#
# You can change this behaviour with a decorator, overwriting this method.
# For example, if you want the activities shared publicly by default, create
# a decorator in app/decorators/actor_decorator.rb with
# Actor.class_eval do
# def activity_relations
# [ Relation::Public.instance ]
# end
# end
#
def activity_relations
relations.
allowing('read', 'activity')
end
# The ids of the default {Relation Relations} for sharing an {Activity}
# owned by this {Actor}
def activity_relation_ids
activity_relations.map(&:id)
end
# This method returns all the {relations Relation} that subject can choose to broadcast an Activity in this {Actor}'s wall
#
# See {Activity} on how they can be shared with multiple {audicences Audience}, which corresponds to a {Relation}.
#
def activity_relations_for(subject, options = {})
if Actor.normalize(subject) == self
return relation_customs + Array.wrap(Relation::Public.instance)
else
Array.new
end
end
# Are {#activity_relations} available for subject?
def activity_relations_for?(subject, options = {})
activity_relations(subject, options).any?
end
# Is this {Actor} allowed to create a comment on activity?
#
# We are allowing comments from everyone signed in by now
def can_comment?(activity)
return true
comment_relations(activity).any?
end
# Are there any relations that allow this actor to create a comment on activity?
def comment_relations(activity)
activity.relations.select{ |r| r.is_a?(Relation::Public) } |
Relation.allow(self, 'create', 'activity', :in => activity.relations)
end
def pending_contacts_count
received_contacts.not_reflexive.pending.count
end
def pending_contacts?
pending_contacts_count > 0
end
# Build a new {Contact} from each that has not inverse
def pending_contacts
received_contacts.pending.includes(:inverse).all.map do |c|
c.inverse ||
c.receiver.contact_to!(c.sender)
end
end
# Count the contacts in common between this {Actor} and subject
def common_contacts_count(subject)
(sent_active_contact_ids & subject.sent_active_contact_ids).size
end
# Use slug as parameter
def to_param
slug
end
def as_json(options)
{
id: id,
slug: slug,
name: name,
url: options[:helper].polymorphic_url(subject),
image: {
url: options[:helper].root_url + logo.url(:small)
}
}
end
def notification_settings
self.update_attribute(:notification_settings, SocialStream.default_notification_settings) unless super
super
end
private
# After create callback
def create_initial_relations
Relation::Custom.defaults_for(self)
end
# After create callback
#
# Save the profile if it is present. Otherwise create it
def save_or_create_profile
if profile.present?
profile.actor_id = id
profile.save!
else
create_profile
end
end
# Calculate {#egocentric_ties}
def load_egocentric_ties
ties = sent_ties.includes(:contact).to_a
contact_ids = ties.map{ |t| t.contact.receiver_id }
second_grade_ties =
contact_ids.
map{ |i| Tie.sent_by(i) }.
flatten
ties + second_grade_ties
end
# Calculate {#sent_active_contact_ids}
def load_sent_active_contact_ids
sent_contacts.active.map(&:receiver_id)
end
def unread_messages_count
mailbox.inbox(:unread => true).count(:id, :distinct => true)
end
end