1
0
Fork 0
forked from gitea/nas

Merge commit 'faed9bf9f1' into kb-draft-15.8-lts

This commit is contained in:
KMY 2025-01-16 23:10:39 +09:00
commit be6dc25206
23 changed files with 313 additions and 107 deletions

View file

@ -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]),

View file

@ -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

View file

@ -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

View file

@ -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 = [

View file

@ -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]

View file

@ -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?

View file

@ -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|

View file

@ -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)

View file

@ -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

View file

@ -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