about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authormultiple creatures <dev@multiple-creature.party>2019-08-07 01:08:07 -0500
committermultiple creatures <dev@multiple-creature.party>2019-08-07 01:08:34 -0500
commitef04f3879ac3bd7ec6dddd6cb843c8cdb79a1175 (patch)
tree719373d32c084e20d878e9de13a034946c5663b3 /app
parenta8475313b8e81f1e91ee446599a9b7b78716f30c (diff)
add option to automatically space out boosts over configurable random intervals
Diffstat (limited to 'app')
-rw-r--r--app/controllers/settings/preferences_controller.rb4
-rw-r--r--app/lib/user_settings_decorator.rb24
-rw-r--r--app/models/concerns/account_associations.rb3
-rw-r--r--app/models/queued_boost.rb17
-rw-r--r--app/models/status.rb2
-rw-r--r--app/models/user.rb26
-rw-r--r--app/services/reblog_service.rb30
-rw-r--r--app/services/remove_status_service.rb6
-rw-r--r--app/views/settings/preferences/show.html.haml12
-rw-r--r--app/workers/reblog_status_worker.rb17
-rw-r--r--app/workers/scheduler/boosts_scheduler.rb49
11 files changed, 173 insertions, 17 deletions
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 2ae90c7fb..483af2c16 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -56,6 +56,10 @@ class Settings::PreferencesController < Settings::BaseController
       :setting_roar_lifespan,
       :setting_delayed_roars,
       :setting_delayed_for,
+      :setting_boost_interval,
+      :setting_boost_random,
+      :setting_boost_interval_from,
+      :setting_boost_interval_to,
       :setting_show_cursor,
 
       :setting_default_privacy,
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index 73831208a..b7e0d577b 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -40,6 +40,10 @@ class UserSettingsDecorator
     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['delayed_for']         = delayed_for_preference if change?('setting_delayed_for')
+    user.settings['boost_interval']      = boost_interval_preference if change?('setting_boost_interval')
+    user.settings['boost_random']        = boost_random_preference if change?('setting_boost_random')
+    user.settings['boost_interval_from'] = boost_interval_from_preference if change?('setting_boost_interval_from')
+    user.settings['boost_interval_to']   = boost_interval_to_preference if change?('setting_boost_interval_to')
     user.settings['show_cursor']         = show_cursor_preference if change?('setting_show_cursor')
 
     user.settings['notification_emails'] = merged_notification_emails if change?('notification_emails')
@@ -150,6 +154,26 @@ class UserSettingsDecorator
     boolean_cast_setting 'setting_delayed_roars'
   end
 
+  def boost_interval_preference
+    boolean_cast_setting 'setting_boost_interval'
+  end
+
+  def boost_random_preference
+    boolean_cast_setting 'setting_boost_random'
+  end
+
+  def boost_interval_from_preference
+    settings['setting_boost_interval_from']
+  end
+
+  def boost_interval_to_preference
+    settings['setting_boost_interval_to']
+  end
+
+  def delayed_for_preference
+    settings['setting_delayed_for']
+  end
+
   def merged_notification_emails
     user.settings['notification_emails'].merge coerced_settings('notification_emails').to_h
   end
diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb
index 0c3725e54..a90104943 100644
--- a/app/models/concerns/account_associations.rb
+++ b/app/models/concerns/account_associations.rb
@@ -58,5 +58,8 @@ module AccountAssociations
     # Hashtags
     has_and_belongs_to_many :tags
     has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
+
+    # queued boosts
+    has_many :queued_boosts, dependent: :destroy, inverse_of: :account
   end
 end
diff --git a/app/models/queued_boost.rb b/app/models/queued_boost.rb
new file mode 100644
index 000000000..b23282697
--- /dev/null
+++ b/app/models/queued_boost.rb
@@ -0,0 +1,17 @@
+# == Schema Information
+#
+# Table name: queued_boosts
+#
+#  id         :bigint(8)        not null, primary key
+#  account_id :bigint(8)
+#  status_id  :bigint(8)
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+
+class QueuedBoost < ApplicationRecord
+  belongs_to :account, inverse_of: :queued_boosts
+  belongs_to :status, inverse_of: :queued_boosts
+
+  validates :account_id, uniqueness: { scope: :status_id }
+end
diff --git a/app/models/status.rb b/app/models/status.rb
index 9f11e6d5d..a2d64ecf0 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -76,6 +76,8 @@ class Status < ApplicationRecord
   has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
   has_many :media_attachments, dependent: :nullify
 
+  has_many :queued_boosts, dependent: :destroy, inverse_of: :status
+
   has_and_belongs_to_many :tags
   has_and_belongs_to_many :preview_cards
 
diff --git a/app/models/user.rb b/app/models/user.rb
index 479392642..a0786aa69 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -131,6 +131,10 @@ class User < ApplicationRecord
     :roar_lifespan,
     :delayed_roars,
     :delayed_for,
+    :boost_interval,
+    :boost_random,
+    :boost_interval_from,
+    :boost_interval_to,
     :show_cursor,
 
     :auto_play_gif,
@@ -303,11 +307,11 @@ class User < ApplicationRecord
   end
 
   def max_public_history
-    @_max_public_history ||= (settings.max_public_history || 6)
+    @_max_public_history ||= [1, (settings.max_public_history || 6).to_i].max
   end
 
   def roar_lifespan
-    @_roar_lifespan ||= (settings.roar_lifespan || 0)
+    @_roar_lifespan ||= [0, (settings.roar_lifespan || 0).to_i].max
   end
 
   def delayed_roars?
@@ -315,7 +319,23 @@ class User < ApplicationRecord
   end
 
   def delayed_for
-    @_delayed_for ||= (settings.delayed_for || 60)
+    @_delayed_for ||= [5, (settings.delayed_for || 60).to_i].max
+  end
+
+  def boost_interval?
+    @boost_interval ||= (settings.boost_interval || false)
+  end
+
+  def boost_random?
+    @boost_random ||= (settings.boost_random || false)
+  end
+
+  def boost_interval_from
+    @boost_interval_from ||= [1, (settings.boost_interval_from || 1).to_i].max
+  end
+
+  def boost_interval_to
+    @boost_interval_to ||= [2, (settings.boost_interval_to || 15).to_i].max
   end
 
   def shows_cursor?
diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb
index 9ac38ac9d..7d72357f9 100644
--- a/app/services/reblog_service.rb
+++ b/app/services/reblog_service.rb
@@ -14,22 +14,30 @@ class ReblogService < BaseService
     authorize_with account, reblogged_status, :reblog?
 
     reblog = account.statuses.find_by(reblog: reblogged_status)
+    new_reblog = reblog.nil?
 
-    return reblog unless reblog.nil?
+    if new_reblog
+      visibility = options[:visibility] || account.user&.setting_default_privacy
+      visibility = reblogged_status.visibility if reblogged_status.hidden?
+      reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility)
+    end
 
-    visibility = options[:visibility] || account.user&.setting_default_privacy
-    visibility = reblogged_status.visibility if reblogged_status.hidden?
-    reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility)
+    if !options[:distribute] && account&.user&.boost_interval?
+      QueuedBoost.find_or_create_by!(account_id: account.id, status_id: reblogged_status.id) if account&.user&.boost_interval?
+    elsif !options[:nodistribute]
+      return reblog unless options[:distribute] || new_reblog
 
-    DistributionWorker.perform_async(reblog.id)
+      DistributionWorker.perform_async(reblog.id)
 
-    unless reblogged_status.local_only?
-      ActivityPub::DistributionWorker.perform_async(reblog.id)
-    end
+      unless reblogged_status.local_only?
+        ActivityPub::DistributionWorker.perform_async(reblog.id)
+      end
 
-    curate_status(reblogged_status)
-    create_notification(reblog) unless options[:skip_notify]
-    bump_potential_friendship(account, reblog)
+      curate_status(reblogged_status)
+
+      create_notification(reblog) unless options[:skip_notify]
+      bump_potential_friendship(account, reblog)
+    end
 
     reblog
   end
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index d55ecadef..f9f20aae5 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -17,6 +17,7 @@ class RemoveStatusService < BaseService
 
     RedisLock.acquire(lock_options) do |lock|
       if lock.acquired?
+        remove_from_queued
         remove_from_self if status.account.local?
         remove_from_followers
         remove_from_lists
@@ -46,6 +47,11 @@ class RemoveStatusService < BaseService
 
   private
 
+  def remove_from_queued
+    QueuedBoost.where(account_id: @account.id, status_id: @status.proper.id).destroy_all
+    QueuedBoost.where(status_id: @status.id).destroy_all
+  end
+
   def remove_from_self
     FeedManager.instance.unpush_from_home(@account, @status)
   end
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index 07c9fd86f..e6198de6a 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -35,7 +35,13 @@
 
   .fields-group
     = f.input :setting_delayed_roars, as: :boolean, wrapper: :with_label
-    = f.input :setting_delayed_for, collection: [5, 10, 15, 30, 60, 120, 180, 300, 360, 600, 1800, 3600], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.delayed_for.#{item}")]) }, selected: [5, current_user.delayed_for.to_i].max
+    = f.input :setting_delayed_for, collection: [5, 10, 15, 30, 60, 120, 180, 300, 360, 600, 1800, 3600], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.delayed_for.#{item}")]) }, selected: current_user.delayed_for
+
+  .fields-group
+    = f.input :setting_boost_interval, as: :boolean, wrapper: :with_label
+    = f.input :setting_boost_random, as: :boolean, wrapper: :with_label
+    = f.input :setting_boost_interval_from, collection: [1, 2, 3, 4, 5, 6, 10, 15, 30, 60, 120, 180, 300, 360, 720, 1440], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.boost_interval.#{item}")]) }, selected: current_user.boost_interval_from
+    = f.input :setting_boost_interval_to, collection: [1, 2, 3, 4, 5, 6, 10, 15, 30, 60, 120, 180, 300, 360, 720, 1440], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.boost_interval.#{item}")]) }, selected: current_user.boost_interval_to
 
   %hr#settings_other/
 
@@ -54,8 +60,8 @@
   %hr/
 
   .fields-group
-    = f.input :setting_max_public_history, collection: [1, 3, 6, 7, 14, 30, 60, 90, 180, 365, 730, 1095, 2190], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.lifespan.#{item}")]) }, selected: current_user.max_public_history.to_i
-    = f.input :setting_roar_lifespan, collection: [0, 1, 3, 6, 7, 14, 30, 60, 90, 180, 365, 730, 1095, 2190], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.lifespan.#{item}")]) }, selected: current_user.roar_lifespan.to_i
+    = f.input :setting_max_public_history, collection: [1, 3, 6, 7, 14, 30, 60, 90, 180, 365, 730, 1095, 2190], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.lifespan.#{item}")]) }, selected: current_user.max_public_history
+    = f.input :setting_roar_lifespan, collection: [0, 1, 3, 6, 7, 14, 30, 60, 90, 180, 365, 730, 1095, 2190], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.lifespan.#{item}")]) }, selected: current_user.roar_lifespan
     = f.input :setting_hide_public_profile, as: :boolean, wrapper: :with_label
     = f.input :setting_hide_public_outbox, as: :boolean, wrapper: :with_label
 
diff --git a/app/workers/reblog_status_worker.rb b/app/workers/reblog_status_worker.rb
new file mode 100644
index 000000000..c0b2153b2
--- /dev/null
+++ b/app/workers/reblog_status_worker.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class ReblogStatusWorker
+  include Sidekiq::Worker
+
+  sidekiq_options unique: :until_executed
+
+  def perform(account_id, status_id, reblog_params = {})
+    account = Account.find(account_id)
+    status = Status.find(status_id)
+    return false if status.destroyed?
+    ReblogService.new.call(account, status, reblog_params.symbolize_keys)
+    true
+  rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
+    true
+  end
+end
diff --git a/app/workers/scheduler/boosts_scheduler.rb b/app/workers/scheduler/boosts_scheduler.rb
new file mode 100644
index 000000000..de0d89992
--- /dev/null
+++ b/app/workers/scheduler/boosts_scheduler.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+class Scheduler::BoostsScheduler
+  include Sidekiq::Worker
+  include Redisable
+
+  sidekiq_options unique: :until_executed, retry: 0
+
+  def perform
+    process_queued_boosts!
+  end
+
+  private
+
+  def process_queued_boosts!
+    queued_accounts.find_each do |account|
+      next if redis.exists("queued_boost:#{account.id}") || account&.user.nil?
+
+      q = next_boost(account.id, account.user.boost_random?)
+      next if q.empty?
+
+      from_interval = account.user.boost_interval_from
+      to_interval = account.user.boost_interval_to
+
+      if from_interval > to_interval
+        from_interval, to_interval = [to_interval, from_interval]
+      end
+
+      interval = rand(from_interval .. to_interval).minutes
+
+      redis.setex("queued_boost:#{account.id}", interval, 1)
+      ReblogStatusWorker.perform_async(account.id, q.first.status_id, distribute: true)
+      q.destroy_all
+    end
+  end
+
+  def queued_accounts
+    Account.where(id: queued_account_ids)
+  end
+
+  def queued_account_ids
+    QueuedBoost.distinct.pluck(:account_id)
+  end
+
+  def next_boost(account_id, boost_random = false)
+    q = QueuedBoost.where(account_id: account_id)
+    (boost_random ? q.order(Arel.sql('RANDOM()')) : q.order(:id)).limit(1)
+  end
+end