From 3e1127d27bb7afca134c6cb471253957256a3bd8 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 11 Feb 2026 14:52:29 +0100 Subject: [PATCH] Federate `Add` when item is added to Collection (#37823) --- .../add_featured_item_serializer.rb | 26 ++++++++++++++++++ .../featured_collection_serializer.rb | 18 +------------ .../activitypub/featured_item_serializer.rb | 17 ++++++++++++ .../add_account_to_collection_service.rb | 14 +++++++++- .../add_featured_item_serializer_spec.rb | 27 +++++++++++++++++++ .../add_account_to_collection_service_spec.rb | 6 +++++ 6 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 app/serializers/activitypub/add_featured_item_serializer.rb create mode 100644 app/serializers/activitypub/featured_item_serializer.rb create mode 100644 spec/serializers/activitypub/add_featured_item_serializer_spec.rb diff --git a/app/serializers/activitypub/add_featured_item_serializer.rb b/app/serializers/activitypub/add_featured_item_serializer.rb new file mode 100644 index 0000000000..2962d782ed --- /dev/null +++ b/app/serializers/activitypub/add_featured_item_serializer.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class ActivityPub::AddFeaturedItemSerializer < ActivityPub::Serializer + include RoutingHelper + + attributes :type, :actor, :target + has_one :object, serializer: ActivityPub::FeaturedItemSerializer + + def type + 'Add' + end + + def actor + ActivityPub::TagManager.instance.uri_for(collection.account) + end + + def target + ActivityPub::TagManager.instance.uri_for(collection) + end + + private + + def collection + @collection ||= object.collection + end +end diff --git a/app/serializers/activitypub/featured_collection_serializer.rb b/app/serializers/activitypub/featured_collection_serializer.rb index af4c554851..d6242b9d5b 100644 --- a/app/serializers/activitypub/featured_collection_serializer.rb +++ b/app/serializers/activitypub/featured_collection_serializer.rb @@ -1,22 +1,6 @@ # frozen_string_literal: true class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer - class FeaturedItemSerializer < ActivityPub::Serializer - attributes :type, :featured_object, :featured_object_type - - def type - 'FeaturedItem' - end - - def featured_object - ActivityPub::TagManager.instance.uri_for(object.account) - end - - def featured_object_type - object.account.actor_type || 'Person' - end - end - attributes :id, :type, :total_items, :name, :attributed_to, :sensitive, :discoverable, :published, :updated @@ -25,7 +9,7 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer has_one :tag, key: :topic, serializer: ActivityPub::NoteSerializer::TagSerializer - has_many :collection_items, key: :ordered_items, serializer: FeaturedItemSerializer + has_many :collection_items, key: :ordered_items, serializer: ActivityPub::FeaturedItemSerializer def id ActivityPub::TagManager.instance.uri_for(object) diff --git a/app/serializers/activitypub/featured_item_serializer.rb b/app/serializers/activitypub/featured_item_serializer.rb new file mode 100644 index 0000000000..87db67d95b --- /dev/null +++ b/app/serializers/activitypub/featured_item_serializer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class ActivityPub::FeaturedItemSerializer < ActivityPub::Serializer + attributes :type, :featured_object, :featured_object_type + + def type + 'FeaturedItem' + end + + def featured_object + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def featured_object_type + object.account.actor_type || 'Person' + end +end diff --git a/app/services/add_account_to_collection_service.rb b/app/services/add_account_to_collection_service.rb index 4477384dd0..2109baf67e 100644 --- a/app/services/add_account_to_collection_service.rb +++ b/app/services/add_account_to_collection_service.rb @@ -9,7 +9,11 @@ class AddAccountToCollectionService raise Mastodon::NotPermittedError, I18n.t('accounts.errors.cannot_be_added_to_collections') unless AccountPolicy.new(@collection.account, @account).feature? - create_collection_item + @collection_item = create_collection_item + + distribute_add_activity if @account.local? && Mastodon::Feature.collections_federation_enabled? + + @collection_item end private @@ -20,4 +24,12 @@ class AddAccountToCollectionService state: :accepted ) end + + def distribute_add_activity + ActivityPub::AccountRawDistributionWorker.perform_async(activity_json, @collection.account_id) + end + + def activity_json + ActiveModelSerializers::SerializableResource.new(@collection_item, serializer: ActivityPub::AddFeaturedItemSerializer, adapter: ActivityPub::Adapter).to_json + end end diff --git a/spec/serializers/activitypub/add_featured_item_serializer_spec.rb b/spec/serializers/activitypub/add_featured_item_serializer_spec.rb new file mode 100644 index 0000000000..57bece6273 --- /dev/null +++ b/spec/serializers/activitypub/add_featured_item_serializer_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::AddFeaturedItemSerializer do + subject { serialized_record_json(object, described_class, adapter: ActivityPub::Adapter) } + + let(:tag_manager) { ActivityPub::TagManager.instance } + let(:collection) { Fabricate(:collection) } + let(:object) { Fabricate(:collection_item, collection:) } + + it 'serializes to the expected json' do + expect(subject).to include({ + 'type' => 'Add', + 'actor' => tag_manager.uri_for(collection.account), + 'target' => tag_manager.uri_for(collection), + 'object' => a_hash_including({ + 'type' => 'FeaturedItem', + }), + }) + + expect(subject).to_not have_key('id') + expect(subject).to_not have_key('published') + expect(subject).to_not have_key('to') + expect(subject).to_not have_key('cc') + end +end diff --git a/spec/services/add_account_to_collection_service_spec.rb b/spec/services/add_account_to_collection_service_spec.rb index 98d88d0b8f..3085c597a7 100644 --- a/spec/services/add_account_to_collection_service_spec.rb +++ b/spec/services/add_account_to_collection_service_spec.rb @@ -20,6 +20,12 @@ RSpec.describe AddAccountToCollectionService do expect(new_item.state).to eq 'accepted' expect(new_item.account).to eq account end + + it 'federates an `Add` activity', feature: :collections_federation do + subject.call(collection, account) + + expect(ActivityPub::AccountRawDistributionWorker).to have_enqueued_sidekiq_job + end end context 'when given an account that is not featureable' do