broke out some comment logic to a replayable module

This commit is contained in:
danielvincent 2011-02-28 15:23:24 -08:00
parent bd908a9b95
commit f4e6d0d82b
14 changed files with 317 additions and 302 deletions

View file

@ -18,8 +18,7 @@ class CommentsController < ApplicationController
if @comment.save
Rails.logger.info("event=create type=comment user=#{current_user.diaspora_handle} status=success comment=#{@comment.id} chars=#{params[:text].length}")
current_user.dispatch_comment(@comment)
Postzord::Dispatch.new(current_user, @comment).post
respond_to do |format|
format.js{

View file

@ -7,16 +7,15 @@ class Comment < ActiveRecord::Base
require File.join(Rails.root, 'lib/youtube_titles')
include YoutubeTitles
include ROXML
include Diaspora::Webhooks
include Encryptable
include Diaspora::Socketable
include Diaspora::Relayable
include Diaspora::Guid
include Diaspora::Socketable
xml_attr :text
xml_attr :diaspora_handle
xml_attr :post_guid
xml_attr :creator_signature
xml_attr :post_creator_signature
belongs_to :post, :touch => true
belongs_to :person
@ -35,12 +34,6 @@ class Comment < ActiveRecord::Base
def diaspora_handle= nh
self.person = Webfinger.new(nh).fetch
end
def post_guid
self.post.guid
end
def post_guid= new_post_guid
self.post = Post.where(:guid => new_post_guid).first
end
def notification_type(user, person)
if self.post.person == user.person
@ -52,63 +45,15 @@ class Comment < ActiveRecord::Base
end
end
def subscribers(user)
if user.owns?(self.post)
p = self.post.subscribers(user)
elsif user.owns?(self)
p = [self.post.person]
end
p
def parent_class
Post
end
def receive(user, person)
local_comment = Comment.where(:guid => self.guid).first
comment = local_comment || self
unless comment.post.person == user.person || comment.verify_post_creator_signature
Rails.logger.info("event=receive status=abort reason='comment signature not valid' recipient=#{user.diaspora_handle} sender=#{self.post.person.diaspora_handle} payload_type=#{self.class} post_id=#{self.post_id}")
return
def parent
self.post
end
#sign comment as the post creator if you've been hit UPSTREAM
if user.owns? comment.post
comment.post_creator_signature = comment.sign_with_key(user.encryption_key)
comment.save
def parent= parent
self.post = parent
end
#dispatch comment DOWNSTREAM, received it via UPSTREAM
unless user.owns?(comment)
comment.save
user.dispatch_comment(comment)
end
comment.socket_to_user(user, :aspect_ids => comment.post.aspect_ids)
comment
end
#ENCRYPTION
def signable_accessors
accessors = self.class.roxml_attrs.collect{|definition|
definition.accessor}
accessors.delete 'person'
accessors.delete 'creator_signature'
accessors.delete 'post_creator_signature'
accessors
end
def signable_string
signable_accessors.collect{|accessor|
(self.send accessor.to_sym).to_s}.join ';'
end
def verify_post_creator_signature
verify_signature(post_creator_signature, post.person)
end
def signature_valid?
verify_signature(creator_signature, person)
end
end

View file

@ -3,7 +3,6 @@
# the COPYRIGHT file.
class Post < ActiveRecord::Base
require File.join(Rails.root, 'lib/encryptable')
require File.join(Rails.root, 'lib/diaspora/web_socket')
include ApplicationHelper
include ROXML

View file

@ -128,8 +128,7 @@ class User < ActiveRecord::Base
end
def salmon(post)
created_salmon = Salmon::SalmonSlap.create(self, post.to_diaspora_xml)
created_salmon
Salmon::SalmonSlap.create(self, post.to_diaspora_xml)
end
######## Commenting ########
@ -139,21 +138,16 @@ class User < ActiveRecord::Base
:post => options[:on])
comment.set_guid
#sign comment as commenter
comment.creator_signature = comment.sign_with_key(self.encryption_key)
comment.author_signature = comment.sign_with_key(self.encryption_key)
if !comment.post_id.blank? && person.owns?(comment.post)
if !comment.post_id.blank? && person.owns?(comment.parent)
#sign comment as post owner
comment.post_creator_signature = comment.sign_with_key(self.encryption_key)
comment.parent_author_signature = comment.sign_with_key(self.encryption_key)
end
comment
end
def dispatch_comment(comment)
mailman = Postzord::Dispatch.new(self, comment)
mailman.post
end
######### Mailer #######################
def mail(job, *args)
unless self.disable_mail

View file

@ -0,0 +1,11 @@
class RenamePostToParentAndCreatorToAuthor < ActiveRecord::Migration
def self.up
rename_column :comments, :creator_signature, :author_signature
rename_column :comments, :post_creator_signature, :parent_author_signature
end
def self.down
rename_column :comments, :author_signature, :creator_signature
rename_column :comments, :parent_author_signature, :post_creator_signature
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20110228201109) do
ActiveRecord::Schema.define(:version => 20110228220810) do
create_table "aspect_memberships", :force => true do |t|
t.integer "aspect_id", :null => false
@ -43,8 +43,8 @@ ActiveRecord::Schema.define(:version => 20110228201109) do
t.integer "post_id", :null => false
t.integer "person_id", :null => false
t.string "guid", :null => false
t.text "creator_signature"
t.text "post_creator_signature"
t.text "author_signature"
t.text "parent_author_signature"
t.text "youtube_titles"
t.datetime "created_at"
t.datetime "updated_at"

125
lib/diaspora/relayable.rb Normal file
View file

@ -0,0 +1,125 @@
# Copyright (c) 2010, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
module Diaspora
module Relayable
def self.included(model)
model.class_eval do
#these fields must be in the schema for a relayable model
xml_attr :parent_guid
xml_attr :parent_author_signature
xml_attr :author_signature
end
end
def parent_guid
self.parent.guid
end
def parent_guid= new_parent_guid
self.parent = parent_class.where(:guid => new_parent_guid).first
end
def subscribers(user)
if user.owns?(self.parent)
self.parent.subscribers(user)
elsif user.owns?(self)
[self.parent.person]
end
end
def receive(user, person)
object = self.class.where(:guid => self.guid).first || self
unless object.parent.person == user.person || object.verify_parent_author_signature
Rails.logger.info("event=receive status=abort reason='object signature not valid' recipient=#{user.diaspora_handle} sender=#{self.parent.person.diaspora_handle} payload_type=#{self.class} parent_id=#{self.parent.id}")
return
end
#sign object as the parent creator if you've been hit UPSTREAM
if user.owns? object.parent
object.parent_author_signature = object.sign_with_key(user.encryption_key)
object.save
end
#dispatch object DOWNSTREAM, received it via UPSTREAM
unless user.owns?(object)
object.save
Postzord::Dispatch.new(user, object).post
end
object.socket_to_user(user, :aspect_ids => object.parent.aspect_ids)
object
end
def signable_string
raise NotImplementedException("Override this in your encryptable class")
end
def signature_valid?
verify_signature(creator_signature, person)
end
def verify_signature(signature, person)
if person.nil?
Rails.logger.info("event=verify_signature status=abort reason=no_person guid=#{self.guid} model_id=#{self.id}")
return false
elsif person.public_key.nil?
Rails.logger.info("event=verify_signature status=abort reason=no_key guid=#{self.guid} model_id=#{self.id}")
return false
elsif signature.nil?
Rails.logger.info("event=verify_signature status=abort reason=no_signature guid=#{self.guid} model_id=#{self.id}")
return false
end
log_string = "event=verify_signature status=complete model_id=#{id}"
validity = person.public_key.verify "SHA", Base64.decode64(signature), signable_string
log_string += " validity=#{validity}"
Rails.logger.info(log_string)
validity
end
def sign_with_key(key)
sig = Base64.encode64(key.sign "SHA", signable_string)
Rails.logger.info("event=sign_with_key status=complete model_id=#{id}")
sig
end
def signable_accessors
accessors = self.class.roxml_attrs.collect do |definition|
definition.accessor
end
['person', 'author_signature', 'parent_author_signature'].each do |acc|
accessors.delete acc
end
accessors
end
def signable_string
signable_accessors.collect{ |accessor|
(self.send accessor.to_sym).to_s
}.join(';')
end
def verify_parent_author_signature
verify_signature(self.parent_author_signature, parent.person)
end
def signature_valid?
verify_signature(self.author_signature, person)
end
def parent_class
raise NotImplementedError.new('you must override parent_class in order to enable relayable on this model')
end
def parent
raise NotImplementedError.new('you must override parent in order to enable relayable on this model')
end
def parent= parent
raise NotImplementedError.new('you must override parent= in order to enable relayable on this model')
end
end
end

View file

@ -1,39 +0,0 @@
# Copyright (c) 2010, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
module Encryptable
def signable_string
raise NotImplementedException("Override this in your encryptable class")
end
def signature_valid?
verify_signature(creator_signature, person)
end
def verify_signature(signature, person)
if person.nil?
Rails.logger.info("event=verify_signature status=abort reason=no_person guid=#{self.guid} model_id=#{self.id}")
return false
elsif person.public_key.nil?
Rails.logger.info("event=verify_signature status=abort reason=no_key guid=#{self.guid} model_id=#{self.id}")
return false
elsif signature.nil?
Rails.logger.info("event=verify_signature status=abort reason=no_signature guid=#{self.guid} model_id=#{self.id}")
return false
end
log_string = "event=verify_signature status=complete model_id=#{id}"
validity = person.public_key.verify "SHA", Base64.decode64(signature), signable_string
log_string += " validity=#{validity}"
Rails.logger.info(log_string)
validity
end
def sign_with_key(key)
sig = Base64.encode64(key.sign "SHA", signable_string)
Rails.logger.info("event=sign_with_key status=complete model_id=#{id}")
sig
end
end

View file

@ -186,7 +186,7 @@ describe 'a user receives a post' do
receive_with_zord(@user3, @user1.person, xml)
@comment = @user3.comment('tada',:on => @post)
@comment.post_creator_signature = @comment.sign_with_key(@user1.encryption_key)
@comment.parent_author_signature = @comment.sign_with_key(@user1.encryption_key)
@xml = @comment.to_diaspora_xml
@comment.delete
end

View file

@ -0,0 +1,58 @@
# Copyright (c) 2010, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
require 'spec_helper'
describe Diaspora::Relayable do
before do
@alices_aspect = alice.aspects.first
@bobs_aspect = bob.aspects.first
@remote_message = bob.post :status_message, :message => "hello", :to => @bobs_aspect.id
@message = alice.post :status_message, :message => "hi", :to => @alices_aspect.id
end
describe '#parent_author_signature' do
it 'should sign the comment if the user is the post author' do
message = alice.post :status_message, :message => "hi", :to => @alices_aspect.id
alice.comment "Yeah, it was great", :on => message
message.comments.reset
message.comments.first.signature_valid?.should be_true
message.comments.first.verify_parent_author_signature.should be_true
end
it 'should verify a comment made on a remote post by a different contact' do
comment = Comment.new(:person => bob.person, :text => "cats", :post => @remote_message)
comment.author_signature = comment.send(:sign_with_key, bob.encryption_key)
comment.signature_valid?.should be_true
comment.verify_parent_author_signature.should be_false
comment.parent_author_signature = comment.send(:sign_with_key, alice.encryption_key)
comment.verify_parent_author_signature.should be_true
end
end
describe '#author_signature' do
it 'should attach the author signature if the user is commenting' do
comment = alice.comment "Yeah, it was great", :on => @remote_message
@remote_message.comments.reset
@remote_message.comments.first.signature_valid?.should be_true
end
it 'should reject comments on a remote post with only a author sig' do
comment = Comment.new(:person => bob.person, :text => "cats", :post => @remote_message)
comment.author_signature = comment.send(:sign_with_key, bob.encryption_key)
comment.signature_valid?.should be_true
comment.verify_parent_author_signature.should be_false
end
it 'should receive remote comments on a user post with a author sig' do
comment = Comment.new(:person => bob.person, :text => "cats", :post => @message)
comment.author_signature = comment.send(:sign_with_key, bob.encryption_key)
comment.signature_valid?.should be_true
comment.verify_parent_author_signature.should be_false
end
end
end

View file

@ -49,4 +49,17 @@ describe 'making sure the spec runner works' do
@user2.reload.visible_posts.should include message
end
end
describe '#comment' do
it "should send a user's comment on a person's post to that person" do
person = Factory.create(:person)
person_status = Factory.create(:status_message, :person => person)
m = mock()
m.stub!(:post)
Postzord::Dispatch.should_receive(:new).and_return(m)
alice.comment "yo", :on => person_status
end
end
end

View file

@ -8,48 +8,41 @@ describe Comment do
before do
@alices_aspect = alice.aspects.first
@bobs_aspect = bob.aspects.first
@bob = bob
@eve = eve
@status = alice.post(:status_message, :message => "hello", :to => @alices_aspect.id)
end
describe 'comment#notification_type' do
before do
@sam = Factory(:user_with_aspect)
connect_users(alice, @alices_aspect, @sam, @sam.aspects.first)
@alices_post = alice.post(:status_message, :message => "hello", :to => @alices_aspect.id)
end
it "returns 'comment_on_post' if the comment is on a post you own" do
comment = bob.comment("why so formal?", :on => @alices_post)
comment = bob.comment("why so formal?", :on => @status)
comment.notification_type(alice, bob.person).should == Notifications::CommentOnPost
end
it 'returns false if the comment is not on a post you own and no one "also_commented"' do
comment = alice.comment("I simply felt like issuing a greeting. Do step off.", :on => @alices_post)
comment.notification_type(@sam, alice.person).should == false
comment = alice.comment("I simply felt like issuing a greeting. Do step off.", :on => @status)
comment.notification_type(@bob, alice.person).should == false
end
context "also commented" do
before do
bob.comment("a-commenta commenta", :on => @alices_post)
@comment = @sam.comment("I also commented on the first user's post", :on => @alices_post)
@bob.comment("a-commenta commenta", :on => @status)
@comment = @eve.comment("I also commented on the first user's post", :on => @status)
end
it 'does not return also commented if the user commented' do
@comment.notification_type(@sam, alice.person).should == false
@comment.notification_type(@eve, alice.person).should == false
end
it "returns 'also_commented' if another person commented on a post you commented on" do
@comment.notification_type(bob, alice.person).should == Notifications::AlsoCommented
@comment.notification_type(@bob, alice.person).should == Notifications::AlsoCommented
end
end
end
describe 'User#comment' do
before do
@status = alice.post(:status_message, :message => "hello", :to => @alices_aspect.id)
end
it "should be able to comment on one's own status" do
alice.comment("Yeah, it was great", :on => @status)
@status.reload.comments.first.text.should == "Yeah, it was great"
@ -59,66 +52,13 @@ describe Comment do
bob.comment("sup dog", :on => @status)
@status.reload.comments.first.text.should == "sup dog"
end
end
context 'comment propagation' do
before do
@person = Factory.create(:person)
alice.activate_contact(@person, @alices_aspect)
@person2 = Factory.create(:person)
@person3 = Factory.create(:person)
alice.activate_contact(@person3, @alices_aspect)
@person_status = Factory.create(:status_message, :person => @person)
alice.reload
@user_status = alice.post :status_message, :message => "hi", :to => @alices_aspect.id
@alices_aspect.reload
alice.reload
end
it 'should send the comment to the postman' do
m = mock()
m.stub!(:post)
Postzord::Dispatch.should_receive(:new).and_return(m)
alice.comment "yo", :on => @person_status
end
describe '#subscribers' do
it 'returns the posts original audience, if the post is owned by the user' do
comment = alice.build_comment "yo", :on => @person_status
comment.subscribers(alice).should =~ [@person]
end
it 'returns the owner of the original post, if the user owns the comment' do
comment = alice.build_comment "yo", :on => @user_status
comment.subscribers(alice).map { |s| s.id }.should =~ [@person, @person3, bob.person].map { |s| s.id }
it 'does not multi-post a comment' do
lambda {
alice.comment 'hello', :on => @status
}.should change { Comment.count }.by(1)
end
end
context 'testing a method only used for testing' do
it "should send a user's comment on a person's post to that person" do
m = mock()
m.stub!(:post)
Postzord::Dispatch.should_receive(:new).and_return(m)
alice.comment "yo", :on => @person_status
end
end
it 'should not clear the aspect post array on receiving a comment' do
@alices_aspect.post_ids.include?(@user_status.id).should be_true
comment = Comment.new(:person_id => @person.id, :text => "cats", :post => @user_status)
zord = Postzord::Receiver.new(alice, :person => @person)
zord.parse_and_receive(comment.to_diaspora_xml)
@alices_aspect.reload
@alices_aspect.post_ids.include?(@user_status.id).should be_true
end
end
describe 'xml' do
before do
@commenter = Factory.create(:user)
@ -146,59 +86,6 @@ describe Comment do
end
end
end
describe 'local commenting' do
before do
@status = alice.post(:status_message, :message => "hello", :to => @alices_aspect.id)
end
it 'does not multi-post a comment' do
lambda {
alice.comment 'hello', :on => @status
}.should change { Comment.count }.by(1)
end
end
describe 'comments' do
before do
@remote_message = bob.post :status_message, :message => "hello", :to => @bobs_aspect.id
@message = alice.post :status_message, :message => "hi", :to => @alices_aspect.id
end
it 'should attach the creator signature if the user is commenting' do
comment = alice.comment "Yeah, it was great", :on => @remote_message
@remote_message.comments.reset
@remote_message.comments.first.signature_valid?.should be_true
end
it 'should sign the comment if the user is the post creator' do
message = alice.post :status_message, :message => "hi", :to => @alices_aspect.id
alice.comment "Yeah, it was great", :on => message
message.comments.reset
message.comments.first.signature_valid?.should be_true
message.comments.first.verify_post_creator_signature.should be_true
end
it 'should verify a comment made on a remote post by a different contact' do
comment = Comment.new(:person => bob.person, :text => "cats", :post => @remote_message)
comment.creator_signature = comment.send(:sign_with_key, bob.encryption_key)
comment.signature_valid?.should be_true
comment.verify_post_creator_signature.should be_false
comment.post_creator_signature = comment.send(:sign_with_key, alice.encryption_key)
comment.verify_post_creator_signature.should be_true
end
it 'should reject comments on a remote post with only a creator sig' do
comment = Comment.new(:person => bob.person, :text => "cats", :post => @remote_message)
comment.creator_signature = comment.send(:sign_with_key, bob.encryption_key)
comment.signature_valid?.should be_true
comment.verify_post_creator_signature.should be_false
end
it 'should receive remote comments on a user post with a creator sig' do
comment = Comment.new(:person => bob.person, :text => "cats", :post => @message)
comment.creator_signature = comment.send(:sign_with_key, bob.encryption_key)
comment.signature_valid?.should be_true
comment.verify_post_creator_signature.should be_false
end
end
describe 'youtube' do
before do
@ -220,4 +107,74 @@ describe Comment do
Comment.find(comment.id).youtube_titles.should == {video_id => CGI::escape(expected_title)}
end
end
context 'comment propagation' do
before do
@local_luke, @local_leia, @remote_raphael = set_up_friends
@person_status = Factory.create(:status_message, :person => @remote_raphael)
@user_status = @local_luke.post :status_message, :message => "hi", :to => @local_luke.aspects.first
@lukes_aspect = @local_luke.aspects.first
end
it 'should not clear the aspect post array on receiving a comment' do
@lukes_aspect.post_ids.include?(@user_status.id).should be_true
comment = Comment.new(:person_id => @remote_raphael.id, :text => "cats", :post => @user_status)
zord = Postzord::Receiver.new(alice, :person => @remote_raphael)
zord.parse_and_receive(comment.to_diaspora_xml)
@lukes_aspect.reload
@lukes_aspect.post_ids.include?(@user_status.id).should be_true
end
describe '#receive' do
before do
@comment = @local_luke.comment("yo", :on => @user_status)
@comment2 = @local_leia.build_comment("yo", :on => @user_status)
@new_c = @comment.dup
end
it 'does not overwrite a comment that is already in the db' do
lambda{
@new_c.receive(@local_leia, @local_luke.person)
}.should_not change(Comment, :count)
end
it 'does not process if post_creator_signature is invalid' do
@comment.delete # remove comment from db so we set a creator sig
@new_c.parent_author_signature = "dsfadsfdsa"
@new_c.receive(@local_leia, @local_luke.person).should == nil
end
it 'signs when the person receiving is the parent author' do
@comment2.save
@comment2.receive(@local_luke, @local_leia.person)
@comment2.reload.parent_author_signature.should_not be_blank
end
it 'dispatches when the person receiving is the parent author' do
p = Postzord::Dispatch.new(@local_luke, @comment2)
p.should_receive(:post)
Postzord::Dispatch.stub!(:new).and_return(p)
@comment2.receive(@local_luke, @local_leia.person)
end
it 'sockets to the user' do
@comment2.should_receive(:socket_to_user).exactly(3).times
@comment2.receive(@local_luke, @local_leia.person)
end
end
describe '#subscribers' do
it 'returns the posts original audience, if the post is owned by the user' do
comment = @local_luke.build_comment "yo", :on => @user_status
comment.subscribers(@local_luke).map(&:id).should =~ [@local_leia.person, @remote_raphael].map(&:id)
end
it 'returns the owner of the original post, if the user owns the comment' do
comment = @local_leia.build_comment "yo", :on => @user_status
comment.subscribers(@local_leia).map(&:id).should =~ [@local_luke.person].map(&:id)
end
end
end
end

View file

@ -1,43 +0,0 @@
# Copyright (c) 2010, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
require 'spec_helper'
describe User do
let!(:user1){alice}
let!(:user2){bob}
let!(:aspect1){user1.aspects.first}
let!(:aspect2){user2.aspects.first}
before do
@post = user1.build_post(:status_message, :message => "hey", :to => aspect1.id)
@post.save
user1.dispatch_post(@post, :to => "all")
end
describe '#dispatch_comment' do
context "post owner's contact is commenting" do
it "doesn't call receive on local users" do
user1.should_not_receive(:receive_comment)
user2.should_not_receive(:receive_comment)
comment = user2.build_comment "why so formal?", :on => @post
comment.save!
user2.dispatch_comment comment
end
end
context "post owner is commenting on own post" do
it "doesn't call receive on local users" do
user1.should_not_receive(:receive_comment)
user2.should_not_receive(:receive_comment)
comment = user1.build_comment "why so formal?", :on => @post
comment.save!
user1.dispatch_comment comment
end
end
end
end

View file

@ -18,10 +18,7 @@ class User
fantasy_resque do
p = build_post(class_name, opts)
if p.save!
raise 'MongoMapper failed to catch a failed save' unless p.id
self.aspects.reload
aspects = self.aspects_from_ids(opts[:to])
add_to_streams(p, aspects)
dispatch_post(p, :to => opts[:to])
@ -34,8 +31,7 @@ class User
fantasy_resque do
c = build_comment(text, options)
if c.save!
raise 'MongoMapper failed to catch a failed save' unless c.id
dispatch_comment(c)
Postzord::Dispatch.new(self, c).post
end
c
end