From 5f36c482d285f240d29ddb888790bdb20adc1be7 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Tue, 17 Mar 2026 12:45:00 +0100 Subject: [PATCH] Handle `Accept` of a `FeatureRequest` (#38251) --- app/lib/activitypub/activity/accept.rb | 18 ++++++ spec/lib/activitypub/activity/accept_spec.rb | 66 ++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/app/lib/activitypub/activity/accept.rb b/app/lib/activitypub/activity/accept.rb index 92a8190c03..45d761f3ce 100644 --- a/app/lib/activitypub/activity/accept.rb +++ b/app/lib/activitypub/activity/accept.rb @@ -5,6 +5,7 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity return accept_follow_for_relay if relay_follow? return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil? return accept_quote!(quote_request_from_object) unless quote_request_from_object.nil? + return accept_feature_request! if Mastodon::Feature.collections_federation_enabled? && feature_request_from_object.present? case @object['type'] when 'Follow' @@ -44,6 +45,17 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity accept_quote!(quote) end + def accept_feature_request! + approval_uri = value_or_id(first_of_value(@json['result'])) + return if approval_uri.nil? || unsupported_uri_scheme?(approval_uri) || non_matching_uri_hosts?(approval_uri, @account.uri) + + collection_item = feature_request_from_object + collection_item.update!(approval_uri:, state: :accepted) + + activity_json = ActiveModelSerializers::SerializableResource.new(collection_item, serializer: ActivityPub::AddFeaturedItemSerializer, adapter: ActivityPub::Adapter).to_json + ActivityPub::AccountRawDistributionWorker.perform_async(activity_json, collection_item.collection.account_id) + end + def accept_quote!(quote) approval_uri = value_or_id(first_of_value(@json['result'])) return if unsupported_uri_scheme?(approval_uri) || quote.quoted_account != @account || !quote.status.local? || !quote.pending? @@ -72,4 +84,10 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity def target_uri @target_uri ||= value_or_id(@object['actor']) end + + def feature_request_from_object + return @collection_item if instance_variable_defined?(:@collection_item) + + @collection_item = CollectionItem.local.find_by(activity_uri: value_or_id(@object), account_id: @account.id) + end end diff --git a/spec/lib/activitypub/activity/accept_spec.rb b/spec/lib/activitypub/activity/accept_spec.rb index 615287389c..f7c1e1d617 100644 --- a/spec/lib/activitypub/activity/accept_spec.rb +++ b/spec/lib/activitypub/activity/accept_spec.rb @@ -171,5 +171,71 @@ RSpec.describe ActivityPub::Activity::Accept do end end end + + context 'with a FeatureRequest', feature: :collections_federation do + let(:collection) { Fabricate(:collection, account: recipient) } + let(:collection_item) { Fabricate(:collection_item, collection:, account: sender, state: :pending) } + let(:object) { collection_item.activity_uri } + let(:approval_uri) { 'https://example.com/stamps/1' } + let(:json) do + { + 'id' => 'https://example.com/accepts/1', + 'type' => 'Accept', + 'actor' => sender.uri, + 'to' => ActivityPub::TagManager.instance.uri_for(recipient), + 'object' => object, + 'result' => approval_uri, + } + end + + context 'when activity is valid' do + it 'accepts the collection item, stores the authorization uri and federates an `Add` activity' do + subject.perform + + expect(collection_item.reload).to be_accepted + expect(collection_item.approval_uri).to eq 'https://example.com/stamps/1' + expect(ActivityPub::AccountRawDistributionWorker) + .to have_enqueued_sidekiq_job + end + end + + context 'when activity is invalid' do + shared_examples 'ignoring activity' do + it 'does not accept the item and does not send out an activity' do + subject.perform + + expect(collection_item.reload).to_not be_accepted + expect(collection_item.approval_uri).to be_nil + expect(ActivityPub::AccountRawDistributionWorker) + .to_not have_enqueued_sidekiq_job + end + end + + context 'when matching collection item cannot be found' do + let(:object) { 'https://localhost/feature_requests/1' } + + it_behaves_like 'ignoring activity' + end + + context 'when the sender is not the featured account' do + let(:other_account) { Fabricate(:remote_account) } + let(:collection_item) { Fabricate(:collection_item, collection:, account: other_account, state: :pending) } + + it_behaves_like 'ignoring activity' + end + + context "when approval_uri does not match the sender's uri" do + let(:approval_uri) { 'https://other.localhost/authorizations/1' } + + it_behaves_like 'ignoring activity' + end + + context 'when approval_uri is missing' do + let(:approval_uri) { nil } + + it_behaves_like 'ignoring activity' + end + end + end end end