about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/settings/preferences_controller.rb3
-rw-r--r--app/lib/status_filter.rb9
-rw-r--r--app/models/status.rb8
-rw-r--r--app/models/user.rb1
-rw-r--r--app/views/settings/preferences/show.html.haml1
-rw-r--r--config/locales/simple_form.en.yml1
-rw-r--r--db/migrate/20191221183232_add_invert_filters_to_user.rb5
-rw-r--r--db/schema.rb3
-rw-r--r--streaming/index.js5
9 files changed, 29 insertions, 7 deletions
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 66e9033d3..4ac654590 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class Settings::PreferencesController < Settings::BaseController
+  include Redisable
+
   layout 'admin'
 
   before_action :authenticate_user!
@@ -29,6 +31,7 @@ class Settings::PreferencesController < Settings::BaseController
       :locale,
       :hide_boosts,
       :only_known,
+      :invert_filters,
       chosen_languages: []
     )
   end
diff --git a/app/lib/status_filter.rb b/app/lib/status_filter.rb
index 81faf129f..fb675cbbd 100644
--- a/app/lib/status_filter.rb
+++ b/app/lib/status_filter.rb
@@ -15,7 +15,7 @@ class StatusFilter
   def filtered?
     return true if status.nil? || account.nil?
     return false if !account.nil? && account.id == status.account_id
-    return true if redis.sismember("filtered_statuses:#{account.id}", status.id)
+    return !account.user.invert_filters if redis.sismember("filtered_statuses:#{account.id}", status.id)
     if blocked_by_policy? || (account_present? && filtered_status?) || silenced_account?
       redis.sadd("filtered_statuses:#{account.id}", status.id)
       return true
@@ -40,7 +40,7 @@ class StatusFilter
     return true if account.user_hides_replies_of_blocker? && reply_to_blocker?
 
     # filtered by user?
-    return true if phrase_filtered?(status, account.id)
+    return true if !account.user.invert_filters && phrase_filtered?(status, account.id)
 
     # kajiht has no filters if status has no mentions
     return false if status&.mentions.blank?
@@ -74,7 +74,10 @@ class StatusFilter
     return true if !@preloaded_relations[:muting] && account.user_hides_mentions_of_muted? && account.muting?(mentioned_account_ids)
     return true if !@preloaded_relations[:blocking] && account.user_hides_mentions_of_blocked? && account.blocking?(mentioned_account_ids)
     return false unless status.reply? && status.private_visibility? && account.user_hides_mentions_outside_scope?
-    !@preloaded_relations[:following] && (mentioned_account_ids - account.following_ids).any?
+    return true if !@preloaded_relations[:following] && (mentioned_account_ids - account.following_ids).any?
+
+    # filtered by user?
+    account.user.invert_filters && !phrase_filtered?(status, account.id)
   end
 
   def reply_to_blocked?
diff --git a/app/models/status.rb b/app/models/status.rb
index 8a1680ccf..a47f846a5 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -580,7 +580,13 @@ class Status < ApplicationRecord
       query = query.in_chosen_languages(account) if account.chosen_languages.present?
       query = query.reply_not_excluded_by_account(account) unless tag_timeline
       query = query.mention_not_excluded_by_account(account)
-      query = query.regex_not_filtered_by_account(account.id) if account.custom_filters.present?
+      unless account.custom_filters.nil?
+        if account.user.invert_filters
+          query = query.regex_filtered_by_account(account.id)
+        else
+          query = query.regex_not_filtered_by_account(account.id)
+        end
+      end
       query = query.not_missing_media_desc if account.filter_undescribed?
       query.merge(account_silencing_filter(account))
     end
diff --git a/app/models/user.rb b/app/models/user.rb
index 59067cb36..f6e1a369d 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -41,6 +41,7 @@
 #  vars                      :jsonb            not null
 #  hide_boosts               :boolean
 #  only_known                :boolean
+#  invert_filters            :boolean          default(FALSE), not null
 #
 
 class User < ApplicationRecord
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index 81bfc7345..8f4563fde 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -46,6 +46,7 @@
   %hr#settings_other/
 
   .fields-group
+    = f.input :invert_filters, as: :boolean, wrapper: :with_label
     = f.input :setting_rawr_federated, as: :boolean, wrapper: :with_label
     = f.input :hide_boosts, as: :boolean, wrapper: :with_label
     = f.input :only_known, as: :boolean, wrapper: :with_label
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 6983d0867..cb106082d 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -182,6 +182,7 @@ en:
         setting_unfollow_modal: Show confirmation dialog before unfollowing someone
         hide_boosts: Don't show boosts on any timeline
         only_known: Only show posts from packmates on all timelines
+        invert_filters: Use allow list mode for filters
         severity: Severity
         type: Import type
         username: Username
diff --git a/db/migrate/20191221183232_add_invert_filters_to_user.rb b/db/migrate/20191221183232_add_invert_filters_to_user.rb
new file mode 100644
index 000000000..d30b865f1
--- /dev/null
+++ b/db/migrate/20191221183232_add_invert_filters_to_user.rb
@@ -0,0 +1,5 @@
+class AddInvertFiltersToUser < ActiveRecord::Migration[5.2]
+  def change
+    add_column :users, :invert_filters, :boolean, null: false, default: false
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f6f1ba209..631469a2a 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2019_12_21_164844) do
+ActiveRecord::Schema.define(version: 2019_12_21_183232) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "pg_trgm"
@@ -810,6 +810,7 @@ ActiveRecord::Schema.define(version: 2019_12_21_164844) do
     t.jsonb "vars", default: {}, null: false
     t.boolean "hide_boosts"
     t.boolean "only_known"
+    t.boolean "invert_filters", default: false, null: false
     t.index ["account_id"], name: "index_users_on_account_id"
     t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
     t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id"
diff --git a/streaming/index.js b/streaming/index.js
index 20ec00d9f..db421ff03 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -202,7 +202,7 @@ const startWorker = (workerId) => {
         return;
       }
 
-      client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, users.hide_boosts, users.only_known, oauth_access_tokens.scopes FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token], (err, result) => {
+      client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, users.hide_boosts, users.only_known, users.invert_filters, oauth_access_tokens.scopes FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token], (err, result) => {
         done();
 
         if (err) {
@@ -232,6 +232,7 @@ const startWorker = (workerId) => {
         req.chosenLanguages = result.rows[0].chosen_languages;
         req.hideBoosts = result.rows[0].hide_boosts;
         req.onlyKnown = result.rows[0].only_known;
+        req.invertFilters = result.rows[0].invert_filters;
         req.allowNotifications = scopes.some(scope => ['read', 'read:notifications'].includes(scope));
 
         next();
@@ -423,7 +424,7 @@ const startWorker = (workerId) => {
         }
 
         const queries = [
-          client.query(`SELECT 1 FROM blocks WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 3)})) OR (account_id = $2 AND target_account_id = $1) UNION SELECT 1 FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 3)}) UNION SELECT 1 FROM normalized_statuses WHERE status_id = $3 AND text ~ ANY(ARRAY(SELECT f_normalize(phrase) FROM custom_filters WHERE account_id = $1)) UNION SELECT 1 FROM media_attachments WHERE (1 = (SELECT 1 FROM accounts WHERE id = $1 AND filter_undescribed)) AND status_id = $3 AND description IS NULL LIMIT 1`, [req.accountId, unpackedPayload.account.id, unpackedPayload.id].concat(targetAccountIds)),
+          client.query(`SELECT 1 FROM blocks WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 3)})) OR (account_id = $2 AND target_account_id = $1) UNION SELECT 1 FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 3)}) UNION SELECT 1 FROM normalized_statuses WHERE status_id = $3 AND text ${req.invertFilters ? '!~' : '~'} ANY(ARRAY(SELECT f_normalize(phrase) FROM custom_filters WHERE account_id = $1)) UNION SELECT 1 FROM media_attachments WHERE (1 = (SELECT 1 FROM accounts WHERE id = $1 AND filter_undescribed)) AND status_id = $3 AND description IS NULL LIMIT 1`, [req.accountId, unpackedPayload.account.id, unpackedPayload.id].concat(targetAccountIds)),
         ];
 
         if (accountDomain) {