103 lines
3.6 KiB
Ruby
103 lines
3.6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class ActivityPub::SynchronizeFollowersService < BaseService
|
|
include JsonLdHelper
|
|
include Payloadable
|
|
|
|
MAX_COLLECTION_PAGES = 10
|
|
|
|
def call(account, partial_collection_url)
|
|
@account = account
|
|
@expected_followers_ids = []
|
|
|
|
return unless process_collection!(partial_collection_url)
|
|
|
|
remove_unexpected_local_followers!
|
|
end
|
|
|
|
private
|
|
|
|
def process_page!(items)
|
|
page_expected_followers = extract_local_followers(items)
|
|
@expected_followers_ids.concat(page_expected_followers.pluck(:id))
|
|
|
|
handle_unexpected_outgoing_follows!(page_expected_followers)
|
|
end
|
|
|
|
def extract_local_followers(items)
|
|
# There could be unresolved accounts (hence the call to .filter_map) but this
|
|
# should never happen in practice, since in almost all cases we keep an
|
|
# Account record, and should we not do that, we should have sent a Delete.
|
|
# In any case there is not much we can do if that occurs.
|
|
|
|
# TODO: this will need changes when switching to numeric IDs
|
|
|
|
usernames = items.filter_map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username)&.downcase }
|
|
Account.local.with_username(usernames)
|
|
end
|
|
|
|
def remove_unexpected_local_followers!
|
|
@account.followers.local.where.not(id: @expected_followers_ids).reorder(nil).find_each do |unexpected_follower|
|
|
UnfollowService.new.call(unexpected_follower, @account)
|
|
end
|
|
end
|
|
|
|
def handle_unexpected_outgoing_follows!(expected_followers)
|
|
expected_followers.each do |expected_follower|
|
|
next if expected_follower.following?(@account)
|
|
|
|
if expected_follower.requested?(@account)
|
|
# For some reason the follow request went through but we missed it
|
|
expected_follower.follow_requests.find_by(target_account: @account)&.authorize!
|
|
else
|
|
# Since we were not aware of the follow from our side, we do not have an
|
|
# ID for it that we can include in the Undo activity. For this reason,
|
|
# the Undo may not work with software that relies exclusively on
|
|
# matching activity IDs and not the actor and target
|
|
follow = Follow.new(account: expected_follower, target_account: @account)
|
|
ActivityPub::DeliveryWorker.perform_async(build_undo_follow_json(follow), follow.account_id, follow.target_account.inbox_url)
|
|
end
|
|
end
|
|
end
|
|
|
|
def build_undo_follow_json(follow)
|
|
Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer))
|
|
end
|
|
|
|
# Only returns true if the whole collection has been processed
|
|
def process_collection!(collection_uri, max_pages: MAX_COLLECTION_PAGES)
|
|
collection = fetch_collection(collection_uri)
|
|
return false unless collection.is_a?(Hash)
|
|
|
|
collection = fetch_collection(collection['first']) if collection['first'].present?
|
|
|
|
while collection.is_a?(Hash)
|
|
process_page!(as_array(collection_page_items(collection)))
|
|
|
|
max_pages -= 1
|
|
|
|
return true if collection['next'].blank? # We reached the end of the collection
|
|
return false if max_pages <= 0 # We reached our pages limit
|
|
|
|
collection = fetch_collection(collection['next'])
|
|
end
|
|
|
|
false
|
|
end
|
|
|
|
def collection_page_items(collection)
|
|
case collection['type']
|
|
when 'Collection', 'CollectionPage'
|
|
collection['items']
|
|
when 'OrderedCollection', 'OrderedCollectionPage'
|
|
collection['orderedItems']
|
|
end
|
|
end
|
|
|
|
def fetch_collection(collection_or_uri)
|
|
return collection_or_uri if collection_or_uri.is_a?(Hash)
|
|
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
|
|
|
|
fetch_resource_without_id_validation(collection_or_uri, nil, true)
|
|
end
|
|
end
|