diff --git a/app/models/quote.rb b/app/models/quote.rb index 4b1072d6c..2b683b273 100644 --- a/app/models/quote.rb +++ b/app/models/quote.rb @@ -34,6 +34,7 @@ class Quote < ApplicationRecord before_validation :set_activity_uri, only: :create, if: -> { account.local? && quoted_account&.remote? } validates :activity_uri, presence: true, if: -> { account.local? && quoted_account&.remote? } validate :validate_visibility + validate :validate_original_quoted_status def accept! update!(state: :accepted) @@ -70,6 +71,10 @@ class Quote < ApplicationRecord errors.add(:quoted_status_id, :visibility_mismatch) end + def validate_original_quoted_status + errors.add(:quoted_status_id, :reblog_unallowed) if quoted_status&.reblog? + end + def set_activity_uri self.activity_uri = [ActivityPub::TagManager.instance.uri_for(account), '/quote_requests/', SecureRandom.uuid].join end diff --git a/app/serializers/rest/base_quote_serializer.rb b/app/serializers/rest/base_quote_serializer.rb index be9d5cbe6..2637014b6 100644 --- a/app/serializers/rest/base_quote_serializer.rb +++ b/app/serializers/rest/base_quote_serializer.rb @@ -14,7 +14,7 @@ class REST::BaseQuoteSerializer < ActiveModel::Serializer end def quoted_status - object.quoted_status if object.accepted? && object.quoted_status.present? && !status_filter.filtered_for_quote? + object.quoted_status if object.accepted? && object.quoted_status.present? && !object.quoted_status&.reblog? && !status_filter.filtered_for_quote? end private diff --git a/app/services/activitypub/verify_quote_service.rb b/app/services/activitypub/verify_quote_service.rb index 822abcf40..4f92b3af5 100644 --- a/app/services/activitypub/verify_quote_service.rb +++ b/app/services/activitypub/verify_quote_service.rb @@ -72,7 +72,7 @@ class ActivityPub::VerifyQuoteService < BaseService status ||= ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: @quote.account.followers.local.first, prefetched_body:, request_id: @request_id, depth: @depth + 1) - @quote.update(quoted_status: status) if status.present? + @quote.update(quoted_status: status) if status.present? && !status.reblog? rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e @fetching_error = e end @@ -90,7 +90,7 @@ class ActivityPub::VerifyQuoteService < BaseService status = ActivityPub::FetchRemoteStatusService.new.call(object['id'], prefetched_body: object, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id, depth: @depth) - if status.present? + if status.present? && !status.reblog? @quote.update(quoted_status: status) true else diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 12fbdba80..1c79174c6 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -1046,6 +1046,60 @@ RSpec.describe ActivityPub::Activity::Create do end end + context 'with a quote of a known reblog that is otherwise valid' do + let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account, reblog: Fabricate(:status)) } + let(:approval_uri) { 'https://quoted.example.com/quote-approval' } + + let(:object_json) do + build_object( + type: 'Note', + content: 'woah what she said is amazing', + quote: ActivityPub::TagManager.instance.uri_for(quoted_status), + quoteAuthorization: approval_uri + ) + end + + before do + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization', + gts: 'https://gotosocial.org/ns#', + interactionPolicy: { + '@id': 'gts:interactionPolicy', + '@type': '@id', + }, + interactingObject: { + '@id': 'gts:interactingObject', + '@type': '@id', + }, + interactionTarget: { + '@id': 'gts:interactionTarget', + '@type': '@id', + }, + }, + ], + type: 'QuoteAuthorization', + id: approval_uri, + attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), + interactingObject: object_json[:id], + interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), + })) + end + + it 'creates a status without the verified quote' do + expect { subject.perform }.to change(sender.statuses, :count).by(1) + + status = sender.statuses.first + expect(status).to_not be_nil + expect(status.quote).to_not be_nil + expect(status.quote.state).to_not eq 'accepted' + expect(status.quote.quoted_status).to be_nil + end + end + context 'when a vote to a local poll' do let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) } let!(:local_status) { Fabricate(:status, poll: poll) } diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 43e6f682c..8537931c7 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -733,6 +733,72 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do end end + context 'when the status adds a verifiable quote of a reblog through an explicit update' do + let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account, reblog: Fabricate(:status)) } + let(:approval_uri) { 'https://quoted.example.com/approvals/1' } + + let(:payload) 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: 'foo', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + quote: ActivityPub::TagManager.instance.uri_for(quoted_status), + quoteAuthorization: approval_uri, + } + end + + before do + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization', + gts: 'https://gotosocial.org/ns#', + interactionPolicy: { + '@id': 'gts:interactionPolicy', + '@type': '@id', + }, + interactingObject: { + '@id': 'gts:interactingObject', + '@type': '@id', + }, + interactionTarget: { + '@id': 'gts:interactionTarget', + '@type': '@id', + }, + }, + ], + type: 'QuoteAuthorization', + id: approval_uri, + attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), + interactingObject: ActivityPub::TagManager.instance.uri_for(status), + interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), + })) + end + + it 'updates the approval URI but does not verify the quote' do + expect { subject.call(status, json, json) } + .to change(status, :quote).from(nil) + expect(status.quote.approval_uri).to eq approval_uri + expect(status.quote.state).to_not eq 'accepted' + expect(status.quote.quoted_status).to be_nil + end + end + context 'when the status adds a unverifiable quote through an implicit update' do let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } let(:quoted_status) { Fabricate(:status, account: quoted_account) }