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
This commit is contained in:
parent
fbee9b5ac8
commit
cdb101340a
38 changed files with 530 additions and 72 deletions
|
@ -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
|
||||
|
|
24
app/models/concerns/expireable.rb
Normal file
24
app/models/concerns/expireable.rb
Normal file
|
@ -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
|
55
app/models/custom_filter.rb
Normal file
55
app/models/custom_filter.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue