123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- # frozen_string_literal: true
- # == Schema Information
- #
- # Table name: custom_filters
- #
- # id :bigint(8) not null, primary key
- # account_id :bigint(8)
- # expires_at :datetime
- # phrase :text default(""), not null
- # context :string default([]), not null, is an Array
- # created_at :datetime not null
- # updated_at :datetime not null
- # action :integer default("warn"), not null
- #
- class CustomFilter < ApplicationRecord
- self.ignored_columns = %w(whole_word irreversible)
- alias_attribute :title, :phrase
- alias_attribute :filter_action, :action
- VALID_CONTEXTS = %w(
- home
- notifications
- public
- thread
- account
- ).freeze
- include Expireable
- include Redisable
- enum action: [:warn, :hide], _suffix: :action
- 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
- validate :context_must_be_valid
- before_validation :clean_up_contexts
- before_save :prepare_cache_invalidation!
- before_destroy :prepare_cache_invalidation!
- after_commit :invalidate_cache!
- def expires_in
- return @expires_in if defined?(@expires_in)
- return nil if expires_at.nil?
- [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].find { |expires_in| expires_in.from_now >= expires_at }
- end
- def irreversible=(value)
- self.action = value ? :hide : :warn
- end
- def irreversible?
- hide_action?
- end
- 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).each do |filter, keywords|
- keywords.map! do |keyword|
- if keyword.whole_word
- sb = /\A[[:word:]]/.match?(keyword.keyword) ? '\b' : ''
- eb = /[[:word:]]\z/.match?(keyword.keyword) ? '\b' : ''
- /(?mix:#{sb}#{Regexp.escape(keyword.keyword)}#{eb})/
- else
- /#{Regexp.escape(keyword.keyword)}/i
- end
- end
- 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
- def invalidate_cache!
- return unless @should_invalidate_cache
- @should_invalidate_cache = false
- Rails.cache.delete("filters:v3:#{account_id}")
- redis.publish("timeline:#{account_id}", Oj.dump(event: :filters_changed))
- redis.publish("timeline:system:#{account_id}", Oj.dump(event: :filters_changed))
- end
- private
- def clean_up_contexts
- self.context = Array(context).map(&:strip).filter_map(&:presence)
- end
- def context_must_be_valid
- errors.add(:context, I18n.t('filters.errors.invalid_context')) if context.empty? || context.any? { |c| !VALID_CONTEXTS.include?(c) }
- end
- end
|