Merge remote-tracking branch 'parent/main' into upstream-20240906

This commit is contained in:
KMY 2024-09-06 08:42:24 +09:00
commit f18eabfe75
689 changed files with 4369 additions and 2434 deletions

View file

@ -95,6 +95,7 @@ class Account < ApplicationRecord
include DomainMaterializable
include DomainNormalizable
include Paginable
include Reviewable
enum :protocol, { ostatus: 0, activitypub: 1 }
enum :suspension_origin, { local: 0, remote: 1 }, prefix: true
@ -153,6 +154,7 @@ class Account < ApplicationRecord
scope :with_username, ->(value) { where arel_table[:username].lower.eq(value.to_s.downcase) }
scope :with_domain, ->(value) { where arel_table[:domain].lower.eq(value&.to_s&.downcase) }
scope :without_memorial, -> { where(memorial: false) }
scope :duplicate_uris, -> { select(:uri, Arel.star.count).group(:uri).having(Arel.star.count.gt(1)) }
after_update_commit :trigger_update_webhooks
@ -482,22 +484,6 @@ class Account < ApplicationRecord
@synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/"
end
def requires_review?
reviewed_at.nil?
end
def reviewed?
reviewed_at.present?
end
def requested_review?
requested_review_at.present?
end
def requires_review_notification?
requires_review? && !requested_review?
end
class << self
def readonly_attributes
super - %w(statuses_count following_count followers_count)

View file

@ -151,7 +151,7 @@ class AccountStatusesCleanupPolicy < ApplicationRecord
end
def without_self_fav_scope
Status.where('NOT EXISTS (SELECT 1 FROM favourites fav WHERE fav.account_id = statuses.account_id AND fav.status_id = statuses.id)')
Status.where.not(self_status_reference_exists(Favourite))
end
def without_self_emoji_scope
@ -159,11 +159,11 @@ class AccountStatusesCleanupPolicy < ApplicationRecord
end
def without_self_bookmark_scope
Status.where('NOT EXISTS (SELECT 1 FROM bookmarks bookmark WHERE bookmark.account_id = statuses.account_id AND bookmark.status_id = statuses.id)')
Status.where.not(self_status_reference_exists(Bookmark))
end
def without_pinned_scope
Status.where('NOT EXISTS (SELECT 1 FROM status_pins pin WHERE pin.account_id = statuses.account_id AND pin.status_id = statuses.id)')
Status.where.not(self_status_reference_exists(StatusPin))
end
def without_media_scope
@ -185,4 +185,13 @@ class AccountStatusesCleanupPolicy < ApplicationRecord
def account_statuses
Status.where(account_id: account_id)
end
def self_status_reference_exists(model)
model
.where(model.arel_table[:account_id].eq Status.arel_table[:account_id])
.where(model.arel_table[:status_id].eq Status.arel_table[:id])
.select(1)
.arel
.exists
end
end

View file

@ -2,10 +2,10 @@
module Redisable
def redis
Thread.current[:redis] ||= RedisConfiguration.pool.checkout
Thread.current[:redis] ||= RedisConnection.pool.checkout
end
def with_redis(&block)
RedisConfiguration.with(&block)
RedisConnection.with(&block)
end
end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
module Reviewable
extend ActiveSupport::Concern
def requires_review?
reviewed_at.nil?
end
def reviewed?
reviewed_at.present?
end
def requested_review?
requested_review_at.present?
end
def requires_review_notification?
requires_review? && !requested_review?
end
end

View file

@ -31,8 +31,4 @@ class Mention < ApplicationRecord
to: :account,
prefix: true
)
def active?
!silent?
end
end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class NotificationGroup < ActiveModelSerializers::Model
attributes :group_key, :sample_accounts, :notifications_count, :notification, :most_recent_notification_id, :emoji_reaction_groups, :list
attributes :group_key, :sample_accounts, :notifications_count, :notification, :most_recent_notification_id, :pagination_data, :emoji_reaction_groups, :list
# Try to keep this consistent with `app/javascript/mastodon/models/notification_group.ts`
SAMPLE_ACCOUNTS_SIZE = 8
@ -11,41 +11,55 @@ class NotificationGroup < ActiveModelSerializers::Model
attributes :emoji_reaction, :sample_accounts
end
def self.from_notification(notification, max_id: nil, grouped_types: nil)
def self.from_notifications(notifications, pagination_range: nil, grouped_types: nil)
return [] if notifications.empty?
grouped_types = grouped_types.presence&.map(&:to_sym) || Notification::GROUPABLE_NOTIFICATION_TYPES
groupable = notification.group_key.present? && grouped_types.include?(notification.type)
if groupable
# TODO: caching, and, if caching, preloading
scope = notification.account.notifications.where(group_key: notification.group_key)
scope = scope.where(id: ..max_id) if max_id.present?
grouped_notifications = notifications.filter { |notification| notification.group_key.present? && grouped_types.include?(notification.type) }
group_keys = grouped_notifications.pluck(:group_key)
# Ideally, we would not load accounts for each notification group
most_recent_notifications = scope.order(id: :desc).includes(:from_account, :list_status).take(SAMPLE_ACCOUNTS_SIZE)
most_recent_id = most_recent_notifications.first.id
sample_accounts = most_recent_notifications.map(&:from_account)
emoji_reaction_groups = extract_emoji_reaction_pair(
scope.order(id: :desc).includes(emoji_reaction: :account).take(SAMPLE_ACCOUNTS_SIZE_FOR_EMOJI_REACTION)
)
list = pick_list(most_recent_notifications)
notifications_count = scope.count
else
most_recent_id = notification.id
sample_accounts = [notification.from_account]
emoji_reaction_groups = extract_emoji_reaction_pair([notification])
list = pick_list([notification])
notifications_count = 1
with_emoji_reaction = grouped_notifications.any? { |notification| notification.type == :emoji_reaction }
notifications.any? { |notification| notification.type == :list_status }
groups_data = load_groups_data(notifications.first.account_id, group_keys, pagination_range: pagination_range)
accounts_map = Account.where(id: groups_data.values.pluck(1).flatten).index_by(&:id)
notifications.map do |notification|
if notification.group_key.present? && grouped_types.include?(notification.type)
most_recent_notification_id, sample_account_ids, count, activity_ids, *raw_pagination_data = groups_data[notification.group_key]
pagination_data = raw_pagination_data.empty? ? nil : { min_id: raw_pagination_data[0], latest_notification_at: raw_pagination_data[1] }
emoji_reaction_groups = with_emoji_reaction ? convert_emoji_reaction_pair(activity_ids) : []
NotificationGroup.new(
notification: notification,
group_key: notification.group_key,
sample_accounts: sample_account_ids.map { |id| accounts_map[id] },
notifications_count: count,
most_recent_notification_id: most_recent_notification_id,
pagination_data: pagination_data,
emoji_reaction_groups: emoji_reaction_groups
)
else
pagination_data = pagination_range.blank? ? nil : { min_id: notification.id, latest_notification_at: notification.created_at }
emoji_reaction_groups = convert_emoji_reaction_pair([notification.activity_id])
list = notification.type == :list_status ? notification.list_status&.list : nil
NotificationGroup.new(
notification: notification,
group_key: "ungrouped-#{notification.id}",
sample_accounts: [notification.from_account],
notifications_count: 1,
most_recent_notification_id: notification.id,
pagination_data: pagination_data,
emoji_reaction_groups: emoji_reaction_groups,
list: list
)
end
end
NotificationGroup.new(
notification: notification,
group_key: groupable ? notification.group_key : "ungrouped-#{notification.id}",
sample_accounts: sample_accounts,
emoji_reaction_groups: emoji_reaction_groups,
list: list,
notifications_count: notifications_count,
most_recent_notification_id: most_recent_id
)
end
delegate :type,
@ -55,24 +69,60 @@ class NotificationGroup < ActiveModelSerializers::Model
:account_warning,
to: :notification, prefix: false
def self.extract_emoji_reaction_pair(scope)
return [] unless scope.first.type == :emoji_reaction
def self.convert_emoji_reaction_pair(activity_ids)
return [] if activity_ids.empty?
scope = scope.filter { |g| g.emoji_reaction.present? }
return [] if scope.empty?
scope
.each_with_object({}) { |e, h| h[e.emoji_reaction.name] = (h[e.emoji_reaction.name] || []).push(e.emoji_reaction) }
.to_a
.map { |pair| NotificationEmojiReactionGroup.new(emoji_reaction: pair[1].first, sample_accounts: pair[1].take(SAMPLE_ACCOUNTS_SIZE).map(&:account)) }
EmojiReaction.where(id: activity_ids)
.each_with_object({}) { |e, h| h[e.name] = (h[e.name] || []).push(e) }
.to_a
.map { |pair| NotificationEmojiReactionGroup.new(emoji_reaction: pair[1].first, sample_accounts: pair[1].take(SAMPLE_ACCOUNTS_SIZE).map(&:account)) }
end
def self.pick_list(scope)
return [] unless scope.first.type == :list_status
class << self
private
scope = scope.filter { |g| g.list_status.present? }
return [] if scope.empty?
def load_groups_data(account_id, group_keys, pagination_range: nil)
return {} if group_keys.empty?
scope.first.list_status.list
if pagination_range.present?
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)),
]
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 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)
FROM
unnest($5::text[]) AS groups(group_key);
SQL
else
binds = [
account_id,
SAMPLE_ACCOUNTS_SIZE,
ActiveRecord::Relation::QueryAttribute.new('group_keys', group_keys, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::String.new)),
]
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 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 ORDER BY id DESC LIMIT $2),
(SELECT count(*) FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key) AS notifications_count,
array(SELECT activity_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND activity_type = 'EmojiReaction')
FROM
unnest($3::text[]) AS groups(group_key);
SQL
end
end
end
end

View file

@ -21,6 +21,7 @@ class PreviewCardProvider < ApplicationRecord
include Paginable
include DomainNormalizable
include Attachmentable
include Reviewable
ICON_MIME_TYPES = %w(image/x-icon image/vnd.microsoft.icon image/png).freeze
LIMIT = 1.megabyte
@ -36,22 +37,6 @@ class PreviewCardProvider < ApplicationRecord
scope :reviewed, -> { where.not(reviewed_at: nil) }
scope :pending_review, -> { where(reviewed_at: nil) }
def requires_review?
reviewed_at.nil?
end
def reviewed?
reviewed_at.present?
end
def requested_review?
requested_review_at.present?
end
def requires_review_notification?
requires_review? && !requested_review?
end
def self.matching_domain(domain)
segments = domain.split('.')
where(domain: segments.map.with_index { |_, i| segments[i..].join('.') }).by_domain_length.first

View file

@ -21,6 +21,8 @@
class Tag < ApplicationRecord
include Paginable
include Reviewable
# rubocop:disable Rails/HasAndBelongsToMany
has_and_belongs_to_many :statuses
has_and_belongs_to_many :accounts
@ -99,22 +101,6 @@ class Tag < ApplicationRecord
alias trendable? trendable
def requires_review?
reviewed_at.nil?
end
def reviewed?
reviewed_at.present?
end
def requested_review?
requested_review_at.present?
end
def requires_review_notification?
requires_review? && !requested_review?
end
def decaying?
max_score_at && max_score_at >= Trends.tags.options[:max_score_cooldown].ago && max_score_at < 1.day.ago
end

View file

@ -269,10 +269,6 @@ class User < ApplicationRecord
unconfirmed? || pending?
end
def inactive_message
approved? ? super : :pending
end
def approve!
return if approved?