diff --git a/app/controllers/api/v1_alpha/collections_controller.rb b/app/controllers/api/v1_alpha/collections_controller.rb index 792a072d32..feea6c6b32 100644 --- a/app/controllers/api/v1_alpha/collections_controller.rb +++ b/app/controllers/api/v1_alpha/collections_controller.rb @@ -51,7 +51,7 @@ class Api::V1Alpha::CollectionsController < Api::BaseController def update authorize @collection, :update? - @collection.update!(collection_update_params) # TODO: Create a service for this to federate changes + UpdateCollectionService.new.call(@collection, collection_update_params) render json: @collection, serializer: REST::CollectionSerializer, adapter: :json end diff --git a/app/serializers/activitypub/update_featured_collection_serializer.rb b/app/serializers/activitypub/update_featured_collection_serializer.rb new file mode 100644 index 0000000000..b349223ad6 --- /dev/null +++ b/app/serializers/activitypub/update_featured_collection_serializer.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class ActivityPub::UpdateFeaturedCollectionSerializer < ActivityPub::Serializer + attributes :id, :type, :actor, :to + + has_one :object, serializer: ActivityPub::FeaturedCollectionSerializer + + def id + [ActivityPub::TagManager.instance.uri_for(object), '#updates/', object.updated_at.to_i].join + end + + def type + 'Update' + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def to + [ActivityPub::TagManager::COLLECTIONS[:public]] + end +end diff --git a/app/services/update_collection_service.rb b/app/services/update_collection_service.rb new file mode 100644 index 0000000000..9dffac9e2b --- /dev/null +++ b/app/services/update_collection_service.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class UpdateCollectionService + UPDATEABLE_PARAMS = %w(name description language sensitive discoverable tag_id).freeze + + def call(collection, params) + @collection = collection + @collection.update!(params) + + distribute_update_activity if Mastodon::Feature.collections_federation_enabled? + end + + private + + def distribute_update_activity + return unless relevant_attributes_changed? + + ActivityPub::AccountRawDistributionWorker.perform_async(activity_json, @collection.account.id) + end + + def activity_json + ActiveModelSerializers::SerializableResource.new(@collection, serializer: ActivityPub::UpdateFeaturedCollectionSerializer, adapter: ActivityPub::Adapter).to_json + end + + def relevant_attributes_changed? + (@collection.saved_changes.keys & UPDATEABLE_PARAMS).any? + end +end diff --git a/spec/serializers/activitypub/update_featured_collection_serializer_spec.rb b/spec/serializers/activitypub/update_featured_collection_serializer_spec.rb new file mode 100644 index 0000000000..9f9255fea3 --- /dev/null +++ b/spec/serializers/activitypub/update_featured_collection_serializer_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::UpdateFeaturedCollectionSerializer do + subject { serialized_record_json(collection, described_class, adapter: ActivityPub::Adapter) } + + let(:tag_manager) { ActivityPub::TagManager.instance } + let(:collection) { Fabricate(:collection) } + + it 'serializes to the expected json' do + expect(subject).to include({ + 'id' => "#{tag_manager.uri_for(collection)}#updates/#{collection.updated_at.to_i}", + 'type' => 'Update', + 'actor' => tag_manager.uri_for(collection.account), + 'to' => ['https://www.w3.org/ns/activitystreams#Public'], + 'object' => a_hash_including({ + 'id' => tag_manager.uri_for(collection), + 'type' => 'FeaturedCollection', + }), + }) + + expect(subject).to_not have_key('published') + expect(subject).to_not have_key('cc') + expect(subject).to_not have_key('target') + end +end diff --git a/spec/services/update_collection_service_spec.rb b/spec/services/update_collection_service_spec.rb new file mode 100644 index 0000000000..b7b9547dbb --- /dev/null +++ b/spec/services/update_collection_service_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UpdateCollectionService do + subject { described_class.new } + + let(:collection) { Fabricate(:collection) } + + describe '#call' do + context 'when given valid parameters' do + it 'updates the collection' do + subject.call(collection, { name: 'Newly updated name' }) + + expect(collection.name).to eq 'Newly updated name' + end + + context 'when something actually changed' do + it 'federates an `Update` activity', feature: :collections_federation do + subject.call(collection, { name: 'updated' }) + + expect(ActivityPub::AccountRawDistributionWorker).to have_enqueued_sidekiq_job + end + end + + context 'when nothing changed' do + it 'does not federate an activity', feature: :collections_federation do + subject.call(collection, { name: collection.name }) + + expect(ActivityPub::AccountRawDistributionWorker).to_not have_enqueued_sidekiq_job + end + end + end + + context 'when given invalid parameters' do + it 'raises an exception' do + expect do + subject.call(collection, { name: '' }) + end.to raise_error(ActiveRecord::RecordInvalid) + end + end + end +end