add option to automatically space out boosts over configurable random intervals
parent
a8475313b8
commit
ef04f3879a
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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?
|
||||
|
||||
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)
|
||||
|
||||
DistributionWorker.perform_async(reblog.id)
|
||||
|
||||
unless reblogged_status.local_only?
|
||||
ActivityPub::DistributionWorker.perform_async(reblog.id)
|
||||
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
|
||||
|
||||
curate_status(reblogged_status)
|
||||
create_notification(reblog) unless options[:skip_notify]
|
||||
bump_potential_friendship(account, reblog)
|
||||
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)
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
reblog
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -127,6 +127,10 @@ en:
|
|||
setting_roar_lifespan: Auto-delete new roars after
|
||||
setting_delayed_roars: Delayed publishing of roars for proofreading
|
||||
setting_delayed_for: Delay for
|
||||
setting_boost_interval: Automatically space out consecutive boosts
|
||||
setting_boost_random: Boost in random order
|
||||
setting_boost_interval_from: Minimum boost interval
|
||||
setting_boost_interval_to: Maximum boost interval
|
||||
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!)
|
||||
|
@ -176,6 +180,23 @@ en:
|
|||
username: Username
|
||||
username_or_email: Username or Email
|
||||
whole_word: Whole word
|
||||
boost_interval:
|
||||
1: 1 minute
|
||||
2: 2 minutes
|
||||
3: 3 minutes
|
||||
4: 4 minutes
|
||||
5: 5 minutes
|
||||
6: 6 minutes
|
||||
10: 10 minutes
|
||||
15: 15 minutes
|
||||
30: 30 minutes
|
||||
60: 1 hour
|
||||
120: 2 hours
|
||||
180: 3 hours
|
||||
300: 5 hours
|
||||
360: 6 hours
|
||||
720: 12 hours
|
||||
1440: 1 day
|
||||
delayed_for:
|
||||
5: 5 seconds
|
||||
10: 10 seconds
|
||||
|
|
|
@ -12,6 +12,9 @@
|
|||
destructing_statuses_scheduler:
|
||||
every: '1m'
|
||||
class: Scheduler::DestructingStatusesScheduler
|
||||
boosts_scheduler:
|
||||
every: '1m'
|
||||
class: Scheduler::BoostsScheduler
|
||||
janitor_scheduler:
|
||||
every: '1h'
|
||||
class: Scheduler::JanitorScheduler
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
class CreateQueuedBoosts < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :queued_boosts do |t|
|
||||
t.references :account, foreign_key: { on_delete: :cascade }
|
||||
t.references :status, foreign_key: { on_delete: :cascade }
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :queued_boosts, [:account_id, :status_id], unique: true
|
||||
end
|
||||
end
|
14
db/schema.rb
14
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2019_08_05_203816) do
|
||||
ActiveRecord::Schema.define(version: 2019_08_06_195913) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -554,6 +554,16 @@ ActiveRecord::Schema.define(version: 2019_08_05_203816) do
|
|||
t.index ["status_id", "preview_card_id"], name: "index_preview_cards_statuses_on_status_id_and_preview_card_id"
|
||||
end
|
||||
|
||||
create_table "queued_boosts", force: :cascade do |t|
|
||||
t.bigint "account_id"
|
||||
t.bigint "status_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id", "status_id"], name: "index_queued_boosts_on_account_id_and_status_id", unique: true
|
||||
t.index ["account_id"], name: "index_queued_boosts_on_account_id"
|
||||
t.index ["status_id"], name: "index_queued_boosts_on_status_id"
|
||||
end
|
||||
|
||||
create_table "relays", force: :cascade do |t|
|
||||
t.string "inbox_url", default: "", null: false
|
||||
t.string "follow_activity_id"
|
||||
|
@ -861,6 +871,8 @@ ActiveRecord::Schema.define(version: 2019_08_05_203816) do
|
|||
add_foreign_key "poll_votes", "polls", on_delete: :cascade
|
||||
add_foreign_key "polls", "accounts", on_delete: :cascade
|
||||
add_foreign_key "polls", "statuses", on_delete: :cascade
|
||||
add_foreign_key "queued_boosts", "accounts", on_delete: :cascade
|
||||
add_foreign_key "queued_boosts", "statuses", on_delete: :cascade
|
||||
add_foreign_key "report_notes", "accounts", on_delete: :cascade
|
||||
add_foreign_key "report_notes", "reports", on_delete: :cascade
|
||||
add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", name: "fk_bca45b75fd", on_delete: :nullify
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Fabricator(:queued_boost) do
|
||||
account nil
|
||||
status nil
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe QueuedBoost, type: :model do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
Loading…
Reference in New Issue