Add option to not consider word boundaries when processing keyword filtering (#7975)
* Add option to not consider word boundaries when filtering phrases * Add a few tests for keyword/phrase filteringgh/stable
parent
451e585b97
commit
1ca4e51eb3
|
@ -43,6 +43,6 @@ class Api::V1::FiltersController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.permit(:phrase, :expires_in, :irreversible, context: [])
|
params.permit(:phrase, :expires_in, :irreversible, :whole_word, context: [])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -45,7 +45,10 @@ export const regexFromFilters = filters => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RegExp(filters.map(filter => escapeRegExp(filter.get('phrase'))).map(expr => `\\b${expr}\\b`).join('|'), 'i');
|
return new RegExp(filters.map(filter => {
|
||||||
|
let expr = escapeRegExp(filter.get('phrase'));
|
||||||
|
return filter.get('whole_word') ? `\\b${expr}\\b` : expr;
|
||||||
|
}).join('|'), 'i');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const makeGetStatus = () => {
|
export const makeGetStatus = () => {
|
||||||
|
|
|
@ -200,7 +200,16 @@ class FeedManager
|
||||||
active_filters = Rails.cache.fetch("filters:#{receiver_id}") { CustomFilter.where(account_id: receiver_id).active_irreversible.to_a }.to_a
|
active_filters = Rails.cache.fetch("filters:#{receiver_id}") { CustomFilter.where(account_id: receiver_id).active_irreversible.to_a }.to_a
|
||||||
|
|
||||||
active_filters.select! { |filter| filter.context.include?(context.to_s) && !filter.expired? }
|
active_filters.select! { |filter| filter.context.include?(context.to_s) && !filter.expired? }
|
||||||
active_filters.map! { |filter| Regexp.new("\\b#{Regexp.escape(filter.phrase)}\\b", true) }
|
active_filters.map! do |filter|
|
||||||
|
if filter.whole_word
|
||||||
|
sb = filter.phrase =~ /\A[[:word:]]/ ? '\b' : ''
|
||||||
|
eb = filter.phrase =~ /[[:word:]]\Z/ ? '\b' : ''
|
||||||
|
|
||||||
|
/(?mix:#{sb}#{Regexp.escape(filter.phrase)}#{eb})/
|
||||||
|
else
|
||||||
|
/#{Regexp.escape(filter.phrase)}/i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return false if active_filters.empty?
|
return false if active_filters.empty?
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
# expires_at :datetime
|
# expires_at :datetime
|
||||||
# phrase :text default(""), not null
|
# phrase :text default(""), not null
|
||||||
# context :string default([]), not null, is an Array
|
# context :string default([]), not null, is an Array
|
||||||
|
# whole_word :boolean default(TRUE), not null
|
||||||
# irreversible :boolean default(FALSE), not null
|
# irreversible :boolean default(FALSE), not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class REST::FilterSerializer < ActiveModel::Serializer
|
class REST::FilterSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :phrase, :context, :expires_at,
|
attributes :id, :phrase, :context, :whole_word, :expires_at,
|
||||||
:irreversible
|
:irreversible
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,5 +7,8 @@
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :irreversible, wrapper: :with_label
|
= f.input :irreversible, wrapper: :with_label
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= f.input :whole_word, wrapper: :with_label
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt')
|
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt')
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||||
|
|
||||||
|
class AddWholeWordToCustomFilter < ActiveRecord::Migration[5.2]
|
||||||
|
include Mastodon::MigrationHelpers
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def change
|
||||||
|
safety_assured do
|
||||||
|
add_column_with_default :custom_filters, :whole_word, :boolean, default: true, allow_null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :custom_filters, :whole_word
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2018_06_28_181026) do
|
ActiveRecord::Schema.define(version: 2018_07_07_154237) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -149,6 +149,7 @@ ActiveRecord::Schema.define(version: 2018_06_28_181026) do
|
||||||
t.text "phrase", default: "", null: false
|
t.text "phrase", default: "", null: false
|
||||||
t.string "context", default: [], null: false, array: true
|
t.string "context", default: [], null: false, array: true
|
||||||
t.boolean "irreversible", default: false, null: false
|
t.boolean "irreversible", default: false, null: false
|
||||||
|
t.boolean "whole_word", default: true, null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["account_id"], name: "index_custom_filters_on_account_id"
|
t.index ["account_id"], name: "index_custom_filters_on_account_id"
|
||||||
|
|
|
@ -127,12 +127,28 @@ RSpec.describe FeedManager do
|
||||||
expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
|
expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns true if status contains irreversibly muted phrase' do
|
context 'for irreversibly muted phrases' do
|
||||||
alice.custom_filters.create!(phrase: 'farts', context: %w(home public), irreversible: true)
|
it 'considers word boundaries when matching' do
|
||||||
alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true)
|
alice.custom_filters.create!(phrase: 'bob', context: %w(home), irreversible: true)
|
||||||
alice.follow!(jeff)
|
alice.follow!(jeff)
|
||||||
status = Fabricate(:status, text: 'i sure like POP TARts', account: jeff)
|
status = Fabricate(:status, text: 'bobcats', account: jeff)
|
||||||
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
|
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be_falsy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true if phrase is contained' do
|
||||||
|
alice.custom_filters.create!(phrase: 'farts', context: %w(home public), irreversible: true)
|
||||||
|
alice.custom_filters.create!(phrase: 'pop tarts', context: %w(home), irreversible: true)
|
||||||
|
alice.follow!(jeff)
|
||||||
|
status = Fabricate(:status, text: 'i sure like POP TARts', account: jeff)
|
||||||
|
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches substrings if whole_word is false' do
|
||||||
|
alice.custom_filters.create!(phrase: 'take', context: %w(home), whole_word: false, irreversible: true)
|
||||||
|
alice.follow!(jeff)
|
||||||
|
status = Fabricate(:status, text: 'shiitake', account: jeff)
|
||||||
|
expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Reference in New Issue