Merge pull request #2242 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 00c222377d
			
			
This commit is contained in:
		
						commit
						d907e79140
					
				| 
						 | 
					@ -323,7 +323,7 @@ module.exports = {
 | 
				
			||||||
        'plugin:import/recommended',
 | 
					        'plugin:import/recommended',
 | 
				
			||||||
        'plugin:import/typescript',
 | 
					        'plugin:import/typescript',
 | 
				
			||||||
        'plugin:promise/recommended',
 | 
					        'plugin:promise/recommended',
 | 
				
			||||||
        'plugin:jsdoc/recommended',
 | 
					        'plugin:jsdoc/recommended-typescript',
 | 
				
			||||||
        'plugin:prettier/recommended',
 | 
					        'plugin:prettier/recommended',
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										111
									
								
								.rubocop.yml
								
								
								
								
							
							
						
						
									
										111
									
								
								.rubocop.yml
								
								
								
								
							| 
						 | 
					@ -53,6 +53,28 @@ Lint/UselessAccessModifier:
 | 
				
			||||||
  ContextCreatingMethods:
 | 
					  ContextCreatingMethods:
 | 
				
			||||||
    - class_methods
 | 
					    - class_methods
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Disable most Metrics/*Length cops
 | 
				
			||||||
 | 
					# Reason: those are often triggered and force significant refactors when this happend
 | 
				
			||||||
 | 
					#         but the team feel they are not really improving the code quality.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocklength
 | 
				
			||||||
 | 
					Metrics/BlockLength:
 | 
				
			||||||
 | 
					  Enabled: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsclasslength
 | 
				
			||||||
 | 
					Metrics/ClassLength:
 | 
				
			||||||
 | 
					  Enabled: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmethodlength
 | 
				
			||||||
 | 
					Metrics/MethodLength:
 | 
				
			||||||
 | 
					  Enabled: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength
 | 
				
			||||||
 | 
					Metrics/ModuleLength:
 | 
				
			||||||
 | 
					  Enabled: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## End Disable Metrics/*Length cops
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Reason: Currently disabled in .rubocop_todo.yml
 | 
					# Reason: Currently disabled in .rubocop_todo.yml
 | 
				
			||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize
 | 
					# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize
 | 
				
			||||||
Metrics/AbcSize:
 | 
					Metrics/AbcSize:
 | 
				
			||||||
| 
						 | 
					@ -60,88 +82,12 @@ Metrics/AbcSize:
 | 
				
			||||||
    - 'lib/mastodon/cli/*.rb'
 | 
					    - 'lib/mastodon/cli/*.rb'
 | 
				
			||||||
    - db/*migrate/**/*
 | 
					    - db/*migrate/**/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Reason: Some functions cannot be broken up, but others may be refactor candidates
 | 
					 | 
				
			||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocklength
 | 
					 | 
				
			||||||
Metrics/BlockLength:
 | 
					 | 
				
			||||||
  CountAsOne: ['array', 'hash', 'heredoc', 'method_call']
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'config/routes.rb'
 | 
					 | 
				
			||||||
    - 'lib/mastodon/cli/*.rb'
 | 
					 | 
				
			||||||
    - 'lib/tasks/*.rake'
 | 
					 | 
				
			||||||
    - 'app/models/concerns/account_associations.rb'
 | 
					 | 
				
			||||||
    - 'app/models/concerns/account_interactions.rb'
 | 
					 | 
				
			||||||
    - 'app/models/concerns/ldap_authenticable.rb'
 | 
					 | 
				
			||||||
    - 'app/models/concerns/omniauthable.rb'
 | 
					 | 
				
			||||||
    - 'app/models/concerns/pam_authenticable.rb'
 | 
					 | 
				
			||||||
    - 'app/models/concerns/remotable.rb'
 | 
					 | 
				
			||||||
    - 'app/services/suspend_account_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/unsuspend_account_service.rb'
 | 
					 | 
				
			||||||
    - 'app/views/accounts/show.rss.ruby'
 | 
					 | 
				
			||||||
    - 'app/views/tags/show.rss.ruby'
 | 
					 | 
				
			||||||
    - 'config/environments/development.rb'
 | 
					 | 
				
			||||||
    - 'config/environments/production.rb'
 | 
					 | 
				
			||||||
    - 'config/initializers/devise.rb'
 | 
					 | 
				
			||||||
    - 'config/initializers/doorkeeper.rb'
 | 
					 | 
				
			||||||
    - 'config/initializers/omniauth.rb'
 | 
					 | 
				
			||||||
    - 'config/initializers/simple_form.rb'
 | 
					 | 
				
			||||||
    - 'config/navigation.rb'
 | 
					 | 
				
			||||||
    - 'config/routes.rb'
 | 
					 | 
				
			||||||
    - 'config/routes/*.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb'
 | 
					 | 
				
			||||||
    - 'lib/paperclip/gif_transcoder.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Reason:
 | 
					# Reason:
 | 
				
			||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocknesting
 | 
					# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocknesting
 | 
				
			||||||
Metrics/BlockNesting:
 | 
					Metrics/BlockNesting:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'lib/mastodon/cli/*.rb'
 | 
					    - 'lib/mastodon/cli/*.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Reason: Some Excluded files would be candidates for refactoring but not currently addressed
 | 
					 | 
				
			||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsclasslength
 | 
					 | 
				
			||||||
Metrics/ClassLength:
 | 
					 | 
				
			||||||
  CountAsOne: ['array', 'hash', 'heredoc', 'method_call']
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'lib/mastodon/cli/*.rb'
 | 
					 | 
				
			||||||
    - 'app/controllers/admin/accounts_controller.rb'
 | 
					 | 
				
			||||||
    - 'app/controllers/api/base_controller.rb'
 | 
					 | 
				
			||||||
    - 'app/controllers/api/v1/admin/accounts_controller.rb'
 | 
					 | 
				
			||||||
    - 'app/controllers/application_controller.rb'
 | 
					 | 
				
			||||||
    - 'app/controllers/auth/registrations_controller.rb'
 | 
					 | 
				
			||||||
    - 'app/controllers/auth/sessions_controller.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/activitypub/activity.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/activitypub/activity/create.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/activitypub/tag_manager.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/feed_manager.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/link_details_extractor.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/request.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/text_formatter.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/user_settings_decorator.rb'
 | 
					 | 
				
			||||||
    - 'app/mailers/user_mailer.rb'
 | 
					 | 
				
			||||||
    - 'app/models/account.rb'
 | 
					 | 
				
			||||||
    - 'app/models/admin/account_action.rb'
 | 
					 | 
				
			||||||
    - 'app/models/form/account_batch.rb'
 | 
					 | 
				
			||||||
    - 'app/models/media_attachment.rb'
 | 
					 | 
				
			||||||
    - 'app/models/status.rb'
 | 
					 | 
				
			||||||
    - 'app/models/tag.rb'
 | 
					 | 
				
			||||||
    - 'app/models/user.rb'
 | 
					 | 
				
			||||||
    - 'app/serializers/activitypub/actor_serializer.rb'
 | 
					 | 
				
			||||||
    - 'app/serializers/activitypub/note_serializer.rb'
 | 
					 | 
				
			||||||
    - 'app/serializers/rest/status_serializer.rb'
 | 
					 | 
				
			||||||
    - 'app/services/account_search_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/activitypub/process_account_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/activitypub/process_status_update_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/backup_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/bulk_import_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/delete_account_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/fan_out_on_write_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/fetch_link_card_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/import_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/notify_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/post_status_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/update_status_service.rb'
 | 
					 | 
				
			||||||
    - 'lib/paperclip/color_extractor.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Reason: Currently disabled in .rubocop_todo.yml
 | 
					# Reason: Currently disabled in .rubocop_todo.yml
 | 
				
			||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity
 | 
					# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity
 | 
				
			||||||
Metrics/CyclomaticComplexity:
 | 
					Metrics/CyclomaticComplexity:
 | 
				
			||||||
| 
						 | 
					@ -149,17 +95,10 @@ Metrics/CyclomaticComplexity:
 | 
				
			||||||
    - lib/mastodon/cli/*.rb
 | 
					    - lib/mastodon/cli/*.rb
 | 
				
			||||||
    - db/*migrate/**/*
 | 
					    - db/*migrate/**/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Reason: Currently disabled in .rubocop_todo.yml
 | 
					 | 
				
			||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmethodlength
 | 
					 | 
				
			||||||
Metrics/MethodLength:
 | 
					 | 
				
			||||||
  CountAsOne: [array, heredoc]
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'lib/mastodon/cli/*.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Reason:
 | 
					# Reason:
 | 
				
			||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength
 | 
					# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsparameterlists
 | 
				
			||||||
Metrics/ModuleLength:
 | 
					Metrics/ParameterLists:
 | 
				
			||||||
  CountAsOne: [array, heredoc]
 | 
					  CountKeywordArgs: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Reason: Prevailing style is argument file paths
 | 
					# Reason: Prevailing style is argument file paths
 | 
				
			||||||
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath
 | 
					# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -156,12 +156,6 @@ Metrics/AbcSize:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'app/serializers/initial_state_serializer.rb'
 | 
					    - 'app/serializers/initial_state_serializer.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
 | 
					 | 
				
			||||||
# AllowedMethods: refine
 | 
					 | 
				
			||||||
Metrics/BlockLength:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/models/concerns/status_safe_reblog_insert.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Configuration parameters: CountBlocks, Max.
 | 
					# Configuration parameters: CountBlocks, Max.
 | 
				
			||||||
Metrics/BlockNesting:
 | 
					Metrics/BlockNesting:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
| 
						 | 
					@ -171,28 +165,6 @@ Metrics/BlockNesting:
 | 
				
			||||||
Metrics/CyclomaticComplexity:
 | 
					Metrics/CyclomaticComplexity:
 | 
				
			||||||
  Max: 25
 | 
					  Max: 25
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
 | 
					 | 
				
			||||||
Metrics/MethodLength:
 | 
					 | 
				
			||||||
  Max: 58
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Configuration parameters: CountComments, Max, CountAsOne.
 | 
					 | 
				
			||||||
Metrics/ModuleLength:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/controllers/concerns/signature_verification.rb'
 | 
					 | 
				
			||||||
    - 'app/helpers/application_helper.rb'
 | 
					 | 
				
			||||||
    - 'app/helpers/jsonld_helper.rb'
 | 
					 | 
				
			||||||
    - 'app/models/concerns/account_interactions.rb'
 | 
					 | 
				
			||||||
    - 'app/models/concerns/has_user_settings.rb'
 | 
					 | 
				
			||||||
    - 'lib/sanitize_ext/sanitize_config.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters.
 | 
					 | 
				
			||||||
Metrics/ParameterLists:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/models/concerns/account_interactions.rb'
 | 
					 | 
				
			||||||
    - 'app/services/activitypub/fetch_remote_account_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/activitypub/fetch_remote_actor_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/activitypub/fetch_remote_status_service.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
 | 
					# Configuration parameters: AllowedMethods, AllowedPatterns.
 | 
				
			||||||
Metrics/PerceivedComplexity:
 | 
					Metrics/PerceivedComplexity:
 | 
				
			||||||
  Max: 28
 | 
					  Max: 28
 | 
				
			||||||
| 
						 | 
					@ -894,7 +866,6 @@ Rails/WhereExists:
 | 
				
			||||||
    - 'app/validators/vote_validator.rb'
 | 
					    - 'app/validators/vote_validator.rb'
 | 
				
			||||||
    - 'app/workers/move_worker.rb'
 | 
					    - 'app/workers/move_worker.rb'
 | 
				
			||||||
    - 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb'
 | 
					    - 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb'
 | 
				
			||||||
    - 'lib/mastodon/cli/email_domain_blocks.rb'
 | 
					 | 
				
			||||||
    - 'lib/tasks/tests.rake'
 | 
					    - 'lib/tasks/tests.rake'
 | 
				
			||||||
    - 'spec/controllers/api/v1/accounts/notes_controller_spec.rb'
 | 
					    - 'spec/controllers/api/v1/accounts/notes_controller_spec.rb'
 | 
				
			||||||
    - 'spec/controllers/api/v1/tags_controller_spec.rb'
 | 
					    - 'spec/controllers/api/v1/tags_controller_spec.rb'
 | 
				
			||||||
| 
						 | 
					@ -956,7 +927,6 @@ Style/FormatStringToken:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'app/models/privacy_policy.rb'
 | 
					    - 'app/models/privacy_policy.rb'
 | 
				
			||||||
    - 'config/initializers/devise.rb'
 | 
					    - 'config/initializers/devise.rb'
 | 
				
			||||||
    - 'lib/mastodon/cli/maintenance.rb'
 | 
					 | 
				
			||||||
    - 'lib/paperclip/color_extractor.rb'
 | 
					    - 'lib/paperclip/color_extractor.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										2
									
								
								Gemfile
								
								
								
								
							| 
						 | 
					@ -59,7 +59,7 @@ gem 'idn-ruby', require: 'idn'
 | 
				
			||||||
gem 'kaminari', '~> 1.2'
 | 
					gem 'kaminari', '~> 1.2'
 | 
				
			||||||
gem 'link_header', '~> 0.0'
 | 
					gem 'link_header', '~> 0.0'
 | 
				
			||||||
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
 | 
					gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
 | 
				
			||||||
gem 'nokogiri', '~> 1.14'
 | 
					gem 'nokogiri', '~> 1.15'
 | 
				
			||||||
gem 'nsa', '~> 0.2'
 | 
					gem 'nsa', '~> 0.2'
 | 
				
			||||||
gem 'oj', '~> 3.14'
 | 
					gem 'oj', '~> 3.14'
 | 
				
			||||||
gem 'ox', '~> 2.14'
 | 
					gem 'ox', '~> 2.14'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -439,8 +439,8 @@ GEM
 | 
				
			||||||
      net-protocol
 | 
					      net-protocol
 | 
				
			||||||
    net-ssh (7.1.0)
 | 
					    net-ssh (7.1.0)
 | 
				
			||||||
    nio4r (2.5.9)
 | 
					    nio4r (2.5.9)
 | 
				
			||||||
    nokogiri (1.14.3)
 | 
					    nokogiri (1.15.2)
 | 
				
			||||||
      mini_portile2 (~> 2.8.0)
 | 
					      mini_portile2 (~> 2.8.2)
 | 
				
			||||||
      racc (~> 1.4)
 | 
					      racc (~> 1.4)
 | 
				
			||||||
    nsa (0.2.8)
 | 
					    nsa (0.2.8)
 | 
				
			||||||
      activesupport (>= 4.2, < 7)
 | 
					      activesupport (>= 4.2, < 7)
 | 
				
			||||||
| 
						 | 
					@ -642,7 +642,7 @@ GEM
 | 
				
			||||||
      activerecord (>= 4.0.0)
 | 
					      activerecord (>= 4.0.0)
 | 
				
			||||||
      railties (>= 4.0.0)
 | 
					      railties (>= 4.0.0)
 | 
				
			||||||
    semantic_range (3.0.0)
 | 
					    semantic_range (3.0.0)
 | 
				
			||||||
    sidekiq (6.5.8)
 | 
					    sidekiq (6.5.9)
 | 
				
			||||||
      connection_pool (>= 2.2.5, < 3)
 | 
					      connection_pool (>= 2.2.5, < 3)
 | 
				
			||||||
      rack (~> 2.0)
 | 
					      rack (~> 2.0)
 | 
				
			||||||
      redis (>= 4.5.0, < 5)
 | 
					      redis (>= 4.5.0, < 5)
 | 
				
			||||||
| 
						 | 
					@ -829,7 +829,7 @@ DEPENDENCIES
 | 
				
			||||||
  mime-types (~> 3.4.1)
 | 
					  mime-types (~> 3.4.1)
 | 
				
			||||||
  net-http (~> 0.3.2)
 | 
					  net-http (~> 0.3.2)
 | 
				
			||||||
  net-ldap (~> 0.18)
 | 
					  net-ldap (~> 0.18)
 | 
				
			||||||
  nokogiri (~> 1.14)
 | 
					  nokogiri (~> 1.15)
 | 
				
			||||||
  nsa (~> 0.2)
 | 
					  nsa (~> 0.2)
 | 
				
			||||||
  oj (~> 3.14)
 | 
					  oj (~> 3.14)
 | 
				
			||||||
  omniauth (~> 1.9)
 | 
					  omniauth (~> 1.9)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,5 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# rubocop:disable Metrics/ModuleLength
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
module LanguagesHelper
 | 
					module LanguagesHelper
 | 
				
			||||||
  ISO_639_1 = {
 | 
					  ISO_639_1 = {
 | 
				
			||||||
    aa: ['Afar', 'Afaraf'].freeze,
 | 
					    aa: ['Afar', 'Afaraf'].freeze,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,7 @@ import { registrationsOpen } from 'flavours/glitch/initial_state';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = (state, { accountId }) => ({
 | 
					const mapStateToProps = (state, { accountId }) => ({
 | 
				
			||||||
  displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']),
 | 
					  displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']),
 | 
				
			||||||
  signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], '/auth/sign_up'),
 | 
					  signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapDispatchToProps = (dispatch) => ({
 | 
					const mapDispatchToProps = (dispatch) => ({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -161,8 +161,9 @@ const makeMapStateToProps = () => {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const truncate = (str, num) => {
 | 
					const truncate = (str, num) => {
 | 
				
			||||||
  if (str.length > num) {
 | 
					  const arr = Array.from(str);
 | 
				
			||||||
    return str.slice(0, num) + '…';
 | 
					  if (arr.length > num) {
 | 
				
			||||||
 | 
					    return arr.slice(0, num).join('') + '…';
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    return str;
 | 
					    return str;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ const SignInBanner = () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let signupButton;
 | 
					  let signupButton;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], '/auth/sign_up'));
 | 
					  const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (registrationsOpen) {
 | 
					  if (registrationsOpen) {
 | 
				
			||||||
    signupButton = (
 | 
					    signupButton = (
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,14 +14,15 @@ export type DecimalUnits = ValueOf<typeof DECIMAL_UNITS>;
 | 
				
			||||||
const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10;
 | 
					const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10;
 | 
				
			||||||
const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
 | 
					const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ShortNumber = [number, DecimalUnits, 0 | 1]; // Array of: shorten number, unit of shorten number and maximum fraction digits
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {number} sourceNumber Number to convert to short number
 | 
					 * @param sourceNumber Number to convert to short number
 | 
				
			||||||
 * @returns {ShortNumber} Calculated short number
 | 
					 * @returns Calculated short number
 | 
				
			||||||
 * @example
 | 
					 * @example
 | 
				
			||||||
 * shortNumber(5936);
 | 
					 * shortNumber(5936);
 | 
				
			||||||
 * // => [5.936, 1000, 1]
 | 
					 * // => [5.936, 1000, 1]
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export type ShortNumber = [number, DecimalUnits, 0 | 1]; // Array of: shorten number, unit of shorten number and maximum fraction digits
 | 
					 | 
				
			||||||
export function toShortNumber(sourceNumber: number): ShortNumber {
 | 
					export function toShortNumber(sourceNumber: number): ShortNumber {
 | 
				
			||||||
  if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
 | 
					  if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
 | 
				
			||||||
    return [sourceNumber, DECIMAL_UNITS.ONE, 0];
 | 
					    return [sourceNumber, DECIMAL_UNITS.ONE, 0];
 | 
				
			||||||
| 
						 | 
					@ -45,9 +46,9 @@ export function toShortNumber(sourceNumber: number): ShortNumber {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {number} sourceNumber Original number that is shortened
 | 
					 * @param sourceNumber Original number that is shortened
 | 
				
			||||||
 * @param {number} division The scale in which short number is displayed
 | 
					 * @param division The scale in which short number is displayed
 | 
				
			||||||
 * @returns {number} Number that can be used for plurals when short form used
 | 
					 * @returns Number that can be used for plurals when short form used
 | 
				
			||||||
 * @example
 | 
					 * @example
 | 
				
			||||||
 * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
 | 
					 * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
 | 
				
			||||||
 * // => 1790
 | 
					 * // => 1790
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,7 @@ import { registrationsOpen } from 'mastodon/initial_state';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = (state, { accountId }) => ({
 | 
					const mapStateToProps = (state, { accountId }) => ({
 | 
				
			||||||
  displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']),
 | 
					  displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']),
 | 
				
			||||||
  signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], '/auth/sign_up'),
 | 
					  signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapDispatchToProps = (dispatch) => ({
 | 
					const mapDispatchToProps = (dispatch) => ({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -166,8 +166,9 @@ const makeMapStateToProps = () => {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const truncate = (str, num) => {
 | 
					const truncate = (str, num) => {
 | 
				
			||||||
  if (str.length > num) {
 | 
					  const arr = Array.from(str);
 | 
				
			||||||
    return str.slice(0, num) + '…';
 | 
					  if (arr.length > num) {
 | 
				
			||||||
 | 
					    return arr.slice(0, num).join('') + '…';
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    return str;
 | 
					    return str;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,7 @@ const SignInBanner = () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let signupButton;
 | 
					  let signupButton;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], '/auth/sign_up'));
 | 
					  const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (registrationsOpen) {
 | 
					  if (registrationsOpen) {
 | 
				
			||||||
    signupButton = (
 | 
					    signupButton = (
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,14 +14,15 @@ export type DecimalUnits = ValueOf<typeof DECIMAL_UNITS>;
 | 
				
			||||||
const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10;
 | 
					const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10;
 | 
				
			||||||
const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
 | 
					const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ShortNumber = [number, DecimalUnits, 0 | 1]; // Array of: shorten number, unit of shorten number and maximum fraction digits
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {number} sourceNumber Number to convert to short number
 | 
					 * @param sourceNumber Number to convert to short number
 | 
				
			||||||
 * @returns {ShortNumber} Calculated short number
 | 
					 * @returns Calculated short number
 | 
				
			||||||
 * @example
 | 
					 * @example
 | 
				
			||||||
 * shortNumber(5936);
 | 
					 * shortNumber(5936);
 | 
				
			||||||
 * // => [5.936, 1000, 1]
 | 
					 * // => [5.936, 1000, 1]
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export type ShortNumber = [number, DecimalUnits, 0 | 1]; // Array of: shorten number, unit of shorten number and maximum fraction digits
 | 
					 | 
				
			||||||
export function toShortNumber(sourceNumber: number): ShortNumber {
 | 
					export function toShortNumber(sourceNumber: number): ShortNumber {
 | 
				
			||||||
  if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
 | 
					  if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
 | 
				
			||||||
    return [sourceNumber, DECIMAL_UNITS.ONE, 0];
 | 
					    return [sourceNumber, DECIMAL_UNITS.ONE, 0];
 | 
				
			||||||
| 
						 | 
					@ -45,9 +46,9 @@ export function toShortNumber(sourceNumber: number): ShortNumber {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {number} sourceNumber Original number that is shortened
 | 
					 * @param sourceNumber Original number that is shortened
 | 
				
			||||||
 * @param {number} division The scale in which short number is displayed
 | 
					 * @param division The scale in which short number is displayed
 | 
				
			||||||
 * @returns {number} Number that can be used for plurals when short form used
 | 
					 * @returns Number that can be used for plurals when short form used
 | 
				
			||||||
 * @example
 | 
					 * @example
 | 
				
			||||||
 * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
 | 
					 * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
 | 
				
			||||||
 * // => 1790
 | 
					 * // => 1790
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +0,0 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
module Settings
 | 
					 | 
				
			||||||
  module Extend
 | 
					 | 
				
			||||||
    def settings
 | 
					 | 
				
			||||||
      @settings ||= ScopedSettings.new(self)
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
| 
						 | 
					@ -123,7 +123,7 @@ class Account < ApplicationRecord
 | 
				
			||||||
  scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
 | 
					  scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
 | 
				
			||||||
  scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) }
 | 
					  scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) }
 | 
				
			||||||
  scope :popular, -> { order('account_stats.followers_count desc') }
 | 
					  scope :popular, -> { order('account_stats.followers_count desc') }
 | 
				
			||||||
  scope :by_domain_and_subdomains, ->(domain) { where(domain: Instance.by_domain_and_subdomain(domain).select(:domain)) }
 | 
					  scope :by_domain_and_subdomains, ->(domain) { where(domain: Instance.by_domain_and_subdomains(domain).select(:domain)) }
 | 
				
			||||||
  scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
 | 
					  scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
 | 
				
			||||||
  scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) }
 | 
					  scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@ class Instance < ApplicationRecord
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
 | 
					  scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
 | 
				
			||||||
  scope :by_domain_and_subdomain, ->(domain) { where("reverse('.' || domain) LIKE reverse(?)", "%.#{domain}") }
 | 
					  scope :by_domain_and_subdomains, ->(domain) { where("reverse('.' || domain) LIKE reverse(?)", "%.#{domain}") }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def self.refresh
 | 
					  def self.refresh
 | 
				
			||||||
    Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false)
 | 
					    Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,7 +19,7 @@ class FetchResourceService < BaseService
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def process(url, terminal = false)
 | 
					  def process(url, terminal: false)
 | 
				
			||||||
    @url = url
 | 
					    @url = url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    perform_request { |response| process_response(response, terminal) }
 | 
					    perform_request { |response| process_response(response, terminal) }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@ module.exports = (api) => {
 | 
				
			||||||
    modules: false,
 | 
					    modules: false,
 | 
				
			||||||
    debug: false,
 | 
					    debug: false,
 | 
				
			||||||
    include: [
 | 
					    include: [
 | 
				
			||||||
      'proposal-numeric-separator',
 | 
					      'transform-numeric-separator',
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,8 +24,8 @@ module.exports = (api) => {
 | 
				
			||||||
    plugins: [
 | 
					    plugins: [
 | 
				
			||||||
      ['react-intl', { messagesDir: './build/messages' }],
 | 
					      ['react-intl', { messagesDir: './build/messages' }],
 | 
				
			||||||
      'preval',
 | 
					      'preval',
 | 
				
			||||||
      '@babel/plugin-proposal-optional-chaining',
 | 
					      '@babel/plugin-transform-optional-chaining',
 | 
				
			||||||
      '@babel/plugin-proposal-nullish-coalescing-operator',
 | 
					      '@babel/plugin-transform-nullish-coalescing-operator',
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    overrides: [
 | 
					    overrides: [
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -113,12 +113,7 @@ module Mastodon::CLI
 | 
				
			||||||
        say('OK', :green)
 | 
					        say('OK', :green)
 | 
				
			||||||
        say("New password: #{password}")
 | 
					        say("New password: #{password}")
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
        user.errors.each do |error|
 | 
					        report_errors(user.errors)
 | 
				
			||||||
          say('Failure/Error: ', :red)
 | 
					 | 
				
			||||||
          say(error.attribute)
 | 
					 | 
				
			||||||
          say("    #{error.type}", :red)
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        exit(1)
 | 
					        exit(1)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
| 
						 | 
					@ -189,12 +184,7 @@ module Mastodon::CLI
 | 
				
			||||||
        say('OK', :green)
 | 
					        say('OK', :green)
 | 
				
			||||||
        say("New password: #{password}") if options[:reset_password]
 | 
					        say("New password: #{password}") if options[:reset_password]
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
        user.errors.each do |error|
 | 
					        report_errors(user.errors)
 | 
				
			||||||
          say('Failure/Error: ', :red)
 | 
					 | 
				
			||||||
          say(error.attribute)
 | 
					 | 
				
			||||||
          say("    #{error.type}", :red)
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        exit(1)
 | 
					        exit(1)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
| 
						 | 
					@ -217,7 +207,6 @@ module Mastodon::CLI
 | 
				
			||||||
        exit(1)
 | 
					        exit(1)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
 | 
					 | 
				
			||||||
      account = nil
 | 
					      account = nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if username.present?
 | 
					      if username.present?
 | 
				
			||||||
| 
						 | 
					@ -234,9 +223,9 @@ module Mastodon::CLI
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      say("Deleting user with #{account.statuses_count} statuses, this might take a while...#{dry_run}")
 | 
					      say("Deleting user with #{account.statuses_count} statuses, this might take a while...#{dry_run_mode_suffix}")
 | 
				
			||||||
      DeleteAccountService.new.call(account, reserve_email: false) unless options[:dry_run]
 | 
					      DeleteAccountService.new.call(account, reserve_email: false) unless dry_run?
 | 
				
			||||||
      say("OK#{dry_run}", :green)
 | 
					      say("OK#{dry_run_mode_suffix}", :green)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    option :force, type: :boolean, aliases: [:f], description: 'Override public key check'
 | 
					    option :force, type: :boolean, aliases: [:f], description: 'Override public key check'
 | 
				
			||||||
| 
						 | 
					@ -291,7 +280,7 @@ module Mastodon::CLI
 | 
				
			||||||
      Account.remote.select(:uri, 'count(*)').group(:uri).having('count(*) > 1').pluck(:uri).each do |uri|
 | 
					      Account.remote.select(:uri, 'count(*)').group(:uri).having('count(*) > 1').pluck(:uri).each do |uri|
 | 
				
			||||||
        say("Duplicates found for #{uri}")
 | 
					        say("Duplicates found for #{uri}")
 | 
				
			||||||
        begin
 | 
					        begin
 | 
				
			||||||
          ActivityPub::FetchRemoteAccountService.new.call(uri) unless options[:dry_run]
 | 
					          ActivityPub::FetchRemoteAccountService.new.call(uri) unless dry_run?
 | 
				
			||||||
        rescue => e
 | 
					        rescue => e
 | 
				
			||||||
          say("Error processing #{uri}: #{e}", :red)
 | 
					          say("Error processing #{uri}: #{e}", :red)
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
| 
						 | 
					@ -332,7 +321,6 @@ module Mastodon::CLI
 | 
				
			||||||
    LONG_DESC
 | 
					    LONG_DESC
 | 
				
			||||||
    def cull(*domains)
 | 
					    def cull(*domains)
 | 
				
			||||||
      skip_threshold = 7.days.ago
 | 
					      skip_threshold = 7.days.ago
 | 
				
			||||||
      dry_run        = options[:dry_run] ? ' (DRY RUN)' : ''
 | 
					 | 
				
			||||||
      skip_domains   = Concurrent::Set.new
 | 
					      skip_domains   = Concurrent::Set.new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      query = Account.remote.where(protocol: :activitypub)
 | 
					      query = Account.remote.where(protocol: :activitypub)
 | 
				
			||||||
| 
						 | 
					@ -350,7 +338,7 @@ module Mastodon::CLI
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if [404, 410].include?(code)
 | 
					        if [404, 410].include?(code)
 | 
				
			||||||
          DeleteAccountService.new.call(account, reserve_username: false) unless options[:dry_run]
 | 
					          DeleteAccountService.new.call(account, reserve_username: false) unless dry_run?
 | 
				
			||||||
          1
 | 
					          1
 | 
				
			||||||
        else
 | 
					        else
 | 
				
			||||||
          # Touch account even during dry run to avoid getting the account into the window again
 | 
					          # Touch account even during dry run to avoid getting the account into the window again
 | 
				
			||||||
| 
						 | 
					@ -358,7 +346,7 @@ module Mastodon::CLI
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      say("Visited #{processed} accounts, removed #{culled}#{dry_run}", :green)
 | 
					      say("Visited #{processed} accounts, removed #{culled}#{dry_run_mode_suffix}", :green)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      unless skip_domains.empty?
 | 
					      unless skip_domains.empty?
 | 
				
			||||||
        say('The following domains were not available during the check:', :yellow)
 | 
					        say('The following domains were not available during the check:', :yellow)
 | 
				
			||||||
| 
						 | 
					@ -381,21 +369,19 @@ module Mastodon::CLI
 | 
				
			||||||
      specified with space-separated USERNAMES.
 | 
					      specified with space-separated USERNAMES.
 | 
				
			||||||
    LONG_DESC
 | 
					    LONG_DESC
 | 
				
			||||||
    def refresh(*usernames)
 | 
					    def refresh(*usernames)
 | 
				
			||||||
      dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if options[:domain] || options[:all]
 | 
					      if options[:domain] || options[:all]
 | 
				
			||||||
        scope  = Account.remote
 | 
					        scope  = Account.remote
 | 
				
			||||||
        scope  = scope.where(domain: options[:domain]) if options[:domain]
 | 
					        scope  = scope.where(domain: options[:domain]) if options[:domain]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        processed, = parallelize_with_progress(scope) do |account|
 | 
					        processed, = parallelize_with_progress(scope) do |account|
 | 
				
			||||||
          next if options[:dry_run]
 | 
					          next if dry_run?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          account.reset_avatar!
 | 
					          account.reset_avatar!
 | 
				
			||||||
          account.reset_header!
 | 
					          account.reset_header!
 | 
				
			||||||
          account.save
 | 
					          account.save
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        say("Refreshed #{processed} accounts#{dry_run}", :green, true)
 | 
					        say("Refreshed #{processed} accounts#{dry_run_mode_suffix}", :green, true)
 | 
				
			||||||
      elsif !usernames.empty?
 | 
					      elsif !usernames.empty?
 | 
				
			||||||
        usernames.each do |user|
 | 
					        usernames.each do |user|
 | 
				
			||||||
          user, domain = user.split('@')
 | 
					          user, domain = user.split('@')
 | 
				
			||||||
| 
						 | 
					@ -406,7 +392,7 @@ module Mastodon::CLI
 | 
				
			||||||
            exit(1)
 | 
					            exit(1)
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          next if options[:dry_run]
 | 
					          next if dry_run?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          begin
 | 
					          begin
 | 
				
			||||||
            account.reset_avatar!
 | 
					            account.reset_avatar!
 | 
				
			||||||
| 
						 | 
					@ -417,7 +403,7 @@ module Mastodon::CLI
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        say("OK#{dry_run}", :green)
 | 
					        say("OK#{dry_run_mode_suffix}", :green)
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
        say('No account(s) given', :red)
 | 
					        say('No account(s) given', :red)
 | 
				
			||||||
        exit(1)
 | 
					        exit(1)
 | 
				
			||||||
| 
						 | 
					@ -568,8 +554,6 @@ module Mastodon::CLI
 | 
				
			||||||
      - not muted/blocked by us
 | 
					      - not muted/blocked by us
 | 
				
			||||||
    LONG_DESC
 | 
					    LONG_DESC
 | 
				
			||||||
    def prune
 | 
					    def prune
 | 
				
			||||||
      dry_run = options[:dry_run] ? ' (dry run)' : ''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      query = Account.remote.where.not(actor_type: %i(Application Service))
 | 
					      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 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 favourites WHERE account_id = accounts.id)')
 | 
				
			||||||
| 
						 | 
					@ -585,11 +569,11 @@ module Mastodon::CLI
 | 
				
			||||||
        next if account.suspended?
 | 
					        next if account.suspended?
 | 
				
			||||||
        next if account.silenced?
 | 
					        next if account.silenced?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        account.destroy unless options[:dry_run]
 | 
					        account.destroy unless dry_run?
 | 
				
			||||||
        1
 | 
					        1
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      say("OK, pruned #{deleted} accounts#{dry_run}", :green)
 | 
					      say("OK, pruned #{deleted} accounts#{dry_run_mode_suffix}", :green)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    option :force, type: :boolean
 | 
					    option :force, type: :boolean
 | 
				
			||||||
| 
						 | 
					@ -667,6 +651,14 @@ module Mastodon::CLI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private
 | 
					    private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def report_errors(errors)
 | 
				
			||||||
 | 
					      errors.each do |error|
 | 
				
			||||||
 | 
					        say('Failure/Error: ', :red)
 | 
				
			||||||
 | 
					        say(error.attribute)
 | 
				
			||||||
 | 
					        say("    #{error.type}", :red)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def rotate_keys_for_account(account, delay = 0)
 | 
					    def rotate_keys_for_account(account, delay = 0)
 | 
				
			||||||
      if account.nil?
 | 
					      if account.nil?
 | 
				
			||||||
        say('No such account', :red)
 | 
					        say('No such account', :red)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,6 @@ module Mastodon::CLI
 | 
				
			||||||
      When the --purge-domain-blocks option is given, also purge matching domain blocks.
 | 
					      When the --purge-domain-blocks option is given, also purge matching domain blocks.
 | 
				
			||||||
    LONG_DESC
 | 
					    LONG_DESC
 | 
				
			||||||
    def purge(*domains)
 | 
					    def purge(*domains)
 | 
				
			||||||
      dry_run            = options[:dry_run] ? ' (DRY RUN)' : ''
 | 
					 | 
				
			||||||
      domains            = domains.map { |domain| TagManager.instance.normalize_domain(domain) }
 | 
					      domains            = domains.map { |domain| TagManager.instance.normalize_domain(domain) }
 | 
				
			||||||
      account_scope      = Account.none
 | 
					      account_scope      = Account.none
 | 
				
			||||||
      domain_block_scope = DomainBlock.none
 | 
					      domain_block_scope = DomainBlock.none
 | 
				
			||||||
| 
						 | 
					@ -79,23 +78,23 @@ module Mastodon::CLI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      # Actually perform the deletions
 | 
					      # Actually perform the deletions
 | 
				
			||||||
      processed, = parallelize_with_progress(account_scope) do |account|
 | 
					      processed, = parallelize_with_progress(account_scope) do |account|
 | 
				
			||||||
        DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless options[:dry_run]
 | 
					        DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless dry_run?
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      say("Removed #{processed} accounts#{dry_run}", :green)
 | 
					      say("Removed #{processed} accounts#{dry_run_mode_suffix}", :green)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if options[:purge_domain_blocks]
 | 
					      if options[:purge_domain_blocks]
 | 
				
			||||||
        domain_block_count = domain_block_scope.count
 | 
					        domain_block_count = domain_block_scope.count
 | 
				
			||||||
        domain_block_scope.in_batches.destroy_all unless options[:dry_run]
 | 
					        domain_block_scope.in_batches.destroy_all unless dry_run?
 | 
				
			||||||
        say("Removed #{domain_block_count} domain blocks#{dry_run}", :green)
 | 
					        say("Removed #{domain_block_count} domain blocks#{dry_run_mode_suffix}", :green)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      custom_emojis_count = emoji_scope.count
 | 
					      custom_emojis_count = emoji_scope.count
 | 
				
			||||||
      emoji_scope.in_batches.destroy_all unless options[:dry_run]
 | 
					      emoji_scope.in_batches.destroy_all unless dry_run?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Instance.refresh unless options[:dry_run]
 | 
					      Instance.refresh unless dry_run?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      say("Removed #{custom_emojis_count} custom emojis#{dry_run}", :green)
 | 
					      say("Removed #{custom_emojis_count} custom emojis#{dry_run_mode_suffix}", :green)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    option :concurrency, type: :numeric, default: 50, aliases: [:c]
 | 
					    option :concurrency, type: :numeric, default: 50, aliases: [:c]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,7 +39,7 @@ module Mastodon::CLI
 | 
				
			||||||
      processed = 0
 | 
					      processed = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      domains.each do |domain|
 | 
					      domains.each do |domain|
 | 
				
			||||||
        if EmailDomainBlock.where(domain: domain).exists?
 | 
					        if EmailDomainBlock.exists?(domain: domain)
 | 
				
			||||||
          say("#{domain} is already blocked.", :yellow)
 | 
					          say("#{domain} is already blocked.", :yellow)
 | 
				
			||||||
          skipped += 1
 | 
					          skipped += 1
 | 
				
			||||||
          next
 | 
					          next
 | 
				
			||||||
| 
						 | 
					@ -60,7 +60,7 @@ module Mastodon::CLI
 | 
				
			||||||
        (email_domain_block.other_domains || []).uniq.each do |hostname|
 | 
					        (email_domain_block.other_domains || []).uniq.each do |hostname|
 | 
				
			||||||
          another_email_domain_block = EmailDomainBlock.new(domain: hostname, parent: email_domain_block)
 | 
					          another_email_domain_block = EmailDomainBlock.new(domain: hostname, parent: email_domain_block)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if EmailDomainBlock.where(domain: hostname).exists?
 | 
					          if EmailDomainBlock.exists?(domain: hostname)
 | 
				
			||||||
            say("#{hostname} is already blocked.", :yellow)
 | 
					            say("#{hostname} is already blocked.", :yellow)
 | 
				
			||||||
            skipped += 1
 | 
					            skipped += 1
 | 
				
			||||||
            next
 | 
					            next
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,14 +18,12 @@ module Mastodon::CLI
 | 
				
			||||||
      Otherwise, a single user specified by USERNAME.
 | 
					      Otherwise, a single user specified by USERNAME.
 | 
				
			||||||
    LONG_DESC
 | 
					    LONG_DESC
 | 
				
			||||||
    def build(username = nil)
 | 
					    def build(username = nil)
 | 
				
			||||||
      dry_run = options[:dry_run] ? '(DRY RUN)' : ''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if options[:all] || username.nil?
 | 
					      if options[:all] || username.nil?
 | 
				
			||||||
        processed, = parallelize_with_progress(Account.joins(:user).merge(User.active)) do |account|
 | 
					        processed, = parallelize_with_progress(Account.joins(:user).merge(User.active)) do |account|
 | 
				
			||||||
          PrecomputeFeedService.new.call(account) unless options[:dry_run]
 | 
					          PrecomputeFeedService.new.call(account) unless dry_run?
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        say("Regenerated feeds for #{processed} accounts #{dry_run}", :green, true)
 | 
					        say("Regenerated feeds for #{processed} accounts #{dry_run_mode_suffix}", :green, true)
 | 
				
			||||||
      elsif username.present?
 | 
					      elsif username.present?
 | 
				
			||||||
        account = Account.find_local(username)
 | 
					        account = Account.find_local(username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,9 +32,9 @@ module Mastodon::CLI
 | 
				
			||||||
          exit(1)
 | 
					          exit(1)
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        PrecomputeFeedService.new.call(account) unless options[:dry_run]
 | 
					        PrecomputeFeedService.new.call(account) unless dry_run?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        say("OK #{dry_run}", :green, true)
 | 
					        say("OK #{dry_run_mode_suffix}", :green, true)
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
        say('No account(s) given', :red)
 | 
					        say('No account(s) given', :red)
 | 
				
			||||||
        exit(1)
 | 
					        exit(1)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,10 @@ module Mastodon::CLI
 | 
				
			||||||
      options[:dry_run]
 | 
					      options[:dry_run]
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def dry_run_mode_suffix
 | 
				
			||||||
 | 
					      dry_run? ? ' (DRY RUN)' : ''
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create_progress_bar(total = nil)
 | 
					    def create_progress_bar(total = nil)
 | 
				
			||||||
      ProgressBar.create(total: total, format: '%c/%u |%b%i| %e')
 | 
					      ProgressBar.create(total: total, format: '%c/%u |%b%i| %e')
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -94,7 +94,7 @@ module Mastodon::CLI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain
 | 
					      exit(1) unless prompt.ask('Type in the domain of the server to confirm:', required: true) == Rails.configuration.x.local_domain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      unless options[:dry_run]
 | 
					      unless dry_run?
 | 
				
			||||||
        prompt.warn('This operation WILL NOT be reversible. It can also take a long time.')
 | 
					        prompt.warn('This operation WILL NOT be reversible. It can also take a long time.')
 | 
				
			||||||
        prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.')
 | 
					        prompt.warn('While the data won\'t be erased locally, the server will be in a BROKEN STATE afterwards.')
 | 
				
			||||||
        prompt.warn('A running Sidekiq process is required. Do not shut it down until queues clear.')
 | 
					        prompt.warn('A running Sidekiq process is required. Do not shut it down until queues clear.')
 | 
				
			||||||
| 
						 | 
					@ -104,12 +104,11 @@ module Mastodon::CLI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      inboxes   = Account.inboxes
 | 
					      inboxes   = Account.inboxes
 | 
				
			||||||
      processed = 0
 | 
					      processed = 0
 | 
				
			||||||
      dry_run   = options[:dry_run] ? ' (DRY RUN)' : ''
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Setting.registrations_mode = 'none' unless options[:dry_run]
 | 
					      Setting.registrations_mode = 'none' unless dry_run?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if inboxes.empty?
 | 
					      if inboxes.empty?
 | 
				
			||||||
        Account.local.without_suspended.in_batches.update_all(suspended_at: Time.now.utc, suspension_origin: :local) unless options[:dry_run]
 | 
					        Account.local.without_suspended.in_batches.update_all(suspended_at: Time.now.utc, suspension_origin: :local) unless dry_run?
 | 
				
			||||||
        prompt.ok('It seems like your server has not federated with anything')
 | 
					        prompt.ok('It seems like your server has not federated with anything')
 | 
				
			||||||
        prompt.ok('You can shut it down and delete it any time')
 | 
					        prompt.ok('You can shut it down and delete it any time')
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
| 
						 | 
					@ -126,7 +125,7 @@ module Mastodon::CLI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(account))
 | 
					        json = Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(account))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        unless options[:dry_run]
 | 
					        unless dry_run?
 | 
				
			||||||
          ActivityPub::DeliveryWorker.push_bulk(inboxes, limit: 1_000) do |inbox_url|
 | 
					          ActivityPub::DeliveryWorker.push_bulk(inboxes, limit: 1_000) do |inbox_url|
 | 
				
			||||||
            [json, account.id, inbox_url]
 | 
					            [json, account.id, inbox_url]
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
| 
						 | 
					@ -140,7 +139,7 @@ module Mastodon::CLI
 | 
				
			||||||
      Account.local.without_suspended.find_each { |account| delete_account.call(account) }
 | 
					      Account.local.without_suspended.find_each { |account| delete_account.call(account) }
 | 
				
			||||||
      Account.local.suspended.joins(:deletion_request).find_each { |account| delete_account.call(account) }
 | 
					      Account.local.suspended.joins(:deletion_request).find_each { |account| delete_account.call(account) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      prompt.ok("Queued #{inboxes.size * processed} items into Sidekiq for #{processed} accounts#{dry_run}")
 | 
					      prompt.ok("Queued #{inboxes.size * processed} items into Sidekiq for #{processed} accounts#{dry_run_mode_suffix}")
 | 
				
			||||||
      prompt.ok('Wait until Sidekiq processes all items, then you can shut everything down and delete the data')
 | 
					      prompt.ok('Wait until Sidekiq processes all items, then you can shut everything down and delete the data')
 | 
				
			||||||
    rescue TTY::Reader::InputInterrupt
 | 
					    rescue TTY::Reader::InputInterrupt
 | 
				
			||||||
      exit(1)
 | 
					      exit(1)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,5 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require 'tty-prompt'
 | 
					 | 
				
			||||||
require_relative 'base'
 | 
					require_relative 'base'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module Mastodon::CLI
 | 
					module Mastodon::CLI
 | 
				
			||||||
| 
						 | 
					@ -134,25 +133,23 @@ module Mastodon::CLI
 | 
				
			||||||
      Mastodon has to be stopped to run this task, which will take a long time and may be destructive.
 | 
					      Mastodon has to be stopped to run this task, which will take a long time and may be destructive.
 | 
				
			||||||
    LONG_DESC
 | 
					    LONG_DESC
 | 
				
			||||||
    def fix_duplicates
 | 
					    def fix_duplicates
 | 
				
			||||||
      @prompt = TTY::Prompt.new
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if ActiveRecord::Migrator.current_version < MIN_SUPPORTED_VERSION
 | 
					      if ActiveRecord::Migrator.current_version < MIN_SUPPORTED_VERSION
 | 
				
			||||||
        @prompt.error 'Your version of the database schema is too old and is not supported by this script.'
 | 
					        say 'Your version of the database schema is too old and is not supported by this script.', :red
 | 
				
			||||||
        @prompt.error 'Please update to at least Mastodon 3.0.0 before running this script.'
 | 
					        say 'Please update to at least Mastodon 3.0.0 before running this script.', :red
 | 
				
			||||||
        exit(1)
 | 
					        exit(1)
 | 
				
			||||||
      elsif ActiveRecord::Migrator.current_version > MAX_SUPPORTED_VERSION
 | 
					      elsif ActiveRecord::Migrator.current_version > MAX_SUPPORTED_VERSION
 | 
				
			||||||
        @prompt.warn 'Your version of the database schema is more recent than this script, this may cause unexpected errors.'
 | 
					        say 'Your version of the database schema is more recent than this script, this may cause unexpected errors.', :yellow
 | 
				
			||||||
        exit(1) unless @prompt.yes?('Continue anyway? (Yes/No)')
 | 
					        exit(1) unless yes?('Continue anyway? (Yes/No)')
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if Sidekiq::ProcessSet.new.any?
 | 
					      if Sidekiq::ProcessSet.new.any?
 | 
				
			||||||
        @prompt.error 'It seems Sidekiq is running. All Mastodon processes need to be stopped when using this script.'
 | 
					        say 'It seems Sidekiq is running. All Mastodon processes need to be stopped when using this script.', :red
 | 
				
			||||||
        exit(1)
 | 
					        exit(1)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.warn 'This task will take a long time to run and is potentially destructive.'
 | 
					      say 'This task will take a long time to run and is potentially destructive.', :yellow
 | 
				
			||||||
      @prompt.warn 'Please make sure to stop Mastodon and have a backup.'
 | 
					      say 'Please make sure to stop Mastodon and have a backup.', :yellow
 | 
				
			||||||
      exit(1) unless @prompt.yes?('Continue? (Yes/No)')
 | 
					      exit(1) unless yes?('Continue? (Yes/No)')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      deduplicate_users!
 | 
					      deduplicate_users!
 | 
				
			||||||
      deduplicate_account_domain_blocks!
 | 
					      deduplicate_account_domain_blocks!
 | 
				
			||||||
| 
						 | 
					@ -176,7 +173,7 @@ module Mastodon::CLI
 | 
				
			||||||
      Scenic.database.refresh_materialized_view('instances', concurrently: true, cascade: false) if ActiveRecord::Migrator.current_version >= 2020_12_06_004238
 | 
					      Scenic.database.refresh_materialized_view('instances', concurrently: true, cascade: false) if ActiveRecord::Migrator.current_version >= 2020_12_06_004238
 | 
				
			||||||
      Rails.cache.clear
 | 
					      Rails.cache.clear
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Finished!'
 | 
					      say 'Finished!'
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private
 | 
					    private
 | 
				
			||||||
| 
						 | 
					@ -184,7 +181,7 @@ module Mastodon::CLI
 | 
				
			||||||
    def deduplicate_accounts!
 | 
					    def deduplicate_accounts!
 | 
				
			||||||
      remove_index_if_exists!(:accounts, 'index_accounts_on_username_and_domain_lower')
 | 
					      remove_index_if_exists!(:accounts, 'index_accounts_on_username_and_domain_lower')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating accounts… for local accounts, you will be asked to chose which account to keep unchanged.'
 | 
					      say 'Deduplicating accounts… for local accounts, you will be asked to chose which account to keep unchanged.'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      find_duplicate_accounts.each do |row|
 | 
					      find_duplicate_accounts.each do |row|
 | 
				
			||||||
        accounts = Account.where(id: row['ids'].split(',')).to_a
 | 
					        accounts = Account.where(id: row['ids'].split(',')).to_a
 | 
				
			||||||
| 
						 | 
					@ -196,14 +193,14 @@ module Mastodon::CLI
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring index_accounts_on_username_and_domain_lower…'
 | 
					      say 'Restoring index_accounts_on_username_and_domain_lower…'
 | 
				
			||||||
      if ActiveRecord::Migrator.current_version < 2020_06_20_164023
 | 
					      if ActiveRecord::Migrator.current_version < 2020_06_20_164023
 | 
				
			||||||
        ActiveRecord::Base.connection.add_index :accounts, 'lower (username), lower(domain)', name: 'index_accounts_on_username_and_domain_lower', unique: true
 | 
					        ActiveRecord::Base.connection.add_index :accounts, 'lower (username), lower(domain)', name: 'index_accounts_on_username_and_domain_lower', unique: true
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
        ActiveRecord::Base.connection.add_index :accounts, "lower (username), COALESCE(lower(domain), '')", name: 'index_accounts_on_username_and_domain_lower', unique: true
 | 
					        ActiveRecord::Base.connection.add_index :accounts, "lower (username), COALESCE(lower(domain), '')", name: 'index_accounts_on_username_and_domain_lower', unique: true
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Reindexing textual indexes on accounts…'
 | 
					      say 'Reindexing textual indexes on accounts…'
 | 
				
			||||||
      ActiveRecord::Base.connection.execute('REINDEX INDEX search_index;')
 | 
					      ActiveRecord::Base.connection.execute('REINDEX INDEX search_index;')
 | 
				
			||||||
      ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_uri;')
 | 
					      ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_uri;')
 | 
				
			||||||
      ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_url;')
 | 
					      ActiveRecord::Base.connection.execute('REINDEX INDEX index_accounts_on_url;')
 | 
				
			||||||
| 
						 | 
					@ -215,19 +212,18 @@ module Mastodon::CLI
 | 
				
			||||||
      remove_index_if_exists!(:users, 'index_users_on_remember_token')
 | 
					      remove_index_if_exists!(:users, 'index_users_on_remember_token')
 | 
				
			||||||
      remove_index_if_exists!(:users, 'index_users_on_reset_password_token')
 | 
					      remove_index_if_exists!(:users, 'index_users_on_reset_password_token')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating user records…'
 | 
					      say 'Deduplicating user records…'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      # Deduplicating email
 | 
					      # Deduplicating email
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users GROUP BY email HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users GROUP BY email HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse
 | 
					        users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse
 | 
				
			||||||
        ref_user = users.shift
 | 
					        ref_user = users.shift
 | 
				
			||||||
        @prompt.warn "Multiple users registered with e-mail address #{ref_user.email}."
 | 
					        say "Multiple users registered with e-mail address #{ref_user.email}.", :yellow
 | 
				
			||||||
        @prompt.warn "e-mail will be disabled for the following accounts: #{user.map(&:account).map(&:acct).join(', ')}"
 | 
					        say "e-mail will be disabled for the following accounts: #{user.map(&:account).map(&:acct).join(', ')}", :yellow
 | 
				
			||||||
        @prompt.warn 'Please reach out to them and set another address with `tootctl account modify` or delete them.'
 | 
					        say 'Please reach out to them and set another address with `tootctl account modify` or delete them.', :yellow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        i = 0
 | 
					        users.each_with_index do |user, index|
 | 
				
			||||||
        users.each do |user|
 | 
					          user.update!(email: "#{index} " + user.email)
 | 
				
			||||||
          user.update!(email: "#{i} " + user.email)
 | 
					 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -235,7 +231,7 @@ module Mastodon::CLI
 | 
				
			||||||
      deduplicate_users_process_remember_token
 | 
					      deduplicate_users_process_remember_token
 | 
				
			||||||
      deduplicate_users_process_password_token
 | 
					      deduplicate_users_process_password_token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring users indexes…'
 | 
					      say 'Restoring users indexes…'
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :users, ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true
 | 
					      ActiveRecord::Base.connection.add_index :users, ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :users, ['email'], name: 'index_users_on_email', unique: true
 | 
					      ActiveRecord::Base.connection.add_index :users, ['email'], name: 'index_users_on_email', unique: true
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :users, ['remember_token'], name: 'index_users_on_remember_token', unique: true if ActiveRecord::Migrator.current_version < 2022_01_18_183010
 | 
					      ActiveRecord::Base.connection.add_index :users, ['remember_token'], name: 'index_users_on_remember_token', unique: true if ActiveRecord::Migrator.current_version < 2022_01_18_183010
 | 
				
			||||||
| 
						 | 
					@ -250,7 +246,7 @@ module Mastodon::CLI
 | 
				
			||||||
    def deduplicate_users_process_confirmation_token
 | 
					    def deduplicate_users_process_confirmation_token
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE confirmation_token IS NOT NULL GROUP BY confirmation_token HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE confirmation_token IS NOT NULL GROUP BY confirmation_token HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        users = User.where(id: row['ids'].split(',')).sort_by(&:created_at).reverse.drop(1)
 | 
					        users = User.where(id: row['ids'].split(',')).sort_by(&:created_at).reverse.drop(1)
 | 
				
			||||||
        @prompt.warn "Unsetting confirmation token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}"
 | 
					        say "Unsetting confirmation token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}", :yellow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        users.each do |user|
 | 
					        users.each do |user|
 | 
				
			||||||
          user.update!(confirmation_token: nil)
 | 
					          user.update!(confirmation_token: nil)
 | 
				
			||||||
| 
						 | 
					@ -262,7 +258,7 @@ module Mastodon::CLI
 | 
				
			||||||
      if ActiveRecord::Migrator.current_version < 2022_01_18_183010
 | 
					      if ActiveRecord::Migrator.current_version < 2022_01_18_183010
 | 
				
			||||||
        ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE remember_token IS NOT NULL GROUP BY remember_token HAVING count(*) > 1").each do |row|
 | 
					        ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE remember_token IS NOT NULL GROUP BY remember_token HAVING count(*) > 1").each do |row|
 | 
				
			||||||
          users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse.drop(1)
 | 
					          users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse.drop(1)
 | 
				
			||||||
          @prompt.warn "Unsetting remember token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}"
 | 
					          say "Unsetting remember token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}", :yellow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          users.each do |user|
 | 
					          users.each do |user|
 | 
				
			||||||
            user.update!(remember_token: nil)
 | 
					            user.update!(remember_token: nil)
 | 
				
			||||||
| 
						 | 
					@ -274,7 +270,7 @@ module Mastodon::CLI
 | 
				
			||||||
    def deduplicate_users_process_password_token
 | 
					    def deduplicate_users_process_password_token
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE reset_password_token IS NOT NULL GROUP BY reset_password_token HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM users WHERE reset_password_token IS NOT NULL GROUP BY reset_password_token HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse.drop(1)
 | 
					        users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse.drop(1)
 | 
				
			||||||
        @prompt.warn "Unsetting password reset token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}"
 | 
					        say "Unsetting password reset token for those accounts: #{users.map(&:account).map(&:acct).join(', ')}", :yellow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        users.each do |user|
 | 
					        users.each do |user|
 | 
				
			||||||
          user.update!(reset_password_token: nil)
 | 
					          user.update!(reset_password_token: nil)
 | 
				
			||||||
| 
						 | 
					@ -285,12 +281,12 @@ module Mastodon::CLI
 | 
				
			||||||
    def deduplicate_account_domain_blocks!
 | 
					    def deduplicate_account_domain_blocks!
 | 
				
			||||||
      remove_index_if_exists!(:account_domain_blocks, 'index_account_domain_blocks_on_account_id_and_domain')
 | 
					      remove_index_if_exists!(:account_domain_blocks, 'index_account_domain_blocks_on_account_id_and_domain')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Removing duplicate account domain blocks…'
 | 
					      say 'Removing duplicate account domain blocks…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_domain_blocks GROUP BY account_id, domain HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_domain_blocks GROUP BY account_id, domain HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        AccountDomainBlock.where(id: row['ids'].split(',').drop(1)).delete_all
 | 
					        AccountDomainBlock.where(id: row['ids'].split(',').drop(1)).delete_all
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring account domain blocks indexes…'
 | 
					      say 'Restoring account domain blocks indexes…'
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :account_domain_blocks, %w(account_id domain), name: 'index_account_domain_blocks_on_account_id_and_domain', unique: true
 | 
					      ActiveRecord::Base.connection.add_index :account_domain_blocks, %w(account_id domain), name: 'index_account_domain_blocks_on_account_id_and_domain', unique: true
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -299,12 +295,12 @@ module Mastodon::CLI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      remove_index_if_exists!(:account_identity_proofs, 'index_account_proofs_on_account_and_provider_and_username')
 | 
					      remove_index_if_exists!(:account_identity_proofs, 'index_account_proofs_on_account_and_provider_and_username')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Removing duplicate account identity proofs…'
 | 
					      say 'Removing duplicate account identity proofs…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_identity_proofs GROUP BY account_id, provider, provider_username HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM account_identity_proofs GROUP BY account_id, provider, provider_username HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        AccountIdentityProof.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
 | 
					        AccountIdentityProof.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring account identity proofs indexes…'
 | 
					      say 'Restoring account identity proofs indexes…'
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :account_identity_proofs, %w(account_id provider provider_username), name: 'index_account_proofs_on_account_and_provider_and_username', unique: true
 | 
					      ActiveRecord::Base.connection.add_index :account_identity_proofs, %w(account_id provider provider_username), name: 'index_account_proofs_on_account_and_provider_and_username', unique: true
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -313,19 +309,19 @@ module Mastodon::CLI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      remove_index_if_exists!(:announcement_reactions, 'index_announcement_reactions_on_account_id_and_announcement_id')
 | 
					      remove_index_if_exists!(:announcement_reactions, 'index_announcement_reactions_on_account_id_and_announcement_id')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Removing duplicate account identity proofs…'
 | 
					      say 'Removing duplicate account identity proofs…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM announcement_reactions GROUP BY account_id, announcement_id, name HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM announcement_reactions GROUP BY account_id, announcement_id, name HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        AnnouncementReaction.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
 | 
					        AnnouncementReaction.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring announcement_reactions indexes…'
 | 
					      say 'Restoring announcement_reactions indexes…'
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :announcement_reactions, %w(account_id announcement_id name), name: 'index_announcement_reactions_on_account_id_and_announcement_id', unique: true
 | 
					      ActiveRecord::Base.connection.add_index :announcement_reactions, %w(account_id announcement_id name), name: 'index_announcement_reactions_on_account_id_and_announcement_id', unique: true
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def deduplicate_conversations!
 | 
					    def deduplicate_conversations!
 | 
				
			||||||
      remove_index_if_exists!(:conversations, 'index_conversations_on_uri')
 | 
					      remove_index_if_exists!(:conversations, 'index_conversations_on_uri')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating conversations…'
 | 
					      say 'Deduplicating conversations…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM conversations WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM conversations WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        conversations = Conversation.where(id: row['ids'].split(',')).sort_by(&:id).reverse
 | 
					        conversations = Conversation.where(id: row['ids'].split(',')).sort_by(&:id).reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -337,7 +333,7 @@ module Mastodon::CLI
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring conversations indexes…'
 | 
					      say 'Restoring conversations indexes…'
 | 
				
			||||||
      if ActiveRecord::Migrator.current_version < 2022_03_07_083603
 | 
					      if ActiveRecord::Migrator.current_version < 2022_03_07_083603
 | 
				
			||||||
        ActiveRecord::Base.connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true
 | 
					        ActiveRecord::Base.connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
| 
						 | 
					@ -348,7 +344,7 @@ module Mastodon::CLI
 | 
				
			||||||
    def deduplicate_custom_emojis!
 | 
					    def deduplicate_custom_emojis!
 | 
				
			||||||
      remove_index_if_exists!(:custom_emojis, 'index_custom_emojis_on_shortcode_and_domain')
 | 
					      remove_index_if_exists!(:custom_emojis, 'index_custom_emojis_on_shortcode_and_domain')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating custom_emojis…'
 | 
					      say 'Deduplicating custom_emojis…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emojis GROUP BY shortcode, domain HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emojis GROUP BY shortcode, domain HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        emojis = CustomEmoji.where(id: row['ids'].split(',')).sort_by(&:id).reverse
 | 
					        emojis = CustomEmoji.where(id: row['ids'].split(',')).sort_by(&:id).reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -360,14 +356,14 @@ module Mastodon::CLI
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring custom_emojis indexes…'
 | 
					      say 'Restoring custom_emojis indexes…'
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :custom_emojis, %w(shortcode domain), name: 'index_custom_emojis_on_shortcode_and_domain', unique: true
 | 
					      ActiveRecord::Base.connection.add_index :custom_emojis, %w(shortcode domain), name: 'index_custom_emojis_on_shortcode_and_domain', unique: true
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def deduplicate_custom_emoji_categories!
 | 
					    def deduplicate_custom_emoji_categories!
 | 
				
			||||||
      remove_index_if_exists!(:custom_emoji_categories, 'index_custom_emoji_categories_on_name')
 | 
					      remove_index_if_exists!(:custom_emoji_categories, 'index_custom_emoji_categories_on_name')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating custom_emoji_categories…'
 | 
					      say 'Deduplicating custom_emoji_categories…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emoji_categories GROUP BY name HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM custom_emoji_categories GROUP BY name HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        categories = CustomEmojiCategory.where(id: row['ids'].split(',')).sort_by(&:id).reverse
 | 
					        categories = CustomEmojiCategory.where(id: row['ids'].split(',')).sort_by(&:id).reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -379,26 +375,26 @@ module Mastodon::CLI
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring custom_emoji_categories indexes…'
 | 
					      say 'Restoring custom_emoji_categories indexes…'
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :custom_emoji_categories, ['name'], name: 'index_custom_emoji_categories_on_name', unique: true
 | 
					      ActiveRecord::Base.connection.add_index :custom_emoji_categories, ['name'], name: 'index_custom_emoji_categories_on_name', unique: true
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def deduplicate_domain_allows!
 | 
					    def deduplicate_domain_allows!
 | 
				
			||||||
      remove_index_if_exists!(:domain_allows, 'index_domain_allows_on_domain')
 | 
					      remove_index_if_exists!(:domain_allows, 'index_domain_allows_on_domain')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating domain_allows…'
 | 
					      say 'Deduplicating domain_allows…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_allows GROUP BY domain HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_allows GROUP BY domain HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        DomainAllow.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
 | 
					        DomainAllow.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring domain_allows indexes…'
 | 
					      say 'Restoring domain_allows indexes…'
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :domain_allows, ['domain'], name: 'index_domain_allows_on_domain', unique: true
 | 
					      ActiveRecord::Base.connection.add_index :domain_allows, ['domain'], name: 'index_domain_allows_on_domain', unique: true
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def deduplicate_domain_blocks!
 | 
					    def deduplicate_domain_blocks!
 | 
				
			||||||
      remove_index_if_exists!(:domain_blocks, 'index_domain_blocks_on_domain')
 | 
					      remove_index_if_exists!(:domain_blocks, 'index_domain_blocks_on_domain')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating domain_allows…'
 | 
					      say 'Deduplicating domain_allows…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        domain_blocks = DomainBlock.where(id: row['ids'].split(',')).by_severity.reverse.to_a
 | 
					        domain_blocks = DomainBlock.where(id: row['ids'].split(',')).by_severity.reverse.to_a
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -415,7 +411,7 @@ module Mastodon::CLI
 | 
				
			||||||
        domain_blocks.each(&:destroy)
 | 
					        domain_blocks.each(&:destroy)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring domain_blocks indexes…'
 | 
					      say 'Restoring domain_blocks indexes…'
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :domain_blocks, ['domain'], name: 'index_domain_blocks_on_domain', unique: true
 | 
					      ActiveRecord::Base.connection.add_index :domain_blocks, ['domain'], name: 'index_domain_blocks_on_domain', unique: true
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -424,37 +420,37 @@ module Mastodon::CLI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      remove_index_if_exists!(:unavailable_domains, 'index_unavailable_domains_on_domain')
 | 
					      remove_index_if_exists!(:unavailable_domains, 'index_unavailable_domains_on_domain')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating unavailable_domains…'
 | 
					      say 'Deduplicating unavailable_domains…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM unavailable_domains GROUP BY domain HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM unavailable_domains GROUP BY domain HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        UnavailableDomain.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
 | 
					        UnavailableDomain.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring domain_allows indexes…'
 | 
					      say 'Restoring domain_allows indexes…'
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :unavailable_domains, ['domain'], name: 'index_unavailable_domains_on_domain', unique: true
 | 
					      ActiveRecord::Base.connection.add_index :unavailable_domains, ['domain'], name: 'index_unavailable_domains_on_domain', unique: true
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def deduplicate_email_domain_blocks!
 | 
					    def deduplicate_email_domain_blocks!
 | 
				
			||||||
      remove_index_if_exists!(:email_domain_blocks, 'index_email_domain_blocks_on_domain')
 | 
					      remove_index_if_exists!(:email_domain_blocks, 'index_email_domain_blocks_on_domain')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating email_domain_blocks…'
 | 
					      say 'Deduplicating email_domain_blocks…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM email_domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM email_domain_blocks GROUP BY domain HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        domain_blocks = EmailDomainBlock.where(id: row['ids'].split(',')).sort_by { |b| b.parent.nil? ? 1 : 0 }.to_a
 | 
					        domain_blocks = EmailDomainBlock.where(id: row['ids'].split(',')).sort_by { |b| b.parent.nil? ? 1 : 0 }.to_a
 | 
				
			||||||
        domain_blocks.drop(1).each(&:destroy)
 | 
					        domain_blocks.drop(1).each(&:destroy)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring email_domain_blocks indexes…'
 | 
					      say 'Restoring email_domain_blocks indexes…'
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :email_domain_blocks, ['domain'], name: 'index_email_domain_blocks_on_domain', unique: true
 | 
					      ActiveRecord::Base.connection.add_index :email_domain_blocks, ['domain'], name: 'index_email_domain_blocks_on_domain', unique: true
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def deduplicate_media_attachments!
 | 
					    def deduplicate_media_attachments!
 | 
				
			||||||
      remove_index_if_exists!(:media_attachments, 'index_media_attachments_on_shortcode')
 | 
					      remove_index_if_exists!(:media_attachments, 'index_media_attachments_on_shortcode')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating media_attachments…'
 | 
					      say 'Deduplicating media_attachments…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM media_attachments WHERE shortcode IS NOT NULL GROUP BY shortcode HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM media_attachments WHERE shortcode IS NOT NULL GROUP BY shortcode HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        MediaAttachment.where(id: row['ids'].split(',').drop(1)).update_all(shortcode: nil)
 | 
					        MediaAttachment.where(id: row['ids'].split(',').drop(1)).update_all(shortcode: nil)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring media_attachments indexes…'
 | 
					      say 'Restoring media_attachments indexes…'
 | 
				
			||||||
      if ActiveRecord::Migrator.current_version < 2022_03_10_060626
 | 
					      if ActiveRecord::Migrator.current_version < 2022_03_10_060626
 | 
				
			||||||
        ActiveRecord::Base.connection.add_index :media_attachments, ['shortcode'], name: 'index_media_attachments_on_shortcode', unique: true
 | 
					        ActiveRecord::Base.connection.add_index :media_attachments, ['shortcode'], name: 'index_media_attachments_on_shortcode', unique: true
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
| 
						 | 
					@ -465,19 +461,19 @@ module Mastodon::CLI
 | 
				
			||||||
    def deduplicate_preview_cards!
 | 
					    def deduplicate_preview_cards!
 | 
				
			||||||
      remove_index_if_exists!(:preview_cards, 'index_preview_cards_on_url')
 | 
					      remove_index_if_exists!(:preview_cards, 'index_preview_cards_on_url')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating preview_cards…'
 | 
					      say 'Deduplicating preview_cards…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM preview_cards GROUP BY url HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM preview_cards GROUP BY url HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        PreviewCard.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
 | 
					        PreviewCard.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring preview_cards indexes…'
 | 
					      say 'Restoring preview_cards indexes…'
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :preview_cards, ['url'], name: 'index_preview_cards_on_url', unique: true
 | 
					      ActiveRecord::Base.connection.add_index :preview_cards, ['url'], name: 'index_preview_cards_on_url', unique: true
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def deduplicate_statuses!
 | 
					    def deduplicate_statuses!
 | 
				
			||||||
      remove_index_if_exists!(:statuses, 'index_statuses_on_uri')
 | 
					      remove_index_if_exists!(:statuses, 'index_statuses_on_uri')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating statuses…'
 | 
					      say 'Deduplicating statuses…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM statuses WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM statuses WHERE uri IS NOT NULL GROUP BY uri HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        statuses = Status.where(id: row['ids'].split(',')).sort_by(&:id)
 | 
					        statuses = Status.where(id: row['ids'].split(',')).sort_by(&:id)
 | 
				
			||||||
        ref_status = statuses.shift
 | 
					        ref_status = statuses.shift
 | 
				
			||||||
| 
						 | 
					@ -487,7 +483,7 @@ module Mastodon::CLI
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring statuses indexes…'
 | 
					      say 'Restoring statuses indexes…'
 | 
				
			||||||
      if ActiveRecord::Migrator.current_version < 2022_03_10_060706
 | 
					      if ActiveRecord::Migrator.current_version < 2022_03_10_060706
 | 
				
			||||||
        ActiveRecord::Base.connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true
 | 
					        ActiveRecord::Base.connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
| 
						 | 
					@ -499,7 +495,7 @@ module Mastodon::CLI
 | 
				
			||||||
      remove_index_if_exists!(:tags, 'index_tags_on_name_lower')
 | 
					      remove_index_if_exists!(:tags, 'index_tags_on_name_lower')
 | 
				
			||||||
      remove_index_if_exists!(:tags, 'index_tags_on_name_lower_btree')
 | 
					      remove_index_if_exists!(:tags, 'index_tags_on_name_lower_btree')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating tags…'
 | 
					      say 'Deduplicating tags…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM tags GROUP BY lower((name)::text) HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM tags GROUP BY lower((name)::text) HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        tags = Tag.where(id: row['ids'].split(',')).sort_by { |t| [t.usable?, t.trendable?, t.listable?].count(false) }
 | 
					        tags = Tag.where(id: row['ids'].split(',')).sort_by { |t| [t.usable?, t.trendable?, t.listable?].count(false) }
 | 
				
			||||||
        ref_tag = tags.shift
 | 
					        ref_tag = tags.shift
 | 
				
			||||||
| 
						 | 
					@ -509,7 +505,7 @@ module Mastodon::CLI
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring tags indexes…'
 | 
					      say 'Restoring tags indexes…'
 | 
				
			||||||
      if ActiveRecord::Migrator.current_version < 2021_04_21_121431
 | 
					      if ActiveRecord::Migrator.current_version < 2021_04_21_121431
 | 
				
			||||||
        ActiveRecord::Base.connection.add_index :tags, 'lower((name)::text)', name: 'index_tags_on_name_lower', unique: true
 | 
					        ActiveRecord::Base.connection.add_index :tags, 'lower((name)::text)', name: 'index_tags_on_name_lower', unique: true
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
| 
						 | 
					@ -522,12 +518,12 @@ module Mastodon::CLI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      remove_index_if_exists!(:webauthn_credentials, 'index_webauthn_credentials_on_external_id')
 | 
					      remove_index_if_exists!(:webauthn_credentials, 'index_webauthn_credentials_on_external_id')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating webauthn_credentials…'
 | 
					      say 'Deduplicating webauthn_credentials…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webauthn_credentials GROUP BY external_id HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webauthn_credentials GROUP BY external_id HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        WebauthnCredential.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
 | 
					        WebauthnCredential.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring webauthn_credentials indexes…'
 | 
					      say 'Restoring webauthn_credentials indexes…'
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :webauthn_credentials, ['external_id'], name: 'index_webauthn_credentials_on_external_id', unique: true
 | 
					      ActiveRecord::Base.connection.add_index :webauthn_credentials, ['external_id'], name: 'index_webauthn_credentials_on_external_id', unique: true
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -536,28 +532,37 @@ module Mastodon::CLI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      remove_index_if_exists!(:webhooks, 'index_webhooks_on_url')
 | 
					      remove_index_if_exists!(:webhooks, 'index_webhooks_on_url')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Deduplicating webhooks…'
 | 
					      say 'Deduplicating webhooks…'
 | 
				
			||||||
      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webhooks GROUP BY url HAVING count(*) > 1").each do |row|
 | 
					      ActiveRecord::Base.connection.select_all("SELECT string_agg(id::text, ',') AS ids FROM webhooks GROUP BY url HAVING count(*) > 1").each do |row|
 | 
				
			||||||
        Webhooks.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
 | 
					        Webhooks.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Restoring webhooks indexes…'
 | 
					      say 'Restoring webhooks indexes…'
 | 
				
			||||||
      ActiveRecord::Base.connection.add_index :webhooks, ['url'], name: 'index_webhooks_on_url', unique: true
 | 
					      ActiveRecord::Base.connection.add_index :webhooks, ['url'], name: 'index_webhooks_on_url', unique: true
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def deduplicate_local_accounts!(accounts)
 | 
					    def deduplicate_local_accounts!(accounts)
 | 
				
			||||||
      accounts = accounts.sort_by(&:id).reverse
 | 
					      accounts = accounts.sort_by(&:id).reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.warn "Multiple local accounts were found for username '#{accounts.first.username}'."
 | 
					      say "Multiple local accounts were found for username '#{accounts.first.username}'.", :yellow
 | 
				
			||||||
      @prompt.warn 'All those accounts are distinct accounts but only the most recently-created one is fully-functional.'
 | 
					      say 'All those accounts are distinct accounts but only the most recently-created one is fully-functional.', :yellow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      accounts.each_with_index do |account, idx|
 | 
					      accounts.each_with_index do |account, idx|
 | 
				
			||||||
        @prompt.say format('%2d. %s: created at: %s; updated at: %s; last logged in at: %s; statuses: %5d; last status at: %s', idx, account.username, account.created_at, account.updated_at, account.user&.last_sign_in_at&.to_s || 'N/A', account.account_stat&.statuses_count || 0, account.account_stat&.last_status_at || 'N/A')
 | 
					        say format(
 | 
				
			||||||
 | 
					          '%<index>2d. %<username>s: created at: %<created_at>s; updated at: %<updated_at>s; last logged in at: %<last_log_in_at>s; statuses: %<status_count>5d; last status at: %<last_status_at>s',
 | 
				
			||||||
 | 
					          index: idx,
 | 
				
			||||||
 | 
					          username: account.username,
 | 
				
			||||||
 | 
					          created_at: account.created_at,
 | 
				
			||||||
 | 
					          updated_at: account.updated_at,
 | 
				
			||||||
 | 
					          last_log_in_at: account.user&.last_sign_in_at&.to_s || 'N/A',
 | 
				
			||||||
 | 
					          status_count: account.account_stat&.statuses_count || 0,
 | 
				
			||||||
 | 
					          last_status_at: account.account_stat&.last_status_at || 'N/A'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @prompt.say 'Please chose the one to keep unchanged, other ones will be automatically renamed.'
 | 
					      say 'Please chose the one to keep unchanged, other ones will be automatically renamed.'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      ref_id = @prompt.ask('Account to keep unchanged:') do |q|
 | 
					      ref_id = ask('Account to keep unchanged:') do |q|
 | 
				
			||||||
        q.required true
 | 
					        q.required true
 | 
				
			||||||
        q.default 0
 | 
					        q.default 0
 | 
				
			||||||
        q.convert :int
 | 
					        q.convert :int
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -35,12 +35,12 @@ module Mastodon::CLI
 | 
				
			||||||
        say('--prune-profiles and --remove-headers should not be specified simultaneously', :red, true)
 | 
					        say('--prune-profiles and --remove-headers should not be specified simultaneously', :red, true)
 | 
				
			||||||
        exit(1)
 | 
					        exit(1)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if options[:include_follows] && !(options[:prune_profiles] || options[:remove_headers])
 | 
					      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)
 | 
					        say('--include-follows can only be used with --prune-profiles or --remove-headers', :red, true)
 | 
				
			||||||
        exit(1)
 | 
					        exit(1)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
      time_ago        = options[:days].days.ago
 | 
					      time_ago = options[:days].days.ago
 | 
				
			||||||
      dry_run         = options[:dry_run] ? ' (DRY RUN)' : ''
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if options[:prune_profiles] || options[:remove_headers]
 | 
					      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|
 | 
					        processed, aggregate = parallelize_with_progress(Account.remote.where({ last_webfingered_at: ..time_ago, updated_at: ..time_ago })) do |account|
 | 
				
			||||||
| 
						 | 
					@ -51,7 +51,7 @@ module Mastodon::CLI
 | 
				
			||||||
          size = (account.header_file_size || 0)
 | 
					          size = (account.header_file_size || 0)
 | 
				
			||||||
          size += (account.avatar_file_size || 0) if options[:prune_profiles]
 | 
					          size += (account.avatar_file_size || 0) if options[:prune_profiles]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          unless options[:dry_run]
 | 
					          unless dry_run?
 | 
				
			||||||
            account.header.destroy
 | 
					            account.header.destroy
 | 
				
			||||||
            account.avatar.destroy if options[:prune_profiles]
 | 
					            account.avatar.destroy if options[:prune_profiles]
 | 
				
			||||||
            account.save!
 | 
					            account.save!
 | 
				
			||||||
| 
						 | 
					@ -60,7 +60,7 @@ module Mastodon::CLI
 | 
				
			||||||
          size
 | 
					          size
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        say("Visited #{processed} accounts and removed profile media totaling #{number_to_human_size(aggregate)}#{dry_run}", :green, true)
 | 
					        say("Visited #{processed} accounts and removed profile media totaling #{number_to_human_size(aggregate)}#{dry_run_mode_suffix}", :green, true)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      unless options[:prune_profiles] || options[:remove_headers]
 | 
					      unless options[:prune_profiles] || options[:remove_headers]
 | 
				
			||||||
| 
						 | 
					@ -69,7 +69,7 @@ module Mastodon::CLI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          size = (media_attachment.file_file_size || 0) + (media_attachment.thumbnail_file_size || 0)
 | 
					          size = (media_attachment.file_file_size || 0) + (media_attachment.thumbnail_file_size || 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          unless options[:dry_run]
 | 
					          unless dry_run?
 | 
				
			||||||
            media_attachment.file.destroy
 | 
					            media_attachment.file.destroy
 | 
				
			||||||
            media_attachment.thumbnail.destroy
 | 
					            media_attachment.thumbnail.destroy
 | 
				
			||||||
            media_attachment.save
 | 
					            media_attachment.save
 | 
				
			||||||
| 
						 | 
					@ -78,7 +78,7 @@ module Mastodon::CLI
 | 
				
			||||||
          size
 | 
					          size
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        say("Removed #{processed} media attachments (approx. #{number_to_human_size(aggregate)})#{dry_run}", :green, true)
 | 
					        say("Removed #{processed} media attachments (approx. #{number_to_human_size(aggregate)})#{dry_run_mode_suffix}", :green, true)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -97,7 +97,6 @@ module Mastodon::CLI
 | 
				
			||||||
      progress        = create_progress_bar(nil)
 | 
					      progress        = create_progress_bar(nil)
 | 
				
			||||||
      reclaimed_bytes = 0
 | 
					      reclaimed_bytes = 0
 | 
				
			||||||
      removed         = 0
 | 
					      removed         = 0
 | 
				
			||||||
      dry_run         = options[:dry_run] ? ' (DRY RUN)' : ''
 | 
					 | 
				
			||||||
      prefix          = options[:prefix]
 | 
					      prefix          = options[:prefix]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case Paperclip::Attachment.default_options[:storage]
 | 
					      case Paperclip::Attachment.default_options[:storage]
 | 
				
			||||||
| 
						 | 
					@ -123,7 +122,7 @@ module Mastodon::CLI
 | 
				
			||||||
          record_map = preload_records_from_mixed_objects(objects)
 | 
					          record_map = preload_records_from_mixed_objects(objects)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          objects.each do |object|
 | 
					          objects.each do |object|
 | 
				
			||||||
            object.acl.put(acl: s3_permissions) if options[:fix_permissions] && !options[:dry_run]
 | 
					            object.acl.put(acl: s3_permissions) if options[:fix_permissions] && !dry_run?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            path_segments = object.key.split('/')
 | 
					            path_segments = object.key.split('/')
 | 
				
			||||||
            path_segments.delete('cache')
 | 
					            path_segments.delete('cache')
 | 
				
			||||||
| 
						 | 
					@ -145,7 +144,7 @@ module Mastodon::CLI
 | 
				
			||||||
            next unless attachment.blank? || !attachment.variant?(file_name)
 | 
					            next unless attachment.blank? || !attachment.variant?(file_name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            begin
 | 
					            begin
 | 
				
			||||||
              object.delete unless options[:dry_run]
 | 
					              object.delete unless dry_run?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              reclaimed_bytes += object.size
 | 
					              reclaimed_bytes += object.size
 | 
				
			||||||
              removed += 1
 | 
					              removed += 1
 | 
				
			||||||
| 
						 | 
					@ -194,7 +193,7 @@ module Mastodon::CLI
 | 
				
			||||||
          begin
 | 
					          begin
 | 
				
			||||||
            size = File.size(path)
 | 
					            size = File.size(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            unless options[:dry_run]
 | 
					            unless dry_run?
 | 
				
			||||||
              File.delete(path)
 | 
					              File.delete(path)
 | 
				
			||||||
              begin
 | 
					              begin
 | 
				
			||||||
                FileUtils.rmdir(File.dirname(path), parents: true)
 | 
					                FileUtils.rmdir(File.dirname(path), parents: true)
 | 
				
			||||||
| 
						 | 
					@ -216,7 +215,7 @@ module Mastodon::CLI
 | 
				
			||||||
      progress.total = progress.progress
 | 
					      progress.total = progress.progress
 | 
				
			||||||
      progress.finish
 | 
					      progress.finish
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      say("Removed #{removed} orphans (approx. #{number_to_human_size(reclaimed_bytes)})#{dry_run}", :green, true)
 | 
					      say("Removed #{removed} orphans (approx. #{number_to_human_size(reclaimed_bytes)})#{dry_run_mode_suffix}", :green, true)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    option :account, type: :string
 | 
					    option :account, type: :string
 | 
				
			||||||
| 
						 | 
					@ -246,8 +245,6 @@ module Mastodon::CLI
 | 
				
			||||||
      not be re-downloaded. To force re-download of every URL, use --force.
 | 
					      not be re-downloaded. To force re-download of every URL, use --force.
 | 
				
			||||||
    DESC
 | 
					    DESC
 | 
				
			||||||
    def refresh
 | 
					    def refresh
 | 
				
			||||||
      dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if options[:status]
 | 
					      if options[:status]
 | 
				
			||||||
        scope = MediaAttachment.where(status_id: options[:status])
 | 
					        scope = MediaAttachment.where(status_id: options[:status])
 | 
				
			||||||
      elsif options[:account]
 | 
					      elsif options[:account]
 | 
				
			||||||
| 
						 | 
					@ -274,7 +271,7 @@ module Mastodon::CLI
 | 
				
			||||||
        next if media_attachment.remote_url.blank? || (!options[:force] && media_attachment.file_file_name.present?)
 | 
					        next if media_attachment.remote_url.blank? || (!options[:force] && media_attachment.file_file_name.present?)
 | 
				
			||||||
        next if DomainBlock.reject_media?(media_attachment.account.domain)
 | 
					        next if DomainBlock.reject_media?(media_attachment.account.domain)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        unless options[:dry_run]
 | 
					        unless dry_run?
 | 
				
			||||||
          media_attachment.reset_file!
 | 
					          media_attachment.reset_file!
 | 
				
			||||||
          media_attachment.reset_thumbnail!
 | 
					          media_attachment.reset_thumbnail!
 | 
				
			||||||
          media_attachment.save
 | 
					          media_attachment.save
 | 
				
			||||||
| 
						 | 
					@ -283,7 +280,7 @@ module Mastodon::CLI
 | 
				
			||||||
        media_attachment.file_file_size + (media_attachment.thumbnail_file_size || 0)
 | 
					        media_attachment.file_file_size + (media_attachment.thumbnail_file_size || 0)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      say("Downloaded #{processed} media attachments (approx. #{number_to_human_size(aggregate)})#{dry_run}", :green, true)
 | 
					      say("Downloaded #{processed} media attachments (approx. #{number_to_human_size(aggregate)})#{dry_run_mode_suffix}", :green, true)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    desc 'usage', 'Calculate disk space consumed by Mastodon'
 | 
					    desc 'usage', 'Calculate disk space consumed by Mastodon'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,6 @@ module Mastodon::CLI
 | 
				
			||||||
    DESC
 | 
					    DESC
 | 
				
			||||||
    def remove
 | 
					    def remove
 | 
				
			||||||
      time_ago = options[:days].days.ago
 | 
					      time_ago = options[:days].days.ago
 | 
				
			||||||
      dry_run  = options[:dry_run] ? ' (DRY RUN)' : ''
 | 
					 | 
				
			||||||
      link     = options[:link] ? 'link-type ' : ''
 | 
					      link     = options[:link] ? 'link-type ' : ''
 | 
				
			||||||
      scope    = PreviewCard.cached
 | 
					      scope    = PreviewCard.cached
 | 
				
			||||||
      scope    = scope.where(type: :link) if options[:link]
 | 
					      scope    = scope.where(type: :link) if options[:link]
 | 
				
			||||||
| 
						 | 
					@ -38,7 +37,7 @@ module Mastodon::CLI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        size = preview_card.image_file_size
 | 
					        size = preview_card.image_file_size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        unless options[:dry_run]
 | 
					        unless dry_run?
 | 
				
			||||||
          preview_card.image.destroy
 | 
					          preview_card.image.destroy
 | 
				
			||||||
          preview_card.save
 | 
					          preview_card.save
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
| 
						 | 
					@ -46,7 +45,7 @@ module Mastodon::CLI
 | 
				
			||||||
        size
 | 
					        size
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      say("Removed #{processed} #{link}preview cards (approx. #{number_to_human_size(aggregate)})#{dry_run}", :green, true)
 | 
					      say("Removed #{processed} #{link}preview cards (approx. #{number_to_human_size(aggregate)})#{dry_run_mode_suffix}", :green, true)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,6 @@ module Mastodon::CLI
 | 
				
			||||||
    LONG_DESC
 | 
					    LONG_DESC
 | 
				
			||||||
    def storage_schema
 | 
					    def storage_schema
 | 
				
			||||||
      progress = create_progress_bar(nil)
 | 
					      progress = create_progress_bar(nil)
 | 
				
			||||||
      dry_run  = dry_run? ? ' (DRY RUN)' : ''
 | 
					 | 
				
			||||||
      records  = 0
 | 
					      records  = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      klasses = [
 | 
					      klasses = [
 | 
				
			||||||
| 
						 | 
					@ -69,7 +68,7 @@ module Mastodon::CLI
 | 
				
			||||||
      progress.total = progress.progress
 | 
					      progress.total = progress.progress
 | 
				
			||||||
      progress.finish
 | 
					      progress.finish
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      say("Upgraded storage schema of #{records} records#{dry_run}", :green, true)
 | 
					      say("Upgraded storage schema of #{records} records#{dry_run_mode_suffix}", :green, true)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private
 | 
					    private
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										44
									
								
								package.json
								
								
								
								
							
							
						
						
									
										44
									
								
								package.json
								
								
								
								
							| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
  "name": "@mastodon/mastodon",
 | 
					  "name": "@mastodon/mastodon",
 | 
				
			||||||
  "license": "AGPL-3.0-or-later",
 | 
					  "license": "AGPL-3.0-or-later",
 | 
				
			||||||
  "engines": {
 | 
					  "engines": {
 | 
				
			||||||
    "node": ">=14"
 | 
					    "node": ">=16"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "postversion": "git push --tags",
 | 
					    "postversion": "git push --tags",
 | 
				
			||||||
| 
						 | 
					@ -26,14 +26,14 @@
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "private": true,
 | 
					  "private": true,
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@babel/core": "^7.21.8",
 | 
					    "@babel/core": "^7.22.1",
 | 
				
			||||||
    "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
 | 
					    "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.3",
 | 
				
			||||||
    "@babel/plugin-transform-react-inline-elements": "^7.21.0",
 | 
					    "@babel/plugin-transform-react-inline-elements": "^7.21.0",
 | 
				
			||||||
    "@babel/plugin-transform-runtime": "^7.21.4",
 | 
					    "@babel/plugin-transform-runtime": "^7.22.4",
 | 
				
			||||||
    "@babel/preset-env": "^7.21.5",
 | 
					    "@babel/preset-env": "^7.22.4",
 | 
				
			||||||
    "@babel/preset-react": "^7.18.6",
 | 
					    "@babel/preset-react": "^7.22.3",
 | 
				
			||||||
    "@babel/preset-typescript": "^7.21.5",
 | 
					    "@babel/preset-typescript": "^7.21.5",
 | 
				
			||||||
    "@babel/runtime": "^7.21.5",
 | 
					    "@babel/runtime": "^7.22.3",
 | 
				
			||||||
    "@gamestdio/websocket": "^0.3.2",
 | 
					    "@gamestdio/websocket": "^0.3.2",
 | 
				
			||||||
    "@github/webauthn-json": "^2.1.1",
 | 
					    "@github/webauthn-json": "^2.1.1",
 | 
				
			||||||
    "@rails/ujs": "^6.1.7",
 | 
					    "@rails/ujs": "^6.1.7",
 | 
				
			||||||
| 
						 | 
					@ -76,7 +76,7 @@
 | 
				
			||||||
    "intl-messageformat": "^2.2.0",
 | 
					    "intl-messageformat": "^2.2.0",
 | 
				
			||||||
    "intl-relativeformat": "^6.4.3",
 | 
					    "intl-relativeformat": "^6.4.3",
 | 
				
			||||||
    "js-yaml": "^4.1.0",
 | 
					    "js-yaml": "^4.1.0",
 | 
				
			||||||
    "jsdom": "^22.0.0",
 | 
					    "jsdom": "^22.1.0",
 | 
				
			||||||
    "lodash": "^4.17.21",
 | 
					    "lodash": "^4.17.21",
 | 
				
			||||||
    "mark-loader": "^0.1.6",
 | 
					    "mark-loader": "^0.1.6",
 | 
				
			||||||
    "marky": "^1.2.5",
 | 
					    "marky": "^1.2.5",
 | 
				
			||||||
| 
						 | 
					@ -86,7 +86,7 @@
 | 
				
			||||||
    "path-complete-extname": "^1.0.0",
 | 
					    "path-complete-extname": "^1.0.0",
 | 
				
			||||||
    "pg": "^8.5.0",
 | 
					    "pg": "^8.5.0",
 | 
				
			||||||
    "pg-connection-string": "^2.6.0",
 | 
					    "pg-connection-string": "^2.6.0",
 | 
				
			||||||
    "postcss": "^8.4.23",
 | 
					    "postcss": "^8.4.24",
 | 
				
			||||||
    "postcss-loader": "^4.3.0",
 | 
					    "postcss-loader": "^4.3.0",
 | 
				
			||||||
    "prop-types": "^15.8.1",
 | 
					    "prop-types": "^15.8.1",
 | 
				
			||||||
    "punycode": "^2.3.0",
 | 
					    "punycode": "^2.3.0",
 | 
				
			||||||
| 
						 | 
					@ -133,18 +133,18 @@
 | 
				
			||||||
    "webpack-cli": "^3.3.12",
 | 
					    "webpack-cli": "^3.3.12",
 | 
				
			||||||
    "webpack-merge": "^5.9.0",
 | 
					    "webpack-merge": "^5.9.0",
 | 
				
			||||||
    "wicg-inert": "^3.1.2",
 | 
					    "wicg-inert": "^3.1.2",
 | 
				
			||||||
    "workbox-expiration": "^6.5.4",
 | 
					    "workbox-expiration": "^6.6.0",
 | 
				
			||||||
    "workbox-precaching": "^6.5.4",
 | 
					    "workbox-precaching": "^6.6.0",
 | 
				
			||||||
    "workbox-routing": "^6.5.4",
 | 
					    "workbox-routing": "^6.6.0",
 | 
				
			||||||
    "workbox-strategies": "^6.5.4",
 | 
					    "workbox-strategies": "^6.6.0",
 | 
				
			||||||
    "workbox-webpack-plugin": "^6.5.4",
 | 
					    "workbox-webpack-plugin": "^6.6.0",
 | 
				
			||||||
    "workbox-window": "^6.5.4",
 | 
					    "workbox-window": "^6.6.0",
 | 
				
			||||||
    "ws": "^8.12.1"
 | 
					    "ws": "^8.12.1"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@testing-library/jest-dom": "^5.16.5",
 | 
					    "@testing-library/jest-dom": "^5.16.5",
 | 
				
			||||||
    "@testing-library/react": "^14.0.0",
 | 
					    "@testing-library/react": "^14.0.0",
 | 
				
			||||||
    "@types/babel__core": "^7.20.0",
 | 
					    "@types/babel__core": "^7.20.1",
 | 
				
			||||||
    "@types/emoji-mart": "^3.0.9",
 | 
					    "@types/emoji-mart": "^3.0.9",
 | 
				
			||||||
    "@types/escape-html": "^1.0.2",
 | 
					    "@types/escape-html": "^1.0.2",
 | 
				
			||||||
    "@types/express": "^4.17.17",
 | 
					    "@types/express": "^4.17.17",
 | 
				
			||||||
| 
						 | 
					@ -152,18 +152,18 @@
 | 
				
			||||||
    "@types/intl": "^1.2.0",
 | 
					    "@types/intl": "^1.2.0",
 | 
				
			||||||
    "@types/jest": "^29.5.1",
 | 
					    "@types/jest": "^29.5.1",
 | 
				
			||||||
    "@types/js-yaml": "^4.0.5",
 | 
					    "@types/js-yaml": "^4.0.5",
 | 
				
			||||||
    "@types/lodash": "^4.14.194",
 | 
					    "@types/lodash": "^4.14.195",
 | 
				
			||||||
    "@types/npmlog": "^4.1.4",
 | 
					    "@types/npmlog": "^4.1.4",
 | 
				
			||||||
    "@types/object-assign": "^4.0.30",
 | 
					    "@types/object-assign": "^4.0.30",
 | 
				
			||||||
    "@types/pg": "^8.6.6",
 | 
					    "@types/pg": "^8.6.6",
 | 
				
			||||||
    "@types/prop-types": "^15.7.5",
 | 
					    "@types/prop-types": "^15.7.5",
 | 
				
			||||||
    "@types/punycode": "^2.1.0",
 | 
					    "@types/punycode": "^2.1.0",
 | 
				
			||||||
    "@types/react": "^18.0.26",
 | 
					    "@types/react": "^18.2.7",
 | 
				
			||||||
    "@types/react-dom": "^18.2.4",
 | 
					    "@types/react-dom": "^18.2.4",
 | 
				
			||||||
    "@types/react-helmet": "^6.1.6",
 | 
					    "@types/react-helmet": "^6.1.6",
 | 
				
			||||||
    "@types/react-immutable-proptypes": "^2.1.0",
 | 
					    "@types/react-immutable-proptypes": "^2.1.0",
 | 
				
			||||||
    "@types/react-intl": "2.3.18",
 | 
					    "@types/react-intl": "2.3.18",
 | 
				
			||||||
    "@types/react-motion": "^0.0.33",
 | 
					    "@types/react-motion": "^0.0.34",
 | 
				
			||||||
    "@types/react-overlays": "^3.1.0",
 | 
					    "@types/react-overlays": "^3.1.0",
 | 
				
			||||||
    "@types/react-router-dom": "^5.3.3",
 | 
					    "@types/react-router-dom": "^5.3.3",
 | 
				
			||||||
    "@types/react-select": "^5.0.1",
 | 
					    "@types/react-select": "^5.0.1",
 | 
				
			||||||
| 
						 | 
					@ -177,15 +177,15 @@
 | 
				
			||||||
    "@types/uuid": "^9.0.0",
 | 
					    "@types/uuid": "^9.0.0",
 | 
				
			||||||
    "@types/webpack": "^4.41.33",
 | 
					    "@types/webpack": "^4.41.33",
 | 
				
			||||||
    "@types/yargs": "^17.0.24",
 | 
					    "@types/yargs": "^17.0.24",
 | 
				
			||||||
    "@typescript-eslint/eslint-plugin": "^5.59.7",
 | 
					    "@typescript-eslint/eslint-plugin": "^5.59.8",
 | 
				
			||||||
    "@typescript-eslint/parser": "^5.59.7",
 | 
					    "@typescript-eslint/parser": "^5.59.8",
 | 
				
			||||||
    "babel-jest": "^29.5.0",
 | 
					    "babel-jest": "^29.5.0",
 | 
				
			||||||
    "eslint": "^8.41.0",
 | 
					    "eslint": "^8.41.0",
 | 
				
			||||||
    "eslint-config-prettier": "^8.8.0",
 | 
					    "eslint-config-prettier": "^8.8.0",
 | 
				
			||||||
    "eslint-import-resolver-typescript": "^3.5.5",
 | 
					    "eslint-import-resolver-typescript": "^3.5.5",
 | 
				
			||||||
    "eslint-plugin-formatjs": "^4.10.1",
 | 
					    "eslint-plugin-formatjs": "^4.10.1",
 | 
				
			||||||
    "eslint-plugin-import": "~2.27.5",
 | 
					    "eslint-plugin-import": "~2.27.5",
 | 
				
			||||||
    "eslint-plugin-jsdoc": "^44.2.5",
 | 
					    "eslint-plugin-jsdoc": "^45.0.0",
 | 
				
			||||||
    "eslint-plugin-jsx-a11y": "~6.7.1",
 | 
					    "eslint-plugin-jsx-a11y": "~6.7.1",
 | 
				
			||||||
    "eslint-plugin-prettier": "^4.2.1",
 | 
					    "eslint-plugin-prettier": "^4.2.1",
 | 
				
			||||||
    "eslint-plugin-promise": "~6.1.1",
 | 
					    "eslint-plugin-promise": "~6.1.1",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,4 +18,37 @@ describe Admin::IpBlocksController do
 | 
				
			||||||
      expect(response).to have_http_status(:success)
 | 
					      expect(response).to have_http_status(:success)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'GET #new' do
 | 
				
			||||||
 | 
					    it 'returns http success and renders view' do
 | 
				
			||||||
 | 
					      get :new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					      expect(response).to render_template(:new)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #create' do
 | 
				
			||||||
 | 
					    context 'with valid data' do
 | 
				
			||||||
 | 
					      it 'creates a new ip block and redirects' do
 | 
				
			||||||
 | 
					        expect do
 | 
				
			||||||
 | 
					          post :create, params: { ip_block: { ip: '1.1.1.1', severity: 'no_access', expires_in: 1.day.to_i.to_s } }
 | 
				
			||||||
 | 
					        end.to change(IpBlock, :count).by(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to redirect_to(admin_ip_blocks_path)
 | 
				
			||||||
 | 
					        expect(flash.notice).to match(I18n.t('admin.ip_blocks.created_msg'))
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with invalid data' do
 | 
				
			||||||
 | 
					      it 'does not create new a ip block and renders new' do
 | 
				
			||||||
 | 
					        expect do
 | 
				
			||||||
 | 
					          post :create, params: { ip_block: { ip: '1.1.1.1' } }
 | 
				
			||||||
 | 
					        end.to_not change(IpBlock, :count)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					        expect(response).to render_template(:new)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,4 +18,42 @@ describe Admin::RelaysController do
 | 
				
			||||||
      expect(response).to have_http_status(:success)
 | 
					      expect(response).to have_http_status(:success)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'GET #new' do
 | 
				
			||||||
 | 
					    it 'returns http success and renders view' do
 | 
				
			||||||
 | 
					      get :new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					      expect(response).to render_template(:new)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #create' do
 | 
				
			||||||
 | 
					    context 'with valid data' do
 | 
				
			||||||
 | 
					      let(:inbox_url) { 'https://example.com/inbox' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        stub_request(:post, inbox_url).to_return(status: 200)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'creates a new relay and redirects' do
 | 
				
			||||||
 | 
					        expect do
 | 
				
			||||||
 | 
					          post :create, params: { relay: { inbox_url: inbox_url } }
 | 
				
			||||||
 | 
					        end.to change(Relay, :count).by(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to redirect_to(admin_relays_path)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with invalid data' do
 | 
				
			||||||
 | 
					      it 'does not create new a relay and renders new' do
 | 
				
			||||||
 | 
					        expect do
 | 
				
			||||||
 | 
					          post :create, params: { relay: { inbox_url: 'invalid' } }
 | 
				
			||||||
 | 
					        end.to_not change(Relay, :count)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					        expect(response).to render_template(:new)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,4 +18,68 @@ describe Admin::RulesController do
 | 
				
			||||||
      expect(response).to have_http_status(:success)
 | 
					      expect(response).to have_http_status(:success)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'GET #edit' do
 | 
				
			||||||
 | 
					    let(:rule) { Fabricate(:rule) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'returns http success and renders edit' do
 | 
				
			||||||
 | 
					      get :edit, params: { id: rule.id }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					      expect(response).to render_template(:edit)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #create' do
 | 
				
			||||||
 | 
					    context 'with valid data' do
 | 
				
			||||||
 | 
					      it 'creates a new rule and redirects' do
 | 
				
			||||||
 | 
					        expect do
 | 
				
			||||||
 | 
					          post :create, params: { rule: { text: 'The rule text.' } }
 | 
				
			||||||
 | 
					        end.to change(Rule, :count).by(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to redirect_to(admin_rules_path)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with invalid data' do
 | 
				
			||||||
 | 
					      it 'does creates a new rule and renders index' do
 | 
				
			||||||
 | 
					        expect do
 | 
				
			||||||
 | 
					          post :create, params: { rule: { text: '' } }
 | 
				
			||||||
 | 
					        end.to_not change(Rule, :count)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to render_template(:index)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'PUT #update' do
 | 
				
			||||||
 | 
					    let(:rule) { Fabricate(:rule, text: 'Original text') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with valid data' do
 | 
				
			||||||
 | 
					      it 'updates the rule and redirects' do
 | 
				
			||||||
 | 
					        put :update, params: { id: rule.id, rule: { text: 'Updated text.' } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to redirect_to(admin_rules_path)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with invalid data' do
 | 
				
			||||||
 | 
					      it 'does not update the rule and renders index' do
 | 
				
			||||||
 | 
					        put :update, params: { id: rule.id, rule: { text: '' } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to render_template(:edit)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'DELETE #destroy' do
 | 
				
			||||||
 | 
					    let!(:rule) { Fabricate(:rule) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'destroys the rule and redirects' do
 | 
				
			||||||
 | 
					      delete :destroy, params: { id: rule.id }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(rule.reload).to be_discarded
 | 
				
			||||||
 | 
					      expect(response).to redirect_to(admin_rules_path)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,4 +18,82 @@ describe Admin::WebhooksController do
 | 
				
			||||||
      expect(response).to have_http_status(:success)
 | 
					      expect(response).to have_http_status(:success)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'GET #new' do
 | 
				
			||||||
 | 
					    it 'returns http success and renders view' do
 | 
				
			||||||
 | 
					      get :new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					      expect(response).to render_template(:new)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'POST #create' do
 | 
				
			||||||
 | 
					    it 'creates a new webhook record with valid data' do
 | 
				
			||||||
 | 
					      expect do
 | 
				
			||||||
 | 
					        post :create, params: { webhook: { url: 'https://example.com/hook', events: ['account.approved'] } }
 | 
				
			||||||
 | 
					      end.to change(Webhook, :count).by(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(response).to be_redirect
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it 'does not create a new webhook record with invalid data' do
 | 
				
			||||||
 | 
					      expect do
 | 
				
			||||||
 | 
					        post :create, params: { webhook: { url: 'https://example.com/hook', events: [] } }
 | 
				
			||||||
 | 
					      end.to_not change(Webhook, :count)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					      expect(response).to render_template(:new)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  context 'with an existing record' do
 | 
				
			||||||
 | 
					    let!(:webhook) { Fabricate :webhook }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    describe 'GET #show' do
 | 
				
			||||||
 | 
					      it 'returns http success and renders view' do
 | 
				
			||||||
 | 
					        get :show, params: { id: webhook.id }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					        expect(response).to render_template(:show)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    describe 'GET #edit' do
 | 
				
			||||||
 | 
					      it 'returns http success and renders view' do
 | 
				
			||||||
 | 
					        get :edit, params: { id: webhook.id }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					        expect(response).to render_template(:edit)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    describe 'PUT #update' do
 | 
				
			||||||
 | 
					      it 'updates the record with valid data' do
 | 
				
			||||||
 | 
					        put :update, params: { id: webhook.id, webhook: { url: 'https://example.com/new/location' } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(webhook.reload.url).to match(%r{new/location})
 | 
				
			||||||
 | 
					        expect(response).to redirect_to(admin_webhook_path(webhook))
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'does not update the record with invalid data' do
 | 
				
			||||||
 | 
					        expect do
 | 
				
			||||||
 | 
					          put :update, params: { id: webhook.id, webhook: { url: '' } }
 | 
				
			||||||
 | 
					        end.to_not change(webhook, :url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to have_http_status(:success)
 | 
				
			||||||
 | 
					        expect(response).to render_template(:show)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    describe 'DELETE #destroy' do
 | 
				
			||||||
 | 
					      it 'destroys the record' do
 | 
				
			||||||
 | 
					        expect do
 | 
				
			||||||
 | 
					          delete :destroy, params: { id: webhook.id }
 | 
				
			||||||
 | 
					        end.to change(Webhook, :count).by(-1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response).to redirect_to(admin_webhooks_path)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,9 +4,662 @@ require 'rails_helper'
 | 
				
			||||||
require 'mastodon/cli/accounts'
 | 
					require 'mastodon/cli/accounts'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe Mastodon::CLI::Accounts do
 | 
					describe Mastodon::CLI::Accounts do
 | 
				
			||||||
 | 
					  let(:cli) { described_class.new }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe '.exit_on_failure?' do
 | 
					  describe '.exit_on_failure?' do
 | 
				
			||||||
    it 'returns true' do
 | 
					    it 'returns true' do
 | 
				
			||||||
      expect(described_class.exit_on_failure?).to be true
 | 
					      expect(described_class.exit_on_failure?).to be true
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#create' do
 | 
				
			||||||
 | 
					    shared_examples 'a new user with given email address and username' do
 | 
				
			||||||
 | 
					      it 'creates a new user with the specified email address' do
 | 
				
			||||||
 | 
					        cli.invoke(:create, arguments, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(User.find_by(email: options[:email])).to be_present
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'creates a new local account with the specified username' do
 | 
				
			||||||
 | 
					        cli.invoke(:create, arguments, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(Account.find_local('tootctl_username')).to be_present
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'returns "OK" and newly generated password' do
 | 
				
			||||||
 | 
					        allow(SecureRandom).to receive(:hex).and_return('test_password')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect { cli.invoke(:create, arguments, options) }.to output(
 | 
				
			||||||
 | 
					          a_string_including("OK\nNew password: test_password")
 | 
				
			||||||
 | 
					        ).to_stdout
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when required USERNAME and --email are provided' do
 | 
				
			||||||
 | 
					      let(:arguments) { ['tootctl_username'] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with USERNAME and --email only' do
 | 
				
			||||||
 | 
					        let(:options) { { email: 'tootctl@example.com' } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it_behaves_like 'a new user with given email address and username'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        context 'with invalid --email value' do
 | 
				
			||||||
 | 
					          let(:options) { { email: 'invalid' } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          it 'exits with an error message' do
 | 
				
			||||||
 | 
					            expect { cli.invoke(:create, arguments, options) }.to output(
 | 
				
			||||||
 | 
					              a_string_including('Failure/Error: email')
 | 
				
			||||||
 | 
					            ).to_stdout
 | 
				
			||||||
 | 
					              .and raise_error(SystemExit)
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --confirmed option' do
 | 
				
			||||||
 | 
					        let(:options) { { email: 'tootctl@example.com', confirmed: true } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it_behaves_like 'a new user with given email address and username'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'creates a new user with confirmed status' do
 | 
				
			||||||
 | 
					          cli.invoke(:create, arguments, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          user = User.find_by(email: options[:email])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(user.confirmed?).to be(true)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --approve option' do
 | 
				
			||||||
 | 
					        let(:options) { { email: 'tootctl@example.com', approve: true } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        before do
 | 
				
			||||||
 | 
					          Form::AdminSettings.new(registrations_mode: 'approved').save
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it_behaves_like 'a new user with given email address and username'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'creates a new user with approved status' do
 | 
				
			||||||
 | 
					          cli.invoke(:create, arguments, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          user = User.find_by(email: options[:email])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(user.approved?).to be(true)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --role option' do
 | 
				
			||||||
 | 
					        context 'when role exists' do
 | 
				
			||||||
 | 
					          let(:default_role) { Fabricate(:user_role) }
 | 
				
			||||||
 | 
					          let(:options) { { email: 'tootctl@example.com', role: default_role.name } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          it_behaves_like 'a new user with given email address and username'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          it 'creates a new user and assigns the specified role' do
 | 
				
			||||||
 | 
					            cli.invoke(:create, arguments, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            role = User.find_by(email: options[:email])&.role
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            expect(role.name).to eq(default_role.name)
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        context 'when role does not exist' do
 | 
				
			||||||
 | 
					          let(:options) { { email: 'tootctl@example.com', role: '404' } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          it 'exits with an error message indicating the role name was not found' do
 | 
				
			||||||
 | 
					            expect { cli.invoke(:create, arguments, options) }.to output(
 | 
				
			||||||
 | 
					              a_string_including('Cannot find user role with that name')
 | 
				
			||||||
 | 
					            ).to_stdout
 | 
				
			||||||
 | 
					              .and raise_error(SystemExit)
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --reattach option' do
 | 
				
			||||||
 | 
					        context "when account's user is present" do
 | 
				
			||||||
 | 
					          let(:options) { { email: 'tootctl_new@example.com', reattach: true } }
 | 
				
			||||||
 | 
					          let(:user) { Fabricate.build(:user, email: 'tootctl@example.com') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          before do
 | 
				
			||||||
 | 
					            Fabricate(:account, username: 'tootctl_username', user: user)
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          it 'returns an error message indicating the username is already taken' do
 | 
				
			||||||
 | 
					            expect { cli.invoke(:create, arguments, options) }.to output(
 | 
				
			||||||
 | 
					              a_string_including("The chosen username is currently in use\nUse --force to reattach it anyway and delete the other user")
 | 
				
			||||||
 | 
					            ).to_stdout
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          context 'with --force option' do
 | 
				
			||||||
 | 
					            let(:options) { { email: 'tootctl_new@example.com', reattach: true, force: true } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            it 'reattaches the account to the new user and deletes the previous user' do
 | 
				
			||||||
 | 
					              cli.invoke(:create, arguments, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              user = Account.find_local('tootctl_username')&.user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              expect(user.email).to eq(options[:email])
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        context "when account's user is not present" do
 | 
				
			||||||
 | 
					          let(:options) { { email: 'tootctl@example.com', reattach: true } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          before do
 | 
				
			||||||
 | 
					            Fabricate(:account, username: 'tootctl_username', user: nil)
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          it_behaves_like 'a new user with given email address and username'
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when required --email option is not provided' do
 | 
				
			||||||
 | 
					      let(:arguments) { ['tootctl_username'] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'raises a required argument missing error (Thor::RequiredArgumentMissingError)' do
 | 
				
			||||||
 | 
					        expect { cli.invoke(:create, arguments) }
 | 
				
			||||||
 | 
					          .to raise_error(Thor::RequiredArgumentMissingError)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#modify' do
 | 
				
			||||||
 | 
					    context 'when the given username is not found' do
 | 
				
			||||||
 | 
					      let(:arguments) { ['non_existent_username'] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'exits with an error message indicating the user was not found' do
 | 
				
			||||||
 | 
					        expect { cli.invoke(:modify, arguments) }.to output(
 | 
				
			||||||
 | 
					          a_string_including('No user with such username')
 | 
				
			||||||
 | 
					        ).to_stdout
 | 
				
			||||||
 | 
					          .and raise_error(SystemExit)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when the given username is found' do
 | 
				
			||||||
 | 
					      let(:user) { Fabricate(:user) }
 | 
				
			||||||
 | 
					      let(:arguments) { [user.account.username] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'when no option is provided' do
 | 
				
			||||||
 | 
					        it 'returns a successful message' do
 | 
				
			||||||
 | 
					          expect { cli.invoke(:modify, arguments) }.to output(
 | 
				
			||||||
 | 
					            a_string_including('OK')
 | 
				
			||||||
 | 
					          ).to_stdout
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'does not modify the user' do
 | 
				
			||||||
 | 
					          cli.invoke(:modify, arguments)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(user).to eq(user.reload)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --role option' do
 | 
				
			||||||
 | 
					        context 'when the given role is not found' do
 | 
				
			||||||
 | 
					          let(:options) { { role: '404' } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          it 'exits with an error message indicating the role was not found' do
 | 
				
			||||||
 | 
					            expect { cli.invoke(:modify, arguments, options) }.to output(
 | 
				
			||||||
 | 
					              a_string_including('Cannot find user role with that name')
 | 
				
			||||||
 | 
					            ).to_stdout
 | 
				
			||||||
 | 
					              .and raise_error(SystemExit)
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        context 'when the given role is found' do
 | 
				
			||||||
 | 
					          let(:default_role) { Fabricate(:user_role) }
 | 
				
			||||||
 | 
					          let(:options) { { role: default_role.name } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          it "updates the user's role to the specified role" do
 | 
				
			||||||
 | 
					            cli.invoke(:modify, arguments, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            role = user.reload.role
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            expect(role.name).to eq(default_role.name)
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --remove-role option' do
 | 
				
			||||||
 | 
					        let(:options) { { remove_role: true } }
 | 
				
			||||||
 | 
					        let(:role) { Fabricate(:user_role) }
 | 
				
			||||||
 | 
					        let(:user) { Fabricate(:user, role: role) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it "removes the user's role successfully" do
 | 
				
			||||||
 | 
					          cli.invoke(:modify, arguments, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          role = user.reload.role
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(role.name).to be_empty
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --email option' do
 | 
				
			||||||
 | 
					        let(:user) { Fabricate(:user, email: 'old_email@email.com') }
 | 
				
			||||||
 | 
					        let(:options) { { email: 'new_email@email.com' } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it "sets the user's unconfirmed email to the provided email address" do
 | 
				
			||||||
 | 
					          cli.invoke(:modify, arguments, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(user.reload.unconfirmed_email).to eq(options[:email])
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it "does not update the user's original email address" do
 | 
				
			||||||
 | 
					          cli.invoke(:modify, arguments, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(user.reload.email).to eq('old_email@email.com')
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        context 'with --confirm option' do
 | 
				
			||||||
 | 
					          let(:user) { Fabricate(:user, email: 'old_email@email.com', confirmed_at: nil) }
 | 
				
			||||||
 | 
					          let(:options) { { email: 'new_email@email.com', confirm: true } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          it "updates the user's email address to the provided email" do
 | 
				
			||||||
 | 
					            cli.invoke(:modify, arguments, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            expect(user.reload.email).to eq(options[:email])
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          it "sets the user's email address as confirmed" do
 | 
				
			||||||
 | 
					            cli.invoke(:modify, arguments, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            expect(user.reload.confirmed?).to be(true)
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --confirm option' do
 | 
				
			||||||
 | 
					        let(:user) { Fabricate(:user, confirmed_at: nil) }
 | 
				
			||||||
 | 
					        let(:options) { { confirm: true } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it "confirms the user's email address" do
 | 
				
			||||||
 | 
					          cli.invoke(:modify, arguments, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(user.reload.confirmed?).to be(true)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --approve option' do
 | 
				
			||||||
 | 
					        let(:user) { Fabricate(:user, approved: false) }
 | 
				
			||||||
 | 
					        let(:options) { { approve: true } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        before do
 | 
				
			||||||
 | 
					          Form::AdminSettings.new(registrations_mode: 'approved').save
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'approves the user' do
 | 
				
			||||||
 | 
					          expect { cli.invoke(:modify, arguments, options) }.to change { user.reload.approved }.from(false).to(true)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --disable option' do
 | 
				
			||||||
 | 
					        let(:user) { Fabricate(:user, disabled: false) }
 | 
				
			||||||
 | 
					        let(:options) { { disable: true } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'disables the user' do
 | 
				
			||||||
 | 
					          expect { cli.invoke(:modify, arguments, options) }.to change { user.reload.disabled }.from(false).to(true)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --enable option' do
 | 
				
			||||||
 | 
					        let(:user) { Fabricate(:user, disabled: true) }
 | 
				
			||||||
 | 
					        let(:options) { { enable: true } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'enables the user' do
 | 
				
			||||||
 | 
					          expect { cli.invoke(:modify, arguments, options) }.to change { user.reload.disabled }.from(true).to(false)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --reset-password option' do
 | 
				
			||||||
 | 
					        let(:options) { { reset_password: true } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'returns a new password for the user' do
 | 
				
			||||||
 | 
					          allow(SecureRandom).to receive(:hex).and_return('new_password')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect { cli.invoke(:modify, arguments, options) }.to output(
 | 
				
			||||||
 | 
					            a_string_including('new_password')
 | 
				
			||||||
 | 
					          ).to_stdout
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --disable-2fa option' do
 | 
				
			||||||
 | 
					        let(:user) { Fabricate(:user, otp_required_for_login: true) }
 | 
				
			||||||
 | 
					        let(:options) { { disable_2fa: true } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'disables the two-factor authentication for the user' do
 | 
				
			||||||
 | 
					          expect { cli.invoke(:modify, arguments, options) }.to change { user.reload.otp_required_for_login }.from(true).to(false)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'when provided data is invalid' do
 | 
				
			||||||
 | 
					        let(:user) { Fabricate(:user) }
 | 
				
			||||||
 | 
					        let(:options) { { email: 'invalid' } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'exits with an error message' do
 | 
				
			||||||
 | 
					          expect { cli.invoke(:modify, arguments, options) }.to output(
 | 
				
			||||||
 | 
					            a_string_including('Failure/Error: email')
 | 
				
			||||||
 | 
					          ).to_stdout
 | 
				
			||||||
 | 
					            .and raise_error(SystemExit)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#delete' do
 | 
				
			||||||
 | 
					    let(:account) { Fabricate(:account) }
 | 
				
			||||||
 | 
					    let(:arguments) { [account.username] }
 | 
				
			||||||
 | 
					    let(:options) { { email: account.user.email } }
 | 
				
			||||||
 | 
					    let(:delete_account_service) { instance_double(DeleteAccountService) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      allow(DeleteAccountService).to receive(:new).and_return(delete_account_service)
 | 
				
			||||||
 | 
					      allow(delete_account_service).to receive(:call)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when both username and --email are provided' do
 | 
				
			||||||
 | 
					      it 'exits with an error message indicating that only one should be used' do
 | 
				
			||||||
 | 
					        expect { cli.invoke(:delete, arguments, options) }.to output(
 | 
				
			||||||
 | 
					          a_string_including('Use username or --email, not both')
 | 
				
			||||||
 | 
					        ).to_stdout
 | 
				
			||||||
 | 
					          .and raise_error(SystemExit)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when neither username nor --email are provided' do
 | 
				
			||||||
 | 
					      it 'exits with an error message indicating that no username was provided' do
 | 
				
			||||||
 | 
					        expect { cli.invoke(:delete) }.to output(
 | 
				
			||||||
 | 
					          a_string_including('No username provided')
 | 
				
			||||||
 | 
					        ).to_stdout
 | 
				
			||||||
 | 
					          .and raise_error(SystemExit)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when username is provided' do
 | 
				
			||||||
 | 
					      it 'deletes the specified user successfully' do
 | 
				
			||||||
 | 
					        cli.invoke(:delete, arguments)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(delete_account_service).to have_received(:call).with(account, reserve_email: false).once
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --dry-run option' do
 | 
				
			||||||
 | 
					        let(:options) { { dry_run: true } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'does not delete the specified user' do
 | 
				
			||||||
 | 
					          cli.invoke(:delete, arguments, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(delete_account_service).to_not have_received(:call).with(account, reserve_email: false)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'outputs a successful message in dry run mode' do
 | 
				
			||||||
 | 
					          expect { cli.invoke(:delete, arguments, options) }.to output(
 | 
				
			||||||
 | 
					            a_string_including('OK (DRY RUN)')
 | 
				
			||||||
 | 
					          ).to_stdout
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'when the given username is not found' do
 | 
				
			||||||
 | 
					        let(:arguments) { ['non_existent_username'] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'exits with an error message indicating that no user was found' do
 | 
				
			||||||
 | 
					          expect { cli.invoke(:delete, arguments) }.to output(
 | 
				
			||||||
 | 
					            a_string_including('No user with such username')
 | 
				
			||||||
 | 
					          ).to_stdout
 | 
				
			||||||
 | 
					            .and raise_error(SystemExit)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when --email is provided' do
 | 
				
			||||||
 | 
					      it 'deletes the specified user successfully' do
 | 
				
			||||||
 | 
					        cli.invoke(:delete, nil, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(delete_account_service).to have_received(:call).with(account, reserve_email: false).once
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'with --dry-run option' do
 | 
				
			||||||
 | 
					        let(:options) { { email: account.user.email, dry_run: true } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'does not delete the user' do
 | 
				
			||||||
 | 
					          cli.invoke(:delete, nil, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(delete_account_service).to_not have_received(:call).with(account, reserve_email: false)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'outputs a successful message in dry run mode' do
 | 
				
			||||||
 | 
					          expect { cli.invoke(:delete, nil, options) }.to output(
 | 
				
			||||||
 | 
					            a_string_including('OK (DRY RUN)')
 | 
				
			||||||
 | 
					          ).to_stdout
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'when the given email address is not found' do
 | 
				
			||||||
 | 
					        let(:options) { { email: '404@example.com' } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'exits with an error message indicating that no user was found' do
 | 
				
			||||||
 | 
					          expect { cli.invoke(:delete, nil, options) }.to output(
 | 
				
			||||||
 | 
					            a_string_including('No user with such email')
 | 
				
			||||||
 | 
					          ).to_stdout
 | 
				
			||||||
 | 
					            .and raise_error(SystemExit)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#approve' do
 | 
				
			||||||
 | 
					    let(:total_users) { 10 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      Form::AdminSettings.new(registrations_mode: 'approved').save
 | 
				
			||||||
 | 
					      Fabricate.times(total_users, :user)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with --all option' do
 | 
				
			||||||
 | 
					      it 'approves all pending registrations' do
 | 
				
			||||||
 | 
					        cli.invoke(:approve, nil, all: true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(User.pluck(:approved).all?(true)).to be(true)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with --number option' do
 | 
				
			||||||
 | 
					      context 'when the number is positive' do
 | 
				
			||||||
 | 
					        let(:options) { { number: 3 } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'approves the earliest n pending registrations' do
 | 
				
			||||||
 | 
					          cli.invoke(:approve, nil, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          n_earliest_pending_registrations = User.order(created_at: :asc).first(options[:number])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(n_earliest_pending_registrations.all?(&:approved?)).to be(true)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'does not approve the remaining pending registrations' do
 | 
				
			||||||
 | 
					          cli.invoke(:approve, nil, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          pending_registrations = User.order(created_at: :asc).last(total_users - options[:number])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(pending_registrations.all?(&:approved?)).to be(false)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'when the number is negative' do
 | 
				
			||||||
 | 
					        it 'exits with an error message indicating that the number must be positive' do
 | 
				
			||||||
 | 
					          expect { cli.invoke(:approve, nil, number: -1) }.to output(
 | 
				
			||||||
 | 
					            a_string_including('Number must be positive')
 | 
				
			||||||
 | 
					          ).to_stdout
 | 
				
			||||||
 | 
					            .and raise_error(SystemExit)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'when the given number is greater than the number of users' do
 | 
				
			||||||
 | 
					        let(:options) { { number: total_users * 2 } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'approves all users' do
 | 
				
			||||||
 | 
					          cli.invoke(:approve, nil, options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(User.pluck(:approved).all?(true)).to be(true)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'does not raise any error' do
 | 
				
			||||||
 | 
					          expect { cli.invoke(:approve, nil, options) }
 | 
				
			||||||
 | 
					            .to_not raise_error
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'with username argument' do
 | 
				
			||||||
 | 
					      context 'when the given username is found' do
 | 
				
			||||||
 | 
					        let(:user) { User.last }
 | 
				
			||||||
 | 
					        let(:arguments) { [user.account.username] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'approves the specified user successfully' do
 | 
				
			||||||
 | 
					          cli.invoke(:approve, arguments)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          expect(user.reload.approved?).to be(true)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      context 'when the given username is not found' do
 | 
				
			||||||
 | 
					        let(:arguments) { ['non_existent_username'] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        it 'exits with an error message indicating that no such account was found' do
 | 
				
			||||||
 | 
					          expect { cli.invoke(:approve, arguments) }.to output(
 | 
				
			||||||
 | 
					            a_string_including('No such account')
 | 
				
			||||||
 | 
					          ).to_stdout
 | 
				
			||||||
 | 
					            .and raise_error(SystemExit)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#follow' do
 | 
				
			||||||
 | 
					    context 'when the given username is not found' do
 | 
				
			||||||
 | 
					      let(:arguments) { ['non_existent_username'] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'exits with an error message indicating that no account with the given username was found' do
 | 
				
			||||||
 | 
					        expect { cli.invoke(:follow, arguments) }.to output(
 | 
				
			||||||
 | 
					          a_string_including('No such account')
 | 
				
			||||||
 | 
					        ).to_stdout
 | 
				
			||||||
 | 
					          .and raise_error(SystemExit)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when the given username is found' do
 | 
				
			||||||
 | 
					      let!(:target_account)   { Fabricate(:account) }
 | 
				
			||||||
 | 
					      let!(:follower_bob)     { Fabricate(:account, username: 'bob') }
 | 
				
			||||||
 | 
					      let!(:follower_rony)    { Fabricate(:account, username: 'rony') }
 | 
				
			||||||
 | 
					      let!(:follower_charles) { Fabricate(:account, username: 'charles') }
 | 
				
			||||||
 | 
					      let(:follow_service)    { instance_double(FollowService, call: nil) }
 | 
				
			||||||
 | 
					      let(:scope)             { Account.local.without_suspended }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        allow(cli).to receive(:parallelize_with_progress).and_yield(follower_bob)
 | 
				
			||||||
 | 
					                                                         .and_yield(follower_rony)
 | 
				
			||||||
 | 
					                                                         .and_yield(follower_charles)
 | 
				
			||||||
 | 
					                                                         .and_return([3, nil])
 | 
				
			||||||
 | 
					        allow(FollowService).to receive(:new).and_return(follow_service)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'makes all local accounts follow the target account' do
 | 
				
			||||||
 | 
					        cli.follow(target_account.username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(cli).to have_received(:parallelize_with_progress).with(scope).once
 | 
				
			||||||
 | 
					        expect(follow_service).to have_received(:call).with(follower_bob, target_account, any_args).once
 | 
				
			||||||
 | 
					        expect(follow_service).to have_received(:call).with(follower_rony, target_account, any_args).once
 | 
				
			||||||
 | 
					        expect(follow_service).to have_received(:call).with(follower_charles, target_account, any_args).once
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'displays a successful message' do
 | 
				
			||||||
 | 
					        expect { cli.follow(target_account.username) }.to output(
 | 
				
			||||||
 | 
					          a_string_including('OK, followed target from 3 accounts')
 | 
				
			||||||
 | 
					        ).to_stdout
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#unfollow' do
 | 
				
			||||||
 | 
					    context 'when the given username is not found' do
 | 
				
			||||||
 | 
					      let(:arguments) { ['non_existent_username'] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'exits with an error message indicating that no account with the given username was found' do
 | 
				
			||||||
 | 
					        expect { cli.invoke(:unfollow, arguments) }.to output(
 | 
				
			||||||
 | 
					          a_string_including('No such account')
 | 
				
			||||||
 | 
					        ).to_stdout
 | 
				
			||||||
 | 
					          .and raise_error(SystemExit)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when the given username is found' do
 | 
				
			||||||
 | 
					      let!(:target_account)  { Fabricate(:account) }
 | 
				
			||||||
 | 
					      let!(:follower_chris)  { Fabricate(:account, username: 'chris') }
 | 
				
			||||||
 | 
					      let!(:follower_rambo)  { Fabricate(:account, username: 'rambo') }
 | 
				
			||||||
 | 
					      let!(:follower_ana)    { Fabricate(:account, username: 'ana') }
 | 
				
			||||||
 | 
					      let(:unfollow_service) { instance_double(UnfollowService, call: nil) }
 | 
				
			||||||
 | 
					      let(:scope)            { target_account.followers.local }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        accounts = [follower_chris, follower_rambo, follower_ana]
 | 
				
			||||||
 | 
					        accounts.each { |account| target_account.follow!(account) }
 | 
				
			||||||
 | 
					        allow(cli).to receive(:parallelize_with_progress).and_yield(follower_chris)
 | 
				
			||||||
 | 
					                                                         .and_yield(follower_rambo)
 | 
				
			||||||
 | 
					                                                         .and_yield(follower_ana)
 | 
				
			||||||
 | 
					                                                         .and_return([3, nil])
 | 
				
			||||||
 | 
					        allow(UnfollowService).to receive(:new).and_return(unfollow_service)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'makes all local accounts unfollow the target account' do
 | 
				
			||||||
 | 
					        cli.unfollow(target_account.username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(cli).to have_received(:parallelize_with_progress).with(scope).once
 | 
				
			||||||
 | 
					        expect(unfollow_service).to have_received(:call).with(follower_chris, target_account).once
 | 
				
			||||||
 | 
					        expect(unfollow_service).to have_received(:call).with(follower_rambo, target_account).once
 | 
				
			||||||
 | 
					        expect(unfollow_service).to have_received(:call).with(follower_ana, target_account).once
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'displays a successful message' do
 | 
				
			||||||
 | 
					        expect { cli.unfollow(target_account.username) }.to output(
 | 
				
			||||||
 | 
					          a_string_including('OK, unfollowed target from 3 accounts')
 | 
				
			||||||
 | 
					        ).to_stdout
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#backup' do
 | 
				
			||||||
 | 
					    context 'when the given username is not found' do
 | 
				
			||||||
 | 
					      let(:arguments) { ['non_existent_username'] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'exits with an error message indicating that there is no such account' do
 | 
				
			||||||
 | 
					        expect { cli.invoke(:backup, arguments) }.to output(
 | 
				
			||||||
 | 
					          a_string_including('No user with such username')
 | 
				
			||||||
 | 
					        ).to_stdout
 | 
				
			||||||
 | 
					          .and raise_error(SystemExit)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when the given username is found' do
 | 
				
			||||||
 | 
					      let(:account) { Fabricate(:account) }
 | 
				
			||||||
 | 
					      let(:user) { account.user }
 | 
				
			||||||
 | 
					      let(:arguments) { [account.username] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'creates a new backup for the specified user' do
 | 
				
			||||||
 | 
					        expect { cli.invoke(:backup, arguments) }.to change { user.backups.count }.by(1)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'creates a backup job' do
 | 
				
			||||||
 | 
					        allow(BackupWorker).to receive(:perform_async)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        cli.invoke(:backup, arguments)
 | 
				
			||||||
 | 
					        latest_backup = user.backups.last
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(BackupWorker).to have_received(:perform_async).with(latest_backup.id).once
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'displays a successful message' do
 | 
				
			||||||
 | 
					        expect { cli.invoke(:backup, arguments) }.to output(
 | 
				
			||||||
 | 
					          a_string_including('OK')
 | 
				
			||||||
 | 
					        ).to_stdout
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -62,6 +62,10 @@ RSpec.configure do |config|
 | 
				
			||||||
  config.infer_spec_type_from_file_location!
 | 
					  config.infer_spec_type_from_file_location!
 | 
				
			||||||
  config.filter_rails_from_backtrace!
 | 
					  config.filter_rails_from_backtrace!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  config.define_derived_metadata(file_path: Regexp.new('spec/lib/mastodon/cli')) do |metadata|
 | 
				
			||||||
 | 
					    metadata[:type] = :cli
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  config.include Devise::Test::ControllerHelpers, type: :controller
 | 
					  config.include Devise::Test::ControllerHelpers, type: :controller
 | 
				
			||||||
  config.include Devise::Test::ControllerHelpers, type: :helper
 | 
					  config.include Devise::Test::ControllerHelpers, type: :helper
 | 
				
			||||||
  config.include Devise::Test::ControllerHelpers, type: :view
 | 
					  config.include Devise::Test::ControllerHelpers, type: :view
 | 
				
			||||||
| 
						 | 
					@ -73,6 +77,10 @@ RSpec.configure do |config|
 | 
				
			||||||
  config.include Redisable
 | 
					  config.include Redisable
 | 
				
			||||||
  config.include SignedRequestHelpers, type: :request
 | 
					  config.include SignedRequestHelpers, type: :request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  config.before :each, type: :cli do
 | 
				
			||||||
 | 
					    stub_stdout
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  config.before :each, type: :feature do
 | 
					  config.before :each, type: :feature do
 | 
				
			||||||
    https = ENV['LOCAL_HTTPS'] == 'true'
 | 
					    https = ENV['LOCAL_HTTPS'] == 'true'
 | 
				
			||||||
    Capybara.app_host = "http#{https ? 's' : ''}://#{ENV.fetch('LOCAL_DOMAIN')}"
 | 
					    Capybara.app_host = "http#{https ? 's' : ''}://#{ENV.fetch('LOCAL_DOMAIN')}"
 | 
				
			||||||
| 
						 | 
					@ -106,6 +114,10 @@ def attachment_fixture(name)
 | 
				
			||||||
  Rails.root.join('spec', 'fixtures', 'files', name).open
 | 
					  Rails.root.join('spec', 'fixtures', 'files', name).open
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def stub_stdout
 | 
				
			||||||
 | 
					  allow($stdout).to receive(:write)
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def stub_jsonld_contexts!
 | 
					def stub_jsonld_contexts!
 | 
				
			||||||
  stub_request(:get, 'https://www.w3.org/ns/activitystreams').to_return(request_fixture('json-ld.activitystreams.txt'))
 | 
					  stub_request(:get, 'https://www.w3.org/ns/activitystreams').to_return(request_fixture('json-ld.activitystreams.txt'))
 | 
				
			||||||
  stub_request(:get, 'https://w3id.org/identity/v1').to_return(request_fixture('json-ld.identity.txt'))
 | 
					  stub_request(:get, 'https://w3id.org/identity/v1').to_return(request_fixture('json-ld.identity.txt'))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue