about summary refs log tree commit diff
path: root/app/models
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2018-06-29 15:34:36 +0200
committerGitHub <noreply@github.com>2018-06-29 15:34:36 +0200
commitcdb101340a20183a82889f811d9311c370c855e5 (patch)
treedaacd56d1edbf359b0d97a926e90b999ba7f6129 /app/models
parentfbee9b5ac898e571e384792a92b40fa1524cf127 (diff)
Keyword/phrase filtering (#7905)
* Add keyword filtering

    GET|POST       /api/v1/filters
    GET|PUT|DELETE /api/v1/filters/:id

- Irreversible filters can drop toots from home or notifications
- Other filters can hide toots through the client app
- Filters use a phrase valid in particular contexts, expiration

* Make sure expired filters don't get applied client-side

* Add missing API methods

* Remove "regex filter" from column settings

* Add tests

* Add test for FeedManager

* Add CustomFilter test

* Add UI for managing filters

* Add streaming API event to allow syncing filters

* Fix tests
Diffstat (limited to 'app/models')
-rw-r--r--app/models/account.rb1
-rw-r--r--app/models/concerns/expireable.rb24
-rw-r--r--app/models/custom_filter.rb55
-rw-r--r--app/models/invite.rb18
4 files changed, 82 insertions, 16 deletions
diff --git a/app/models/account.rb b/app/models/account.rb
index c3eea79cc..40a45b1f8 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -99,6 +99,7 @@ class Account < ApplicationRecord
   has_many :targeted_reports, class_name: 'Report', foreign_key: :target_account_id
 
   has_many :report_notes, dependent: :destroy
+  has_many :custom_filters, inverse_of: :account, dependent: :destroy
 
   # Moderation notes
   has_many :account_moderation_notes, dependent: :destroy
diff --git a/app/models/concerns/expireable.rb b/app/models/concerns/expireable.rb
new file mode 100644
index 000000000..444ccdfdb
--- /dev/null
+++ b/app/models/concerns/expireable.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Expireable
+  extend ActiveSupport::Concern
+
+  included do
+    scope :expired, -> { where.not(expires_at: nil).where('expires_at < ?', Time.now.utc) }
+
+    attr_reader :expires_in
+
+    def expires_in=(interval)
+      self.expires_at = interval.to_i.seconds.from_now unless interval.blank?
+      @expires_in     = interval
+    end
+
+    def expire!
+      touch(:expires_at)
+    end
+
+    def expired?
+      !expires_at.nil? && expires_at < Time.now.utc
+    end
+  end
+end
diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb
new file mode 100644
index 000000000..2c1a54375
--- /dev/null
+++ b/app/models/custom_filter.rb
@@ -0,0 +1,55 @@
+# 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
+#  irreversible :boolean          default(FALSE), not null
+#  created_at   :datetime         not null
+#  updated_at   :datetime         not null
+#
+
+class CustomFilter < ApplicationRecord
+  VALID_CONTEXTS = %w(
+    home
+    notifications
+    public
+    thread
+  ).freeze
+
+  include Expireable
+
+  belongs_to :account
+
+  validates :phrase, :context, presence: true
+  validate :context_must_be_valid
+  validate :irreversible_must_be_within_context
+
+  scope :active_irreversible, -> { where(irreversible: true).where(Arel.sql('expires_at IS NULL OR expires_at > NOW()')) }
+
+  before_validation :clean_up_contexts
+  after_commit :remove_cache
+
+  private
+
+  def clean_up_contexts
+    self.context = Array(context).map(&:strip).map(&:presence).compact
+  end
+
+  def remove_cache
+    Rails.cache.delete("filters:#{account_id}")
+    Redis.current.publish("timeline:#{account_id}", Oj.dump(event: :filters_changed))
+  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
+
+  def irreversible_must_be_within_context
+    errors.add(:irreversible, I18n.t('filters.errors.invalid_irreversible')) if irreversible? && !context.include?('home') && !context.include?('notifications')
+  end
+end
diff --git a/app/models/invite.rb b/app/models/invite.rb
index d0cc427c4..fe2322462 100644
--- a/app/models/invite.rb
+++ b/app/models/invite.rb
@@ -15,33 +15,19 @@
 #
 
 class Invite < ApplicationRecord
+  include Expireable
+
   belongs_to :user
   has_many :users, inverse_of: :invite
 
   scope :available, -> { where(expires_at: nil).or(where('expires_at >= ?', Time.now.utc)) }
-  scope :expired, -> { where.not(expires_at: nil).where('expires_at < ?', Time.now.utc) }
 
   before_validation :set_code
 
-  attr_reader :expires_in
-
-  def expires_in=(interval)
-    self.expires_at = interval.to_i.seconds.from_now unless interval.blank?
-    @expires_in     = interval
-  end
-
   def valid_for_use?
     (max_uses.nil? || uses < max_uses) && !expired?
   end
 
-  def expire!
-    touch(:expires_at)
-  end
-
-  def expired?
-    !expires_at.nil? && expires_at < Time.now.utc
-  end
-
   private
 
   def set_code