diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index d3f9fea0be..d137a64200 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -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? diff --git a/app/services/activitypub/fetch_references_service.rb b/app/services/activitypub/fetch_references_service.rb index 92d9c1da3f..0c71af58fd 100644 --- a/app/services/activitypub/fetch_references_service.rb +++ b/app/services/activitypub/fetch_references_service.rb @@ -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 diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index 296f87b9cf..5dc3aa4fc1 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -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 diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 91520557d4..6776d08db6 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -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 = [] diff --git a/app/services/process_references_service.rb b/app/services/process_references_service.rb index 910a8c086e..4177dcb383 100644 --- a/app/services/process_references_service.rb +++ b/app/services/process_references_service.rb @@ -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) diff --git a/app/services/update_status_service.rb b/app/services/update_status_service.rb index 3bd234a1ef..da47dfb5f5 100644 --- a/app/services/update_status_service.rb +++ b/app/services/update_status_service.rb @@ -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) diff --git a/config/locales/en.yml b/config/locales/en.yml index a8b0c0ff06..2955685c8f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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ワードを、ローカルユーザーによる投稿にも適用する diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 34d8a56300..983b327c45 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -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ワードを、ローカルユーザーによる投稿にも適用する diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 2966ae82b6..e71c263070 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -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 diff --git a/spec/services/activitypub/fetch_references_service_spec.rb b/spec/services/activitypub/fetch_references_service_spec.rb index 90566818f6..b5d9caabfd 100644 --- a/spec/services/activitypub/fetch_references_service_spec.rb +++ b/spec/services/activitypub/fetch_references_service_spec.rb @@ -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) } diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 4bc5dce046..c08bc59f1e 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -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 [ diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index 2c1b72116d..36e92edfdf 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -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' diff --git a/spec/services/update_status_service_spec.rb b/spec/services/update_status_service_spec.rb index f422d2a206..3893855252 100644 --- a/spec/services/update_status_service_spec.rb +++ b/spec/services/update_status_service_spec.rb @@ -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