Change: #538 NGワード「フォローしていないアカウントへのメンションで利用できないキーワード」を、参照投稿にも適用する (#544)

* Change: #538 NGワード「フォローしていないアカウントへのメンションで利用できないキーワード」を、参照投稿にも適用する

* Update create.rb

* Update create.rb

* Update create.rb

* Fix test
This commit is contained in:
KMY(雪あすか) 2024-02-16 18:19:14 +09:00 committed by GitHub
parent 5c543c602b
commit 76b5d4f2c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 265 additions and 41 deletions

View file

@ -83,8 +83,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
process_audience
return nil unless valid_status?
return nil if (reply_to_local? || reply_to_local_account? || reply_to_local_from_tags?) && reject_reply_to_local?
return nil if mention_to_local_but_not_followed? && reject_reply_exclude_followers?
return nil if (mention_to_local? || reference_to_local_account?) && reject_reply_to_local?
return nil if (mention_to_local_stranger? || reference_to_local_stranger?) && reject_reply_exclude_followers?
ApplicationRecord.transaction do
@status = Status.create!(@params)
@ -145,7 +145,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def valid_status?
valid = !Admin::NgWord.reject?("#{@params[:spoiler_text]}\n#{@params[:text]}", uri: @params[:uri], target_type: :status, public: @status_parser.distributable_visibility?) && !Admin::NgWord.hashtag_reject?(@tags.size)
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_but_not_followed?
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
end
@ -447,32 +447,30 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
@skip_download ||= DomainBlock.reject_media?(@account.domain)
end
def reply_to_local_account?
accounts_in_audience.any?(&:local?)
end
def reply_to_local_account_following?
!reply_to_local_account? || accounts_in_audience.none? { |account| account.local? && !account.following?(@account) }
end
def reply_to_local_from_tags?
@mentions.present? && @mentions.any? { |m| m.account.local? }
end
def reply_to_local_from_tags_following?
@mentions.nil? || @mentions.none? { |m| m.account.local? && !m.account.following?(@account) }
end
def reply_to_local?
!replied_to_status.nil? && replied_to_status.account.local?
end
def reply_to_local_status_following?
!reply_to_local? || replied_to_status.account.following?(@account)
def mention_to_local?
mentioned_accounts.any?(&:local?)
end
def mention_to_local_but_not_followed?
!reply_to_local_account_following? || !reply_to_local_status_following? || !reply_to_local_from_tags_following?
def mention_to_local_stranger?
mentioned_accounts.any? { |account| account.local? && !account.following?(@account) }
end
def mentioned_accounts
return @mentioned_accounts if defined?(@mentioned_accounts)
@mentioned_accounts = (accounts_in_audience + [replied_to_status&.account] + (@mentions&.map(&:account) || [])).compact.uniq
end
def reference_to_local_account?
local_referred_accounts.any?
end
def reference_to_local_stranger?
local_referred_accounts.any? { |account| !account.following?(@account) }
end
def reject_reply_to_local?
@ -540,10 +538,25 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
retry
end
def process_references!
references = @object['references'].nil? ? [] : ActivityPub::FetchReferencesService.new.call(@status, @object['references'])
def reference_uris
return @reference_uris if defined?(@reference_uris)
ProcessReferencesService.call_service_without_error(@status, [], references, [quote].compact)
@reference_uris = @object['references'].nil? ? [] : (ActivityPub::FetchReferencesService.new.call(@account, @object['references']) || []).uniq
@reference_uris += ProcessReferencesService.extract_uris(@object['content'] || '')
end
def local_referred_accounts
return @local_referred_accounts if defined?(@local_referred_accounts)
local_referred_statuses = reference_uris.filter_map do |uri|
ActivityPub::TagManager.instance.local_uri?(uri) && ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
end.compact
@local_referred_accounts = local_referred_statuses.map(&:account)
end
def process_references!
ProcessReferencesService.call_service_without_error(@status, [], reference_uris, [quote].compact)
end
def quote_local?

View file

@ -3,8 +3,8 @@
class ActivityPub::FetchReferencesService < BaseService
include JsonLdHelper
def call(status, collection_or_uri)
@account = status.account
def call(account, collection_or_uri)
@account = account
collection_items(collection_or_uri)&.take(8)&.map { |item| value_or_id(item) }
end

View file

@ -165,12 +165,16 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
end
def validate_status_mentions!
raise AbortError if mention_to_stranger? && Admin::NgWord.stranger_mention_reject?("#{@status.spoiler_text}\n#{@status.text}", uri: @status.uri, target_type: :status)
raise AbortError if (mention_to_stranger? || reference_to_stranger?) && Admin::NgWord.stranger_mention_reject?("#{@status.spoiler_text}\n#{@status.text}", uri: @status.uri, target_type: :status)
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))
@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?
local_referred_accounts.any? { |account| !account.following?(@account) }
end
def update_immediate_attributes!
@ -271,12 +275,32 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
end
def update_references!
references = @json['references'].nil? ? [] : ActivityPub::FetchReferencesService.new.call(@status, @json['references'])
quote = @json['quote'] || @json['quoteUrl'] || @json['quoteURL'] || @json['_misskey_quote']
references = reference_uris
ProcessReferencesService.call_service_without_error(@status, [], references, [quote].compact)
end
def reference_uris
return @reference_uris if defined?(@reference_uris)
@reference_uris = @json['references'].nil? ? [] : (ActivityPub::FetchReferencesService.new.call(@status.account, @json['references']) || [])
@reference_uris += ProcessReferencesService.extract_uris(@json['content'] || '')
end
def quote
@json['quote'] || @json['quoteUrl'] || @json['quoteURL'] || @json['_misskey_quote']
end
def local_referred_accounts
return @local_referred_accounts if defined?(@local_referred_accounts)
local_referred_statuses = reference_uris.filter_map do |uri|
ActivityPub::TagManager.instance.local_uri?(uri) && ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
end.compact
@local_referred_accounts = local_referred_statuses.map(&:account)
end
def expected_type?
equals_or_includes_any?(@json['type'], %w(Note Question))
end

View file

@ -212,7 +212,7 @@ class PostStatusService < BaseService
end
def validate_status_mentions!
raise Mastodon::ValidationError, I18n.t('statuses.contains_ng_words') if mention_to_stranger? && Setting.stranger_mention_from_local_ng && Admin::NgWord.stranger_mention_reject?("#{@options[:spoiler_text]}\n#{@options[:text]}")
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 mention_to_stranger?
@ -220,6 +220,17 @@ class PostStatusService < BaseService
(@in_reply_to && @in_reply_to.account.id != @account.id && !@in_reply_to.account.following?(@account))
end
def reference_to_stranger?
referred_statuses.any? { |status| !status.account.following?(@account) }
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 += Status.where(id: @reference_ids) if @reference_ids.present?
statuses
end
def validate_media!
if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable)
@media = []

View file

@ -45,6 +45,10 @@ 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)
end
def self.perform_worker_async(status, reference_parameters, urls, quote_urls)
return unless need_process?(status, reference_parameters, urls, quote_urls)

View file

@ -86,7 +86,7 @@ class UpdateStatusService < BaseService
end
def validate_status_mentions!
raise Mastodon::ValidationError, I18n.t('statuses.contains_ng_words') if mention_to_stranger? && Setting.stranger_mention_from_local_ng && Admin::NgWord.stranger_mention_reject?("#{@options[:spoiler_text]}\n#{@options[:text]}")
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 mention_to_stranger?
@ -94,6 +94,16 @@ class UpdateStatusService < BaseService
(@status.thread.present? && @status.thread.account.id != @status.account.id && !@status.thread.account.following?(@status.account))
end
def reference_to_stranger?
referred_statuses.any? { |status| !status.account.following?(@status.account) }
end
def referred_statuses
return [] unless @options[: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) }
end
def validate_media!
return [] if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable)

View file

@ -644,8 +644,8 @@ en:
hide_local_users_for_anonymous: Hide timeline local user posts from anonymous
history_hint: We recommend that you regularly check your NG words to make sure that you have not specified the NG words incorrectly.
keywords: Reject keywords
keywords_for_stranger_mention: Reject keywords when mention/reply from strangers
keywords_for_stranger_mention_hint: Currently this words are checked posts from other servers only.
keywords_for_stranger_mention: Reject keywords when mention/reply/reference/quote from strangers
keywords_for_stranger_mention_hint: This words are checked posts from other servers only.
keywords_hint: The first character of the line is "?". to use regular expressions
post_hash_tags_max: Hash tags max for posts
stranger_mention_from_local_ng: フォローしていないアカウントへのメンションのNGワードを、ローカルユーザーによる投稿にも適用する

View file

@ -637,8 +637,8 @@ ja:
hide_local_users_for_anonymous: ログインしていない状態でローカルユーザーの投稿をタイムラインから取得できないようにする
history_hint: 設定されたNGワードによって実際に拒否された投稿などは、履歴より確認できます。NGワードの指定に誤りがないか定期的に確認することをおすすめします。
keywords: 投稿できないキーワード
keywords_for_stranger_mention: フォローしていないアカウントへのメンションで利用できないキーワード
keywords_for_stranger_mention_hint: フォローしていないアカウントへのメンションにのみ適用されます。現状は外部サーバーから来た投稿のみに適用されます
keywords_for_stranger_mention: フォローしていないアカウントへのメンションや参照で利用できないキーワード
keywords_for_stranger_mention_hint: フォローしていないアカウントへのメンション、参照、引用にのみ適用されます
keywords_hint: 行を「?」で始めると、正規表現が使えます
post_hash_tags_max: 投稿に設定可能なハッシュタグの最大数
stranger_mention_from_local_ng: フォローしていないアカウントへのメンションのNGワードを、ローカルユーザーによる投稿にも適用する

View file

@ -1978,6 +1978,50 @@ RSpec.describe ActivityPub::Activity::Create do
end
end
end
context 'with references' do
let(:recipient) { Fabricate(:account) }
let!(:target_status) { Fabricate(:status, account: recipient) }
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'ohagi is bad',
references: {
id: 'target_status',
type: 'Collection',
first: {
type: 'CollectionPage',
next: nil,
partOf: 'target_status',
items: [
ActivityPub::TagManager.instance.uri_for(target_status),
],
},
},
}
end
context 'with a simple case' do
it 'creates status' do
expect(sender.statuses.first).to be_nil
end
end
context 'with following' do
let(:custom_before_sub) { true }
before do
recipient.follow!(sender)
subject.perform
end
it 'creates status' do
expect(sender.statuses.first).to_not be_nil
end
end
end
end
context 'when hashtags limit is set' do

View file

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe ActivityPub::FetchReferencesService, type: :service do
subject { described_class.new.call(status, payload) }
subject { described_class.new.call(status.account, payload) }
let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') }
let(:status) { Fabricate(:status, account: actor) }

View file

@ -29,7 +29,8 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
tag: json_tags,
}
end
let(:json) { Oj.load(Oj.dump(payload)) }
let(:payload_override) { {} }
let(:json) { Oj.load(Oj.dump(payload.merge(payload_override))) }
let(:alice) { Fabricate(:account) }
let(:bob) { Fabricate(:account) }
@ -601,6 +602,49 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
end
end
context 'when hit ng words for reference' do
let!(:target_status) { Fabricate(:status, account: alice) }
let(:payload_override) do
{
references: {
id: 'target_status',
type: 'Collection',
first: {
type: 'CollectionPage',
next: nil,
partOf: 'target_status',
items: [
ActivityPub::TagManager.instance.uri_for(target_status),
],
},
},
}
end
let(:content) { 'ng word test' }
it 'update status' do
Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '1').save
subject.call(status, json, json)
expect(status.reload.text).to_not eq content
expect(status.references.pluck(:id)).to_not include target_status.id
end
context 'when alice follows sender' do
before do
alice.follow!(status.account)
end
it 'update status' do
Form::AdminSettings.new(ng_words_for_stranger_mention: 'test').save
subject.call(status, json, json)
expect(status.reload.text).to eq content
expect(status.references.pluck(:id)).to include target_status.id
end
end
end
context 'when using hashtag under limit' do
let(:json_tags) do
[

View file

@ -684,6 +684,43 @@ RSpec.describe PostStatusService, type: :service do
expect(status.text).to eq text
end
it 'with a reference' do
target_status = Fabricate(:status)
account = Fabricate(:account)
Fabricate(:account, username: 'ohagi', domain: nil)
text = "ref BT: #{ActivityPub::TagManager.instance.uri_for(target_status)}"
status = subject.call(account, text: text)
expect(status).to be_persisted
expect(status.text).to eq text
expect(status.references.pluck(:id)).to include target_status.id
end
it 'hit ng words for reference' do
target_status = Fabricate(:status)
account = Fabricate(:account)
Fabricate(:account, username: 'ohagi', domain: nil)
text = "ng word test BT: #{ActivityPub::TagManager.instance.uri_for(target_status)}"
Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '1').save
expect { subject.call(account, text: text) }.to raise_error(Mastodon::ValidationError)
end
it 'hit ng words for reference to follower' do
target_status = Fabricate(:status)
account = Fabricate(:account)
target_status.account.follow!(account)
Fabricate(:account, username: 'ohagi', domain: nil)
text = "ng word test BT: #{ActivityPub::TagManager.instance.uri_for(target_status)}"
Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '1').save
status = subject.call(account, text: text)
expect(status).to be_persisted
expect(status.text).to eq text
end
it 'using hashtag under limit' do
account = Fabricate(:account)
text = '#a #b'

View file

@ -344,6 +344,43 @@ RSpec.describe UpdateStatusService, type: :service do
expect(status.text).to eq text
end
it 'add reference' do
target_status = Fabricate(:status)
text = "ng word test BT: #{ActivityPub::TagManager.instance.uri_for(target_status)}"
status = PostStatusService.new.call(account, text: 'hello')
status = subject.call(status, status.account_id, text: text)
expect(status).to be_persisted
expect(status.text).to eq text
expect(status.references.pluck(:id)).to include target_status.id
end
it 'hit ng words for reference' do
target_status = Fabricate(:status)
text = "ng word test BT: #{ActivityPub::TagManager.instance.uri_for(target_status)}"
Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '1').save
status = PostStatusService.new.call(account, text: 'hello')
expect { subject.call(status, status.account_id, text: text) }.to raise_error(Mastodon::ValidationError)
end
it 'hit ng words for reference to follower' do
target_status = Fabricate(:status)
target_status.account.follow!(status.account)
text = "ng word test BT: #{ActivityPub::TagManager.instance.uri_for(target_status)}"
Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '1').save
status = PostStatusService.new.call(account, text: 'hello')
status = subject.call(status, status.account_id, text: text)
expect(status).to be_persisted
expect(status.text).to eq text
end
it 'using hashtag under limit' do
text = '#a #b'
Form::AdminSettings.new(post_hash_tags_max: 2).save