Merge commit 'faed9bf9f1
' into kb-draft-15.8-lts
This commit is contained in:
commit
be6dc25206
23 changed files with 313 additions and 107 deletions
|
@ -80,10 +80,31 @@ class Api::V2::NotificationsController < Api::BaseController
|
|||
return [] if @notifications.empty?
|
||||
|
||||
MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_grouped_notifications') do
|
||||
NotificationGroup.from_notifications(@notifications, pagination_range: (@notifications.last.id)..(@notifications.first.id), grouped_types: params[:grouped_types])
|
||||
pagination_range = (@notifications.last.id)..@notifications.first.id
|
||||
|
||||
# If the page is incomplete, we know we are on the last page
|
||||
if incomplete_page?
|
||||
if paginating_up?
|
||||
pagination_range = @notifications.last.id...(params[:max_id]&.to_i)
|
||||
else
|
||||
range_start = params[:since_id]&.to_i
|
||||
range_start += 1 unless range_start.nil?
|
||||
pagination_range = range_start..(@notifications.first.id)
|
||||
end
|
||||
end
|
||||
|
||||
NotificationGroup.from_notifications(@notifications, pagination_range: pagination_range, grouped_types: params[:grouped_types])
|
||||
end
|
||||
end
|
||||
|
||||
def incomplete_page?
|
||||
@notifications.size < limit_param(DEFAULT_NOTIFICATIONS_LIMIT)
|
||||
end
|
||||
|
||||
def paginating_up?
|
||||
params[:min_id].present?
|
||||
end
|
||||
|
||||
def browserable_account_notifications
|
||||
current_account.notifications.without_suspended.browserable(
|
||||
types: Array(browserable_params[:types]),
|
||||
|
|
|
@ -46,6 +46,8 @@ class DeliveryFailureTracker
|
|||
urls.reject do |url|
|
||||
host = Addressable::URI.parse(url).normalized_host
|
||||
unavailable_domains_map[host]
|
||||
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ class FeaturedTag < ApplicationRecord
|
|||
def decrement(deleted_status)
|
||||
if statuses_count <= 1
|
||||
update(statuses_count: 0, last_status_at: nil)
|
||||
elsif last_status_at > deleted_status.created_at
|
||||
elsif last_status_at.present? && last_status_at > deleted_status.created_at
|
||||
update(statuses_count: statuses_count - 1)
|
||||
else
|
||||
# Fetching the latest status creation time can be expensive, so only perform it
|
||||
|
|
|
@ -88,22 +88,32 @@ class NotificationGroup < ActiveModelSerializers::Model
|
|||
binds = [
|
||||
account_id,
|
||||
SAMPLE_ACCOUNTS_SIZE,
|
||||
pagination_range.begin,
|
||||
pagination_range.end,
|
||||
ActiveRecord::Relation::QueryAttribute.new('group_keys', group_keys, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::String.new)),
|
||||
pagination_range.begin || 0,
|
||||
]
|
||||
binds << pagination_range.end unless pagination_range.end.nil?
|
||||
|
||||
upper_bound_cond = begin
|
||||
if pagination_range.end.nil?
|
||||
''
|
||||
elsif pagination_range.exclude_end?
|
||||
'AND id < $5'
|
||||
else
|
||||
'AND id <= $5'
|
||||
end
|
||||
end
|
||||
|
||||
ActiveRecord::Base.connection.select_all(<<~SQL.squish, 'grouped_notifications', binds).cast_values.to_h { |k, *values| [k, values] }
|
||||
SELECT
|
||||
groups.group_key,
|
||||
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT 1),
|
||||
array(SELECT from_account_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT $2),
|
||||
(SELECT count(*) FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4) AS notifications_count,
|
||||
array(SELECT activity_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 AND activity_type = 'EmojiReaction'),
|
||||
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND #{upper_bound_cond} ORDER BY id DESC LIMIT 1),
|
||||
array(SELECT from_account_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND #{upper_bound_cond} ORDER BY id DESC LIMIT $2),
|
||||
(SELECT count(*) FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND #{upper_bound_cond}) AS notifications_count,
|
||||
array(SELECT activity_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND #{upper_bound_cond} AND activity_type = 'EmojiReaction'),
|
||||
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id >= $3 ORDER BY id ASC LIMIT 1) AS min_id,
|
||||
(SELECT created_at FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT 1)
|
||||
(SELECT created_at FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND #{upper_bound_cond} ORDER BY id DESC LIMIT 1)
|
||||
FROM
|
||||
unnest($5::text[]) AS groups(group_key);
|
||||
unnest($3::text[]) AS groups(group_key);
|
||||
SQL
|
||||
else
|
||||
binds = [
|
||||
|
|
|
@ -134,7 +134,7 @@ class PreviewCard < ApplicationRecord
|
|||
end
|
||||
|
||||
def authors
|
||||
@authors ||= [PreviewCard::Author.new(self)]
|
||||
@authors ||= Array(serialized_authors)
|
||||
end
|
||||
|
||||
class Author < ActiveModelSerializers::Model
|
||||
|
@ -169,6 +169,13 @@ class PreviewCard < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def serialized_authors
|
||||
if author_name? || author_url? || author_account_id?
|
||||
PreviewCard::Author
|
||||
.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
def extract_dimensions
|
||||
file = image.queued_for_write[:original]
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
SCAN_SEARCHABILITY_RE = /\[searchability:(public|followers|reactors|private)\]/
|
||||
SCAN_SEARCHABILITY_FEDIBIRD_RE = /searchable_by_(all_users|followers_only|reacted_users_only|nobody)/
|
||||
|
||||
VALID_URI_SCHEMES = %w(http https).freeze
|
||||
|
||||
# Should be called with confirmed valid JSON
|
||||
# and WebFinger-resolved username and domain
|
||||
def call(username, domain, json, options = {}) # rubocop:disable Metrics/PerceivedComplexity
|
||||
|
@ -113,16 +115,28 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
end
|
||||
|
||||
def set_immediate_protocol_attributes!
|
||||
@account.inbox_url = @json['inbox'] || ''
|
||||
@account.outbox_url = @json['outbox'] || ''
|
||||
@account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
|
||||
@account.followers_url = @json['followers'] || ''
|
||||
@account.inbox_url = valid_collection_uri(@json['inbox'])
|
||||
@account.outbox_url = valid_collection_uri(@json['outbox'])
|
||||
@account.shared_inbox_url = valid_collection_uri(@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox'])
|
||||
@account.followers_url = valid_collection_uri(@json['followers'])
|
||||
@account.url = url || @uri
|
||||
@account.uri = @uri
|
||||
@account.actor_type = actor_type
|
||||
@account.created_at = @json['published'] if @json['published'].present?
|
||||
end
|
||||
|
||||
def valid_collection_uri(uri)
|
||||
uri = uri.first if uri.is_a?(Array)
|
||||
uri = uri['id'] if uri.is_a?(Hash)
|
||||
return '' unless uri.is_a?(String)
|
||||
|
||||
parsed_uri = Addressable::URI.parse(uri)
|
||||
|
||||
VALID_URI_SCHEMES.include?(parsed_uri.scheme) && parsed_uri.host.present? ? parsed_uri : ''
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
''
|
||||
end
|
||||
|
||||
def set_immediate_attributes!
|
||||
@account.featured_collection_url = @json['featured'] || ''
|
||||
@account.display_name = @json['name'] || ''
|
||||
|
@ -398,10 +412,11 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
end
|
||||
|
||||
def collection_info(type)
|
||||
return [nil, nil] if @json[type].blank?
|
||||
collection_uri = valid_collection_uri(@json[type])
|
||||
return [nil, nil] if collection_uri.blank?
|
||||
return @collections[type] if @collections.key?(type)
|
||||
|
||||
collection = fetch_resource_without_id_validation(@json[type])
|
||||
collection = fetch_resource_without_id_validation(collection_uri)
|
||||
|
||||
total_items = collection.is_a?(Hash) && collection['totalItems'].present? && collection['totalItems'].is_a?(Numeric) ? collection['totalItems'] : nil
|
||||
has_first_page = collection.is_a?(Hash) && collection['first'].present?
|
||||
|
|
|
@ -257,40 +257,30 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
end
|
||||
|
||||
def update_mentions!
|
||||
previous_mentions = @status.active_mentions.includes(:account).to_a
|
||||
current_mentions = []
|
||||
unresolved_mentions = []
|
||||
|
||||
@raw_mentions.each do |href|
|
||||
currently_mentioned_account_ids = @raw_mentions.filter_map do |href|
|
||||
next if href.blank?
|
||||
|
||||
account = ActivityPub::TagManager.instance.uri_to_resource(href, Account)
|
||||
account ||= ActivityPub::FetchRemoteAccountService.new.call(href, request_id: @request_id)
|
||||
|
||||
next if account.nil?
|
||||
|
||||
mention = previous_mentions.find { |x| x.account_id == account.id }
|
||||
mention ||= account.mentions.new(status: @status)
|
||||
|
||||
current_mentions << mention
|
||||
rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
||||
account&.id
|
||||
rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
|
||||
# Since previous mentions are about already-known accounts,
|
||||
# they don't try to resolve again and won't fall into this case.
|
||||
# In other words, this failure case is only for new mentions and won't
|
||||
# affect `removed_mentions` so they can safely be retried asynchronously
|
||||
unresolved_mentions << href
|
||||
nil
|
||||
end
|
||||
|
||||
current_mentions.each do |mention|
|
||||
mention.save if mention.new_record?
|
||||
end
|
||||
@status.mentions.upsert_all(currently_mentioned_account_ids.map { |id| { account_id: id, silent: false } }, unique_by: %w(status_id account_id))
|
||||
|
||||
# If previous mentions are no longer contained in the text, convert them
|
||||
# to silent mentions, since withdrawing access from someone who already
|
||||
# received a notification might be more confusing
|
||||
removed_mentions = previous_mentions - current_mentions
|
||||
|
||||
Mention.where(id: removed_mentions.map(&:id)).update_all(silent: true) unless removed_mentions.empty?
|
||||
@status.mentions.where.not(account_id: currently_mentioned_account_ids).update_all(silent: true)
|
||||
|
||||
# Queue unresolved mentions for later
|
||||
unresolved_mentions.uniq.each do |uri|
|
||||
|
|
|
@ -15,7 +15,7 @@ class ProcessMentionsService < BaseService
|
|||
|
||||
return unless @status.local?
|
||||
|
||||
@previous_mentions = @status.active_mentions.includes(:account).to_a
|
||||
@previous_mentions = @status.mentions.includes(:account).to_a
|
||||
@current_mentions = []
|
||||
|
||||
Status.transaction do
|
||||
|
@ -63,6 +63,8 @@ class ProcessMentionsService < BaseService
|
|||
mention ||= @current_mentions.find { |x| x.account_id == mentioned_account.id }
|
||||
mention ||= @status.mentions.new(account: mentioned_account)
|
||||
|
||||
mention.silent = false
|
||||
|
||||
@current_mentions << mention
|
||||
|
||||
"@#{mentioned_account.acct}"
|
||||
|
@ -87,7 +89,7 @@ class ProcessMentionsService < BaseService
|
|||
end
|
||||
|
||||
@current_mentions.each do |mention|
|
||||
mention.save if mention.new_record? && @save_records
|
||||
mention.save if (mention.new_record? || mention.silent_changed?) && @save_records
|
||||
end
|
||||
|
||||
# If previous mentions are no longer contained in the text, convert them
|
||||
|
@ -95,7 +97,7 @@ class ProcessMentionsService < BaseService
|
|||
# received a notification might be more confusing
|
||||
removed_mentions = @previous_mentions - @current_mentions
|
||||
|
||||
Mention.where(id: removed_mentions.map(&:id)).update_all(silent: true) unless removed_mentions.empty?
|
||||
Mention.where(id: removed_mentions.map(&:id), silent: false).update_all(silent: true) unless removed_mentions.empty?
|
||||
end
|
||||
|
||||
def mention_undeliverable?(mentioned_account)
|
||||
|
|
|
@ -16,7 +16,7 @@ class MentionResolveWorker
|
|||
|
||||
return if account.nil?
|
||||
|
||||
status.mentions.create!(account: account, silent: false)
|
||||
status.mentions.upsert({ account_id: account.id, silent: false }, unique_by: %w(status_id account_id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
# Do nothing
|
||||
rescue Mastodon::UnexpectedResponseError => e
|
||||
|
|
|
@ -19,6 +19,7 @@ class Scheduler::UserCleanupScheduler
|
|||
User.unconfirmed.where(confirmation_sent_at: ..UNCONFIRMED_ACCOUNTS_MAX_AGE_DAYS.days.ago).find_in_batches do |batch|
|
||||
# We have to do it separately because of missing database constraints
|
||||
AccountModerationNote.where(target_account_id: batch.map(&:account_id)).delete_all
|
||||
WebauthnCredential.where(user_id: batch.map(&:id)).delete_all
|
||||
Account.where(id: batch.map(&:account_id)).delete_all
|
||||
User.where(id: batch.map(&:id)).delete_all
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue