about summary refs log tree commit diff
path: root/app/controllers
diff options
context:
space:
mode:
authorClaire <claire.github-309c@sitedethib.com>2022-06-28 09:42:13 +0200
committerGitHub <noreply@github.com>2022-06-28 09:42:13 +0200
commit02851848e964675bb59919fa5fd1bdee2c1c29db (patch)
tree55f0836a4dda9f8de3a9c9511d59e5b726fd77a7 /app/controllers
parent5823ae70c4c7c297c8d69ecd0be8df65019411e3 (diff)
Revamp post filtering system (#18058)
* Add model for custom filter keywords

* Use CustomFilterKeyword internally

Does not change the API

* Fix /filters/edit and /filters/new

* Add migration tests

* Remove whole_word column from custom_filters (covered by custom_filter_keywords)

* Redesign /filters

Instead of a list, present a card that displays more information and handles
multiple keywords per filter.

* Redesign /filters/new and /filters/edit to add and remove keywords

This adds a new gem dependency: cocoon, as well as a npm dependency:
cocoon-js-vanilla. Those are used to easily populate and remove form fields
from the user interface when manipulating multiple keyword filters at once.

* Add /api/v2/filters to edit filter with multiple keywords

Entities:
- `Filter`: `id`, `title`, `filter_action` (either `hide` or `warn`), `context`
  `keywords`
- `FilterKeyword`: `id`, `keyword`, `whole_word`

API endpoits:
- `GET /api/v2/filters` to list filters (including keywords)
- `POST /api/v2/filters` to create a new filter
  `keywords_attributes` can also be passed to create keywords in one request
- `GET /api/v2/filters/:id` to read a particular filter
- `PUT /api/v2/filters/:id` to update a new filter
  `keywords_attributes` can also be passed to edit, delete or add keywords in
   one request
- `DELETE /api/v2/filters/:id` to delete a particular filter
- `GET /api/v2/filters/:id/keywords` to list keywords for a filter
- `POST /api/v2/filters/:filter_id/keywords/:id` to add a new keyword to a
   filter
- `GET /api/v2/filter_keywords/:id` to read a particular keyword
- `PUT /api/v2/filter_keywords/:id` to edit a particular keyword
- `DELETE /api/v2/filter_keywords/:id` to delete a particular keyword

* Change from `irreversible` boolean to `action` enum

* Remove irrelevent `irreversible_must_be_within_context` check

* Fix /filters/new and /filters/edit with update for filter_action

* Fix Rubocop/Codeclimate complaining about task names

* Refactor FeedManager#phrase_filtered?

This moves regexp building and filter caching to the `CustomFilter` class.

This does not change the functional behavior yet, but this changes how the
cache is built, doing per-custom_filter regexps so that filters can be matched
independently, while still offering caching.

* Perform server-side filtering and output result in REST API

* Fix numerous filters_changed events being sent when editing multiple keywords at once

* Add some tests

* Use the new API in the WebUI

- use client-side logic for filters we have fetched rules for.
  This is so that filter changes can be retroactively applied without
  reloading the UI.
- use server-side logic for filters we haven't fetched rules for yet
  (e.g. network error, or initial timeline loading)

* Minor optimizations and refactoring

* Perform server-side filtering on the streaming server

* Change the wording of filter action labels

* Fix issues pointed out by linter

* Change design of “Show anyway” link in accordence to review comments

* Drop “irreversible” filtering behavior

* Move /api/v2/filter_keywords to /api/v1/filters/keywords

* Rename `filter_results` attribute to `filtered`

* Rename REST::LegacyFilterSerializer to REST::V1::FilterSerializer

* Fix systemChannelId value in streaming server

* Simplify code by removing client-side filtering code

The simplifcation comes at a cost though: filters aren't retroactively
applied anymore.
Diffstat (limited to 'app/controllers')
-rw-r--r--app/controllers/api/v1/filters/keywords_controller.rb50
-rw-r--r--app/controllers/api/v1/filters_controller.rb35
-rw-r--r--app/controllers/api/v2/filters_controller.rb48
-rw-r--r--app/controllers/filters_controller.rb12
4 files changed, 129 insertions, 16 deletions
diff --git a/app/controllers/api/v1/filters/keywords_controller.rb b/app/controllers/api/v1/filters/keywords_controller.rb
new file mode 100644
index 000000000..d3718a137
--- /dev/null
+++ b/app/controllers/api/v1/filters/keywords_controller.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+class Api::V1::Filters::KeywordsController < Api::BaseController
+  before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show]
+  before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show]
+  before_action :require_user!
+
+  before_action :set_keywords, only: :index
+  before_action :set_keyword, only: [:show, :update, :destroy]
+
+  def index
+    render json: @keywords, each_serializer: REST::FilterKeywordSerializer
+  end
+
+  def create
+    @keyword = current_account.custom_filters.find(params[:filter_id]).keywords.create!(resource_params)
+
+    render json: @keyword, serializer: REST::FilterKeywordSerializer
+  end
+
+  def show
+    render json: @keyword, serializer: REST::FilterKeywordSerializer
+  end
+
+  def update
+    @keyword.update!(resource_params)
+
+    render json: @keyword, serializer: REST::FilterKeywordSerializer
+  end
+
+  def destroy
+    @keyword.destroy!
+    render_empty
+  end
+
+  private
+
+  def set_keywords
+    filter = current_account.custom_filters.includes(:keywords).find(params[:filter_id])
+    @keywords = filter.keywords
+  end
+
+  def set_keyword
+    @keyword = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account: current_account }).find(params[:id])
+  end
+
+  def resource_params
+    params.permit(:keyword, :whole_word)
+  end
+end
diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb
index b0ace3af0..07cd14147 100644
--- a/app/controllers/api/v1/filters_controller.rb
+++ b/app/controllers/api/v1/filters_controller.rb
@@ -8,21 +8,32 @@ class Api::V1::FiltersController < Api::BaseController
   before_action :set_filter, only: [:show, :update, :destroy]
 
   def index
-    render json: @filters, each_serializer: REST::FilterSerializer
+    render json: @filters, each_serializer: REST::V1::FilterSerializer
   end
 
   def create
-    @filter = current_account.custom_filters.create!(resource_params)
-    render json: @filter, serializer: REST::FilterSerializer
+    ApplicationRecord.transaction do
+      filter_category = current_account.custom_filters.create!(resource_params)
+      @filter = filter_category.keywords.create!(keyword_params)
+    end
+
+    render json: @filter, serializer: REST::V1::FilterSerializer
   end
 
   def show
-    render json: @filter, serializer: REST::FilterSerializer
+    render json: @filter, serializer: REST::V1::FilterSerializer
   end
 
   def update
-    @filter.update!(resource_params)
-    render json: @filter, serializer: REST::FilterSerializer
+    ApplicationRecord.transaction do
+      @filter.update!(keyword_params)
+      @filter.custom_filter.assign_attributes(filter_params)
+      raise Mastodon::ValidationError, I18n.t('filters.errors.deprecated_api_multiple_keywords') if @filter.custom_filter.changed? && @filter.custom_filter.keywords.count > 1
+
+      @filter.custom_filter.save!
+    end
+
+    render json: @filter, serializer: REST::V1::FilterSerializer
   end
 
   def destroy
@@ -33,14 +44,22 @@ class Api::V1::FiltersController < Api::BaseController
   private
 
   def set_filters
-    @filters = current_account.custom_filters
+    @filters = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account: current_account })
   end
 
   def set_filter
-    @filter = current_account.custom_filters.find(params[:id])
+    @filter = CustomFilterKeyword.includes(:custom_filter).where(custom_filter: { account: current_account }).find(params[:id])
   end
 
   def resource_params
     params.permit(:phrase, :expires_in, :irreversible, :whole_word, context: [])
   end
+
+  def filter_params
+    resource_params.slice(:expires_in, :irreversible, :context)
+  end
+
+  def keyword_params
+    resource_params.slice(:phrase, :whole_word)
+  end
 end
diff --git a/app/controllers/api/v2/filters_controller.rb b/app/controllers/api/v2/filters_controller.rb
new file mode 100644
index 000000000..8ff3076cf
--- /dev/null
+++ b/app/controllers/api/v2/filters_controller.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+class Api::V2::FiltersController < Api::BaseController
+  before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show]
+  before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show]
+  before_action :require_user!
+  before_action :set_filters, only: :index
+  before_action :set_filter, only: [:show, :update, :destroy]
+
+  def index
+    render json: @filters, each_serializer: REST::FilterSerializer, rules_requested: true
+  end
+
+  def create
+    @filter = current_account.custom_filters.create!(resource_params)
+
+    render json: @filter, serializer: REST::FilterSerializer, rules_requested: true
+  end
+
+  def show
+    render json: @filter, serializer: REST::FilterSerializer, rules_requested: true
+  end
+
+  def update
+    @filter.update!(resource_params)
+
+    render json: @filter, serializer: REST::FilterSerializer, rules_requested: true
+  end
+
+  def destroy
+    @filter.destroy!
+    render_empty
+  end
+
+  private
+
+  def set_filters
+    @filters = current_account.custom_filters.includes(:keywords)
+  end
+
+  def set_filter
+    @filter = current_account.custom_filters.find(params[:id])
+  end
+
+  def resource_params
+    params.permit(:title, :expires_in, :filter_action, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
+  end
+end
diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb
index 79a1ab02b..5ed53bce1 100644
--- a/app/controllers/filters_controller.rb
+++ b/app/controllers/filters_controller.rb
@@ -4,16 +4,16 @@ class FiltersController < ApplicationController
   layout 'admin'
 
   before_action :authenticate_user!
-  before_action :set_filters, only: :index
   before_action :set_filter, only: [:edit, :update, :destroy]
   before_action :set_body_classes
 
   def index
-    @filters = current_account.custom_filters.order(:phrase)
+    @filters = current_account.custom_filters.includes(:keywords).order(:phrase)
   end
 
   def new
-    @filter = current_account.custom_filters.build
+    @filter = current_account.custom_filters.build(action: :warn)
+    @filter.keywords.build
   end
 
   def create
@@ -43,16 +43,12 @@ class FiltersController < ApplicationController
 
   private
 
-  def set_filters
-    @filters = current_account.custom_filters
-  end
-
   def set_filter
     @filter = current_account.custom_filters.find(params[:id])
   end
 
   def resource_params
-    params.require(:custom_filter).permit(:phrase, :expires_in, :irreversible, :whole_word, context: [])
+    params.require(:custom_filter).permit(:title, :expires_in, :filter_action, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
   end
 
   def set_body_classes