diff --git a/Gemfile b/Gemfile index f0a77f7b6..ef4273993 100644 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,7 @@ gem 'grape' gem 'grape-route-helpers' gem 'grape-entity' gem 'hashie-forbidden_attributes' +gem 'paranoia', '~> 2.0' gem 'http' gem 'addressable' diff --git a/Gemfile.lock b/Gemfile.lock index fad87d191..434d72f42 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -152,6 +152,8 @@ GEM addressable (~> 2.4) http (~> 1.0) nokogiri (~> 1.6) + paranoia (2.1.5) + activerecord (~> 4.0) parser (2.3.0.6) ast (~> 2.2) pg (0.18.4) @@ -305,6 +307,7 @@ DEPENDENCIES nokogiri nyan-cat-formatter ostatus2 + paranoia (~> 2.0) pg pry-rails puma diff --git a/app/models/favourite.rb b/app/models/favourite.rb index 20260f46b..ad5380f43 100644 --- a/app/models/favourite.rb +++ b/app/models/favourite.rb @@ -2,7 +2,7 @@ class Favourite < ActiveRecord::Base belongs_to :account, inverse_of: :favourites belongs_to :status, inverse_of: :favourites - has_one :stream_entry, as: :activity + has_one :stream_entry, as: :activity, dependent: :destroy def verb :favorite diff --git a/app/models/follow.rb b/app/models/follow.rb index aa723d705..3521247e8 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -2,13 +2,13 @@ class Follow < ActiveRecord::Base belongs_to :account belongs_to :target_account, class_name: 'Account' - has_one :stream_entry, as: :activity + has_one :stream_entry, as: :activity, dependent: :destroy validates :account, :target_account, presence: true validates :account_id, uniqueness: { scope: :target_account_id } def verb - :follow + self.destroyed? ? :unfollow : :follow end def target @@ -20,7 +20,7 @@ class Follow < ActiveRecord::Base end def content - "#{self.account.acct} started following #{self.target_account.acct}" + self.destroyed? ? "#{self.account.acct} is no longer following #{self.target_account.acct}" : "#{self.account.acct} started following #{self.target_account.acct}" end def title diff --git a/app/models/status.rb b/app/models/status.rb index 72bf1b790..be616dce6 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -4,8 +4,11 @@ class Status < ActiveRecord::Base belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status' belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status' - has_one :stream_entry, as: :activity - has_many :favourites, inverse_of: :status + has_one :stream_entry, as: :activity, dependent: :destroy + + has_many :favourites, inverse_of: :status, dependent: :destroy + has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status' + has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status' validates :account, presence: true validates :uri, uniqueness: true, unless: 'local?' diff --git a/app/services/base_service.rb b/app/services/base_service.rb new file mode 100644 index 000000000..0816b3503 --- /dev/null +++ b/app/services/base_service.rb @@ -0,0 +1,3 @@ +class BaseService + include ApplicationHelper +end diff --git a/app/services/fetch_entry_service.rb b/app/services/fetch_entry_service.rb new file mode 100644 index 000000000..c4a5460e9 --- /dev/null +++ b/app/services/fetch_entry_service.rb @@ -0,0 +1,16 @@ +class FetchEntryService < BaseService + # Knowing nothing but the URL of a remote status, create a local representation of it and return it + # @param [String] url Atom URL + # @return [Status] + def call(url) + body = http_client.get(url) + xml = Nokogiri::XML(body) + # todo + end + + private + + def http_client + HTTP + end +end diff --git a/app/services/fetch_feed_service.rb b/app/services/fetch_feed_service.rb index 059d65925..f18e9fc06 100644 --- a/app/services/fetch_feed_service.rb +++ b/app/services/fetch_feed_service.rb @@ -1,4 +1,6 @@ -class FetchFeedService +class FetchFeedService < BaseService + # Fetch an account's feed and process it + # @param [Account] account def call(account) process_service.(http_client.get(account.remote_url), account) end @@ -6,7 +8,7 @@ class FetchFeedService private def process_service - ProcessFeedService.new + @process_service ||= ProcessFeedService.new end def http_client diff --git a/app/services/follow_remote_account_service.rb b/app/services/follow_remote_account_service.rb index bb55362a6..de2ca6e75 100644 --- a/app/services/follow_remote_account_service.rb +++ b/app/services/follow_remote_account_service.rb @@ -1,6 +1,10 @@ -class FollowRemoteAccountService - include ApplicationHelper - +class FollowRemoteAccountService < BaseService + # Find or create a local account for a remote user. + # When creating, look up the user's webfinger and fetch all + # important information from their feed + # @param [String] uri User URI in the form of username@domain + # @param [Boolean] subscribe Whether to initiate a PubSubHubbub subscription + # @return [Account] def call(uri, subscribe = true) username, domain = uri.split('@') account = Account.where(username: username, domain: domain).first diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index ea868b544..623d52b74 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -1,12 +1,23 @@ -class FollowService +class FollowService < BaseService + # Follow a remote user, notify remote user about the follow + # @param [Account] source_account From which to follow + # @param [String] uri User URI to follow in the form of username@domain def call(source_account, uri) target_account = follow_remote_account_service.(uri) - source_account.follow!(target_account) unless target_account.nil? + + return if target_account.nil? + + follow = source_account.follow!(target_account) + send_interaction_service.(follow.stream_entry, target_account) end private def follow_remote_account_service - FollowRemoteAccountService.new + @follow_remote_account_service ||= FollowRemoteAccountService.new + end + + def send_interaction_service + @send_interaction_service ||= SendInteractionService.new end end diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb index b7952035b..1aaf85d94 100644 --- a/app/services/process_feed_service.rb +++ b/app/services/process_feed_service.rb @@ -1,6 +1,7 @@ -class ProcessFeedService - include ApplicationHelper - +class ProcessFeedService < BaseService + # Create local statuses from an Atom feed + # @param [String] body Atom feed + # @param [Account] account Account this feed belongs to def call(body, account) xml = Nokogiri::XML(body) @@ -105,6 +106,6 @@ class ProcessFeedService end def follow_remote_account_service - FollowRemoteAccountService.new + @follow_remote_account_service ||= FollowRemoteAccountService.new end end diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb index ee04f01af..3a8332118 100644 --- a/app/services/process_interaction_service.rb +++ b/app/services/process_interaction_service.rb @@ -1,6 +1,7 @@ -class ProcessInteractionService - include ApplicationHelper - +class ProcessInteractionService < BaseService + # Record locally the remote interaction with our user + # @param [String] envelope Salmon envelope + # @param [Account] target_account Account the Salmon was addressed to def call(envelope, target_account) body = salmon.unpack(envelope) xml = Nokogiri::XML(body) @@ -75,14 +76,14 @@ class ProcessInteractionService end def salmon - OStatus2::Salmon.new + @salmon ||= OStatus2::Salmon.new end def follow_remote_account_service - FollowRemoteAccountService.new + @follow_remote_account_service ||= FollowRemoteAccountService.new end def process_feed_service - ProcessFeedService.new + @process_feed_service ||= ProcessFeedService.new end end diff --git a/app/services/send_interaction_service.rb b/app/services/send_interaction_service.rb new file mode 100644 index 000000000..42b273ed6 --- /dev/null +++ b/app/services/send_interaction_service.rb @@ -0,0 +1,29 @@ +class SendInteractionService < BaseService + include AtomHelper + + # Send an Atom representation of an interaction to a remote Salmon endpoint + # @param [StreamEntry] stream_entry + # @param [Account] target_account + def call(stream_entry, target_account) + envelope = salmon.pack(entry_xml(stream_entry), target_account.keypair) + salmon.post(target_account.salmon_url, envelope) + end + + private + + def entry_xml(stream_entry) + Nokogiri::XML::Builder.new do |xml| + entry(xml, true) do + author(xml) do + include_author xml, stream_entry.account + end + + include_entry xml, stream_entry + end + end.to_xml + end + + def salmon + @salmon ||= OStatus2::Salmon.new + end +end diff --git a/app/services/setup_local_account_service.rb b/app/services/setup_local_account_service.rb index c40e51855..a5ef68996 100644 --- a/app/services/setup_local_account_service.rb +++ b/app/services/setup_local_account_service.rb @@ -1,4 +1,8 @@ -class SetupLocalAccountService +class SetupLocalAccountService < BaseService + # Setup an account for a new user instance by generating + # an RSA key pair and a profile + # @param [User] user Unsaved user instance + # @param [String] username def call(user, username) user.build_account diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb new file mode 100644 index 000000000..039838d32 --- /dev/null +++ b/app/services/unfollow_service.rb @@ -0,0 +1,15 @@ +class UnfollowService < BaseService + # Unfollow and notify the remote user + # @param [Account] source_account Where to unfollow from + # @param [Account] target_account Which to unfollow + def call(source_account, target_account) + follow = source_account.unfollow!(target_account) + send_interaction_service.(follow.stream_entry, target_account) unless target_account.local? + end + + private + + def send_interaction_service + @send_interaction_service ||= SendInteractionService.new + end +end