diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 048a54fdb0..8d6203b99d 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -142,7 +142,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def valid_status? - !Admin::NgWord.reject?("#{@params[:spoiler_text]}\n#{@params[:text]}") + !Admin::NgWord.reject?("#{@params[:spoiler_text]}\n#{@params[:text]}") && !Admin::NgWord.hashtag_reject?(@tags.size) end def reply_to_local_account? diff --git a/app/models/admin/ng_word.rb b/app/models/admin/ng_word.rb index 3051c5d1bb..878151f648 100644 --- a/app/models/admin/ng_word.rb +++ b/app/models/admin/ng_word.rb @@ -6,10 +6,23 @@ class Admin::NgWord ng_words.any? { |word| text.include?(word) } end + def hashtag_reject?(hashtag_count) + post_hash_tags_max.positive? && post_hash_tags_max < hashtag_count + end + + def hashtag_reject_with_extractor?(text) + hashtag_reject?(Extractor.extract_hashtags(text)&.size || 0) + end + private def ng_words Setting.ng_words end + + def post_hash_tags_max + value = Setting.post_hash_tags_max + value.is_a?(Integer) && value.positive? ? value : 0 + end end end diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 2b77353783..ff7ef50c12 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -37,12 +37,14 @@ class Form::AdminSettings ng_words enable_block_emoji_reaction_settings hide_local_users_for_anonymous + post_hash_tags_max ).freeze INTEGER_KEYS = %i( media_cache_retention_period content_cache_retention_period backups_retention_period + post_hash_tags_max ).freeze BOOLEAN_KEYS = %i( diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index edfd2b315b..e058290cf4 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -153,7 +153,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService end def valid_status? - !Admin::NgWord.reject?("#{@status_parser.spoiler_text}\n#{@status_parser.text}") + !Admin::NgWord.reject?("#{@status_parser.spoiler_text}\n#{@status_parser.text}") && !Admin::NgWord.hashtag_reject?(@tags.size) end def update_immediate_attributes! diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 806afed340..08d3f1998a 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -161,6 +161,7 @@ class PostStatusService < BaseService def validate_status! raise Mastodon::ValidationError, I18n.t('statuses.contains_ng_words') if Admin::NgWord.reject?("#{@options[:spoiler_text]}\n#{@options[:text]}") + raise Mastodon::ValidationError, I18n.t('statuses.too_many_hashtags') if Admin::NgWord.hashtag_reject_with_extractor?(@options[:text]) end def validate_media! diff --git a/app/services/update_status_service.rb b/app/services/update_status_service.rb index 63f77cd753..be0bf526bc 100644 --- a/app/services/update_status_service.rb +++ b/app/services/update_status_service.rb @@ -75,6 +75,7 @@ class UpdateStatusService < BaseService def validate_status! raise Mastodon::ValidationError, I18n.t('statuses.contains_ng_words') if Admin::NgWord.reject?("#{@options[:spoiler_text]}\n#{@options[:text]}") + raise Mastodon::ValidationError, I18n.t('statuses.too_many_hashtags') if Admin::NgWord.hashtag_reject_with_extractor?(@options[:text]) end def validate_media! diff --git a/app/views/admin/ng_words/show.html.haml b/app/views/admin/ng_words/show.html.haml index e8a87519c6..be8b56ac40 100644 --- a/app/views/admin/ng_words/show.html.haml +++ b/app/views/admin/ng_words/show.html.haml @@ -10,6 +10,9 @@ .fields-group = f.input :ng_words, wrapper: :with_label, as: :text, input_html: { rows: 12 }, label: t('admin.ng_words.keywords') + .fields-group + = f.input :post_hash_tags_max, wrapper: :with_label, as: :integer, label: t('admin.ng_words.post_hash_tags_max') + .fields-group = f.input :enable_block_emoji_reaction_settings, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.enable_block_emoji_reaction_settings') diff --git a/config/locales/en.yml b/config/locales/en.yml index a70a11bf75..7da4133fbd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -599,6 +599,7 @@ en: enable_block_emoji_reaction_settings: Enable block emoji reactions settings for users hide_local_users_for_anonymous: Hide timeline local user posts from anonymous keywords: Reject keywords + post_hash_tags_max: Hash tags max for posts title: NG words and against spams relationships: title: "%{acct}'s relationships" @@ -1754,6 +1755,7 @@ en: show_thread: Show thread sign_in_to_participate: Login to participate in the conversation title: '%{name}: "%{quote}"' + too_many_hashtags: Too many hashtags visibilities: direct: Direct private: Followers-only diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 49df945803..9eb9a9f3d2 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -598,6 +598,7 @@ ja: enable_block_emoji_reaction_settings: 各ユーザーにスタンプ機能のブロック設定項目を解放する hide_local_users_for_anonymous: ログインしていない状態でローカルユーザーの投稿をタイムラインから取得できないようにする keywords: 投稿できないキーワード + post_hash_tags_max: 投稿に設定可能なハッシュタグの最大数 title: NGワードとスパム relationships: title: "%{acct} さんのフォロー・フォロワー" @@ -1735,6 +1736,7 @@ ja: show_thread: スレッドを表示 sign_in_to_participate: ログインして会話に参加 title: '%{name}: "%{quote}"' + too_many_hashtags: ハッシュタグが多すぎます visibilities: direct: ダイレクト private: フォロワー限定