Change: #647 NGワードの入力フォーム (#663)

* Change: #647 NGワードの入力フォーム

* Wip: 画面改造

* テストコード、画面

* Fix: 複数の問題
This commit is contained in:
KMY(雪あすか) 2024-03-26 08:44:16 +09:00 committed by GitHub
parent 0d2b415e26
commit 95ab1f729c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 526 additions and 172 deletions

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
module Admin
class NgWords::KeywordsController < NgWordsController
def show
super
@ng_words = ::NgWord.caches.presence || [::NgWord.new]
end
protected
def validate
begin
::NgWord.save_from_raws(settings_params_test)
return true
rescue
flash[:alert] = I18n.t('admin.ng_words.test_error')
redirect_to after_update_redirect_path
end
false
end
private
def after_update_redirect_path
admin_ng_words_keywords_path
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Admin
class NgWords::SettingsController < NgWordsController
protected
def after_update_redirect_path
admin_ng_words_settings_path
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Admin
class NgWords::WhiteListController < NgWordsController
protected
def after_update_redirect_path
admin_ng_words_white_list_path
end
end
end

View file

@ -11,13 +11,7 @@ module Admin
def create
authorize :ng_words, :create?
begin
test_words
rescue
flash[:alert] = I18n.t('admin.ng_words.test_error')
redirect_to after_update_redirect_path
return
end
return unless validate
@admin_settings = Form::AdminSettings.new(settings_params)
@ -29,19 +23,24 @@ module Admin
end
end
private
protected
def test_words
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)
def validate
true
end
def after_update_redirect_path
admin_ng_words_path
end
private
def settings_params
params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS)
end
def settings_params_test
params.require(:form_admin_settings)[:ng_words_test]
end
end
end

View file

@ -13,7 +13,7 @@ module Admin
authorize :sensitive_words, :create?
begin
test_words
::SensitiveWord.save_from_raws(settings_params_test)
rescue
flash[:alert] = I18n.t('admin.ng_words.test_error')
redirect_to after_update_redirect_path
@ -22,7 +22,7 @@ module Admin
@admin_settings = Form::AdminSettings.new(settings_params)
if @admin_settings.save && ::SensitiveWord.save_from_raws(settings_params_test)
if @admin_settings.save
flash[:notice] = I18n.t('generic.changes_saved_msg')
redirect_to after_update_redirect_path
else
@ -32,11 +32,6 @@ module Admin
private
def test_words
sensitive_words = settings_params_test['keywords'].compact.uniq
Admin::NgWord.reject_with_custom_words?('Sample text', sensitive_words)
end
def after_update_redirect_path
admin_sensitive_words_path
end

View file

@ -320,7 +320,8 @@ Rails.delegate(
document,
'#sensitive-words-table .add-row-button',
'click',
() => {
(ev) => {
ev.preventDefault();
addTableRow('sensitive-words-table');
},
);
@ -329,8 +330,24 @@ Rails.delegate(
document,
'#sensitive-words-table .delete-row-button',
'click',
({ target }) => {
removeTableRow(target, 'sensitive-words-table');
(ev) => {
ev.preventDefault();
removeTableRow(ev.target, 'sensitive-words-table');
},
);
Rails.delegate(document, '#ng-words-table .add-row-button', 'click', (ev) => {
ev.preventDefault();
addTableRow('ng-words-table');
});
Rails.delegate(
document,
'#ng-words-table .delete-row-button',
'click',
(ev) => {
ev.preventDefault();
removeTableRow(ev.target, 'ng-words-table');
},
);

View file

@ -160,11 +160,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def valid_status?
valid = true
valid = false if valid && !valid_status_for_ng_rule?
valid = !Admin::NgWord.reject?("#{@params[:spoiler_text]}\n#{@params[:text]}", uri: @params[:uri], target_type: :status, public: @status_parser.distributable_visibility?) if valid
valid = !Admin::NgWord.reject?("#{@params[:spoiler_text]}\n#{@params[:text]}", uri: @params[:uri], target_type: :status, public: @status_parser.distributable_visibility?, stranger: mention_to_local_stranger? || reference_to_local_stranger?) if valid
valid = !Admin::NgWord.hashtag_reject?(@tags.size, uri: @params[:uri], target_type: :status, public: @status_parser.distributable_visibility?, text: "#{@params[:spoiler_text]}\n#{@params[:text]}") if valid
valid = !Admin::NgWord.mention_reject?(@raw_mention_uris.size, uri: @params[:uri], target_type: :status, public: @status_parser.distributable_visibility?, text: "#{@params[:spoiler_text]}\n#{@params[:text]}") if valid
valid = !Admin::NgWord.stranger_mention_reject_with_count?(@raw_mention_uris.size, uri: @params[:uri], target_type: :status, public: @status_parser.distributable_visibility?, text: "#{@params[:spoiler_text]}\n#{@params[:text]}") if valid && (mention_to_local_stranger? || reference_to_local_stranger?)
valid = !Admin::NgWord.stranger_mention_reject?("#{@params[:spoiler_text]}\n#{@params[:text]}", uri: @params[:uri], target_type: :status, public: @status_parser.distributable_visibility?) if valid && (mention_to_local_stranger? || reference_to_local_stranger?)
valid = false if valid && Setting.block_unfollow_account_mention && (mention_to_local_stranger? || reference_to_local_stranger?) && !local_following_sender?
valid

View file

@ -4,20 +4,25 @@ class Admin::NgWord
class << self
def reject?(text, **options)
text = PlainTextFormatter.new(text, false).to_s if options[:uri].present?
hit_word = ng_words.detect { |word| include?(text, word) ? word : nil }
record!(:ng_words, text, hit_word, options) if hit_word.present?
hit_word.present?
if options.delete(:stranger)
::NgWord.caches.detect { |word| include?(text, word) ? word : nil }&.keyword.tap do |hit_word|
record!(:ng_words_for_stranger_mention, text, hit_word, options) if hit_word.present?
end.present?
else
::NgWord.caches.filter { |w| !w.stranger }.detect { |word| include?(text, word) ? word : nil }&.keyword.tap do |hit_word|
record!(:ng_words, text, hit_word, options) if hit_word.present?
end.present?
end
end
def stranger_mention_reject?(text, **options)
text = PlainTextFormatter.new(text, false).to_s if options[:uri].present?
hit_word = ng_words_for_stranger_mention.detect { |word| include?(text, word) ? word : nil }
record!(:ng_words_for_stranger_mention, text, hit_word, options) if hit_word.present?
hit_word.present?
opts = options.merge({ stranger: true })
reject?(text, **opts)
end
def reject_with_custom_words?(text, custom_ng_words)
custom_ng_words.any? { |word| include?(text, word) }
def reject_with_custom_word?(text, word)
include_with_regexp?(text, word)
end
def hashtag_reject?(hashtag_count, **options)
@ -53,19 +58,15 @@ class Admin::NgWord
private
def include?(text, word)
if word.start_with?('?') && word.size >= 2
text =~ /#{word[1..]}/i
if word.regexp
text =~ /#{word.keyword}/
else
text.include?(word)
text.include?(word.keyword)
end
end
def ng_words
Setting.ng_words || []
end
def ng_words_for_stranger_mention
Setting.ng_words_for_stranger_mention || []
def include_with_regexp?(text, word)
text =~ /#{word}/i
end
def post_hash_tags_max

View file

@ -44,8 +44,6 @@ class Form::AdminSettings
delete_content_cache_without_reaction
status_page_url
captcha_enabled
ng_words
ng_words_for_stranger_mention
stranger_mention_from_local_ng
hide_local_users_for_anonymous
post_hash_tags_max
@ -122,8 +120,6 @@ class Form::AdminSettings
}.freeze
STRING_ARRAY_KEYS = %i(
ng_words
ng_words_for_stranger_mention
emoji_reaction_disallow_domains
permit_new_account_domains
).freeze

87
app/models/ng_word.rb Normal file
View file

@ -0,0 +1,87 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: ng_words
#
# id :bigint(8) not null, primary key
# keyword :string not null
# regexp :boolean default(FALSE), not null
# stranger :boolean default(TRUE), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class NgWord < ApplicationRecord
attr_accessor :keywords, :regexps, :strangers
validate :check_regexp
class << self
def caches
Rails.cache.fetch('ng_words') { NgWord.where.not(id: 0).order(:keyword).to_a }
end
def save_from_hashes(rows)
unmatched = caches
matched = []
NgWord.transaction do
rows.filter { |item| item[:keyword].present? }.each do |item|
exists = unmatched.find { |i| i.keyword == item[:keyword] }
if exists.present?
unmatched.delete(exists)
matched << exists
next if exists.regexp == item[:regexp] && exists.stranger == item[:stranger]
exists.update!(regexp: item[:regexp], stranger: item[:stranger])
elsif matched.none? { |i| i.keyword == item[:keyword] }
NgWord.create!(
keyword: item[:keyword],
regexp: item[:regexp],
stranger: item[:stranger]
)
end
end
NgWord.destroy(unmatched.map(&:id))
end
true
end
def save_from_raws(rows)
regexps = rows['regexps'] || []
strangers = rows['strangers'] || []
hashes = (rows['keywords'] || []).zip(rows['temporary_ids'] || []).map do |item|
temp_id = item[1]
{
keyword: item[0],
regexp: regexps.include?(temp_id),
stranger: strangers.include?(temp_id),
}
end
save_from_hashes(hashes)
end
end
private
def invalidate_cache!
Rails.cache.delete('ng_words')
end
def check_regexp
return if keyword.blank? || !regexp
begin
Admin::NgWord.reject_with_custom_word?('Sample text', keyword)
rescue
raise Mastodon::ValidationError, I18n.t('admin.ng_words.test_error')
end
end
end

View file

@ -16,6 +16,8 @@
class SensitiveWord < ApplicationRecord
attr_accessor :keywords, :regexps, :remotes, :spoilers
validate :check_regexp
class << self
def caches
Rails.cache.fetch('sensitive_words') { SensitiveWord.where.not(id: 0).order(:keyword).to_a }
@ -50,8 +52,6 @@ class SensitiveWord < ApplicationRecord
end
true
# rescue
# false
end
def save_from_raws(rows)
@ -78,4 +78,14 @@ class SensitiveWord < ApplicationRecord
def invalidate_cache!
Rails.cache.delete('sensitive_words')
end
def check_regexp
return if keyword.blank? || !regexp
begin
Admin::NgWord.reject_with_custom_word?('Sample text', keyword)
rescue
raise Mastodon::ValidationError, I18n.t('admin.ng_words.test_error')
end
end
end

View file

@ -162,10 +162,9 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
end
def valid_status?
valid = !Admin::NgWord.reject?("#{@status_parser.spoiler_text}\n#{@status_parser.text}", uri: @status.uri, target_type: :status)
valid = !Admin::NgWord.reject?("#{@status_parser.spoiler_text}\n#{@status_parser.text}", uri: @status.uri, target_type: :status, stranger: mention_to_local_stranger? || reference_to_local_stranger?)
valid = !Admin::NgWord.hashtag_reject?(@raw_tags.size) if valid
valid = false if valid && Admin::NgWord.mention_reject?(@raw_mentions.size, uri: @status.uri, target_type: :status, text: "#{@status_parser.spoiler_text}\n#{@status_parser.text}")
valid = false if valid && (mention_to_local_stranger? || reference_to_local_stranger?) && Admin::NgWord.stranger_mention_reject?("#{@status_parser.spoiler_text}\n#{@status_parser.text}", uri: @status.uri, target_type: :status)
valid = false if valid && (mention_to_local_stranger? || reference_to_local_stranger?) && Admin::NgWord.stranger_mention_reject_with_count?(@raw_mentions.size, uri: @status.uri, target_type: :status, text: "#{@status_parser.spoiler_text}\n#{@status_parser.text}")
valid = false if valid && (mention_to_local_stranger? || reference_to_local_stranger?) && reject_reply_exclude_followers?

View file

@ -0,0 +1,10 @@
- temporary_id = defined?(@temp_id) ? @temp_id += 1 : @temp_id = 1
%tr{ class: template ? 'template-row' : nil }
%td= f.input :keywords, as: :string, input_html: { multiple: true, value: ng_word.keyword }
%td
.label_input__wrapper= f.check_box :regexps, { multiple: true, checked: ng_word.regexp }, temporary_id, nil
%td
.label_input__wrapper= f.check_box :strangers, { multiple: true, checked: ng_word.stranger }, temporary_id, nil
%td
= hidden_field_tag :'form_admin_settings[ng_words_test][temporary_ids][]', temporary_id, class: 'temporary_id'
= link_to safe_join([fa_icon('times'), t('filters.index.delete')]), '#', class: 'table-action-link delete-row-button'

View file

@ -0,0 +1,43 @@
- content_for :page_title do
= t('admin.ng_words.title')
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
- content_for :heading do
%h2= t('admin.ng_words.title')
= render partial: 'admin/ng_words/shared/links'
= simple_form_for @admin_settings, url: admin_ng_words_keywords_path, html: { method: :post } do |f|
= render 'shared/error_messages', object: @admin_settings
%p.lead
= t('admin.ng_words.preamble')
= link_to t('admin.ngword_histories.title'), admin_ngword_histories_path
%p= t 'admin.ng_words.phrases.regexp_html'
%p= t 'admin.ng_words.phrases.stranger_html'
%hr/
.table-wrapper
%table.table.keywords-table#ng-words-table
%thead
%tr
%th= t('simple_form.labels.defaults.phrase')
%th= t('admin.ng_words.phrases.regexp_short')
%th= t('admin.ng_words.phrases.stranger_short')
%th
%tbody
= f.simple_fields_for :ng_words_test, @ng_words do |keyword|
= render partial: 'ng_word', collection: @ng_words, locals: { f: keyword, template: false }
= f.simple_fields_for :ng_words_test, @ng_words do |keyword|
= render partial: 'ng_word', collection: [NgWord.new], locals: { f: keyword, template: true }
%tfoot
%tr
%td{ colspan: 4 }
= link_to safe_join([fa_icon('plus'), t('filters.edit.add_keyword')]), '#', class: 'table-action-link add-row-button'
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -4,21 +4,13 @@
- 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|
- content_for :heading do
%h2= t('admin.ng_words.title')
= render partial: 'admin/ng_words/shared/links'
= simple_form_for @admin_settings, url: admin_ng_words_settings_path, html: { method: :post } do |f|
= render 'shared/error_messages', object: @admin_settings
%p.lead= t('admin.ng_words.preamble')
%p.hint
= t 'admin.ng_words.history_hint'
= link_to t('admin.ngword_histories.title'), admin_ngword_histories_path
.fields-group
= f.input :ng_words_for_stranger_mention, wrapper: :with_label, as: :text, input_html: { rows: 10 }, 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: 10 }, label: t('admin.ng_words.keywords'), hint: t('admin.ng_words.keywords_hint')
.fields-group
= f.input :post_hash_tags_max, wrapper: :with_label, as: :integer, label: t('admin.ng_words.post_hash_tags_max')
@ -28,17 +20,6 @@
.fields-group
= f.input :post_mentions_max, wrapper: :with_label, as: :integer, label: t('admin.ng_words.post_mentions_max')
%h4= t('admin.ng_words.white_list')
%p.lead
= t('admin.ng_words.white_list_hint')
= link_to t('admin.ng_words.remote_approval_list'), admin_accounts_path(status: 'remote_pending', origin: 'remote')
.fields-group
= f.input :hold_remote_new_accounts, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.hold_remote_new_accounts'), hint: t('admin.ng_words.remote_approval_hint')
.fields-group
= f.input :permit_new_account_domains, wrapper: :with_label, as: :text, kmyblue: true, input_html: { rows: 6 }, label: t('admin.ng_words.permit_new_account_domains')
%h4= t('admin.ng_words.deprecated')
%p.hint= t('admin.ng_words.deprecated_hint')

View file

@ -0,0 +1,6 @@
.content__heading__tabs
= render_navigation renderer: :links do |primary|
:ruby
primary.item :keywords, safe_join([fa_icon('pencil fw'), t('admin.ng_words.keywords')]), admin_ng_words_keywords_path
primary.item :white_list, safe_join([fa_icon('list fw'), t('admin.ng_words.white_list')]), admin_ng_words_white_list_path
primary.item :settings, safe_join([fa_icon('cog fw'), t('admin.ng_words.settings')]), admin_ng_words_settings_path

View file

@ -0,0 +1,25 @@
- content_for :page_title do
= t('admin.ng_words.title')
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
- content_for :heading do
%h2= t('admin.ng_words.title')
= render partial: 'admin/ng_words/shared/links'
= simple_form_for @admin_settings, url: admin_ng_words_white_list_path, html: { method: :post } do |f|
= render 'shared/error_messages', object: @admin_settings
%p.lead
= t('admin.ng_words.white_list_hint')
= link_to t('admin.ng_words.remote_approval_list'), admin_accounts_path(status: 'remote_pending', origin: 'remote')
.fields-group
= f.input :hold_remote_new_accounts, wrapper: :with_label, as: :boolean, label: t('admin.ng_words.hold_remote_new_accounts'), hint: t('admin.ng_words.remote_approval_hint')
.fields-group
= f.input :permit_new_account_domains, wrapper: :with_label, as: :text, kmyblue: true, input_html: { rows: 6 }, label: t('admin.ng_words.permit_new_account_domains')
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -3,7 +3,7 @@
.filters
.back-link
= link_to admin_ng_words_path do
= link_to admin_ng_words_keywords_path do
= fa_icon 'chevron-left fw'
= t('admin.ngword_histories.back_to_ng_words')