From f6c156197ea0e042a6eb58332d7b0f0dd3e78217 Mon Sep 17 00:00:00 2001 From: "Pia B." Date: Mon, 1 Jun 2026 11:56:17 +0200 Subject: [PATCH] Add export for custom filters (#39085) Co-authored-by: Claire --- .../settings/export_controller_concern.rb | 7 +-- .../exports/custom_filters_controller.rb | 19 ++++++ app/models/export.rb | 17 +++++ app/presenters/export_summary.rb | 6 ++ app/views/settings/exports/show.html.haml | 4 ++ config/locales/en.yml | 2 + config/routes/settings.rb | 1 + .../export_controller_concern_spec.rb | 29 ++++++++- spec/presenters/export_summary_spec.rb | 8 +++ .../settings/exports/custom_filters_spec.rb | 62 +++++++++++++++++++ 10 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 app/controllers/settings/exports/custom_filters_controller.rb create mode 100644 spec/requests/settings/exports/custom_filters_spec.rb diff --git a/app/controllers/concerns/settings/export_controller_concern.rb b/app/controllers/concerns/settings/export_controller_concern.rb index 2cf28cced8..9917a5bd04 100644 --- a/app/controllers/concerns/settings/export_controller_concern.rb +++ b/app/controllers/concerns/settings/export_controller_concern.rb @@ -19,15 +19,12 @@ module Settings::ExportControllerConcern def send_export_file respond_to do |format| - format.csv { send_data export_data, filename: export_filename } + format.csv { send_data export_data, filename: "#{controller_name}.csv" } + format.json { send_data export_data, filename: "#{controller_name}.json" } end end def export_data raise 'Override in controller' end - - def export_filename - "#{controller_name}.csv" - end end diff --git a/app/controllers/settings/exports/custom_filters_controller.rb b/app/controllers/settings/exports/custom_filters_controller.rb new file mode 100644 index 0000000000..ec7c0dfa04 --- /dev/null +++ b/app/controllers/settings/exports/custom_filters_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Settings + module Exports + class CustomFiltersController < BaseController + include Settings::ExportControllerConcern + + def index + send_export_file + end + + private + + def export_data + @export.to_custom_filters_json + end + end + end +end diff --git a/app/models/export.rb b/app/models/export.rb index b430a2a0d9..94b2753375 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -55,6 +55,23 @@ class Export end end + def to_custom_filters_json + data_collection = { custom_filters: [] } + account.custom_filters.includes(:keywords, :statuses).order(:phrase).each do |filter| + keywords_attributes = filter.keywords.map { |k| { keyword: k.keyword, whole_word: k.whole_word } } + + data_collection[:custom_filters] << { + title: filter.title, + expire_at: filter.expires_at, + context: filter.context, + action: filter.action, + keywords_attributes: keywords_attributes, + statuses: filter.statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s.status) }, + } + end + JSON.generate(data_collection) + end + private def to_csv(accounts) diff --git a/app/presenters/export_summary.rb b/app/presenters/export_summary.rb index 8e45aadf67..e0e701094d 100644 --- a/app/presenters/export_summary.rb +++ b/app/presenters/export_summary.rb @@ -6,6 +6,7 @@ class ExportSummary delegate( :blocking, :bookmarks, + :custom_filters, :domain_blocks, :owned_lists, :media_attachments, @@ -27,6 +28,10 @@ class ExportSummary counts[:bookmarks].value end + def total_custom_filters + counts[:custom_filters].value + end + def total_domain_blocks counts[:domain_blocks].value end @@ -61,6 +66,7 @@ class ExportSummary { blocks: account_blocking.async_count, bookmarks: account_bookmarks.async_count, + custom_filters: account_custom_filters.async_count, domain_blocks: account_domain_blocks.async_count, owned_lists: account_owned_lists.async_count, muting: account_muting.async_count, diff --git a/app/views/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml index 5a151be73b..efaa4c9bd7 100644 --- a/app/views/settings/exports/show.html.haml +++ b/app/views/settings/exports/show.html.haml @@ -40,6 +40,10 @@ %th= t('exports.bookmarks') %td= number_with_delimiter @export_summary.total_bookmarks %td= table_link_to 'download', t('exports.csv'), settings_exports_bookmarks_path(format: :csv) + %tr + %th= t('exports.custom_filters') + %td= number_with_delimiter @export_summary.total_custom_filters + %td= table_link_to 'download', t('exports.json'), settings_exports_custom_filters_path(format: :json) %hr.spacer/ diff --git a/config/locales/en.yml b/config/locales/en.yml index f47eac1fe1..00070ad052 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1569,7 +1569,9 @@ en: blocks: You block bookmarks: Bookmarks csv: CSV + custom_filters: Filters domain_blocks: Domain blocks + json: JSON lists: Lists mutes: You mute storage: Media storage diff --git a/config/routes/settings.rb b/config/routes/settings.rb index bec68fc6e2..f34a474f9a 100644 --- a/config/routes/settings.rb +++ b/config/routes/settings.rb @@ -30,6 +30,7 @@ namespace :settings do resources :lists, only: :index resources :domain_blocks, only: :index, controller: :blocked_domains resources :bookmarks, only: :index + resources :custom_filters, only: :index, constraints: { format: :json }, controller: :custom_filters end resources :two_factor_authentication_methods, only: [:index] do diff --git a/spec/controllers/concerns/settings/export_controller_concern_spec.rb b/spec/controllers/concerns/settings/export_controller_concern_spec.rb index 782ec88ecd..8e3f674a09 100644 --- a/spec/controllers/concerns/settings/export_controller_concern_spec.rb +++ b/spec/controllers/concerns/settings/export_controller_concern_spec.rb @@ -15,10 +15,13 @@ RSpec.describe Settings::ExportControllerConcern do end end + def sign_in_user + sign_in Fabricate(:user) + end + describe 'GET #index' do it 'returns a csv of the exported data when signed in' do - user = Fabricate(:user) - sign_in user + sign_in_user get :index, format: :csv expect(response).to have_http_status(200) @@ -33,4 +36,26 @@ RSpec.describe Settings::ExportControllerConcern do expect(response).to have_http_status(401) end end + + context 'when with json format' do + it 'returns a json of the exported data when signed in' do + sign_in_user + get :index, format: :json + + expect(response).to have_http_status(200) + expect(response.media_type).to eq 'application/json' + expect(response) + .to have_attachment('anonymous.json') + expect(response.body).to be_present + end + end + + context 'when without format' do + it 'does not send exported data' do + sign_in_user + get :index + + expect(response).to have_http_status(406) + end + end end diff --git a/spec/presenters/export_summary_spec.rb b/spec/presenters/export_summary_spec.rb index 0ed46c857d..1344b438a8 100644 --- a/spec/presenters/export_summary_spec.rb +++ b/spec/presenters/export_summary_spec.rb @@ -52,6 +52,14 @@ RSpec.describe ExportSummary do end end + describe '#total_custom_filters' do + before { Fabricate.times(2, :custom_filter, account: account) } + + it 'returns the total number of lists' do + expect(subject.total_custom_filters).to eq(2) + end + end + describe '#total_followers' do before { target_accounts.each { |target_account| target_account.follow!(account) } } diff --git a/spec/requests/settings/exports/custom_filters_spec.rb b/spec/requests/settings/exports/custom_filters_spec.rb new file mode 100644 index 0000000000..c9c7645ddb --- /dev/null +++ b/spec/requests/settings/exports/custom_filters_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Settings / Exports / CustomFilters' do + describe 'GET /settings/exports/custom_filters' do + context 'with a signed in user who has custom_filters' do + let(:user) { Fabricate(:user) } + let(:filter) { Fabricate(:custom_filter, account: user.account, phrase: 'foo') } + let(:other_filter) { Fabricate(:custom_filter, account: user.account, phrase: 'bar') } + let!(:keyword) { Fabricate(:custom_filter_keyword, custom_filter: filter) } + let!(:filter_keyword) { Fabricate(:custom_filter_keyword, keyword: 'something', custom_filter: filter, whole_word: false) } + let!(:other_keyword) { Fabricate(:custom_filter_keyword, custom_filter: other_filter) } + let!(:other_filter_keyword) { Fabricate(:custom_filter_keyword, keyword: 'something', custom_filter: other_filter, whole_word: false) } + let!(:status_filter) { Fabricate(:custom_filter_status, custom_filter: filter) } + let(:expected_response_body) do + { 'custom_filters' => [ + { + 'title' => other_filter.phrase, + 'expire_at' => nil, + 'context' => other_filter.context, + 'action' => other_filter.action, + 'keywords_attributes' => [{ + 'keyword' => other_keyword.keyword, + 'whole_word' => other_keyword.whole_word, + }, { + 'keyword' => other_filter_keyword.keyword, + 'whole_word' => other_filter_keyword.whole_word, + }], + 'statuses' => [], + }, + { + 'title' => filter.phrase, + 'expire_at' => nil, + 'context' => filter.context, + 'action' => filter.action, + 'keywords_attributes' => [{ + 'keyword' => keyword.keyword, + 'whole_word' => keyword.whole_word, + }, { + 'keyword' => filter_keyword.keyword, + 'whole_word' => filter_keyword.whole_word, + }], + 'statuses' => [ActivityPub::TagManager.instance.uri_for(status_filter.status)], + }, + ] } + end + + before do + sign_in user + end + + it 'returns a JSON with the custom filters' do + get '/settings/exports/custom_filters.json' + + expect(response).to have_http_status(200) + expect(response.content_type).to eq('application/json') + expect(response.parsed_body).to eq(expected_response_body) + end + end + end +end