diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb index 07a9fe0395..e62470f707 100644 --- a/app/services/activitypub/fetch_featured_collection_service.rb +++ b/app/services/activitypub/fetch_featured_collection_service.rb @@ -7,19 +7,33 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService return if account.featured_collection_url.blank? || account.suspended? || account.local? @account = account - @json = fetch_resource(@account.featured_collection_url, true) + @json = fetch_resource(@account.featured_collection_url, true, local_follower) - return unless supported_context? + return unless supported_context?(@json) - case @json['type'] + process_items(collection_items(@json)) + end + + private + + def collection_items(collection) + collection = fetch_collection(collection['first']) if collection['first'].present? + return unless collection.is_a?(Hash) + + case collection['type'] when 'Collection', 'CollectionPage' - process_items @json['items'] + collection['items'] when 'OrderedCollection', 'OrderedCollectionPage' - process_items @json['orderedItems'] + collection['orderedItems'] end end - private + def fetch_collection(collection_or_uri) + return collection_or_uri if collection_or_uri.is_a?(Hash) + return if invalid_origin?(collection_or_uri) + + fetch_resource_without_id_validation(collection_or_uri, nil, true, local_follower) + end def process_items(items) status_ids = items.filter_map do |item| @@ -53,11 +67,9 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService end end - def supported_context? - super(@json) - end - def local_follower - @local_follower ||= @account.followers.local.without_suspended.first + return @local_follower if defined?(@local_follower) + + @local_follower = @account.followers.local.without_suspended.first end end diff --git a/spec/services/activitypub/fetch_featured_collection_service_spec.rb b/spec/services/activitypub/fetch_featured_collection_service_spec.rb new file mode 100644 index 0000000000..f552b9dc07 --- /dev/null +++ b/spec/services/activitypub/fetch_featured_collection_service_spec.rb @@ -0,0 +1,123 @@ +require 'rails_helper' + +RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do + let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/account', featured_collection_url: 'https://example.com/account/pinned') } + + let!(:known_status) { Fabricate(:status, account: actor, uri: 'https://example.com/account/pinned/1') } + + let(:status_json_1) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Note', + id: 'https://example.com/account/pinned/1', + content: 'foo', + attributedTo: actor.uri, + to: 'https://www.w3.org/ns/activitystreams#Public', + } + end + + let(:status_json_2) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Note', + id: 'https://example.com/account/pinned/2', + content: 'foo', + attributedTo: actor.uri, + to: 'https://www.w3.org/ns/activitystreams#Public', + } + end + + let(:status_json_4) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Note', + id: 'https://example.com/account/pinned/4', + content: 'foo', + attributedTo: actor.uri, + to: 'https://www.w3.org/ns/activitystreams#Public', + } + end + + let(:items) do + [ + 'https://example.com/account/pinned/1', # known + status_json_2, # unknown inlined + 'https://example.com/account/pinned/3', # unknown unreachable + 'https://example.com/account/pinned/4', # unknown reachable + ] + end + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: actor.featured_collection_url, + items: items, + }.with_indifferent_access + end + + subject { described_class.new } + + shared_examples 'sets pinned posts' do + before do + stub_request(:get, 'https://example.com/account/pinned/1').to_return(status: 200, body: Oj.dump(status_json_1)) + stub_request(:get, 'https://example.com/account/pinned/2').to_return(status: 200, body: Oj.dump(status_json_2)) + stub_request(:get, 'https://example.com/account/pinned/3').to_return(status: 404) + stub_request(:get, 'https://example.com/account/pinned/4').to_return(status: 200, body: Oj.dump(status_json_4)) + + subject.call(actor) + end + + it 'sets expected posts as pinned posts' do + expect(actor.pinned_statuses.pluck(:uri)).to match_array ['https://example.com/account/pinned/1', 'https://example.com/account/pinned/2', 'https://example.com/account/pinned/4'] + end + end + + describe '#call' do + context 'when the endpoint is a Collection' do + before do + stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload)) + end + + it_behaves_like 'sets pinned posts' + end + + context 'when the endpoint is an OrderedCollection' do + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'OrderedCollection', + id: actor.featured_collection_url, + orderedItems: items, + }.with_indifferent_access + end + + before do + stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload)) + end + + it_behaves_like 'sets pinned posts' + end + + context 'when the endpoint is a paginated Collection' do + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: actor.featured_collection_url, + first: { + type: 'CollectionPage', + partOf: actor.featured_collection_url, + items: items, + } + }.with_indifferent_access + end + + before do + stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload)) + end + + it_behaves_like 'sets pinned posts' + end + end +end