# frozen_string_literal: true 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) @request_id = request_id @quote = quote @fetching_error = nil fetch_quoted_post_if_needed!(fetchable_quoted_uri) return if fast_track_approval! || quote.approval_uri.blank? @json = fetch_approval_object(quote.approval_uri, prefetched_body:) return quote.reject! if @json.nil? return if non_matching_uri_hosts?(quote.approval_uri, value_or_id(@json['attributedTo'])) return unless matching_type? && matching_quote_uri? # Opportunistically import embedded posts if needed return if import_quoted_post_if_needed!(fetchable_quoted_uri) && fast_track_approval! # Raise an error if we failed to fetch the status raise @fetching_error if @quote.status.nil? && @fetching_error return unless matching_quoted_post? && matching_quoted_author? quote.accept! end private # FEP-044f defines rules that don't require the approval flow def fast_track_approval! return false if @quote.quoted_status_id.blank? # Always allow someone to quote themselves if @quote.account_id == @quote.quoted_account_id @quote.accept! true end # Always allow someone to quote posts in which they are mentioned if @quote.quoted_status.active_mentions.exists?(mentions: { account_id: @quote.account_id }) @quote.accept! true else false end end def fetch_approval_object(uri, prefetched_body: nil) if prefetched_body.nil? fetch_resource(uri, true, @quote.account.followers.local.first, raise_on_error: :temporary) else body_to_json(prefetched_body, compare_id: uri) end end def matching_type? supported_context?(@json) && equals_or_includes?(@json['type'], 'QuoteAuthorization') end def matching_quote_uri? ActivityPub::TagManager.instance.uri_for(@quote.status) == value_or_id(@json['interactingObject']) end def fetch_quoted_post_if_needed!(uri) 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) @quote.update(quoted_status: status) if status.present? rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e @fetching_error = e end def import_quoted_post_if_needed!(uri) # No need to fetch if we already have a post return if uri.nil? || @quote.quoted_status_id.present? || !@json['interactionTarget'].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 = @json['interactionTarget'].merge({ '@context' => @json['@context'] }) # It's not safe to fetch if the inlined object is cross-origin or doesn't match expectations return if object['id'] != uri || non_matching_uri_hosts?(@quote.approval_uri, object['id']) status = ActivityPub::FetchRemoteStatusService.new.call(object['id'], prefetched_body: object, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id) if status.present? @quote.update(quoted_status: status) true else false end end def matching_quoted_post? return false if @quote.quoted_status_id.blank? ActivityPub::TagManager.instance.uri_for(@quote.quoted_status) == value_or_id(@json['interactionTarget']) end def matching_quoted_author? ActivityPub::TagManager.instance.uri_for(@quote.quoted_account) == value_or_id(@json['attributedTo']) end end