custom_filter.rb 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: custom_filters
  5. #
  6. # id :bigint(8) not null, primary key
  7. # account_id :bigint(8)
  8. # expires_at :datetime
  9. # phrase :text default(""), not null
  10. # context :string default([]), not null, is an Array
  11. # created_at :datetime not null
  12. # updated_at :datetime not null
  13. # action :integer default("warn"), not null
  14. #
  15. class CustomFilter < ApplicationRecord
  16. self.ignored_columns += %w(whole_word irreversible)
  17. alias_attribute :title, :phrase
  18. alias_attribute :filter_action, :action
  19. VALID_CONTEXTS = %w(
  20. home
  21. notifications
  22. public
  23. thread
  24. account
  25. ).freeze
  26. include Expireable
  27. include Redisable
  28. enum action: { warn: 0, hide: 1 }, _suffix: :action
  29. belongs_to :account
  30. has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy
  31. has_many :statuses, class_name: 'CustomFilterStatus', inverse_of: :custom_filter, dependent: :destroy
  32. accepts_nested_attributes_for :keywords, reject_if: :all_blank, allow_destroy: true
  33. validates :title, :context, presence: true
  34. validate :context_must_be_valid
  35. before_validation :clean_up_contexts
  36. before_save :prepare_cache_invalidation!
  37. before_destroy :prepare_cache_invalidation!
  38. after_commit :invalidate_cache!
  39. def expires_in
  40. return @expires_in if defined?(@expires_in)
  41. return nil if expires_at.nil?
  42. [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].find { |expires_in| expires_in.from_now >= expires_at }
  43. end
  44. def irreversible=(value)
  45. self.action = ActiveModel::Type::Boolean.new.cast(value) ? :hide : :warn
  46. end
  47. def irreversible?
  48. hide_action?
  49. end
  50. def self.cached_filters_for(account_id)
  51. active_filters = Rails.cache.fetch("filters:v3:#{account_id}") do
  52. filters_hash = {}
  53. scope = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account_id: account_id }).where(Arel.sql('expires_at IS NULL OR expires_at > NOW()'))
  54. scope.to_a.group_by(&:custom_filter).each do |filter, keywords|
  55. keywords.map! do |keyword|
  56. if keyword.whole_word
  57. sb = /\A[[:word:]]/.match?(keyword.keyword) ? '\b' : ''
  58. eb = /[[:word:]]\z/.match?(keyword.keyword) ? '\b' : ''
  59. /(?mix:#{sb}#{Regexp.escape(keyword.keyword)}#{eb})/
  60. else
  61. /#{Regexp.escape(keyword.keyword)}/i
  62. end
  63. end
  64. filters_hash[filter.id] = { keywords: Regexp.union(keywords), filter: filter }
  65. end.to_h
  66. scope = CustomFilterStatus.includes(:custom_filter).where(custom_filter: { account_id: account_id }).where(Arel.sql('expires_at IS NULL OR expires_at > NOW()'))
  67. scope.to_a.group_by(&:custom_filter).each do |filter, statuses|
  68. filters_hash[filter.id] ||= { filter: filter }
  69. filters_hash[filter.id].merge!(status_ids: statuses.map(&:status_id))
  70. end
  71. filters_hash.values.map { |cache| [cache.delete(:filter), cache] }
  72. end.to_a
  73. active_filters.reject { |custom_filter, _| custom_filter.expired? }
  74. end
  75. def self.apply_cached_filters(cached_filters, status)
  76. cached_filters.filter_map do |filter, rules|
  77. match = rules[:keywords].match(status.proper.searchable_text) if rules[:keywords].present?
  78. keyword_matches = [match.to_s] unless match.nil?
  79. status_matches = [status.id, status.reblog_of_id].compact & rules[:status_ids] if rules[:status_ids].present?
  80. next if keyword_matches.blank? && status_matches.blank?
  81. FilterResultPresenter.new(filter: filter, keyword_matches: keyword_matches, status_matches: status_matches)
  82. end
  83. end
  84. def prepare_cache_invalidation!
  85. @should_invalidate_cache = true
  86. end
  87. def invalidate_cache!
  88. return unless @should_invalidate_cache
  89. @should_invalidate_cache = false
  90. Rails.cache.delete("filters:v3:#{account_id}")
  91. redis.publish("timeline:#{account_id}", Oj.dump(event: :filters_changed))
  92. redis.publish("timeline:system:#{account_id}", Oj.dump(event: :filters_changed))
  93. end
  94. private
  95. def clean_up_contexts
  96. self.context = Array(context).map(&:strip).filter_map(&:presence)
  97. end
  98. def context_must_be_valid
  99. errors.add(:context, I18n.t('filters.errors.invalid_context')) if context.empty? || context.any? { |c| !VALID_CONTEXTS.include?(c) }
  100. end
  101. end