From d018d9db578147fd0807de77bddf42bb076d4ed0 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 5 Mar 2026 15:53:00 +0100 Subject: [PATCH] Fix poll expiration notification being re-triggered on implicit updates (#38078) --- .../process_status_update_service.rb | 5 +++ .../process_status_update_service_spec.rb | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index 23a2305a1a..4bff0d3e22 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -411,6 +411,11 @@ class ActivityPub::ProcessStatusUpdateService < BaseService return unless poll.present? && poll.expires_at.present? && poll.votes.exists? + # If the poll had previously expired, notifications should have already been sent out (or scheduled), + # and re-scheduling them would cause duplicate notifications for people who had already dismissed them + # (see #37948) + return if @previous_expires_at&.past? + PollExpirationNotifyWorker.remove_from_scheduled(poll.id) if @previous_expires_at.present? && @previous_expires_at > poll.expires_at PollExpirationNotifyWorker.perform_at(poll.expires_at + 5.minutes, poll.id) end diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 59f712b9b8..b099b69c88 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -136,6 +136,48 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do end end + context 'with an implicit update of a poll that has already expired' do + let(:account) { Fabricate(:account, domain: 'example.com') } + let!(:expiration) { 10.days.ago.utc } + let!(:status) do + Fabricate(:status, + text: 'Hello world', + account: account, + poll_attributes: { + options: %w(Foo Bar), + account: account, + multiple: false, + hide_totals: false, + expires_at: expiration, + }) + end + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://example.com/foo', + type: 'Question', + content: 'Hello world', + endTime: expiration.iso8601, + oneOf: [ + poll_option_json('Foo', 4), + poll_option_json('Bar', 3), + ], + } + end + + before do + travel_to(expiration - 1.day) do + Fabricate(:poll_vote, poll: status.poll) + end + end + + it 'does not re-trigger notifications' do + expect { subject.call(status, json, json) } + .to_not enqueue_sidekiq_job(PollExpirationNotifyWorker) + end + end + context 'when the status changes a poll despite being not explicitly marked as updated' do let(:account) { Fabricate(:account, domain: 'example.com') } let!(:expiration) { 10.days.from_now.utc }