diff --git a/app/controllers/admin/ng_words_controller.rb b/app/controllers/admin/ng_words_controller.rb new file mode 100644 index 0000000000..1af50acb42 --- /dev/null +++ b/app/controllers/admin/ng_words_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Admin + class NgWordsController < BaseController + def show + authorize :ng_words, :show? + + @admin_settings = Form::AdminSettings.new + end + + def create + authorize :ng_words, :create? + + @admin_settings = Form::AdminSettings.new(settings_params) + + if @admin_settings.save + flash[:notice] = I18n.t('generic.changes_saved_msg') + redirect_to after_update_redirect_path + else + render :index + end + end + + private + + def after_update_redirect_path + admin_ng_words_path + end + + def settings_params + params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) + end + end +end diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index a9669e7cbf..048a54fdb0 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -88,6 +88,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity process_tags process_audience + return unless valid_status? + ApplicationRecord.transaction do @status = Status.create!(@params) attach_tags(@status) @@ -139,6 +141,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity } end + def valid_status? + !Admin::NgWord.reject?("#{@params[:spoiler_text]}\n#{@params[:text]}") + end + def reply_to_local_account? accounts_in_audience.any?(&:local?) end diff --git a/app/models/admin/ng_word.rb b/app/models/admin/ng_word.rb new file mode 100644 index 0000000000..3051c5d1bb --- /dev/null +++ b/app/models/admin/ng_word.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Admin::NgWord + class << self + def reject?(text) + ng_words.any? { |word| text.include?(word) } + end + + private + + def ng_words + Setting.ng_words + end + end +end diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index a6be55fd7b..89625249d3 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -34,6 +34,7 @@ class Form::AdminSettings backups_retention_period status_page_url captcha_enabled + ng_words ).freeze INTEGER_KEYS = %i( @@ -61,6 +62,10 @@ class Form::AdminSettings mascot ).freeze + STRING_ARRAY_KEYS = %i( + ng_words + ).freeze + attr_accessor(*KEYS) validates :registrations_mode, inclusion: { in: %w(open approved none) }, if: -> { defined?(@registrations_mode) } @@ -80,6 +85,8 @@ class Form::AdminSettings stored_value = if UPLOAD_KEYS.include?(key) SiteUpload.where(var: key).first_or_initialize(var: key) + elsif STRING_ARRAY_KEYS.include?(key) + Setting.public_send(key)&.join("\n") || '' else Setting.public_send(key) end @@ -122,6 +129,8 @@ class Form::AdminSettings value == '1' elsif INTEGER_KEYS.include?(key) value.blank? ? value : Integer(value) + elsif STRING_ARRAY_KEYS.include?(key) + value&.split(/\r\n|\r|\n/)&.filter(&:present?)&.uniq || [] else value end diff --git a/app/models/user_role.rb b/app/models/user_role.rb index 5472646c60..0fd9b6e498 100644 --- a/app/models/user_role.rb +++ b/app/models/user_role.rb @@ -36,6 +36,7 @@ class UserRole < ApplicationRecord manage_roles: (1 << 17), manage_user_access: (1 << 18), delete_user_data: (1 << 19), + manage_ng_words: (1 << 30), }.freeze module Flags @@ -61,6 +62,7 @@ class UserRole < ApplicationRecord manage_blocks manage_taxonomies manage_invites + manage_ng_words ).freeze, administration: %w( diff --git a/app/policies/ng_words_policy.rb b/app/policies/ng_words_policy.rb new file mode 100644 index 0000000000..0ba640a895 --- /dev/null +++ b/app/policies/ng_words_policy.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class NgWordsPolicy < ApplicationPolicy + def show? + role.can?(:manage_ng_words) + end + + def create? + role.can?(:manage_ng_words) + end +end diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index 5cf89a5a6f..edfd2b315b 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -18,7 +18,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService @request_id = request_id # Only native types can be updated at the moment - return @status if !expected_type? || already_updated_more_recently? + return @status if !expected_type? || already_updated_more_recently? || !valid_status? if @status_parser.edited_at.present? && (@status.edited_at.nil? || @status_parser.edited_at > @status.edited_at) handle_explicit_update! @@ -152,6 +152,10 @@ class ActivityPub::ProcessStatusUpdateService < BaseService end end + def valid_status? + !Admin::NgWord.reject?("#{@status_parser.spoiler_text}\n#{@status_parser.text}") + end + def update_immediate_attributes! @status.text = @status_parser.text || '' @status.spoiler_text = @status_parser.spoiler_text || '' diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index ed8f215d8d..806afed340 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -44,6 +44,7 @@ class PostStatusService < BaseService return idempotency_duplicate if idempotency_given? && idempotency_duplicate? + validate_status! validate_media! preprocess_attributes! @@ -158,6 +159,10 @@ class PostStatusService < BaseService GroupReblogService.new.call(@status) end + def validate_status! + raise Mastodon::ValidationError, I18n.t('statuses.contains_ng_words') if Admin::NgWord.reject?("#{@options[:spoiler_text]}\n#{@options[:text]}") + end + def validate_media! if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable) @media = [] diff --git a/app/services/update_status_service.rb b/app/services/update_status_service.rb index 8ca2abe2ee..63f77cd753 100644 --- a/app/services/update_status_service.rb +++ b/app/services/update_status_service.rb @@ -27,6 +27,8 @@ class UpdateStatusService < BaseService clear_histories! if @options[:no_history] + validate_status! + Status.transaction do create_previous_edit! unless @options[:no_history] update_media_attachments! if @options.key?(:media_ids) @@ -71,6 +73,10 @@ class UpdateStatusService < BaseService @status.media_attachments.reload end + def validate_status! + raise Mastodon::ValidationError, I18n.t('statuses.contains_ng_words') if Admin::NgWord.reject?("#{@options[:spoiler_text]}\n#{@options[:text]}") + end + def validate_media! return [] if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable) diff --git a/app/views/admin/ng_words/show.html.haml b/app/views/admin/ng_words/show.html.haml new file mode 100644 index 0000000000..4168b135c0 --- /dev/null +++ b/app/views/admin/ng_words/show.html.haml @@ -0,0 +1,14 @@ +- content_for :page_title do + = t('admin.ng_words.title') + +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + += 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, wrapper: :with_label, as: :text, input_html: { rows: 12 }, label: t('antennas.edit.keywords_raw') + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/config/navigation.rb b/config/navigation.rb index c8de1cb031..8b7b6b7df2 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -36,10 +36,11 @@ SimpleNavigation::Configuration.run do |navigation| s.item :links, safe_join([fa_icon('newspaper-o fw'), t('admin.trends.links.title')]), admin_trends_links_path, highlights_on: %r{/admin/trends/links} end - n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), nil, if: -> { current_user.can?(:manage_reports, :view_audit_log, :manage_users, :manage_invites, :manage_taxonomies, :manage_federation, :manage_blocks) } do |s| + n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), nil, if: -> { current_user.can?(:manage_reports, :view_audit_log, :manage_users, :manage_invites, :manage_taxonomies, :manage_federation, :manage_blocks, :manage_ng_words) } do |s| s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_path, highlights_on: %r{/admin/reports}, if: -> { current_user.can?(:manage_reports) } s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_path(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts|/admin/disputes|/admin/users}, if: -> { current_user.can?(:manage_users) } s.item :media_attachments, safe_join([fa_icon('picture-o fw'), t('admin.media_attachments.title')]), admin_media_attachments_path, highlights_on: %r{/admin/media_attachments}, if: -> { current_user.can?(:manage_users) } + s.item :ng_words, safe_join([fa_icon('picture-o fw'), t('admin.ng_words.title')]), admin_ng_words_path, highlights_on: %r{/admin/ng_words}, if: -> { current_user.can?(:manage_ng_words) } s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path, if: -> { current_user.can?(:manage_invites) } s.item :follow_recommendations, safe_join([fa_icon('user-plus fw'), t('admin.follow_recommendations.title')]), admin_follow_recommendations_path, highlights_on: %r{/admin/follow_recommendations}, if: -> { current_user.can?(:manage_taxonomies) } s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_path(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.can?(:manage_federation) } diff --git a/config/routes/admin.rb b/config/routes/admin.rb index b7edca79e7..6c2a37f3c2 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -33,6 +33,7 @@ namespace :admin do resources :action_logs, only: [:index] resources :warning_presets, except: [:new, :show] resources :media_attachments, only: [:index] + resource :ng_words, only: [:show, :create] resources :announcements, except: [:show] do member do