Merge remote-tracking branch 'upstream/master' into master

This commit is contained in:
wuyingren 2020-09-27 00:55:40 +08:00
commit ef09081456
174 changed files with 2198 additions and 1506 deletions

View file

@ -222,23 +222,20 @@ class Account < ApplicationRecord
def suspend!(date = Time.now.utc)
transaction do
user&.disable! if local?
create_deletion_request!
update!(suspended_at: date)
end
end
def unsuspend!
transaction do
user&.enable! if local?
deletion_request&.destroy!
update!(suspended_at: nil)
end
end
def memorialize!
transaction do
user&.disable! if local?
update!(memorial: true)
end
update!(memorial: true)
end
def sign?

View file

@ -38,15 +38,16 @@ class AccountConversation < ApplicationRecord
class << self
def to_a_paginated_by_id(limit, options = {})
if options[:min_id]
paginate_by_min_id(limit, options[:min_id]).reverse
paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse
else
paginate_by_max_id(limit, options[:max_id], options[:since_id]).to_a
end
end
def paginate_by_min_id(limit, min_id = nil)
def paginate_by_min_id(limit, min_id = nil, max_id = nil)
query = order(arel_table[:last_status_id].asc).limit(limit)
query = query.where(arel_table[:last_status_id].gt(min_id)) if min_id.present?
query = query.where(arel_table[:last_status_id].lt(max_id)) if max_id.present?
query
end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_deletion_requests
#
# id :bigint(8) not null, primary key
# account_id :bigint(8)
# created_at :datetime not null
# updated_at :datetime not null
#
class AccountDeletionRequest < ApplicationRecord
DELAY_TO_DELETION = 30.days.freeze
belongs_to :account
def due_at
created_at + DELAY_TO_DELETION
end
end

View file

@ -134,7 +134,7 @@ class Admin::AccountAction
end
def process_email!
UserMailer.warning(target_account.user, warning, status_ids).deliver_now! if warnable?
UserMailer.warning(target_account.user, warning, status_ids).deliver_later! if warnable?
end
def warnable?
@ -142,7 +142,7 @@ class Admin::AccountAction
end
def status_ids
@report.status_ids if @report && include_statuses
report.status_ids if report && include_statuses
end
def reports

View file

@ -60,5 +60,8 @@ module AccountAssociations
# Hashtags
has_and_belongs_to_many :tags
has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
# Account deletion requests
has_one :deletion_request, class_name: 'AccountDeletionRequest', inverse_of: :account, dependent: :destroy
end
end

View file

@ -8,6 +8,7 @@ module AccountInteractions
Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
mapping[follow.target_account_id] = {
reblogs: follow.show_reblogs?,
notify: follow.notify?,
}
end
end
@ -36,6 +37,7 @@ module AccountInteractions
FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping|
mapping[follow_request.target_account_id] = {
reblogs: follow_request.show_reblogs?,
notify: follow_request.notify?,
}
end
end
@ -95,25 +97,29 @@ module AccountInteractions
has_many :announcement_mutes, dependent: :destroy
end
def follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
reblogs = true if reblogs.nil?
rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false)
rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit)
.find_or_create_by!(target_account: other_account)
rel.update!(show_reblogs: reblogs)
rel.show_reblogs = reblogs unless reblogs.nil?
rel.notify = notify unless notify.nil?
rel.save! if rel.changed?
remove_potential_friendship(other_account)
rel
end
def request_follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
reblogs = true if reblogs.nil?
rel = follow_requests.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
def request_follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false)
rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit)
.find_or_create_by!(target_account: other_account)
rel.update!(show_reblogs: reblogs)
rel.show_reblogs = reblogs unless reblogs.nil?
rel.notify = notify unless notify.nil?
rel.save! if rel.changed?
remove_potential_friendship(other_account)
rel

View file

@ -14,15 +14,16 @@ module Paginable
# Differs from :paginate_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.
scope :paginate_by_min_id, ->(limit, min_id = nil) {
scope :paginate_by_min_id, ->(limit, min_id = nil, max_id = nil) {
query = reorder(arel_table[:id]).limit(limit)
query = query.where(arel_table[:id].gt(min_id)) if min_id.present?
query = query.where(arel_table[:id].lt(max_id)) if max_id.present?
query
}
def self.to_a_paginated_by_id(limit, options = {})
if options[:min_id].present?
paginate_by_min_id(limit, options[:min_id]).reverse
paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse
else
paginate_by_max_id(limit, options[:max_id], options[:since_id]).to_a
end

View file

@ -20,12 +20,12 @@ class Feed
protected
def from_redis(limit, max_id, since_id, min_id)
max_id = '+inf' if max_id.blank?
if min_id.blank?
max_id = '+inf' if max_id.blank?
since_id = '-inf' if since_id.blank?
unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
else
unhydrated = redis.zrangebyscore(key, "(#{min_id}", '+inf', limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
unhydrated = redis.zrangebyscore(key, "(#{min_id}", "(#{max_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
end
Status.where(id: unhydrated).cache_ids

View file

@ -10,6 +10,7 @@
# target_account_id :bigint(8) not null
# show_reblogs :boolean default(TRUE), not null
# uri :string
# notify :boolean default(FALSE), not null
#
class Follow < ApplicationRecord
@ -34,7 +35,7 @@ class Follow < ApplicationRecord
end
def revoke_request!
FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, uri: uri)
FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, notify: notify, uri: uri)
destroy!
end

View file

@ -10,6 +10,7 @@
# target_account_id :bigint(8) not null
# show_reblogs :boolean default(TRUE), not null
# uri :string
# notify :boolean default(FALSE), not null
#
class FollowRequest < ApplicationRecord
@ -28,7 +29,7 @@ class FollowRequest < ApplicationRecord
validates_with FollowLimitValidator, on: :create
def authorize!
account.follow!(target_account, reblogs: show_reblogs, uri: uri)
account.follow!(target_account, reblogs: show_reblogs, notify: notify, uri: uri)
MergeWorker.perform_async(target_account.id, account.id) if account.local?
destroy!
end

View file

@ -69,6 +69,6 @@ class Form::AccountBatch
records = accounts.includes(:user)
records.each { |account| authorize(account.user, :reject?) }
.each { |account| SuspendAccountService.new.call(account, reserve_email: false, reserve_username: false) }
.each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) }
end
end

View file

@ -28,7 +28,7 @@ class Invite < ApplicationRecord
before_validation :set_code
def valid_for_use?
(max_uses.nil? || uses < max_uses) && !expired? && !(user.nil? || user.disabled?)
(max_uses.nil? || uses < max_uses) && !expired? && user&.functional?
end
private

View file

@ -10,21 +10,34 @@
# updated_at :datetime not null
# account_id :bigint(8) not null
# from_account_id :bigint(8) not null
# type :string
#
class Notification < ApplicationRecord
self.inheritance_column = nil
include Paginable
include Cacheable
TYPE_CLASS_MAP = {
mention: 'Mention',
reblog: 'Status',
follow: 'Follow',
follow_request: 'FollowRequest',
favourite: 'Favourite',
poll: 'Poll',
LEGACY_TYPE_CLASS_MAP = {
'Mention' => :mention,
'Status' => :reblog,
'Follow' => :follow,
'FollowRequest' => :follow_request,
'Favourite' => :favourite,
'Poll' => :poll,
}.freeze
TYPES = %i(
mention
status
reblog
follow
follow_request
favourite
poll
).freeze
STATUS_INCLUDES = [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account]].freeze
belongs_to :account, optional: true
@ -38,26 +51,30 @@ class Notification < ApplicationRecord
belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id', optional: true
belongs_to :poll, foreign_type: 'Poll', foreign_key: 'activity_id', optional: true
validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
validates :type, inclusion: { in: TYPES }
scope :without_suspended, -> { joins(:from_account).merge(Account.without_suspended) }
scope :browserable, ->(exclude_types = [], account_id = nil) {
types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types)
types = TYPES - exclude_types.map(&:to_sym)
if account_id.nil?
where(activity_type: types)
where(type: types)
else
where(activity_type: types, from_account_id: account_id)
where(type: types, from_account_id: account_id)
end
}
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, follow_request: :account, poll: [status: STATUS_INCLUDES]
def type
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
@type ||= (super || LEGACY_TYPE_CLASS_MAP[activity_type]).to_sym
end
def target_status
case type
when :status
status
when :reblog
status&.reblog
when :favourite
@ -86,10 +103,6 @@ class Notification < ApplicationRecord
item.target_status.account = accounts[item.target_status.account_id] if item.target_status
end
end
def activity_types_from_types(types)
types.map { |type| TYPE_CLASS_MAP[type.to_sym] }.compact
end
end
after_initialize :set_from_account

90
app/models/public_feed.rb Normal file
View file

@ -0,0 +1,90 @@
# frozen_string_literal: true
class PublicFeed < Feed
# @param [Account] account
# @param [Hash] options
# @option [Boolean] :with_replies
# @option [Boolean] :with_reblogs
# @option [Boolean] :local
# @option [Boolean] :remote
# @option [Boolean] :only_media
def initialize(account, options = {})
@account = account
@options = options
end
# @param [Integer] limit
# @param [Integer] max_id
# @param [Integer] since_id
# @param [Integer] min_id
# @return [Array<Status>]
def get(limit, max_id = nil, since_id = nil, min_id = nil)
scope = public_scope
scope.merge!(without_replies_scope) unless with_replies?
scope.merge!(without_reblogs_scope) unless with_reblogs?
scope.merge!(local_only_scope) if local_only?
scope.merge!(remote_only_scope) if remote_only?
scope.merge!(account_filters_scope) if account?
scope.merge!(media_only_scope) if media_only?
scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
end
private
def with_reblogs?
@options[:with_reblogs]
end
def with_replies?
@options[:with_replies]
end
def local_only?
@options[:local]
end
def remote_only?
@options[:remote]
end
def account?
@account.present?
end
def media_only?
@options[:only_media]
end
def public_scope
Status.with_public_visibility.joins(:account).merge(Account.without_suspended.without_silenced)
end
def local_only_scope
Status.local
end
def remote_only_scope
Status.remote
end
def without_replies_scope
Status.without_replies
end
def without_reblogs_scope
Status.without_reblogs
end
def media_only_scope
Status.joins(:media_attachments).group(:id)
end
def account_filters_scope
Status.not_excluded_by_account(@account).tap do |scope|
scope.merge!(Status.not_domain_blocked_by_account(@account)) unless local_only?
scope.merge!(Status.in_chosen_languages(@account)) if @account.chosen_languages.present?
end
end
end

View file

@ -85,23 +85,23 @@ class Status < ApplicationRecord
scope :recent, -> { reorder(id: :desc) }
scope :remote, -> { where(local: false).where.not(uri: nil) }
scope :local, -> { where(local: true).or(where(uri: nil)) }
scope :with_accounts, ->(ids) { where(id: ids).includes(:account) }
scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
scope :with_public_visibility, -> { where(visibility: :public) }
scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) }
scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) }
scope :in_chosen_languages, ->(account) { where(language: nil).or where(language: account.chosen_languages) }
scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) }
scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) }
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
scope :tagged_with_all, ->(tags) {
Array(tags).map(&:id).map(&:to_i).reduce(self) do |result, id|
scope :tagged_with_all, ->(tag_ids) {
Array(tag_ids).reduce(self) do |result, id|
result.joins("INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
end
}
scope :tagged_with_none, ->(tags) {
Array(tags).map(&:id).map(&:to_i).reduce(self) do |result, id|
scope :tagged_with_none, ->(tag_ids) {
Array(tag_ids).reduce(self) do |result, id|
result.joins("LEFT OUTER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
.where("t#{id}.tag_id IS NULL")
end
@ -277,26 +277,6 @@ class Status < ApplicationRecord
visibilities.keys - %w(direct limited)
end
def in_chosen_languages(account)
where(language: nil).or where(language: account.chosen_languages)
end
def as_public_timeline(account = nil, local_only = false)
query = timeline_scope(local_only).without_replies
apply_timeline_filters(query, account, [:local, true].include?(local_only))
end
def as_tag_timeline(tag, account = nil, local_only = false)
query = timeline_scope(local_only).tagged_with(tag)
apply_timeline_filters(query, account, local_only)
end
def as_outbox_timeline(account)
where(account: account, visibility: :public)
end
def favourites_map(status_ids, account_id)
Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true }
end
@ -373,51 +353,6 @@ class Status < ApplicationRecord
status&.distributable? ? status : nil
end.compact
end
private
def timeline_scope(scope = false)
starting_scope = case scope
when :local, true
Status.local
when :remote
Status.remote
else
Status
end
starting_scope
.with_public_visibility
.without_reblogs
end
def apply_timeline_filters(query, account, local_only)
if account.nil?
filter_timeline_default(query)
else
filter_timeline_for_account(query, account, local_only)
end
end
def filter_timeline_for_account(query, account, local_only)
query = query.not_excluded_by_account(account)
query = query.not_domain_blocked_by_account(account) unless local_only
query = query.in_chosen_languages(account) if account.chosen_languages.present?
query.merge(account_silencing_filter(account))
end
def filter_timeline_default(query)
query.excluding_silenced_accounts
end
def account_silencing_filter(account)
if account.silenced?
including_myself = left_outer_joins(:account).where(account_id: account.id).references(:accounts)
excluding_silenced_accounts.or(including_myself)
else
excluding_silenced_accounts
end
end
end
def status_stat

View file

@ -39,7 +39,7 @@ class Tag < ApplicationRecord
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 :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('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,

57
app/models/tag_feed.rb Normal file
View file

@ -0,0 +1,57 @@
# frozen_string_literal: true
class TagFeed < PublicFeed
LIMIT_PER_MODE = 4
# @param [Tag] tag
# @param [Account] account
# @param [Hash] options
# @option [Enumerable<String>] :any
# @option [Enumerable<String>] :all
# @option [Enumerable<String>] :none
# @option [Boolean] :local
# @option [Boolean] :remote
# @option [Boolean] :only_media
def initialize(tag, account, options = {})
@tag = tag
@account = account
@options = options
end
# @param [Integer] limit
# @param [Integer] max_id
# @param [Integer] since_id
# @param [Integer] min_id
# @return [Array<Status>]
def get(limit, max_id = nil, since_id = nil, min_id = nil)
scope = public_scope
scope.merge!(tagged_with_any_scope)
scope.merge!(tagged_with_all_scope)
scope.merge!(tagged_with_none_scope)
scope.merge!(local_only_scope) if local_only?
scope.merge!(remote_only_scope) if remote_only?
scope.merge!(account_filters_scope) if account?
scope.merge!(media_only_scope) if media_only?
scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
end
private
def tagged_with_any_scope
Status.group(:id).tagged_with(tags_for(Array(@tag.name) | Array(@options[:any])))
end
def tagged_with_all_scope
Status.group(:id).tagged_with_all(tags_for(@options[:all]))
end
def tagged_with_none_scope
Status.group(:id).tagged_with_none(tags_for(@options[:none]))
end
def tags_for(names)
Tag.matching_name(Array(names).take(LIMIT_PER_MODE)).pluck(:id) if names.present?
end
end

View file

@ -168,7 +168,7 @@ class User < ApplicationRecord
end
def active_for_authentication?
true
!account.memorial?
end
def suspicious_sign_in?(ip)
@ -176,7 +176,7 @@ class User < ApplicationRecord
end
def functional?
confirmed? && approved? && !disabled? && !account.suspended? && account.moved_to_account_id.nil?
confirmed? && approved? && !disabled? && !account.suspended? && !account.memorial? && account.moved_to_account_id.nil?
end
def unconfirmed_or_pending?

View file

@ -18,5 +18,5 @@ class WebauthnCredential < ApplicationRecord
validates :external_id, uniqueness: true
validates :nickname, uniqueness: { scope: :user_id }
validates :sign_count,
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 }
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**63 - 1 }
end