Add ability to filter individual posts (#18945)
* Add database table for status-specific filters * Add REST endpoints, entities and attributes * Show status filters in /filters interface * Perform server-side filtering for individual posts filters * Fix filtering on context mismatch * Refactor `toServerSideType` by moving it to its own module * Move loupe and delete icons to their own module * Add ability to filter individual posts from WebUI * Replace keyword list by warnings (expired, context mismatch) * Refactor server-side filtering code * Add tests
This commit is contained in:
parent
d156e9b823
commit
50487db122
40 changed files with 1138 additions and 63 deletions
|
@ -249,15 +249,7 @@ module AccountInteractions
|
|||
|
||||
def status_matches_filters(status)
|
||||
active_filters = CustomFilter.cached_filters_for(id)
|
||||
|
||||
filter_matches = active_filters.filter_map do |filter, rules|
|
||||
next if rules[:keywords].blank?
|
||||
|
||||
match = rules[:keywords].match(status.proper.searchable_text)
|
||||
FilterResultPresenter.new(filter: filter, keyword_matches: [match.to_s]) unless match.nil?
|
||||
end
|
||||
|
||||
filter_matches
|
||||
CustomFilter.apply_cached_filters(active_filters, status)
|
||||
end
|
||||
|
||||
def followers_for_local_distribution
|
||||
|
|
|
@ -34,6 +34,7 @@ class CustomFilter < ApplicationRecord
|
|||
|
||||
belongs_to :account
|
||||
has_many :keywords, class_name: 'CustomFilterKeyword', foreign_key: :custom_filter_id, inverse_of: :custom_filter, dependent: :destroy
|
||||
has_many :statuses, class_name: 'CustomFilterStatus', foreign_key: :custom_filter_id, inverse_of: :custom_filter, dependent: :destroy
|
||||
accepts_nested_attributes_for :keywords, reject_if: :all_blank, allow_destroy: true
|
||||
|
||||
validates :title, :context, presence: true
|
||||
|
@ -62,8 +63,10 @@ class CustomFilter < ApplicationRecord
|
|||
|
||||
def self.cached_filters_for(account_id)
|
||||
active_filters = Rails.cache.fetch("filters:v3:#{account_id}") do
|
||||
filters_hash = {}
|
||||
|
||||
scope = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account_id: account_id }).where(Arel.sql('expires_at IS NULL OR expires_at > NOW()'))
|
||||
scope.to_a.group_by(&:custom_filter).map do |filter, keywords|
|
||||
scope.to_a.group_by(&:custom_filter).each do |filter, keywords|
|
||||
keywords.map! do |keyword|
|
||||
if keyword.whole_word
|
||||
sb = /\A[[:word:]]/.match?(keyword.keyword) ? '\b' : ''
|
||||
|
@ -74,13 +77,34 @@ class CustomFilter < ApplicationRecord
|
|||
/#{Regexp.escape(keyword.keyword)}/i
|
||||
end
|
||||
end
|
||||
[filter, { keywords: Regexp.union(keywords) }]
|
||||
|
||||
filters_hash[filter.id] = { keywords: Regexp.union(keywords), filter: filter }
|
||||
end.to_h
|
||||
|
||||
scope = CustomFilterStatus.includes(:custom_filter).where(custom_filter: { account_id: account_id }).where(Arel.sql('expires_at IS NULL OR expires_at > NOW()'))
|
||||
scope.to_a.group_by(&:custom_filter).each do |filter, statuses|
|
||||
filters_hash[filter.id] ||= { filter: filter }
|
||||
filters_hash[filter.id].merge!(status_ids: statuses.map(&:status_id))
|
||||
end
|
||||
|
||||
filters_hash.values.map { |cache| [cache.delete(:filter), cache] }
|
||||
end.to_a
|
||||
|
||||
active_filters.select { |custom_filter, _| !custom_filter.expired? }
|
||||
end
|
||||
|
||||
def self.apply_cached_filters(cached_filters, status)
|
||||
cached_filters.filter_map do |filter, rules|
|
||||
match = rules[:keywords].match(status.proper.searchable_text) if rules[:keywords].present?
|
||||
keyword_matches = [match.to_s] unless match.nil?
|
||||
|
||||
status_matches = [status.id, status.reblog_of_id].compact & rules[:status_ids] if rules[:status_ids].present?
|
||||
|
||||
next if keyword_matches.blank? && status_matches.blank?
|
||||
FilterResultPresenter.new(filter: filter, keyword_matches: keyword_matches, status_matches: status_matches)
|
||||
end
|
||||
end
|
||||
|
||||
def prepare_cache_invalidation!
|
||||
@should_invalidate_cache = true
|
||||
end
|
||||
|
|
37
app/models/custom_filter_status.rb
Normal file
37
app/models/custom_filter_status.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: custom_filter_statuses
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# custom_filter_id :bigint(8) not null
|
||||
# status_id :bigint(8) default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class CustomFilterStatus < ApplicationRecord
|
||||
belongs_to :custom_filter
|
||||
belongs_to :status
|
||||
|
||||
validates :status, uniqueness: { scope: :custom_filter }
|
||||
validate :validate_status_access
|
||||
|
||||
before_save :prepare_cache_invalidation!
|
||||
before_destroy :prepare_cache_invalidation!
|
||||
after_commit :invalidate_cache!
|
||||
|
||||
private
|
||||
|
||||
def validate_status_access
|
||||
errors.add(:status_id, :invalid) unless StatusPolicy.new(custom_filter.account, status).show?
|
||||
end
|
||||
|
||||
def prepare_cache_invalidation!
|
||||
custom_filter.prepare_cache_invalidation!
|
||||
end
|
||||
|
||||
def invalidate_cache!
|
||||
custom_filter.invalidate_cache!
|
||||
end
|
||||
end
|
34
app/models/form/status_filter_batch_action.rb
Normal file
34
app/models/form/status_filter_batch_action.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Form::StatusFilterBatchAction
|
||||
include ActiveModel::Model
|
||||
include AccountableConcern
|
||||
include Authorization
|
||||
|
||||
attr_accessor :current_account, :type,
|
||||
:status_filter_ids, :filter_id
|
||||
|
||||
def save!
|
||||
process_action!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def status_filters
|
||||
filter = current_account.custom_filters.find(filter_id)
|
||||
filter.statuses.where(id: status_filter_ids)
|
||||
end
|
||||
|
||||
def process_action!
|
||||
return if status_filter_ids.empty?
|
||||
|
||||
case type
|
||||
when 'remove'
|
||||
handle_remove!
|
||||
end
|
||||
end
|
||||
|
||||
def handle_remove!
|
||||
status_filters.destroy_all
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue