Skip to content

Commit

Permalink
Change: #648 センシティブワードの入力フォーム (#653)
Browse files Browse the repository at this point in the history
* Change: #648 センシティブワードの入力フォーム

* Wip: 行の追加削除

* Wip: 設定の保存、マイグレーション

* 不要な処理を削除

* マイグレーションコード調整
  • Loading branch information
kmycode authored Mar 18, 2024
1 parent f509bd4 commit ed246f0
Show file tree
Hide file tree
Showing 17 changed files with 380 additions and 64 deletions.
1 change: 1 addition & 0 deletions .haml-lint_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ linters:
exclude:
- 'app/views/application/_sidebar.html.haml'
- 'app/views/admin/ng_rules/_ng_rule_fields.html.haml'
- 'app/views/admin/sensitive_words/_sensitive_word.html.haml'
14 changes: 8 additions & 6 deletions app/controllers/admin/sensitive_words_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ def show
authorize :sensitive_words, :show?

@admin_settings = Form::AdminSettings.new
@sensitive_words = ::SensitiveWord.caches.presence || [::SensitiveWord.new]
end

def create
Expand All @@ -21,7 +22,7 @@ def create

@admin_settings = Form::AdminSettings.new(settings_params)

if @admin_settings.save
if @admin_settings.save && ::SensitiveWord.save_from_raws(settings_params_test)
flash[:notice] = I18n.t('generic.changes_saved_msg')
redirect_to after_update_redirect_path
else
Expand All @@ -32,11 +33,8 @@ def create
private

def test_words
sensitive_words = settings_params['sensitive_words'].split(/\r\n|\r|\n/)
sensitive_words_for_full = settings_params['sensitive_words_for_full'].split(/\r\n|\r|\n/)
sensitive_words_all = settings_params['sensitive_words_all'].split(/\r\n|\r|\n/)
sensitive_words_all_for_full = settings_params['sensitive_words_all_for_full'].split(/\r\n|\r|\n/)
Admin::NgWord.reject_with_custom_words?('Sample text', sensitive_words + sensitive_words_for_full + sensitive_words_all + sensitive_words_all_for_full)
sensitive_words = settings_params_test['keywords'].compact.uniq
Admin::NgWord.reject_with_custom_words?('Sample text', sensitive_words)
end

def after_update_redirect_path
Expand All @@ -46,5 +44,9 @@ def after_update_redirect_path
def settings_params
params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS)
end

def settings_params_test
params.require(:form_admin_settings)[:sensitive_words_test]
end
end
end
62 changes: 62 additions & 0 deletions app/javascript/packs/admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,68 @@ Rails.delegate(
},
);

const addTableRow = (tableId: string) => {
const templateElement = document.querySelector(`#${tableId} .template-row`)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
const tableElement = document.querySelector(`#${tableId} tbody`)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion

if (
typeof templateElement === 'undefined' ||
typeof tableElement === 'undefined'
)
return;

let temporaryId = 0;
tableElement
.querySelectorAll<HTMLInputElement>('.temporary_id')
.forEach((input) => {
if (parseInt(input.value) + 1 > temporaryId) {
temporaryId = parseInt(input.value) + 1;
}
});

const cloned = templateElement.cloneNode(true) as HTMLTableRowElement;
cloned.className = '';
cloned.querySelector<HTMLInputElement>('.temporary_id')!.value = // eslint-disable-line @typescript-eslint/no-non-null-assertion
temporaryId.toString();
cloned
.querySelectorAll<HTMLInputElement>('input[type=checkbox]')
.forEach((input) => {
input.value = temporaryId.toString();
});
tableElement.appendChild(cloned);
};

const removeTableRow = (target: EventTarget | null, tableId: string) => {
const tableRowElement = (target as HTMLElement).closest('tr') as Node;
const tableElement = document.querySelector(`#${tableId} tbody`)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion

if (
typeof tableRowElement === 'undefined' ||
typeof tableElement === 'undefined'
)
return;

tableElement.removeChild(tableRowElement);
};

Rails.delegate(
document,
'#sensitive-words-table .add-row-button',
'click',
() => {
addTableRow('sensitive-words-table');
},
);

Rails.delegate(
document,
'#sensitive-words-table .delete-row-button',
'click',
({ target }) => {
removeTableRow(target, 'sensitive-words-table');
},
);

async function mountReactComponent(element: Element) {
const componentName = element.getAttribute('data-admin-component');
const stringProps = element.getAttribute('data-props');
Expand Down
4 changes: 4 additions & 0 deletions app/javascript/styles/mastodon/forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,10 @@ code {
margin-top: 10px;
white-space: nowrap;
}

.template-row {
display: none;
}
}

.progress-tracker {
Expand Down
32 changes: 8 additions & 24 deletions app/models/admin/sensitive_word.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ class << self
def sensitive?(text, spoiler_text, local: true)
exposure_text = spoiler_text.presence || text

sensitive = (spoiler_text.blank? && sensitive_words_all.any? { |word| include?(text, word) }) ||
sensitive_words_all_for_full.any? { |word| include?(exposure_text, word) }
return sensitive if sensitive || !local
sensitive_words = ::SensitiveWord.caches
sensitive_words.select!(&:remote) unless local

(spoiler_text.blank? && sensitive_words.any? { |word| include?(text, word) }) ||
sensitive_words_for_full.any? { |word| include?(exposure_text, word) }
return sensitive_words.filter(&:spoiler).any? { |word| include?(spoiler_text, word) } if spoiler_text.present?

sensitive_words.any? { |word| include?(exposure_text, word) }
end

def modified_text(text, spoiler_text)
Expand All @@ -24,27 +24,11 @@ def alternative_text
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 sensitive_words
Setting.sensitive_words || []
end

def sensitive_words_for_full
Setting.sensitive_words_for_full || []
end

def sensitive_words_all
Setting.sensitive_words_all || []
end

def sensitive_words_all_for_full
Setting.sensitive_words_all_for_full || []
end
end
end
8 changes: 0 additions & 8 deletions app/models/form/admin_settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ class Form::AdminSettings
post_hash_tags_max
post_mentions_max
post_stranger_mentions_max
sensitive_words
sensitive_words_for_full
sensitive_words_all
sensitive_words_all_for_full
auto_warning_text
authorized_fetch
receive_other_servers_emoji_reaction
Expand Down Expand Up @@ -128,10 +124,6 @@ class Form::AdminSettings
STRING_ARRAY_KEYS = %i(
ng_words
ng_words_for_stranger_mention
sensitive_words
sensitive_words_for_full
sensitive_words_all
sensitive_words_all_for_full
emoji_reaction_disallow_domains
permit_new_account_domains
).freeze
Expand Down
81 changes: 81 additions & 0 deletions app/models/sensitive_word.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: sensitive_words
#
# id :bigint(8) not null, primary key
# keyword :string not null
# regexp :boolean default(FALSE), not null
# remote :boolean default(FALSE), not null
# spoiler :boolean default(TRUE), not null
# created_at :datetime not null
# updated_at :datetime not null
#

class SensitiveWord < ApplicationRecord
attr_accessor :keywords, :regexps, :remotes, :spoilers

class << self
def caches
Rails.cache.fetch('sensitive_words') { SensitiveWord.where.not(id: 0).order(:keyword).to_a }
end

def save_from_hashes(rows)
unmatched = caches
matched = []

SensitiveWord.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.remote == item[:remote] && exists.spoiler == item[:spoiler]

exists.update!(regexp: item[:regexp], remote: item[:remote], spoiler: item[:spoiler])
elsif matched.none? { |i| i.keyword == item[:keyword] }
SensitiveWord.create!(
keyword: item[:keyword],
regexp: item[:regexp],
remote: item[:remote],
spoiler: item[:spoiler]
)
end
end

SensitiveWord.destroy(unmatched.map(&:id))
end

true
# rescue
# false
end

def save_from_raws(rows)
regexps = rows['regexps'] || []
remotes = rows['remotes'] || []
spoilers = rows['spoilers'] || []

hashes = (rows['keywords'] || []).zip(rows['temporary_ids'] || []).map do |item|
temp_id = item[1]
{
keyword: item[0],
regexp: regexps.include?(temp_id),
remote: remotes.include?(temp_id),
spoiler: spoilers.include?(temp_id),
}
end

save_from_hashes(hashes)
end
end

private

def invalidate_cache!
Rails.cache.delete('sensitive_words')
end
end
12 changes: 12 additions & 0 deletions app/views/admin/sensitive_words/_sensitive_word.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- 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: sensitive_word.keyword }
%td
.label_input__wrapper= f.check_box :regexps, { multiple: true, checked: sensitive_word.regexp }, temporary_id, nil
%td
.label_input__wrapper= f.check_box :remotes, { multiple: true, checked: sensitive_word.remote }, temporary_id, nil
%td
.label_input__wrapper= f.check_box :spoilers, { multiple: true, checked: sensitive_word.spoiler }, temporary_id, nil
%td
= hidden_field_tag :'form_admin_settings[sensitive_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'
36 changes: 25 additions & 11 deletions app/views/admin/sensitive_words/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,31 @@

%p.lead= t 'admin.sensitive_words.hint'

.fields-group
= f.input :sensitive_words_for_full, wrapper: :with_label, as: :text, input_html: { rows: 8 }, label: t('admin.sensitive_words.keywords_for_all'), hint: t('admin.sensitive_words.keywords_for_all_hint')

.fields-group
= f.input :sensitive_words, wrapper: :with_label, as: :text, input_html: { rows: 8 }, label: t('admin.sensitive_words.keywords'), hint: t('admin.sensitive_words.keywords_hint')

.fields-group
= f.input :sensitive_words_all_for_full, wrapper: :with_label, as: :text, input_html: { rows: 8 }, label: t('admin.sensitive_words.keywords_all_for_all'), hint: t('admin.sensitive_words.keywords_for_all_hint')

.fields-group
= f.input :sensitive_words_all, wrapper: :with_label, as: :text, input_html: { rows: 8 }, label: t('admin.sensitive_words.keywords_all'), hint: t('admin.sensitive_words.keywords_hint')
%p= t 'admin.sensitive_words.phrases.regexp_html'
%p= t 'admin.sensitive_words.phrases.remote_html'
%p= t 'admin.sensitive_words.phrases.spoiler_html'

%hr/

.table-wrapper
%table.table.keywords-table#sensitive-words-table
%thead
%tr
%th= t('simple_form.labels.defaults.phrase')
%th= t('admin.sensitive_words.phrases.regexp_short')
%th= t('admin.sensitive_words.phrases.remote_short')
%th= t('admin.sensitive_words.phrases.spoiler_short')
%th
%tbody
= f.simple_fields_for :sensitive_words_test, @sensitive_words do |keyword|
= render partial: 'sensitive_word', collection: @sensitive_words, locals: { f: keyword, template: false }

= f.simple_fields_for :sensitive_words_test, @sensitive_words do |keyword|
= render partial: 'sensitive_word', collection: [SensitiveWord.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'

.fields-group
= f.input :auto_warning_text, wrapper: :with_label, input_html: { placeholder: t('admin.sensitive_words.alert') }, label: t('admin.sensitive_words.auto_warning_text'), hint: t('admin.sensitive_words.auto_warning_text_hint')
Expand Down
12 changes: 7 additions & 5 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -963,11 +963,13 @@ en:
auto_warning_text: Custom warning text
auto_warning_text_hint: If not specified, the default warning text is used.
hint: This keywords is applied to public posts only..
keywords: Sensitive keywords for local only
keywords_all: Sensitive keywords for local+remote
keywords_all_for_all: Sensitive keywords for local+remote (Contains CW alert)
keywords_for_all: Sensitive keywords for local only (Contains CW alert)
keywords_for_all_hint: The first character of the line is "?". to use regular expressions
phrases:
remote_html: <strong>Rem</strong> ote にチェックの入っている項目は、リモートからの投稿にも適用されます。
remote_short: Rem
regexp_html: <strong>Reg</strong> Exp にチェックの入っている項目は、正規表現を用いての比較となります。
regexp_short: Reg
spoiler_html: <strong>War</strong> ning にチェックの入っている項目は、コンテンツ警告文にも適用されます。
spoiler_short: War
title: Sensitive words and moderation options
settings:
about:
Expand Down
13 changes: 7 additions & 6 deletions config/locales/ja.yml
Original file line number Diff line number Diff line change
Expand Up @@ -959,12 +959,13 @@ ja:
auto_warning_text: カスタム警告文
auto_warning_text_hint: 指定しなかった場合は、各言語のデフォルト警告文が使用されます
hint: センシティブなキーワードの設定は、当サーバーのローカルユーザーによる公開範囲「公開」「ローカル公開」「ログインユーザーのみ」に対して適用されます。
keywords: ローカルの投稿に適用するセンシティブなキーワード(警告文は除外)
keywords_all: ローカル・リモートの投稿に適用するセンシティブなキーワード(警告文は除外)
keywords_all_for_all: ローカル・リモートの投稿に適用するセンシティブなキーワード(警告文にも適用)
keywords_for_all: ローカルの投稿に適用するセンシティブなキーワード(警告文にも適用)
keywords_for_all_hint: ここで指定したキーワードを含む投稿は強制的にCWになります。警告文にも含まれていればCWになります。行が「?」で始まっていれば正規表現が使えます
keywords_hint: ここで指定したキーワードを含む投稿は強制的にCWになります。ただし警告文に使用していた場合は無視されます
phrases:
remote_html: <strong>リモ</strong> ート にチェックの入っている項目は、リモートからの投稿にも適用されます。
remote_short: リモ
regexp_html: <strong>正規</strong> 表現 にチェックの入っている項目は、正規表現を用いての比較となります。
regexp_short: 正規
spoiler_html: <strong>警告</strong> 文 にチェックの入っている項目は、コンテンツ警告文にも適用されます。
spoiler_short: 警告
title: センシティブ単語と設定
settings:
about:
Expand Down
Loading

0 comments on commit ed246f0

Please sign in to comment.