diff --git a/app/lib/activitypub/activity/quote_request.rb b/app/lib/activitypub/activity/quote_request.rb index 27dea05bf..12f48ebb2 100644 --- a/app/lib/activitypub/activity/quote_request.rb +++ b/app/lib/activitypub/activity/quote_request.rb @@ -7,7 +7,7 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity return if non_matching_uri_hosts?(@account.uri, @json['id']) quoted_status = status_from_uri(object_uri) - return if quoted_status.nil? || !quoted_status.account.local? || !quoted_status.distributable? + return if quoted_status.nil? || !quoted_status.account.local? || !quoted_status.distributable? || quoted_status.reblog? if StatusPolicy.new(@account, quoted_status).quote? accept_quote_request!(quoted_status) diff --git a/app/models/concerns/status/interaction_policy_concern.rb b/app/models/concerns/status/interaction_policy_concern.rb index dbac017b3..da132a450 100644 --- a/app/models/concerns/status/interaction_policy_concern.rb +++ b/app/models/concerns/status/interaction_policy_concern.rb @@ -27,7 +27,7 @@ module Status::InteractionPolicyConcern # Returns `:automatic`, `:manual`, `:unknown` or `:denied` def quote_policy_for_account(other_account, preloaded_relations: {}) - return :denied if other_account.nil? || direct_visibility? + return :denied if other_account.nil? || direct_visibility? || reblog? following_author = nil followed_by_author = nil diff --git a/app/models/quote.rb b/app/models/quote.rb index 0d24cb239..e81d42708 100644 --- a/app/models/quote.rb +++ b/app/models/quote.rb @@ -39,6 +39,7 @@ class Quote < ApplicationRecord validates :activity_uri, presence: true, if: -> { account.local? && quoted_account&.remote? } validates :approval_uri, absence: true, if: -> { quoted_account&.local? } validate :validate_visibility + validate :validate_original_quoted_status after_create_commit :increment_counter_caches! after_destroy_commit :decrement_counter_caches! @@ -85,6 +86,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 2badadd42..4dcf11cdf 100644 --- a/app/services/activitypub/verify_quote_service.rb +++ b/app/services/activitypub/verify_quote_service.rb @@ -73,7 +73,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 @@ -91,7 +91,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 2f2f91e36..1e8a2a29d 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/models/concerns/status/interaction_policy_concern_spec.rb b/spec/models/concerns/status/interaction_policy_concern_spec.rb index b59a1186d..ebc261fc7 100644 --- a/spec/models/concerns/status/interaction_policy_concern_spec.rb +++ b/spec/models/concerns/status/interaction_policy_concern_spec.rb @@ -15,6 +15,22 @@ RSpec.describe Status::InteractionPolicyConcern do describe '#quote_policy_for_account' do let(:account) { Fabricate(:account) } + context 'when the account is the author' do + let(:status) { Fabricate(:status, account: account, quote_approval_policy: 0) } + + it 'returns :automatic' do + expect(status.quote_policy_for_account(account)).to eq :automatic + end + + context 'when it is a reblog' do + let(:status) { Fabricate(:status, account: account, quote_approval_policy: 0, reblog: Fabricate(:status)) } + + it 'returns :automatic' do + expect(status.quote_policy_for_account(account)).to eq :denied + end + end + end + context 'when the account is not following the user' do it 'returns :manual because of the public entry in the manual policy' do expect(status.quote_policy_for_account(account)).to eq :manual diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 56a8c71cb..9d63c5f1f 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -810,6 +810,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) }