diff --git a/app/models/collection_item.rb b/app/models/collection_item.rb index c5c9ebc16e9..e113b3b5227 100644 --- a/app/models/collection_item.rb +++ b/app/models/collection_item.rb @@ -42,6 +42,10 @@ class CollectionItem < ApplicationRecord scope :not_blocked_by, ->(account) { where.not(accounts: { id: account.blocking }) } scope :local, -> { joins(:collection).merge(Collection.local) } + def revoke! + update!(state: :revoked) + end + def local_item_with_remote_account? local? && account&.remote? end diff --git a/app/serializers/activitypub/delete_feature_authorization_serializer.rb b/app/serializers/activitypub/delete_feature_authorization_serializer.rb new file mode 100644 index 00000000000..eb9f39fb4b9 --- /dev/null +++ b/app/serializers/activitypub/delete_feature_authorization_serializer.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class ActivityPub::DeleteFeatureAuthorizationSerializer < ActivityPub::Serializer + include RoutingHelper + + attributes :id, :type, :actor, :to + + has_one :object, serializer: ActivityPub::FeatureAuthorizationSerializer + + def id + [ap_account_feature_authorization_url(object.account_id, object), '#delete'].join + end + + def type + 'Delete' + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def to + [ActivityPub::TagManager::COLLECTIONS[:public]] + end +end diff --git a/app/services/revoke_collection_item_service.rb b/app/services/revoke_collection_item_service.rb new file mode 100644 index 00000000000..d299b567f22 --- /dev/null +++ b/app/services/revoke_collection_item_service.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class RevokeCollectionItemService < BaseService + include Payloadable + + def call(collection_item) + @collection_item = collection_item + @account = collection_item.account + + @collection_item.revoke! + + distribute_stamp_deletion! if Mastodon::Feature.collections_federation_enabled? && @collection_item.remote? + end + + private + + def distribute_stamp_deletion! + ActivityPub::AccountRawDistributionWorker.perform_async(signed_activity_json, @collection_item.collection.account_id) + end + + def signed_activity_json + @signed_activity_json ||= Oj.dump(serialize_payload(@collection_item, ActivityPub::DeleteFeatureAuthorizationSerializer, signer: @account, always_sign: true)) + end +end diff --git a/spec/serializers/activitypub/delete_feature_authorization_serializer_spec.rb b/spec/serializers/activitypub/delete_feature_authorization_serializer_spec.rb new file mode 100644 index 00000000000..a1b2de79667 --- /dev/null +++ b/spec/serializers/activitypub/delete_feature_authorization_serializer_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::DeleteFeatureAuthorizationSerializer do + include RoutingHelper + + subject { serialized_record_json(collection_item, described_class, adapter: ActivityPub::Adapter) } + + describe 'serializing an object' do + let(:collection) { Fabricate(:remote_collection) } + let(:collection_item) { Fabricate(:collection_item, collection:, uri: 'https://example.com') } + + it 'returns expected json structure' do + expect(subject) + .to include({ + 'type' => 'Delete', + 'to' => ['https://www.w3.org/ns/activitystreams#Public'], + 'actor' => ActivityPub::TagManager.instance.uri_for(collection_item.account), + 'object' => a_hash_including({ + 'type' => 'FeatureAuthorization', + 'id' => ap_account_feature_authorization_url(collection_item.account_id, collection_item), + }), + }) + end + end +end diff --git a/spec/services/revoke_collection_item_service_spec.rb b/spec/services/revoke_collection_item_service_spec.rb new file mode 100644 index 00000000000..8ea753dcc5e --- /dev/null +++ b/spec/services/revoke_collection_item_service_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe RevokeCollectionItemService do + subject { described_class.new } + + let(:collection_item) { Fabricate(:collection_item) } + + it 'revokes the collection item and sends a Delete activity' do + expect { subject.call(collection_item) } + .to change { collection_item.reload.state }.from('accepted').to('revoked') + end + + context 'when the collection is remote', feature: :collections_federation do + let(:collection) { Fabricate(:remote_collection) } + let(:collection_item) { Fabricate(:collection_item, collection:, uri: 'https://example.com') } + + it 'federates a `Delete` activity' do + subject.call(collection_item) + + expect(ActivityPub::AccountRawDistributionWorker).to have_enqueued_sidekiq_job + end + end +end