Merge tag 'v4.4.0-rc.1' into chinwag-next
This commit is contained in:
commit
fbbcaf4efd
2660 changed files with 83548 additions and 52192 deletions
|
|
@ -20,9 +20,9 @@ class ActivityPub::Activity
|
|||
end
|
||||
|
||||
class << self
|
||||
def factory(json, account, **options)
|
||||
def factory(json, account, **)
|
||||
@json = json
|
||||
klass&.new(json, account, **options)
|
||||
klass&.new(json, account, **)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -57,6 +57,8 @@ class ActivityPub::Activity
|
|||
ActivityPub::Activity::Remove
|
||||
when 'Move'
|
||||
ActivityPub::Activity::Move
|
||||
when 'QuoteRequest'
|
||||
ActivityPub::Activity::QuoteRequest
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -130,12 +132,7 @@ class ActivityPub::Activity
|
|||
|
||||
def first_mentioned_local_account
|
||||
audience = (as_array(@json['to']) + as_array(@json['cc'])).map { |x| value_or_id(x) }.uniq
|
||||
local_usernames = audience.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }
|
||||
.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
|
||||
|
||||
return if local_usernames.empty?
|
||||
|
||||
Account.local.where(username: local_usernames).first
|
||||
ActivityPub::TagManager.instance.uris_to_local_accounts(audience).first
|
||||
end
|
||||
|
||||
def first_local_follower
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
class ActivityPub::Activity::Announce < ActivityPub::Activity
|
||||
def perform
|
||||
return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
|
||||
return reject_payload! if @object.nil?
|
||||
|
||||
with_redis_lock("announce:#{value_or_id(@object)}") do
|
||||
original_status = status_from_object
|
||||
|
|
|
|||
|
|
@ -45,15 +45,20 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
@unresolved_mentions = []
|
||||
@silenced_account_ids = []
|
||||
@params = {}
|
||||
@quote = nil
|
||||
@quote_uri = nil
|
||||
|
||||
process_status_params
|
||||
process_tags
|
||||
process_quote
|
||||
process_audience
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
@status = Status.create!(@params)
|
||||
attach_tags(@status)
|
||||
attach_mentions(@status)
|
||||
attach_counts(@status)
|
||||
attach_quote(@status)
|
||||
end
|
||||
|
||||
resolve_thread(@status)
|
||||
|
|
@ -78,7 +83,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
end
|
||||
|
||||
def process_status_params
|
||||
@status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: @account.followers_url, object: @object)
|
||||
@status_parser = ActivityPub::Parser::StatusParser.new(
|
||||
@json,
|
||||
followers_collection: @account.followers_url,
|
||||
actor_uri: ActivityPub::TagManager.instance.uri_for(@account),
|
||||
object: @object
|
||||
)
|
||||
|
||||
attachment_ids = process_attachments.take(Status::MEDIA_ATTACHMENTS_LIMIT).map(&:id)
|
||||
|
||||
|
|
@ -100,6 +110,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
media_attachment_ids: attachment_ids,
|
||||
ordered_media_attachment_ids: attachment_ids,
|
||||
poll: process_poll,
|
||||
quote_approval_policy: @status_parser.quote_policy,
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -176,6 +187,30 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
end
|
||||
end
|
||||
|
||||
def attach_counts(status)
|
||||
likes = @status_parser.favourites_count
|
||||
shares = @status_parser.reblogs_count
|
||||
return if likes.nil? && shares.nil?
|
||||
|
||||
status.status_stat.tap do |status_stat|
|
||||
status_stat.untrusted_reblogs_count = shares unless shares.nil?
|
||||
status_stat.untrusted_favourites_count = likes unless likes.nil?
|
||||
status_stat.save if status_stat.changed?
|
||||
end
|
||||
end
|
||||
|
||||
def attach_quote(status)
|
||||
return if @quote.nil?
|
||||
|
||||
@quote.status = status
|
||||
@quote.save
|
||||
|
||||
embedded_quote = safe_prefetched_embed(@account, @status_parser.quoted_object, @json['context'])
|
||||
ActivityPub::VerifyQuoteService.new.call(@quote, fetchable_quoted_uri: @quote_uri, prefetched_quoted_object: embedded_quote, 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 process_tags
|
||||
return if @object['tag'].nil?
|
||||
|
||||
|
|
@ -190,6 +225,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
end
|
||||
end
|
||||
|
||||
def process_quote
|
||||
@quote_uri = @status_parser.quote_uri
|
||||
return if @quote_uri.blank?
|
||||
|
||||
approval_uri = @status_parser.quote_approval_uri
|
||||
approval_uri = nil if unsupported_uri_scheme?(approval_uri)
|
||||
@quote = Quote.new(account: @account, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?)
|
||||
end
|
||||
|
||||
def process_hashtag(tag)
|
||||
return if tag['name'].blank?
|
||||
|
||||
|
|
@ -209,7 +253,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
return if account.nil?
|
||||
|
||||
@mentions << Mention.new(account: account, silent: false)
|
||||
rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
|
||||
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
||||
@unresolved_mentions << tag['href']
|
||||
end
|
||||
|
||||
|
|
@ -260,7 +304,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
media_attachment.download_file!
|
||||
media_attachment.download_thumbnail!
|
||||
media_attachment.save
|
||||
rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
|
||||
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
||||
RedownloadMediaWorker.perform_in(rand(30..600).seconds, media_attachment.id)
|
||||
rescue Seahorse::Client::NetworkingError => e
|
||||
Rails.logger.warn "Error storing media attachment: #{e}"
|
||||
|
|
@ -325,7 +369,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
collection = @object['replies']
|
||||
return if collection.blank?
|
||||
|
||||
replies = ActivityPub::FetchRepliesService.new.call(status, collection, allow_synchronous_requests: false, request_id: @options[:request_id])
|
||||
replies = ActivityPub::FetchRepliesService.new.call(status.account.uri, collection, allow_synchronous_requests: false, request_id: @options[:request_id])
|
||||
return unless replies.nil?
|
||||
|
||||
uri = value_or_id(collection)
|
||||
|
|
@ -399,11 +443,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
def addresses_local_accounts?
|
||||
return true if @options[:delivered_to_account_id]
|
||||
|
||||
local_usernames = (audience_to + audience_cc).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
|
||||
|
||||
return false if local_usernames.empty?
|
||||
|
||||
Account.local.exists?(username: local_usernames)
|
||||
ActivityPub::TagManager.instance.uris_to_local_accounts((audience_to + audience_cc).uniq).exists?
|
||||
end
|
||||
|
||||
def tombstone_exists?
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
|||
if @account.uri == object_uri
|
||||
delete_person
|
||||
else
|
||||
delete_note
|
||||
delete_object
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
|||
end
|
||||
end
|
||||
|
||||
def delete_note
|
||||
def delete_object
|
||||
return if object_uri.nil?
|
||||
|
||||
with_redis_lock("delete_status_in_progress:#{object_uri}", raise_on_failure: false) do
|
||||
|
|
@ -32,21 +32,38 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
|||
Tombstone.find_or_create_by(uri: object_uri, account: @account)
|
||||
end
|
||||
|
||||
@status = Status.find_by(uri: object_uri, account: @account)
|
||||
@status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
|
||||
|
||||
return if @status.nil?
|
||||
|
||||
forwarder.forward! if forwarder.forwardable?
|
||||
delete_now!
|
||||
case @object['type']
|
||||
when 'QuoteAuthorization'
|
||||
revoke_quote
|
||||
when 'Note', 'Question'
|
||||
delete_status
|
||||
else
|
||||
delete_status || revoke_quote
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def delete_status
|
||||
@status = Status.find_by(uri: object_uri, account: @account)
|
||||
@status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
|
||||
|
||||
return if @status.nil?
|
||||
|
||||
forwarder.forward! if forwarder.forwardable?
|
||||
RemoveStatusService.new.call(@status, redraft: false)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def revoke_quote
|
||||
@quote = Quote.find_by(approval_uri: object_uri, quoted_account: @account)
|
||||
return if @quote.nil?
|
||||
|
||||
ActivityPub::Forwarder.new(@account, @json, @quote.status).forward!
|
||||
@quote.reject!
|
||||
end
|
||||
|
||||
def forwarder
|
||||
@forwarder ||= ActivityPub::Forwarder.new(@account, @json, @status)
|
||||
end
|
||||
|
||||
def delete_now!
|
||||
RemoveStatusService.new.call(@status, redraft: false)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
29
app/lib/activitypub/activity/quote_request.rb
Normal file
29
app/lib/activitypub/activity/quote_request.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity
|
||||
include Payloadable
|
||||
|
||||
def perform
|
||||
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?
|
||||
|
||||
# For now, we don't support being quoted by external servers
|
||||
reject_quote_request!(quoted_status)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reject_quote_request!(quoted_status)
|
||||
quote = Quote.new(
|
||||
quoted_status: quoted_status,
|
||||
quoted_account: quoted_status.account,
|
||||
status: Status.new(account: @account, uri: @json['instrument']),
|
||||
account: @account,
|
||||
activity_uri: @json['id']
|
||||
)
|
||||
json = Oj.dump(serialize_payload(quote, ActivityPub::RejectQuoteRequestSerializer))
|
||||
ActivityPub::DeliveryWorker.perform_async(json, quoted_status.account_id, @account.inbox_url)
|
||||
end
|
||||
end
|
||||
|
|
@ -17,7 +17,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
|||
|
||||
options = serialization_options(options)
|
||||
serialized_hash = serializer.serializable_hash(options.merge(named_contexts: named_contexts, context_extensions: context_extensions))
|
||||
serialized_hash = serialized_hash.select { |k, _| options[:fields].include?(k) } if options[:fields]
|
||||
serialized_hash = serialized_hash.slice(*options[:fields]) if options[:fields]
|
||||
serialized_hash = self.class.transform_key_casing!(serialized_hash, instance_options)
|
||||
|
||||
{ '@context': serialized_context(named_contexts, context_extensions) }.merge(serialized_hash)
|
||||
|
|
|
|||
|
|
@ -15,13 +15,15 @@ class ActivityPub::Parser::MediaAttachmentParser
|
|||
end
|
||||
|
||||
def remote_url
|
||||
Addressable::URI.parse(@json['url'])&.normalize&.to_s
|
||||
url = Addressable::URI.parse(url_to_href(@json['url']))&.normalize&.to_s
|
||||
url unless unsupported_uri_scheme?(url)
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
nil
|
||||
end
|
||||
|
||||
def thumbnail_remote_url
|
||||
Addressable::URI.parse(@json['icon'].is_a?(Hash) ? @json['icon']['url'] : @json['icon'])&.normalize&.to_s
|
||||
url = Addressable::URI.parse(@json['icon'].is_a?(Hash) ? @json['icon']['url'] : @json['icon'])&.normalize&.to_s
|
||||
url unless unsupported_uri_scheme?(url)
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
nil
|
||||
end
|
||||
|
|
@ -41,7 +43,7 @@ class ActivityPub::Parser::MediaAttachmentParser
|
|||
end
|
||||
|
||||
def file_content_type
|
||||
@json['mediaType']
|
||||
@json['mediaType'] || url_to_media_type(@json['url'])
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class ActivityPub::Parser::StatusParser
|
|||
# @param [Hash] json
|
||||
# @param [Hash] options
|
||||
# @option options [String] :followers_collection
|
||||
# @option options [String] :actor_uri
|
||||
# @option options [Hash] :object
|
||||
def initialize(json, **options)
|
||||
@json = json
|
||||
|
|
@ -28,7 +29,10 @@ class ActivityPub::Parser::StatusParser
|
|||
end
|
||||
|
||||
def url
|
||||
url_to_href(@object['url'], 'text/html') if @object['url'].present?
|
||||
return if @object['url'].blank?
|
||||
|
||||
url = url_to_href(@object['url'], 'text/html')
|
||||
url unless unsupported_uri_scheme?(url)
|
||||
end
|
||||
|
||||
def text
|
||||
|
|
@ -93,8 +97,70 @@ class ActivityPub::Parser::StatusParser
|
|||
lang.presence && NORMALIZED_LOCALE_NAMES.fetch(lang.downcase.to_sym, lang)
|
||||
end
|
||||
|
||||
def favourites_count
|
||||
@object['likes']['totalItems'] if @object.is_a?(Hash) && @object['likes'].is_a?(Hash)
|
||||
end
|
||||
|
||||
def reblogs_count
|
||||
@object['shares']['totalItems'] if @object.is_a?(Hash) && @object['shares'].is_a?(Hash)
|
||||
end
|
||||
|
||||
def quote_policy
|
||||
flags = 0
|
||||
policy = @object.dig('interactionPolicy', 'canQuote')
|
||||
return flags if policy.blank?
|
||||
|
||||
flags |= quote_subpolicy(policy['automaticApproval'])
|
||||
flags <<= 16
|
||||
flags |= quote_subpolicy(policy['manualApproval'])
|
||||
|
||||
flags
|
||||
end
|
||||
|
||||
def quote_uri
|
||||
%w(quote _misskey_quote quoteUrl quoteUri).filter_map do |key|
|
||||
value_or_id(as_array(@object[key]).first)
|
||||
end.first
|
||||
end
|
||||
|
||||
def legacy_quote?
|
||||
!@object.key?('quote')
|
||||
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
|
||||
|
||||
private
|
||||
|
||||
def quote_subpolicy(subpolicy)
|
||||
flags = 0
|
||||
|
||||
allowed_actors = as_array(subpolicy)
|
||||
allowed_actors.uniq!
|
||||
|
||||
flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] if allowed_actors.delete('as:Public') || allowed_actors.delete('Public') || allowed_actors.delete('https://www.w3.org/ns/activitystreams#Public')
|
||||
flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] if allowed_actors.delete(@options[:followers_collection])
|
||||
# TODO: we don't actually store that collection URI
|
||||
# flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:followed]
|
||||
|
||||
# Remove the special-meaning actor URI
|
||||
allowed_actors.delete(@options[:actor_uri])
|
||||
|
||||
# Tagged users are always allowed, so remove them
|
||||
allowed_actors -= as_array(@object['tag']).filter_map { |tag| tag['href'] if equals_or_includes?(tag['type'], 'Mention') }
|
||||
|
||||
# Any unrecognized actor is marked as unknown
|
||||
flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:unknown] unless allowed_actors.empty?
|
||||
|
||||
flags
|
||||
end
|
||||
|
||||
def raw_language_code
|
||||
if content_language_map?
|
||||
@object['contentMap'].keys.first
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ require 'singleton'
|
|||
|
||||
class ActivityPub::TagManager
|
||||
include Singleton
|
||||
include JsonLdHelper
|
||||
include RoutingHelper
|
||||
|
||||
CONTEXT = 'https://www.w3.org/ns/activitystreams'
|
||||
|
|
@ -13,11 +14,11 @@ class ActivityPub::TagManager
|
|||
}.freeze
|
||||
|
||||
def public_collection?(uri)
|
||||
uri == COLLECTIONS[:public] || uri == 'as:Public' || uri == 'Public'
|
||||
uri == COLLECTIONS[:public] || %w(as:Public Public).include?(uri)
|
||||
end
|
||||
|
||||
def url_for(target)
|
||||
return target.url if target.respond_to?(:local?) && !target.local?
|
||||
return unsupported_uri_scheme?(target.url) ? nil : target.url if target.respond_to?(:local?) && !target.local?
|
||||
|
||||
return unless target.respond_to?(:object_type)
|
||||
|
||||
|
|
@ -86,8 +87,34 @@ class ActivityPub::TagManager
|
|||
account_status_shares_url(target.account, target)
|
||||
end
|
||||
|
||||
def followers_uri_for(target)
|
||||
target.local? ? account_followers_url(target) : target.followers_url.presence
|
||||
def following_uri_for(target, ...)
|
||||
raise ArgumentError, 'target must be a local account' unless target.local?
|
||||
|
||||
account_following_index_url(target, ...)
|
||||
end
|
||||
|
||||
def followers_uri_for(target, ...)
|
||||
return target.followers_url.presence unless target.local?
|
||||
|
||||
account_followers_url(target, ...)
|
||||
end
|
||||
|
||||
def collection_uri_for(target, ...)
|
||||
raise NotImplementedError unless target.local?
|
||||
|
||||
account_collection_url(target, ...)
|
||||
end
|
||||
|
||||
def inbox_uri_for(target)
|
||||
raise NotImplementedError unless target.local?
|
||||
|
||||
target.instance_actor? ? instance_actor_inbox_url : account_inbox_url(target)
|
||||
end
|
||||
|
||||
def outbox_uri_for(target, ...)
|
||||
raise NotImplementedError unless target.local?
|
||||
|
||||
target.instance_actor? ? instance_actor_outbox_url(...) : account_outbox_url(target, ...)
|
||||
end
|
||||
|
||||
# Primary audience of a status
|
||||
|
|
@ -99,7 +126,7 @@ class ActivityPub::TagManager
|
|||
when 'public'
|
||||
[COLLECTIONS[:public]]
|
||||
when 'unlisted', 'private'
|
||||
[account_followers_url(status.account)]
|
||||
[followers_uri_for(status.account)]
|
||||
when 'direct', 'limited'
|
||||
if status.account.silenced?
|
||||
# Only notify followers if the account is locally silenced
|
||||
|
|
@ -133,7 +160,7 @@ class ActivityPub::TagManager
|
|||
|
||||
case status.visibility
|
||||
when 'public'
|
||||
cc << account_followers_url(status.account)
|
||||
cc << followers_uri_for(status.account)
|
||||
when 'unlisted'
|
||||
cc << COLLECTIONS[:public]
|
||||
end
|
||||
|
|
@ -177,6 +204,19 @@ class ActivityPub::TagManager
|
|||
path_params[param]
|
||||
end
|
||||
|
||||
def uris_to_local_accounts(uris)
|
||||
usernames = []
|
||||
ids = []
|
||||
|
||||
uris.each do |uri|
|
||||
param, value = uri_to_local_account_params(uri)
|
||||
usernames << value.downcase if param == :username
|
||||
ids << value if param == :id
|
||||
end
|
||||
|
||||
Account.local.with_username(usernames).or(Account.local.where(id: ids))
|
||||
end
|
||||
|
||||
def uri_to_actor(uri)
|
||||
uri_to_resource(uri, Account)
|
||||
end
|
||||
|
|
@ -187,7 +227,7 @@ class ActivityPub::TagManager
|
|||
if local_uri?(uri)
|
||||
case klass.name
|
||||
when 'Account'
|
||||
klass.find_local(uri_to_local_id(uri, :username))
|
||||
uris_to_local_accounts([uri]).first
|
||||
else
|
||||
StatusFinder.new(uri).status
|
||||
end
|
||||
|
|
@ -199,4 +239,20 @@ class ActivityPub::TagManager
|
|||
rescue ActiveRecord::RecordNotFound
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def uri_to_local_account_params(uri)
|
||||
return unless local_uri?(uri)
|
||||
|
||||
path_params = Rails.application.routes.recognize_path(uri)
|
||||
|
||||
# TODO: handle numeric IDs
|
||||
case path_params[:controller]
|
||||
when 'accounts'
|
||||
[:username, path_params[:username]]
|
||||
when 'instance_actors'
|
||||
[:id, -99]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue