Add: #600 NGルール (#602)

* Wip

* Wip

* Wip: History

* Wip: テストコード作成

* Fix test

* Wip

* Wip

* Wip

* Fix test

* Wip

* Wip

* Wip

* Wip

* なんとか完成、これから動作確認

* spell miss

* Change ng rule timings

* Fix test

* Wip

* Fix test

* Wip

* Fix form

* 表示まわりの改善
This commit is contained in:
KMY(雪あすか) 2024-02-26 17:45:41 +09:00 committed by GitHub
parent 0779c748a6
commit 7d96d5828e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 2062 additions and 42 deletions

View file

@ -4,6 +4,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
include JsonLdHelper
include Redisable
include Lockable
include NgRuleHelper
class AbortError < ::StandardError; end
@ -168,17 +169,38 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
end
def validate_status_mentions!
raise AbortError if (mention_to_stranger? || reference_to_stranger?) && Admin::NgWord.stranger_mention_reject?("#{@status_parser.spoiler_text}\n#{@status_parser.text}", uri: @status.uri, target_type: :status)
raise AbortError unless valid_status_for_ng_rule?
raise AbortError if (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)
raise AbortError if Admin::NgWord.mention_reject?(@raw_mentions.size, uri: @status.uri, target_type: :status, text: "#{@status_parser.spoiler_text}\n#{@status_parser.text}")
raise AbortError if (mention_to_stranger? || reference_to_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}")
raise AbortError if (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}")
end
def mention_to_stranger?
def valid_status_for_ng_rule?
check_invalid_status_for_ng_rule! @account,
reaction_type: 'edit',
uri: @status.uri,
url: @status_parser.url || @status.url,
spoiler_text: @status.spoiler_text,
text: @status.text,
tag_names: @raw_tags,
visibility: @status.visibility,
searchability: @status.searchability,
sensitive: @status.sensitive,
media_count: @next_media_attachments.size,
poll_count: @status.poll&.options&.size || 0,
quote: quote,
reply: @status.reply?,
mention_count: @status.mentions.count,
reference_count: reference_uris.size,
mention_to_following: !(mention_to_local_stranger? || reference_to_local_stranger?)
end
def mention_to_local_stranger?
@status.mentions.map(&:account).to_a.any? { |mentioned_account| mentioned_account.id != @status.account.id && mentioned_account.local? && !mentioned_account.following?(@status.account) } ||
(@status.thread.present? && @status.thread.account.id != @status.account.id && @status.thread.account.local? && !@status.thread.account.following?(@status.account))
end
def reference_to_stranger?
def reference_to_local_stranger?
local_referred_accounts.any? { |account| !account.following?(@account) }
end

View file

@ -5,6 +5,7 @@ class EmojiReactService < BaseService
include Payloadable
include Redisable
include Lockable
include NgRuleHelper
# React a status with emoji and notify remote user
# @param [Account] account
@ -18,9 +19,12 @@ class EmojiReactService < BaseService
raise Mastodon::ValidationError, I18n.t('reactions.errors.banned') if account.silenced? && !status.account.following?(account)
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, target_status: status
with_redis_lock("emoji_reaction:#{status.id}") do
shortcode, domain = name.split('@')
domain = nil if TagManager.instance.local_domain?(domain)
custom_emoji = CustomEmoji.find_by(shortcode: shortcode, domain: domain)
return if domain.present? && !EmojiReaction.exists?(status: status, custom_emoji: custom_emoji)

View file

@ -3,6 +3,7 @@
class FavouriteService < BaseService
include Authorization
include Payloadable
include NgRuleHelper
# Favourite a status and notify remote user
# @param [Account] account
@ -11,6 +12,8 @@ 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, target_status: status
favourite = Favourite.find_by(account: account, status: status)
return favourite unless favourite.nil?

View file

@ -4,6 +4,7 @@ class FollowService < BaseService
include Redisable
include Payloadable
include DomainControlHelper
include NgRuleHelper
# Follow a remote user, notify remote user about the follow
# @param [Account] source_account From which to follow
@ -23,6 +24,8 @@ class FollowService < BaseService
raise ActiveRecord::RecordNotFound if following_not_possible?
raise Mastodon::NotPermittedError if following_not_allowed?
raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless check_invalid_reaction_for_ng_rule! @source_account, reaction_type: 'follow', recipient: @target_account
if @source_account.following?(@target_account)
return change_follow_options!
elsif @source_account.requested?(@target_account)

View file

@ -4,6 +4,7 @@ class PostStatusService < BaseService
include Redisable
include LanguagesHelper
include DtlHelper
include NgRuleHelper
MIN_SCHEDULE_OFFSET = 5.minutes.freeze
@ -74,7 +75,7 @@ class PostStatusService < BaseService
@visibility = :limited if %w(mutual circle reply).include?(@options[:visibility])
@visibility = :unlisted if (@visibility == :public || @visibility == :public_unlisted || @visibility == :login) && @account.silenced?
@visibility = :public_unlisted if @visibility == :public && !@options[:force_visibility] && !@options[:application]&.superapp && @account.user&.setting_public_post_to_unlisted && Setting.enable_public_unlisted_visibility
@visibility = Setting.enable_public_unlisted_visibility ? :public_unlisted : :unlisted unless Setting.enable_public_visibility
@visibility = Setting.enable_public_unlisted_visibility ? :public_unlisted : :unlisted if !Setting.enable_public_visibility && @visibility == :public
@limited_scope = @options[:visibility]&.to_sym if @visibility == :limited && @options[:visibility] != 'limited'
@searchability = searchability
@searchability = :private if @account.silenced? && %i(public public_unlisted).include?(@searchability&.to_sym)
@ -148,6 +149,7 @@ class PostStatusService < BaseService
@status = @account.statuses.new(status_attributes)
process_mentions_service.call(@status, limited_type: @status.limited_visibility? ? @limited_scope : '', circle: @circle, save_records: false)
safeguard_mentions!(@status)
validate_status_ng_rules!
validate_status_mentions!
@status.limited_scope = :personal if @status.limited_visibility? && !@status.reply_limited? && !process_mentions_service.mentions?
@ -218,9 +220,35 @@ class PostStatusService < BaseService
raise Mastodon::ValidationError, I18n.t('statuses.contains_ng_words') if (mention_to_stranger? || reference_to_stranger?) && Setting.stranger_mention_from_local_ng && Admin::NgWord.stranger_mention_reject?("#{@options[:spoiler_text]}\n#{@options[:text]}")
end
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) || [],
visibility: @visibility.to_s,
searchability: @searchability.to_s,
sensitive: @sensitive,
media_count: (@media || []).size,
poll_count: @options.dig(:poll, 'options')&.size || 0,
quote: quote_url,
reply: @in_reply_to.present?,
mention_count: mention_count,
reference_count: reference_urls.size,
mention_to_following: !(mention_to_stranger? || reference_to_stranger?)
raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless result
end
def mention_count
@text.gsub(Account::MENTION_RE)&.count || 0
end
def mention_to_stranger?
@status.mentions.map(&:account).to_a.any? { |mentioned_account| mentioned_account.id != @account.id && !mentioned_account.following?(@account) } ||
(@in_reply_to && @in_reply_to.account.id != @account.id && !@in_reply_to.account.following?(@account))
return @mention_to_stranger if defined?(@mention_to_stranger)
@mention_to_stranger = @status.mentions.map(&:account).to_a.any? { |mentioned_account| mentioned_account.id != @account.id && !mentioned_account.following?(@account) } ||
(@in_reply_to && @in_reply_to.account.id != @account.id && !@in_reply_to.account.following?(@account))
end
def reference_to_stranger?
@ -228,12 +256,20 @@ class PostStatusService < BaseService
end
def referred_statuses
statuses = ProcessReferencesService.extract_uris(@text).filter_map { |uri| ActivityPub::TagManager.instance.local_uri?(uri) && ActivityPub::TagManager.instance.uri_to_resource(uri, Status, url: true) }
statuses = reference_urls.filter_map { |uri| ActivityPub::TagManager.instance.local_uri?(uri) && ActivityPub::TagManager.instance.uri_to_resource(uri, Status, url: true) }
statuses += Status.where(id: @reference_ids) if @reference_ids.present?
statuses
end
def quote_url
ProcessReferencesService.extract_quote(@text)
end
def reference_urls
@reference_urls ||= ProcessReferencesService.extract_uris(@text) || []
end
def validate_media!
if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable)
@media = []

View file

@ -8,6 +8,7 @@ class ProcessReferencesService < BaseService
DOMAIN = ENV['WEB_DOMAIN'] || ENV.fetch('LOCAL_DOMAIN', nil)
REFURL_EXP = /(RT|QT|BT|RN|RE)((:|;)?\s+|:|;)(#{URI::DEFAULT_PARSER.make_regexp(%w(http https))})/
QUOTEURL_EXP = /(QT|RN|RE)((:|;)?\s+|:|;)(#{URI::DEFAULT_PARSER.make_regexp(%w(http https))})/
MAX_REFERENCES = 5
def call(status, reference_parameters, urls: nil, fetch_remote: true, no_fetch_urls: nil, quote_urls: nil)
@ -45,8 +46,14 @@ class ProcessReferencesService < BaseService
reference_parameters.any? || (urls || []).any? || (quote_urls || []).any? || FormattingHelper.extract_status_plain_text(status).scan(REFURL_EXP).pluck(3).uniq.any?
end
def self.extract_uris(text)
text.scan(REFURL_EXP).pluck(3)
def self.extract_uris(text, remote: false)
return text.scan(REFURL_EXP).pluck(3) unless remote
PlainTextFormatter.new(text, false).to_s.scan(REFURL_EXP).pluck(3)
end
def self.extract_quote(text)
text.scan(QUOTEURL_EXP).pick(3)
end
def self.perform_worker_async(status, reference_parameters, urls, quote_urls)

View file

@ -3,6 +3,7 @@
class ReblogService < BaseService
include Authorization
include Payloadable
include NgRuleHelper
# Reblog a status and notify its remote author
# @param [Account] account Account to reblog from
@ -16,6 +17,8 @@ class ReblogService < BaseService
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, target_status: reblogged_status
reblog = account.statuses.find_by(reblog: reblogged_status)
return reblog unless reblog.nil?

View file

@ -3,6 +3,7 @@
class UpdateStatusService < BaseService
include Redisable
include LanguagesHelper
include NgRuleHelper
class NoChangesSubmittedError < StandardError; end
@ -31,6 +32,8 @@ class UpdateStatusService < BaseService
validate_status!
Status.transaction do
validate_status_ng_rules!
create_previous_edit! unless @options[:no_history]
update_media_attachments! if @options.key?(:media_ids)
update_poll! if @options.key?(:poll)
@ -91,6 +94,30 @@ class UpdateStatusService < BaseService
raise Mastodon::ValidationError, I18n.t('statuses.contains_ng_words') if (mention_to_stranger? || reference_to_stranger?) && Setting.stranger_mention_from_local_ng && Admin::NgWord.stranger_mention_reject?("#{@options[:spoiler_text]}\n#{@options[:text]}")
end
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) || [],
visibility: @status.visibility,
searchability: @status.searchability,
sensitive: @options.key?(:sensitive) ? @options[:sensitive] : @status.sensitive,
media_count: @options[:media_ids].present? ? @options[:media_ids].size : @status.media_attachments.count,
poll_count: @options.dig(:poll, 'options')&.size || 0,
quote: quote_url,
reply: @status.reply?,
mention_count: mention_count,
reference_count: reference_urls.size,
mention_to_following: !(mention_to_stranger? || reference_to_stranger?)
raise Mastodon::ValidationError, I18n.t('statuses.violate_rules') unless result
end
def mention_count
text.gsub(Account::MENTION_RE)&.count || 0
end
def mention_to_stranger?
@status.mentions.map(&:account).to_a.any? { |mentioned_account| mentioned_account.id != @status.account.id && !mentioned_account.following?(@status.account) } ||
(@status.thread.present? && @status.thread.account.id != @status.account.id && !@status.thread.account.following?(@status.account))
@ -101,9 +128,21 @@ class UpdateStatusService < BaseService
end
def referred_statuses
return [] unless @options[:text]
return [] unless text
ProcessReferencesService.extract_uris(@options[:text]).filter_map { |uri| ActivityPub::TagManager.instance.local_uri?(uri) && ActivityPub::TagManager.instance.uri_to_resource(uri, Status, url: true) }
reference_urls.filter_map { |uri| ActivityPub::TagManager.instance.local_uri?(uri) && ActivityPub::TagManager.instance.uri_to_resource(uri, Status, url: true) }
end
def quote_url
ProcessReferencesService.extract_quote(text)
end
def reference_urls
@reference_urls ||= ProcessReferencesService.extract_uris(text) || []
end
def text
@options.key?(:text) ? (@options[:text] || '') : @status.text
end
def validate_media!

View file

@ -5,6 +5,7 @@ class VoteService < BaseService
include Payloadable
include Redisable
include Lockable
include NgRuleHelper
def call(account, poll, choices)
return if choices.empty?
@ -16,6 +17,8 @@ class VoteService < BaseService
@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, target_status: @poll.status
already_voted = true
with_redis_lock("vote:#{@poll.id}:#{@account.id}") do