diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index 4345b61ac7..c97e9720ad 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -52,11 +52,11 @@ class Api::V1::FiltersController < Api::BaseController end def resource_params - params.permit(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_quote, :whole_word, context: []) + params.permit(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_quote, :with_profile, :whole_word, context: []) end def filter_params - resource_params.slice(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_quote, :context) + resource_params.slice(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_quote, :with_profile, :context) end def keyword_params diff --git a/app/controllers/api/v2/filters_controller.rb b/app/controllers/api/v2/filters_controller.rb index 0b63caa36a..51e778e1d6 100644 --- a/app/controllers/api/v2/filters_controller.rb +++ b/app/controllers/api/v2/filters_controller.rb @@ -43,6 +43,6 @@ class Api::V2::FiltersController < Api::BaseController end def resource_params - params.permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :with_quote, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) + params.permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :with_quote, :with_profile, 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 0b7f9f9e68..a1e058b94b 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -49,7 +49,7 @@ class FiltersController < ApplicationController end def resource_params - params.require(:custom_filter).permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :exclude_quote, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) + params.require(:custom_filter).permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :exclude_quote, :exclude_profile, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) end def set_body_classes diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb index e613de44f9..0e3e64fcf0 100644 --- a/app/models/custom_filter.rb +++ b/app/models/custom_filter.rb @@ -15,6 +15,7 @@ # exclude_follows :boolean default(FALSE), not null # exclude_localusers :boolean default(FALSE), not null # with_quote :boolean default(TRUE), not null +# with_profile :boolean default(FALSE), not null # class CustomFilter < ApplicationRecord @@ -76,6 +77,14 @@ class CustomFilter < ApplicationRecord !with_quote end + def exclude_profile=(value) + self.with_profile = !ActiveModel::Type::Boolean.new.cast(value) + end + + def exclude_profile + !with_profile + end + def self.cached_filters_for(account_id) active_filters = Rails.cache.fetch("filters:v3:#{account_id}") do filters_hash = {} @@ -111,6 +120,7 @@ class CustomFilter < ApplicationRecord if rules[:keywords].present? match = rules[:keywords].match(status.proper.searchable_text) + match = rules[:keywords].match([status.account.display_name, status.account.note].join("\n\n")) if !match && filter.with_profile if match.nil? && filter.with_quote && status.proper.reference_objects.exists? references_text_cache = status.proper.references.pluck(:text).join("\n\n") if references_text_cache.nil? references_spoiler_text_cache = status.proper.references.pluck(:spoiler_text).join("\n\n") if references_spoiler_text_cache.nil? diff --git a/app/serializers/rest/filter_serializer.rb b/app/serializers/rest/filter_serializer.rb index e4deae9d3d..02df25226a 100644 --- a/app/serializers/rest/filter_serializer.rb +++ b/app/serializers/rest/filter_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class REST::FilterSerializer < ActiveModel::Serializer - attributes :id, :title, :exclude_follows, :exclude_localusers, :with_quote, :context, :expires_at, :filter_action, :filter_action_ex + attributes :id, :title, :exclude_follows, :exclude_localusers, :with_quote, :with_profile, :context, :expires_at, :filter_action, :filter_action_ex has_many :keywords, serializer: REST::FilterKeywordSerializer, if: :rules_requested? has_many :statuses, serializer: REST::FilterStatusSerializer, if: :rules_requested? diff --git a/app/views/filters/_filter_fields.html.haml b/app/views/filters/_filter_fields.html.haml index 0ca23e8d90..42cec9431f 100644 --- a/app/views/filters/_filter_fields.html.haml +++ b/app/views/filters/_filter_fields.html.haml @@ -39,6 +39,7 @@ .fields-group = f.input :exclude_quote, wrapper: :with_label, as: :boolean, kmyblue: true, label: t('simple_form.labels.filters.options.exclude_quote') + = f.input :exclude_profile, wrapper: :with_label, as: :boolean, kmyblue: true, label: t('simple_form.labels.filters.options.exclude_profile') %hr.spacer/ diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 8ac7699b64..4a726bd65b 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -362,6 +362,7 @@ en: options: exclude_follows: Exclude following users exclude_localusers: Exclude local users + exclude_profile: Exclude account name and bio exclude_quote: Exclude quote or references form_admin_settings: activity_api_enabled: Publish aggregate statistics about user activity in the API diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index d087955cfd..7933a6b383 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -358,6 +358,7 @@ ja: options: exclude_follows: フォロー中のユーザーをフィルターの対象にしない exclude_localusers: ローカルユーザーをフィルターの対象にしない + exclude_profile: アカウント名および紹介文をフィルターの対象にしない exclude_quote: 引用の内容をフィルターの対象にしない form_admin_settings: activity_api_enabled: APIでユーザーアクティビティに関する集計統計を公開する diff --git a/db/migrate/20240709063700_add_with_profile_to_custom_filters.rb b/db/migrate/20240709063700_add_with_profile_to_custom_filters.rb new file mode 100644 index 0000000000..1f9c8d5213 --- /dev/null +++ b/db/migrate/20240709063700_add_with_profile_to_custom_filters.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddWithProfileToCustomFilters < ActiveRecord::Migration[7.1] + def change + add_column :custom_filters, :with_profile, :boolean, default: false, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index f9114ea233..f1cde1f528 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[7.1].define(version: 2024_06_07_094856) do +ActiveRecord::Schema[7.1].define(version: 2024_07_09_063700) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -539,6 +539,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_07_094856) do t.boolean "exclude_follows", default: false, null: false t.boolean "exclude_localusers", default: false, null: false t.boolean "with_quote", default: true, null: false + t.boolean "with_profile", default: false, null: false t.index ["account_id"], name: "index_custom_filters_on_account_id" end diff --git a/lib/tasks/dangerous.rake b/lib/tasks/dangerous.rake index f258efd7b7..c2ca637645 100644 --- a/lib/tasks/dangerous.rake +++ b/lib/tasks/dangerous.rake @@ -14,6 +14,7 @@ namespace :dangerous do end target_migrations = %w( + 20240709063700 20240426233435 20240426000034 20240401222541 @@ -149,6 +150,7 @@ namespace :dangerous do %w(custom_emojis license), %w(custom_filters exclude_follows), %w(custom_filters exclude_localusers), + %w(custom_filters with_profile), %w(custom_filters with_quote), %w(domain_blocks block_trends), %w(domain_blocks detect_invalid_subscription), diff --git a/spec/requests/api/v2/filters_spec.rb b/spec/requests/api/v2/filters_spec.rb index 1e0f417dd5..01c0dea016 100644 --- a/spec/requests/api/v2/filters_spec.rb +++ b/spec/requests/api/v2/filters_spec.rb @@ -54,6 +54,7 @@ RSpec.describe 'Filters' do filter_action: 'hide', exclude_follows: true, exclude_localusers: true, + with_profile: true, keywords_attributes: [keyword: 'magic', whole_word: true], } end @@ -75,6 +76,7 @@ RSpec.describe 'Filters' do expect(json[:keywords].map { |keyword| keyword.slice(:keyword, :whole_word) }).to eq [{ keyword: 'magic', whole_word: true }] expect(json[:exclude_follows]).to be true expect(json[:exclude_localusers]).to be true + expect(json[:with_profile]).to be true end it 'creates a filter', :aggregate_failures do @@ -87,6 +89,7 @@ RSpec.describe 'Filters' do expect(filter.context).to eq %w(home) expect(filter.exclude_follows).to be true expect(filter.exclude_localusers).to be true + expect(filter.with_profile).to be true expect(filter.irreversible?).to be true expect(filter.expires_at).to be_nil end @@ -175,7 +178,7 @@ RSpec.describe 'Filters' do context 'when updating filter parameters' do context 'with valid params' do - let(:params) { { title: 'updated', context: %w(home public), exclude_follows: true, exclude_localusers: true } } + let(:params) { { title: 'updated', context: %w(home public), exclude_follows: true, exclude_localusers: true, exclude_profile: true } } it 'updates the filter successfully', :aggregate_failures do subject @@ -187,6 +190,7 @@ RSpec.describe 'Filters' do expect(filter.reload.context).to eq %w(home public) expect(filter.exclude_follows).to be true expect(filter.exclude_localusers).to be true + expect(filter.exclude_profile).to be true end end diff --git a/streaming/index.js b/streaming/index.js index 39022911db..94ce534f87 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -934,7 +934,7 @@ const startServer = async () => { // @ts-ignore if (!payload.filtered && !req.cachedFilters) { // @ts-ignore - queries.push(client.query('SELECT filter.id AS id, filter.phrase AS title, filter.context AS context, filter.expires_at AS expires_at, filter.action AS filter_action, filter.with_quote AS with_quote, keyword.keyword AS keyword, keyword.whole_word AS whole_word, filter.exclude_follows AS exclude_follows, filter.exclude_localusers AS exclude_localusers FROM custom_filter_keywords keyword JOIN custom_filters filter ON keyword.custom_filter_id = filter.id WHERE filter.account_id = $1 AND (filter.expires_at IS NULL OR filter.expires_at > NOW())', [req.accountId])); + queries.push(client.query('SELECT filter.id AS id, filter.phrase AS title, filter.context AS context, filter.expires_at AS expires_at, filter.action AS filter_action, filter.with_quote AS with_quote, filter.with_profile AS with_profile, keyword.keyword AS keyword, keyword.whole_word AS whole_word, filter.exclude_follows AS exclude_follows, filter.exclude_localusers AS exclude_localusers FROM custom_filter_keywords keyword JOIN custom_filters filter ON keyword.custom_filter_id = filter.id WHERE filter.account_id = $1 AND (filter.expires_at IS NULL OR filter.expires_at > NOW())', [req.accountId])); } if (!payload.filtered) { // @ts-ignore @@ -989,6 +989,7 @@ const startServer = async () => { filter_action: filter.filter_action === 2 ? 'warn' : ['warn', 'hide', 'half_warn'][filter.filter_action], filter_action_ex: ['warn', 'hide', 'half_warn'][filter.filter_action], with_quote: filter.with_quote, + withAccountName: filter.with_profile, excludeFollows: filter.exclude_follows, excludeLocalusers: filter.exclude_localusers, }, @@ -1032,6 +1033,7 @@ const startServer = async () => { // @ts-ignore const searchableContent = ([status.spoiler_text || '', status.content, ...(reference_texts || [])].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); const searchableTextContent = JSDOM.fragment(searchableContent).textContent; + const searchableAccountContent = JSDOM.fragment([status.account.display_name, status.account.note].join('\n\n')).textContent; const now = new Date(); // @ts-ignore @@ -1054,7 +1056,8 @@ const startServer = async () => { return results; } - const keyword_matches = searchableTextContent.match(cachedFilter.regexp); + const keyword_matches = searchableTextContent.match(cachedFilter.regexp) || + ((cachedFilter.withAccountName && searchableAccountContent) ? searchableAccountContent.match(cachedFilter.regexp) : null); if (keyword_matches) { // results is an Array of FilterResult; status_matches is always // null as we only are only applying the keyword-based custom