about summary refs log tree commit diff
path: root/app/services
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2020-11-08 00:28:39 +0100
committerGitHub <noreply@github.com>2020-11-08 00:28:39 +0100
commit3134691948aeacb16b7386ed77bbea4581beec40 (patch)
tree45ecf62f19879f08bf4c35584c58a64ea09c0c27 /app/services
parentee8cf246cfe8e05914ad7dcf81596f8535b3e161 (diff)
Add support for reversible suspensions through ActivityPub (#14989)
Diffstat (limited to 'app/services')
-rw-r--r--app/services/activitypub/process_account_service.rb55
-rw-r--r--app/services/activitypub/process_collection_service.rb10
-rw-r--r--app/services/block_domain_service.rb5
-rw-r--r--app/services/delete_account_service.rb40
-rw-r--r--app/services/resolve_account_service.rb27
-rw-r--r--app/services/suspend_account_service.rb29
-rw-r--r--app/services/unblock_domain_service.rb2
-rw-r--r--app/services/unsuspend_account_service.rb20
8 files changed, 148 insertions, 40 deletions
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index 9f95f1950..4cb8e09db 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -18,10 +18,11 @@ class ActivityPub::ProcessAccountService < BaseService
 
     RedisLock.acquire(lock_options) do |lock|
       if lock.acquired?
-        @account          = Account.remote.find_by(uri: @uri) if @options[:only_key]
-        @account        ||= Account.find_remote(@username, @domain)
-        @old_public_key   = @account&.public_key
-        @old_protocol     = @account&.protocol
+        @account            = Account.remote.find_by(uri: @uri) if @options[:only_key]
+        @account          ||= Account.find_remote(@username, @domain)
+        @old_public_key     = @account&.public_key
+        @old_protocol       = @account&.protocol
+        @suspension_changed = false
 
         create_account if @account.nil?
         update_account
@@ -37,8 +38,9 @@ class ActivityPub::ProcessAccountService < BaseService
     after_protocol_change! if protocol_changed?
     after_key_change! if key_changed? && !@options[:signed_with_known_key]
     clear_tombstones! if key_changed?
+    after_suspension_change! if suspension_changed?
 
-    unless @options[:only_key]
+    unless @options[:only_key] || @account.suspended?
       check_featured_collection! if @account.featured_collection_url.present?
       check_links! unless @account.fields.empty?
     end
@@ -52,20 +54,23 @@ class ActivityPub::ProcessAccountService < BaseService
 
   def create_account
     @account = Account.new
-    @account.protocol     = :activitypub
-    @account.username     = @username
-    @account.domain       = @domain
-    @account.private_key  = nil
-    @account.suspended_at = domain_block.created_at if auto_suspend?
-    @account.silenced_at  = domain_block.created_at if auto_silence?
+    @account.protocol          = :activitypub
+    @account.username          = @username
+    @account.domain            = @domain
+    @account.private_key       = nil
+    @account.suspended_at      = domain_block.created_at if auto_suspend?
+    @account.suspension_origin = :local if auto_suspend?
+    @account.silenced_at       = domain_block.created_at if auto_silence?
+    @account.save
   end
 
   def update_account
     @account.last_webfingered_at = Time.now.utc unless @options[:only_key]
     @account.protocol            = :activitypub
 
-    set_immediate_attributes!
-    set_fetchable_attributes! unless @options[:only_keys]
+    set_suspension!
+    set_immediate_attributes! unless @account.suspended?
+    set_fetchable_attributes! unless @options[:only_keys] || @account.suspended?
 
     @account.save_with_optional_media!
   end
@@ -99,6 +104,18 @@ class ActivityPub::ProcessAccountService < BaseService
     @account.moved_to_account  = @json['movedTo'].present? ? moved_account : nil
   end
 
+  def set_suspension!
+    return if @account.suspended? && @account.suspension_origin_local?
+
+    if @account.suspended? && !@json['suspended']
+      @account.unsuspend!
+      @suspension_changed = true
+    elsif !@account.suspended? && @json['suspended']
+      @account.suspend!(origin: :remote)
+      @suspension_changed = true
+    end
+  end
+
   def after_protocol_change!
     ActivityPub::PostUpgradeWorker.perform_async(@account.domain)
   end
@@ -107,6 +124,14 @@ class ActivityPub::ProcessAccountService < BaseService
     RefollowWorker.perform_async(@account.id)
   end
 
+  def after_suspension_change!
+    if @account.suspended?
+      Admin::SuspensionWorker.perform_async(@account.id)
+    else
+      Admin::UnsuspensionWorker.perform_async(@account.id)
+    end
+  end
+
   def check_featured_collection!
     ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id)
   end
@@ -227,6 +252,10 @@ class ActivityPub::ProcessAccountService < BaseService
     !@old_public_key.nil? && @old_public_key != @account.public_key
   end
 
+  def suspension_changed?
+    @suspension_changed
+  end
+
   def clear_tombstones!
     Tombstone.where(account_id: @account.id).delete_all
   end
diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb
index e6ccaccc9..f1d175dac 100644
--- a/app/services/activitypub/process_collection_service.rb
+++ b/app/services/activitypub/process_collection_service.rb
@@ -8,7 +8,7 @@ class ActivityPub::ProcessCollectionService < BaseService
     @json    = Oj.load(body, mode: :strict)
     @options = options
 
-    return if !supported_context? || (different_actor? && verify_account!.nil?) || @account.suspended? || @account.local?
+    return if !supported_context? || (different_actor? && verify_account!.nil?) || suspended_actor? || @account.local?
 
     case @json['type']
     when 'Collection', 'CollectionPage'
@@ -28,6 +28,14 @@ class ActivityPub::ProcessCollectionService < BaseService
     @json['actor'].present? && value_or_id(@json['actor']) != @account.uri
   end
 
+  def suspended_actor?
+    @account.suspended? && !activity_allowed_while_suspended?
+  end
+
+  def activity_allowed_while_suspended?
+    %w(Delete Reject Undo Update).include?(@json['type'])
+  end
+
   def process_items(items)
     items.reverse_each.map { |item| process_item(item) }.compact
   end
diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb
index 1cf3382b3..76cc36ff6 100644
--- a/app/services/block_domain_service.rb
+++ b/app/services/block_domain_service.rb
@@ -16,7 +16,7 @@ class BlockDomainService < BaseService
     scope = Account.by_domain_and_subdomains(domain_block.domain)
 
     scope.where(silenced_at: domain_block.created_at).in_batches.update_all(silenced_at: nil) unless domain_block.silence?
-    scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil) unless domain_block.suspend?
+    scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil, suspension_origin: nil) unless domain_block.suspend?
   end
 
   def process_domain_block!
@@ -34,7 +34,8 @@ class BlockDomainService < BaseService
   end
 
   def suspend_accounts!
-    blocked_domain_accounts.without_suspended.in_batches.update_all(suspended_at: @domain_block.created_at)
+    blocked_domain_accounts.without_suspended.in_batches.update_all(suspended_at: @domain_block.created_at, suspension_origin: :local)
+
     blocked_domain_accounts.where(suspended_at: @domain_block.created_at).reorder(nil).find_each do |account|
       DeleteAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at)
     end
diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb
index 15bdd13e3..de6488c78 100644
--- a/app/services/delete_account_service.rb
+++ b/app/services/delete_account_service.rb
@@ -64,8 +64,15 @@ class DeleteAccountService < BaseService
   def reject_follows!
     return if @account.local? || !@account.activitypub?
 
+    # When deleting a remote account, the account obviously doesn't
+    # actually become deleted on its origin server, i.e. unlike a
+    # locally deleted account it continues to have access to its home
+    # feed and other content. To prevent it from being able to continue
+    # to access toots it would receive because it follows local accounts,
+    # we have to force it to unfollow them.
+
     ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
-      [build_reject_json(follow), follow.target_account_id, follow.account.inbox_url]
+      [Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url]
     end
   end
 
@@ -114,19 +121,20 @@ class DeleteAccountService < BaseService
 
     return unless @options[:reserve_username]
 
-    @account.silenced_at      = nil
-    @account.suspended_at     = @options[:suspended_at] || Time.now.utc
-    @account.locked           = false
-    @account.memorial         = false
-    @account.discoverable     = false
-    @account.display_name     = ''
-    @account.note             = ''
-    @account.fields           = []
-    @account.statuses_count   = 0
-    @account.followers_count  = 0
-    @account.following_count  = 0
-    @account.moved_to_account = nil
-    @account.trust_level      = :untrusted
+    @account.silenced_at       = nil
+    @account.suspended_at      = @options[:suspended_at] || Time.now.utc
+    @account.suspension_origin = :local
+    @account.locked            = false
+    @account.memorial          = false
+    @account.discoverable      = false
+    @account.display_name      = ''
+    @account.note              = ''
+    @account.fields            = []
+    @account.statuses_count    = 0
+    @account.followers_count   = 0
+    @account.following_count   = 0
+    @account.moved_to_account  = nil
+    @account.trust_level       = :untrusted
     @account.avatar.destroy
     @account.header.destroy
     @account.save!
@@ -154,10 +162,6 @@ class DeleteAccountService < BaseService
     @delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account))
   end
 
-  def build_reject_json(follow)
-    Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
-  end
-
   def delivery_inboxes
     @delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url)
   end
diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb
index 3f7bb7cc5..4783e6d33 100644
--- a/app/services/resolve_account_service.rb
+++ b/app/services/resolve_account_service.rb
@@ -5,8 +5,6 @@ class ResolveAccountService < BaseService
   include DomainControlHelper
   include WebfingerHelper
 
-  class WebfingerRedirectError < StandardError; end
-
   # Find or create an account record for a remote user. When creating,
   # look up the user's webfinger and fetch ActivityPub data
   # @param [String, Account] uri URI in the username@domain format or account record
@@ -40,13 +38,18 @@ class ResolveAccountService < BaseService
 
     @account ||= Account.find_remote(@username, @domain)
 
-    return @account if @account&.local? || !webfinger_update_due?
+    if gone_from_origin? && not_yet_deleted?
+      queue_deletion!
+      return
+    end
+
+    return @account if @account&.local? || gone_from_origin? || !webfinger_update_due?
 
     # Now it is certain, it is definitely a remote account, and it
     # either needs to be created, or updated from fresh data
 
     process_account!
-  rescue Webfinger::Error, WebfingerRedirectError, Oj::ParseError => e
+  rescue Webfinger::Error, Oj::ParseError => e
     Rails.logger.debug "Webfinger query for #{@uri} failed: #{e}"
     nil
   end
@@ -86,10 +89,12 @@ class ResolveAccountService < BaseService
     elsif !redirected
       return process_webfinger!("#{confirmed_username}@#{confirmed_domain}", true)
     else
-      raise WebfingerRedirectError, "The URI #{uri} tries to hijack #{@username}@#{@domain}"
+      raise Webfinger::RedirectError, "The URI #{uri} tries to hijack #{@username}@#{@domain}"
     end
 
     @domain = nil if TagManager.instance.local_domain?(@domain)
+  rescue Webfinger::GoneError
+    @gone = true
   end
 
   def process_account!
@@ -131,6 +136,18 @@ class ResolveAccountService < BaseService
     @actor_json = supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) ? json : nil
   end
 
+  def gone_from_origin?
+    @gone
+  end
+
+  def not_yet_deleted?
+    @account.present? && !@account.local?
+  end
+
+  def queue_deletion!
+    AccountDeletionWorker.perform_async(@account.id, reserve_username: false)
+  end
+
   def lock_options
     { redis: Redis.current, key: "resolve:#{@username}@#{@domain}" }
   end
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index f08c41e17..d7f29963c 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -1,10 +1,14 @@
 # frozen_string_literal: true
 
 class SuspendAccountService < BaseService
+  include Payloadable
+
   def call(account)
     @account = account
 
     suspend!
+    reject_remote_follows!
+    distribute_update_actor!
     unmerge_from_home_timelines!
     unmerge_from_list_timelines!
     privatize_media_attachments!
@@ -16,6 +20,31 @@ class SuspendAccountService < BaseService
     @account.suspend! unless @account.suspended?
   end
 
+  def reject_remote_follows!
+    return if @account.local? || !@account.activitypub?
+
+    # When suspending a remote account, the account obviously doesn't
+    # actually become suspended on its origin server, i.e. unlike a
+    # locally suspended account it continues to have access to its home
+    # feed and other content. To prevent it from being able to continue
+    # to access toots it would receive because it follows local accounts,
+    # we have to force it to unfollow them. Unfortunately, there is no
+    # counterpart to this operation, i.e. you can't then force a remote
+    # account to re-follow you, so this part is not reversible.
+
+    follows = Follow.where(account: @account).to_a
+
+    ActivityPub::DeliveryWorker.push_bulk(follows) do |follow|
+      [Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url]
+    end
+
+    follows.in_batches.destroy_all
+  end
+
+  def distribute_update_actor!
+    ActivityPub::UpdateDistributionWorker.perform_async(@account.id) if @account.local?
+  end
+
   def unmerge_from_home_timelines!
     @account.followers_for_local_distribution.find_each do |follower|
       FeedManager.instance.unmerge_from_home(@account, follower)
diff --git a/app/services/unblock_domain_service.rb b/app/services/unblock_domain_service.rb
index d502d9e49..e765fb7a8 100644
--- a/app/services/unblock_domain_service.rb
+++ b/app/services/unblock_domain_service.rb
@@ -13,6 +13,6 @@ class UnblockDomainService < BaseService
     scope = Account.by_domain_and_subdomains(domain_block.domain)
 
     scope.where(silenced_at: domain_block.created_at).in_batches.update_all(silenced_at: nil) unless domain_block.noop?
-    scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil) if domain_block.suspend?
+    scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil, suspension_origin: nil) if domain_block.suspend?
   end
 end
diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb
index 91dbc9c18..a81d1ac4f 100644
--- a/app/services/unsuspend_account_service.rb
+++ b/app/services/unsuspend_account_service.rb
@@ -5,6 +5,10 @@ class UnsuspendAccountService < BaseService
     @account = account
 
     unsuspend!
+    refresh_remote_account!
+
+    return if @account.nil?
+
     merge_into_home_timelines!
     merge_into_list_timelines!
     publish_media_attachments!
@@ -16,6 +20,22 @@ class UnsuspendAccountService < BaseService
     @account.unsuspend! if @account.suspended?
   end
 
+  def refresh_remote_account!
+    return if @account.local?
+
+    # While we had the remote account suspended, it could be that
+    # it got suspended on its origin, too. So, we need to refresh
+    # it straight away so it gets marked as remotely suspended in
+    # that case.
+
+    @account.update!(last_webfingered_at: nil)
+    @account = ResolveAccountService.new.call(@account)
+
+    # Worth noting that it is possible that the remote has not only
+    # been suspended, but deleted permanently, in which case
+    # @account would now be nil.
+  end
+
   def merge_into_home_timelines!
     @account.followers_for_local_distribution.find_each do |follower|
       FeedManager.instance.merge_into_home(@account, follower)