Add support for reversible suspensions through ActivityPub (#14989)
This commit is contained in:
parent
ee8cf246cf
commit
3134691948
47 changed files with 1049 additions and 204 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue