From 1d4c2c56701e7e3ea6292d0b5fb1e559f8e71675 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Tue, 27 Jan 2026 11:52:54 +0100 Subject: [PATCH] Federate creation of collections (#37618) --- app/serializers/activitypub/add_serializer.rb | 20 ++++++++++--- app/services/create_collection_service.rb | 11 ++++++++ .../activitypub/add_serializer_spec.rb | 28 +++++++++++++++++++ .../create_collection_service_spec.rb | 6 ++++ 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/app/serializers/activitypub/add_serializer.rb b/app/serializers/activitypub/add_serializer.rb index 640d774272..a2a1256ec5 100644 --- a/app/serializers/activitypub/add_serializer.rb +++ b/app/serializers/activitypub/add_serializer.rb @@ -10,11 +10,13 @@ class ActivityPub::AddSerializer < ActivityPub::Serializer end def self.serializer_for(model, options) - case model.class.name - when 'Status' + case model + when Status UriSerializer - when 'FeaturedTag' + when FeaturedTag ActivityPub::HashtagSerializer + when Collection + ActivityPub::FeaturedCollectionSerializer else super end @@ -38,6 +40,16 @@ class ActivityPub::AddSerializer < ActivityPub::Serializer end def target - ActivityPub::TagManager.instance.collection_uri_for(object.account, :featured) + case object + when Status, FeaturedTag + # Technically this is not correct, as tags have their own collection. + # But sadly we do not store the collection URI for tags anywhere so cannot + # handle `Add` activities to that properly (yet). The receiving code for + # this currently looks at the type of the contained objects to do the + # right thing. + ActivityPub::TagManager.instance.collection_uri_for(object.account, :featured) + when Collection + ap_account_featured_collections_url(object.account_id) + end end end diff --git a/app/services/create_collection_service.rb b/app/services/create_collection_service.rb index 10843cb967..008d4b5c09 100644 --- a/app/services/create_collection_service.rb +++ b/app/services/create_collection_service.rb @@ -8,11 +8,18 @@ class CreateCollectionService build_items @collection.save! + + distribute_add_activity if Mastodon::Feature.collections_federation_enabled? + @collection end private + def distribute_add_activity + ActivityPub::AccountRawDistributionWorker.perform_async(activity_json, @account.id) + end + def build_items return if @accounts_to_add.empty? @@ -23,4 +30,8 @@ class CreateCollectionService @collection.collection_items.build(account: account_to_add) end end + + def activity_json + ActiveModelSerializers::SerializableResource.new(@collection, serializer: ActivityPub::AddSerializer, adapter: ActivityPub::Adapter).to_json + end end diff --git a/spec/serializers/activitypub/add_serializer_spec.rb b/spec/serializers/activitypub/add_serializer_spec.rb index 3b3eaeb1b0..a5e1052fa0 100644 --- a/spec/serializers/activitypub/add_serializer_spec.rb +++ b/spec/serializers/activitypub/add_serializer_spec.rb @@ -18,10 +18,38 @@ RSpec.describe ActivityPub::AddSerializer do it { is_expected.to eq(ActivityPub::HashtagSerializer) } end + context 'with a Collection model' do + let(:model) { Collection.new } + + it { is_expected.to eq(ActivityPub::FeaturedCollectionSerializer) } + end + context 'with an Array' do let(:model) { [] } it { is_expected.to eq(ActiveModel::Serializer::CollectionSerializer) } end end + + describe '#target' do + subject { described_class.new(object).target } + + context 'when object is a Status' do + let(:object) { Fabricate(:status) } + + it { is_expected.to match(%r{/#{object.account_id}/collections/featured$}) } + end + + context 'when object is a FeaturedTag' do + let(:object) { Fabricate(:featured_tag) } + + it { is_expected.to match(%r{/#{object.account_id}/collections/featured$}) } + end + + context 'when object is a Collection' do + let(:object) { Fabricate(:collection) } + + it { is_expected.to match(%r{/#{object.account_id}/featured_collections$}) } + end + end end diff --git a/spec/services/create_collection_service_spec.rb b/spec/services/create_collection_service_spec.rb index f88a366a6c..8189b01fbe 100644 --- a/spec/services/create_collection_service_spec.rb +++ b/spec/services/create_collection_service_spec.rb @@ -29,6 +29,12 @@ RSpec.describe CreateCollectionService do expect(collection).to be_local end + it 'federates an `Add` activity', feature: :collections_federation do + subject.call(base_params, author) + + expect(ActivityPub::AccountRawDistributionWorker).to have_enqueued_sidekiq_job + end + context 'when given account ids' do let(:accounts) do Fabricate.times(2, :account)