1
0
Fork 0
forked from gitea/nas

Merge pull request #912 from kmycode/upstream-20241126

Upstream 20241126
This commit is contained in:
KMY(雪あすか) 2024-11-29 12:08:39 +09:00 committed by GitHub
commit 8d94a8dfac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
387 changed files with 9794 additions and 9803 deletions

View file

@ -61,7 +61,7 @@ class AccountFilter
when 'email'
accounts_with_users.merge(User.matches_email(value.to_s.strip))
when 'ip'
valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value).group('users.id, accounts.id')) : Account.none
valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value).group(users: [:id], accounts: [:id])) : Account.none
when 'invited_by'
invited_by_scope(value)
when 'order'

View file

@ -3,9 +3,8 @@
module Account::Avatar
extend ActiveSupport::Concern
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
LIMIT = 2.megabytes
AVATAR_IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
AVATAR_LIMIT = Rails.configuration.x.use_vips ? 8.megabytes : 2.megabytes
AVATAR_DIMENSIONS = [400, 400].freeze
AVATAR_GEOMETRY = [AVATAR_DIMENSIONS.first, AVATAR_DIMENSIONS.last].join('x')
@ -22,9 +21,9 @@ module Account::Avatar
included do
# Avatar upload
has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '+profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, processors: [:lazy_thumbnail]
validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES
validates_attachment_size :avatar, less_than: LIMIT
remotable_attachment :avatar, LIMIT, suppress_errors: false
validates_attachment_content_type :avatar, content_type: AVATAR_IMAGE_MIME_TYPES
validates_attachment_size :avatar, less_than: AVATAR_LIMIT
remotable_attachment :avatar, AVATAR_LIMIT, suppress_errors: false
end
def avatar_original_url

View file

@ -3,16 +3,15 @@
module Account::Header
extend ActiveSupport::Concern
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
LIMIT = 2.megabytes
HEADER_IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
HEADER_LIMIT = Rails.configuration.x.use_vips ? 8.megabytes : 2.megabytes
HEADER_DIMENSIONS = [1500, 500].freeze
HEADER_GEOMETRY = [HEADER_DIMENSIONS.first, HEADER_DIMENSIONS.last].join('x')
MAX_PIXELS = HEADER_DIMENSIONS.first * HEADER_DIMENSIONS.last
HEADER_MAX_PIXELS = HEADER_DIMENSIONS.first * HEADER_DIMENSIONS.last
class_methods do
def header_styles(file)
styles = { original: { pixels: MAX_PIXELS, file_geometry_parser: FastGeometryParser } }
styles = { original: { pixels: HEADER_MAX_PIXELS, file_geometry_parser: FastGeometryParser } }
styles[:static] = { format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
styles
end
@ -23,9 +22,9 @@ module Account::Header
included do
# Header upload
has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '+profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, processors: [:lazy_thumbnail]
validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES
validates_attachment_size :header, less_than: LIMIT
remotable_attachment :header, LIMIT, suppress_errors: false
validates_attachment_content_type :header, content_type: HEADER_IMAGE_MIME_TYPES
validates_attachment_size :header, less_than: HEADER_LIMIT
remotable_attachment :header, HEADER_LIMIT, suppress_errors: false
end
def header_original_url

View file

@ -88,6 +88,9 @@ module Account::Interactions
has_many :remote_severed_relationships, foreign_key: 'remote_account_id', inverse_of: :remote_account
end
# Hashtag follows
has_many :tag_follows, inverse_of: :account, dependent: :destroy
# Account notes
has_many :account_notes, dependent: :destroy

View file

@ -16,7 +16,7 @@ module Account::Merging
Follow, FollowRequest, Block, Mute,
AccountModerationNote, AccountPin, AccountStat, ListAccount,
PollVote, Mention, AccountDeletionRequest, AccountNote, FollowRecommendationSuppression,
Appeal
Appeal, TagFollow
]
owned_classes.each do |klass|

View file

@ -0,0 +1,123 @@
# frozen_string_literal: true
module Notification::Groups
extend ActiveSupport::Concern
# `set_group_key!` needs to be updated if this list changes
GROUPABLE_NOTIFICATION_TYPES = %i(favourite reblog follow emoji_reaction).freeze
MAXIMUM_GROUP_SPAN_HOURS = 12
def set_group_key!
return if filtered? || GROUPABLE_NOTIFICATION_TYPES.exclude?(type)
type_prefix = case type
when :favourite, :reblog, :emoji_reaction
[type, target_status&.id].join('-')
when :follow
type
else
raise NotImplementedError
end
redis_key = "notif-group/#{account.id}/#{type_prefix}"
hour_bucket = activity.created_at.utc.to_i / 1.hour.to_i
# Reuse previous group if it does not span too large an amount of time
previous_bucket = redis.get(redis_key).to_i
hour_bucket = previous_bucket if hour_bucket < previous_bucket + MAXIMUM_GROUP_SPAN_HOURS
# We do not concern ourselves with race conditions since we use hour buckets
redis.set(redis_key, hour_bucket, ex: MAXIMUM_GROUP_SPAN_HOURS.hours.to_i)
self.group_key = "#{type_prefix}-#{hour_bucket}"
end
class_methods do
def paginate_groups(limit, pagination_order, grouped_types: nil)
raise ArgumentError unless %i(asc desc).include?(pagination_order)
query = reorder(id: pagination_order)
# Ideally `:types` would be a bind rather than part of the SQL itself, but that does not
# seem to be possible to do with Rails, considering that the expression would occur in
# multiple places, including in a `select`
group_key_sql = begin
if grouped_types.present?
# Normalize `grouped_types` so the number of different SQL query shapes remains small, and
# the queries can be analyzed in monitoring/telemetry tools
grouped_types = (grouped_types.map(&:to_sym) & GROUPABLE_NOTIFICATION_TYPES).sort
sanitize_sql_array([<<~SQL.squish, { types: grouped_types }])
COALESCE(
CASE
WHEN notifications.type IN (:types) THEN notifications.group_key
ELSE NULL
END,
'ungrouped-' || notifications.id
)
SQL
else
"COALESCE(notifications.group_key, 'ungrouped-' || notifications.id)"
end
end
unscoped
.with_recursive(
grouped_notifications: [
# Base case: fetching one notification and annotating it with visited groups
query
.select('notifications.*', "ARRAY[#{group_key_sql}] AS groups")
.limit(1),
# Recursive case, always yielding at most one annotated notification
unscoped
.from(
[
# Expose the working table as `wt`, but quit early if we've reached the limit
unscoped
.select('id', 'groups')
.from('grouped_notifications')
.where('array_length(grouped_notifications.groups, 1) < :limit', limit: limit)
.arel.as('wt'),
# Recursive query, using `LATERAL` so we can refer to `wt`
query
.where(pagination_order == :desc ? 'notifications.id < wt.id' : 'notifications.id > wt.id')
.where.not("#{group_key_sql} = ANY(wt.groups)")
.limit(1)
.arel.lateral('notifications'),
]
)
.select('notifications.*', "array_append(wt.groups, #{group_key_sql}) AS groups"),
]
)
.from('grouped_notifications AS notifications')
.order(id: pagination_order)
.limit(limit)
end
# This returns notifications from the request page, but with at most one notification per group.
# Notifications that have no `group_key` each count as a separate group.
def paginate_groups_by_max_id(limit, max_id: nil, since_id: nil, grouped_types: nil)
query = reorder(id: :desc)
query = query.where(id: ...(max_id.to_i)) if max_id.present?
query = query.where(id: (since_id.to_i + 1)...) if since_id.present?
query.paginate_groups(limit, :desc, grouped_types: grouped_types)
end
# Differs from :paginate_groups_by_max_id in that it gives the results immediately following min_id,
# whereas since_id gives the items with largest id, but with since_id as a cutoff.
# Results will be in ascending order by id.
def paginate_groups_by_min_id(limit, max_id: nil, min_id: nil, grouped_types: nil)
query = reorder(id: :asc)
query = query.where(id: (min_id.to_i + 1)...) if min_id.present?
query = query.where(id: ...(max_id.to_i)) if max_id.present?
query.paginate_groups(limit, :asc, grouped_types: grouped_types)
end
def to_a_grouped_paginated_by_id(limit, options = {})
if options[:min_id].present?
paginate_groups_by_min_id(limit, min_id: options[:min_id], max_id: options[:max_id], grouped_types: options[:grouped_types]).reverse
else
paginate_groups_by_max_id(limit, max_id: options[:max_id], since_id: options[:since_id], grouped_types: options[:grouped_types]).to_a
end
end
end
end

View file

@ -33,8 +33,15 @@ class FollowRequest < ApplicationRecord
def authorize!
follow = account.follow!(target_account, reblogs: show_reblogs, notify: notify, languages: languages, uri: uri, bypass_limit: true)
ListAccount.where(follow_request: self).update_all(follow_request_id: nil, follow_id: follow.id)
MergeWorker.perform_async(target_account.id, account.id) if account.local?
if account.local?
ListAccount.where(follow_request: self).update_all(follow_request_id: nil, follow_id: follow.id)
MergeWorker.perform_async(target_account.id, account.id, 'home')
MergeWorker.push_bulk(List.where(account: account).joins(:list_accounts).where(list_accounts: { account_id: target_account.id }).pluck(:id)) do |list_id|
[target_account.id, list_id, 'list']
end
end
destroy!
end

View file

@ -19,6 +19,7 @@
class Notification < ApplicationRecord
self.inheritance_column = nil
include Notification::Groups
include Paginable
include Redisable
@ -35,10 +36,6 @@ class Notification < ApplicationRecord
'AccountWarning' => :moderation_warning,
}.freeze
# `set_group_key!` needs to be updated if this list changes
GROUPABLE_NOTIFICATION_TYPES = %i(favourite reblog follow emoji_reaction).freeze
MAXIMUM_GROUP_SPAN_HOURS = 12
# Please update app/javascript/api_types/notification.ts if you change this
PROPERTIES = {
mention: {
@ -159,30 +156,6 @@ class Notification < ApplicationRecord
end
end
def set_group_key!
return if filtered? || Notification::GROUPABLE_NOTIFICATION_TYPES.exclude?(type)
type_prefix = case type
when :favourite, :reblog, :emoji_reaction
[type, target_status&.id].join('-')
when :follow
type
else
raise NotImplementedError
end
redis_key = "notif-group/#{account.id}/#{type_prefix}"
hour_bucket = activity.created_at.utc.to_i / 1.hour.to_i
# Reuse previous group if it does not span too large an amount of time
previous_bucket = redis.get(redis_key).to_i
hour_bucket = previous_bucket if hour_bucket < previous_bucket + MAXIMUM_GROUP_SPAN_HOURS
# We do not concern ourselves with race conditions since we use hour buckets
redis.set(redis_key, hour_bucket, ex: MAXIMUM_GROUP_SPAN_HOURS.hours.to_i)
self.group_key = "#{type_prefix}-#{hour_bucket}"
end
class << self
def browserable(types: [], exclude_types: [], from_account_id: nil, include_filtered: false)
requested_types = if types.empty?
@ -200,94 +173,6 @@ class Notification < ApplicationRecord
end
end
def paginate_groups(limit, pagination_order, grouped_types: nil)
raise ArgumentError unless %i(asc desc).include?(pagination_order)
query = reorder(id: pagination_order)
# Ideally `:types` would be a bind rather than part of the SQL itself, but that does not
# seem to be possible to do with Rails, considering that the expression would occur in
# multiple places, including in a `select`
group_key_sql = begin
if grouped_types.present?
# Normalize `grouped_types` so the number of different SQL query shapes remains small, and
# the queries can be analyzed in monitoring/telemetry tools
grouped_types = (grouped_types.map(&:to_sym) & GROUPABLE_NOTIFICATION_TYPES).sort
sanitize_sql_array([<<~SQL.squish, { types: grouped_types }])
COALESCE(
CASE
WHEN notifications.type IN (:types) THEN notifications.group_key
ELSE NULL
END,
'ungrouped-' || notifications.id
)
SQL
else
"COALESCE(notifications.group_key, 'ungrouped-' || notifications.id)"
end
end
unscoped
.with_recursive(
grouped_notifications: [
# Base case: fetching one notification and annotating it with visited groups
query
.select('notifications.*', "ARRAY[#{group_key_sql}] AS groups")
.limit(1),
# Recursive case, always yielding at most one annotated notification
unscoped
.from(
[
# Expose the working table as `wt`, but quit early if we've reached the limit
unscoped
.select('id', 'groups')
.from('grouped_notifications')
.where('array_length(grouped_notifications.groups, 1) < :limit', limit: limit)
.arel.as('wt'),
# Recursive query, using `LATERAL` so we can refer to `wt`
query
.where(pagination_order == :desc ? 'notifications.id < wt.id' : 'notifications.id > wt.id')
.where.not("#{group_key_sql} = ANY(wt.groups)")
.limit(1)
.arel.lateral('notifications'),
]
)
.select('notifications.*', "array_append(wt.groups, #{group_key_sql}) AS groups"),
]
)
.from('grouped_notifications AS notifications')
.order(id: pagination_order)
.limit(limit)
end
# This returns notifications from the request page, but with at most one notification per group.
# Notifications that have no `group_key` each count as a separate group.
def paginate_groups_by_max_id(limit, max_id: nil, since_id: nil, grouped_types: nil)
query = reorder(id: :desc)
query = query.where(id: ...(max_id.to_i)) if max_id.present?
query = query.where(id: (since_id.to_i + 1)...) if since_id.present?
query.paginate_groups(limit, :desc, grouped_types: grouped_types)
end
# Differs from :paginate_groups_by_max_id in that it gives the results immediately following min_id,
# whereas since_id gives the items with largest id, but with since_id as a cutoff.
# Results will be in ascending order by id.
def paginate_groups_by_min_id(limit, max_id: nil, min_id: nil, grouped_types: nil)
query = reorder(id: :asc)
query = query.where(id: (min_id.to_i + 1)...) if min_id.present?
query = query.where(id: ...(max_id.to_i)) if max_id.present?
query.paginate_groups(limit, :asc, grouped_types: grouped_types)
end
def to_a_grouped_paginated_by_id(limit, options = {})
if options[:min_id].present?
paginate_groups_by_min_id(limit, min_id: options[:min_id], max_id: options[:max_id], grouped_types: options[:grouped_types]).reverse
else
paginate_groups_by_max_id(limit, max_id: options[:max_id], since_id: options[:since_id], grouped_types: options[:grouped_types]).to_a
end
end
def preload_cache_collection_target_statuses(notifications, &_block)
notifications.group_by(&:type).each do |type, grouped_notifications|
associations = TARGET_STATUS_INCLUDES_BY_TYPE[type]

View file

@ -29,8 +29,8 @@ class Poll < ApplicationRecord
has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :delete_all
with_options class_name: 'Account', source: :account, through: :votes do
has_many :voters, -> { group('accounts.id') }
has_many :local_voters, -> { group('accounts.id').merge(Account.local) }
has_many :voters, -> { group(accounts: [:id]) }
has_many :local_voters, -> { group(accounts: [:id]).merge(Account.local) }
end
has_many :notifications, as: :activity, dependent: :destroy

View file

@ -21,4 +21,6 @@ class TagFollow < ApplicationRecord
accepts_nested_attributes_for :tag
rate_limit by: :account, family: :follows
scope :for_local_distribution, -> { joins(account: :user).merge(User.signed_in_recently) }
end

View file

@ -106,7 +106,8 @@ class Trends::Statuses < Trends::Base
private
def eligible?(status)
(status.searchability.nil? || status.compute_searchability == 'public') &&
status.created_at.past? &&
(status.searchability.nil? || status.compute_searchability == 'public') &&
(status.public_visibility? || status.public_unlisted_visibility?) &&
status.account.discoverable? && !status.account.silenced? && !status.account.sensitized? &&
status.spoiler_text.blank? && (!status.sensitive? || status.media_attachments.none?) &&

View file

@ -130,7 +130,7 @@ class User < ApplicationRecord
scope :signed_in_recently, -> { where(current_sign_in_at: ACTIVE_DURATION.ago..) }
scope :not_signed_in_recently, -> { where(current_sign_in_at: ...ACTIVE_DURATION.ago) }
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
scope :matches_ip, ->(value) { left_joins(:ips).merge(IpBlock.contained_by(value)).group('users.id') }
scope :matches_ip, ->(value) { left_joins(:ips).merge(IpBlock.contained_by(value)).group(users: [:id]) }
before_validation :sanitize_role
before_create :set_approved
@ -170,6 +170,10 @@ class User < ApplicationRecord
end
end
def signed_in_recently?
current_sign_in_at.present? && current_sign_in_at >= ACTIVE_DURATION.ago
end
def confirmed?
confirmed_at.present?
end

View file

@ -147,6 +147,10 @@ class UserRole < ApplicationRecord
other_role.nil? || position > other_role.position
end
def bypass_block?(role)
overrides?(role) && highlighted? && can?(*Flags::CATEGORIES[:moderation])
end
def computed_permissions
# If called on the everyone role, no further computation needed
return permissions if everyone?

View file

@ -53,7 +53,7 @@ class Webhook < ApplicationRecord
end
def required_permissions
events.map { |event| Webhook.permission_for_event(event) }
events.map { |event| Webhook.permission_for_event(event) }.uniq
end
def self.permission_for_event(event)