Merge branch 'chinwag-next'
This commit is contained in:
commit
095ce4fb34
2690 changed files with 96825 additions and 58372 deletions
|
|
@ -1,6 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
require_relative 'base'
|
||||
|
||||
module Mastodon::CLI
|
||||
|
|
@ -80,7 +79,14 @@ module Mastodon::CLI
|
|||
|
||||
account = Account.new(username: username)
|
||||
password = SecureRandom.hex
|
||||
user = User.new(email: options[:email], password: password, agreement: true, role_id: role_id, confirmed_at: options[:confirmed] ? Time.now.utc : nil, bypass_invite_request_check: true)
|
||||
user = User.new(
|
||||
email: options[:email],
|
||||
password: password,
|
||||
agreement: true,
|
||||
role_id: role_id,
|
||||
confirmed_at: options[:confirmed] ? Time.now.utc : nil,
|
||||
bypass_registration_checks: true
|
||||
)
|
||||
|
||||
if options[:reattach]
|
||||
account = Account.find_local(username) || Account.new(username: username)
|
||||
|
|
@ -165,7 +171,7 @@ module Mastodon::CLI
|
|||
user.disabled = false if options[:enable]
|
||||
user.disabled = true if options[:disable]
|
||||
user.approved = true if options[:approve]
|
||||
user.otp_required_for_login = false if options[:disable_2fa]
|
||||
user.disable_two_factor! if options[:disable_2fa]
|
||||
|
||||
if user.save
|
||||
user.confirm if options[:confirm]
|
||||
|
|
@ -305,7 +311,7 @@ module Mastodon::CLI
|
|||
|
||||
begin
|
||||
code = Request.new(:head, account.uri).perform(&:code)
|
||||
rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Mastodon::PrivateNetworkAddressError
|
||||
rescue *Mastodon::HTTP_CONNECTION_ERRORS, Mastodon::PrivateNetworkAddressError
|
||||
skip_domains << account.domain
|
||||
end
|
||||
|
||||
|
|
@ -322,7 +328,9 @@ module Mastodon::CLI
|
|||
|
||||
unless skip_domains.empty?
|
||||
say('The following domains were not available during the check:', :yellow)
|
||||
skip_domains.each { |domain| say(" #{domain}") }
|
||||
shell.indent(2) do
|
||||
skip_domains.each { |domain| say(domain) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ module Mastodon::CLI
|
|||
account.account_stat.tap do |account_stat|
|
||||
account_stat.following_count = account.active_relationships.count
|
||||
account_stat.followers_count = account.passive_relationships.count
|
||||
account_stat.statuses_count = account.statuses.where.not(visibility: :direct).count
|
||||
account_stat.statuses_count = account.statuses.not_direct_visibility.count
|
||||
|
||||
account_stat.save if account_stat.changed?
|
||||
end
|
||||
|
|
@ -60,7 +60,7 @@ module Mastodon::CLI
|
|||
|
||||
def recount_status_stats(status)
|
||||
status.status_stat.tap do |status_stat|
|
||||
status_stat.replies_count = status.replies.where.not(visibility: :direct).count
|
||||
status_stat.replies_count = status.replies.not_direct_visibility.count
|
||||
status_stat.reblogs_count = status.reblogs.count
|
||||
status_stat.favourites_count = status.favourites.count
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ module Mastodon::CLI
|
|||
class EmailDomainBlocks < Base
|
||||
desc 'list', 'List blocked e-mail domains'
|
||||
def list
|
||||
EmailDomainBlock.where(parent_id: nil).find_each do |entry|
|
||||
say(entry.domain.to_s, :white)
|
||||
EmailDomainBlock.parents.find_each do |parent|
|
||||
say(parent.domain.to_s, :white)
|
||||
|
||||
EmailDomainBlock.where(parent_id: entry.id).find_each do |child|
|
||||
say(" #{child.domain}", :cyan)
|
||||
shell.indent do
|
||||
EmailDomainBlock.where(parent_id: parent.id).find_each do |child|
|
||||
say(child.domain, :cyan)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -43,12 +45,7 @@ module Mastodon::CLI
|
|||
end
|
||||
|
||||
other_domains = []
|
||||
if options[:with_dns_records]
|
||||
Resolv::DNS.open do |dns|
|
||||
dns.timeouts = 5
|
||||
other_domains = dns.getresources(domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }.compact_blank
|
||||
end
|
||||
end
|
||||
other_domains = DomainResource.new(domain).mx if options[:with_dns_records]
|
||||
|
||||
email_domain_block = EmailDomainBlock.new(domain: domain, other_domains: other_domains)
|
||||
email_domain_block.save!
|
||||
|
|
|
|||
|
|
@ -62,7 +62,9 @@ module Mastodon::CLI
|
|||
failed += 1
|
||||
say('Failure/Error: ', :red)
|
||||
say(entry.full_name)
|
||||
say(" #{custom_emoji.errors[:image].join(', ')}", :red)
|
||||
shell.indent(2) do
|
||||
say(custom_emoji.errors[:image].join(', '), :red)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ module Mastodon::CLI
|
|||
def self_destruct_value
|
||||
Rails
|
||||
.application
|
||||
.message_verifier('self-destruct')
|
||||
.message_verifier(SelfDestructHelper::VERIFY_PURPOSE)
|
||||
.generate(Rails.configuration.x.local_domain)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,17 +11,21 @@ module Mastodon::CLI
|
|||
option :concurrency, type: :numeric, default: 5, aliases: [:c]
|
||||
option :verbose, type: :boolean, aliases: [:v]
|
||||
option :dry_run, type: :boolean, default: false
|
||||
option :skip_filled_timelines
|
||||
desc 'build [USERNAME]', 'Build home and list feeds for one or all users'
|
||||
long_desc <<-LONG_DESC
|
||||
Build home and list feeds that are stored in Redis from the database.
|
||||
|
||||
With the --skip-filled-timelines, timelines which contain more than half
|
||||
the maximum number of posts will be skipped.
|
||||
|
||||
With the --all option, all active users will be processed.
|
||||
Otherwise, a single user specified by USERNAME.
|
||||
LONG_DESC
|
||||
def build(username = nil)
|
||||
if options[:all] || username.nil?
|
||||
processed, = parallelize_with_progress(active_user_accounts) do |account|
|
||||
PrecomputeFeedService.new.call(account) unless dry_run?
|
||||
PrecomputeFeedService.new.call(account, skip_filled_timelines: options[:skip_filled_timelines]) unless dry_run?
|
||||
end
|
||||
|
||||
say("Regenerated feeds for #{processed} accounts #{dry_run_mode_suffix}", :green, true)
|
||||
|
|
@ -30,7 +34,7 @@ module Mastodon::CLI
|
|||
|
||||
fail_with_message 'No such account' if account.nil?
|
||||
|
||||
PrecomputeFeedService.new.call(account) unless dry_run?
|
||||
PrecomputeFeedService.new.call(account, skip_filled_timelines: options[:skip_filled_timelines]) unless dry_run?
|
||||
|
||||
say("OK #{dry_run_mode_suffix}", :green, true)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -80,9 +80,9 @@ module Mastodon::CLI
|
|||
end
|
||||
|
||||
ip_blocks = if options[:force]
|
||||
IpBlock.where('ip >>= ?', address)
|
||||
IpBlock.containing(address)
|
||||
else
|
||||
IpBlock.where('ip <<= ?', address)
|
||||
IpBlock.contained_by(address)
|
||||
end
|
||||
|
||||
if ip_blocks.empty?
|
||||
|
|
|
|||
|
|
@ -192,6 +192,7 @@ module Mastodon::CLI
|
|||
verify_schema_version!
|
||||
verify_sidekiq_not_active!
|
||||
verify_backup_warning!
|
||||
disable_timeout!
|
||||
end
|
||||
|
||||
def process_deduplications
|
||||
|
|
@ -251,6 +252,13 @@ module Mastodon::CLI
|
|||
fail_with_message 'Maintenance process stopped.' unless yes?('Continue? (Yes/No)')
|
||||
end
|
||||
|
||||
def disable_timeout!
|
||||
# Remove server-configured timeout if present
|
||||
database_connection.execute(<<~SQL.squish)
|
||||
SET statement_timeout = 0
|
||||
SQL
|
||||
end
|
||||
|
||||
def deduplicate_accounts!
|
||||
remove_index_if_exists!(:accounts, 'index_accounts_on_username_and_domain_lower')
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ module Mastodon::CLI
|
|||
class Media < Base
|
||||
include ActionView::Helpers::NumberHelper
|
||||
|
||||
class UnrecognizedOrphanType < StandardError; end
|
||||
|
||||
VALID_PATH_SEGMENTS_SIZE = [7, 10].freeze
|
||||
|
||||
option :days, type: :numeric, default: 7, aliases: [:d]
|
||||
|
|
@ -120,23 +122,10 @@ module Mastodon::CLI
|
|||
object.acl.put(acl: s3_permissions) if options[:fix_permissions] && !dry_run?
|
||||
|
||||
path_segments = object.key.split('/')
|
||||
path_segments.delete('cache')
|
||||
|
||||
unless VALID_PATH_SEGMENTS_SIZE.include?(path_segments.size)
|
||||
progress.log(pastel.yellow("Unrecognized file found: #{object.key}"))
|
||||
next
|
||||
end
|
||||
|
||||
model_name = path_segments.first.classify
|
||||
attachment_name = path_segments[1].singularize
|
||||
record_id = path_segments[2...-2].join.to_i
|
||||
file_name = path_segments.last
|
||||
record = record_map.dig(model_name, record_id)
|
||||
attachment = record&.public_send(attachment_name)
|
||||
|
||||
progress.increment
|
||||
|
||||
next unless attachment.blank? || !attachment.variant?(file_name)
|
||||
next unless orphaned_file?(path_segments, record_map)
|
||||
|
||||
begin
|
||||
object.delete unless dry_run?
|
||||
|
|
@ -148,6 +137,8 @@ module Mastodon::CLI
|
|||
rescue => e
|
||||
progress.log(pastel.red("Error processing #{object.key}: #{e}"))
|
||||
end
|
||||
rescue UnrecognizedOrphanType
|
||||
progress.log(pastel.yellow("Unrecognized file found: #{object.key}"))
|
||||
end
|
||||
end
|
||||
when :fog
|
||||
|
|
@ -165,26 +156,10 @@ module Mastodon::CLI
|
|||
key = path.gsub("#{root_path}#{File::SEPARATOR}", '')
|
||||
|
||||
path_segments = key.split(File::SEPARATOR)
|
||||
path_segments.delete('cache')
|
||||
|
||||
unless VALID_PATH_SEGMENTS_SIZE.include?(path_segments.size)
|
||||
progress.log(pastel.yellow("Unrecognized file found: #{key}"))
|
||||
next
|
||||
end
|
||||
|
||||
model_name = path_segments.first.classify
|
||||
record_id = path_segments[2...-2].join.to_i
|
||||
attachment_name = path_segments[1].singularize
|
||||
file_name = path_segments.last
|
||||
|
||||
next unless PRELOAD_MODEL_WHITELIST.include?(model_name)
|
||||
|
||||
record = model_name.constantize.find_by(id: record_id)
|
||||
attachment = record&.public_send(attachment_name)
|
||||
|
||||
progress.increment
|
||||
|
||||
next unless attachment.blank? || !attachment.variant?(file_name)
|
||||
next unless orphaned_file?(path_segments)
|
||||
|
||||
begin
|
||||
size = File.size(path)
|
||||
|
|
@ -205,6 +180,8 @@ module Mastodon::CLI
|
|||
rescue => e
|
||||
progress.log(pastel.red("Error processing #{key}: #{e}"))
|
||||
end
|
||||
rescue UnrecognizedOrphanType
|
||||
progress.log(pastel.yellow("Unrecognized file found: #{path}"))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -278,14 +255,10 @@ module Mastodon::CLI
|
|||
|
||||
desc 'usage', 'Calculate disk space consumed by Mastodon'
|
||||
def usage
|
||||
say("Attachments:\t#{number_to_human_size(media_attachment_storage_size)} (#{number_to_human_size(local_media_attachment_storage_size)} local)")
|
||||
say("Custom emoji:\t#{number_to_human_size(CustomEmoji.sum(:image_file_size))} (#{number_to_human_size(CustomEmoji.local.sum(:image_file_size))} local)")
|
||||
say("Preview cards:\t#{number_to_human_size(PreviewCard.sum(:image_file_size))}")
|
||||
say("Avatars:\t#{number_to_human_size(Account.sum(:avatar_file_size))} (#{number_to_human_size(Account.local.sum(:avatar_file_size))} local)")
|
||||
say("Headers:\t#{number_to_human_size(Account.sum(:header_file_size))} (#{number_to_human_size(Account.local.sum(:header_file_size))} local)")
|
||||
say("Backups:\t#{number_to_human_size(Backup.sum(:dump_file_size))}")
|
||||
say("Imports:\t#{number_to_human_size(Import.sum(:data_file_size))}")
|
||||
say("Settings:\t#{number_to_human_size(SiteUpload.sum(:file_file_size))}")
|
||||
print_table [
|
||||
%w(Object Total Local),
|
||||
*object_storage_summary,
|
||||
]
|
||||
end
|
||||
|
||||
desc 'lookup URL', 'Lookup where media is displayed by passing a media URL'
|
||||
|
|
@ -300,7 +273,7 @@ module Mastodon::CLI
|
|||
model_name = path_segments.first.classify
|
||||
record_id = path_segments[2...-2].join.to_i
|
||||
|
||||
fail_with_message "Cannot find corresponding model: #{model_name}" unless PRELOAD_MODEL_WHITELIST.include?(model_name)
|
||||
fail_with_message "Cannot find corresponding model: #{model_name}" unless PRELOADED_MODELS.include?(model_name)
|
||||
|
||||
record = model_name.constantize.find_by(id: record_id)
|
||||
record = record.status if record.respond_to?(:status)
|
||||
|
|
@ -316,34 +289,35 @@ module Mastodon::CLI
|
|||
fail_with_message 'Invalid URL'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def media_attachment_storage_size
|
||||
MediaAttachment.sum(file_and_thumbnail_size_sql)
|
||||
end
|
||||
|
||||
def local_media_attachment_storage_size
|
||||
MediaAttachment.where(account: Account.local).sum(file_and_thumbnail_size_sql)
|
||||
end
|
||||
|
||||
def file_and_thumbnail_size_sql
|
||||
Arel.sql(
|
||||
<<~SQL.squish
|
||||
COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)
|
||||
SQL
|
||||
)
|
||||
end
|
||||
|
||||
PRELOAD_MODEL_WHITELIST = %w(
|
||||
PRELOADED_MODELS = %w(
|
||||
Account
|
||||
Backup
|
||||
CustomEmoji
|
||||
Import
|
||||
MediaAttachment
|
||||
PreviewCard
|
||||
SiteUpload
|
||||
).freeze
|
||||
|
||||
private
|
||||
|
||||
def object_storage_summary
|
||||
[
|
||||
[:attachments, MediaAttachment.sum(combined_media_sum), MediaAttachment.where(account: Account.local).sum(combined_media_sum)],
|
||||
[:custom_emoji, CustomEmoji.sum(:image_file_size), CustomEmoji.local.sum(:image_file_size)],
|
||||
[:avatars, Account.sum(:avatar_file_size), Account.local.sum(:avatar_file_size)],
|
||||
[:headers, Account.sum(:header_file_size), Account.local.sum(:header_file_size)],
|
||||
[:preview_cards, PreviewCard.sum(:image_file_size), nil],
|
||||
[:backups, Backup.sum(:dump_file_size), nil],
|
||||
[:settings, SiteUpload.sum(:file_file_size), nil],
|
||||
].map { |label, total, local| [label.to_s.titleize, number_to_human_size(total), local.present? ? number_to_human_size(local) : nil] }
|
||||
end
|
||||
|
||||
def combined_media_sum
|
||||
Arel.sql(<<~SQL.squish)
|
||||
COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)
|
||||
SQL
|
||||
end
|
||||
|
||||
def preload_records_from_mixed_objects(objects)
|
||||
preload_map = Hash.new { |hash, key| hash[key] = [] }
|
||||
|
||||
|
|
@ -356,7 +330,7 @@ module Mastodon::CLI
|
|||
model_name = segments.first.classify
|
||||
record_id = segments[2...-2].join.to_i
|
||||
|
||||
next unless PRELOAD_MODEL_WHITELIST.include?(model_name)
|
||||
next unless PRELOADED_MODELS.include?(model_name)
|
||||
|
||||
preload_map[model_name] << record_id
|
||||
end
|
||||
|
|
@ -365,5 +339,23 @@ module Mastodon::CLI
|
|||
model_map[model_name] = model_name.constantize.where(id: record_ids).index_by(&:id)
|
||||
end
|
||||
end
|
||||
|
||||
def orphaned_file?(path_segments, record_map = nil)
|
||||
path_segments.delete('cache')
|
||||
|
||||
raise UnrecognizedOrphanType unless VALID_PATH_SEGMENTS_SIZE.include?(path_segments.size)
|
||||
|
||||
model_name = path_segments.first.classify
|
||||
record_id = path_segments[2...-2].join.to_i
|
||||
attachment_name = path_segments[1].singularize
|
||||
file_name = path_segments.last
|
||||
|
||||
raise UnrecognizedOrphanType unless PRELOADED_MODELS.include?(model_name)
|
||||
|
||||
record = record_map.present? ? record_map.dig(model_name, record_id) : model_name.constantize.find_by(id: record_id)
|
||||
attachment = record&.public_send(attachment_name)
|
||||
|
||||
attachment.blank? || !attachment.variant?(file_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
dev_null = Logger.new('/dev/null')
|
||||
dev_null = Logger.new(File::NULL)
|
||||
|
||||
Rails.logger = dev_null
|
||||
ActiveRecord::Base.logger = dev_null
|
||||
ActiveJob::Base.logger = dev_null
|
||||
HttpLog.configuration.logger = dev_null
|
||||
HttpLog.configuration.logger = dev_null if defined?(HttpLog)
|
||||
Paperclip.options[:log] = false
|
||||
Chewy.logger = dev_null
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ module Mastodon::CLI
|
|||
option :import, type: :boolean, default: true, desc: 'Import data from the database to the index'
|
||||
option :clean, type: :boolean, default: true, desc: 'Remove outdated documents from the index'
|
||||
option :reset_chewy, type: :boolean, default: false, desc: "Reset Chewy's internal index"
|
||||
option :only_mapping, type: :boolean, default: false, desc: 'Update the index specification without re-index'
|
||||
desc 'deploy', 'Create or upgrade Elasticsearch indices and populate them'
|
||||
long_desc <<~LONG_DESC
|
||||
If Elasticsearch is empty, this command will create the necessary indices
|
||||
|
|
@ -52,6 +53,20 @@ module Mastodon::CLI
|
|||
|
||||
Chewy::Stash::Specification.reset! if options[:reset_chewy]
|
||||
|
||||
if options[:only_mapping]
|
||||
indices.select { |index| index.specification.changed? }.each do |index|
|
||||
progress.title = "Updating mapping for #{index} "
|
||||
index.update_specification
|
||||
index.specification.lock!
|
||||
end
|
||||
|
||||
progress.title = 'Done! '
|
||||
progress.finish
|
||||
|
||||
say('Updated index mappings', :green, true)
|
||||
return
|
||||
end
|
||||
|
||||
# First, ensure all indices are created and have the correct
|
||||
# structure, so that live data can already be written
|
||||
indices.select { |index| index.specification.changed? }.each do |index|
|
||||
|
|
|
|||
|
|
@ -40,17 +40,11 @@ module Mastodon::CLI
|
|||
def remove_statuses
|
||||
return if options[:skip_status_remove]
|
||||
|
||||
say('Creating temporary database indices...')
|
||||
|
||||
ActiveRecord::Base.connection.add_index(:media_attachments, :remote_url, name: :index_media_attachments_remote_url, where: 'remote_url is not null', algorithm: :concurrently, if_not_exists: true)
|
||||
|
||||
max_id = Mastodon::Snowflake.id_at(options[:days].days.ago, with_random: false)
|
||||
start_at = Time.now.to_f
|
||||
|
||||
unless options[:continue] && ActiveRecord::Base.connection.table_exists?('statuses_to_be_deleted')
|
||||
ActiveRecord::Base.connection.add_index(:accounts, :id, name: :index_accounts_local, where: 'domain is null', algorithm: :concurrently, if_not_exists: true)
|
||||
ActiveRecord::Base.connection.add_index(:status_pins, :status_id, name: :index_status_pins_status_id, algorithm: :concurrently, if_not_exists: true)
|
||||
max_id = Mastodon::Snowflake.id_at(options[:days].days.ago, with_random: false)
|
||||
|
||||
unless options[:continue] && ActiveRecord::Base.connection.table_exists?('statuses_to_be_deleted')
|
||||
say('Extract the deletion target from statuses... This might take a while...')
|
||||
|
||||
ActiveRecord::Base.connection.create_table('statuses_to_be_deleted', force: true)
|
||||
|
|
@ -72,9 +66,6 @@ module Mastodon::CLI
|
|||
SQL
|
||||
|
||||
say('Removing temporary database indices to restore write performance...')
|
||||
|
||||
ActiveRecord::Base.connection.remove_index(:accounts, name: :index_accounts_local, if_exists: true)
|
||||
ActiveRecord::Base.connection.remove_index(:status_pins, name: :index_status_pins_status_id, if_exists: true)
|
||||
end
|
||||
|
||||
say('Beginning statuses removal... This might take a while...')
|
||||
|
|
@ -102,12 +93,6 @@ module Mastodon::CLI
|
|||
ActiveRecord::Base.connection.drop_table('statuses_to_be_deleted')
|
||||
|
||||
say("Done after #{Time.now.to_f - start_at}s, removed #{removed} out of #{processed} statuses.", :green)
|
||||
ensure
|
||||
say('Removing temporary database indices to restore write performance...')
|
||||
|
||||
ActiveRecord::Base.connection.remove_index(:accounts, name: :index_accounts_local, if_exists: true)
|
||||
ActiveRecord::Base.connection.remove_index(:status_pins, name: :index_status_pins_status_id, if_exists: true)
|
||||
ActiveRecord::Base.connection.remove_index(:media_attachments, name: :index_media_attachments_remote_url, if_exists: true)
|
||||
end
|
||||
|
||||
def remove_orphans_media_attachments
|
||||
|
|
|
|||
46
lib/mastodon/database.rb
Normal file
46
lib/mastodon/database.rb
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This file is entirely lifted from GitLab.
|
||||
|
||||
# The original file:
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/blob/69127d59467185cf4ff1109d88ceec1f499f354f/lib/gitlab/database.rb#L244-258
|
||||
|
||||
# Copyright (c) 2011-present GitLab B.V.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
module Mastodon
|
||||
module Database
|
||||
def self.add_post_migrate_path_to_rails(force: false)
|
||||
return if ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS'] && !force
|
||||
|
||||
Rails.application.config.paths['db'].each do |db_path|
|
||||
path = Rails.root.join(db_path, 'post_migrate').to_s
|
||||
|
||||
next if Rails.application.config.paths['db/migrate'].include?(path)
|
||||
|
||||
Rails.application.config.paths['db/migrate'] << path
|
||||
|
||||
# Rails memoizes migrations at certain points where it won't read the above
|
||||
# path just yet. As such we must also update the following list of paths.
|
||||
ActiveRecord::Migrator.migrations_paths << path
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
33
lib/mastodon/email_configuration_helper.rb
Normal file
33
lib/mastodon/email_configuration_helper.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mastodon
|
||||
module EmailConfigurationHelper
|
||||
module_function
|
||||
|
||||
# Convert smtp settings from environment variables (or defaults in
|
||||
# `config/email.yml`) into the format that `ActionMailer` understands
|
||||
def convert_smtp_settings(config)
|
||||
enable_starttls = nil
|
||||
enable_starttls_auto = nil
|
||||
|
||||
case config[:enable_starttls]
|
||||
when 'always'
|
||||
enable_starttls = true
|
||||
when 'never'
|
||||
enable_starttls = false
|
||||
when 'auto'
|
||||
enable_starttls_auto = true
|
||||
else
|
||||
enable_starttls_auto = config[:enable_starttls_auto] != 'false'
|
||||
end
|
||||
|
||||
authentication = config[:authentication] == 'none' ? nil : (config[:authentication] || 'plain')
|
||||
|
||||
config.merge(
|
||||
authentication:,
|
||||
enable_starttls:,
|
||||
enable_starttls_auto:
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
26
lib/mastodon/feature.rb
Normal file
26
lib/mastodon/feature.rb
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mastodon::Feature
|
||||
class << self
|
||||
def enabled_features
|
||||
@enabled_features ||=
|
||||
(Rails.configuration.x.mastodon.experimental_features || '').split(',').map(&:strip)
|
||||
end
|
||||
|
||||
def method_missing(name)
|
||||
if respond_to_missing?(name)
|
||||
feature = name.to_s.delete_suffix('_enabled?')
|
||||
enabled = enabled_features.include?(feature)
|
||||
define_singleton_method(name) { enabled }
|
||||
|
||||
return enabled
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def respond_to_missing?(name, include_all = false)
|
||||
name.to_s.end_with?('_enabled?') || super
|
||||
end
|
||||
end
|
||||
end
|
||||
24
lib/mastodon/middleware/prometheus_queue_time.rb
Normal file
24
lib/mastodon/middleware/prometheus_queue_time.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mastodon
|
||||
module Middleware
|
||||
class PrometheusQueueTime < ::PrometheusExporter::Middleware
|
||||
# Overwrite to only collect the queue time metric
|
||||
def call(env)
|
||||
queue_time = measure_queue_time(env)
|
||||
|
||||
result = @app.call(env)
|
||||
|
||||
result
|
||||
ensure
|
||||
obj = {
|
||||
type: 'web',
|
||||
queue_time: queue_time,
|
||||
default_labels: {},
|
||||
}
|
||||
|
||||
@client.send_json(obj)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
51
lib/mastodon/middleware/public_file_server.rb
Normal file
51
lib/mastodon/middleware/public_file_server.rb
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'action_dispatch/middleware/static'
|
||||
|
||||
module Mastodon
|
||||
module Middleware
|
||||
class PublicFileServer
|
||||
SERVICE_WORKER_TTL = 7.days.to_i
|
||||
CACHE_TTL = 28.days.to_i
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
@file_handler = ActionDispatch::FileHandler.new(Rails.application.paths['public'].first)
|
||||
end
|
||||
|
||||
def call(env)
|
||||
file = @file_handler.attempt(env)
|
||||
|
||||
# If the request is not a static file, move on!
|
||||
return @app.call(env) if file.nil?
|
||||
|
||||
status, headers, response = file
|
||||
|
||||
# Set cache headers on static files. Some paths require different cache headers
|
||||
request = Rack::Request.new env
|
||||
headers['cache-control'] = begin
|
||||
if request.path.start_with?('/sw.js')
|
||||
"public, max-age=#{SERVICE_WORKER_TTL}, must-revalidate"
|
||||
elsif request.path.start_with?(paperclip_root_url)
|
||||
"public, max-age=#{CACHE_TTL}, immutable"
|
||||
else
|
||||
"public, max-age=#{CACHE_TTL}, must-revalidate"
|
||||
end
|
||||
end
|
||||
|
||||
# Override the default CSP header set by the CSP middleware
|
||||
headers['content-security-policy'] = "default-src 'none'; form-action 'none'" if request.path.start_with?(paperclip_root_url)
|
||||
|
||||
headers['x-content-type-options'] = 'nosniff'
|
||||
|
||||
[status, headers, response]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def paperclip_root_url
|
||||
ENV.fetch('PAPERCLIP_ROOT_URL', '/system')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
34
lib/mastodon/middleware/socket_cleanup.rb
Normal file
34
lib/mastodon/middleware/socket_cleanup.rb
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mastodon
|
||||
module Middleware
|
||||
class SocketCleanup
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
@app.call(env)
|
||||
ensure
|
||||
clean_up_sockets!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clean_up_sockets!
|
||||
clean_up_redis_socket!
|
||||
clean_up_statsd_socket!
|
||||
end
|
||||
|
||||
def clean_up_redis_socket!
|
||||
RedisConnection.pool.checkin if Thread.current[:redis]
|
||||
Thread.current[:redis] = nil
|
||||
end
|
||||
|
||||
def clean_up_statsd_socket!
|
||||
Thread.current[:statsd_socket]&.close
|
||||
Thread.current[:statsd_socket] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,7 +4,7 @@ module Mastodon
|
|||
module MigrationWarning
|
||||
WARNING_SECONDS = 10
|
||||
|
||||
DEFAULT_WARNING = <<~WARNING_MESSAGE
|
||||
DEFAULT_WARNING = <<~WARNING_MESSAGE.freeze
|
||||
WARNING: This migration may take a *long* time for large instances.
|
||||
It will *not* lock tables for any significant time, but it may run
|
||||
for a very long time. We will pause for #{WARNING_SECONDS} seconds to allow you to
|
||||
|
|
@ -23,7 +23,7 @@ module Mastodon
|
|||
|
||||
def announce_countdown
|
||||
WARNING_SECONDS.downto(1) do |i|
|
||||
say "Continuing in #{i} second#{i == 1 ? '' : 's'}...", true
|
||||
say "Continuing in #{i} second#{'s' unless i == 1}...", true
|
||||
sleep 1
|
||||
end
|
||||
end
|
||||
|
|
|
|||
18
lib/mastodon/prometheus_exporter/local_server.rb
Normal file
18
lib/mastodon/prometheus_exporter/local_server.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'prometheus_exporter/server'
|
||||
require 'prometheus_exporter/client'
|
||||
|
||||
module Mastodon::PrometheusExporter
|
||||
module LocalServer
|
||||
mattr_accessor :bind, :port
|
||||
|
||||
def self.setup!
|
||||
server = PrometheusExporter::Server::WebServer.new(bind:, port:)
|
||||
server.start
|
||||
|
||||
# wire up a default local client
|
||||
PrometheusExporter::Client.default = PrometheusExporter::LocalClient.new(collector: server.collector)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Mastodon::RackMiddleware
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
@app.call(env)
|
||||
ensure
|
||||
clean_up_sockets!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clean_up_sockets!
|
||||
clean_up_redis_socket!
|
||||
clean_up_statsd_socket!
|
||||
end
|
||||
|
||||
def clean_up_redis_socket!
|
||||
RedisConnection.pool.checkin if Thread.current[:redis]
|
||||
Thread.current[:redis] = nil
|
||||
end
|
||||
|
||||
def clean_up_statsd_socket!
|
||||
Thread.current[:statsd_socket]&.close
|
||||
Thread.current[:statsd_socket] = nil
|
||||
end
|
||||
end
|
||||
|
|
@ -9,22 +9,20 @@ class Mastodon::RedisConfiguration
|
|||
|
||||
def base
|
||||
@base ||= setup_config(prefix: nil, defaults: DEFAULTS)
|
||||
.merge(namespace: base_namespace)
|
||||
end
|
||||
|
||||
def sidekiq
|
||||
@sidekiq ||= setup_config(prefix: 'SIDEKIQ_')
|
||||
.merge(namespace: sidekiq_namespace)
|
||||
end
|
||||
|
||||
def cache
|
||||
@cache ||= setup_config(prefix: 'CACHE_')
|
||||
.merge({
|
||||
namespace: cache_namespace,
|
||||
namespace: 'cache',
|
||||
expires_in: 10.minutes,
|
||||
connect_timeout: 5,
|
||||
pool: {
|
||||
size: Sidekiq.server? ? Sidekiq[:concurrency] : Integer(ENV['MAX_THREADS'] || 5),
|
||||
size: Sidekiq.server? ? Sidekiq.default_configuration[:concurrency] : Integer(ENV['MAX_THREADS'] || 5),
|
||||
timeout: 5,
|
||||
},
|
||||
})
|
||||
|
|
@ -36,24 +34,6 @@ class Mastodon::RedisConfiguration
|
|||
ENV['REDIS_DRIVER'] == 'ruby' ? :ruby : :hiredis
|
||||
end
|
||||
|
||||
def namespace
|
||||
@namespace ||= ENV.fetch('REDIS_NAMESPACE', nil)
|
||||
end
|
||||
|
||||
def base_namespace
|
||||
return "mastodon_test#{ENV.fetch('TEST_ENV_NUMBER', nil)}" if Rails.env.test?
|
||||
|
||||
namespace
|
||||
end
|
||||
|
||||
def sidekiq_namespace
|
||||
namespace
|
||||
end
|
||||
|
||||
def cache_namespace
|
||||
namespace ? "#{namespace}_cache" : 'cache'
|
||||
end
|
||||
|
||||
def setup_config(prefix: nil, defaults: {})
|
||||
prefix = "#{prefix}REDIS_"
|
||||
|
||||
|
|
@ -62,7 +42,7 @@ class Mastodon::RedisConfiguration
|
|||
password = ENV.fetch("#{prefix}PASSWORD", nil)
|
||||
host = ENV.fetch("#{prefix}HOST", defaults[:host])
|
||||
port = ENV.fetch("#{prefix}PORT", defaults[:port])
|
||||
db = ENV.fetch("#{prefix}DB", defaults[:db])
|
||||
db = Rails.env.test? ? ENV.fetch('TEST_ENV_NUMBER', defaults[:db]).to_i + 1 : ENV.fetch("#{prefix}DB", defaults[:db])
|
||||
|
||||
return { url:, driver: } if url
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
class Mastodon::SidekiqMiddleware
|
||||
BACKTRACE_LIMIT = 3
|
||||
|
||||
def call(*, &block)
|
||||
Chewy.strategy(:mastodon, &block)
|
||||
def call(_worker_class, job, _queue, &block)
|
||||
setup_query_log_tags(job) do
|
||||
Chewy.strategy(:mastodon, &block)
|
||||
end
|
||||
rescue Mastodon::HostValidationError
|
||||
# Do not retry
|
||||
rescue => e
|
||||
|
|
@ -61,4 +63,14 @@ class Mastodon::SidekiqMiddleware
|
|||
Thread.current[:statsd_socket]&.close
|
||||
Thread.current[:statsd_socket] = nil
|
||||
end
|
||||
|
||||
def setup_query_log_tags(job, &block)
|
||||
if Rails.configuration.active_record.query_log_tags_enabled
|
||||
# If `wrapped` is set, this is an `ActiveJob` which is already in the execution context
|
||||
sidekiq_job_class = job['wrapped'].present? ? nil : job['class'].to_s
|
||||
ActiveSupport::ExecutionContext.set(sidekiq_job_class: sidekiq_job_class, &block)
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ module Mastodon
|
|||
end
|
||||
|
||||
def minor
|
||||
3
|
||||
4
|
||||
end
|
||||
|
||||
def patch
|
||||
8
|
||||
4
|
||||
end
|
||||
|
||||
def default_prerelease
|
||||
|
|
@ -21,7 +21,7 @@ module Mastodon
|
|||
end
|
||||
|
||||
def prerelease
|
||||
ENV['MASTODON_VERSION_PRERELEASE'].presence || default_prerelease
|
||||
version_configuration[:prerelease].presence || default_prerelease
|
||||
end
|
||||
|
||||
def build_metadata
|
||||
|
|
@ -45,7 +45,7 @@ module Mastodon
|
|||
|
||||
def api_versions
|
||||
{
|
||||
mastodon: 2,
|
||||
mastodon: 6,
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ module Mastodon
|
|||
|
||||
# specify git tag or commit hash here
|
||||
def source_tag
|
||||
ENV.fetch('SOURCE_TAG', nil)
|
||||
source_configuration[:tag]
|
||||
end
|
||||
|
||||
def source_url
|
||||
|
|
@ -70,8 +70,24 @@ module Mastodon
|
|||
end
|
||||
end
|
||||
|
||||
def source_commit
|
||||
ENV.fetch('SOURCE_COMMIT', nil)
|
||||
end
|
||||
|
||||
def user_agent
|
||||
@user_agent ||= "Mastodon/#{Version} (#{HTTP::Request::USER_AGENT}; +http#{Rails.configuration.x.use_https ? 's' : ''}://#{Rails.configuration.x.web_domain}/)"
|
||||
@user_agent ||= "Mastodon/#{Version} (#{HTTP::Request::USER_AGENT}; +http#{'s' if Rails.configuration.x.use_https}://#{Rails.configuration.x.web_domain}/)"
|
||||
end
|
||||
|
||||
def version_configuration
|
||||
mastodon_configuration.version
|
||||
end
|
||||
|
||||
def source_configuration
|
||||
mastodon_configuration.source
|
||||
end
|
||||
|
||||
def mastodon_configuration
|
||||
Rails.configuration.x.mastodon
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue