From 37c67c756b46712faae70714d92b0cd9113a4396 Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 22 Feb 2024 12:28:51 +0900 Subject: [PATCH] =?UTF-8?q?=E3=81=AA=E3=82=93=E3=81=A8=E3=81=8B=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E3=80=81=E3=81=93=E3=82=8C=E3=81=8B=E3=82=89=E5=8B=95?= =?UTF-8?q?=E4=BD=9C=E7=A2=BA=E8=AA=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/admin/ng_rules_controller.rb | 3 +- app/lib/activitypub/activity/announce.rb | 2 +- app/lib/activitypub/activity/create.rb | 6 ++-- app/lib/activitypub/activity/like.rb | 4 +-- app/lib/vacuum/ng_histories_vacuum.rb | 17 +++++++++ app/models/admin/ng_rule.rb | 35 ++++++++++++++++--- app/models/ng_rule.rb | 3 +- app/models/ng_rule_history.rb | 35 +++++++++++++------ .../process_status_update_service.rb | 4 ++- app/services/emoji_react_service.rb | 2 +- app/services/favourite_service.rb | 2 +- app/services/post_status_service.rb | 1 + app/services/reblog_service.rb | 2 +- app/services/update_status_service.rb | 1 + app/services/vote_service.rb | 2 +- .../ng_rule_histories/_history.html.haml | 25 +++++++++++-- .../admin/ng_rule_histories/show.html.haml | 2 +- app/views/admin/ng_rules/_ng_rule.html.haml | 10 +++--- .../admin/ng_rules/_ng_rule_fields.html.haml | 15 ++++---- app/workers/scheduler/vacuum_scheduler.rb | 5 +++ config/locales/en.yml | 21 ++++++++++- config/locales/ja.yml | 24 +++++++++++-- config/navigation.rb | 2 +- db/migrate/20240218233621_create_ng_rules.rb | 15 +++++--- db/schema.rb | 14 +++++--- .../fabricators/ng_rule_history_fabricator.rb | 3 +- spec/lib/vacuum/ng_histories_vacuum_spec.rb | 24 +++++++++++++ spec/models/admin/ng_rule_spec.rb | 15 +++++--- spec/services/delete_account_service_spec.rb | 3 ++ 29 files changed, 237 insertions(+), 60 deletions(-) create mode 100644 app/lib/vacuum/ng_histories_vacuum.rb create mode 100644 spec/lib/vacuum/ng_histories_vacuum_spec.rb diff --git a/app/controllers/admin/ng_rules_controller.rb b/app/controllers/admin/ng_rules_controller.rb index cc008277338226..163a3b959095b4 100644 --- a/app/controllers/admin/ng_rules_controller.rb +++ b/app/controllers/admin/ng_rules_controller.rb @@ -77,9 +77,10 @@ def resource_params :account_include_local, :status_spoiler_text, :status_text, :status_tag, :status_sensitive_state, :status_cw_state, :status_media_state, :status_poll_state, :status_quote_state, :status_reply_state, :status_media_threshold, :status_poll_threshold, - :status_mention_threshold, :status_mention_allow_follower, :rule_violation_threshold_per_account, + :status_mention_threshold, :status_allow_follower_mention, :rule_violation_threshold_per_account, :reaction_allow_follower, :emoji_reaction_name, :emoji_reaction_origin_domain, :status_reference_threshold, :account_action, :status_action, :reaction_action, + :account_allow_followed_by_local, status_visibility: [], status_searchability: [], reaction_type: []) end diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index 69df066a4d2fc5..ceeccd2b3f5392 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -11,7 +11,7 @@ def perform return reject_payload! if original_status.nil? || !announceable?(original_status) return if requested_through_relay? - return unless check_invalid_reaction_for_ng_rule! @account, uri: @json['id'], reaction_type: 'reblog', recipient: original_status.account + return unless check_invalid_reaction_for_ng_rule! @account, uri: @json['id'], reaction_type: 'reblog', recipient: original_status.account, target_status: original_status @status = Status.find_by(account: @account, reblog: original_status) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 8f48d06b31b413..6edaafc5552a73 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -158,7 +158,9 @@ def valid_status? def valid_status_for_ng_rule? check_invalid_status_for_ng_rule! @account, - uri: @status_parser.uri, + reaction_type: 'create', + uri: @params[:uri], + url: @params[:url], spoiler_text: @params[:spoiler_text], text: @params[:text], tag_names: @tags.map(&:name), @@ -373,7 +375,7 @@ def process_poll def poll_vote? return false if replied_to_status.nil? || replied_to_status.preloadable_poll.nil? || !replied_to_status.local? || !replied_to_status.preloadable_poll.options.include?(@object['name']) - return true unless check_invalid_reaction_for_ng_rule! @account, uri: @json['id'], reaction_type: 'vote', recipient: replied_to_status.account + return true unless check_invalid_reaction_for_ng_rule! @account, uri: @json['id'], reaction_type: 'vote', recipient: replied_to_status.account, target_status: replied_to_status poll_vote! unless replied_to_status.preloadable_poll.expired? diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index 9b920ce2682340..4f149857c3dcd5 100644 --- a/app/lib/activitypub/activity/like.rb +++ b/app/lib/activitypub/activity/like.rb @@ -26,7 +26,7 @@ def reject_favourite? def process_favourite return if @account.favourited?(@original_status) - return unless check_invalid_reaction_for_ng_rule! @account, uri: @json['id'], reaction_type: 'favourite', recipient: @original_status.account + return unless check_invalid_reaction_for_ng_rule! @account, uri: @json['id'], reaction_type: 'favourite', recipient: @original_status.account, target_status: @original_status favourite = @original_status.favourites.create!(account: @account, uri: @json['id']) @@ -45,7 +45,7 @@ def process_emoji_reaction return if emoji.nil? end - return unless check_invalid_reaction_for_ng_rule! @account, uri: @json['id'], reaction_type: 'emoji_reaction', emoji_reaction_name: emoji&.shortcode || shortcode, emoji_reaction_origin_domain: emoji&.domain, recipient: @original_status.account + return unless check_invalid_reaction_for_ng_rule! @account, uri: @json['id'], reaction_type: 'emoji_reaction', emoji_reaction_name: emoji&.shortcode || shortcode, emoji_reaction_origin_domain: emoji&.domain, recipient: @original_status.account, target_status: @original_status reaction = nil diff --git a/app/lib/vacuum/ng_histories_vacuum.rb b/app/lib/vacuum/ng_histories_vacuum.rb new file mode 100644 index 00000000000000..204e6de23fa0e6 --- /dev/null +++ b/app/lib/vacuum/ng_histories_vacuum.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Vacuum::NgHistoriesVacuum + include Redisable + + HISTORY_LIFE_DURATION = 7.days.freeze + + def perform + vacuum_histories! + end + + private + + def vacuum_histories! + NgRuleHistory.where('created_at < ?', HISTORY_LIFE_DURATION.ago).in_batches.destroy_all + end +end diff --git a/app/models/admin/ng_rule.rb b/app/models/admin/ng_rule.rb index 627a6e030ca0ff..ed085f11e81a87 100644 --- a/app/models/admin/ng_rule.rb +++ b/app/models/admin/ng_rule.rb @@ -10,6 +10,7 @@ def initialize(ng_rule, account, **options) def account_match? return false if @account.local? && !@ng_rule.account_include_local + return false if !@account.local? && @ng_rule.account_allow_followed_by_local && followed_by_local_accounts? if @account.local? return false unless @ng_rule.account_include_local @@ -27,7 +28,7 @@ def account_match? end def status_match? - return false if @ng_rule.status_mention_allow_follower && @options[:mention_to_following] + return false if @ng_rule.status_allow_follower_mention && @options[:mention_to_following] has_media = @options[:media_count].is_a?(Integer) && @options[:media_count].positive? has_poll = @options[:poll_count].is_a?(Integer) && @options[:poll_count].positive? @@ -68,7 +69,7 @@ def reaction_match? def check_account_or_record! return true unless account_match? - record!('account', @account.uri) if !@account.local? || @ng_rule.record_history_also_local + record!('account', @account.uri, 'account_create') if !@account.local? || @ng_rule.record_history_also_local !violation? end @@ -76,7 +77,13 @@ def check_account_or_record! def check_status_or_record! return true unless account_match? && status_match? - record!('status', @options[:uri], text: "#{@options[:spoiler_text]}\n\n#{@options[:text]}") if (!@options.key?(:visibility) || %i(public public_unlisted login unlsited).include?(@options[:visibility].to_sym)) && (!@account.local? || @ng_rule.record_history_also_local) + text = [@options[:spoiler_text], @options[:text]].compact_blank.join("\n\n") + data = { + media_count: @options[:media_count], + poll_count: @options[:poll_count], + url: @options[:url], + } + record!('status', @options[:uri], "status_#{@options[:reaction_type]}", text: text, data: data) if loggable_visibility? && (!@account.local? || @ng_rule.record_history_also_local) !violation? end @@ -84,11 +91,22 @@ def check_status_or_record! def check_reaction_or_record! return true unless account_match? && reaction_match? - record!('reaction', @options[:uri]) if !@account.local? || @ng_rule.record_history_also_local + text = @options[:target_status].present? ? [@options[:target_status].spoiler_text, @options[:target_status].text].compact_blank.join("\n\n") : nil + data = { + url: @options[:target_status].present? ? @options[:target_status].url : nil, + } + record!('reaction', @options[:uri], "reaction_#{@options[:reaction_type]}", text: text, data: data) if loggable_visibility? && (!@account.local? || @ng_rule.record_history_also_local) !violation? end + def loggable_visibility? + visibility = @options[:target_status]&.visibility || @options[:visibility] + return true unless visibility + + %i(public public_unlisted login unlsited).include?(visibility.to_sym) + end + def account_action @ng_rule.account_action.to_sym end @@ -107,6 +125,10 @@ def self.extract_test!(custom_ng_words) private + def followed_by_local_accounts? + Follow.exists?(account: Account.local, target_account: @account) + end + def already_did_count return @already_did_count if defined?(@already_did_count) @@ -123,14 +145,17 @@ def violation? already_did_count >= limit end - def record!(reason, uri, **options) + def record!(reason, uri, reason_action, **options) opts = options.merge({ ng_rule: @ng_rule, account: @account, + local: @account.local?, reason: reason, + reason_action: reason_action, uri: uri, }) opts = opts.merge({ skip_count: already_did_count, skip: true }) unless violation? + NgRuleHistory.create!(**opts) end diff --git a/app/models/ng_rule.rb b/app/models/ng_rule.rb index 6309f1d1055fd7..3e06a5858bee90 100644 --- a/app/models/ng_rule.rb +++ b/app/models/ng_rule.rb @@ -17,6 +17,7 @@ # account_avatar_state :integer default("optional"), not null # account_header_state :integer default("optional"), not null # account_include_local :boolean default(TRUE), not null +# account_allow_followed_by_local :boolean default(FALSE), not null # status_spoiler_text :string default(""), not null # status_text :string default(""), not null # status_tag :string default(""), not null @@ -32,7 +33,7 @@ # status_media_threshold :integer default(-1), not null # status_poll_threshold :integer default(-1), not null # status_mention_threshold :integer default(-1), not null -# status_mention_allow_follower :boolean default(TRUE), not null +# status_allow_follower_mention :boolean default(TRUE), not null # status_reference_threshold :integer default(-1), not null # reaction_type :string default([]), not null, is an Array # reaction_allow_follower :boolean default(TRUE), not null diff --git a/app/models/ng_rule_history.rb b/app/models/ng_rule_history.rb index cac9b684bef5d1..6e748fc9e0515f 100644 --- a/app/models/ng_rule_history.rb +++ b/app/models/ng_rule_history.rb @@ -4,18 +4,33 @@ # # Table name: ng_rule_histories # -# id :bigint(8) not null, primary key -# ng_rule_id :bigint(8) not null -# account_id :bigint(8) not null -# text :string -# uri :string -# reason :string not null -# skip :boolean default(FALSE), not null -# skip_count :integer -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint(8) not null, primary key +# ng_rule_id :bigint(8) not null +# account_id :bigint(8) +# text :string +# uri :string +# reason :integer not null +# reason_action :integer not null +# skip :boolean default(FALSE), not null +# skip_count :integer +# local :boolean default(TRUE), not null +# data :jsonb +# created_at :datetime not null +# updated_at :datetime not null # class NgRuleHistory < ApplicationRecord + enum :reason, { account: 0, status: 1, reaction: 2 }, prefix: :reason + enum :reason_action, { + account_create: 0, + status_create: 10, + status_edit: 11, + reaction_favourite: 20, + reaction_emoji_reaction: 21, + reaction_follow: 22, + reaction_reblog: 23, + reaction_vote: 24, + }, prefix: :reason_action + belongs_to :ng_rule belongs_to :account end diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index 46a297dc76dd37..f6a0627233c8e7 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -177,7 +177,9 @@ def validate_status_mentions! def valid_status_for_ng_rule? check_invalid_status_for_ng_rule! @account, - uri: @status_parser.uri, + reaction_type: 'edit', + uri: @status.uri, + url: @status_parser.url || @status.url, spoiler_text: @status.spoiler_text, text: @status.text, tag_names: @raw_tags, diff --git a/app/services/emoji_react_service.rb b/app/services/emoji_react_service.rb index 4f04ba7303e48f..f7d4b0ff37d599 100644 --- a/app/services/emoji_react_service.rb +++ b/app/services/emoji_react_service.rb @@ -22,7 +22,7 @@ def call(account, status, name) shortcode, domain = name.split('@') domain = nil if TagManager.instance.local_domain?(domain) - raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless check_invalid_reaction_for_ng_rule! account, reaction_type: 'emoji_reaction', emoji_reaction_name: shortcode, emoji_reaction_origin_domain: domain, recipient: status.account + raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless check_invalid_reaction_for_ng_rule! account, reaction_type: 'emoji_reaction', emoji_reaction_name: shortcode, emoji_reaction_origin_domain: domain, recipient: status.account, target_status: status with_redis_lock("emoji_reaction:#{status.id}") do custom_emoji = CustomEmoji.find_by(shortcode: shortcode, domain: domain) diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index c19fd996f02aa4..50ed5831eac4f1 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -12,7 +12,7 @@ class FavouriteService < BaseService def call(account, status) authorize_with account, status, :favourite? - raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless check_invalid_reaction_for_ng_rule! account, reaction_type: 'favourite', recipient: status.account + raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless check_invalid_reaction_for_ng_rule! account, reaction_type: 'favourite', recipient: status.account, target_status: status favourite = Favourite.find_by(account: account, status: status) diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 987d97b7b6b820..389c1976f992d5 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -222,6 +222,7 @@ def validate_status_mentions! def validate_status_ng_rules! result = check_invalid_status_for_ng_rule! @account, + reaction_type: 'create', spoiler_text: @options[:spoiler_text] || '', text: @text, tag_names: Extractor.extract_hashtags(@text) || [], diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index d6d77b6e66513a..2c04d2becd505d 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -17,7 +17,7 @@ def call(account, reblogged_status, options = {}) authorize_with account, reblogged_status, :reblog? - raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless check_invalid_reaction_for_ng_rule! account, reaction_type: 'reblog', recipient: reblogged_status.account + raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless check_invalid_reaction_for_ng_rule! account, reaction_type: 'reblog', recipient: reblogged_status.account, target_status: reblogged_status reblog = account.statuses.find_by(reblog: reblogged_status) diff --git a/app/services/update_status_service.rb b/app/services/update_status_service.rb index e0db098c040861..3b37e405f60182 100644 --- a/app/services/update_status_service.rb +++ b/app/services/update_status_service.rb @@ -96,6 +96,7 @@ def validate_status_mentions! def validate_status_ng_rules! result = check_invalid_status_for_ng_rule! @status.account, + reaction_type: 'edit', spoiler_text: @options.key?(:spoiler_text) ? (@options[:spoiler_text] || '') : @status.spoiler_text, text: text, tag_names: Extractor.extract_hashtags(text) || [], diff --git a/app/services/vote_service.rb b/app/services/vote_service.rb index 7b5a9071a5345b..d4f5006be3d87c 100644 --- a/app/services/vote_service.rb +++ b/app/services/vote_service.rb @@ -17,7 +17,7 @@ def call(account, poll, choices) @choices = choices @votes = [] - raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless check_invalid_reaction_for_ng_rule! @account, reaction_type: 'vote', recipient: @poll.status.account + raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless check_invalid_reaction_for_ng_rule! @account, reaction_type: 'vote', recipient: @poll.status.account, target_status: @poll.status already_voted = true diff --git a/app/views/admin/ng_rule_histories/_history.html.haml b/app/views/admin/ng_rule_histories/_history.html.haml index 87ed93ea61f986..86776f6941f5ff 100644 --- a/app/views/admin/ng_rule_histories/_history.html.haml +++ b/app/views/admin/ng_rule_histories/_history.html.haml @@ -3,11 +3,32 @@ -# = f.check_box :history_ids, { multiple: true, include_hidden: false }, history.id .batch-table__row__content .status__content>< - = html_aware_format(history.text, false) + = html_aware_format(history.text, history.local) .detailed-status__meta + = t("admin.ng_rule_histories.reason_actions.#{history.reason_action}") + - if history.skip + · + = t('admin.ng_rule_histories.skipped', count: history.skip_count) + - if history.data.present? + - if history.data['media_count'].present? && history.data['media_count'].positive? + · + = t('admin.ng_rule_histories.data.media_count', count: history.data['media_count']) + - if history.data['poll_count'].present? && history.data['poll_count'].positive? + · + = t('admin.ng_rule_histories.data.poll_count', count: history.data['poll_count']) + + %br/ + + - if history.account.present? + = link_to t('admin.ng_rule_histories.moderate_account'), admin_account_path(history.account.id) + · + %time.formatted{ datetime: history.created_at.iso8601, title: l(history.created_at) }= l(history.created_at) - if history.uri.present? · - = history.uri + - if history.data.present? && history.data['url'].present? + = link_to history.uri, history.data['url'] || history.uri, target: '_blank', rel: 'noopener' + - else + = link_to history.uri, target: '_blank', rel: 'noopener' diff --git a/app/views/admin/ng_rule_histories/show.html.haml b/app/views/admin/ng_rule_histories/show.html.haml index 4322d00ec90230..313e40c4255338 100644 --- a/app/views/admin/ng_rule_histories/show.html.haml +++ b/app/views/admin/ng_rule_histories/show.html.haml @@ -1,5 +1,5 @@ - content_for :page_title do - = t('admin.ng_rule_histories.title') + = t('admin.ng_rule_histories.title', title: @ng_rule.title) .filters .back-link diff --git a/app/views/admin/ng_rules/_ng_rule.html.haml b/app/views/admin/ng_rules/_ng_rule.html.haml index 4280323e67d1d5..51ef41a42dc5e9 100644 --- a/app/views/admin/ng_rules/_ng_rule.html.haml +++ b/app/views/admin/ng_rules/_ng_rule.html.haml @@ -1,14 +1,14 @@ .filters-list__item{ class: [ng_rule.expired? && 'expired'] } = link_to edit_admin_ng_rule_path(ng_rule), class: 'filters-list__item__title' do - %h3= ng_rule.title.presence || t('ng_rules.index.empty_title') + %h3= ng_rule.title.presence || t('admin.ng_rules.index.empty_title') - if ng_rule.expires? - .expiration{ title: t('ng_rules.index.expires_on', date: l(ng_rule.expires_at)) } + .expiration{ title: t('filters.index.expires_on', date: l(ng_rule.expires_at)) } - if ng_rule.expired? = t('invites.expired') - else - = t('ng_rules.index.expires_in', distance: distance_of_time_in_words_to_now(ng_rule.expires_at)) + = t('filters.index.expires_in', distance: distance_of_time_in_words_to_now(ng_rule.expires_at)) %div - = table_link_to 'pencil', t('ng_rules.edit.title'), edit_admin_ng_rule_path(ng_rule) - = table_link_to 'times', t('ng_rules.index.delete'), admin_ng_rule_path(ng_rule), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } + = table_link_to 'pencil', t('admin.ng_rules.index.edit.title'), edit_admin_ng_rule_path(ng_rule) + = table_link_to 'times', t('admin.ng_rules.index.delete'), admin_ng_rule_path(ng_rule), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } diff --git a/app/views/admin/ng_rules/_ng_rule_fields.html.haml b/app/views/admin/ng_rules/_ng_rule_fields.html.haml index 53a6d2e6a16162..2845ceb5831003 100644 --- a/app/views/admin/ng_rules/_ng_rule_fields.html.haml +++ b/app/views/admin/ng_rules/_ng_rule_fields.html.haml @@ -19,6 +19,9 @@ %h4= t('admin.ng_rules.edit.headers.account') %p.lead= t('admin.ng_rules.edit.summary.account') +.fields-group + = f.input :account_allow_followed_by_local, as: :boolean, wrapper: :with_label, hint: t('admin.ng_rules.account_allow_followed_by_local_hint'), label: t('admin.ng_rules.account_allow_followed_by_local') + .fields-row .fields-row__column.fields-row__column-6.fields-group = f.input :account_domain, as: :text, input_html: { rows: 4 }, wrapper: :with_label, label: t('admin.ng_rules.account_domain'), hint: false @@ -47,6 +50,9 @@ %h4= t('admin.ng_rules.edit.headers.status') %p.lead= t('admin.ng_rules.edit.summary.status') +.fields-group + = f.input :status_allow_follower_mention, as: :boolean, wrapper: :with_label, hint: t('admin.ng_rules.status_allow_follower_mention_hint'), label: t('admin.ng_rules.status_allow_follower_mention') + .fields-row .fields-row__column.fields-row__column-6.fields-group = f.input :status_text, as: :text, input_html: { rows: 4 }, wrapper: :with_label, label: t('admin.ng_rules.status_text'), hint: false @@ -86,18 +92,15 @@ .fields-row__column.fields-row__column-6.fields-group = f.input :status_reference_threshold, as: :string, wrapper: :with_label, hint: false, label: t('admin.ng_rules.status_reference_threshold') -.fields-group - = f.input :status_mention_allow_follower, as: :boolean, wrapper: :with_label, hint: false, label: t('admin.ng_rules.status_mention_allow_follower') - %h4= t('admin.ng_rules.edit.headers.reaction') %p.lead= t('admin.ng_rules.edit.summary.reaction') -.fields-group - = f.input :reaction_type, wrapper: :with_block_label, collection: %i(favourite emoji_reaction reblog follow vote), as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label_method: ->(context) { I18n.t("admin.ng_rules.reaction_types.#{context}") }, include_blank: false, label: t('admin.ng_rules.reaction_type') - .fields-row = f.input :reaction_allow_follower, wrapper: :with_label, hint: t('admin.ng_rules.reaction_allow_follower_hint'), label: t('admin.ng_rules.reaction_allow_follower') +.fields-group + = f.input :reaction_type, wrapper: :with_block_label, collection: %i(favourite emoji_reaction reblog follow vote), as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label_method: ->(context) { I18n.t("admin.ng_rules.reaction_types.#{context}") }, include_blank: false, label: t('admin.ng_rules.reaction_type') + .fields-row .fields-row__column.fields-row__column-6.fields-group = f.input :emoji_reaction_name, as: :text, input_html: { rows: 4 }, wrapper: :with_label, hint: false, label: t('admin.ng_rules.emoji_reaction_name') diff --git a/app/workers/scheduler/vacuum_scheduler.rb b/app/workers/scheduler/vacuum_scheduler.rb index eb4d2f5b10a2d5..5c6e74e3d453c9 100644 --- a/app/workers/scheduler/vacuum_scheduler.rb +++ b/app/workers/scheduler/vacuum_scheduler.rb @@ -26,6 +26,7 @@ def vacuum_operations feeds_vacuum, imports_vacuum, list_statuses_vacuum, + ng_histories_vacuum, ] end @@ -65,6 +66,10 @@ def applications_vacuum Vacuum::ApplicationsVacuum.new end + def ng_histories_vacuum + Vacuum::NgHistoriesVacuum.new + end + def content_retention_policy ContentRetentionPolicy.current end diff --git a/config/locales/en.yml b/config/locales/en.yml index 192f280e89d907..99e3fd8196156a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -686,6 +686,9 @@ en: status: Set the conditions of your submission. The account conditions must match at the same time. Please note that by default, not all posts will be applicable unless you set the "Visibility" and "Searchability". title: Edit NG Rule index: + delete: Delete + edit: + title: Edit empty: NGルールが空です title: NG Rules new: @@ -697,6 +700,7 @@ en: follow: Follow reblog: Boost vote: Vote + reaction_allow_follower: Allow all reactions targeted at followers reaction_allow_follower_hint: If enabled, reactions between other servers are unconditionally allowed record_history_also_local: Local users are also subject to history recording rubular: Regular Expression Checker @@ -705,10 +709,11 @@ en: no_needed: Should not have optional: Optional status_action: Action for posts + status_allow_follower_mention: Check posts only if they contain mentions/references to non-followers + status_allow_follower_mention_hint: If enabled, mentions between other servers are unconditionally allowed status_cw_state: Has warning or not status_media_state: Has media or not status_media_threshold: Medias limit - status_mention_allow_follower: フォローしていない相手へのメンションを含む場合のみ、メンション数の上限を確認する status_mention_threshold: Mentions limit status_poll_state: Has poll or not status_poll_threshold: Poll items limit @@ -723,6 +728,20 @@ en: status_visibility: Visibility test_error: Regular expression syntax is incorrect. title: NG Rule + ng_rule_histories: + data: + media_count: "%{count} medias" + poll_count: "%{count} polls" + moderate_account: Moderate account + reason_actions: + reaction_emoji_reaction: Emoji reaction + reaction_favourite: Favourite + reaction_follow: Follow request + reaction_reblog: Boost + reaction_vote: Vote + status_create: Post + status_edit: Edit post + title: NG Rule History %{title} ng_words: block_unfollow_account_mention: Reject all mentions/references from all accounts that do not have followers on your server hide_local_users_for_anonymous: Hide timeline local user posts from anonymous diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 5d43b1d53c1107..80a59ace18a1a5 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -641,6 +641,8 @@ ja: media_attachments: title: 投稿された画像 ng_rules: + account_allow_followed_by_local: ローカルユーザーからフォローされていないアカウントのみチェックする + account_allow_followed_by_local_hint: 1以上のフォローを持つ全てのローカルユーザーが信頼できる場合にのみこのオプションを使用してください account_avatar_state: アイコンの有無 account_display_name: 名前 account_domain: ドメイン @@ -683,12 +685,15 @@ ja: emoji_reaction_origin_domain: 絵文字リアクションで使われたカスタム絵文字のドメイン emoji_reaction_origin_domain_hint: 他のサーバーの絵文字に相乗りした場合、その絵文字がもともと登録されていたサーバーのドメインが使用されます index: + delete: 削除 + edit: + title: 編集 empty: NGルールが空です title: NGルール new: title: 新規NGルール reaction_action: リアクションに対して行うアクション - reaction_allow_follower: フォロワーを対象とした全てのリアクションを許可する + reaction_allow_follower: フォロワー以外に対するリアクションのみチェックする reaction_allow_follower_hint: これを有効にすると、他のサーバー同士のリアクションは無条件で許可されます reaction_type: リアクションの種類 reaction_types: @@ -705,10 +710,11 @@ ja: no_needed: 無し optional: 不問 status_action: 投稿に対して行うアクション + status_allow_follower_mention: フォロワー以外へのメンション・参照を含む場合のみ投稿をチェックする + status_allow_follower_mention_hint: これを有効にすると、他のサーバー同士のメンションは無条件で許可されます status_cw_state: 警告文の有無 status_media_state: メディアの有無 status_media_threshold: メディア数の条件 - status_mention_allow_follower: フォローしていない相手へのメンション・参照を含む場合のみ、メンション・参照数の上限を確認する status_mention_threshold: メンション数の上限 status_poll_state: 投票の有無 status_poll_threshold: 投票項目数の上限 @@ -723,6 +729,20 @@ ja: status_visibility: 公開範囲 test_error: 正規表現の文法が誤っています title: NGルール + ng_rule_histories: + data: + media_count: "%{count} のメディア" + poll_count: 項目 %{count} の投票 + moderate_account: アカウントをモデレートする + reason_actions: + reaction_emoji_reaction: 絵文字リアクション + reaction_favourite: お気に入りに登録 + reaction_follow: フォローリクエスト + reaction_reblog: ブースト + reaction_vote: 投票 + status_create: 投稿 + status_edit: 投稿を編集 + title: NGルール「%{title}」の履歴 ng_words: block_unfollow_account_mention: 自分のサーバーのフォロワーを持たない全てのアカウントからのメンション・参照を全て拒否する hide_local_users_for_anonymous: ログインしていない状態でローカルユーザーの投稿をタイムラインから取得できないようにする diff --git a/config/navigation.rb b/config/navigation.rb index 3dff78794e123c..0f1c8818db457e 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -50,7 +50,7 @@ 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 :ng_words, safe_join([fa_icon('list fw'), t('admin.ng_words.title')]), admin_ng_words_path, highlights_on: %r{/admin/(ng_words|ngword_histories)}, if: -> { current_user.can?(:manage_ng_words) } - s.item :ng_rules, safe_join([fa_icon('list fw'), t('admin.ng_rules.title')]), admin_ng_rules_path, highlights_on: %r{/admin/(ng_rules|ng_rule_histories)}, if: -> { current_user.can?(:manage_ng_words) } + s.item :ng_rules, safe_join([fa_icon('rub fw'), t('admin.ng_rules.title')]), admin_ng_rules_path, highlights_on: %r{/admin/(ng_rules|ng_rule_histories)}, if: -> { current_user.can?(:manage_ng_words) } s.item :sensitive_words, safe_join([fa_icon('list fw'), t('admin.sensitive_words.title')]), admin_sensitive_words_path, highlights_on: %r{/admin/sensitive_words}, if: -> { current_user.can?(:manage_sensitive_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) } diff --git a/db/migrate/20240218233621_create_ng_rules.rb b/db/migrate/20240218233621_create_ng_rules.rb index a48ff2adbfb2d4..fb1758fa2cafae 100644 --- a/db/migrate/20240218233621_create_ng_rules.rb +++ b/db/migrate/20240218233621_create_ng_rules.rb @@ -15,6 +15,7 @@ def change t.integer :account_avatar_state, null: false, default: 0 t.integer :account_header_state, null: false, default: 0 t.boolean :account_include_local, null: false, default: true + t.boolean :account_allow_followed_by_local, null: false, default: false t.string :status_spoiler_text, null: false, default: '' t.string :status_text, null: false, default: '' t.string :status_tag, null: false, default: '' @@ -30,7 +31,7 @@ def change t.integer :status_media_threshold, null: false, default: -1 t.integer :status_poll_threshold, null: false, default: -1 t.integer :status_mention_threshold, null: false, default: -1 - t.boolean :status_mention_allow_follower, null: false, default: true + t.boolean :status_allow_follower_mention, null: false, default: true t.integer :status_reference_threshold, null: false, default: -1 t.string :reaction_type, null: false, default: [], array: true t.boolean :reaction_allow_follower, null: false, default: true @@ -46,15 +47,21 @@ def change end create_table :ng_rule_histories do |t| - t.belongs_to :ng_rule, null: false, foreign_key: { on_cascade: :delete } - t.belongs_to :account, null: false, foreign_key: { on_cascade: :delete } + t.belongs_to :ng_rule, null: false, foreign_key: { on_cascade: :delete }, index: false + t.belongs_to :account, foreign_key: { on_cascade: :nullify }, index: false t.string :text t.string :uri, index: true - t.string :reason, null: false + t.integer :reason, null: false + t.integer :reason_action, null: false t.boolean :skip, null: false, default: false t.integer :skip_count + t.boolean :local, null: false, default: true + t.jsonb :data t.timestamps end + + add_index :ng_rule_histories, [:ng_rule_id, :account_id] + add_index :ng_rule_histories, :created_at end end diff --git a/db/schema.rb b/db/schema.rb index fc5b5ab2957614..a0224470fe9dfe 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -872,16 +872,19 @@ create_table "ng_rule_histories", force: :cascade do |t| t.bigint "ng_rule_id", null: false - t.bigint "account_id", null: false + t.bigint "account_id" t.string "text" t.string "uri" - t.string "reason", null: false + t.integer "reason", null: false + t.integer "reason_action", null: false t.boolean "skip", default: false, null: false t.integer "skip_count" + t.boolean "local", default: true, null: false + t.jsonb "data" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index ["account_id"], name: "index_ng_rule_histories_on_account_id" - t.index ["ng_rule_id"], name: "index_ng_rule_histories_on_ng_rule_id" + t.index ["created_at"], name: "index_ng_rule_histories_on_created_at" + t.index ["ng_rule_id", "account_id"], name: "index_ng_rule_histories_on_ng_rule_id_and_account_id" t.index ["uri"], name: "index_ng_rule_histories_on_uri" end @@ -898,6 +901,7 @@ t.integer "account_avatar_state", default: 0, null: false t.integer "account_header_state", default: 0, null: false t.boolean "account_include_local", default: true, null: false + t.boolean "account_allow_followed_by_local", default: false, null: false t.string "status_spoiler_text", default: "", null: false t.string "status_text", default: "", null: false t.string "status_tag", default: "", null: false @@ -913,7 +917,7 @@ t.integer "status_media_threshold", default: -1, null: false t.integer "status_poll_threshold", default: -1, null: false t.integer "status_mention_threshold", default: -1, null: false - t.boolean "status_mention_allow_follower", default: true, null: false + t.boolean "status_allow_follower_mention", default: true, null: false t.integer "status_reference_threshold", default: -1, null: false t.string "reaction_type", default: [], null: false, array: true t.boolean "reaction_allow_follower", default: true, null: false diff --git a/spec/fabricators/ng_rule_history_fabricator.rb b/spec/fabricators/ng_rule_history_fabricator.rb index 0358a31fa16d23..75c40880da8ef4 100644 --- a/spec/fabricators/ng_rule_history_fabricator.rb +++ b/spec/fabricators/ng_rule_history_fabricator.rb @@ -3,5 +3,6 @@ Fabricator(:ng_rule_history) do ng_rule { Fabricate.build(:ng_rule) } account { Fabricate.build(:account) } - reason '' + reason 0 + reason_action 0 end diff --git a/spec/lib/vacuum/ng_histories_vacuum_spec.rb b/spec/lib/vacuum/ng_histories_vacuum_spec.rb new file mode 100644 index 00000000000000..d75d7f353b3dad --- /dev/null +++ b/spec/lib/vacuum/ng_histories_vacuum_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Vacuum::NgHistoriesVacuum do + subject { described_class.new } + + describe '#perform' do + let!(:rule_history_old) { Fabricate(:ng_rule_history, created_at: 30.days.ago) } + let!(:rule_history_recent) { Fabricate(:ng_rule_history, created_at: 2.days.ago) } + + before do + subject.perform + end + + it 'deletes old history' do + expect { rule_history_old.reload }.to raise_error ActiveRecord::RecordNotFound + end + + it 'does not delete recent history' do + expect { rule_history_recent.reload }.to_not raise_error + end + end +end diff --git a/spec/models/admin/ng_rule_spec.rb b/spec/models/admin/ng_rule_spec.rb index 595c610d3586df..f4e9d337ecbb1a 100644 --- a/spec/models/admin/ng_rule_spec.rb +++ b/spec/models/admin/ng_rule_spec.rb @@ -103,7 +103,10 @@ end describe '#check_status_or_record!' do - subject { described_class.new(ng_rule, account, **options).check_status_or_record! } + subject do + opts = { reaction_type: 'create' }.merge(options) + described_class.new(ng_rule, account, **opts).check_status_or_record! + end context 'when status matches but account does not match' do let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor') } @@ -188,20 +191,20 @@ context 'with mention size rule' do let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor') } let(:options) { { uri: uri, mention_count: 5 } } - let(:ng_rule) { Fabricate(:ng_rule, status_mention_threshold: 4, status_mention_allow_follower: false) } + let(:ng_rule) { Fabricate(:ng_rule, status_mention_threshold: 4, status_allow_follower_mention: false) } it_behaves_like 'matches rule', 'status' context 'when mention to stranger' do let(:options) { { uri: uri, mention_count: 5, mention_to_following: false } } - let(:ng_rule) { Fabricate(:ng_rule, status_mention_threshold: 4, status_mention_allow_follower: true) } + let(:ng_rule) { Fabricate(:ng_rule, status_mention_threshold: 4, status_allow_follower_mention: true) } it_behaves_like 'matches rule', 'status' end context 'when mention to follower' do let(:options) { { uri: uri, mention_count: 5, mention_to_following: true } } - let(:ng_rule) { Fabricate(:ng_rule, status_mention_threshold: 4, status_mention_allow_follower: true) } + let(:ng_rule) { Fabricate(:ng_rule, status_mention_threshold: 4, status_allow_follower_mention: true) } it_behaves_like 'does not match rule', 'status' end @@ -209,7 +212,9 @@ end describe '#check_reaction_or_record!' do - subject { described_class.new(ng_rule, account, **options).check_reaction_or_record! } + subject do + described_class.new(ng_rule, account, **options).check_reaction_or_record! + end context 'when account matches but reaction does not match' do let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor') } diff --git a/spec/services/delete_account_service_spec.rb b/spec/services/delete_account_service_spec.rb index a5ad55162eb87a..7e0ac4fcdd419c 100644 --- a/spec/services/delete_account_service_spec.rb +++ b/spec/services/delete_account_service_spec.rb @@ -47,6 +47,8 @@ let!(:account_note) { Fabricate(:account_note, account: account) } + let!(:ng_rule_history) { Fabricate(:ng_rule_history, account: account) } + it 'deletes associated owned and target records and target notifications' do subject @@ -68,6 +70,7 @@ expect { bookmark_category_status.status.reload }.to_not raise_error expect { antenna_account.account.reload }.to_not raise_error expect { circle_account.account.reload }.to_not raise_error + expect { ng_rule_history.reload }.to_not raise_error expect { list.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { list_account.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { antenna_account.reload }.to raise_error(ActiveRecord::RecordNotFound)