Update to v4.1.0rc3
This commit is contained in:
commit
822569fedc
907 changed files with 51658 additions and 18972 deletions
|
@ -1 +1 @@
|
|||
// Not needed
|
||||
/* Not needed */
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Enumerable
|
||||
# TODO: Remove this once stop to support Ruby 2.6
|
||||
if RUBY_VERSION < '2.7.0'
|
||||
def filter_map
|
||||
if block_given?
|
||||
result = []
|
||||
each do |element|
|
||||
res = yield element
|
||||
result << res if res
|
||||
end
|
||||
result
|
||||
else
|
||||
Enumerator.new do |yielder|
|
||||
result = []
|
||||
each do |element|
|
||||
res = yielder.yield element
|
||||
result << res if res
|
||||
end
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -200,21 +200,44 @@ module Mastodon
|
|||
end
|
||||
end
|
||||
|
||||
desc 'delete USERNAME', 'Delete a user'
|
||||
option :email
|
||||
option :dry_run, type: :boolean
|
||||
desc 'delete [USERNAME]', 'Delete a user'
|
||||
long_desc <<-LONG_DESC
|
||||
Remove a user account with a given USERNAME.
|
||||
LONG_DESC
|
||||
def delete(username)
|
||||
account = Account.find_local(username)
|
||||
|
||||
if account.nil?
|
||||
say('No user with such username', :red)
|
||||
With the --email option, the user is selected based on email
|
||||
rather than username.
|
||||
LONG_DESC
|
||||
def delete(username = nil)
|
||||
if username.present? && options[:email].present?
|
||||
say('Use username or --email, not both', :red)
|
||||
exit(1)
|
||||
elsif username.blank? && options[:email].blank?
|
||||
say('No username provided', :red)
|
||||
exit(1)
|
||||
end
|
||||
|
||||
say("Deleting user with #{account.statuses_count} statuses, this might take a while...")
|
||||
DeleteAccountService.new.call(account, reserve_email: false)
|
||||
say('OK', :green)
|
||||
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||
account = nil
|
||||
|
||||
if username.present?
|
||||
account = Account.find_local(username)
|
||||
if account.nil?
|
||||
say('No user with such username', :red)
|
||||
exit(1)
|
||||
end
|
||||
else
|
||||
account = Account.left_joins(:user).find_by(user: { email: options[:email] })
|
||||
if account.nil?
|
||||
say('No user with such email', :red)
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
say("Deleting user with #{account.statuses_count} statuses, this might take a while...#{dry_run}")
|
||||
DeleteAccountService.new.call(account, reserve_email: false) unless options[:dry_run]
|
||||
say("OK#{dry_run}", :green)
|
||||
end
|
||||
|
||||
option :force, type: :boolean, aliases: [:f], description: 'Override public key check'
|
||||
|
@ -530,6 +553,116 @@ module Mastodon
|
|||
end
|
||||
end
|
||||
|
||||
option :concurrency, type: :numeric, default: 5, aliases: [:c]
|
||||
option :dry_run, type: :boolean
|
||||
desc 'prune', 'Prune remote accounts that never interacted with local users'
|
||||
long_desc <<-LONG_DESC
|
||||
Prune remote account that
|
||||
- follows no local accounts
|
||||
- is not followed by any local accounts
|
||||
- has no statuses on local
|
||||
- has not been mentioned
|
||||
- has not been favourited local posts
|
||||
- not muted/blocked by us
|
||||
LONG_DESC
|
||||
def prune
|
||||
dry_run = options[:dry_run] ? ' (dry run)' : ''
|
||||
|
||||
query = Account.remote.where.not(actor_type: %i(Application Service))
|
||||
query = query.where('NOT EXISTS (SELECT 1 FROM mentions WHERE account_id = accounts.id)')
|
||||
query = query.where('NOT EXISTS (SELECT 1 FROM favourites WHERE account_id = accounts.id)')
|
||||
query = query.where('NOT EXISTS (SELECT 1 FROM statuses WHERE account_id = accounts.id)')
|
||||
query = query.where('NOT EXISTS (SELECT 1 FROM follows WHERE account_id = accounts.id OR target_account_id = accounts.id)')
|
||||
query = query.where('NOT EXISTS (SELECT 1 FROM blocks WHERE account_id = accounts.id OR target_account_id = accounts.id)')
|
||||
query = query.where('NOT EXISTS (SELECT 1 FROM mutes WHERE target_account_id = accounts.id)')
|
||||
query = query.where('NOT EXISTS (SELECT 1 FROM reports WHERE target_account_id = accounts.id)')
|
||||
query = query.where('NOT EXISTS (SELECT 1 FROM follow_requests WHERE account_id = accounts.id OR target_account_id = accounts.id)')
|
||||
|
||||
_, deleted = parallelize_with_progress(query) do |account|
|
||||
next if account.bot? || account.group?
|
||||
next if account.suspended?
|
||||
next if account.silenced?
|
||||
|
||||
account.destroy unless options[:dry_run]
|
||||
1
|
||||
end
|
||||
|
||||
say("OK, pruned #{deleted} accounts#{dry_run}", :green)
|
||||
end
|
||||
|
||||
option :force, type: :boolean
|
||||
option :replay, type: :boolean
|
||||
option :target
|
||||
desc 'migrate USERNAME', 'Migrate a local user to another account'
|
||||
long_desc <<~LONG_DESC
|
||||
With --replay, replay the last migration of the specified account, in
|
||||
case some remote server may not have properly processed the associated
|
||||
`Move` activity.
|
||||
|
||||
With --target, specify another account to migrate to.
|
||||
|
||||
With --force, perform the migration even if the selected account
|
||||
redirects to a different account that the one specified.
|
||||
LONG_DESC
|
||||
def migrate(username)
|
||||
if options[:replay].present? && options[:target].present?
|
||||
say('Use --replay or --target, not both', :red)
|
||||
exit(1)
|
||||
end
|
||||
|
||||
if options[:replay].blank? && options[:target].blank?
|
||||
say('Use either --replay or --target', :red)
|
||||
exit(1)
|
||||
end
|
||||
|
||||
account = Account.find_local(username)
|
||||
|
||||
if account.nil?
|
||||
say("No such account: #{username}", :red)
|
||||
exit(1)
|
||||
end
|
||||
|
||||
migration = nil
|
||||
|
||||
if options[:replay]
|
||||
migration = account.migrations.last
|
||||
if migration.nil?
|
||||
say('The specified account has not performed any migration', :red)
|
||||
exit(1)
|
||||
end
|
||||
|
||||
unless options[:force] || migration.target_acount_id == account.moved_to_account_id
|
||||
say('The specified account is not redirecting to its last migration target. Use --force if you want to replay the migration anyway', :red)
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
if options[:target]
|
||||
target_account = ResolveAccountService.new.call(options[:target])
|
||||
|
||||
if target_account.nil?
|
||||
say("The specified target account could not be found: #{options[:target]}", :red)
|
||||
exit(1)
|
||||
end
|
||||
|
||||
unless options[:force] || account.moved_to_account_id.nil? || account.moved_to_account_id == target_account.id
|
||||
say('The specified account is redirecting to a different target account. Use --force if you want to change the migration target', :red)
|
||||
exit(1)
|
||||
end
|
||||
|
||||
begin
|
||||
migration = account.migrations.create!(acct: target_account.acct)
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
say("Error: #{e.message}", :red)
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
MoveService.new.call(migration)
|
||||
|
||||
say("OK, migrated #{account.acct} to #{migration.target_account.acct}", :green)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rotate_keys_for_account(account, delay = 0)
|
||||
|
@ -541,7 +674,7 @@ module Mastodon
|
|||
old_key = account.private_key
|
||||
new_key = OpenSSL::PKey::RSA.new(2048)
|
||||
account.update(private_key: new_key.to_pem, public_key: new_key.public_key.to_pem)
|
||||
ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, sign_with: old_key)
|
||||
ActivityPub::UpdateDistributionWorker.perform_in(delay, account.id, { 'sign_with' => old_key })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,6 +18,8 @@ module Mastodon
|
|||
option :dry_run, type: :boolean
|
||||
option :limited_federation_mode, type: :boolean
|
||||
option :by_uri, type: :boolean
|
||||
option :include_subdomains, type: :boolean
|
||||
option :purge_domain_blocks, type: :boolean
|
||||
desc 'purge [DOMAIN...]', 'Remove accounts from a DOMAIN without a trace'
|
||||
long_desc <<-LONG_DESC
|
||||
Remove all accounts from a given DOMAIN without leaving behind any
|
||||
|
@ -33,40 +35,75 @@ module Mastodon
|
|||
that has the handle `foo@bar.com` but whose profile is at the URL
|
||||
`https://mastodon-bar.com/users/foo`, would be purged by either
|
||||
`tootctl domains purge bar.com` or `tootctl domains purge --by-uri mastodon-bar.com`.
|
||||
|
||||
When the --include-subdomains option is given, not only DOMAIN is deleted, but all
|
||||
subdomains as well. Note that this may be considerably slower.
|
||||
|
||||
When the --purge-domain-blocks option is given, also purge matching domain blocks.
|
||||
LONG_DESC
|
||||
def purge(*domains)
|
||||
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||
domains = domains.map { |domain| TagManager.instance.normalize_domain(domain) }
|
||||
account_scope = Account.none
|
||||
domain_block_scope = DomainBlock.none
|
||||
emoji_scope = CustomEmoji.none
|
||||
|
||||
scope = begin
|
||||
if options[:limited_federation_mode]
|
||||
Account.remote.where.not(domain: DomainAllow.pluck(:domain))
|
||||
elsif !domains.empty?
|
||||
if options[:by_uri]
|
||||
domains.map { |domain| Account.remote.where(Account.arel_table[:uri].matches("https://#{domain}/%", false, true)) }.reduce(:or)
|
||||
else
|
||||
Account.remote.where(domain: domains)
|
||||
end
|
||||
# Sanity check on command arguments
|
||||
if options[:limited_federation_mode] && !domains.empty?
|
||||
say('DOMAIN parameter not supported with --limited-federation-mode', :red)
|
||||
exit(1)
|
||||
elsif domains.empty? && !options[:limited_federation_mode]
|
||||
say('No domain(s) given', :red)
|
||||
exit(1)
|
||||
end
|
||||
|
||||
# Build scopes from command arguments
|
||||
if options[:limited_federation_mode]
|
||||
account_scope = Account.remote.where.not(domain: DomainAllow.select(:domain))
|
||||
emoji_scope = CustomEmoji.remote.where.not(domain: DomainAllow.select(:domain))
|
||||
else
|
||||
# Handle wildcard subdomains
|
||||
subdomain_patterns = domains.filter_map { |domain| "%.#{Account.sanitize_sql_like(domain[2..])}" if domain.start_with?('*.') }
|
||||
domains = domains.filter { |domain| !domain.start_with?('*.') }
|
||||
# Handle --include-subdomains
|
||||
subdomain_patterns += domains.map { |domain| "%.#{Account.sanitize_sql_like(domain)}" } if options[:include_subdomains]
|
||||
uri_patterns = (domains.map { |domain| Account.sanitize_sql_like(domain) } + subdomain_patterns).map { |pattern| "https://#{pattern}/%" }
|
||||
|
||||
if options[:purge_domain_blocks]
|
||||
domain_block_scope = DomainBlock.where(domain: domains)
|
||||
domain_block_scope = domain_block_scope.or(DomainBlock.where(DomainBlock.arel_table[:domain].matches_any(subdomain_patterns))) unless subdomain_patterns.empty?
|
||||
end
|
||||
|
||||
if options[:by_uri]
|
||||
account_scope = Account.remote.where(Account.arel_table[:uri].matches_any(uri_patterns, false, true))
|
||||
emoji_scope = CustomEmoji.remote.where(CustomEmoji.arel_table[:uri].matches_any(uri_patterns, false, true))
|
||||
else
|
||||
say('No domain(s) given', :red)
|
||||
exit(1)
|
||||
account_scope = Account.remote.where(domain: domains)
|
||||
account_scope = account_scope.or(Account.remote.where(Account.arel_table[:domain].matches_any(subdomain_patterns))) unless subdomain_patterns.empty?
|
||||
emoji_scope = CustomEmoji.where(domain: domains)
|
||||
emoji_scope = emoji_scope.or(CustomEmoji.remote.where(CustomEmoji.arel_table[:uri].matches_any(subdomain_patterns))) unless subdomain_patterns.empty?
|
||||
end
|
||||
end
|
||||
|
||||
processed, = parallelize_with_progress(scope) do |account|
|
||||
# Actually perform the deletions
|
||||
processed, = parallelize_with_progress(account_scope) do |account|
|
||||
DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless options[:dry_run]
|
||||
end
|
||||
|
||||
DomainBlock.where(domain: domains).destroy_all unless options[:dry_run]
|
||||
|
||||
say("Removed #{processed} accounts#{dry_run}", :green)
|
||||
|
||||
custom_emojis = CustomEmoji.where(domain: domains)
|
||||
custom_emojis_count = custom_emojis.count
|
||||
custom_emojis.destroy_all unless options[:dry_run]
|
||||
if options[:purge_domain_blocks]
|
||||
domain_block_count = domain_block_scope.count
|
||||
domain_block_scope.in_batches.destroy_all unless options[:dry_run]
|
||||
say("Removed #{domain_block_count} domain blocks#{dry_run}", :green)
|
||||
end
|
||||
|
||||
custom_emojis_count = emoji_scope.count
|
||||
emoji_scope.in_batches.destroy_all unless options[:dry_run]
|
||||
|
||||
Instance.refresh unless options[:dry_run]
|
||||
|
||||
say("Removed #{custom_emojis_count} custom emojis", :green)
|
||||
say("Removed #{custom_emojis_count} custom emojis#{dry_run}", :green)
|
||||
end
|
||||
|
||||
option :concurrency, type: :numeric, default: 50, aliases: [:c]
|
||||
|
@ -97,7 +134,7 @@ module Mastodon
|
|||
failed = Concurrent::AtomicFixnum.new(0)
|
||||
start_at = Time.now.to_f
|
||||
seed = start ? [start] : Instance.pluck(:domain)
|
||||
blocked_domains = Regexp.new('\\.?' + DomainBlock.where(severity: 1).pluck(:domain).join('|') + '$')
|
||||
blocked_domains = /\.?(#{DomainBlock.where(severity: 1).pluck(:domain).map { |domain| Regexp.escape(domain) }.join('|')})$/
|
||||
progress = create_progress_bar
|
||||
|
||||
pool = Concurrent::ThreadPoolExecutor.new(min_threads: 0, max_threads: options[:concurrency], idletime: 10, auto_terminate: true, max_queue: 0)
|
||||
|
|
|
@ -14,35 +14,78 @@ module Mastodon
|
|||
end
|
||||
|
||||
option :days, type: :numeric, default: 7, aliases: [:d]
|
||||
option :prune_profiles, type: :boolean, default: false
|
||||
option :remove_headers, type: :boolean, default: false
|
||||
option :include_follows, type: :boolean, default: false
|
||||
option :concurrency, type: :numeric, default: 5, aliases: [:c]
|
||||
option :verbose, type: :boolean, default: false, aliases: [:v]
|
||||
option :dry_run, type: :boolean, default: false
|
||||
desc 'remove', 'Remove remote media files'
|
||||
desc 'remove', 'Remove remote media files, headers or avatars'
|
||||
long_desc <<-DESC
|
||||
Removes locally cached copies of media attachments from other servers.
|
||||
|
||||
Removes locally cached copies of media attachments (and optionally profile
|
||||
headers and avatars) from other servers. By default, only media attachements
|
||||
are removed.
|
||||
The --days option specifies how old media attachments have to be before
|
||||
they are removed. It defaults to 7 days.
|
||||
they are removed. In case of avatars and headers, it specifies how old
|
||||
the last webfinger request and update to the user has to be before they
|
||||
are pruned. It defaults to 7 days.
|
||||
If --prune-profiles is specified, only avatars and headers are removed.
|
||||
If --remove-headers is specified, only headers are removed.
|
||||
If --include-follows is specified along with --prune-profiles or
|
||||
--remove-headers, all non-local profiles will be pruned irrespective of
|
||||
follow status. By default, only accounts that are not followed by or
|
||||
following anyone locally are pruned.
|
||||
DESC
|
||||
# rubocop:disable Metrics/PerceivedComplexity
|
||||
def remove
|
||||
time_ago = options[:days].days.ago
|
||||
dry_run = options[:dry_run] ? '(DRY RUN)' : ''
|
||||
if options[:prune_profiles] && options[:remove_headers]
|
||||
say('--prune-profiles and --remove-headers should not be specified simultaneously', :red, true)
|
||||
exit(1)
|
||||
end
|
||||
if options[:include_follows] && !(options[:prune_profiles] || options[:remove_headers])
|
||||
say('--include-follows can only be used with --prune-profiles or --remove-headers', :red, true)
|
||||
exit(1)
|
||||
end
|
||||
time_ago = options[:days].days.ago
|
||||
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||
|
||||
processed, aggregate = parallelize_with_progress(MediaAttachment.cached.where.not(remote_url: '').where('created_at < ?', time_ago)) do |media_attachment|
|
||||
next if media_attachment.file.blank?
|
||||
if options[:prune_profiles] || options[:remove_headers]
|
||||
processed, aggregate = parallelize_with_progress(Account.remote.where({ last_webfingered_at: ..time_ago, updated_at: ..time_ago })) do |account|
|
||||
next if !options[:include_follows] && Follow.where(account: account).or(Follow.where(target_account: account)).exists?
|
||||
next if account.avatar.blank? && account.header.blank?
|
||||
next if options[:remove_headers] && account.header.blank?
|
||||
|
||||
size = (media_attachment.file_file_size || 0) + (media_attachment.thumbnail_file_size || 0)
|
||||
size = (account.header_file_size || 0)
|
||||
size += (account.avatar_file_size || 0) if options[:prune_profiles]
|
||||
|
||||
unless options[:dry_run]
|
||||
media_attachment.file.destroy
|
||||
media_attachment.thumbnail.destroy
|
||||
media_attachment.save
|
||||
unless options[:dry_run]
|
||||
account.header.destroy
|
||||
account.avatar.destroy if options[:prune_profiles]
|
||||
account.save!
|
||||
end
|
||||
|
||||
size
|
||||
end
|
||||
|
||||
size
|
||||
say("Visited #{processed} accounts and removed profile media totaling #{number_to_human_size(aggregate)}#{dry_run}", :green, true)
|
||||
end
|
||||
|
||||
say("Removed #{processed} media attachments (approx. #{number_to_human_size(aggregate)}) #{dry_run}", :green, true)
|
||||
unless options[:prune_profiles] || options[:remove_headers]
|
||||
processed, aggregate = parallelize_with_progress(MediaAttachment.cached.where.not(remote_url: '').where(created_at: ..time_ago)) do |media_attachment|
|
||||
next if media_attachment.file.blank?
|
||||
|
||||
size = (media_attachment.file_file_size || 0) + (media_attachment.thumbnail_file_size || 0)
|
||||
|
||||
unless options[:dry_run]
|
||||
media_attachment.file.destroy
|
||||
media_attachment.thumbnail.destroy
|
||||
media_attachment.save
|
||||
end
|
||||
|
||||
size
|
||||
end
|
||||
|
||||
say("Removed #{processed} media attachments (approx. #{number_to_human_size(aggregate)})#{dry_run}", :green, true)
|
||||
end
|
||||
end
|
||||
|
||||
option :start_after
|
||||
|
@ -183,6 +226,7 @@ module Mastodon
|
|||
|
||||
say("Removed #{removed} orphans (approx. #{number_to_human_size(reclaimed_bytes)})#{dry_run}", :green, true)
|
||||
end
|
||||
# rubocop:enable Metrics/PerceivedComplexity
|
||||
|
||||
option :account, type: :string
|
||||
option :domain, type: :string
|
||||
|
@ -269,7 +313,7 @@ module Mastodon
|
|||
def lookup(url)
|
||||
path = Addressable::URI.parse(url).path
|
||||
|
||||
path_segments = path.split('/')[2..-1]
|
||||
path_segments = path.split('/')[2..]
|
||||
path_segments.delete('cache')
|
||||
|
||||
unless [7, 10].include?(path_segments.size)
|
||||
|
|
|
@ -37,6 +37,7 @@ REDIS_CACHE_PARAMS = {
|
|||
namespace: cache_namespace,
|
||||
pool_size: Sidekiq.server? ? Sidekiq.options[:concurrency] : Integer(ENV['MAX_THREADS'] || 5),
|
||||
pool_timeout: 5,
|
||||
connect_timeout: 5,
|
||||
}.freeze
|
||||
|
||||
REDIS_SIDEKIQ_PARAMS = {
|
||||
|
|
|
@ -9,19 +9,19 @@ module Mastodon
|
|||
end
|
||||
|
||||
def minor
|
||||
0
|
||||
1
|
||||
end
|
||||
|
||||
def patch
|
||||
2
|
||||
0
|
||||
end
|
||||
|
||||
def flags
|
||||
''
|
||||
'rc3'
|
||||
end
|
||||
|
||||
def suffix
|
||||
'-gh1.3'
|
||||
'-gh2.0'
|
||||
end
|
||||
|
||||
def to_a
|
||||
|
|
|
@ -49,7 +49,7 @@ class Sanitize
|
|||
end
|
||||
end
|
||||
|
||||
current_node.replace(current_node.text) unless LINK_PROTOCOLS.include?(scheme)
|
||||
current_node.replace(Nokogiri::XML::Text.new(current_node.text, current_node.document)) unless LINK_PROTOCOLS.include?(scheme)
|
||||
end
|
||||
|
||||
UNSUPPORTED_ELEMENTS_TRANSFORMER = lambda do |env|
|
||||
|
|
|
@ -142,7 +142,7 @@ namespace :mastodon do
|
|||
prompt.say "\n"
|
||||
|
||||
if prompt.yes?('Do you want to store uploaded files on the cloud?', default: false)
|
||||
case prompt.select('Provider', ['DigitalOcean Spaces', 'Amazon S3', 'Wasabi', 'Minio', 'Google Cloud Storage'])
|
||||
case prompt.select('Provider', ['DigitalOcean Spaces', 'Amazon S3', 'Wasabi', 'Minio', 'Google Cloud Storage', 'Storj DCS'])
|
||||
when 'DigitalOcean Spaces'
|
||||
env['S3_ENABLED'] = 'true'
|
||||
env['S3_PROTOCOL'] = 'https'
|
||||
|
@ -194,7 +194,7 @@ namespace :mastodon do
|
|||
|
||||
env['S3_HOSTNAME'] = prompt.ask('S3 hostname:') do |q|
|
||||
q.required true
|
||||
q.default 's3-us-east-1.amazonaws.com'
|
||||
q.default 's3.us-east-1.amazonaws.com'
|
||||
q.modify :strip
|
||||
end
|
||||
|
||||
|
@ -257,6 +257,42 @@ namespace :mastodon do
|
|||
q.required true
|
||||
q.modify :strip
|
||||
end
|
||||
when 'Storj DCS'
|
||||
env['S3_ENABLED'] = 'true'
|
||||
env['S3_PROTOCOL'] = 'https'
|
||||
env['S3_REGION'] = 'global'
|
||||
|
||||
env['S3_ENDPOINT'] = prompt.ask('Storj DCS endpoint URL:') do |q|
|
||||
q.required true
|
||||
q.default "https://gateway.storjshare.io"
|
||||
q.modify :strip
|
||||
end
|
||||
|
||||
env['S3_PROTOCOL'] = env['S3_ENDPOINT'].start_with?('https') ? 'https' : 'http'
|
||||
env['S3_HOSTNAME'] = env['S3_ENDPOINT'].gsub(/\Ahttps?:\/\//, '')
|
||||
|
||||
env['S3_BUCKET'] = prompt.ask('Storj DCS bucket name:') do |q|
|
||||
q.required true
|
||||
q.default "files.#{env['LOCAL_DOMAIN']}"
|
||||
q.modify :strip
|
||||
end
|
||||
|
||||
env['AWS_ACCESS_KEY_ID'] = prompt.ask('Storj Gateway access key (uplink share --register --readonly=false --not-after=none sj://bucket):') do |q|
|
||||
q.required true
|
||||
q.modify :strip
|
||||
end
|
||||
|
||||
env['AWS_SECRET_ACCESS_KEY'] = prompt.ask('Storj Gateway secret key:') do |q|
|
||||
q.required true
|
||||
q.modify :strip
|
||||
end
|
||||
|
||||
linksharing_access_key = prompt.ask('Storj Linksharing access key (uplink share --register --public --readonly=true --disallow-lists --not-after=none sj://bucket):') do |q|
|
||||
q.required true
|
||||
q.modify :strip
|
||||
end
|
||||
env['S3_ALIAS_HOST'] = "link.storjshare.io/raw/#{linksharing_access_key}/#{env['S3_BUCKET']}"
|
||||
|
||||
when 'Google Cloud Storage'
|
||||
env['S3_ENABLED'] = 'true'
|
||||
env['S3_PROTOCOL'] = 'https'
|
||||
|
@ -395,18 +431,11 @@ namespace :mastodon do
|
|||
incompatible_syntax = false
|
||||
|
||||
env_contents = env.each_pair.map do |key, value|
|
||||
if value.is_a?(String) && value =~ /[\s\#\\"]/
|
||||
incompatible_syntax = true
|
||||
value = value.to_s
|
||||
escaped = dotenv_escape(value)
|
||||
incompatible_syntax = true if value != escaped
|
||||
|
||||
if value =~ /[']/
|
||||
value = value.to_s.gsub(/[\\"\$]/) { |x| "\\#{x}" }
|
||||
"#{key}=\"#{value}\""
|
||||
else
|
||||
"#{key}='#{value}'"
|
||||
end
|
||||
else
|
||||
"#{key}=#{value}"
|
||||
end
|
||||
"#{key}=#{escaped}"
|
||||
end.join("\n")
|
||||
|
||||
generated_header = "# Generated with mastodon:setup on #{Time.now.utc}\n\n".dup
|
||||
|
@ -519,3 +548,49 @@ def disable_log_stdout!
|
|||
HttpLog.configuration.logger = dev_null
|
||||
Paperclip.options[:log] = false
|
||||
end
|
||||
|
||||
def dotenv_escape(value)
|
||||
# Dotenv has its own parser, which unfortunately deviates somewhat from
|
||||
# what shells actually do.
|
||||
#
|
||||
# In particular, we can't use Shellwords::escape because it outputs a
|
||||
# non-quotable string, while Dotenv requires `#` to always be in quoted
|
||||
# strings.
|
||||
#
|
||||
# Therefore, we need to write our own escape code…
|
||||
# Dotenv's parser has a *lot* of edge cases, and I think not every
|
||||
# ASCII string can even be represented into something Dotenv can parse,
|
||||
# so this is a best effort thing.
|
||||
#
|
||||
# In particular, strings with all the following probably cannot be
|
||||
# escaped:
|
||||
# - `#`, or ends with spaces, which requires some form of quoting (simply escaping won't work)
|
||||
# - `'` (single quote), preventing us from single-quoting
|
||||
# - `\` followed by either `r` or `n`
|
||||
|
||||
# No character that would cause Dotenv trouble
|
||||
return value unless /[\s\#\\"'$]/.match?(value)
|
||||
|
||||
# As long as the value doesn't include single quotes, we can safely
|
||||
# rely on single quotes
|
||||
return "'#{value}'" unless /[']/.match?(value)
|
||||
|
||||
# If the value contains the string '\n' or '\r' we simply can't use
|
||||
# a double-quoted string, because Dotenv will expand \n or \r no
|
||||
# matter how much escaping we add.
|
||||
double_quoting_disallowed = /\\[rn]/.match?(value)
|
||||
|
||||
value = value.gsub(double_quoting_disallowed ? /[\\"'\s]/ : /[\\"']/) { |x| "\\#{x}" }
|
||||
|
||||
# Dotenv is especially tricky with `$` as unbalanced
|
||||
# parenthesis will make it not unescape `\$` as `$`…
|
||||
|
||||
# Variables
|
||||
value = value.gsub(/\$(?!\()/) { |x| "\\#{x}" }
|
||||
# Commands
|
||||
value = value.gsub(/\$(?<cmd>\((?:[^()]|\g<cmd>)+\))/) { |x| "\\#{x}" }
|
||||
|
||||
value = "\"#{value}\"" unless double_quoting_disallowed
|
||||
|
||||
value
|
||||
end
|
||||
|
|
|
@ -43,6 +43,16 @@ namespace :tests do
|
|||
puts 'CustomFilterKeyword records not created as expected'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
unless Admin::ActionLog.find_by(target_type: 'DomainBlock', target_id: 1).human_identifier == 'example.org'
|
||||
puts 'Admin::ActionLog domain block records not updated as expected'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
unless Admin::ActionLog.find_by(target_type: 'EmailDomainBlock', target_id: 1).human_identifier == 'example.org'
|
||||
puts 'Admin::ActionLog email domain block records not updated as expected'
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Populate the database with test data for 2.4.3'
|
||||
|
@ -84,8 +94,8 @@ namespace :tests do
|
|||
VALUES
|
||||
(1, 'destroy', 'Account', 1, now(), now()),
|
||||
(1, 'destroy', 'User', 1, now(), now()),
|
||||
(1, 'destroy', 'DomainBlock', 1312, now(), now()),
|
||||
(1, 'destroy', 'EmailDomainBlock', 1312, now(), now()),
|
||||
(1, 'destroy', 'DomainBlock', 1, now(), now()),
|
||||
(1, 'destroy', 'EmailDomainBlock', 1, now(), now()),
|
||||
(1, 'destroy', 'Status', 1, now(), now()),
|
||||
(1, 'destroy', 'CustomEmoji', 3, now(), now());
|
||||
SQL
|
||||
|
|
Reference in a new issue