* Add option to not consider word boundaries when filtering phrases * Add a few tests for keyword/phrase filteringmaster
@@ -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 |
@@ -8,4 +8,7 @@ | |||||
= f.input :irreversible, wrapper: :with_label | = f.input :irreversible, wrapper: :with_label | ||||
.fields-group | .fields-group | ||||
= f.input :whole_word, wrapper: :with_label | |||||
.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 | |||||
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 | |||||
context 'for irreversibly muted phrases' do | |||||
it 'considers word boundaries when matching' do | |||||
alice.custom_filters.create!(phrase: 'bob', context: %w(home), irreversible: true) | |||||
alice.follow!(jeff) | |||||
status = Fabricate(:status, text: 'bobcats', account: jeff) | |||||
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 | ||||