diff --git a/app/controllers/admin/ng_words_controller.rb b/app/controllers/admin/ng_words_controller.rb index e26ca96b8c..17a6306d1a 100644 --- a/app/controllers/admin/ng_words_controller.rb +++ b/app/controllers/admin/ng_words_controller.rb @@ -32,7 +32,7 @@ module Admin private def test_words - ng_words = settings_params['ng_words'].split(/\r\n|\r|\n/) + ng_words = "#{settings_params['ng_words']}\n#{settings_params['ng_words_for_stranger_mention']}".split(/\r\n|\r|\n/).filter(&:present?) Admin::NgWord.reject_with_custom_words?('Sample text', ng_words) end diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index b2bf2afc20..fe37c3234b 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -84,7 +84,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity return nil unless valid_status? return nil if (reply_to_local? || reply_to_local_account? || reply_to_local_from_tags?) && reject_reply_to_local? - return nil if (!reply_to_local_account_following? || !reply_to_local_status_following? || !reply_to_local_from_tags_following?) && reject_reply_exclude_followers? + return nil if mention_to_local_but_not_followed? && reject_reply_exclude_followers? ApplicationRecord.transaction do @status = Status.create!(@params) @@ -139,7 +139,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def valid_status? - !Admin::NgWord.reject?("#{@params[:spoiler_text]}\n#{@params[:text]}") && !Admin::NgWord.hashtag_reject?(@tags.size) + valid = !Admin::NgWord.reject?("#{@params[:spoiler_text]}\n#{@params[:text]}") && !Admin::NgWord.hashtag_reject?(@tags.size) + + valid = !Admin::NgWord.stranger_mention_reject?("#{@params[:spoiler_text]}\n#{@params[:text]}") if valid && mention_to_local_but_not_followed? + + valid end def accounts_in_audience @@ -418,11 +422,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def reply_to_local_from_tags? - (@mentions.present? && @mentions.any? { |m| m.account.local? }) + @mentions.present? && @mentions.any? { |m| m.account.local? } end def reply_to_local_from_tags_following? - (@mentions.present? && @mentions.none? { |m| m.account.local? && !m.account.following?(@account) }) + @mentions.nil? || @mentions.none? { |m| m.account.local? && !m.account.following?(@account) } end def reply_to_local? @@ -433,6 +437,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity !reply_to_local? || replied_to_status.account.following?(@account) end + def mention_to_local_but_not_followed? + !reply_to_local_account_following? || !reply_to_local_status_following? || !reply_to_local_from_tags_following? + end + def reject_reply_to_local? @reject_reply_to_local ||= DomainBlock.reject_reply?(@account.domain) end diff --git a/app/models/admin/ng_word.rb b/app/models/admin/ng_word.rb index b99c52a714..801c12094e 100644 --- a/app/models/admin/ng_word.rb +++ b/app/models/admin/ng_word.rb @@ -18,6 +18,10 @@ class Admin::NgWord hashtag_reject?(Extractor.extract_hashtags(text)&.size || 0) end + def stranger_mention_reject?(text) + ng_words_for_stranger_mention.any? { |word| include?(text, word) } + end + private def include?(text, word) @@ -32,6 +36,10 @@ class Admin::NgWord Setting.ng_words || [] end + def ng_words_for_stranger_mention + Setting.ng_words_for_stranger_mention || [] + end + def post_hash_tags_max value = Setting.post_hash_tags_max value.is_a?(Integer) && value.positive? ? value : 0 diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 681b13814a..bd7bed3af5 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -38,6 +38,7 @@ class Form::AdminSettings status_page_url captcha_enabled ng_words + ng_words_for_stranger_mention hide_local_users_for_anonymous post_hash_tags_max sensitive_words @@ -91,6 +92,7 @@ class Form::AdminSettings STRING_ARRAY_KEYS = %i( ng_words + ng_words_for_stranger_mention sensitive_words sensitive_words_for_full ).freeze diff --git a/app/views/admin/ng_words/show.html.haml b/app/views/admin/ng_words/show.html.haml index 9fad862787..3450135c3c 100644 --- a/app/views/admin/ng_words/show.html.haml +++ b/app/views/admin/ng_words/show.html.haml @@ -7,6 +7,9 @@ = simple_form_for @admin_settings, url: admin_ng_words_path, html: { method: :post } do |f| = render 'shared/error_messages', object: @admin_settings + .fields-group + = f.input :ng_words_for_stranger_mention, wrapper: :with_label, as: :text, input_html: { rows: 12 }, label: t('admin.ng_words.keywords_for_stranger_mention'), hint: t('admin.ng_words.keywords_for_stranger_mention_hint') + .fields-group = f.input :ng_words, wrapper: :with_label, as: :text, input_html: { rows: 12 }, label: t('admin.ng_words.keywords'), hint: t('admin.ng_words.keywords_hint') diff --git a/config/locales/en.yml b/config/locales/en.yml index 25534bd235..b5e9f34823 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -643,6 +643,8 @@ en: ng_words: hide_local_users_for_anonymous: Hide timeline local user posts from anonymous keywords: Reject keywords + keywords_for_stranger_mention: Reject keywords when mention/reply from strangers + keywords_for_stranger_mention_hint: Currently this words are checked posts from other servers only. keywords_hint: The first character of the line is "?". to use regular expressions post_hash_tags_max: Hash tags max for posts test_error: Testing is returned any errors diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 8d3a839551..98de29324f 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -643,6 +643,8 @@ ja: ng_words: hide_local_users_for_anonymous: ログインしていない状態でローカルユーザーの投稿をタイムラインから取得できないようにする keywords: 投稿できないキーワード + keywords_for_stranger_mention: フォローしていないアカウントへのメンションで利用できないキーワード + keywords_for_stranger_mention_hint: フォローしていないアカウントへのメンションにのみ適用されます。現状は外部サーバーから来た投稿のみに適用されます keywords_hint: 行を「?」で始めると、正規表現が使えます post_hash_tags_max: 投稿に設定可能なハッシュタグの最大数 test_error: NGワードのテストに失敗しました。正規表現のミスが含まれているかもしれません diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 7659ed82a0..3e27c32a1b 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -1334,6 +1334,199 @@ RSpec.describe ActivityPub::Activity::Create do expect(status.language).to eq 'en-US' end end + + context 'when ng word is set' do + let(:custom_before) { true } + let(:custom_before_sub) { false } + let(:content) { 'Lorem ipsum' } + let(:ng_words) { 'hello' } + let(:ng_words_for_stranger_mention) { 'ohagi' } + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: content, + to: 'https://www.w3.org/ns/activitystreams#Public', + } + end + + before do + Form::AdminSettings.new(ng_words: ng_words, ng_words_for_stranger_mention: ng_words_for_stranger_mention).save + subject.perform unless custom_before_sub + end + + context 'when not contains ng words' do + let(:content) { 'ohagi, world!' } + + it 'creates status' do + expect(sender.statuses.first).to_not be_nil + end + end + + context 'when hit ng words' do + let(:content) { 'hello, world!' } + + it 'creates status' do + expect(sender.statuses.first).to be_nil + end + end + + context 'when mention from tags' do + let(:recipient) { Fabricate(:user).account } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: content, + tag: [ + { + type: 'Mention', + href: ActivityPub::TagManager.instance.uri_for(recipient), + }, + ], + } + end + + context 'with not using ng words for stranger' do + let(:content) { 'among us' } + + it 'creates status' do + expect(sender.statuses.first).to_not be_nil + end + end + + context 'with using ng words for stranger' do + let(:content) { 'oh, ohagi!' } + + it 'creates status' do + expect(sender.statuses.first).to be_nil + end + end + + context 'with using ng words for stranger but receiver is following him' do + let(:content) { 'oh, ohagi!' } + let(:custom_before_sub) { true } + + before do + recipient.follow!(sender) + subject.perform + end + + it 'creates status' do + expect(sender.statuses.first).to_not be_nil + end + end + + context 'with using ng words for stranger but multiple receivers are partically following him' do + let(:content) { 'oh, ohagi' } + let(:custom_before_sub) { true } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: content, + tag: [ + { + type: 'Mention', + href: ActivityPub::TagManager.instance.uri_for(recipient), + }, + { + type: 'Mention', + href: ActivityPub::TagManager.instance.uri_for(Fabricate(:user).account), + }, + ], + } + end + + before do + recipient.follow!(sender) + subject.perform + end + + it 'creates status' do + expect(sender.statuses.first).to be_nil + end + end + end + + context 'when a reply' do + let(:recipient) { Fabricate(:user).account } + let(:original_status) { Fabricate(:status, account: recipient) } + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'ohagi peers', + inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status), + } + end + + context 'with a simple case' do + it 'creates status' do + expect(sender.statuses.first).to be_nil + end + end + + context 'with following' do + let(:custom_before_sub) { true } + + before do + recipient.follow!(sender) + subject.perform + end + + it 'creates status' do + expect(sender.statuses.first).to_not be_nil + end + end + end + end + + context 'when hashtags limit is set' do + let(:post_hash_tags_max) { 2 } + let(:custom_before) { true } + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + tag: [ + { + type: 'Hashtag', + href: 'http://example.com/blah', + name: '#test', + }, + { + type: 'Hashtag', + href: 'http://example.com/blah2', + name: '#test2', + }, + ], + } + end + + before do + Form::AdminSettings.new(post_hash_tags_max: post_hash_tags_max).save + subject.perform + end + + context 'when limit is enough' do + it 'creates status' do + expect(sender.statuses.first).to_not be_nil + end + end + + context 'when limit is over' do + let(:post_hash_tags_max) { 1 } + + it 'creates status' do + expect(sender.statuses.first).to be_nil + end + end + end end context 'with an encrypted message' do