Add initial support for ingesting and verifying remote quote posts (#34370)
This commit is contained in:
parent
a324edabdf
commit
df2611a10f
33 changed files with 1643 additions and 22 deletions
|
@ -16,6 +16,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
@account = status.account
|
||||
@media_attachments_changed = false
|
||||
@poll_changed = false
|
||||
@quote_changed = false
|
||||
@request_id = request_id
|
||||
|
||||
# Only native types can be updated at the moment
|
||||
|
@ -158,7 +159,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
@status.sensitive = @account.sensitized? || @status_parser.sensitive || false
|
||||
@status.language = @status_parser.language
|
||||
|
||||
@significant_changes = text_significantly_changed? || @status.spoiler_text_changed? || @media_attachments_changed || @poll_changed
|
||||
@significant_changes = text_significantly_changed? || @status.spoiler_text_changed? || @media_attachments_changed || @poll_changed || @quote_changed
|
||||
|
||||
@status.edited_at = @status_parser.edited_at if significant_changes?
|
||||
|
||||
|
@ -183,6 +184,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
update_tags!
|
||||
update_mentions!
|
||||
update_emojis!
|
||||
update_quote!
|
||||
end
|
||||
|
||||
def update_tags!
|
||||
|
@ -262,6 +264,45 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
def update_quote!
|
||||
return unless Mastodon::Feature.inbound_quotes_enabled?
|
||||
|
||||
quote = nil
|
||||
quote_uri = @status_parser.quote_uri
|
||||
|
||||
if quote_uri.present?
|
||||
approval_uri = @status_parser.quote_approval_uri
|
||||
approval_uri = nil if unsupported_uri_scheme?(approval_uri)
|
||||
|
||||
if @status.quote.present?
|
||||
# If the quoted post has changed, discard the old object and create a new one
|
||||
if @status.quote.quoted_status.present? && ActivityPub::TagManager.instance.uri_for(@status.quote.quoted_status) != quote_uri
|
||||
@status.quote.destroy
|
||||
quote = Quote.create(status: @status, approval_uri: approval_uri)
|
||||
@quote_changed = true
|
||||
else
|
||||
quote = @status.quote
|
||||
quote.update(approval_uri: approval_uri, state: :pending) if quote.approval_uri != @status_parser.quote_approval_uri
|
||||
end
|
||||
else
|
||||
quote = Quote.create(status: @status, approval_uri: approval_uri)
|
||||
@quote_changed = true
|
||||
end
|
||||
end
|
||||
|
||||
if quote.present?
|
||||
begin
|
||||
quote.save
|
||||
ActivityPub::VerifyQuoteService.new.call(quote, fetchable_quoted_uri: quote_uri, 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
|
||||
elsif @status.quote.present?
|
||||
@status.quote.destroy!
|
||||
@quote_changed = true
|
||||
end
|
||||
end
|
||||
|
||||
def update_counts!
|
||||
likes = @status_parser.favourites_count
|
||||
shares = @status_parser.reblogs_count
|
||||
|
|
112
app/services/activitypub/verify_quote_service.rb
Normal file
112
app/services/activitypub/verify_quote_service.rb
Normal file
|
@ -0,0 +1,112 @@
|
|||
# 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
|
Loading…
Add table
Add a link
Reference in a new issue