Introduce StatusThreadingConcern (#3490)
* Add a StatusFilter class to identify visibility of statuses by accounts * Extract StatusThreadingConcern from Status * Clarify purpose of checking for nil account
This commit is contained in:
		
				
					committed by
					
						
						Eugen Rochko
					
				
			
			
				
	
			
			
			
						parent
						
							c26cea262b
						
					
				
				
					commit
					6201f96b8a
				
			
							
								
								
									
										56
									
								
								app/lib/status_filter.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								app/lib/status_filter.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class StatusFilter
 | 
			
		||||
  attr_reader :status, :account
 | 
			
		||||
 | 
			
		||||
  def initialize(status, account)
 | 
			
		||||
    @status = status
 | 
			
		||||
    @account = account
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def filtered?
 | 
			
		||||
    account_present? && filtered_status?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def account_present?
 | 
			
		||||
    !account.nil?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def filtered_status?
 | 
			
		||||
    blocking_account? || blocking_domain? || muting_account? || silenced_account? || blocked_by_policy?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def blocking_account?
 | 
			
		||||
    account.blocking? status.account_id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def blocking_domain?
 | 
			
		||||
    account.domain_blocking? status.account_domain
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def muting_account?
 | 
			
		||||
    account.muting? status.account_id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def silenced_account?
 | 
			
		||||
    status_account_silenced? && !account_following_status_account?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def status_account_silenced?
 | 
			
		||||
    status.account.silenced?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def account_following_status_account?
 | 
			
		||||
    account.following? status.account_id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def blocked_by_policy?
 | 
			
		||||
    !policy_allows_show?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def policy_allows_show?
 | 
			
		||||
    StatusPolicy.new(account, status).show?
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										89
									
								
								app/models/concerns/status_threading_concern.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								app/models/concerns/status_threading_concern.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module StatusThreadingConcern
 | 
			
		||||
  extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
  def ancestors(account = nil)
 | 
			
		||||
    find_statuses_from_tree_path(ancestor_ids, account)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def descendants(account = nil)
 | 
			
		||||
    find_statuses_from_tree_path(descendant_ids, account)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def ancestor_ids
 | 
			
		||||
    Rails.cache.fetch("ancestors:#{id}") do
 | 
			
		||||
      ancestors_without_self.pluck(:id)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def ancestors_without_self
 | 
			
		||||
    ancestor_statuses - [self]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def ancestor_statuses
 | 
			
		||||
    Status.find_by_sql([<<-SQL.squish, id: id])
 | 
			
		||||
      WITH RECURSIVE search_tree(id, in_reply_to_id, path)
 | 
			
		||||
      AS (
 | 
			
		||||
        SELECT id, in_reply_to_id, ARRAY[id]
 | 
			
		||||
        FROM statuses
 | 
			
		||||
        WHERE id = :id
 | 
			
		||||
        UNION ALL
 | 
			
		||||
        SELECT statuses.id, statuses.in_reply_to_id, path || statuses.id
 | 
			
		||||
        FROM search_tree
 | 
			
		||||
        JOIN statuses ON statuses.id = search_tree.in_reply_to_id
 | 
			
		||||
        WHERE NOT statuses.id = ANY(path)
 | 
			
		||||
      )
 | 
			
		||||
      SELECT id
 | 
			
		||||
      FROM search_tree
 | 
			
		||||
      ORDER BY path DESC
 | 
			
		||||
    SQL
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def descendant_ids
 | 
			
		||||
    descendants_without_self.pluck(:id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def descendants_without_self
 | 
			
		||||
    descendant_statuses - [self]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def descendant_statuses
 | 
			
		||||
    Status.find_by_sql([<<-SQL.squish, id: id])
 | 
			
		||||
      WITH RECURSIVE search_tree(id, path)
 | 
			
		||||
      AS (
 | 
			
		||||
        SELECT id, ARRAY[id]
 | 
			
		||||
        FROM statuses
 | 
			
		||||
        WHERE id = :id
 | 
			
		||||
        UNION ALL
 | 
			
		||||
        SELECT statuses.id, path || statuses.id
 | 
			
		||||
        FROM search_tree
 | 
			
		||||
        JOIN statuses ON statuses.in_reply_to_id = search_tree.id
 | 
			
		||||
        WHERE NOT statuses.id = ANY(path)
 | 
			
		||||
      )
 | 
			
		||||
      SELECT id
 | 
			
		||||
      FROM search_tree
 | 
			
		||||
      ORDER BY path
 | 
			
		||||
    SQL
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def find_statuses_from_tree_path(ids, account)
 | 
			
		||||
    statuses = statuses_with_accounts(ids).to_a
 | 
			
		||||
 | 
			
		||||
    # FIXME: n+1 bonanza
 | 
			
		||||
    statuses.reject! { |status| filter_from_context?(status, account) }
 | 
			
		||||
 | 
			
		||||
    # Order ancestors/descendants by tree path
 | 
			
		||||
    statuses.sort_by! { |status| ids.index(status.id) }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def statuses_with_accounts(ids)
 | 
			
		||||
    Status.where(id: ids).includes(:account)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def filter_from_context?(status, account)
 | 
			
		||||
    StatusFilter.new(status, account).filtered?
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -28,6 +28,7 @@ class Status < ApplicationRecord
 | 
			
		||||
  include Paginable
 | 
			
		||||
  include Streamable
 | 
			
		||||
  include Cacheable
 | 
			
		||||
  include StatusThreadingConcern
 | 
			
		||||
 | 
			
		||||
  enum visibility: [:public, :unlisted, :private, :direct], _suffix: :visibility
 | 
			
		||||
 | 
			
		||||
@@ -115,16 +116,6 @@ class Status < ApplicationRecord
 | 
			
		||||
    private_visibility? || direct_visibility?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def ancestors(account = nil)
 | 
			
		||||
    ids = Rails.cache.fetch("ancestors:#{id}") { (Status.find_by_sql(['WITH RECURSIVE search_tree(id, in_reply_to_id, path) AS (SELECT id, in_reply_to_id, ARRAY[id] FROM statuses WHERE id = ? UNION ALL SELECT statuses.id, statuses.in_reply_to_id, path || statuses.id FROM search_tree JOIN statuses ON statuses.id = search_tree.in_reply_to_id WHERE NOT statuses.id = ANY(path)) SELECT id FROM search_tree ORDER BY path DESC', id]) - [self]).pluck(:id) }
 | 
			
		||||
    find_statuses_from_tree_path(ids, account)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def descendants(account = nil)
 | 
			
		||||
    ids = (Status.find_by_sql(['WITH RECURSIVE search_tree(id, path) AS (SELECT id, ARRAY[id] FROM statuses WHERE id = ? UNION ALL SELECT statuses.id, path || statuses.id FROM search_tree JOIN statuses ON statuses.in_reply_to_id = search_tree.id WHERE NOT statuses.id = ANY(path)) SELECT id FROM search_tree ORDER BY path', id]) - [self]).pluck(:id)
 | 
			
		||||
    find_statuses_from_tree_path(ids, account)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def non_sensitive_with_media?
 | 
			
		||||
    !sensitive? && media_attachments.any?
 | 
			
		||||
  end
 | 
			
		||||
@@ -277,23 +268,4 @@ class Status < ApplicationRecord
 | 
			
		||||
      thread.account_id
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def find_statuses_from_tree_path(ids, account)
 | 
			
		||||
    statuses = Status.where(id: ids).includes(:account).to_a
 | 
			
		||||
 | 
			
		||||
    # FIXME: n+1 bonanza
 | 
			
		||||
    statuses.reject! { |status| filter_from_context?(status, account) }
 | 
			
		||||
 | 
			
		||||
    # Order ancestors/descendants by tree path
 | 
			
		||||
    statuses.sort_by! { |status| ids.index(status.id) }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def filter_from_context?(status, account)
 | 
			
		||||
    should_filter   = account&.blocking?(status.account_id)
 | 
			
		||||
    should_filter ||= account&.domain_blocking?(status.account_domain)
 | 
			
		||||
    should_filter ||= account&.muting?(status.account_id)
 | 
			
		||||
    should_filter ||= (status.account.silenced? && !account&.following?(status.account_id))
 | 
			
		||||
    should_filter ||= !StatusPolicy.new(account, status).show?
 | 
			
		||||
    should_filter
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user