diff --git a/app/lib/status_reach_finder.rb b/app/lib/status_reach_finder.rb new file mode 100644 index 000000000..35b191dad --- /dev/null +++ b/app/lib/status_reach_finder.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class StatusReachFinder + def initialize(status) + @status = status + end + + def inboxes + Account.where(id: reached_account_ids).inboxes + end + + private + + def reached_account_ids + [ + replied_to_account_id, + reblog_of_account_id, + mentioned_account_ids, + reblogs_account_ids, + favourites_account_ids, + replies_account_ids, + ].tap do |arr| + arr.flatten! + arr.compact! + arr.uniq! + end + end + + def replied_to_account_id + @status.in_reply_to_account_id + end + + def reblog_of_account_id + @status.reblog.account_id if @status.reblog? + end + + def mentioned_account_ids + @status.mentions.pluck(:account_id) + end + + def reblogs_account_ids + @status.reblogs.pluck(:account_id) + end + + def favourites_account_ids + @status.favourites.pluck(:account_id) + end + + def replies_account_ids + @status.replies.pluck(:account_id) + end +end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 4f0edc3cf..d6043fb5d 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -9,44 +9,47 @@ class RemoveStatusService < BaseService # @param [Hash] options # @option [Boolean] :redraft # @option [Boolean] :immediate - # @option [Boolean] :original_removed + # @option [Boolean] :original_removed def call(status, **options) @payload = Oj.dump(event: :delete, payload: status.id.to_s) @status = status @account = status.account - @tags = status.tags.pluck(:name).to_a - @mentions = status.active_mentions.includes(:account).to_a - @reblogs = status.reblogs.includes(:account).to_a @options = options RedisLock.acquire(lock_options) do |lock| if lock.acquired? - remove_from_self if status.account.local? + remove_from_self if @account.local? remove_from_followers remove_from_lists - remove_from_affected - remove_reblogs - remove_from_hashtags - remove_from_public - remove_from_media if status.media_attachments.any? - remove_from_spam_check - remove_media + + # There is no reason to send out Undo activities when the + # cause is that the original object has been removed, since + # original object being removed implicitly removes reblogs + # of it. The Delete activity of the original is forwarded + # separately. + if @account.local? && !@options[:original_removed] + remove_from_remote_followers + remove_from_remote_reach + end + + # Since reblogs don't mention anyone, don't get reblogged, + # favourited and don't contain their own media attachments + # or hashtags, this can be skipped + unless @status.reblog? + remove_from_mentions + remove_reblogs + remove_from_hashtags + remove_from_public + remove_from_media if @status.media_attachments.any? + remove_from_spam_check + remove_media + end @status.destroy! if @options[:immediate] || !@status.reported? else raise Mastodon::RaceConditionError end end - - # There is no reason to send out Undo activities when the - # cause is that the original object has been removed, since - # original object being removed implicitly removes reblogs - # of it. The Delete activity of the original is forwarded - # separately. - return if !@account.local? || @options[:original_removed] - - remove_from_remote_followers - remove_from_remote_affected end private @@ -67,31 +70,35 @@ class RemoveStatusService < BaseService end end - def remove_from_affected - @mentions.map(&:account).select(&:local?).each do |account| - redis.publish("timeline:#{account.id}", @payload) + def remove_from_mentions + # For limited visibility statuses, the mentions that determine + # who receives them in their home feed are a subset of followers + # and therefore the delete is already handled by sending it to all + # followers. Here we send a delete to actively mentioned accounts + # that may not follow the account + + @status.active_mentions.find_each do |mention| + redis.publish("timeline:#{mention.account_id}", @payload) end end - def remove_from_remote_affected + def remove_from_remote_reach + return if @status.reblog? + # People who got mentioned in the status, or who # reblogged it from someone else might not follow # the author and wouldn't normally receive the # delete notification - so here, we explicitly # send it to them - target_accounts = (@mentions.map(&:account).reject(&:local?) + @reblogs.map(&:account).reject(&:local?)) - target_accounts << @status.reblog.account if @status.reblog? && !@status.reblog.account.local? - target_accounts.uniq!(&:id) + status_reach_finder = StatusReachFinder.new(@status) - # ActivityPub - ActivityPub::DeliveryWorker.push_bulk(target_accounts.select(&:activitypub?).uniq(&:preferred_inbox_url)) do |target_account| - [signed_activity_json, @account.id, target_account.preferred_inbox_url] + ActivityPub::DeliveryWorker.push_bulk(status_reach_finder.inboxes) do |inbox_url| + [signed_activity_json, @account.id, inbox_url] end end def remove_from_remote_followers - # ActivityPub ActivityPub::DeliveryWorker.push_bulk(@account.followers.inboxes) do |inbox_url| [signed_activity_json, @account.id, inbox_url] end @@ -118,19 +125,19 @@ class RemoveStatusService < BaseService # because once original status is gone, reblogs will disappear # without us being able to do all the fancy stuff - @reblogs.each do |reblog| + @status.reblogs.includes(:account).find_each do |reblog| RemoveStatusService.new.call(reblog, original_removed: true) end end def remove_from_hashtags - @account.featured_tags.where(tag_id: @status.tags.pluck(:id)).each do |featured_tag| + @account.featured_tags.where(tag_id: @status.tags.map(&:id)).each do |featured_tag| featured_tag.decrement(@status.id) end return unless @status.public_visibility? - @tags.each do |hashtag| + @status.tags.map(&:name).each do |hashtag| redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload) redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if @status.local? end @@ -140,22 +147,14 @@ class RemoveStatusService < BaseService return unless @status.public_visibility? redis.publish('timeline:public', @payload) - if @status.local? - redis.publish('timeline:public:local', @payload) - else - redis.publish('timeline:public:remote', @payload) - end + redis.publish(@status.local? ? 'timeline:public:local' : 'timeline:public:remote', @payload) end def remove_from_media return unless @status.public_visibility? redis.publish('timeline:public:media', @payload) - if @status.local? - redis.publish('timeline:public:local:media', @payload) - else - redis.publish('timeline:public:remote:media', @payload) - end + redis.publish(@status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', @payload) end def remove_media