Merge branch 'main' into glitch-soc/merge-upstream

- `app/views/statuses/_simple_status.html.haml`:
  Small markup change in glitch-soc, on a line that has been modified by
  upstream. Ported upstream changes.
This commit is contained in:
Claire
2021-05-07 18:21:59 +02:00
483 changed files with 13397 additions and 7694 deletions

View File

@@ -115,7 +115,6 @@ class Account < ApplicationRecord
scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) }
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) }
scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) }
scope :popular, -> { order('account_stats.followers_count desc') }
@@ -283,19 +282,13 @@ class Account < ApplicationRecord
if hashtags_map.key?(tag.name)
hashtags_map.delete(tag.name)
else
transaction do
tags.delete(tag)
tag.decrement_count!(:accounts_count)
end
tags.delete(tag)
end
end
# Add hashtags that were so far missing
hashtags_map.each_value do |tag|
transaction do
tags << tag
tag.increment_count!(:accounts_count)
end
tags << tag
end
end

View File

@@ -1,17 +1,28 @@
# frozen_string_literal: true
class AccountSuggestions
class Suggestion < ActiveModelSerializers::Model
attributes :account, :source
end
SOURCES = [
AccountSuggestions::SettingSource,
AccountSuggestions::PastInteractionsSource,
AccountSuggestions::GlobalSource,
].freeze
def self.get(account, limit)
suggestions = PotentialFriendshipTracker.get(account, limit).map { |target_account| Suggestion.new(account: target_account, source: :past_interaction) }
suggestions.concat(FollowRecommendation.get(account, limit - suggestions.size, suggestions.map { |suggestion| suggestion.account.id }).map { |target_account| Suggestion.new(account: target_account, source: :global) }) if suggestions.size < limit
suggestions
SOURCES.each_with_object([]) do |source_class, suggestions|
source_suggestions = source_class.new.get(
account,
skip_account_ids: suggestions.map(&:account_id),
limit: limit - suggestions.size
)
suggestions.concat(source_suggestions)
end
end
def self.remove(account, target_account_id)
PotentialFriendshipTracker.remove(account.id, target_account_id)
SOURCES.each do |source_class|
source = source_class.new
source.remove(account, target_account_id)
end
end
end

View File

@@ -0,0 +1,37 @@
# frozen_string_literal: true
class AccountSuggestions::GlobalSource < AccountSuggestions::Source
def key
:global
end
def get(account, skip_account_ids: [], limit: 40)
account_ids = account_ids_for_locale(account.user_locale) - [account.id] - skip_account_ids
as_ordered_suggestions(
scope(account).where(id: account_ids),
account_ids
).take(limit)
end
def remove(_account, _target_account_id)
nil
end
private
def scope(account)
Account.searchable
.followable_by(account)
.not_excluded_by_account(account)
.not_domain_blocked_by_account(account)
end
def account_ids_for_locale(locale)
Redis.current.zrevrange("follow_recommendations:#{locale}", 0, -1).map(&:to_i)
end
def to_ordered_list_key(account)
account.id
end
end

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
class AccountSuggestions::PastInteractionsSource < AccountSuggestions::Source
include Redisable
def key
:past_interactions
end
def get(account, skip_account_ids: [], limit: 40)
account_ids = account_ids_for_account(account.id, limit + skip_account_ids.size) - skip_account_ids
as_ordered_suggestions(
scope.where(id: account_ids),
account_ids
).take(limit)
end
def remove(account, target_account_id)
redis.zrem("interactions:#{account.id}", target_account_id)
end
private
def scope
Account.searchable
end
def account_ids_for_account(account_id, limit)
redis.zrevrange("interactions:#{account_id}", 0, limit).map(&:to_i)
end
def to_ordered_list_key(account)
account.id
end
end

View File

@@ -0,0 +1,68 @@
# frozen_string_literal: true
class AccountSuggestions::SettingSource < AccountSuggestions::Source
def key
:staff
end
def get(account, skip_account_ids: [], limit: 40)
return [] unless setting_enabled?
as_ordered_suggestions(
scope(account).where(setting_to_where_condition).where.not(id: skip_account_ids),
usernames_and_domains
).take(limit)
end
def remove(_account, _target_account_id)
nil
end
private
def scope(account)
Account.searchable
.followable_by(account)
.not_excluded_by_account(account)
.not_domain_blocked_by_account(account)
.where(locked: false)
.where.not(id: account.id)
end
def usernames_and_domains
@usernames_and_domains ||= setting_to_usernames_and_domains
end
def setting_enabled?
setting.present?
end
def setting_to_where_condition
usernames_and_domains.map do |(username, domain)|
Arel::Nodes::Grouping.new(
Account.arel_table[:username].lower.eq(username.downcase).and(
Account.arel_table[:domain].lower.eq(domain&.downcase)
)
)
end.reduce(:or)
end
def setting_to_usernames_and_domains
setting.split(',').map do |str|
username, domain = str.strip.gsub(/\A@/, '').split('@', 2)
domain = nil if TagManager.instance.local_domain?(domain)
next if username.blank?
[username, domain]
end.compact
end
def setting
Setting.bootstrap_timeline_accounts
end
def to_ordered_list_key(account)
[account.username, account.domain]
end
end

View File

@@ -0,0 +1,34 @@
# frozen_string_literal: true
class AccountSuggestions::Source
def key
raise NotImplementedError
end
def get(_account, **kwargs)
raise NotImplementedError
end
def remove(_account, target_account_id)
raise NotImplementedError
end
protected
def as_ordered_suggestions(scope, ordered_list)
return [] if ordered_list.empty?
map = scope.index_by(&method(:to_ordered_list_key))
ordered_list.map { |ordered_list_key| map[ordered_list_key] }.compact.map do |account|
AccountSuggestions::Suggestion.new(
account: account,
source: key
)
end
end
def to_ordered_list_key(_account)
raise NotImplementedError
end
end

View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AccountSuggestions::Suggestion < ActiveModelSerializers::Model
attributes :account, :source
delegate :id, to: :account, prefix: true
end

View File

@@ -1,24 +0,0 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_tag_stats
#
# id :bigint(8) not null, primary key
# tag_id :bigint(8) not null
# accounts_count :bigint(8) default(0), not null
# hidden :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class AccountTagStat < ApplicationRecord
belongs_to :tag, inverse_of: :account_tag_stat
def increment_count!(key)
update(key => public_send(key) + 1)
end
def decrement_count!(key)
update(key => [public_send(key) - 1, 0].max)
end
end

View File

@@ -17,12 +17,14 @@ class Admin::ActionLogFilter
create_domain_allow: { target_type: 'DomainAllow', action: 'create' }.freeze,
create_domain_block: { target_type: 'DomainBlock', action: 'create' }.freeze,
create_email_domain_block: { target_type: 'EmailDomainBlock', action: 'create' }.freeze,
create_unavailable_domain: { target_type: 'UnavailableDomain', action: 'create' }.freeze,
demote_user: { target_type: 'User', action: 'demote' }.freeze,
destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze,
destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze,
destroy_domain_allow: { target_type: 'DomainAllow', action: 'destroy' }.freeze,
destroy_domain_block: { target_type: 'DomainBlock', action: 'destroy' }.freeze,
destroy_email_domain_block: { target_type: 'EmailDomainBlock', action: 'destroy' }.freeze,
destroy_unavailable_domain: { target_type: 'UnavailableDomain', action: 'destroy' }.freeze,
destroy_status: { target_type: 'Status', action: 'destroy' }.freeze,
disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze,
disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze,

View File

@@ -184,6 +184,14 @@ module AccountInteractions
active_relationships.where(target_account: other_account).exists?
end
def following_anyone?
active_relationships.exists?
end
def not_following_anyone?
!following_anyone?
end
def blocking?(other_account)
block_relationships.where(target_account: other_account).exists?
end

View File

@@ -14,26 +14,13 @@ class FollowRecommendation < ApplicationRecord
belongs_to :account_summary, foreign_key: :account_id
belongs_to :account, foreign_key: :account_id
scope :safe, -> { joins(:account_summary).merge(AccountSummary.safe) }
scope :localized, ->(locale) { joins(:account_summary).merge(AccountSummary.localized(locale)) }
scope :filtered, -> { joins(:account_summary).merge(AccountSummary.filtered) }
def self.refresh
Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false)
end
def readonly?
true
end
def self.get(account, limit, exclude_account_ids = [])
account_ids = Redis.current.zrevrange("follow_recommendations:#{account.user_locale}", 0, -1).map(&:to_i) - exclude_account_ids - [account.id]
return [] if account_ids.empty? || limit < 1
accounts = Account.followable_by(account)
.not_excluded_by_account(account)
.not_domain_blocked_by_account(account)
.where(id: account_ids)
.limit(limit)
.index_by(&:id)
account_ids.map { |id| accounts[id] }.compact
end
end

View File

@@ -29,7 +29,7 @@ class FollowRequest < ApplicationRecord
validates :account_id, uniqueness: { scope: :target_account_id }
def authorize!
account.follow!(target_account, reblogs: show_reblogs, notify: notify, uri: uri)
account.follow!(target_account, reblogs: show_reblogs, notify: notify, uri: uri, bypass_limit: true)
MergeWorker.perform_async(target_account.id, account.id) if account.local?
destroy!
end

View File

@@ -16,7 +16,6 @@ class Form::AdminSettings
open_deletion
timeline_preview
show_staff_badge
enable_bootstrap_timeline_accounts
bootstrap_timeline_accounts
flavour
skin
@@ -48,7 +47,6 @@ class Form::AdminSettings
open_deletion
timeline_preview
show_staff_badge
enable_bootstrap_timeline_accounts
activity_api_enabled
peers_api_enabled
show_known_fediverse_at_about_page

View File

@@ -10,10 +10,13 @@
class Instance < ApplicationRecord
self.primary_key = :domain
attr_accessor :failure_days
has_many :accounts, foreign_key: :domain, primary_key: :domain
belongs_to :domain_block, foreign_key: :domain, primary_key: :domain
belongs_to :domain_allow, foreign_key: :domain, primary_key: :domain
belongs_to :unavailable_domain, foreign_key: :domain, primary_key: :domain # skipcq: RB-RL1031
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }

View File

@@ -4,6 +4,8 @@ class InstanceFilter
KEYS = %i(
limited
by_domain
warning
unavailable
).freeze
attr_reader :params
@@ -13,7 +15,7 @@ class InstanceFilter
end
def results
scope = Instance.includes(:domain_block, :domain_allow).order(accounts_count: :desc)
scope = Instance.includes(:domain_block, :domain_allow, :unavailable_domain).order(accounts_count: :desc)
params.each do |key, value|
scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
@@ -32,6 +34,10 @@ class InstanceFilter
Instance.joins(:domain_allow).reorder(Arel.sql('domain_allows.id desc'))
when 'by_domain'
Instance.matches_domain(value)
when 'warning'
Instance.where(domain: DeliveryFailureTracker.warning_domains)
when 'unavailable'
Instance.joins(:unavailable_domain)
else
raise "Unknown filter: #{key}"
end

View File

@@ -287,7 +287,7 @@ class MediaAttachment < ApplicationRecord
if instance.file_content_type == 'image/gif'
[:gif_transcoder, :blurhash_transcoder]
elsif VIDEO_MIME_TYPES.include?(instance.file_content_type)
[:video_transcoder, :blurhash_transcoder, :type_corrector]
[:transcoder, :blurhash_transcoder, :type_corrector]
elsif AUDIO_MIME_TYPES.include?(instance.file_content_type)
[:image_extractor, :transcoder, :type_corrector]
else
@@ -388,7 +388,7 @@ class MediaAttachment < ApplicationRecord
# paths but ultimately the same file, so it makes sense to memoize the
# result while disregarding the path
def ffmpeg_data(path = nil)
@ffmpeg_data ||= FFMPEG::Movie.new(path)
@ffmpeg_data ||= VideoMetadataExtractor.new(path)
end
def enqueue_processing

View File

@@ -44,7 +44,7 @@ class SessionActivation < ApplicationRecord
end
def activate(**options)
activation = create!(options)
activation = create!(**options)
purge_old
activation
end

View File

@@ -167,6 +167,10 @@ class Status < ApplicationRecord
attributes['local'] || uri.nil?
end
def in_reply_to_local_account?
reply? && thread&.account&.local?
end
def reblog?
!reblog_of_id.nil?
end

View File

@@ -20,10 +20,8 @@
class Tag < ApplicationRecord
has_and_belongs_to_many :statuses
has_and_belongs_to_many :accounts
has_and_belongs_to_many :sample_accounts, -> { local.discoverable.popular.limit(3) }, class_name: 'Account'
has_many :featured_tags, dependent: :destroy, inverse_of: :tag
has_one :account_tag_stat, dependent: :destroy
HASHTAG_SEPARATORS = "_\u00B7\u200c"
HASHTAG_NAME_RE = "([[:word:]_][[:word:]#{HASHTAG_SEPARATORS}]*[[:alpha:]#{HASHTAG_SEPARATORS}][[:word:]#{HASHTAG_SEPARATORS}]*[[:word:]_])|([[:word:]_]*[[:alpha:]][[:word:]_]*)"
@@ -38,28 +36,11 @@ class Tag < ApplicationRecord
scope :usable, -> { where(usable: [true, nil]) }
scope :listable, -> { where(listable: [true, nil]) }
scope :trendable, -> { Setting.trendable_by_default ? where(trendable: [true, nil]) : where(trendable: true) }
scope :discoverable, -> { listable.joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) }
scope :recently_used, ->(account) { joins(:statuses).where(statuses: { id: account.statuses.select(:id).limit(1000) }).group(:id).order(Arel.sql('count(*) desc')) }
scope :matches_name, ->(value) { where(arel_table[:name].matches("#{value}%")) }
delegate :accounts_count,
:accounts_count=,
:increment_count!,
:decrement_count!,
to: :account_tag_stat
after_save :save_account_tag_stat
scope :matches_name, ->(term) { where(arel_table[:name].lower.matches(arel_table.lower("#{sanitize_sql_like(Tag.normalize(term))}%"), nil, true)) } # Search with case-sensitive to use B-tree index
update_index('tags#tag', :self)
def account_tag_stat
super || build_account_tag_stat
end
def cached_sample_accounts
Rails.cache.fetch("#{cache_key}/sample_accounts", expires_in: 12.hours) { sample_accounts }
end
def to_param
name
end
@@ -94,6 +75,10 @@ class Tag < ApplicationRecord
requested_review_at.present?
end
def use!(account, status: nil, at_time: Time.now.utc)
TrendingTags.record_use!(self, account, status: status, at_time: at_time)
end
def trending?
TrendingTags.trending?(self)
end
@@ -126,10 +111,10 @@ class Tag < ApplicationRecord
end
def search_for(term, limit = 5, offset = 0, options = {})
normalized_term = normalize(term.strip)
pattern = sanitize_sql_like(normalized_term) + '%'
query = Tag.listable.where(arel_table[:name].lower.matches(pattern))
query = query.where(arel_table[:name].lower.eq(normalized_term).or(arel_table[:reviewed_at].not_eq(nil))) if options[:exclude_unreviewed]
stripped_term = term.strip
query = Tag.listable.matches_name(stripped_term)
query = query.merge(matching_name(stripped_term).or(where.not(reviewed_at: nil))) if options[:exclude_unreviewed]
query.order(Arel.sql('length(name) ASC, name ASC'))
.limit(limit)
@@ -145,7 +130,7 @@ class Tag < ApplicationRecord
end
def matching_name(name_or_names)
names = Array(name_or_names).map { |name| normalize(name).mb_chars.downcase.to_s }
names = Array(name_or_names).map { |name| arel_table.lower(normalize(name)) }
if names.size == 1
where(arel_table[:name].lower.eq(names.first))
@@ -154,8 +139,6 @@ class Tag < ApplicationRecord
end
end
private
def normalize(str)
str.gsub(/\A#/, '')
end
@@ -163,11 +146,6 @@ class Tag < ApplicationRecord
private
def save_account_tag_stat
return unless account_tag_stat&.changed?
account_tag_stat.save
end
def validate_name_change
errors.add(:name, I18n.t('tags.does_not_match_previous_name')) unless name_was.mb_chars.casecmp(name.mb_chars).zero?
end

View File

@@ -33,8 +33,6 @@ class TagFilter
def scope_for(key, value)
case key.to_s
when 'directory'
Tag.discoverable
when 'reviewed'
Tag.reviewed.order(reviewed_at: :desc)
when 'unreviewed'

View File

@@ -13,19 +13,23 @@ class TrendingTags
class << self
include Redisable
def record_use!(tag, account, at_time = Time.now.utc)
return if account.silenced? || account.bot? || !tag.usable? || !(tag.trendable? || tag.requires_review?)
def record_use!(tag, account, status: nil, at_time: Time.now.utc)
return unless tag.usable? && !account.silenced?
# Even if a tag is not allowed to trend, we still need to
# record the stats since they can be displayed in other places
increment_historical_use!(tag.id, at_time)
increment_unique_use!(tag.id, account.id, at_time)
increment_use!(tag.id, at_time)
tag.update(last_status_at: Time.now.utc) if tag.last_status_at.nil? || tag.last_status_at < 12.hours.ago
# Only update when the tag was last used once every 12 hours
# and only if a status is given (lets use ignore reblogs)
tag.update(last_status_at: at_time) if status.present? && (tag.last_status_at.nil? || (tag.last_status_at < at_time && tag.last_status_at < 12.hours.ago))
end
def update!(at_time = Time.now.utc)
tag_ids = redis.smembers("#{KEY}:used:#{at_time.beginning_of_day.to_i}") + redis.zrange(KEY, 0, -1)
tags = Tag.where(id: tag_ids.uniq)
tags = Tag.trendable.where(id: tag_ids.uniq)
# First pass to calculate scores and update the set

View File

@@ -370,15 +370,20 @@ class User < ApplicationRecord
protected
def send_devise_notification(notification, *args)
def send_devise_notification(notification, *args, **kwargs)
# This method can be called in `after_update` and `after_commit` hooks,
# but we must make sure the mailer is actually called *after* commit,
# otherwise it may work on stale data. To do this, figure out if we are
# within a transaction.
# It seems like devise sends keyword arguments as a hash in the last
# positional argument
kwargs = args.pop if args.last.is_a?(Hash) && kwargs.empty?
if ActiveRecord::Base.connection.current_transaction.try(:records)&.include?(self)
pending_devise_notifications << [notification, args]
pending_devise_notifications << [notification, args, kwargs]
else
render_and_send_devise_message(notification, *args)
render_and_send_devise_message(notification, *args, **kwargs)
end
end
@@ -389,8 +394,8 @@ class User < ApplicationRecord
end
def send_pending_devise_notifications
pending_devise_notifications.each do |notification, args|
render_and_send_devise_message(notification, *args)
pending_devise_notifications.each do |notification, args, kwargs|
render_and_send_devise_message(notification, *args, **kwargs)
end
# Empty the pending notifications array because the
@@ -403,8 +408,8 @@ class User < ApplicationRecord
@pending_devise_notifications ||= []
end
def render_and_send_devise_message(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
def render_and_send_devise_message(notification, *args, **kwargs)
devise_mailer.send(notification, self, *args, **kwargs).deliver_later
end
def set_approved
@@ -458,9 +463,7 @@ class User < ApplicationRecord
end
def regenerate_feed!
return unless Redis.current.setnx("account:#{account_id}:regeneration", true)
Redis.current.expire("account:#{account_id}:regeneration", 1.day.seconds)
RegenerationWorker.perform_async(account_id)
RegenerationWorker.perform_async(account_id) if Redis.current.set("account:#{account_id}:regeneration", true, nx: true, ex: 1.day.seconds)
end
def needs_feed_update?