diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index e7d2008d0..0e5b0f536 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -54,6 +54,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_hide_public_outbox, :setting_max_public_history, :setting_roar_lifespan, + :setting_delayed_roars, :setting_default_privacy, :setting_default_sensitive, diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 50632ec44..801a71bc4 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -38,6 +38,7 @@ class UserSettingsDecorator user.settings['larger_emoji'] = larger_emoji_preference if change?('setting_larger_emoji') user.settings['max_public_history'] = max_public_history_preference if change?('setting_max_public_history') user.settings['roar_lifespan'] = roar_lifespan_preference if change?('setting_roar_lifespan') + user.settings['delayed_roars'] = delayed_roars_preference if change?('setting_delayed_roars') user.settings['notification_emails'] = merged_notification_emails if change?('notification_emails') user.settings['interactions'] = merged_interactions if change?('interactions') @@ -135,6 +136,10 @@ class UserSettingsDecorator settings['setting_roar_lifespan'] end + def delayed_roars_preference + settings['setting_delayed_roars'] + end + def merged_notification_emails user.settings['notification_emails'].merge coerced_settings('notification_emails').to_h end diff --git a/app/models/account.rb b/app/models/account.rb index efa6b8fbd..05fb387aa 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -130,6 +130,7 @@ class Account < ApplicationRecord :always_local_only?, :max_public_history, :roar_lifespan, + :delayed_roars?, :hides_public_profile?, :hides_public_outbox?, diff --git a/app/models/user.rb b/app/models/user.rb index 2f01c2e5a..3ca019cb9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -126,6 +126,7 @@ class User < ApplicationRecord :hide_public_outbox, :max_public_history, :roar_lifespan, + :delayed_roars, :auto_play_gif, :default_sensitive, @@ -304,6 +305,10 @@ class User < ApplicationRecord @_roar_lifespan ||= (settings.roar_lifespan || 0) end + def delayed_roars? + @delayed_roars ||= (settings.delayed_roars || false) + end + def defaults_to_local_only? @defaults_to_local_only ||= (settings.default_local || false) end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 235c156f5..b1db23acd 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -3,10 +3,11 @@ class FanOutOnWriteService < BaseService # Push a status into home and mentions feeds # @param [Status] status - def call(status) + def call(status, delayed = false) raise Mastodon::RaceConditionError if status.visibility.nil? deliver_to_self(status) if status.account.local? + return if delayed render_anonymous_payload(status) diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index b24ca02b3..144cc19c9 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -32,6 +32,7 @@ class PostStatusService < BaseService # @option [String] :delete_after # @option [Boolean] :nocrawl Optional skip link card generation # @option [Boolean] :nomentions Optional skip mention processing + # @option [Boolean] :delayed Optional publishing delay of 30 secs # @option [Hash] :poll Optional poll to attach # @option [Enumerable] :media_ids Optional array of media IDs to attach # @option [Doorkeeper::Application] :application @@ -57,8 +58,23 @@ class PostStatusService < BaseService schedule_status! else return unless process_status! - postprocess_status! - bump_potential_friendship! + if @options[:delayed] || @account&.user&.delayed_roars? + delay_until = Time.now.utc + 30.seconds + opts = { + visibility: @visibility, + federate: @options[:federate], + distribute: @options[:distribute], + nocrawl: @options[:nocrawl], + nomentions: @options[:nomentions], + delete_after: @delete_after.nil? ? nil : @delete_after + 30.seconds, + }.compact + + PostStatusWorker.perform_at(delay_until, @status.id, opts) + DistributionWorker.perform_async(@status.id, delayed = true) unless @options[:distribute] == false + else + postprocess_status! + bump_potential_friendship! + end end redis.setex(idempotency_key, 3_600, @status.id) if idempotency_given? @@ -149,7 +165,7 @@ class PostStatusService < BaseService return false if @status.destroyed? process_hashtags_service.call(@status, @tags, @preloaded_tags) - process_mentions_service.call(@status) unless @options[:nomentions] + process_mentions_service.call(@status) unless @delayed || @options[:nomentions] return true end diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index aeacd2830..f1e9656b6 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -31,6 +31,9 @@ = f.input :setting_always_local, as: :boolean, wrapper: :with_label = f.input :setting_default_sensitive, as: :boolean, wrapper: :with_label + .fields-group + = f.input :setting_delayed_roars, as: :boolean, wrapper: :with_label + %hr#settings_other/ .fields-group diff --git a/app/workers/distribution_worker.rb b/app/workers/distribution_worker.rb index 4e20ef31b..0775e30e6 100644 --- a/app/workers/distribution_worker.rb +++ b/app/workers/distribution_worker.rb @@ -3,10 +3,10 @@ class DistributionWorker include Sidekiq::Worker - def perform(status_id) + def perform(status_id, delayed = false) RedisLock.acquire(redis: Redis.current, key: "distribute:#{status_id}") do |lock| if lock.acquired? - FanOutOnWriteService.new.call(Status.find(status_id)) + FanOutOnWriteService.new.call(Status.find(status_id), delayed) else raise Mastodon::RaceConditionError end diff --git a/app/workers/post_status_worker.rb b/app/workers/post_status_worker.rb new file mode 100644 index 000000000..e9b73b8bc --- /dev/null +++ b/app/workers/post_status_worker.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class PostStatusWorker + include Sidekiq::Worker + + sidekiq_options unique: :until_executed + + def perform(status_id, options = {}) + status = Status.find(status_id) + return false if status.destroyed? + + if options[:visibility] + status.visibility = options[:visibility] + status.save! + end + + process_mentions_service.call(status) unless options[:nomentions] + + LinkCrawlWorker.perform_async(status.id) unless options[:nocrawl] || status.spoiler_text? + DistributionWorker.perform_async(status.id) unless options[:distribute] == false + + unless status.local_only? || options[:distribute] == false || options[:federate] == false + ActivityPub::DistributionWorker.perform_async(status.id) + end + + PollExpirationNotifyWorker.perform_at(status.poll.expires_at, status.poll.id) if status.poll + + status.delete_after = options[:delete_after] if options[:delete_after] + + return true if !status.reply? || status.account.id == status.in_reply_to_account_id + ActivityTracker.increment('activity:interactions') + return if status.account.following?(status.in_reply_to_account_id) + PotentialFriendshipTracker.record(status.account.id, status.in_reply_to_account_id, :reply) + + true + rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid + true + end + + def process_mentions_service + ProcessMentionsService.new + end +end diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index c61b19b52..f9bda46b3 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -118,6 +118,7 @@ en: setting_default_language: Posting language setting_default_privacy: Post privacy setting_roar_lifespan: Auto-delete new roars after + setting_delayed_roars: Delayed publishing of roars for proofreading (30 secs) setting_default_local: Default to Monsterpit-only roars (in Glitch flavour) setting_always_local: Don't send your roars outside Monsterpit setting_rawr_federated: Show raw world timeline (may contain offensive content!)