From ff994d78fa61c4c2e4adeaaf5a15b92321fd9fd5 Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 18 Sep 2023 18:10:53 +0900 Subject: [PATCH 01/11] Add block from bot follow settings --- app/controllers/api/v1/accounts_controller.rb | 2 +- app/lib/activitypub/activity/follow.rb | 2 +- app/lib/status_reach_finder.rb | 28 +++-- app/models/concerns/has_user_settings.rb | 8 ++ app/models/user_settings.rb | 2 + app/services/follow_service.rb | 2 +- .../settings/preferences/other/show.html.haml | 6 + config/locales/simple_form.en.yml | 2 + config/locales/simple_form.ja.yml | 2 + .../api/v1/accounts_controller_spec.rb | 29 +++++ spec/lib/activitypub/activity/follow_spec.rb | 22 +++- spec/lib/status_reach_finder_spec.rb | 116 +++++++++++++++++- 12 files changed, 208 insertions(+), 13 deletions(-) diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index ddb94d5ca48691..0addcbe2152f64 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -36,7 +36,7 @@ def create def follow follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, languages: params.key?(:languages) ? params[:languages] : nil, with_rate_limit: true) - options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages } }, requested_map: { @account.id => false } } + options = @account.locked? || current_user.account.silenced? || (current_user.account.bot? && @account.user&.setting_lock_follow_from_bot) ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(**options) end diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index 3714648c01d4d0..a586298eec3225 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -30,7 +30,7 @@ def perform follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id']) - if target_account.locked? || @account.silenced? || block_straight_follow? + if target_account.locked? || @account.silenced? || block_straight_follow? || (@account.bot? && target_account.user&.setting_lock_follow_from_bot) LocalNotificationWorker.perform_async(target_account.id, follow_request.id, 'FollowRequest', 'follow_request') else AuthorizeFollowService.new.call(@account, target_account) diff --git a/app/lib/status_reach_finder.rb b/app/lib/status_reach_finder.rb index 7bdc300e1c8660..09596d87691f8b 100644 --- a/app/lib/status_reach_finder.rb +++ b/app/lib/status_reach_finder.rb @@ -30,9 +30,9 @@ def reached_account_inboxes if @status.reblog? [] elsif @status.limited_visibility? - Account.where(id: mentioned_account_ids).where.not(domain: banned_domains).inboxes + expect_bot_query(Account.where(id: mentioned_account_ids).where.not(domain: banned_domains)).inboxes else - Account.where(id: reached_account_ids).where.not(domain: banned_domains).inboxes + expect_bot_query(Account.where(id: reached_account_ids).where.not(domain: banned_domains)).inboxes end end @@ -40,9 +40,9 @@ def reached_account_inboxes_for_misskey if @status.reblog? [] elsif @status.limited_visibility? - Account.where(id: mentioned_account_ids).where(domain: banned_domains_for_misskey).inboxes + expect_bot_query(Account.where(id: mentioned_account_ids).where(domain: banned_domains_for_misskey)).inboxes else - Account.where(id: reached_account_ids).where(domain: banned_domains_for_misskey).inboxes + expect_bot_query(Account.where(id: reached_account_ids).where(domain: banned_domains_for_misskey)).inboxes end end @@ -90,21 +90,25 @@ def replies_account_ids def followers_inboxes if @status.in_reply_to_local_account? && distributable? - @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where.not(domain: banned_domains).inboxes + scope = @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where.not(domain: banned_domains) + expect_bot_query(scope).inboxes elsif @status.direct_visibility? || @status.limited_visibility? [] else - @status.account.followers.where.not(domain: banned_domains).inboxes + scope = @status.account.followers.where.not(domain: banned_domains) + expect_bot_query(scope).inboxes end end def followers_inboxes_for_misskey if @status.in_reply_to_local_account? && distributable? - @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where(domain: banned_domains_for_misskey).inboxes + scope = @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where(domain: banned_domains_for_misskey) + expect_bot_query(scope).inboxes elsif @status.direct_visibility? || @status.limited_visibility? [] else - @status.account.followers.where(domain: banned_domains_for_misskey).inboxes + scope = @status.account.followers.where(domain: banned_domains_for_misskey) + expect_bot_query(scope).inboxes end end @@ -163,4 +167,12 @@ def banned_domains_for_misskey_of_status(status) from_domain_block = DomainBlock.where(detect_invalid_subscription: true).pluck(:domain) (from_info + from_domain_block).uniq end + + def expect_bot_query(scope) + if @status.account.user&.setting_stop_deliver_to_bot + scope.where('actor_type NOT IN (\'Application\', \'Service\') OR accounts.id IN (?)', (@status.mentioned_accounts.pluck(:id) + [@status.in_reply_to_account_id]).compact) + else + scope + end + end end diff --git a/app/models/concerns/has_user_settings.rb b/app/models/concerns/has_user_settings.rb index 674804c2956e0a..3d12b0a07f625b 100644 --- a/app/models/concerns/has_user_settings.rb +++ b/app/models/concerns/has_user_settings.rb @@ -235,6 +235,14 @@ def setting_disallow_unlisted_public_searchability settings['disallow_unlisted_public_searchability'] end + def setting_lock_follow_from_bot + settings['lock_follow_from_bot'] + end + + def setting_stop_deliver_to_bot + settings['stop_deliver_to_bot'] + end + def allows_report_emails? settings['notification_emails.report'] end diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index b16e89556b9099..834469d3591e52 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -40,6 +40,8 @@ class KeyError < Error; end setting :unsafe_limited_distribution, default: false setting :dtl_force_with_tag, default: :none, in: %w(full searchability none) setting :dtl_force_subscribable, default: false + setting :lock_follow_from_bot, default: false + setting :stop_deliver_to_bot, default: false setting_inverse_alias :indexable, :noindex diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index feea40e3c0a945..fc2868a02fedca 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -36,7 +36,7 @@ def call(source_account, target_account, options = {}) # and the feeds are being merged mark_home_feed_as_partial! if @source_account.not_following_anyone? - if (@target_account.locked? && !@options[:bypass_locked]) || @source_account.silenced? || @target_account.activitypub? + if (@target_account.locked? && !@options[:bypass_locked]) || @source_account.silenced? || @target_account.activitypub? || (@source_account.bot? && @target_account.user&.setting_lock_follow_from_bot) request_follow! elsif @target_account.local? direct_follow! diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 641efde9dfcc74..30b411c557b9f3 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -11,6 +11,12 @@ .fields-group = ff.input :aggregate_reblogs, wrapper: :with_label, recommended: true, label: I18n.t('simple_form.labels.defaults.setting_aggregate_reblogs'), hint: I18n.t('simple_form.hints.defaults.setting_aggregate_reblogs') + .fields-group + = ff.input :lock_follow_from_bot, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_lock_follow_from_bot') + + .fields-group + = ff.input :stop_deliver_to_bot, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_stop_deliver_to_bot') + %h4= t 'preferences.posting_defaults' .fields-row diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 355bdb4809ee93..a6592a465808f3 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -262,6 +262,7 @@ en: setting_hide_recent_emojis: Hide recent emojis setting_hide_statuses_count: Hide statuses count setting_link_preview: Generate post link preview card + setting_lock_follow_from_bot: Request approval about bot follow setting_noai: Set noai meta tags setting_public_post_to_unlisted: Convert public post to public unlisted if not using Web app setting_reduce_motion: Reduce motion in animations @@ -272,6 +273,7 @@ en: setting_show_emoji_reaction_on_timeline: Show all stamps on timeline setting_simple_timeline_menu: Reduce post menu on timeline setting_stay_privacy: Not change privacy after post + setting_stop_deliver_to_bot: Stop delivering servers only bots follow you setting_stop_emoji_reaction_streaming: Disable stamp streamings setting_system_font_ui: Use system's default font setting_theme: Site theme diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 9c45732db96c88..0bc51eacff1127 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -276,7 +276,9 @@ ja: setting_hide_recent_emojis: 絵文字ピッカーで最近使用した絵文字を隠す(リアクションデッキのみを表示する) setting_hide_statuses_count: 投稿数を隠す setting_link_preview: リンクのプレビューを生成する + setting_lock_follow_from_bot: botからのフォローを承認制にする setting_stay_privacy: 投稿時に公開範囲を保存する + setting_stop_deliver_to_bot: フォロワーがbotしかいないサーバーへメンション以外の投稿の配送を行わない setting_noai: 自分のコンテンツのAI学習利用に対して不快感を表明する setting_public_post_to_unlisted: サードパーティから公開範囲「公開」で投稿した場合、「ローカル公開」に変更する setting_reduce_motion: アニメーションの動きを減らす diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 0daec691a5db77..985e2b947b1cb1 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -49,10 +49,16 @@ describe 'POST #follow' do let(:scopes) { 'write:follows' } + let(:my_actor_type) { 'Person' } + let(:lock_follow_from_bot) { false } let(:other_account) { Fabricate(:account, username: 'bob', locked: locked) } context 'when posting to an other account' do before do + other_account.user.settings['lock_follow_from_bot'] = lock_follow_from_bot + other_account.user.save! + user.account.update!(actor_type: my_actor_type) + post :follow, params: { id: other_account.id } end @@ -97,6 +103,29 @@ it_behaves_like 'forbidden for wrong scope', 'read:accounts' end + + context 'with unlocked account from bot' do + let(:locked) { false } + let(:lock_follow_from_bot) { true } + let(:my_actor_type) { 'Service' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns JSON with following=false and requested=true' do + json = body_as_json + + expect(json[:following]).to be false + expect(json[:requested]).to be true + end + + it 'creates a follow request relation between user and target user' do + expect(user.account.requested?(other_account)).to be true + end + + it_behaves_like 'forbidden for wrong scope', 'read:accounts' + end end context 'when modifying follow options' do diff --git a/spec/lib/activitypub/activity/follow_spec.rb b/spec/lib/activitypub/activity/follow_spec.rb index ee007082a7ce38..890ebe27509516 100644 --- a/spec/lib/activitypub/activity/follow_spec.rb +++ b/spec/lib/activitypub/activity/follow_spec.rb @@ -3,7 +3,8 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Follow do - let(:sender) { Fabricate(:account, domain: 'example.com', inbox_url: 'https://example.com/inbox') } + let(:actor_type) { 'Person' } + let(:sender) { Fabricate(:account, domain: 'example.com', inbox_url: 'https://example.com/inbox', actor_type: actor_type) } let(:recipient) { Fabricate(:account) } let(:json) do @@ -83,6 +84,25 @@ end end + context 'when unlocked account but locked from bot' do + let(:actor_type) { 'Service' } + + before do + recipient.user.settings['lock_follow_from_bot'] = true + recipient.user.save! + subject.perform + end + + it 'does not create a follow from sender to recipient' do + expect(sender.following?(recipient)).to be false + end + + it 'creates a follow request' do + expect(sender.requested?(recipient)).to be true + expect(sender.follow_requests.find_by(target_account: recipient).uri).to eq 'foo' + end + end + context 'when domain block reject_straight_follow' do before do Fabricate(:domain_block, domain: 'example.com', reject_straight_follow: true) diff --git a/spec/lib/status_reach_finder_spec.rb b/spec/lib/status_reach_finder_spec.rb index 57946d3a70e19a..a0128184f7c289 100644 --- a/spec/lib/status_reach_finder_spec.rb +++ b/spec/lib/status_reach_finder_spec.rb @@ -8,9 +8,10 @@ subject { described_class.new(status) } let(:parent_status) { nil } + let(:in_reply_to_account_id) { nil } let(:visibility) { :public } let(:alice) { Fabricate(:account, username: 'alice') } - let(:status) { Fabricate(:status, account: alice, thread: parent_status, visibility: visibility) } + let(:status) { Fabricate(:status, account: alice, thread: parent_status, in_reply_to_account_id: in_reply_to_account_id, visibility: visibility) } context 'with a simple case' do let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') } @@ -32,6 +33,119 @@ end end + context 'with locking bot' do + let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox', actor_type: 'Service') } + + context 'with follower' do + before do + alice.user.settings['stop_deliver_to_bot'] = true + alice.user.save! + bob.follow!(alice) + end + + it 'send status' do + expect(subject.inboxes).to_not include 'https://foo.bar/inbox' + end + end + + context 'with non-follower' do + it 'send status' do + expect(subject.inboxes).to_not include 'https://foo.bar/inbox' + end + end + end + + context 'with locking bot from misskey' do + let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox', actor_type: 'Service') } + + context 'with follower' do + before do + Fabricate(:instance_info, domain: 'foo.bar', software: 'misskey') + alice.user.settings['stop_deliver_to_bot'] = true + alice.user.save! + bob.follow!(alice) + end + + it 'send status' do + expect(subject.inboxes).to_not include 'https://foo.bar/inbox' + expect(subject.inboxes_for_misskey).to_not include 'https://foo.bar/inbox' + end + end + + context 'with non-follower' do + it 'send status' do + expect(subject.inboxes).to_not include 'https://foo.bar/inbox' + expect(subject.inboxes_for_misskey).to_not include 'https://foo.bar/inbox' + end + end + end + + context 'with bot but not locking' do + let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox', actor_type: 'Service') } + + context 'with follower' do + before do + bob.follow!(alice) + end + + it 'send status' do + expect(subject.inboxes).to include 'https://foo.bar/inbox' + end + end + + context 'with non-follower' do + it 'send status' do + expect(subject.inboxes).to_not include 'https://foo.bar/inbox' + end + end + end + + context 'with bot but mention' do + let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox', actor_type: 'Service') } + let(:in_reply_to_account_id) { bob.id } + + context 'with follower' do + before do + alice.user.settings['stop_deliver_to_bot'] = true + alice.user.save! + bob.follow!(alice) + end + + it 'send status' do + expect(subject.inboxes).to include 'https://foo.bar/inbox' + end + end + + context 'with non-follower' do + it 'send status' do + expect(subject.inboxes).to include 'https://foo.bar/inbox' + end + end + end + + context 'with bot but mention to status' do + let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox', actor_type: 'Service') } + let(:parent_status) { Fabricate(:status, account: bob) } + + context 'with follower' do + before do + alice.user.settings['stop_deliver_to_bot'] = true + alice.user.save! + bob.follow!(alice) + end + + it 'send status' do + expect(subject.inboxes).to include 'https://foo.bar/inbox' + end + end + + context 'with non-follower' do + it 'send status' do + expect(subject.inboxes).to include 'https://foo.bar/inbox' + end + end + end + context 'when misskey case with unlisted post' do let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') } let(:sender_software) { 'mastodon' } From cfa250421e23d2f67f6559d5e41954418689a551 Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 18 Sep 2023 21:33:05 +0900 Subject: [PATCH 02/11] Revert "Add block from bot follow settings" This reverts commit ff994d78fa61c4c2e4adeaaf5a15b92321fd9fd5. --- app/controllers/api/v1/accounts_controller.rb | 2 +- app/lib/activitypub/activity/follow.rb | 2 +- app/lib/status_reach_finder.rb | 28 ++--- app/models/concerns/has_user_settings.rb | 8 -- app/models/user_settings.rb | 2 - app/services/follow_service.rb | 2 +- .../settings/preferences/other/show.html.haml | 6 - config/locales/simple_form.en.yml | 2 - config/locales/simple_form.ja.yml | 2 - .../api/v1/accounts_controller_spec.rb | 29 ----- spec/lib/activitypub/activity/follow_spec.rb | 22 +--- spec/lib/status_reach_finder_spec.rb | 116 +----------------- 12 files changed, 13 insertions(+), 208 deletions(-) diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 0addcbe2152f64..ddb94d5ca48691 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -36,7 +36,7 @@ def create def follow follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, languages: params.key?(:languages) ? params[:languages] : nil, with_rate_limit: true) - options = @account.locked? || current_user.account.silenced? || (current_user.account.bot? && @account.user&.setting_lock_follow_from_bot) ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages } }, requested_map: { @account.id => false } } + options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(**options) end diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index a586298eec3225..3714648c01d4d0 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -30,7 +30,7 @@ def perform follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id']) - if target_account.locked? || @account.silenced? || block_straight_follow? || (@account.bot? && target_account.user&.setting_lock_follow_from_bot) + if target_account.locked? || @account.silenced? || block_straight_follow? LocalNotificationWorker.perform_async(target_account.id, follow_request.id, 'FollowRequest', 'follow_request') else AuthorizeFollowService.new.call(@account, target_account) diff --git a/app/lib/status_reach_finder.rb b/app/lib/status_reach_finder.rb index 09596d87691f8b..7bdc300e1c8660 100644 --- a/app/lib/status_reach_finder.rb +++ b/app/lib/status_reach_finder.rb @@ -30,9 +30,9 @@ def reached_account_inboxes if @status.reblog? [] elsif @status.limited_visibility? - expect_bot_query(Account.where(id: mentioned_account_ids).where.not(domain: banned_domains)).inboxes + Account.where(id: mentioned_account_ids).where.not(domain: banned_domains).inboxes else - expect_bot_query(Account.where(id: reached_account_ids).where.not(domain: banned_domains)).inboxes + Account.where(id: reached_account_ids).where.not(domain: banned_domains).inboxes end end @@ -40,9 +40,9 @@ def reached_account_inboxes_for_misskey if @status.reblog? [] elsif @status.limited_visibility? - expect_bot_query(Account.where(id: mentioned_account_ids).where(domain: banned_domains_for_misskey)).inboxes + Account.where(id: mentioned_account_ids).where(domain: banned_domains_for_misskey).inboxes else - expect_bot_query(Account.where(id: reached_account_ids).where(domain: banned_domains_for_misskey)).inboxes + Account.where(id: reached_account_ids).where(domain: banned_domains_for_misskey).inboxes end end @@ -90,25 +90,21 @@ def replies_account_ids def followers_inboxes if @status.in_reply_to_local_account? && distributable? - scope = @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where.not(domain: banned_domains) - expect_bot_query(scope).inboxes + @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where.not(domain: banned_domains).inboxes elsif @status.direct_visibility? || @status.limited_visibility? [] else - scope = @status.account.followers.where.not(domain: banned_domains) - expect_bot_query(scope).inboxes + @status.account.followers.where.not(domain: banned_domains).inboxes end end def followers_inboxes_for_misskey if @status.in_reply_to_local_account? && distributable? - scope = @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where(domain: banned_domains_for_misskey) - expect_bot_query(scope).inboxes + @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where(domain: banned_domains_for_misskey).inboxes elsif @status.direct_visibility? || @status.limited_visibility? [] else - scope = @status.account.followers.where(domain: banned_domains_for_misskey) - expect_bot_query(scope).inboxes + @status.account.followers.where(domain: banned_domains_for_misskey).inboxes end end @@ -167,12 +163,4 @@ def banned_domains_for_misskey_of_status(status) from_domain_block = DomainBlock.where(detect_invalid_subscription: true).pluck(:domain) (from_info + from_domain_block).uniq end - - def expect_bot_query(scope) - if @status.account.user&.setting_stop_deliver_to_bot - scope.where('actor_type NOT IN (\'Application\', \'Service\') OR accounts.id IN (?)', (@status.mentioned_accounts.pluck(:id) + [@status.in_reply_to_account_id]).compact) - else - scope - end - end end diff --git a/app/models/concerns/has_user_settings.rb b/app/models/concerns/has_user_settings.rb index 3d12b0a07f625b..674804c2956e0a 100644 --- a/app/models/concerns/has_user_settings.rb +++ b/app/models/concerns/has_user_settings.rb @@ -235,14 +235,6 @@ def setting_disallow_unlisted_public_searchability settings['disallow_unlisted_public_searchability'] end - def setting_lock_follow_from_bot - settings['lock_follow_from_bot'] - end - - def setting_stop_deliver_to_bot - settings['stop_deliver_to_bot'] - end - def allows_report_emails? settings['notification_emails.report'] end diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index 834469d3591e52..b16e89556b9099 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -40,8 +40,6 @@ class KeyError < Error; end setting :unsafe_limited_distribution, default: false setting :dtl_force_with_tag, default: :none, in: %w(full searchability none) setting :dtl_force_subscribable, default: false - setting :lock_follow_from_bot, default: false - setting :stop_deliver_to_bot, default: false setting_inverse_alias :indexable, :noindex diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index fc2868a02fedca..feea40e3c0a945 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -36,7 +36,7 @@ def call(source_account, target_account, options = {}) # and the feeds are being merged mark_home_feed_as_partial! if @source_account.not_following_anyone? - if (@target_account.locked? && !@options[:bypass_locked]) || @source_account.silenced? || @target_account.activitypub? || (@source_account.bot? && @target_account.user&.setting_lock_follow_from_bot) + if (@target_account.locked? && !@options[:bypass_locked]) || @source_account.silenced? || @target_account.activitypub? request_follow! elsif @target_account.local? direct_follow! diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 30b411c557b9f3..641efde9dfcc74 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -11,12 +11,6 @@ .fields-group = ff.input :aggregate_reblogs, wrapper: :with_label, recommended: true, label: I18n.t('simple_form.labels.defaults.setting_aggregate_reblogs'), hint: I18n.t('simple_form.hints.defaults.setting_aggregate_reblogs') - .fields-group - = ff.input :lock_follow_from_bot, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_lock_follow_from_bot') - - .fields-group - = ff.input :stop_deliver_to_bot, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_stop_deliver_to_bot') - %h4= t 'preferences.posting_defaults' .fields-row diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index a6592a465808f3..355bdb4809ee93 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -262,7 +262,6 @@ en: setting_hide_recent_emojis: Hide recent emojis setting_hide_statuses_count: Hide statuses count setting_link_preview: Generate post link preview card - setting_lock_follow_from_bot: Request approval about bot follow setting_noai: Set noai meta tags setting_public_post_to_unlisted: Convert public post to public unlisted if not using Web app setting_reduce_motion: Reduce motion in animations @@ -273,7 +272,6 @@ en: setting_show_emoji_reaction_on_timeline: Show all stamps on timeline setting_simple_timeline_menu: Reduce post menu on timeline setting_stay_privacy: Not change privacy after post - setting_stop_deliver_to_bot: Stop delivering servers only bots follow you setting_stop_emoji_reaction_streaming: Disable stamp streamings setting_system_font_ui: Use system's default font setting_theme: Site theme diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 0bc51eacff1127..9c45732db96c88 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -276,9 +276,7 @@ ja: setting_hide_recent_emojis: 絵文字ピッカーで最近使用した絵文字を隠す(リアクションデッキのみを表示する) setting_hide_statuses_count: 投稿数を隠す setting_link_preview: リンクのプレビューを生成する - setting_lock_follow_from_bot: botからのフォローを承認制にする setting_stay_privacy: 投稿時に公開範囲を保存する - setting_stop_deliver_to_bot: フォロワーがbotしかいないサーバーへメンション以外の投稿の配送を行わない setting_noai: 自分のコンテンツのAI学習利用に対して不快感を表明する setting_public_post_to_unlisted: サードパーティから公開範囲「公開」で投稿した場合、「ローカル公開」に変更する setting_reduce_motion: アニメーションの動きを減らす diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 985e2b947b1cb1..0daec691a5db77 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -49,16 +49,10 @@ describe 'POST #follow' do let(:scopes) { 'write:follows' } - let(:my_actor_type) { 'Person' } - let(:lock_follow_from_bot) { false } let(:other_account) { Fabricate(:account, username: 'bob', locked: locked) } context 'when posting to an other account' do before do - other_account.user.settings['lock_follow_from_bot'] = lock_follow_from_bot - other_account.user.save! - user.account.update!(actor_type: my_actor_type) - post :follow, params: { id: other_account.id } end @@ -103,29 +97,6 @@ it_behaves_like 'forbidden for wrong scope', 'read:accounts' end - - context 'with unlocked account from bot' do - let(:locked) { false } - let(:lock_follow_from_bot) { true } - let(:my_actor_type) { 'Service' } - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'returns JSON with following=false and requested=true' do - json = body_as_json - - expect(json[:following]).to be false - expect(json[:requested]).to be true - end - - it 'creates a follow request relation between user and target user' do - expect(user.account.requested?(other_account)).to be true - end - - it_behaves_like 'forbidden for wrong scope', 'read:accounts' - end end context 'when modifying follow options' do diff --git a/spec/lib/activitypub/activity/follow_spec.rb b/spec/lib/activitypub/activity/follow_spec.rb index 890ebe27509516..ee007082a7ce38 100644 --- a/spec/lib/activitypub/activity/follow_spec.rb +++ b/spec/lib/activitypub/activity/follow_spec.rb @@ -3,8 +3,7 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Follow do - let(:actor_type) { 'Person' } - let(:sender) { Fabricate(:account, domain: 'example.com', inbox_url: 'https://example.com/inbox', actor_type: actor_type) } + let(:sender) { Fabricate(:account, domain: 'example.com', inbox_url: 'https://example.com/inbox') } let(:recipient) { Fabricate(:account) } let(:json) do @@ -84,25 +83,6 @@ end end - context 'when unlocked account but locked from bot' do - let(:actor_type) { 'Service' } - - before do - recipient.user.settings['lock_follow_from_bot'] = true - recipient.user.save! - subject.perform - end - - it 'does not create a follow from sender to recipient' do - expect(sender.following?(recipient)).to be false - end - - it 'creates a follow request' do - expect(sender.requested?(recipient)).to be true - expect(sender.follow_requests.find_by(target_account: recipient).uri).to eq 'foo' - end - end - context 'when domain block reject_straight_follow' do before do Fabricate(:domain_block, domain: 'example.com', reject_straight_follow: true) diff --git a/spec/lib/status_reach_finder_spec.rb b/spec/lib/status_reach_finder_spec.rb index a0128184f7c289..57946d3a70e19a 100644 --- a/spec/lib/status_reach_finder_spec.rb +++ b/spec/lib/status_reach_finder_spec.rb @@ -8,10 +8,9 @@ subject { described_class.new(status) } let(:parent_status) { nil } - let(:in_reply_to_account_id) { nil } let(:visibility) { :public } let(:alice) { Fabricate(:account, username: 'alice') } - let(:status) { Fabricate(:status, account: alice, thread: parent_status, in_reply_to_account_id: in_reply_to_account_id, visibility: visibility) } + let(:status) { Fabricate(:status, account: alice, thread: parent_status, visibility: visibility) } context 'with a simple case' do let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') } @@ -33,119 +32,6 @@ end end - context 'with locking bot' do - let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox', actor_type: 'Service') } - - context 'with follower' do - before do - alice.user.settings['stop_deliver_to_bot'] = true - alice.user.save! - bob.follow!(alice) - end - - it 'send status' do - expect(subject.inboxes).to_not include 'https://foo.bar/inbox' - end - end - - context 'with non-follower' do - it 'send status' do - expect(subject.inboxes).to_not include 'https://foo.bar/inbox' - end - end - end - - context 'with locking bot from misskey' do - let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox', actor_type: 'Service') } - - context 'with follower' do - before do - Fabricate(:instance_info, domain: 'foo.bar', software: 'misskey') - alice.user.settings['stop_deliver_to_bot'] = true - alice.user.save! - bob.follow!(alice) - end - - it 'send status' do - expect(subject.inboxes).to_not include 'https://foo.bar/inbox' - expect(subject.inboxes_for_misskey).to_not include 'https://foo.bar/inbox' - end - end - - context 'with non-follower' do - it 'send status' do - expect(subject.inboxes).to_not include 'https://foo.bar/inbox' - expect(subject.inboxes_for_misskey).to_not include 'https://foo.bar/inbox' - end - end - end - - context 'with bot but not locking' do - let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox', actor_type: 'Service') } - - context 'with follower' do - before do - bob.follow!(alice) - end - - it 'send status' do - expect(subject.inboxes).to include 'https://foo.bar/inbox' - end - end - - context 'with non-follower' do - it 'send status' do - expect(subject.inboxes).to_not include 'https://foo.bar/inbox' - end - end - end - - context 'with bot but mention' do - let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox', actor_type: 'Service') } - let(:in_reply_to_account_id) { bob.id } - - context 'with follower' do - before do - alice.user.settings['stop_deliver_to_bot'] = true - alice.user.save! - bob.follow!(alice) - end - - it 'send status' do - expect(subject.inboxes).to include 'https://foo.bar/inbox' - end - end - - context 'with non-follower' do - it 'send status' do - expect(subject.inboxes).to include 'https://foo.bar/inbox' - end - end - end - - context 'with bot but mention to status' do - let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox', actor_type: 'Service') } - let(:parent_status) { Fabricate(:status, account: bob) } - - context 'with follower' do - before do - alice.user.settings['stop_deliver_to_bot'] = true - alice.user.save! - bob.follow!(alice) - end - - it 'send status' do - expect(subject.inboxes).to include 'https://foo.bar/inbox' - end - end - - context 'with non-follower' do - it 'send status' do - expect(subject.inboxes).to include 'https://foo.bar/inbox' - end - end - end - context 'when misskey case with unlisted post' do let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') } let(:sender_software) { 'mastodon' } From 9db1fb1153eaf12324b94430bcb3df75a440a9be Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 18 Sep 2023 21:35:47 +0900 Subject: [PATCH 03/11] Remove stop deliver to bot setting --- app/controllers/api/v1/accounts_controller.rb | 2 +- app/lib/activitypub/activity/follow.rb | 2 +- app/models/concerns/has_user_settings.rb | 4 +++ app/models/user_settings.rb | 1 + app/services/follow_service.rb | 2 +- .../settings/preferences/other/show.html.haml | 3 ++ config/locales/simple_form.en.yml | 1 + config/locales/simple_form.ja.yml | 1 + .../api/v1/accounts_controller_spec.rb | 29 +++++++++++++++++++ spec/lib/activitypub/activity/follow_spec.rb | 22 +++++++++++++- 10 files changed, 63 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index ddb94d5ca48691..0addcbe2152f64 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -36,7 +36,7 @@ def create def follow follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, languages: params.key?(:languages) ? params[:languages] : nil, with_rate_limit: true) - options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages } }, requested_map: { @account.id => false } } + options = @account.locked? || current_user.account.silenced? || (current_user.account.bot? && @account.user&.setting_lock_follow_from_bot) ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify?, languages: follow.languages } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(**options) end diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index 3714648c01d4d0..a586298eec3225 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -30,7 +30,7 @@ def perform follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id']) - if target_account.locked? || @account.silenced? || block_straight_follow? + if target_account.locked? || @account.silenced? || block_straight_follow? || (@account.bot? && target_account.user&.setting_lock_follow_from_bot) LocalNotificationWorker.perform_async(target_account.id, follow_request.id, 'FollowRequest', 'follow_request') else AuthorizeFollowService.new.call(@account, target_account) diff --git a/app/models/concerns/has_user_settings.rb b/app/models/concerns/has_user_settings.rb index 674804c2956e0a..430fc9c044f32b 100644 --- a/app/models/concerns/has_user_settings.rb +++ b/app/models/concerns/has_user_settings.rb @@ -235,6 +235,10 @@ def setting_disallow_unlisted_public_searchability settings['disallow_unlisted_public_searchability'] end + def setting_lock_follow_from_bot + settings['lock_follow_from_bot'] + end + def allows_report_emails? settings['notification_emails.report'] end diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index b16e89556b9099..b52bf67e7f4577 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -40,6 +40,7 @@ class KeyError < Error; end setting :unsafe_limited_distribution, default: false setting :dtl_force_with_tag, default: :none, in: %w(full searchability none) setting :dtl_force_subscribable, default: false + setting :lock_follow_from_bot, default: false setting_inverse_alias :indexable, :noindex diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index feea40e3c0a945..fc2868a02fedca 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -36,7 +36,7 @@ def call(source_account, target_account, options = {}) # and the feeds are being merged mark_home_feed_as_partial! if @source_account.not_following_anyone? - if (@target_account.locked? && !@options[:bypass_locked]) || @source_account.silenced? || @target_account.activitypub? + if (@target_account.locked? && !@options[:bypass_locked]) || @source_account.silenced? || @target_account.activitypub? || (@source_account.bot? && @target_account.user&.setting_lock_follow_from_bot) request_follow! elsif @target_account.local? direct_follow! diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 641efde9dfcc74..353715df01a66e 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -11,6 +11,9 @@ .fields-group = ff.input :aggregate_reblogs, wrapper: :with_label, recommended: true, label: I18n.t('simple_form.labels.defaults.setting_aggregate_reblogs'), hint: I18n.t('simple_form.hints.defaults.setting_aggregate_reblogs') + .fields-group + = ff.input :lock_follow_from_bot, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_lock_follow_from_bot') + %h4= t 'preferences.posting_defaults' .fields-row diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 355bdb4809ee93..958f1040f51e4f 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -262,6 +262,7 @@ en: setting_hide_recent_emojis: Hide recent emojis setting_hide_statuses_count: Hide statuses count setting_link_preview: Generate post link preview card + setting_lock_follow_from_bot: Request approval about bot follow setting_noai: Set noai meta tags setting_public_post_to_unlisted: Convert public post to public unlisted if not using Web app setting_reduce_motion: Reduce motion in animations diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 9c45732db96c88..be5d0e9f6567b8 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -276,6 +276,7 @@ ja: setting_hide_recent_emojis: 絵文字ピッカーで最近使用した絵文字を隠す(リアクションデッキのみを表示する) setting_hide_statuses_count: 投稿数を隠す setting_link_preview: リンクのプレビューを生成する + setting_lock_follow_from_bot: botからのフォローを承認制にする setting_stay_privacy: 投稿時に公開範囲を保存する setting_noai: 自分のコンテンツのAI学習利用に対して不快感を表明する setting_public_post_to_unlisted: サードパーティから公開範囲「公開」で投稿した場合、「ローカル公開」に変更する diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 0daec691a5db77..985e2b947b1cb1 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -49,10 +49,16 @@ describe 'POST #follow' do let(:scopes) { 'write:follows' } + let(:my_actor_type) { 'Person' } + let(:lock_follow_from_bot) { false } let(:other_account) { Fabricate(:account, username: 'bob', locked: locked) } context 'when posting to an other account' do before do + other_account.user.settings['lock_follow_from_bot'] = lock_follow_from_bot + other_account.user.save! + user.account.update!(actor_type: my_actor_type) + post :follow, params: { id: other_account.id } end @@ -97,6 +103,29 @@ it_behaves_like 'forbidden for wrong scope', 'read:accounts' end + + context 'with unlocked account from bot' do + let(:locked) { false } + let(:lock_follow_from_bot) { true } + let(:my_actor_type) { 'Service' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns JSON with following=false and requested=true' do + json = body_as_json + + expect(json[:following]).to be false + expect(json[:requested]).to be true + end + + it 'creates a follow request relation between user and target user' do + expect(user.account.requested?(other_account)).to be true + end + + it_behaves_like 'forbidden for wrong scope', 'read:accounts' + end end context 'when modifying follow options' do diff --git a/spec/lib/activitypub/activity/follow_spec.rb b/spec/lib/activitypub/activity/follow_spec.rb index ee007082a7ce38..890ebe27509516 100644 --- a/spec/lib/activitypub/activity/follow_spec.rb +++ b/spec/lib/activitypub/activity/follow_spec.rb @@ -3,7 +3,8 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Follow do - let(:sender) { Fabricate(:account, domain: 'example.com', inbox_url: 'https://example.com/inbox') } + let(:actor_type) { 'Person' } + let(:sender) { Fabricate(:account, domain: 'example.com', inbox_url: 'https://example.com/inbox', actor_type: actor_type) } let(:recipient) { Fabricate(:account) } let(:json) do @@ -83,6 +84,25 @@ end end + context 'when unlocked account but locked from bot' do + let(:actor_type) { 'Service' } + + before do + recipient.user.settings['lock_follow_from_bot'] = true + recipient.user.save! + subject.perform + end + + it 'does not create a follow from sender to recipient' do + expect(sender.following?(recipient)).to be false + end + + it 'creates a follow request' do + expect(sender.requested?(recipient)).to be true + expect(sender.follow_requests.find_by(target_account: recipient).uri).to eq 'foo' + end + end + context 'when domain block reject_straight_follow' do before do Fabricate(:domain_block, domain: 'example.com', reject_straight_follow: true) From 80fadeb7665e8df86e3f6b7dc8433d5f78647d12 Mon Sep 17 00:00:00 2001 From: KMY Date: Tue, 19 Sep 2023 10:04:22 +0900 Subject: [PATCH 04/11] Add single_ref_to_quote setting and check ref sync --- app/models/concerns/has_user_settings.rb | 4 ++++ app/models/user_settings.rb | 1 + .../activitypub/note_serializer.rb | 20 +++++++++++++++++++ app/services/post_status_service.rb | 2 +- app/services/process_references_service.rb | 6 ++++++ app/services/update_status_service.rb | 2 +- .../settings/preferences/other/show.html.haml | 3 +++ config/locales/simple_form.en.yml | 1 + config/locales/simple_form.ja.yml | 1 + 9 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/models/concerns/has_user_settings.rb b/app/models/concerns/has_user_settings.rb index 430fc9c044f32b..780c6345bb3e16 100644 --- a/app/models/concerns/has_user_settings.rb +++ b/app/models/concerns/has_user_settings.rb @@ -127,6 +127,10 @@ def setting_link_preview settings['link_preview'] end + def setting_single_ref_to_quote + settings['single_ref_to_quote'] + end + def setting_dtl_force_with_tag settings['dtl_force_with_tag']&.to_sym || :none end diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index b52bf67e7f4577..93f7f4a64f2c7f 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -41,6 +41,7 @@ class KeyError < Error; end setting :dtl_force_with_tag, default: :none, in: %w(full searchability none) setting :dtl_force_subscribable, default: false setting :lock_follow_from_bot, default: false + setting :single_ref_to_quote, default: false setting_inverse_alias :indexable, :noindex diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index ec16d3b56709f7..19b22b9b0b984f 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -17,6 +17,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer attribute :content_map, if: :language? attribute :updated, if: :edited? + attribute :quote_uri, if: :quote? + attribute :misskey_quote, key: :_misskey_quote, if: :quote? + attribute :misskey_content, key: :_misskey_content, if: :quote? + has_many :virtual_attachments, key: :attachment has_many :virtual_tags, key: :tag @@ -160,6 +164,22 @@ def not_private_post? !object.private_visibility? && !object.direct_visibility? && !object.limited_visibility? end + def quote? + object.references.count == 1 && object.account.user&.single_ref_to_quote + end + + def quote_uri + ActivityPub::TagManager.instance.uri_for(object.references.first) + end + + def misskey_quote + quote_uri + end + + def misskey_content + object.text + end + def poll_options object.preloadable_poll.loaded_options end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index e8725528d9e025..a549ab1c78a893 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -188,7 +188,7 @@ def postprocess_status! process_hashtags_service.call(@status) Trends.tags.register(@status) - ProcessReferencesService.perform_worker_async(@status, @reference_ids, []) + ProcessReferencesService.call_service(@status, @reference_ids, []) LinkCrawlWorker.perform_async(@status.id) DistributionWorker.perform_async(@status.id) ActivityPub::DistributionWorker.perform_async(@status.id) diff --git a/app/services/process_references_service.rb b/app/services/process_references_service.rb index c98a0434c79421..1043190473f63b 100644 --- a/app/services/process_references_service.rb +++ b/app/services/process_references_service.rb @@ -40,6 +40,12 @@ def self.perform_worker_async(status, reference_parameters, urls) ProcessReferencesWorker.perform_async(status.id, reference_parameters, urls) end + def self.call_service(status, reference_parameters, urls) + return unless need_process?(status, reference_parameters, urls) + + ProcessReferencesService.new.call(status, reference_parameters || [], urls: urls || []) + end + private def references diff --git a/app/services/update_status_service.rb b/app/services/update_status_service.rb index 8d99ebdd3ddcd3..6e63fa97421051 100644 --- a/app/services/update_status_service.rb +++ b/app/services/update_status_service.rb @@ -162,7 +162,7 @@ def reset_preview_card! def update_references! reference_ids = (@options[:status_reference_ids] || []).map(&:to_i).filter(&:positive?) - ProcessReferencesService.perform_worker_async(@status, reference_ids, []) + ProcessReferencesService.call_service(@status, reference_ids, []) end def update_metadata! diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 353715df01a66e..149a3d56efbc8b 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -14,6 +14,9 @@ .fields-group = ff.input :lock_follow_from_bot, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_lock_follow_from_bot') + .fields-group + = ff.input :single_ref_to_quote, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_single_ref_to_quote') + %h4= t 'preferences.posting_defaults' .fields-row diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 958f1040f51e4f..576b535f109682 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -272,6 +272,7 @@ en: setting_show_application: Disclose application used to send posts setting_show_emoji_reaction_on_timeline: Show all stamps on timeline setting_simple_timeline_menu: Reduce post menu on timeline + setting_single_ref_to_quote: Deliver single reference to other server as quote setting_stay_privacy: Not change privacy after post setting_stop_emoji_reaction_streaming: Disable stamp streamings setting_system_font_ui: Use system's default font diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index be5d0e9f6567b8..6283e5f431ae27 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -287,6 +287,7 @@ ja: setting_show_application: 送信したアプリを開示する setting_show_emoji_reaction_on_timeline: タイムライン上に他の人のつけたスタンプを表示する setting_simple_timeline_menu: タイムライン上でメニューの項目を減らす + setting_single_ref_to_quote: 参照が1つしかない投稿は、他のサーバーには引用として配信する setting_stay_privacy: 投稿時に公開範囲を保存する setting_stop_emoji_reaction_streaming: スタンプのストリーミングを停止する setting_system_font_ui: システムのデフォルトフォントを使う From dc6ad3847480e4fabefa39d32b3965fe28ff4f31 Mon Sep 17 00:00:00 2001 From: KMY Date: Tue, 19 Sep 2023 10:57:36 +0900 Subject: [PATCH 05/11] Fix original mastodon bug --- app/javascript/mastodon/reducers/search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/reducers/search.js b/app/javascript/mastodon/reducers/search.js index 77c8eebb9afd7e..e34ad8bd2ce799 100644 --- a/app/javascript/mastodon/reducers/search.js +++ b/app/javascript/mastodon/reducers/search.js @@ -75,7 +75,7 @@ export default function search(state = initialState, action) { map.set('isLoading', false); }); case SEARCH_EXPAND_REQUEST: - return state.set('type', action.searchType); + return state.set('type', action.searchType); // .set('isLoading', true); // original Mastodon bug case SEARCH_EXPAND_SUCCESS: const results = action.searchType === 'hashtags' ? ImmutableOrderedSet(fromJS(action.results.hashtags)) : action.results[action.searchType].map(item => item.id); return state.updateIn(['results', action.searchType], list => list.union(results)).setIn(['noMoreResults', action.searchType], results.size <= 0); From ac915654b671f060100154a71fa0d7c2b579580f Mon Sep 17 00:00:00 2001 From: KMY Date: Tue, 19 Sep 2023 17:53:13 +0900 Subject: [PATCH 06/11] Add optional fetching with processing status references --- app/services/process_references_service.rb | 55 +++++++++++++------ app/services/resolve_url_service.rb | 4 +- .../settings/preferences/other/show.html.haml | 2 +- app/workers/process_references_worker.rb | 4 +- config/locales/simple_form.en.yml | 1 + config/locales/simple_form.ja.yml | 1 + 6 files changed, 45 insertions(+), 22 deletions(-) diff --git a/app/services/process_references_service.rb b/app/services/process_references_service.rb index 1043190473f63b..42d47e43fabfc6 100644 --- a/app/services/process_references_service.rb +++ b/app/services/process_references_service.rb @@ -3,30 +3,39 @@ class ProcessReferencesService < BaseService include Payloadable include FormattingHelper + include Redisable + include Lockable 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))})/ MAX_REFERENCES = 5 - def call(status, reference_parameters, urls: nil) + def call(status, reference_parameters, urls: nil, fetch_remote: true, no_fetch_urls: nil) @status = status @reference_parameters = reference_parameters || [] @urls = urls || [] + @no_fetch_urls = no_fetch_urls || [] + @fetch_remote = fetch_remote + @again = false - @references_count = old_references.size + with_redis_lock("process_status_refs:#{@status.id}") do + @references_count = old_references.size - return unless added_references.size.positive? || removed_references.size.positive? + if added_references.size.positive? || removed_references.size.positive? + StatusReference.transaction do + remove_old_references + add_references - StatusReference.transaction do - remove_old_references - add_references + @status.save! + end - @status.save! - end + create_notifications! + end - Rails.cache.delete("status_reference:#{@status.id}") + Rails.cache.delete("status_reference:#{@status.id}") + end - create_notifications! + launch_worker if @again end def self.need_process?(status, reference_parameters, urls) @@ -37,13 +46,13 @@ def self.perform_worker_async(status, reference_parameters, urls) return unless need_process?(status, reference_parameters, urls) Rails.cache.write("status_reference:#{status.id}", true, expires_in: 10.minutes) - ProcessReferencesWorker.perform_async(status.id, reference_parameters, urls) + ProcessReferencesWorker.perform_async(status.id, reference_parameters, urls, []) end def self.call_service(status, reference_parameters, urls) return unless need_process?(status, reference_parameters, urls) - ProcessReferencesService.new.call(status, reference_parameters || [], urls: urls || []) + ProcessReferencesService.new.call(status, reference_parameters || [], urls: urls || [], fetch_remote: false) end private @@ -65,14 +74,22 @@ def removed_references end def scan_text! - text = @status.account.local? ? @status.text : @status.text.gsub(%r{]*>}, '') - @scan_text = fetch_statuses!(text.scan(REFURL_EXP).pluck(3).uniq).map(&:id).uniq.filter { |status_id| !status_id.zero? } + text = extract_status_plain_text(@status) + statuses = fetch_statuses!(text.scan(REFURL_EXP).pluck(3).uniq) + + @again = true if !@fetch_remote && statuses.any?(&:nil?) + + @scan_text = statuses.compact.map(&:id).uniq.filter { |status_id| !status_id.zero? } end def fetch_statuses!(urls) - (urls + @urls) - .map { |url| ResolveURLService.new.call(url, on_behalf_of: @status.account) } - .filter { |status| status } + target_urls = urls + @urls + + target_urls.map do |url| + status = ResolveURLService.new.call(url, on_behalf_of: @status.account, fetch_remote: @fetch_remote && @no_fetch_urls.exclude?(url)) + @no_fetch_urls << url if !@fetch_remote && status.present? + status + end end def add_references @@ -112,4 +129,8 @@ def remove_old_references @references_count -= 1 end end + + def launch_worker + ProcessReferencesWorker.perform_async(@status.id, @reference_parameters, @urls, @no_fetch_urls) + end end diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb index 19a94e77ad13bf..1e068af6ab90ba 100644 --- a/app/services/resolve_url_service.rb +++ b/app/services/resolve_url_service.rb @@ -6,13 +6,13 @@ class ResolveURLService < BaseService USERNAME_STATUS_RE = %r{/@(?#{Account::USERNAME_RE})/(?[0-9]+)\Z} - def call(url, on_behalf_of: nil) + def call(url, on_behalf_of: nil, fetch_remote: true) @url = url @on_behalf_of = on_behalf_of if local_url? process_local_url - elsif !fetched_resource.nil? + elsif fetch_remote && !fetched_resource.nil? process_url else process_url_from_db diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 149a3d56efbc8b..acde16d1c6be97 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -15,7 +15,7 @@ = ff.input :lock_follow_from_bot, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_lock_follow_from_bot') .fields-group - = ff.input :single_ref_to_quote, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_single_ref_to_quote') + = ff.input :single_ref_to_quote, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_single_ref_to_quote'), hint: I18n.t('simple_form.labels.defaults.setting_single_ref_to_quote') %h4= t 'preferences.posting_defaults' diff --git a/app/workers/process_references_worker.rb b/app/workers/process_references_worker.rb index 7d59d964c9de28..a3815e1ece6f0e 100644 --- a/app/workers/process_references_worker.rb +++ b/app/workers/process_references_worker.rb @@ -3,8 +3,8 @@ class ProcessReferencesWorker include Sidekiq::Worker - def perform(status_id, ids, urls) - ProcessReferencesService.new.call(Status.find(status_id), ids || [], urls: urls || []) + def perform(status_id, ids, urls, no_fetch_urls) + ProcessReferencesService.new.call(Status.find(status_id), ids || [], urls: urls || [], no_fetch_urls: no_fetch_urls) rescue ActiveRecord::RecordNotFound true end diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 576b535f109682..550a071303ea3e 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -71,6 +71,7 @@ en: setting_dtl_menu: Show DTL menu on web setting_emoji_reaction_policy: Even with this setting, users on other servers are free to put their stamp on the post and share it within the same server. If you simply want to remove the stamp from your own screen, you can disable it from the appearance settings setting_enable_emoji_reaction: If turn off, other users still can react your posts + setting_single_ref_to_quote: If this server does not have target post, target server maybe cannot read your quote setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed username: You can use letters, numbers, and underscores diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 6283e5f431ae27..45d6516d1dc855 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -82,6 +82,7 @@ ja: setting_public_post_to_unlisted: 未対応のサードパーティアプリからもローカル公開で投稿できますが、公開投稿はWeb以外できなくなります setting_reject_unlisted_subscription: Misskeyやそのフォーク(Calckeyなど)は、フォローしていないアカウントの「未収載」投稿を **購読・検索** することができます。これはkmyblueの挙動と異なります。そのようなサーバーに、指定した公開範囲の投稿を「フォロワーのみ」として配送します。ただし構造上、完璧な対応は困難でたまに未収載として配信されること、ご理解ください setting_show_application: 投稿するのに使用したアプリが投稿の詳細ビューに表示されるようになります + setting_single_ref_to_quote: 当サーバーがまだ対象投稿を取り込んでいない場合、引用が相手に正常に認識されない場合があります setting_stop_emoji_reaction_streaming: 通信容量の節約に役立ちます setting_unsafe_limited_distribution: Mastodon 3.5、4.0、4.1のサーバーにも限定投稿(相互のみ)が届くようになりますが、安全でない方法で送信します setting_use_blurhash: ぼかしはメディアの色を元に生成されますが、細部は見えにくくなっています From 77eb7c03d95c627b6dc02480153b41076580286b Mon Sep 17 00:00:00 2001 From: KMY Date: Tue, 19 Sep 2023 17:53:34 +0900 Subject: [PATCH 07/11] Add ProcessReferencesService test --- app/services/notify_service.rb | 1 + .../process_references_service_spec.rb | 197 ++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 spec/services/process_references_service_spec.rb diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 853c4f577e5349..eaa5ce233b4cda 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -9,6 +9,7 @@ class NotifyService < BaseService update poll emoji_reaction + status_reference warning ).freeze diff --git a/spec/services/process_references_service_spec.rb b/spec/services/process_references_service_spec.rb new file mode 100644 index 00000000000000..0cb2d2f190738b --- /dev/null +++ b/spec/services/process_references_service_spec.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ProcessReferencesService, type: :service do + let(:text) { 'Hello' } + let(:account) { Fabricate(:user).account } + let(:visibility) { :public } + let(:status) { Fabricate(:status, account: account, text: text, visibility: visibility) } + let(:target_status) { Fabricate(:status, account: Fabricate(:user).account) } + let(:target_status_uri) { ActivityPub::TagManager.instance.uri_for(target_status) } + + describe 'posting new status' do + subject do + described_class.new.call(status, reference_parameters, urls: urls, fetch_remote: fetch_remote) + status.references.pluck(:id) + end + + let(:reference_parameters) { [] } + let(:urls) { [] } + let(:fetch_remote) { true } + + context 'when a simple case' do + let(:text) { "Hello RT #{target_status_uri}" } + + it 'post status' do + ids = subject + expect(ids.size).to eq 1 + expect(ids).to include target_status.id + end + end + + context 'when multiple references' do + let(:target_status2) { Fabricate(:status) } + let(:target_status2_uri) { ActivityPub::TagManager.instance.uri_for(target_status2) } + let(:text) { "Hello RT #{target_status_uri}\nBT #{target_status2_uri}" } + + it 'post status' do + ids = subject + expect(ids.size).to eq 2 + expect(ids).to include target_status.id + expect(ids).to include target_status2.id + end + end + + context 'when url only' do + let(:text) { "Hello #{target_status_uri}" } + + it 'post status' do + ids = subject + expect(ids.size).to eq 0 + end + end + + context 'when unfetched remote post' do + let(:account) { Fabricate(:account, followers_url: 'http://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor') } + let(:object_json) do + { + id: 'https://example.com/test_post', + to: ActivityPub::TagManager::COLLECTIONS[:public], + '@context': ActivityPub::TagManager::CONTEXT, + type: 'Note', + actor: account.uri, + attributedTo: account.uri, + content: 'Lorem ipsum', + published: '2022-01-22T15:00:00Z', + updated: '2022-01-22T16:00:00Z', + } + end + let(:text) { 'BT https://example.com/test_post' } + + before do + stub_request(:get, 'https://example.com/test_post').to_return(status: 200, body: Oj.dump(object_json), headers: { 'Content-Type' => 'application/activity+json' }) + stub_request(:get, 'https://example.com/not_found').to_return(status: 404) + end + + it 'reference it' do + ids = subject + expect(ids.size).to eq 1 + + status = Status.find_by(id: ids[0]) + expect(status).to_not be_nil + expect(status.url).to eq 'https://example.com/test_post' + end + + context 'with fetch_remote later' do + let(:fetch_remote) { false } + + it 'reference it' do + ids = subject + expect(ids.size).to eq 1 + + status = Status.find_by(id: ids[0]) + expect(status).to_not be_nil + expect(status.url).to eq 'https://example.com/test_post' + end + end + + context 'with fetch_remote later with has existing reference' do + let(:fetch_remote) { false } + let(:text) { "RT #{ActivityPub::TagManager.instance.uri_for(target_status)} BT https://example.com/test_post" } + + it 'reference it' do + ids = subject + expect(ids.size).to eq 2 + expect(ids).to include target_status.id + + status = Status.find_by(id: ids, uri: 'https://example.com/test_post') + expect(status).to_not be_nil + end + end + + context 'with not exists reference' do + let(:text) { 'BT https://example.com/not_found' } + + it 'reference it' do + ids = subject + expect(ids.size).to eq 0 + end + end + end + end + + describe 'editing new status' do + subject do + status.update!(text: new_text) + described_class.new.call(status, reference_parameters, urls: urls, fetch_remote: fetch_remote) + status.references.pluck(:id) + end + + let(:target_status2) { Fabricate(:status, account: Fabricate(:user).account) } + let(:target_status2_uri) { ActivityPub::TagManager.instance.uri_for(target_status2) } + + let(:new_text) { 'Hello' } + let(:reference_parameters) { [] } + let(:urls) { [] } + let(:fetch_remote) { true } + + before do + described_class.new.call(status, reference_parameters, urls: urls, fetch_remote: fetch_remote) + end + + context 'when add reference to empty' do + let(:new_text) { "BT #{target_status_uri}" } + + it 'post status' do + ids = subject + expect(ids.size).to eq 1 + expect(ids).to include target_status.id + end + end + + context 'when add reference to have anyone' do + let(:text) { "BT #{target_status_uri}" } + let(:new_text) { "BT #{target_status_uri}\nBT #{target_status2_uri}" } + + it 'post status' do + ids = subject + expect(ids.size).to eq 2 + expect(ids).to include target_status.id + expect(ids).to include target_status2.id + end + end + + context 'when add reference but has same' do + let(:text) { "BT #{target_status_uri}" } + let(:new_text) { "BT #{target_status_uri}\nBT #{target_status_uri}" } + + it 'post status' do + ids = subject + expect(ids.size).to eq 1 + expect(ids).to include target_status.id + end + end + + context 'when remove reference' do + let(:text) { "BT #{target_status_uri}" } + let(:new_text) { 'Hello' } + + it 'post status' do + ids = subject + expect(ids.size).to eq 0 + end + end + + context 'when change reference' do + let(:text) { "BT #{target_status_uri}" } + let(:new_text) { "BT #{target_status2_uri}" } + + it 'post status' do + ids = subject + expect(ids.size).to eq 1 + expect(ids).to include target_status2.id + end + end + end +end From 818bcc0277a3c9f6802a15ac39aec00be8a2b7e6 Mon Sep 17 00:00:00 2001 From: KMY Date: Tue, 19 Sep 2023 18:22:18 +0900 Subject: [PATCH 08/11] Fix quote is not set in noteserializer --- app/helpers/context_helper.rb | 1 + app/lib/activitypub/case_transform.rb | 9 +++++++- .../activitypub/note_serializer.rb | 4 ++-- .../activitypub/note_serializer_spec.rb | 22 +++++++++++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 7aa82312c6f4c4..e43c3d154d6325 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -29,6 +29,7 @@ module ContextHelper limited_scope: { 'kmyblue' => 'http://kmy.blue/ns#', 'limitedScope' => { '@id' => 'kmyblue:limitedScope', '@type' => '@id' } }, other_setting: { 'fedibird' => 'http://fedibird.com/ns#', 'otherSetting' => 'fedibird:otherSetting' }, references: { 'fedibird' => 'http://fedibird.com/ns#', 'references' => { '@id' => 'fedibird:references', '@type' => '@id' } }, + quote_uri: { 'fedibird' => 'http://fedibird.com/ns#', 'quoteUri' => 'fedibird:quoteUri' }, olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, diff --git a/app/lib/activitypub/case_transform.rb b/app/lib/activitypub/case_transform.rb index da2c5eb8b05774..891c5a5d433a66 100644 --- a/app/lib/activitypub/case_transform.rb +++ b/app/lib/activitypub/case_transform.rb @@ -2,6 +2,11 @@ module ActivityPub::CaseTransform class << self + NO_CONVERT_VALUES = %w( + _misskey_content + _misskey_quote + ).freeze + def camel_lower_cache @camel_lower_cache ||= {} end @@ -12,7 +17,9 @@ def camel_lower(value) when Hash then value.deep_transform_keys! { |key| camel_lower(key) } when Symbol then camel_lower(value.to_s).to_sym when String - camel_lower_cache[value] ||= if value.start_with?('_:') + camel_lower_cache[value] ||= if NO_CONVERT_VALUES.include?(value) + value + elsif value.start_with?('_:') "_:#{value.delete_prefix('_:').underscore.camelize(:lower)}" else value.underscore.camelize(:lower) diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 19b22b9b0b984f..674727d828e736 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -3,7 +3,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer include FormattingHelper - context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :searchable_by, :references, :limited_scope + context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :searchable_by, :references, :limited_scope, :quote_uri attributes :id, :type, :summary, :in_reply_to, :published, :url, @@ -165,7 +165,7 @@ def not_private_post? end def quote? - object.references.count == 1 && object.account.user&.single_ref_to_quote + object.references.count == 1 && object.account.user&.settings&.[]('single_ref_to_quote') end def quote_uri diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index 4b2b8ec875667f..bcfbb27406654c 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -13,8 +13,12 @@ let!(:reply_by_other_first) { Fabricate(:status, account: other, thread: parent, visibility: :public) } let!(:reply_by_account_third) { Fabricate(:status, account: account, thread: parent, visibility: :public) } let!(:reply_by_account_visibility_direct) { Fabricate(:status, account: account, thread: parent, visibility: :direct) } + let!(:referred) { nil } + let(:convert_to_quote) { false } before(:each) do + parent.references << referred if referred.present? + account.user&.settings&.[]=('single_ref_to_quote', true) if convert_to_quote @serialization = ActiveModelSerializers::SerializableResource.new(parent, serializer: described_class, adapter: ActivityPub::Adapter) end @@ -41,4 +45,22 @@ it 'does not include replies with direct visibility in its replies collection' do expect(subject['replies']['first']['items']).to_not include(reply_by_account_visibility_direct.uri) end + + context 'when has quote but no_convert setting' do + let(:referred) { Fabricate(:status) } + + it 'has as reference' do + expect(subject['quoteUri']).to be_nil + end + end + + context 'when has quote and convert setting' do + let(:referred) { Fabricate(:status) } + let(:convert_to_quote) { true } + + it 'has as quote' do + expect(subject['quoteUri']).to_not be_nil + expect(subject['_misskey_quote'] == subject['quoteUri']).to be true + end + end end From 11300d7550aa5b83213bc3a36ba22f31edb99e2b Mon Sep 17 00:00:00 2001 From: KMY Date: Tue, 19 Sep 2023 18:50:16 +0900 Subject: [PATCH 09/11] Allow private posts with references --- .../activitypub/note_serializer.rb | 21 +++++++++----- .../activitypub/note_serializer_spec.rb | 28 ++++++++++++++++++- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 674727d828e736..109f8115df6bce 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -11,8 +11,6 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer :atom_uri, :in_reply_to_atom_uri, :conversation, :searchable_by, :limited_scope - attribute :references, if: :not_private_post? - attribute :content attribute :content_map, if: :language? attribute :updated, if: :edited? @@ -25,6 +23,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer has_many :virtual_tags, key: :tag has_one :replies, serializer: ActivityPub::CollectionSerializer, if: :local? + has_one :references, serializer: ActivityPub::CollectionSerializer has_many :poll_options, key: :one_of, if: :poll_and_not_multiple? has_many :poll_options, key: :any_of, if: :poll_and_multiple? @@ -71,7 +70,19 @@ def replies end def references - ActivityPub::TagManager.instance.references_uri_for(object) + refs = object.references.reorder(id: :asc).take(5).pluck(:id, :uri) + last_id = refs.last&.first + + ActivityPub::CollectionPresenter.new( + type: :unordered, + id: ActivityPub::TagManager.instance.references_uri_for(object), + first: ActivityPub::CollectionPresenter.new( + type: :unordered, + part_of: ActivityPub::TagManager.instance.references_uri_for(object), + items: refs.map(&:second), + next: last_id ? ActivityPub::TagManager.instance.references_uri_for(object, page: true, min_id: last_id) : ActivityPub::TagManager.instance.references_uri_for(object, page: true, only_other_accounts: true) + ) + ) end def language? @@ -160,10 +171,6 @@ def local? object.account.local? end - def not_private_post? - !object.private_visibility? && !object.direct_visibility? && !object.limited_visibility? - end - def quote? object.references.count == 1 && object.account.user&.settings&.[]('single_ref_to_quote') end diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index bcfbb27406654c..f4248e548e1b1b 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -14,10 +14,12 @@ let!(:reply_by_account_third) { Fabricate(:status, account: account, thread: parent, visibility: :public) } let!(:reply_by_account_visibility_direct) { Fabricate(:status, account: account, thread: parent, visibility: :direct) } let!(:referred) { nil } + let!(:referred2) { nil } let(:convert_to_quote) { false } before(:each) do parent.references << referred if referred.present? + parent.references << referred2 if referred2.present? account.user&.settings&.[]=('single_ref_to_quote', true) if convert_to_quote @serialization = ActiveModelSerializers::SerializableResource.new(parent, serializer: described_class, adapter: ActivityPub::Adapter) end @@ -49,8 +51,17 @@ context 'when has quote but no_convert setting' do let(:referred) { Fabricate(:status) } + it 'has a references collection' do + expect(subject['references']['type']).to eql('Collection') + end + + it 'has a references collection with a first Page' do + expect(subject['references']['first']['type']).to eql('CollectionPage') + end + it 'has as reference' do expect(subject['quoteUri']).to be_nil + expect(subject['references']['first']['items']).to include referred.uri end end @@ -60,7 +71,22 @@ it 'has as quote' do expect(subject['quoteUri']).to_not be_nil - expect(subject['_misskey_quote'] == subject['quoteUri']).to be true + expect(subject['quoteUri']).to eq referred.uri + expect(subject['_misskey_quote']).to eq referred.uri + expect(subject['_misskey_content']).to eq referred.text + expect(subject['references']['first']['items']).to include referred.uri + end + end + + context 'when has multiple references and convert setting' do + let(:referred) { Fabricate(:status) } + let(:referred2) { Fabricate(:status) } + let(:convert_to_quote) { true } + + it 'has as quote' do + expect(subject['quoteUri']).to be_nil + expect(subject['references']['first']['items']).to include referred.uri + expect(subject['references']['first']['items']).to include referred2.uri end end end From 48b78e6d1267e26e54cd0a17f98d03659341475f Mon Sep 17 00:00:00 2001 From: KMY Date: Tue, 19 Sep 2023 18:51:20 +0900 Subject: [PATCH 10/11] Remove reference status visibility validation --- app/models/status_reference.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/models/status_reference.rb b/app/models/status_reference.rb index 62be6df3b119de..dd912d995c32cb 100644 --- a/app/models/status_reference.rb +++ b/app/models/status_reference.rb @@ -17,15 +17,10 @@ class StatusReference < ApplicationRecord has_one :notification, as: :activity, dependent: :destroy - validate :validate_status_visibilities after_commit :reset_parent_cache private - def validate_status_visibilities - raise Mastodon::ValidationError, I18n.t('status_references.errors.invalid_status_visibilities') if [:public, :public_unlisted, :unlisted, :login].exclude?(target_status.visibility.to_sym) - end - def reset_parent_cache Rails.cache.delete("statuses/#{status_id}") Rails.cache.delete("statuses/#{target_status_id}") From be5072bcf6cf1482c83e3f4691fb44b6ba8a1082 Mon Sep 17 00:00:00 2001 From: KMY Date: Tue, 19 Sep 2023 18:51:36 +0900 Subject: [PATCH 11/11] Fix single quote setting hint message --- app/views/settings/preferences/other/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index acde16d1c6be97..c596013ef6c569 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -15,7 +15,7 @@ = ff.input :lock_follow_from_bot, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_lock_follow_from_bot') .fields-group - = ff.input :single_ref_to_quote, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_single_ref_to_quote'), hint: I18n.t('simple_form.labels.defaults.setting_single_ref_to_quote') + = ff.input :single_ref_to_quote, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_single_ref_to_quote'), hint: I18n.t('simple_form.hints.defaults.setting_single_ref_to_quote') %h4= t 'preferences.posting_defaults'