diff --git a/app/lib/activitypub/activity/quote_request.rb b/app/lib/activitypub/activity/quote_request.rb index 2de03df1580..40a38b09d36 100644 --- a/app/lib/activitypub/activity/quote_request.rb +++ b/app/lib/activitypub/activity/quote_request.rb @@ -9,12 +9,31 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity quoted_status = status_from_uri(object_uri) return if quoted_status.nil? || !quoted_status.account.local? || !quoted_status.distributable? - # For now, we don't support being quoted by external servers - reject_quote_request!(quoted_status) + if Mastodon::Feature.outgoing_quotes_enabled? && StatusPolicy.new(@account, quoted_status).quote? + accept_quote_request!(quoted_status) + else + reject_quote_request!(quoted_status) + end end private + def accept_quote_request!(quoted_status) + status = status_from_uri(@json['instrument']) + # TODO: import inlined quote post if possible + status ||= ActivityPub::FetchRemoteStatusService.new.call(@json['instrument'], on_behalf_of: @account.followers.local.first, request_id: @options[:request_id]) + # TODO: raise if status is nil + + # Sanity check + return unless status.quote.quoted_status == quoted_status + + status.quote.update!(activity_uri: @json['id']) + status.quote.accept! + + json = Oj.dump(serialize_payload(status.quote, ActivityPub::AcceptQuoteRequestSerializer)) + ActivityPub::DeliveryWorker.perform_async(json, quoted_status.account_id, @account.inbox_url) + end + def reject_quote_request!(quoted_status) quote = Quote.new( quoted_status: quoted_status, diff --git a/app/serializers/activitypub/accept_quote_request_serializer.rb b/app/serializers/activitypub/accept_quote_request_serializer.rb new file mode 100644 index 00000000000..26ecd099b64 --- /dev/null +++ b/app/serializers/activitypub/accept_quote_request_serializer.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class ActivityPub::AcceptQuoteRequestSerializer < ActivityPub::Serializer + attributes :id, :type, :actor, :result + + has_one :object, serializer: ActivityPub::QuoteRequestSerializer + + def id + [ActivityPub::TagManager.instance.uri_for(object.quoted_account), '#accepts/quote_requests/', object.id].join + end + + def type + 'Accept' + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.quoted_account) + end + + def result + ActivityPub::TagManager.instance.approval_uri_for(object) + end +end diff --git a/app/serializers/activitypub/quote_request_serializer.rb b/app/serializers/activitypub/quote_request_serializer.rb index 840b653a1c7..2b3363fb576 100644 --- a/app/serializers/activitypub/quote_request_serializer.rb +++ b/app/serializers/activitypub/quote_request_serializer.rb @@ -23,6 +23,7 @@ class ActivityPub::QuoteRequestSerializer < ActivityPub::Serializer end def instrument + # TODO: inline object? ActivityPub::TagManager.instance.uri_for(object.status) end end diff --git a/spec/lib/activitypub/activity/quote_request_spec.rb b/spec/lib/activitypub/activity/quote_request_spec.rb index dac0b438cbd..24a245c8981 100644 --- a/spec/lib/activitypub/activity/quote_request_spec.rb +++ b/spec/lib/activitypub/activity/quote_request_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe ActivityPub::Activity::QuoteRequest do +RSpec.describe ActivityPub::Activity::QuoteRequest, feature: :outgoing_quotes do let(:sender) { Fabricate(:account, domain: 'example.com') } let(:recipient) { Fabricate(:account) } let(:quoted_post) { Fabricate(:status, account: recipient) } @@ -47,5 +47,44 @@ RSpec.describe ActivityPub::Activity::QuoteRequest do end, recipient.id, sender.inbox_url) end end + + context 'when trying to quote a quotable local status' do + let(:status_json) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + '@id': 'https://w3id.org/fep/044f#quote', + '@type': '@id', + }, + { + '@id': 'https://w3id.org/fep/044f#quoteAuthorization', + '@type': '@id', + }, + ], + id: 'https://example.com/unknown-status', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + quote: ActivityPub::TagManager.instance.uri_for(quoted_post), + attributedTo: ActivityPub::TagManager.instance.uri_for(sender), + }.deep_stringify_keys + end + + before do + stub_request(:get, 'https://example.com/unknown-status').to_return(status: 200, body: Oj.dump(status_json), headers: { 'Content-Type': 'application/activity+json' }) + quoted_post.update(quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) + end + + it 'accepts the quote and sends an Accept activity' do + expect { subject.perform } + .to change { quoted_post.reload.quotes.accepted.count }.by(1) + .and enqueue_sidekiq_job(ActivityPub::DeliveryWorker) + .with(satisfying do |body| + outgoing_json = Oj.load(body) + outgoing_json['type'] == 'Accept' && %w(type id actor object instrument).all? { |key| json[key] == outgoing_json['object'][key] } + end, recipient.id, sender.inbox_url) + end + end end end diff --git a/spec/serializers/activitypub/accept_quote_request_serializer_spec.rb b/spec/serializers/activitypub/accept_quote_request_serializer_spec.rb new file mode 100644 index 00000000000..986d9112b79 --- /dev/null +++ b/spec/serializers/activitypub/accept_quote_request_serializer_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::AcceptQuoteRequestSerializer do + subject { serialized_record_json(record, described_class, adapter: ActivityPub::Adapter) } + + describe 'serializing an object' do + let(:record) { Fabricate(:quote, state: :accepted) } + + it 'returns expected attributes' do + expect(subject.deep_symbolize_keys) + .to include( + actor: eq(ActivityPub::TagManager.instance.uri_for(record.quoted_account)), + id: match("#accepts/quote_requests/#{record.id}"), + object: include( + type: 'QuoteRequest', + instrument: ActivityPub::TagManager.instance.uri_for(record.status), + object: ActivityPub::TagManager.instance.uri_for(record.quoted_status) + ), + type: 'Accept' + ) + end + end +end