Add support for importing inlined self-quotes

This commit is contained in:
Claire 2025-04-30 16:37:48 +02:00
parent c2ba6b10d7
commit 7ae47b974b
5 changed files with 71 additions and 8 deletions

View File

@ -204,11 +204,25 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
@quote.status = status
@quote.save
ActivityPub::VerifyQuoteService.new.call(@quote, fetchable_quoted_uri: @quote_uri, request_id: @options[:request_id])
ActivityPub::VerifyQuoteService.new.call(@quote, fetchable_quoted_uri: @quote_uri, prefetched_quoted_object: safe_prefetched_quoted_object, request_id: @options[:request_id])
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, @quote.id, @quote_uri, { 'request_id' => @options[:request_id] })
end
def safe_prefetched_quoted_object
object = @status_parser.quoted_object
return unless object.is_a?(Hash)
# NOTE: Replacing the object's context by that of the parent activity is
# not sound, but it's consistent with the rest of the codebase
object = object.merge({ '@context' => @json['@context'] })
return if value_or_id(first_of_value(object['attributedTo'])) != @account.uri || non_matching_uri_hosts?(@account.uri, object['id'])
object
end
def process_tags
return if @object['tag'].nil?

View File

@ -120,6 +120,11 @@ class ActivityPub::Parser::StatusParser
end.first
end
# The inlined quote; out of the attributes we support, only `https://w3id.org/fep/044f#quote` explicitly supports inlined objects
def quoted_object
as_array(@object['quote']).first
end
def quote_approval_uri
as_array(@object['quoteAuthorization']).first
end

View File

@ -304,11 +304,24 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
end
def fetch_and_verify_quote!(quote, quote_uri)
ActivityPub::VerifyQuoteService.new.call(quote, fetchable_quoted_uri: quote_uri, request_id: @request_id)
ActivityPub::VerifyQuoteService.new.call(quote, fetchable_quoted_uri: quote_uri, prefetched_quoted_object: safe_prefetched_quoted_object, request_id: @request_id)
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, quote.id, quote_uri, { 'request_id' => @request_id })
end
def safe_prefetched_quoted_object
object = @status_parser.quoted_object
return unless object.is_a?(Hash)
# NOTE: Replacing the object's context by that of the parent activity is
# not sound, but it's consistent with the rest of the codebase
object = object.merge({ '@context' => @activity_json['@context'] })
return if value_or_id(first_of_value(object['attributedTo'])) != @account.uri || non_matching_uri_hosts?(@account.uri, object['id'])
object
end
def update_counts!
likes = @status_parser.favourites_count
shares = @status_parser.reblogs_count

View File

@ -4,15 +4,15 @@ class ActivityPub::VerifyQuoteService < BaseService
include JsonLdHelper
# Optionally fetch quoted post, and verify the quote is authorized
def call(quote, fetchable_quoted_uri: nil, prefetched_body: nil, request_id: nil)
def call(quote, fetchable_quoted_uri: nil, prefetched_quoted_object: nil, prefetched_approval: nil, request_id: nil)
@request_id = request_id
@quote = quote
@fetching_error = nil
fetch_quoted_post_if_needed!(fetchable_quoted_uri)
fetch_quoted_post_if_needed!(fetchable_quoted_uri, prefetched_body: prefetched_quoted_object)
return if fast_track_approval! || quote.approval_uri.blank?
@json = fetch_approval_object(quote.approval_uri, prefetched_body:)
@json = fetch_approval_object(quote.approval_uri, prefetched_body: prefetched_approval)
return quote.reject! if @json.nil?
return if non_matching_uri_hosts?(quote.approval_uri, value_or_id(@json['attributedTo']))
@ -68,11 +68,11 @@ class ActivityPub::VerifyQuoteService < BaseService
ActivityPub::TagManager.instance.uri_for(@quote.status) == value_or_id(@json['interactingObject'])
end
def fetch_quoted_post_if_needed!(uri)
def fetch_quoted_post_if_needed!(uri, prefetched_body: nil)
return if uri.nil? || @quote.quoted_status.present?
status = ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
status ||= ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id)
status ||= ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: @quote.account.followers.local.first, prefetched_body:, request_id: @request_id)
@quote.update(quoted_status: status) if status.present?
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e

View File

@ -89,6 +89,37 @@ RSpec.describe ActivityPub::VerifyQuoteService do
end
end
context 'with a valid activity for a post that cannot be fetched but is passed as fetched_quoted_object' do
let(:quoted_status) { nil }
let(:approval_interaction_target) { 'https://b.example.com/unknown-quoted' }
let(:prefetched_object) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Note',
id: 'https://b.example.com/unknown-quoted',
to: 'https://www.w3.org/ns/activitystreams#Public',
attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_account),
content: 'previously unknown post',
}.with_indifferent_access
end
before do
stub_request(:get, 'https://b.example.com/unknown-quoted')
.to_return(status: 404)
end
it 'updates the status' do
expect { subject.call(quote, fetchable_quoted_uri: 'https://b.example.com/unknown-quoted', prefetched_quoted_object: prefetched_object) }
.to change(quote, :state).to('accepted')
expect(a_request(:get, approval_uri))
.to have_been_made.once
expect(quote.reload.quoted_status.content).to eq 'previously unknown post'
end
end
context 'with a valid activity for a post that cannot be fetched but is inlined' do
let(:quoted_status) { nil }
@ -148,7 +179,7 @@ RSpec.describe ActivityPub::VerifyQuoteService do
context 'with a valid activity for already-fetched posts, with a pre-fetched approval' do
it 'updates the status without fetching the activity' do
expect { subject.call(quote, prefetched_body: Oj.dump(json)) }
expect { subject.call(quote, prefetched_approval: Oj.dump(json)) }
.to change(quote, :state).to('accepted')
expect(a_request(:get, approval_uri))