about summary refs log tree commit diff
path: root/app/models/custom_filter.rb
blob: 985eab1254cea8db40ea37bd606c01a487ac23a9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# 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
  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
      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|
        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
        [filter, { keywords: Regexp.union(keywords) }]
      end
    end.to_a

    active_filters.select { |custom_filter, _| !custom_filter.expired? }
  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