introduce the idea of Federated::Base. this is mostly just renaming and collasping of different federation modules, but also starting a direct hiearchy of these federation classes to make everything easier to refactor

This commit is contained in:
Maxwell Salzberg 2012-02-25 16:57:14 -08:00
parent 49b0a44738
commit 27a4c1bf2d
24 changed files with 236 additions and 262 deletions

View file

@ -3,8 +3,7 @@
# the COPYRIGHT file. # the COPYRIGHT file.
class AccountDeletion < ActiveRecord::Base class AccountDeletion < ActiveRecord::Base
include ROXML include Diaspora::Federated::Base
include Diaspora::Webhooks
belongs_to :person belongs_to :person
@ -15,6 +14,7 @@ class AccountDeletion < ActiveRecord::Base
xml_name :account_deletion xml_name :account_deletion
xml_attr :diaspora_handle xml_attr :diaspora_handle
def person=(person) def person=(person)
self[:diaspora_handle] = person.diaspora_handle self[:diaspora_handle] = person.diaspora_handle
self[:person_id] = person.id self[:person_id] = person.id

View file

@ -3,9 +3,9 @@
# the COPYRIGHT file. # the COPYRIGHT file.
class Comment < ActiveRecord::Base class Comment < ActiveRecord::Base
include ROXML
include Diaspora::Webhooks include Diaspora::Federated::Base
include Diaspora::Guid include Diaspora::Guid
include Diaspora::Relayable include Diaspora::Relayable

View file

@ -1,7 +1,6 @@
class Conversation < ActiveRecord::Base class Conversation < ActiveRecord::Base
include ROXML include Diaspora::Federated::Base
include Diaspora::Guid include Diaspora::Guid
include Diaspora::Webhooks
xml_attr :subject xml_attr :subject
xml_attr :created_at xml_attr :created_at

View file

@ -1,9 +1,7 @@
class NotVisibleError < RuntimeError; end class NotVisibleError < RuntimeError; end
class Message < ActiveRecord::Base class Message < ActiveRecord::Base
include ROXML include Diaspora::Federated::Base
include Diaspora::Guid include Diaspora::Guid
include Diaspora::Webhooks
include Diaspora::Relayable include Diaspora::Relayable
xml_attr :text xml_attr :text

View file

@ -5,10 +5,12 @@
class Photo < ActiveRecord::Base class Photo < ActiveRecord::Base
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
include Diaspora::Federated::Shareable
include Diaspora::Commentable include Diaspora::Commentable
include Diaspora::Shareable include Diaspora::Shareable
# NOTE API V1 to be extracted # NOTE API V1 to be extracted
acts_as_api acts_as_api
api_accessible :backbone do |t| api_accessible :backbone do |t|

View file

@ -5,10 +5,13 @@
class Post < ActiveRecord::Base class Post < ActiveRecord::Base
include ApplicationHelper include ApplicationHelper
include Diaspora::Federated::Shareable
include Diaspora::Likeable include Diaspora::Likeable
include Diaspora::Commentable include Diaspora::Commentable
include Diaspora::Shareable include Diaspora::Shareable
has_many :participations, :dependent => :delete_all, :as => :target has_many :participations, :dependent => :delete_all, :as => :target
attr_accessor :user_like, attr_accessor :user_like,

View file

@ -3,10 +3,8 @@
# the COPYRIGHT file. # the COPYRIGHT file.
class Profile < ActiveRecord::Base class Profile < ActiveRecord::Base
require File.join(Rails.root, 'lib/diaspora/webhooks') include Diaspora::Federated::Base
include Diaspora::Webhooks
include Diaspora::Taggable include Diaspora::Taggable
include ROXML
attr_accessor :tag_string attr_accessor :tag_string

View file

@ -4,8 +4,7 @@
# the COPYRIGHT file. # the COPYRIGHT file.
class Request class Request
include ROXML include Diaspora::Federated::Base
include Diaspora::Webhooks
include ActiveModel::Validations include ActiveModel::Validations
attr_accessor :sender, :recipient, :aspect attr_accessor :sender, :recipient, :aspect

View file

@ -3,8 +3,7 @@
# the COPYRIGHT file. # the COPYRIGHT file.
class Retraction class Retraction
include ROXML include Diaspora::Federated::Base
include Diaspora::Webhooks
xml_accessor :post_guid xml_accessor :post_guid
xml_accessor :diaspora_handle xml_accessor :diaspora_handle

View file

@ -3,8 +3,8 @@
# the COPYRIGHT file. # the COPYRIGHT file.
class SignedRetraction class SignedRetraction
include ROXML include Diaspora::Federated::Base
include Diaspora::Webhooks
include Diaspora::Encryptable include Diaspora::Encryptable
xml_name :signed_retraction xml_name :signed_retraction

View file

@ -4,5 +4,4 @@
module Diaspora module Diaspora
autoload :Parser autoload :Parser
autoload :Webhooks
end end

View file

@ -0,0 +1,56 @@
# Copyright (c) 2010-2012, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
#the base level federation contract, which right now means that the object
#can be serialized and deserialized from xml, and respond to methods
#in the federation flow
#including this module lets you federate an object at the most basic of level
require 'builder/xchar'
module Diaspora
module Federated
module Base
def self.included(model)
model.instance_eval do
include ROXML
include Diaspora::Federated::Base::InstanceMethods
end
end
module InstanceMethods
def to_diaspora_xml
<<-XML
<XML>
<post>#{to_xml.to_s}</post>
</XML>
XML
end
def x(input)
input.to_s.to_xs
end
# @abstract
# @note this must return [Array<Person>]
# @return [Array<Person>]
def subscribers(user)
raise 'You must override subscribers in order to enable federation on this model'
end
# @abstract
def receive(user, person)
raise 'You must override receive in order to enable federation on this model'
end
# @param [User] sender
# @note this is a hook(optional)
def after_dispatch(sender)
end
end
end
end
end

View file

@ -0,0 +1,116 @@
# Copyright (c) 2012, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
#this module attempts to be what you need to mix into
# base level federation objects that are not relayable, and not persistable
#assumes there is an author, author_id, id,
module Diaspora
module Federated
module Shareable
def self.included(model)
model.instance_eval do
#we are order dependant so you don't have to be!
include Diaspora::Federated::Base
include Diaspora::Federated::Shareable::InstanceMethods
include Diaspora::Guid
xml_attr :diaspora_handle
xml_attr :public
xml_attr :created_at
end
end
module InstanceMethods
def diaspora_handle
read_attribute(:diaspora_handle) || self.author.diaspora_handle
end
def diaspora_handle=(author_handle)
self.author = Person.where(:diaspora_handle => author_handle).first
write_attribute(:diaspora_handle, author_handle)
end
# @param [User] user The user that is receiving this shareable.
# @param [Person] person The person who dispatched this shareable to the
# @return [void]
def receive(user, person)
#exists locally, but you dont know about it
#does not exsist locally, and you dont know about it
#exists_locally?
#you know about it, and it is mutable
#you know about it, and it is not mutable
self.class.transaction do
local_shareable = persisted_shareable
if local_shareable && verify_persisted_shareable(local_shareable)
self.receive_persisted(user, person, local_shareable)
elsif !local_shareable
self.receive_non_persisted(user, person)
else
Rails.logger.info("event=receive payload_type=#{self.class} update=true status=abort sender=#{self.diaspora_handle} reason='update not from shareable owner' existing_shareable=#{self.id}")
false
end
end
end
# The list of people that should receive this Shareable.
#
# @param [User] user The context, or dispatching user.
# @return [Array<Person>] The list of subscribers to this shareable
def subscribers(user)
if self.public?
user.contact_people
else
user.people_in_aspects(user.aspects_with_shareable(self.class, self.id))
end
end
protected
# @return [Shareable,void]
def persisted_shareable
self.class.where(:guid => self.guid).first
end
# @return [Boolean]
def verify_persisted_shareable(persisted_shareable)
persisted_shareable.author_id == self.author_id
end
def receive_persisted(user, person, local_shareable)
known_shareable = user.find_visible_shareable_by_id(self.class.base_class, self.guid, :key => :guid)
if known_shareable
if known_shareable.mutable?
known_shareable.update_attributes(self.attributes)
true
else
Rails.logger.info("event=receive payload_type=#{self.class} update=true status=abort sender=#{self.diaspora_handle} reason=immutable") #existing_shareable=#{known_shareable.id}")
false
end
else
user.contact_for(person).receive_shareable(local_shareable)
user.notify_if_mentioned(local_shareable)
Rails.logger.info("event=receive payload_type=#{self.class} update=true status=complete sender=#{self.diaspora_handle}") #existing_shareable=#{local_shareable.id}")
true
end
end
def receive_non_persisted(user, person)
if self.save
user.contact_for(person).receive_shareable(self)
user.notify_if_mentioned(self)
Rails.logger.info("event=receive payload_type=#{self.class} update=false status=complete sender=#{self.diaspora_handle}")
true
else
Rails.logger.info("event=receive payload_type=#{self.class} update=false status=abort sender=#{self.diaspora_handle} reason=#{self.errors.full_messages}")
false
end
end
end
end
end
end

View file

@ -1,9 +1,13 @@
#implicitly requires roxml
module Diaspora::Guid module Diaspora::Guid
# Creates a before_create callback which calls #set_guid and makes the guid serialize in to_xml # Creates a before_create callback which calls #set_guid and makes the guid serialize in to_xml
def self.included(model) def self.included(model)
model.class_eval do model.class_eval do
before_create :set_guid before_create :set_guid
xml_attr :guid xml_attr :guid
validates :guid, :uniqueness => true
end end
end end

View file

@ -1,80 +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
class Director
def initialize
@structure = [:create_headers, :create_endpoints, :create_subject,
:create_body, :create_footer]
end
def build(builder)
@structure.inject("") do |xml, method|
xml << builder.send(method) if builder.respond_to? method
end
end
end
class OstatusBuilder
include Diaspora::Webhooks
include PeopleHelper
def initialize(user, posts)
@user = user
@posts = posts
end
def create_headers
<<-XML
<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/">
<generator uri="#{AppConfig[:pod_url]}">Diaspora</generator>
<id>#{@user.public_url}.atom</id>
<title>#{x(@user.name)}'s Public Feed</title>
<subtitle>Updates from #{x(@user.name)} on Diaspora</subtitle>
<logo>#{@user.person.profile.image_url(:thumb_small)}</logo>
<updated>#{Time.now.xmlschema}</updated>
XML
end
def create_subject
<<-XML
<author>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<name>#{x(@user.name)}</name>
<uri>#{local_or_remote_person_path(@user.person, :absolute => true)}</uri>
<poco:preferredUsername>#{x(@user.username)}</poco:preferredUsername>
<poco:displayName>#{x(@user.person.name)}</poco:displayName>
</author>
XML
end
def create_endpoints
<<-XML
<link rel="alternate" type="text/html" href="#{@user.public_url}" />
<link rel="avatar" type="image/jpeg" media:width="100" media:height="100" href="#{@user.profile.image_url}"/>
<link href="#{AppConfig[:pubsub_server]}" rel="hub"/>
<link href="#{@user.public_url}.atom" rel="self" type="application/atom+xml"/>
XML
end
def create_body
@posts.inject("") do |xml,curr|
if curr.respond_to?(:to_activity)
xml + curr.to_activity(:author => @user.person)
else
xml
end
end
end
def create_footer
<<-XML
</feed>
XML
end
end
end

View file

@ -1,15 +1,12 @@
# Copyright (c) 2010, Diaspora Inc. This file is # Copyright (c) 2010, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See # licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file. # the COPYRIGHT file.
#the pont of this object is to centralize the simmilarities of Photo and post,
# as they used to be the same class
module Diaspora module Diaspora
module Shareable module Shareable
include Diaspora::Webhooks
def self.included(model) def self.included(model)
model.instance_eval do model.instance_eval do
include ROXML
include Diaspora::Guid
has_many :aspect_visibilities, :as => :shareable has_many :aspect_visibilities, :as => :shareable
has_many :aspects, :through => :aspect_visibilities has_many :aspects, :through => :aspect_visibilities
@ -19,7 +16,6 @@ module Diaspora
belongs_to :author, :class_name => 'Person' belongs_to :author, :class_name => 'Person'
validates :guid, :uniqueness => true
#scopes #scopes
scope :all_public, where(:public => true, :pending => false) scope :all_public, where(:public => true, :pending => false)
@ -27,7 +23,8 @@ module Diaspora
def self.owned_or_visible_by_user(user) def self.owned_or_visible_by_user(user)
self.joins("LEFT OUTER JOIN share_visibilities ON share_visibilities.shareable_id = posts.id AND share_visibilities.shareable_type = 'Post'"). self.joins("LEFT OUTER JOIN share_visibilities ON share_visibilities.shareable_id = posts.id AND share_visibilities.shareable_type = 'Post'").
joins("LEFT OUTER JOIN contacts ON contacts.id = share_visibilities.contact_id"). joins("LEFT OUTER JOIN contacts ON contacts.id = share_visibilities.contact_id").
where(Contact.arel_table[:user_id].eq(user.id).or( where(
Contact.arel_table[:user_id].eq(user.id).or(
self.arel_table[:public].eq(true).or( self.arel_table[:public].eq(true).or(
self.arel_table[:author_id].eq(user.person.id) self.arel_table[:author_id].eq(user.person.id)
) )
@ -45,56 +42,6 @@ module Diaspora
def self.by_max_time(max_time, order='created_at') def self.by_max_time(max_time, order='created_at')
where("#{self.table_name}.#{order} < ?", max_time).order("#{self.table_name}.#{order} desc") where("#{self.table_name}.#{order} < ?", max_time).order("#{self.table_name}.#{order} desc")
end end
xml_attr :diaspora_handle
xml_attr :public
xml_attr :created_at
end
end
def diaspora_handle
read_attribute(:diaspora_handle) || self.author.diaspora_handle
end
def diaspora_handle= nd
self.author = Person.where(:diaspora_handle => nd).first
write_attribute(:diaspora_handle, nd)
end
# @param [User] user The user that is receiving this shareable.
# @param [Person] person The person who dispatched this shareable to the
# @return [void]
def receive(user, person)
#exists locally, but you dont know about it
#does not exsist locally, and you dont know about it
#exists_locally?
#you know about it, and it is mutable
#you know about it, and it is not mutable
self.class.transaction do
local_shareable = persisted_shareable
if local_shareable && verify_persisted_shareable(local_shareable)
self.receive_persisted(user, person, local_shareable)
elsif !local_shareable
self.receive_non_persisted(user, person)
else
Rails.logger.info("event=receive payload_type=#{self.class} update=true status=abort sender=#{self.diaspora_handle} reason='update not from shareable owner' existing_shareable=#{self.id}")
false
end
end
end
# The list of people that should receive this Shareable.
#
# @param [User] user The context, or dispatching user.
# @return [Array<Person>] The list of subscribers to this shareable
def subscribers(user)
if self.public?
user.contact_people
else
user.people_in_aspects(user.aspects_with_shareable(self.class, self.id))
end end
end end
@ -103,48 +50,5 @@ module Diaspora
self.class.where(:id => self.id). self.class.where(:id => self.id).
update_all(:reshares_count => self.reshares.count) update_all(:reshares_count => self.reshares.count)
end end
protected
# @return [Shareable,void]
def persisted_shareable
self.class.where(:guid => self.guid).first
end
# @return [Boolean]
def verify_persisted_shareable(persisted_shareable)
persisted_shareable.author_id == self.author_id
end
def receive_persisted(user, person, local_shareable)
known_shareable = user.find_visible_shareable_by_id(self.class.base_class, self.guid, :key => :guid)
if known_shareable
if known_shareable.mutable?
known_shareable.update_attributes(self.attributes)
true
else
Rails.logger.info("event=receive payload_type=#{self.class} update=true status=abort sender=#{self.diaspora_handle} reason=immutable") #existing_shareable=#{known_shareable.id}")
false
end
else
user.contact_for(person).receive_shareable(local_shareable)
user.notify_if_mentioned(local_shareable)
Rails.logger.info("event=receive payload_type=#{self.class} update=true status=complete sender=#{self.diaspora_handle}") #existing_shareable=#{local_shareable.id}")
true
end
end
def receive_non_persisted(user, person)
if self.save
user.contact_for(person).receive_shareable(self)
user.notify_if_mentioned(self)
Rails.logger.info("event=receive payload_type=#{self.class} update=false status=complete sender=#{self.diaspora_handle}")
true
else
Rails.logger.info("event=receive payload_type=#{self.class} update=false status=abort sender=#{self.diaspora_handle} reason=#{self.errors.full_messages}")
false
end
end
end end
end end

View file

@ -1,38 +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 Webhooks
require 'builder/xchar'
def to_diaspora_xml
<<XML
<XML>
<post>#{to_xml.to_s}</post>
</XML>
XML
end
def x(input)
input.to_s.to_xs
end
# @abstract
# @note this must return [Array<Person>]
# @return [Array<Person>]
def subscribers(user)
raise 'You must override subscribers in order to enable federation on this model'
end
# @abstract
def receive(user, person)
raise 'You must override receive in order to enable federation on this model'
end
# @param [User] sender
# @note this is a hook
def after_dispatch sender
end
end
end

View file

@ -3,9 +3,7 @@ module Federated
self.abstract_class = true self.abstract_class = true
#crazy ordering issues - DEATH TO ROXML #crazy ordering issues - DEATH TO ROXML
include ROXML include Diaspora::Federated::Base
include Diaspora::Webhooks
include Diaspora::Guid include Diaspora::Guid
#seriously, don't try to move this shit around until you have killed ROXML #seriously, don't try to move this shit around until you have killed ROXML

View file

@ -25,7 +25,7 @@ class Postzord::Dispatcher
# @return [Postzord::Dispatcher] Public or private dispatcher depending on the object's intended audience # @return [Postzord::Dispatcher] Public or private dispatcher depending on the object's intended audience
def self.build(user, object, opts={}) def self.build(user, object, opts={})
unless object.respond_to? :to_diaspora_xml unless object.respond_to? :to_diaspora_xml
raise 'This object does not respond_to? to_diaspora xml. Try including Diaspora::Webhooks into your object' raise 'This object does not respond_to? to_diaspora xml. Try including Diaspora::Federated::Base into your object'
end end
if self.object_should_be_processed_as_public?(object) if self.object_should_be_processed_as_public?(object)

33
lib/postzord/presenter.rb Normal file
View file

@ -0,0 +1,33 @@
#dispatching
#a class that figures out the markup of an object
class Federated::Presenter
end
#a class that detects the audience of a post
class Federated::Audience
end
#this class dispatchs the post to services, like facebook, twitter, etc
class ServiceDispatcher
#interacts with a single post, and many provided services
end
#Receiving Phases
#receive request => check author
#xml payload
#decode
#convert to meta object
#perfom various validations
#recieve! <= save object, if applicable
#after_receive hook
Ideas:
Federated objects are delegated stubs of real objects, with converstion constuctors
- this turns receive to "make model level object from payload"
- seperate validations and checking with persistance and noticiation layer
http => deserliaization/decoding/descrypting => meta object => validations => receive => cleanup

View file

@ -47,12 +47,10 @@ describe UsersController do
end end
it 'redirects to a profile page if html is requested' do it 'redirects to a profile page if html is requested' do
Diaspora::OstatusBuilder.should_not_receive(:new)
get :public, :username => @user.username get :public, :username => @user.username
response.should be_redirect response.should be_redirect
end end
it 'redirects to a profile page if mobile is requested' do it 'redirects to a profile page if mobile is requested' do
Diaspora::OstatusBuilder.should_not_receive(:new)
get :public, :username => @user.username, :format => :mobile get :public, :username => @user.username, :format => :mobile
response.should be_redirect response.should be_redirect
end end

View file

@ -4,11 +4,11 @@
require 'spec_helper' require 'spec_helper'
describe Diaspora::Webhooks do describe Diaspora::Federated::Base do
describe '#subscribers' do describe '#subscribers' do
it 'throws an error if the including module does not redefine it' do it 'throws an error if the including module does not redefine it' do
class Foo class Foo
include Diaspora::Webhooks include Diaspora::Federated::Base
end end
f = Foo.new f = Foo.new

View file

@ -42,7 +42,7 @@ describe Postzord::Dispatcher do
it 'raises and gives you a helpful message if the object can not federate' do it 'raises and gives you a helpful message if the object can not federate' do
expect { expect {
Postzord::Dispatcher.build(alice, []) Postzord::Dispatcher.build(alice, [])
}.should raise_error /Diaspora::Webhooks/ }.should raise_error /Diaspora::Federated::Base/
end end
end end

View file

@ -1,14 +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 'spec_helper'
describe Diaspora::Webhooks do
it "should add the following methods to Post on inclusion" do
user = Factory.build(:user)
post = Factory.build(:status_message, :author => user.person)
post.respond_to?(:to_diaspora_xml).should be true
end
end