From d9113976c8287c11e8762aee87f55118babfda2a Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 9 Oct 2025 04:08:29 -0400 Subject: [PATCH 1/4] Use tag filter for pending tag count on admin dashboard (#36404) --- app/controllers/admin/dashboard_controller.rb | 8 +++++++- spec/system/admin/dashboard_spec.rb | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 5b0867dcfb..fe314daeca 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -9,10 +9,16 @@ module Admin @pending_appeals_count = Appeal.pending.async_count @pending_reports_count = Report.unresolved.async_count - @pending_tags_count = Tag.pending_review.async_count + @pending_tags_count = pending_tags.async_count @pending_users_count = User.pending.async_count @system_checks = Admin::SystemCheck.perform(current_user) @time_period = (29.days.ago.to_date...Time.now.utc.to_date) end + + private + + def pending_tags + ::Trends::TagFilter.new(status: :pending_review).results + end end end diff --git a/spec/system/admin/dashboard_spec.rb b/spec/system/admin/dashboard_spec.rb index 06d31cde44..d0cedd2ed1 100644 --- a/spec/system/admin/dashboard_spec.rb +++ b/spec/system/admin/dashboard_spec.rb @@ -9,6 +9,7 @@ RSpec.describe 'Admin Dashboard' do before do stub_system_checks Fabricate :software_update + Fabricate :tag, requested_review_at: 5.minutes.ago sign_in(user) end @@ -18,6 +19,7 @@ RSpec.describe 'Admin Dashboard' do expect(page) .to have_title(I18n.t('admin.dashboard.title')) .and have_content(I18n.t('admin.system_checks.software_version_patch_check.message_html')) + .and have_content('0 pending hashtags') end private From 67575e59e664b0ed63e78d05ee52fabc6b917b4d Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 9 Oct 2025 11:53:45 +0200 Subject: [PATCH 2/4] Fix quote post state sometimes not being updated through streaming server (#36408) --- app/services/activitypub/process_status_update_service.rb | 2 ++ app/workers/activitypub/refetch_and_verify_quote_worker.rb | 1 + 2 files changed, 3 insertions(+) diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index fcc2963e33..b186719c94 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -74,6 +74,8 @@ class ActivityPub::ProcessStatusUpdateService < BaseService update_quote_approval! update_counts! end + + broadcast_updates! if @status.quote&.state_previously_changed? end def update_interaction_policies! diff --git a/app/workers/activitypub/refetch_and_verify_quote_worker.rb b/app/workers/activitypub/refetch_and_verify_quote_worker.rb index 0c7ecd9b2a..e2df023103 100644 --- a/app/workers/activitypub/refetch_and_verify_quote_worker.rb +++ b/app/workers/activitypub/refetch_and_verify_quote_worker.rb @@ -10,6 +10,7 @@ class ActivityPub::RefetchAndVerifyQuoteWorker def perform(quote_id, quoted_uri, options = {}) quote = Quote.find(quote_id) ActivityPub::VerifyQuoteService.new.call(quote, fetchable_quoted_uri: quoted_uri, request_id: options[:request_id]) + ::DistributionWorker.perform_async(quote.status_id, { 'update' => true }) if quote.state_previously_changed? rescue ActiveRecord::RecordNotFound # Do nothing true From a186bad399fa7a3ad8347defc2d7ae688bc10d73 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 7 Oct 2025 15:14:38 +0200 Subject: [PATCH 3/4] Fix `"quote": { "type": "Tombstone" }` being ignored --- app/lib/activitypub/activity/create.rb | 2 +- app/lib/activitypub/parser/status_parser.rb | 4 ++ .../process_status_update_service.rb | 2 +- spec/lib/activitypub/activity/create_spec.rb | 24 ++++++++++++ .../process_status_update_service_spec.rb | 38 +++++++++++++++++++ 5 files changed, 68 insertions(+), 2 deletions(-) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index c247e70259..10e3ecc577 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -217,7 +217,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def process_quote @quote_uri = @status_parser.quote_uri - return if @quote_uri.blank? + return unless @status_parser.quote? approval_uri = @status_parser.quote_approval_uri approval_uri = nil if unsupported_uri_scheme?(approval_uri) diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb index fecfd0e34e..9db1cb572c 100644 --- a/app/lib/activitypub/parser/status_parser.rb +++ b/app/lib/activitypub/parser/status_parser.rb @@ -117,6 +117,10 @@ class ActivityPub::Parser::StatusParser flags end + def quote? + %w(quote _misskey_quote quoteUrl quoteUri).any? { |key| @object[key].present? } + end + def quote_uri %w(quote _misskey_quote quoteUrl quoteUri).filter_map do |key| value_or_id(as_array(@object[key]).first) diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index b186719c94..d29e08cdd6 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -300,7 +300,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService def update_quote! quote_uri = @status_parser.quote_uri - if quote_uri.present? + if @status_parser.quote? approval_uri = @status_parser.quote_approval_uri approval_uri = nil if unsupported_uri_scheme?(approval_uri) diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 7ce9ee5775..1fa04dec09 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -938,6 +938,30 @@ RSpec.describe ActivityPub::Activity::Create do end end + context 'with an unverifiable quote of a dead post' do + let(:quoted_status) { Fabricate(:status) } + + let(:object_json) do + build_object( + type: 'Note', + content: 'woah what she said is amazing', + quote: { type: 'Tombstone' } + ) + end + + it 'creates a status with an unverified quote' do + expect { subject.perform }.to change(sender.statuses, :count).by(1) + + status = sender.statuses.first + expect(status).to_not be_nil + expect(status.quote).to_not be_nil + expect(status.quote).to have_attributes( + state: 'pending', + approval_uri: nil + ) + end + end + context 'with an unverifiable unknown post' do let(:unknown_post_uri) { 'https://unavailable.example.com/unavailable-post' } diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index f99f2ecb33..723f1343bb 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -976,6 +976,44 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do end end + context 'when the status swaps a verified quote with an ID-less Tombstone through an explicit update' do + let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let(:second_quoted_status) { Fabricate(:status, account: quoted_account) } + let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri, state: :accepted) } + let(:approval_uri) { 'https://quoted.example.com/approvals/1' } + + let(:payload) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + '@id': 'https://w3id.org/fep/044f#quote', + '@type': '@id', + }, + { + '@id': 'https://w3id.org/fep/044f#quoteAuthorization', + '@type': '@id', + }, + ], + id: 'foo', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + quote: { type: 'Tombstone' }, + } + end + + it 'updates the URI and unverifies the quote' do + expect { subject.call(status, json, json) } + .to change { status.quote.quoted_status }.from(quoted_status).to(nil) + .and change { status.quote.state }.from('accepted') + + expect { quote.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + context 'when the status swaps a verified quote with another verifiable quote through an explicit update' do let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } let(:second_quoted_account) { Fabricate(:account, domain: 'second-quoted.example.com') } From d7d6407d4196ab6783485b20a3d9e2fbfc00ef01 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 9 Oct 2025 15:55:49 +0200 Subject: [PATCH 4/4] Explicitly record Tombstone quotes as deleted This adds a `deleted` state to the internal representation, but this does not change the API, which already included such a state. --- app/lib/activitypub/activity/create.rb | 4 ++-- app/lib/activitypub/parser/status_parser.rb | 4 ++++ app/models/quote.rb | 2 +- app/services/activitypub/process_status_update_service.rb | 4 +++- spec/lib/activitypub/activity/create_spec.rb | 2 +- .../activitypub/process_status_update_service_spec.rb | 2 +- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 10e3ecc577..f0aa060312 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -220,8 +220,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity return unless @status_parser.quote? approval_uri = @status_parser.quote_approval_uri - approval_uri = nil if unsupported_uri_scheme?(approval_uri) - @quote = Quote.new(account: @account, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?) + approval_uri = nil if unsupported_uri_scheme?(approval_uri) || TagManager.instance.local_url?(approval_uri) + @quote = Quote.new(account: @account, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?, state: @status_parser.deleted_quote? ? :deleted : :pending) end def process_hashtag(tag) diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb index 9db1cb572c..535d5054b7 100644 --- a/app/lib/activitypub/parser/status_parser.rb +++ b/app/lib/activitypub/parser/status_parser.rb @@ -121,6 +121,10 @@ class ActivityPub::Parser::StatusParser %w(quote _misskey_quote quoteUrl quoteUri).any? { |key| @object[key].present? } end + def deleted_quote? + @object['quote'].is_a?(Hash) && @object['quote']['type'] == 'Tombstone' + end + def quote_uri %w(quote _misskey_quote quoteUrl quoteUri).filter_map do |key| value_or_id(as_array(@object[key]).first) diff --git a/app/models/quote.rb b/app/models/quote.rb index 89845ed9f4..4b1072d6cb 100644 --- a/app/models/quote.rb +++ b/app/models/quote.rb @@ -21,7 +21,7 @@ class Quote < ApplicationRecord REFRESH_DEADLINE = 6.hours enum :state, - { pending: 0, accepted: 1, rejected: 2, revoked: 3 }, + { pending: 0, accepted: 1, rejected: 2, revoked: 3, deleted: 4 }, validate: true belongs_to :status diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index d29e08cdd6..9f4d4de558 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -305,10 +305,12 @@ class ActivityPub::ProcessStatusUpdateService < BaseService approval_uri = nil if unsupported_uri_scheme?(approval_uri) if @status.quote.present? + state = @status_parser.deleted_quote? ? :deleted : :pending + # If the quoted post has changed, discard the old object and create a new one if @status.quote.quoted_status.present? && ActivityPub::TagManager.instance.uri_for(@status.quote.quoted_status) != quote_uri @status.quote.destroy - quote = Quote.create(status: @status, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?) + quote = Quote.create(status: @status, approval_uri: approval_uri, legacy: @status_parser.legacy_quote?, state: state) @quote_changed = true else quote = @status.quote diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 1fa04dec09..12fbdba802 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -956,7 +956,7 @@ RSpec.describe ActivityPub::Activity::Create do expect(status).to_not be_nil expect(status.quote).to_not be_nil expect(status.quote).to have_attributes( - state: 'pending', + state: 'deleted', approval_uri: nil ) end diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 723f1343bb..43e6f682c0 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -1008,7 +1008,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do it 'updates the URI and unverifies the quote' do expect { subject.call(status, json, json) } .to change { status.quote.quoted_status }.from(quoted_status).to(nil) - .and change { status.quote.state }.from('accepted') + .and change { status.quote.state }.from('accepted').to('deleted') expect { quote.reload }.to raise_error(ActiveRecord::RecordNotFound) end