diff --git a/app/models/account_deleter.rb b/app/models/account_deleter.rb index f57b01775..f6d8bf45d 100644 --- a/app/models/account_deleter.rb +++ b/app/models/account_deleter.rb @@ -103,7 +103,7 @@ class AccountDeleter end def normal_ar_person_associates_to_delete - [:posts, :photos, :mentions] + [:posts, :photos, :mentions, :participations] end def ignored_or_special_ar_person_associations diff --git a/app/models/comment.rb b/app/models/comment.rb index b4e42f929..a19b09cb9 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -84,4 +84,19 @@ class Comment < ActiveRecord::Base def parent= parent self.post = parent end + + class Generator < Federated::Generator + def self.federated_class + Comment + end + + def initialize(person, target, text) + @text = text + super(person, target) + end + + def relayable_options + {:post => @target, :text => @text} + end + end end diff --git a/app/models/like.rb b/app/models/like.rb index 5466bddd1..af85c8d70 100644 --- a/app/models/like.rb +++ b/app/models/like.rb @@ -2,14 +2,26 @@ # licensed under the Affero General Public License version 3 or later. See # the COPYRIGHT file. -class Like < ActiveRecord::Base - include ROXML +class Like < Federated::Relayable + class Generator < Federated::Generator + def self.federated_class + Like + end - include Diaspora::Webhooks - include Diaspora::Guid + def relayable_options + {:target => @target, :positive => true} + end + end - xml_attr :target_type - include Diaspora::Relayable + after_create do + self.parent.update_likes_counter + end + + after_destroy do + self.parent.update_likes_counter + end + + xml_attr :positive # NOTE API V1 to be extracted acts_as_api @@ -20,43 +32,6 @@ class Like < ActiveRecord::Base t.add :created_at end - xml_attr :positive - xml_attr :diaspora_handle - - belongs_to :target, :polymorphic => true - belongs_to :author, :class_name => 'Person' - - validates_uniqueness_of :target_id, :scope => [:target_type, :author_id] - validates :parent, :presence => true #should be in relayable (pending on fixing Message) - - after_create do - self.parent.update_likes_counter - end - - after_destroy do - self.parent.update_likes_counter - end - - def diaspora_handle - self.author.diaspora_handle - end - - def diaspora_handle= nh - self.author = Webfinger.new(nh).fetch - end - - def parent_class - self.target_type.constantize - end - - def parent - self.target - end - - def parent= parent - self.target = parent - end - def notification_type(user, person) #TODO(dan) need to have a notification for likes on comments, until then, return nil return nil if self.target_type == "Comment" diff --git a/app/models/participation.rb b/app/models/participation.rb new file mode 100644 index 000000000..229d1afc9 --- /dev/null +++ b/app/models/participation.rb @@ -0,0 +1,11 @@ +class Participation < Federated::Relayable + class Generator < Federated::Generator + def self.federated_class + Participation + end + + def relayable_options + {:target => @target} + end + end +end \ No newline at end of file diff --git a/app/models/person.rb b/app/models/person.rb index a056decc2..48b768f08 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -44,6 +44,7 @@ class Person < ActiveRecord::Base has_many :posts, :foreign_key => :author_id, :dependent => :destroy # This person's own posts has_many :photos, :foreign_key => :author_id, :dependent => :destroy # This person's own photos has_many :comments, :foreign_key => :author_id, :dependent => :destroy # This person's own comments + has_many :participations, :foreign_key => :author_id, :dependent => :destroy belongs_to :owner, :class_name => 'User' diff --git a/app/models/post.rb b/app/models/post.rb index 2ed1617b5..769e3522d 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -9,6 +9,8 @@ class Post < ActiveRecord::Base include Diaspora::Commentable include Diaspora::Shareable + has_many :participations, :dependent => :delete_all, :as => :target + attr_accessor :user_like # NOTE API V1 to be extracted diff --git a/app/models/user.rb b/app/models/user.rb index 3fa47a371..7c5ce1c59 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,15 +2,17 @@ # licensed under the Affero General Public License version 3 or later. See # the COPYRIGHT file. -require File.join(Rails.root, 'lib/diaspora/user') require File.join(Rails.root, 'lib/salmon/salmon') require File.join(Rails.root, 'lib/postzord/dispatcher') require 'rest-client' class User < ActiveRecord::Base - include Diaspora::UserModules include Encryptor::Private + include Connecting + include Querying + include SocialActions + devise :invitable, :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :timeoutable, :token_authenticatable, :lockable, @@ -33,7 +35,7 @@ class User < ActiveRecord::Base serialize :hidden_shareables, Hash has_one :person, :foreign_key => :owner_id - delegate :public_key, :posts, :photos, :owns?, :diaspora_handle, :name, :public_url, :profile, :first_name, :last_name, :to => :person + delegate :public_key, :posts, :photos, :owns?, :diaspora_handle, :name, :public_url, :profile, :first_name, :last_name, :participations, :to => :person has_many :invitations_from_me, :class_name => 'Invitation', :foreign_key => :sender_id has_many :invitations_to_me, :class_name => 'Invitation', :foreign_key => :recipient_id @@ -284,43 +286,6 @@ class User < ActiveRecord::Base Salmon::EncryptedSlap.create_by_user_and_activity(self, post.to_diaspora_xml) end - def comment!(post, text, opts={}) - comment = build_comment(opts.merge!(:post => post, :text => text)) - if comment.save - dispatch_post(comment) - comment - else - false - end - end - - def like!(target, opts={}) - like = build_like(opts.merge!(:target => target, :positive => true)) - if like.save - dispatch_post(like) - like - else - false - end - end - - def build_relayable(model, options = {}) - r = model.new(options.merge(:author_id => self.person.id)) - r.set_guid - r.initialize_signatures - r - end - - ######## Commenting ######## - def build_comment(options = {}) - build_relayable(Comment, options) - end - - ######## Liking ######## - def build_like(options = {}) - build_relayable(Like, options) - end - # Check whether the user has liked a post. # @param [Post] post def liked?(target) diff --git a/app/models/user/connecting.rb b/app/models/user/connecting.rb new file mode 100644 index 000000000..5fa7588d1 --- /dev/null +++ b/app/models/user/connecting.rb @@ -0,0 +1,70 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +module User::Connecting + # This will create a contact on the side of the sharer and the sharee. + # @param [Person] person The person to start sharing with. + # @param [Aspect] aspect The aspect to add them to. + # @return [Contact] The newly made contact for the passed in person. + def share_with(person, aspect) + contact = self.contacts.find_or_initialize_by_person_id(person.id) + return false unless contact.valid? + + unless contact.receiving? + contact.dispatch_request + contact.receiving = true + end + + contact.aspects << aspect + contact.save + + if notification = Notification.where(:target_id => person.id).first + notification.update_attributes(:unread=>false) + end + + register_share_visibilities(contact) + contact + end + + # This puts the last 100 public posts by the passed in contact into the user's stream. + # @param [Contact] contact + # @return [void] + def register_share_visibilities(contact) + #should have select here, but proven hard to test + posts = Post.where(:author_id => contact.person_id, :public => true).limit(100) + p = posts.map do |post| + ShareVisibility.new(:contact_id => contact.id, :shareable_id => post.id, :shareable_type => 'Post') + end + ShareVisibility.import(p) unless posts.empty? + nil + end + + def remove_contact(contact, opts={:force => false}) + posts = contact.posts.all + + if !contact.mutual? || opts[:force] + contact.destroy + else + contact.update_attributes(:receiving => false) + end + end + + def disconnect(bad_contact, opts={}) + person = bad_contact.person + Rails.logger.info("event=disconnect user=#{diaspora_handle} target=#{person.diaspora_handle}") + retraction = Retraction.for(self) + retraction.subscribers = [person]#HAX + Postzord::Dispatcher.build(self, retraction).post + + AspectMembership.where(:contact_id => bad_contact.id).delete_all + remove_contact(bad_contact, opts) + end + + def disconnected_by(person) + Rails.logger.info("event=disconnected_by user=#{diaspora_handle} target=#{person.diaspora_handle}") + if contact = self.contact_for(person) + remove_contact(contact) + end + end +end diff --git a/app/models/user/querying.rb b/app/models/user/querying.rb new file mode 100644 index 000000000..d4c7da6c3 --- /dev/null +++ b/app/models/user/querying.rb @@ -0,0 +1,164 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +require File.join(Rails.root, 'lib', 'evil_query') + + +#TODO: THIS FILE SHOULD NOT EXIST, EVIL SQL SHOULD BE ENCAPSULATED IN EvilQueries, +#throwing all of this stuff in user violates demeter like WHOA +module User::Querying + def find_visible_shareable_by_id(klass, id, opts={} ) + key = (opts.delete(:key) || :id) + ::EvilQuery::VisibleShareableById.new(self, klass, key, id, opts).post! + end + + def visible_shareables(klass, opts={}) + opts = prep_opts(klass, opts) + shareable_ids = visible_shareable_ids(klass, opts) + klass.where(:id => shareable_ids).select('DISTINCT '+klass.to_s.tableize+'.*').limit(opts[:limit]).order(opts[:order_with_table]) + end + + def visible_shareable_ids(klass, opts={}) + opts = prep_opts(klass, opts) + visible_ids_from_sql(klass, opts) + end + + # @return [Array] + def visible_ids_from_sql(klass, opts={}) + opts = prep_opts(klass, opts) + opts[:klass] = klass + opts[:by_members_of] ||= self.aspect_ids + + post_ids = klass.connection.select_values(visible_shareable_sql(klass, opts)).map { |id| id.to_i } + post_ids += klass.connection.select_values(construct_public_followings_sql(opts).to_sql).map {|id| id.to_i } + end + + def visible_shareable_sql(klass, opts={}) + table = klass.table_name + opts = prep_opts(klass, opts) + opts[:klass] = klass + + shareable_from_others = construct_shareable_from_others_query(opts) + shareable_from_self = construct_shareable_from_self_query(opts) + + "(#{shareable_from_others.to_sql} LIMIT #{opts[:limit]}) UNION ALL (#{shareable_from_self.to_sql} LIMIT #{opts[:limit]}) ORDER BY #{opts[:order]} LIMIT #{opts[:limit]}" + end + + def ugly_select_clause(query, opts) + klass = opts[:klass] + select_clause ='DISTINCT %s.id, %s.updated_at AS updated_at, %s.created_at AS created_at' % [klass.table_name, klass.table_name, klass.table_name] + query.select(select_clause).order(opts[:order_with_table]).where(klass.arel_table[opts[:order_field]].lt(opts[:max_time])) + end + + def construct_shareable_from_others_query(opts) + conditions = { + :pending => false, + :share_visibilities => {:hidden => opts[:hidden]}, + :contacts => {:user_id => self.id, :receiving => true} + } + + conditions[:type] = opts[:type] if opts.has_key?(:type) + + query = opts[:klass].joins(:contacts).where(conditions) + + if opts[:by_members_of] + query = query.joins(:contacts => :aspect_memberships).where( + :aspect_memberships => {:aspect_id => opts[:by_members_of]}) + end + + ugly_select_clause(query, opts) + end + + def construct_public_followings_sql(opts) + aspects = Aspect.where(:id => opts[:by_members_of]) + person_ids = Person.connection.select_values(people_in_aspects(aspects).select("people.id").to_sql) + + query = opts[:klass].where(:author_id => person_ids, :public => true, :pending => false) + + unless(opts[:klass] == Photo) + query = query.where(:type => opts[:type]) + end + + ugly_select_clause(query, opts) + end + + def construct_shareable_from_self_query(opts) + conditions = {:pending => false } + conditions[:type] = opts[:type] if opts.has_key?(:type) + query = self.person.send(opts[:klass].to_s.tableize).where(conditions) + + if opts[:by_members_of] + query = query.joins(:aspect_visibilities).where(:aspect_visibilities => {:aspect_id => opts[:by_members_of]}) + end + + ugly_select_clause(query, opts) + end + + def contact_for(person) + return nil unless person + contact_for_person_id(person.id) + end + + def aspects_with_shareable(base_class_name_or_class, shareable_id) + base_class_name = base_class_name_or_class + base_class_name = base_class_name_or_class.base_class.to_s if base_class_name_or_class.is_a?(Class) + self.aspects.joins(:aspect_visibilities).where(:aspect_visibilities => {:shareable_id => shareable_id, :shareable_type => base_class_name}) + end + + def contact_for_person_id(person_id) + Contact.where(:user_id => self.id, :person_id => person_id).includes(:person => :profile).first + end + + # @param [Person] person + # @return [Boolean] whether person is a contact of this user + def has_contact_for?(person) + Contact.exists?(:user_id => self.id, :person_id => person.id) + end + + def people_in_aspects(requested_aspects, opts={}) + allowed_aspects = self.aspects & requested_aspects + aspect_ids = allowed_aspects.map(&:id) + + people = Person.in_aspects(aspect_ids) + + if opts[:type] == 'remote' + people = people.where(:owner_id => nil) + elsif opts[:type] == 'local' + people = people.where('people.owner_id IS NOT NULL') + end + people + end + + def aspects_with_person person + contact_for(person).aspects + end + + def posts_from(person) + ::EvilQuery::ShareablesFromPerson.new(self, Post, person).make_relation! + end + + def photos_from(person) + ::EvilQuery::ShareablesFromPerson.new(self, Photo, person).make_relation! + end + + protected + + # @return [Hash] + def prep_opts(klass, opts) + defaults = { + :order => 'created_at DESC', + :limit => 15, + :hidden => false + } + defaults[:type] = Stream::Base::TYPES_OF_POST_IN_STREAM if klass == Post + opts = defaults.merge(opts) + + opts[:order_field] = opts[:order].split.first.to_sym + opts[:order_with_table] = klass.table_name + '.' + opts[:order] + + opts[:max_time] = Time.at(opts[:max_time]) if opts[:max_time].is_a?(Integer) + opts[:max_time] ||= Time.now + 1 + opts + end +end diff --git a/app/models/user/social_actions.rb b/app/models/user/social_actions.rb new file mode 100644 index 000000000..3244b9bad --- /dev/null +++ b/app/models/user/social_actions.rb @@ -0,0 +1,19 @@ +module User::SocialActions + def comment!(target, text, opts={}) + participations.where(:target_id => target).first || participate!(target) + Comment::Generator.new(self, target, text).create!(opts) + end + + def participate!(target, opts={}) + Participation::Generator.new(self, target).create!(opts) + end + + def like!(target, opts={}) + participations.where(:target_id => target).first || participate!(target) + Like::Generator.new(self, target).create!(opts) + end + + def build_comment(options={}) + Comment::Generator.new(self, options.delete(:post), options.delete(:text)).build(options) + end +end \ No newline at end of file diff --git a/db/migrate/20120208231253_create_participations.rb b/db/migrate/20120208231253_create_participations.rb new file mode 100644 index 000000000..62f840bc6 --- /dev/null +++ b/db/migrate/20120208231253_create_participations.rb @@ -0,0 +1,17 @@ +class CreateParticipations < ActiveRecord::Migration + def self.up + create_table "participations", :force => true do |t| + t.string "guid" + t.integer "target_id" + t.string "target_type", :limit => 60, :null => false + t.integer "author_id" + t.text "author_signature" + t.text "parent_author_signature" + t.timestamps + end + end + + def self.down + drop_table :participations + end +end diff --git a/db/schema.rb b/db/schema.rb index b863d086e..c7d72dccb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20120203220932) do +ActiveRecord::Schema.define(:version => 20120208231253) do create_table "account_deletions", :force => true do |t| t.string "diaspora_handle" @@ -244,6 +244,17 @@ ActiveRecord::Schema.define(:version => 20120203220932) do add_index "oauth_clients", ["name"], :name => "index_oauth_clients_on_name", :unique => true add_index "oauth_clients", ["nonce"], :name => "index_oauth_clients_on_nonce", :unique => true + create_table "participations", :force => true do |t| + t.string "guid" + t.integer "target_id" + t.string "target_type", :limit => 60, :null => false + t.integer "author_id" + t.text "author_signature" + t.text "parent_author_signature" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "people", :force => true do |t| t.string "guid", :null => false t.text "url", :null => false diff --git a/lib/diaspora/user.rb b/lib/diaspora/user.rb deleted file mode 100644 index 6c346dabc..000000000 --- a/lib/diaspora/user.rb +++ /dev/null @@ -1,9 +0,0 @@ -require File.join(Rails.root, 'lib/diaspora/user/connecting') -require File.join(Rails.root, 'lib/diaspora/user/querying') - -module Diaspora - module UserModules - include Connecting - include Querying - end -end diff --git a/lib/diaspora/user/connecting.rb b/lib/diaspora/user/connecting.rb deleted file mode 100644 index c52dea5f7..000000000 --- a/lib/diaspora/user/connecting.rb +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (c) 2010-2011, Diaspora Inc. This file is -# licensed under the Affero General Public License version 3 or later. See -# the COPYRIGHT file. - -module Diaspora - module UserModules - module Connecting - # This will create a contact on the side of the sharer and the sharee. - # @param [Person] person The person to start sharing with. - # @param [Aspect] aspect The aspect to add them to. - # @return [Contact] The newly made contact for the passed in person. - def share_with(person, aspect) - contact = self.contacts.find_or_initialize_by_person_id(person.id) - return false unless contact.valid? - - unless contact.receiving? - contact.dispatch_request - contact.receiving = true - end - - contact.aspects << aspect - contact.save - - if notification = Notification.where(:target_id => person.id).first - notification.update_attributes(:unread=>false) - end - - register_share_visibilities(contact) - contact - end - - # This puts the last 100 public posts by the passed in contact into the user's stream. - # @param [Contact] contact - # @return [void] - def register_share_visibilities(contact) - #should have select here, but proven hard to test - posts = Post.where(:author_id => contact.person_id, :public => true).limit(100) - p = posts.map do |post| - ShareVisibility.new(:contact_id => contact.id, :shareable_id => post.id, :shareable_type => 'Post') - end - ShareVisibility.import(p) unless posts.empty? - nil - end - - def remove_contact(contact, opts={:force => false}) - posts = contact.posts.all - - if !contact.mutual? || opts[:force] - contact.destroy - else - contact.update_attributes(:receiving => false) - end - end - - def disconnect(bad_contact, opts={}) - person = bad_contact.person - Rails.logger.info("event=disconnect user=#{diaspora_handle} target=#{person.diaspora_handle}") - retraction = Retraction.for(self) - retraction.subscribers = [person]#HAX - Postzord::Dispatcher.build(self, retraction).post - - AspectMembership.where(:contact_id => bad_contact.id).delete_all - remove_contact(bad_contact, opts) - end - - def disconnected_by(person) - Rails.logger.info("event=disconnected_by user=#{diaspora_handle} target=#{person.diaspora_handle}") - if contact = self.contact_for(person) - remove_contact(contact) - end - end - end - end -end diff --git a/lib/diaspora/user/querying.rb b/lib/diaspora/user/querying.rb deleted file mode 100644 index 61665561b..000000000 --- a/lib/diaspora/user/querying.rb +++ /dev/null @@ -1,169 +0,0 @@ -# Copyright (c) 2010-2011, Diaspora Inc. This file is -# licensed under the Affero General Public License version 3 or later. See -# the COPYRIGHT file. - -require File.join(Rails.root, 'lib', 'evil_query') - - -#TODO: THIS FILE SHOULD NOT EXIST, EVIL SQL SHOULD BE ENCAPSULATED IN EvilQueries, -#throwing all of this stuff in user violates demeter like WHOA - -module Diaspora - module UserModules - module Querying - def find_visible_shareable_by_id(klass, id, opts={} ) - key = (opts.delete(:key) || :id) - ::EvilQuery::VisibleShareableById.new(self, klass, key, id, opts).post! - end - - def visible_shareables(klass, opts={}) - opts = prep_opts(klass, opts) - shareable_ids = visible_shareable_ids(klass, opts) - klass.where(:id => shareable_ids).select('DISTINCT '+klass.to_s.tableize+'.*').limit(opts[:limit]).order(opts[:order_with_table]) - end - - def visible_shareable_ids(klass, opts={}) - opts = prep_opts(klass, opts) - visible_ids_from_sql(klass, opts) - end - - # @return [Array] - def visible_ids_from_sql(klass, opts={}) - opts = prep_opts(klass, opts) - opts[:klass] = klass - opts[:by_members_of] ||= self.aspect_ids - - post_ids = klass.connection.select_values(visible_shareable_sql(klass, opts)).map { |id| id.to_i } - post_ids += klass.connection.select_values(construct_public_followings_sql(opts).to_sql).map {|id| id.to_i } - end - - def visible_shareable_sql(klass, opts={}) - table = klass.table_name - opts = prep_opts(klass, opts) - opts[:klass] = klass - - shareable_from_others = construct_shareable_from_others_query(opts) - shareable_from_self = construct_shareable_from_self_query(opts) - - "(#{shareable_from_others.to_sql} LIMIT #{opts[:limit]}) UNION ALL (#{shareable_from_self.to_sql} LIMIT #{opts[:limit]}) ORDER BY #{opts[:order]} LIMIT #{opts[:limit]}" - end - - def ugly_select_clause(query, opts) - klass = opts[:klass] - select_clause ='DISTINCT %s.id, %s.updated_at AS updated_at, %s.created_at AS created_at' % [klass.table_name, klass.table_name, klass.table_name] - query.select(select_clause).order(opts[:order_with_table]).where(klass.arel_table[opts[:order_field]].lt(opts[:max_time])) - end - - def construct_shareable_from_others_query(opts) - conditions = { - :pending => false, - :share_visibilities => {:hidden => opts[:hidden]}, - :contacts => {:user_id => self.id, :receiving => true} - } - - conditions[:type] = opts[:type] if opts.has_key?(:type) - - query = opts[:klass].joins(:contacts).where(conditions) - - if opts[:by_members_of] - query = query.joins(:contacts => :aspect_memberships).where( - :aspect_memberships => {:aspect_id => opts[:by_members_of]}) - end - - ugly_select_clause(query, opts) - end - - def construct_public_followings_sql(opts) - aspects = Aspect.where(:id => opts[:by_members_of]) - person_ids = Person.connection.select_values(people_in_aspects(aspects).select("people.id").to_sql) - - query = opts[:klass].where(:author_id => person_ids, :public => true, :pending => false) - - unless(opts[:klass] == Photo) - query = query.where(:type => opts[:type]) - end - - ugly_select_clause(query, opts) - end - - def construct_shareable_from_self_query(opts) - conditions = {:pending => false } - conditions[:type] = opts[:type] if opts.has_key?(:type) - query = self.person.send(opts[:klass].to_s.tableize).where(conditions) - - if opts[:by_members_of] - query = query.joins(:aspect_visibilities).where(:aspect_visibilities => {:aspect_id => opts[:by_members_of]}) - end - - ugly_select_clause(query, opts) - end - - def contact_for(person) - return nil unless person - contact_for_person_id(person.id) - end - - def aspects_with_shareable(base_class_name_or_class, shareable_id) - base_class_name = base_class_name_or_class - base_class_name = base_class_name_or_class.base_class.to_s if base_class_name_or_class.is_a?(Class) - self.aspects.joins(:aspect_visibilities).where(:aspect_visibilities => {:shareable_id => shareable_id, :shareable_type => base_class_name}) - end - - def contact_for_person_id(person_id) - Contact.where(:user_id => self.id, :person_id => person_id).includes(:person => :profile).first - end - - # @param [Person] person - # @return [Boolean] whether person is a contact of this user - def has_contact_for?(person) - Contact.exists?(:user_id => self.id, :person_id => person.id) - end - - def people_in_aspects(requested_aspects, opts={}) - allowed_aspects = self.aspects & requested_aspects - aspect_ids = allowed_aspects.map(&:id) - - people = Person.in_aspects(aspect_ids) - - if opts[:type] == 'remote' - people = people.where(:owner_id => nil) - elsif opts[:type] == 'local' - people = people.where('people.owner_id IS NOT NULL') - end - people - end - - def aspects_with_person person - contact_for(person).aspects - end - - def posts_from(person) - ::EvilQuery::ShareablesFromPerson.new(self, Post, person).make_relation! - end - - def photos_from(person) - ::EvilQuery::ShareablesFromPerson.new(self, Photo, person).make_relation! - end - - protected - - # @return [Hash] - def prep_opts(klass, opts) - defaults = { - :order => 'created_at DESC', - :limit => 15, - :hidden => false - } - defaults[:type] = Stream::Base::TYPES_OF_POST_IN_STREAM if klass == Post - opts = defaults.merge(opts) - - opts[:order_field] = opts[:order].split.first.to_sym - opts[:order_with_table] = klass.table_name + '.' + opts[:order] - - opts[:max_time] = Time.at(opts[:max_time]) if opts[:max_time].is_a?(Integer) - opts[:max_time] ||= Time.now + 1 - opts - end - end - end -end diff --git a/lib/evil_query.rb b/lib/evil_query.rb index e507c78ad..113890c3b 100644 --- a/lib/evil_query.rb +++ b/lib/evil_query.rb @@ -17,9 +17,7 @@ module EvilQuery end def posts - liked_post_ids = fetch_ids!(LikedPosts.new(@user).posts, "posts.id") - commented_post_ids = fetch_ids!(CommentedPosts.new(@user).posts, "posts.id") - Post.where(:id => liked_post_ids + commented_post_ids).order("posts.interacted_at DESC") + Post.joins(:participations).where(:participations => {:author_id => @user.person.id}).order("posts.interacted_at DESC") end end diff --git a/lib/federated/generator.rb b/lib/federated/generator.rb new file mode 100644 index 000000000..68f95e292 --- /dev/null +++ b/lib/federated/generator.rb @@ -0,0 +1,32 @@ +module Federated + class Generator + def initialize(user, target) + @user = user + @target = target + end + + def create!(options={}) + relayable = build(options) + if relayable.save + Postzord::Dispatcher.defer_build_and_post(@user, relayable) + relayable + else + false + end + end + + def build(options={}) + options.merge!(relayable_options) + relayable = self.class.federated_class.new(options.merge(:author_id => @user.person.id)) + relayable.set_guid + relayable.initialize_signatures + relayable + end + + protected + + def relayable_options + {} + end + end +end \ No newline at end of file diff --git a/lib/federated/relayable.rb b/lib/federated/relayable.rb new file mode 100644 index 000000000..618c24820 --- /dev/null +++ b/lib/federated/relayable.rb @@ -0,0 +1,44 @@ +module Federated + class Relayable < ActiveRecord::Base + self.abstract_class = true + + #crazy ordering issues - DEATH TO ROXML + include ROXML + + include Diaspora::Webhooks + include Diaspora::Guid + + #seriously, don't try to move this shit around until you have killed ROXML + xml_attr :target_type + include Diaspora::Relayable + + xml_attr :diaspora_handle + + belongs_to :target, :polymorphic => true + belongs_to :author, :class_name => 'Person' + #end crazy ordering issues + + validates_uniqueness_of :target_id, :scope => [:target_type, :author_id] + validates :parent, :presence => true #should be in relayable (pending on fixing Message) + + def diaspora_handle + self.author.diaspora_handle + end + + def diaspora_handle=(nh) + self.author = Webfinger.new(nh).fetch + end + + def parent_class + self.target_type.constantize + end + + def parent + self.target + end + + def parent= parent + self.target = parent + end + end +end \ No newline at end of file diff --git a/spec/lib/evil_query_spec.rb b/spec/lib/evil_query_spec.rb index 10ee9ffe4..68cd11199 100644 --- a/spec/lib/evil_query_spec.rb +++ b/spec/lib/evil_query_spec.rb @@ -6,7 +6,7 @@ describe EvilQuery::Participation do end it "includes posts liked by the user" do - Factory(:like, :target => @status_message, :author => alice.person) + alice.like!(@status_message) EvilQuery::Participation.new(alice).posts.should include(@status_message) end @@ -35,7 +35,7 @@ describe EvilQuery::Participation do alice.comment!(@status_messageB, "party") Timecop.travel time += 1.month - Factory(:like, :target => @status_messageA, :author => alice.person) + alice.like!(@status_messageA) Timecop.travel time += 1.month alice.comment!(@photoC, "party") diff --git a/spec/models/like_spec.rb b/spec/models/like_spec.rb index 568701874..46d676b76 100644 --- a/spec/models/like_spec.rb +++ b/spec/models/like_spec.rb @@ -7,8 +7,7 @@ require File.join(Rails.root, "spec", "shared_behaviors", "relayable") describe Like do before do - bobs_aspect = bob.aspects.first - @status = bob.post(:status_message, :text => "hello", :to => bobs_aspect.id) + @status = bob.post(:status_message, :text => "hello", :to => bob.aspects.first.id) end it 'has a valid factory' do @@ -91,8 +90,7 @@ describe Like do @object_on_remote_parent = @local_luke.like!(@remote_parent) end - let(:build_object) { alice.build_like(:target => @status, :positive => true) } + let(:build_object) { Like::Generator.new(alice, @status).build } it_should_behave_like 'it is relayable' end - end diff --git a/spec/models/participation_spec.rb b/spec/models/participation_spec.rb new file mode 100644 index 000000000..61a8fdac8 --- /dev/null +++ b/spec/models/participation_spec.rb @@ -0,0 +1,23 @@ +require "spec_helper" + +describe Participation do + describe 'it is relayable' do + before do + @status = bob.post(:status_message, :text => "hello", :to => bob.aspects.first.id) + + @local_luke, @local_leia, @remote_raphael = set_up_friends + @remote_parent = Factory(:status_message, :author => @remote_raphael) + @local_parent = @local_luke.post :status_message, :text => "foobar", :to => @local_luke.aspects.first + + @object_by_parent_author = @local_luke.participate!(@local_parent) + @object_by_recipient = @local_leia.participate!(@local_parent) + @dup_object_by_parent_author = @object_by_parent_author.dup + + @object_on_remote_parent = @local_luke.participate!(@remote_parent) + end + + let(:build_object) { Participation::Generator.new(alice, @status).build } + + it_should_behave_like 'it is relayable' + end +end \ No newline at end of file diff --git a/spec/models/user/connecting_spec.rb b/spec/models/user/connecting_spec.rb index 1329120cf..e1114ee12 100644 --- a/spec/models/user/connecting_spec.rb +++ b/spec/models/user/connecting_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' -describe Diaspora::UserModules::Connecting do +describe User::Connecting do let(:aspect) { alice.aspects.first } let(:aspect1) { alice.aspects.create(:name => 'other') } diff --git a/spec/models/user/querying_spec.rb b/spec/models/user/querying_spec.rb index b9cb377fd..36b9b4d02 100644 --- a/spec/models/user/querying_spec.rb +++ b/spec/models/user/querying_spec.rb @@ -4,8 +4,7 @@ require 'spec_helper' -describe User do - +describe User::Querying do before do @alices_aspect = alice.aspects.where(:name => "generic").first @eves_aspect = eve.aspects.where(:name => "generic").first diff --git a/spec/models/user/social_actions_spec.rb b/spec/models/user/social_actions_spec.rb new file mode 100644 index 000000000..a9a88078b --- /dev/null +++ b/spec/models/user/social_actions_spec.rb @@ -0,0 +1,85 @@ +require "spec_helper" + +describe User::SocialActions do + before do + @bobs_aspect = bob.aspects.where(:name => "generic").first + @status = bob.post(:status_message, :text => "hello", :to => @bobs_aspect.id) + end + + describe 'User#comment!' do + it "sets the comment text" do + alice.comment!(@status, "unicorn_mountain").text.should == "unicorn_mountain" + end + + it "creates a partcipation" do + lambda{ alice.comment!(@status, "bro") }.should change(Participation, :count).by(1) + alice.participations.last.target.should == @status + end + + it "creates the like" do + lambda{ alice.comment!(@status, "bro") }.should change(Comment, :count).by(1) + end + + it "federates" do + Participation::Generator.any_instance.stub(:create!) + Postzord::Dispatcher.should_receive(:defer_build_and_post) + alice.comment!(@status, "omg") + end + end + + describe 'User#like!' do + it "creates a partcipation" do + lambda{ alice.like!(@status) }.should change(Participation, :count).by(1) + alice.participations.last.target.should == @status + end + + it "creates the like" do + lambda{ alice.like!(@status) }.should change(Like, :count).by(1) + end + + it "federates" do + #participation and like + Participation::Generator.any_instance.stub(:create!) + Postzord::Dispatcher.should_receive(:defer_build_and_post) + alice.like!(@status) + end + end + + describe 'User#like!' do + before do + @bobs_aspect = bob.aspects.where(:name => "generic").first + @status = bob.post(:status_message, :text => "hello", :to => @bobs_aspect.id) + end + + it "creates a partcipation" do + lambda{ alice.like!(@status) }.should change(Participation, :count).by(1) + end + + it "creates the like" do + lambda{ alice.like!(@status) }.should change(Like, :count).by(1) + end + + it "federates" do + #participation and like + Postzord::Dispatcher.should_receive(:defer_build_and_post).twice + alice.like!(@status) + end + + it "should be able to like on one's own status" do + like = alice.like!(@status) + @status.reload.likes.first.should == like + end + + it "should be able to like on a contact's status" do + like = bob.like!(@status) + @status.reload.likes.first.should == like + end + + it "does not allow multiple likes" do + alice.like!(@status) + lambda { + alice.like!(@status) + }.should_not change(@status, :likes) + end + end +end \ No newline at end of file diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e7c765965..f4f286622 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -5,7 +5,6 @@ require 'spec_helper' describe User do - describe "private key" do it 'has a key' do alice.encryption_key.should_not be nil @@ -688,30 +687,6 @@ describe User do @like2 = bob.like!(@message) end - describe 'User#like' do - before do - @status = bob.post(:status_message, :text => "hello", :to => @bobs_aspect.id) - end - - it "should be able to like on one's own status" do - like = alice.like!(@status) - @status.reload.likes.first.should == like - end - - it "should be able to like on a contact's status" do - like = bob.like!(@status) - @status.reload.likes.first.should == like - end - - it "does not allow multiple likes" do - alice.like!(@status) - - lambda { - alice.like!(@status) - }.should_not change(@status, :likes) - end - end - describe '#like_for' do it 'returns the correct like' do alice.like_for(@message).should == @like