add option to automatically space out boosts over configurable random intervals

staging
multiple creatures 2019-08-07 01:08:07 -05:00
parent a8475313b8
commit ef04f3879a
17 changed files with 232 additions and 20 deletions

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
Fabricator(:queued_boost) do
account nil
status nil
end

View File

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe QueuedBoost, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end