Add support for editing for published statuses (#16697)
* Add support for editing for published statuses * Fix references to stripped-out code * Various fixes and improvements * Further fixes and improvements * Fix updates being potentially sent to unauthorized recipients * Various fixes and improvements * Fix wrong words in test * Fix notifying accounts that were tagged but were not in the audience * Fix mistake
This commit is contained in:
parent
2d1f082bb6
commit
1060666c58
56 changed files with 1415 additions and 574 deletions
|
@ -69,9 +69,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
end
|
||||
|
||||
def process_status
|
||||
@tags = []
|
||||
@mentions = []
|
||||
@params = {}
|
||||
@tags = []
|
||||
@mentions = []
|
||||
@silenced_account_ids = []
|
||||
@params = {}
|
||||
|
||||
process_status_params
|
||||
process_tags
|
||||
|
@ -84,10 +85,18 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
|
||||
resolve_thread(@status)
|
||||
fetch_replies(@status)
|
||||
distribute(@status)
|
||||
distribute
|
||||
forward_for_reply
|
||||
end
|
||||
|
||||
def distribute
|
||||
# Spread out crawling randomly to avoid DDoSing the link
|
||||
LinkCrawlWorker.perform_in(rand(1..59).seconds, @status.id)
|
||||
|
||||
# Distribute into home and list feeds and notify mentioned accounts
|
||||
::DistributionWorker.perform_async(@status.id, silenced_account_ids: @silenced_account_ids) if @options[:override_timestamps] || @status.within_realtime_window?
|
||||
end
|
||||
|
||||
def find_existing_status
|
||||
status = status_from_uri(object_uri)
|
||||
status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present?
|
||||
|
@ -95,19 +104,22 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
end
|
||||
|
||||
def process_status_params
|
||||
@status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: @account.followers_url)
|
||||
|
||||
@params = begin
|
||||
{
|
||||
uri: object_uri,
|
||||
url: object_url || object_uri,
|
||||
uri: @status_parser.uri,
|
||||
url: @status_parser.url || @status_parser.uri,
|
||||
account: @account,
|
||||
text: text_from_content || '',
|
||||
language: detected_language,
|
||||
spoiler_text: converted_object_type? ? '' : (text_from_summary || ''),
|
||||
created_at: @object['published'],
|
||||
text: converted_object_type? ? converted_text : (@status_parser.text || ''),
|
||||
language: @status_parser.language || detected_language,
|
||||
spoiler_text: converted_object_type? ? '' : (@status_parser.spoiler_text || ''),
|
||||
created_at: @status_parser.created_at,
|
||||
edited_at: @status_parser.edited_at,
|
||||
override_timestamps: @options[:override_timestamps],
|
||||
reply: @object['inReplyTo'].present?,
|
||||
sensitive: @account.sensitized? || @object['sensitive'] || false,
|
||||
visibility: visibility_from_audience,
|
||||
reply: @status_parser.reply,
|
||||
sensitive: @account.sensitized? || @status_parser.sensitive || false,
|
||||
visibility: @status_parser.visibility,
|
||||
thread: replied_to_status,
|
||||
conversation: conversation_from_uri(@object['conversation']),
|
||||
media_attachment_ids: process_attachments.take(4).map(&:id),
|
||||
|
@ -117,42 +129,40 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
end
|
||||
|
||||
def process_audience
|
||||
(audience_to + audience_cc).uniq.each do |audience|
|
||||
next if ActivityPub::TagManager.instance.public_collection?(audience)
|
||||
# Unlike with tags, there is no point in resolving accounts we don't already
|
||||
# know here, because silent mentions would only be used for local access control anyway
|
||||
accounts_in_audience = (audience_to + audience_cc).uniq.filter_map do |audience|
|
||||
account_from_uri(audience) unless ActivityPub::TagManager.instance.public_collection?(audience)
|
||||
end
|
||||
|
||||
# Unlike with tags, there is no point in resolving accounts we don't already
|
||||
# know here, because silent mentions would only be used for local access
|
||||
# control anyway
|
||||
account = account_from_uri(audience)
|
||||
# If the payload was delivered to a specific inbox, the inbox owner must have
|
||||
# access to it, unless they already have access to it anyway
|
||||
if @options[:delivered_to_account_id]
|
||||
accounts_in_audience << delivered_to_account
|
||||
accounts_in_audience.uniq!
|
||||
end
|
||||
|
||||
next if account.nil? || @mentions.any? { |mention| mention.account_id == account.id }
|
||||
accounts_in_audience.each do |account|
|
||||
# This runs after tags are processed, and those translate into non-silent
|
||||
# mentions, which take precedence
|
||||
next if @mentions.any? { |mention| mention.account_id == account.id }
|
||||
|
||||
@mentions << Mention.new(account: account, silent: true)
|
||||
|
||||
# If there is at least one silent mention, then the status can be considered
|
||||
# as a limited-audience status, and not strictly a direct message, but only
|
||||
# if we considered a direct message in the first place
|
||||
next unless @params[:visibility] == :direct
|
||||
|
||||
@params[:visibility] = :limited
|
||||
@params[:visibility] = :limited if @params[:visibility] == :direct
|
||||
end
|
||||
|
||||
# If the payload was delivered to a specific inbox, the inbox owner must have
|
||||
# access to it, unless they already have access to it anyway
|
||||
return if @options[:delivered_to_account_id].nil? || @mentions.any? { |mention| mention.account_id == @options[:delivered_to_account_id] }
|
||||
|
||||
@mentions << Mention.new(account_id: @options[:delivered_to_account_id], silent: true)
|
||||
|
||||
return unless @params[:visibility] == :direct
|
||||
|
||||
@params[:visibility] = :limited
|
||||
# Accounts that are tagged but are not in the audience are not
|
||||
# supposed to be notified explicitly
|
||||
@silenced_account_ids = @mentions.map(&:account_id) - accounts_in_audience.map(&:id)
|
||||
end
|
||||
|
||||
def postprocess_audience_and_deliver
|
||||
return if @status.mentions.find_by(account_id: @options[:delivered_to_account_id])
|
||||
|
||||
delivered_to_account = Account.find(@options[:delivered_to_account_id])
|
||||
|
||||
@status.mentions.create(account: delivered_to_account, silent: true)
|
||||
@status.update(visibility: :limited) if @status.direct_visibility?
|
||||
|
||||
|
@ -161,6 +171,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
FeedInsertWorker.perform_async(@status.id, delivered_to_account.id, :home)
|
||||
end
|
||||
|
||||
def delivered_to_account
|
||||
@delivered_to_account ||= Account.find(@options[:delivered_to_account_id])
|
||||
end
|
||||
|
||||
def attach_tags(status)
|
||||
@tags.each do |tag|
|
||||
status.tags << tag
|
||||
|
@ -215,21 +229,22 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
|
||||
def process_emoji(tag)
|
||||
return if skip_download?
|
||||
return if tag['name'].blank? || tag['icon'].blank? || tag['icon']['url'].blank?
|
||||
|
||||
shortcode = tag['name'].delete(':')
|
||||
image_url = tag['icon']['url']
|
||||
uri = tag['id']
|
||||
updated = tag['updated']
|
||||
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
|
||||
custom_emoji_parser = ActivityPub::Parser::CustomEmojiParser.new(tag)
|
||||
|
||||
return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && updated >= emoji.updated_at)
|
||||
return if custom_emoji_parser.shortcode.blank? || custom_emoji_parser.image_remote_url.blank?
|
||||
|
||||
emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
|
||||
emoji.image_remote_url = image_url
|
||||
emoji.save
|
||||
rescue Seahorse::Client::NetworkingError => e
|
||||
Rails.logger.warn "Error storing emoji: #{e}"
|
||||
emoji = CustomEmoji.find_by(shortcode: custom_emoji_parser.shortcode, domain: @account.domain)
|
||||
|
||||
return unless emoji.nil? || custom_emoji_parser.image_remote_url != emoji.image_remote_url || (custom_emoji_parser.updated_at && custom_emoji_parser.updated_at >= emoji.updated_at)
|
||||
|
||||
begin
|
||||
emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: custom_emoji_parser.shortcode, uri: custom_emoji_parser.uri)
|
||||
emoji.image_remote_url = custom_emoji_parser.image_remote_url
|
||||
emoji.save
|
||||
rescue Seahorse::Client::NetworkingError => e
|
||||
Rails.logger.warn "Error storing emoji: #{e}"
|
||||
end
|
||||
end
|
||||
|
||||
def process_attachments
|
||||
|
@ -238,14 +253,23 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
media_attachments = []
|
||||
|
||||
as_array(@object['attachment']).each do |attachment|
|
||||
next if attachment['url'].blank? || media_attachments.size >= 4
|
||||
media_attachment_parser = ActivityPub::Parser::MediaAttachmentParser.new(attachment)
|
||||
|
||||
next if media_attachment_parser.remote_url.blank? || media_attachments.size >= 4
|
||||
|
||||
begin
|
||||
href = Addressable::URI.parse(attachment['url']).normalize.to_s
|
||||
media_attachment = MediaAttachment.create(account: @account, remote_url: href, thumbnail_remote_url: icon_url_from_attachment(attachment), description: attachment['summary'].presence || attachment['name'].presence, focus: attachment['focalPoint'], blurhash: supported_blurhash?(attachment['blurhash']) ? attachment['blurhash'] : nil)
|
||||
media_attachment = MediaAttachment.create(
|
||||
account: @account,
|
||||
remote_url: media_attachment_parser.remote_url,
|
||||
thumbnail_remote_url: media_attachment_parser.thumbnail_remote_url,
|
||||
description: media_attachment_parser.description,
|
||||
focus: media_attachment_parser.focus,
|
||||
blurhash: media_attachment_parser.blurhash
|
||||
)
|
||||
|
||||
media_attachments << media_attachment
|
||||
|
||||
next if unsupported_media_type?(attachment['mediaType']) || skip_download?
|
||||
next if unsupported_media_type?(media_attachment_parser.file_content_type) || skip_download?
|
||||
|
||||
media_attachment.download_file!
|
||||
media_attachment.download_thumbnail!
|
||||
|
@ -263,42 +287,17 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
media_attachments
|
||||
end
|
||||
|
||||
def icon_url_from_attachment(attachment)
|
||||
url = attachment['icon'].is_a?(Hash) ? attachment['icon']['url'] : attachment['icon']
|
||||
Addressable::URI.parse(url).normalize.to_s if url.present?
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
nil
|
||||
end
|
||||
|
||||
def process_poll
|
||||
return unless @object['type'] == 'Question' && (@object['anyOf'].is_a?(Array) || @object['oneOf'].is_a?(Array))
|
||||
poll_parser = ActivityPub::Parser::PollParser.new(@object)
|
||||
|
||||
expires_at = begin
|
||||
if @object['closed'].is_a?(String)
|
||||
@object['closed']
|
||||
elsif !@object['closed'].nil? && !@object['closed'].is_a?(FalseClass)
|
||||
Time.now.utc
|
||||
else
|
||||
@object['endTime']
|
||||
end
|
||||
end
|
||||
|
||||
if @object['anyOf'].is_a?(Array)
|
||||
multiple = true
|
||||
items = @object['anyOf']
|
||||
else
|
||||
multiple = false
|
||||
items = @object['oneOf']
|
||||
end
|
||||
|
||||
voters_count = @object['votersCount']
|
||||
return unless poll_parser.valid?
|
||||
|
||||
@account.polls.new(
|
||||
multiple: multiple,
|
||||
expires_at: expires_at,
|
||||
options: items.map { |item| item['name'].presence || item['content'] }.compact,
|
||||
cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 },
|
||||
voters_count: voters_count
|
||||
multiple: poll_parser.multiple,
|
||||
expires_at: poll_parser.expires_at,
|
||||
options: poll_parser.options,
|
||||
cached_tallies: poll_parser.cached_tallies,
|
||||
voters_count: poll_parser.voters_count
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -351,23 +350,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
end
|
||||
end
|
||||
|
||||
def visibility_from_audience
|
||||
if audience_to.any? { |to| ActivityPub::TagManager.instance.public_collection?(to) }
|
||||
:public
|
||||
elsif audience_cc.any? { |cc| ActivityPub::TagManager.instance.public_collection?(cc) }
|
||||
:unlisted
|
||||
elsif audience_to.include?(@account.followers_url)
|
||||
:private
|
||||
else
|
||||
:direct
|
||||
end
|
||||
end
|
||||
|
||||
def audience_includes?(account)
|
||||
uri = ActivityPub::TagManager.instance.uri_for(account)
|
||||
audience_to.include?(uri) || audience_cc.include?(uri)
|
||||
end
|
||||
|
||||
def replied_to_status
|
||||
return @replied_to_status if defined?(@replied_to_status)
|
||||
|
||||
|
@ -384,81 +366,18 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
value_or_id(@object['inReplyTo'])
|
||||
end
|
||||
|
||||
def text_from_content
|
||||
return Formatter.instance.linkify([[text_from_name, text_from_summary.presence].compact.join("\n\n"), object_url || object_uri].join(' ')) if converted_object_type?
|
||||
|
||||
if @object['content'].present?
|
||||
@object['content']
|
||||
elsif content_language_map?
|
||||
@object['contentMap'].values.first
|
||||
end
|
||||
end
|
||||
|
||||
def text_from_summary
|
||||
if @object['summary'].present?
|
||||
@object['summary']
|
||||
elsif summary_language_map?
|
||||
@object['summaryMap'].values.first
|
||||
end
|
||||
end
|
||||
|
||||
def text_from_name
|
||||
if @object['name'].present?
|
||||
@object['name']
|
||||
elsif name_language_map?
|
||||
@object['nameMap'].values.first
|
||||
end
|
||||
def converted_text
|
||||
Formatter.instance.linkify([@status_parser.title.presence, @status_parser.spoiler_text.presence, @status_parser.url || @status_parser.uri].compact.join("\n\n"))
|
||||
end
|
||||
|
||||
def detected_language
|
||||
if content_language_map?
|
||||
@object['contentMap'].keys.first
|
||||
elsif name_language_map?
|
||||
@object['nameMap'].keys.first
|
||||
elsif summary_language_map?
|
||||
@object['summaryMap'].keys.first
|
||||
elsif supported_object_type?
|
||||
LanguageDetector.instance.detect(text_from_content, @account)
|
||||
end
|
||||
end
|
||||
|
||||
def object_url
|
||||
return if @object['url'].blank?
|
||||
|
||||
url_candidate = url_to_href(@object['url'], 'text/html')
|
||||
|
||||
if invalid_origin?(url_candidate)
|
||||
nil
|
||||
else
|
||||
url_candidate
|
||||
end
|
||||
end
|
||||
|
||||
def summary_language_map?
|
||||
@object['summaryMap'].is_a?(Hash) && !@object['summaryMap'].empty?
|
||||
end
|
||||
|
||||
def content_language_map?
|
||||
@object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty?
|
||||
end
|
||||
|
||||
def name_language_map?
|
||||
@object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty?
|
||||
LanguageDetector.instance.detect(@status_parser.text, @account) if supported_object_type?
|
||||
end
|
||||
|
||||
def unsupported_media_type?(mime_type)
|
||||
mime_type.present? && !MediaAttachment.supported_mime_types.include?(mime_type)
|
||||
end
|
||||
|
||||
def supported_blurhash?(blurhash)
|
||||
components = blurhash.blank? || !blurhash_valid_chars?(blurhash) ? nil : Blurhash.components(blurhash)
|
||||
components.present? && components.none? { |comp| comp > 5 }
|
||||
end
|
||||
|
||||
def blurhash_valid_chars?(blurhash)
|
||||
/^[\w#$%*+-.:;=?@\[\]^{|}~]+$/.match?(blurhash)
|
||||
end
|
||||
|
||||
def skip_download?
|
||||
return @skip_download if defined?(@skip_download)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue