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/typescript',
|
||||
'plugin:promise/recommended',
|
||||
'plugin:jsdoc/recommended',
|
||||
'plugin:jsdoc/recommended-typescript',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
|
||||
|
111
.rubocop.yml
111
.rubocop.yml
@ -53,6 +53,28 @@ Lint/UselessAccessModifier:
|
||||
ContextCreatingMethods:
|
||||
- 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
|
||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize
|
||||
Metrics/AbcSize:
|
||||
@ -60,88 +82,12 @@ Metrics/AbcSize:
|
||||
- 'lib/mastodon/cli/*.rb'
|
||||
- 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:
|
||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocknesting
|
||||
Metrics/BlockNesting:
|
||||
Exclude:
|
||||
- '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
|
||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity
|
||||
Metrics/CyclomaticComplexity:
|
||||
@ -149,17 +95,10 @@ Metrics/CyclomaticComplexity:
|
||||
- lib/mastodon/cli/*.rb
|
||||
- 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:
|
||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength
|
||||
Metrics/ModuleLength:
|
||||
CountAsOne: [array, heredoc]
|
||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsparameterlists
|
||||
Metrics/ParameterLists:
|
||||
CountKeywordArgs: false
|
||||
|
||||
# Reason: Prevailing style is argument file paths
|
||||
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath
|
||||
|
@ -156,12 +156,6 @@ Metrics/AbcSize:
|
||||
Exclude:
|
||||
- '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.
|
||||
Metrics/BlockNesting:
|
||||
Exclude:
|
||||
@ -171,28 +165,6 @@ Metrics/BlockNesting:
|
||||
Metrics/CyclomaticComplexity:
|
||||
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.
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 28
|
||||
@ -894,7 +866,6 @@ Rails/WhereExists:
|
||||
- 'app/validators/vote_validator.rb'
|
||||
- 'app/workers/move_worker.rb'
|
||||
- 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb'
|
||||
- 'lib/mastodon/cli/email_domain_blocks.rb'
|
||||
- 'lib/tasks/tests.rake'
|
||||
- 'spec/controllers/api/v1/accounts/notes_controller_spec.rb'
|
||||
- 'spec/controllers/api/v1/tags_controller_spec.rb'
|
||||
@ -956,7 +927,6 @@ Style/FormatStringToken:
|
||||
Exclude:
|
||||
- 'app/models/privacy_policy.rb'
|
||||
- 'config/initializers/devise.rb'
|
||||
- 'lib/mastodon/cli/maintenance.rb'
|
||||
- 'lib/paperclip/color_extractor.rb'
|
||||
|
||||
# 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 'link_header', '~> 0.0'
|
||||
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
|
||||
gem 'nokogiri', '~> 1.14'
|
||||
gem 'nokogiri', '~> 1.15'
|
||||
gem 'nsa', '~> 0.2'
|
||||
gem 'oj', '~> 3.14'
|
||||
gem 'ox', '~> 2.14'
|
||||
|
@ -439,8 +439,8 @@ GEM
|
||||
net-protocol
|
||||
net-ssh (7.1.0)
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.14.3)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
nokogiri (1.15.2)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nsa (0.2.8)
|
||||
activesupport (>= 4.2, < 7)
|
||||
@ -642,7 +642,7 @@ GEM
|
||||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
semantic_range (3.0.0)
|
||||
sidekiq (6.5.8)
|
||||
sidekiq (6.5.9)
|
||||
connection_pool (>= 2.2.5, < 3)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.5.0, < 5)
|
||||
@ -829,7 +829,7 @@ DEPENDENCIES
|
||||
mime-types (~> 3.4.1)
|
||||
net-http (~> 0.3.2)
|
||||
net-ldap (~> 0.18)
|
||||
nokogiri (~> 1.14)
|
||||
nokogiri (~> 1.15)
|
||||
nsa (~> 0.2)
|
||||
oj (~> 3.14)
|
||||
omniauth (~> 1.9)
|
||||
|
@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# rubocop:disable Metrics/ModuleLength
|
||||
|
||||
module LanguagesHelper
|
||||
ISO_639_1 = {
|
||||
aa: ['Afar', 'Afaraf'].freeze,
|
||||
|
@ -13,7 +13,7 @@ import { registrationsOpen } from 'flavours/glitch/initial_state';
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
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) => ({
|
||||
|
@ -161,8 +161,9 @@ const makeMapStateToProps = () => {
|
||||
};
|
||||
|
||||
const truncate = (str, num) => {
|
||||
if (str.length > num) {
|
||||
return str.slice(0, num) + '…';
|
||||
const arr = Array.from(str);
|
||||
if (arr.length > num) {
|
||||
return arr.slice(0, num).join('') + '…';
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ const SignInBanner = () => {
|
||||
|
||||
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) {
|
||||
signupButton = (
|
||||
|
@ -14,14 +14,15 @@ export type DecimalUnits = ValueOf<typeof DECIMAL_UNITS>;
|
||||
const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 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
|
||||
* @returns {ShortNumber} Calculated short number
|
||||
* @param sourceNumber Number to convert to short number
|
||||
* @returns Calculated short number
|
||||
* @example
|
||||
* shortNumber(5936);
|
||||
* // => [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 {
|
||||
if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
|
||||
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 {number} division The scale in which short number is displayed
|
||||
* @returns {number} Number that can be used for plurals when short form used
|
||||
* @param sourceNumber Original number that is shortened
|
||||
* @param division The scale in which short number is displayed
|
||||
* @returns Number that can be used for plurals when short form used
|
||||
* @example
|
||||
* pluralReady(1793, DECIMAL_UNITS.THOUSAND)
|
||||
* // => 1790
|
||||
|
@ -13,7 +13,7 @@ import { registrationsOpen } from 'mastodon/initial_state';
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
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) => ({
|
||||
|
@ -166,8 +166,9 @@ const makeMapStateToProps = () => {
|
||||
};
|
||||
|
||||
const truncate = (str, num) => {
|
||||
if (str.length > num) {
|
||||
return str.slice(0, num) + '…';
|
||||
const arr = Array.from(str);
|
||||
if (arr.length > num) {
|
||||
return arr.slice(0, num).join('') + '…';
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ const SignInBanner = () => {
|
||||
|
||||
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) {
|
||||
signupButton = (
|
||||
|
@ -14,14 +14,15 @@ export type DecimalUnits = ValueOf<typeof DECIMAL_UNITS>;
|
||||
const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 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
|
||||
* @returns {ShortNumber} Calculated short number
|
||||
* @param sourceNumber Number to convert to short number
|
||||
* @returns Calculated short number
|
||||
* @example
|
||||
* shortNumber(5936);
|
||||
* // => [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 {
|
||||
if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
|
||||
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 {number} division The scale in which short number is displayed
|
||||
* @returns {number} Number that can be used for plurals when short form used
|
||||
* @param sourceNumber Original number that is shortened
|
||||
* @param division The scale in which short number is displayed
|
||||
* @returns Number that can be used for plurals when short form used
|
||||
* @example
|
||||
* pluralReady(1793, DECIMAL_UNITS.THOUSAND)
|
||||
* // => 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_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 :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_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
|
||||
|
||||
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
|
||||
Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false)
|
||||
|
@ -19,7 +19,7 @@ class FetchResourceService < BaseService
|
||||
|
||||
private
|
||||
|
||||
def process(url, terminal = false)
|
||||
def process(url, terminal: false)
|
||||
@url = url
|
||||
|
||||
perform_request { |response| process_response(response, terminal) }
|
||||
|
@ -11,7 +11,7 @@ module.exports = (api) => {
|
||||
modules: false,
|
||||
debug: false,
|
||||
include: [
|
||||
'proposal-numeric-separator',
|
||||
'transform-numeric-separator',
|
||||
],
|
||||
};
|
||||
|
||||
@ -24,8 +24,8 @@ module.exports = (api) => {
|
||||
plugins: [
|
||||
['react-intl', { messagesDir: './build/messages' }],
|
||||
'preval',
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
'@babel/plugin-proposal-nullish-coalescing-operator',
|
||||
'@babel/plugin-transform-optional-chaining',
|
||||
'@babel/plugin-transform-nullish-coalescing-operator',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
|
@ -113,12 +113,7 @@ module Mastodon::CLI
|
||||
say('OK', :green)
|
||||
say("New password: #{password}")
|
||||
else
|
||||
user.errors.each do |error|
|
||||
say('Failure/Error: ', :red)
|
||||
say(error.attribute)
|
||||
say(" #{error.type}", :red)
|
||||
end
|
||||
|
||||
report_errors(user.errors)
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
@ -189,12 +184,7 @@ module Mastodon::CLI
|
||||
say('OK', :green)
|
||||
say("New password: #{password}") if options[:reset_password]
|
||||
else
|
||||
user.errors.each do |error|
|
||||
say('Failure/Error: ', :red)
|
||||
say(error.attribute)
|
||||
say(" #{error.type}", :red)
|
||||
end
|
||||
|
||||
report_errors(user.errors)
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
@ -217,7 +207,6 @@ module Mastodon::CLI
|
||||
exit(1)
|
||||
end
|
||||
|
||||
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||
account = nil
|
||||
|
||||
if username.present?
|
||||
@ -234,9 +223,9 @@ module Mastodon::CLI
|
||||
end
|
||||
end
|
||||
|
||||
say("Deleting user with #{account.statuses_count} statuses, this might take a while...#{dry_run}")
|
||||
DeleteAccountService.new.call(account, reserve_email: false) unless options[:dry_run]
|
||||
say("OK#{dry_run}", :green)
|
||||
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 dry_run?
|
||||
say("OK#{dry_run_mode_suffix}", :green)
|
||||
end
|
||||
|
||||
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|
|
||||
say("Duplicates found for #{uri}")
|
||||
begin
|
||||
ActivityPub::FetchRemoteAccountService.new.call(uri) unless options[:dry_run]
|
||||
ActivityPub::FetchRemoteAccountService.new.call(uri) unless dry_run?
|
||||
rescue => e
|
||||
say("Error processing #{uri}: #{e}", :red)
|
||||
end
|
||||
@ -332,7 +321,6 @@ module Mastodon::CLI
|
||||
LONG_DESC
|
||||
def cull(*domains)
|
||||
skip_threshold = 7.days.ago
|
||||
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||
skip_domains = Concurrent::Set.new
|
||||
|
||||
query = Account.remote.where(protocol: :activitypub)
|
||||
@ -350,7 +338,7 @@ module Mastodon::CLI
|
||||
end
|
||||
|
||||
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
|
||||
else
|
||||
# Touch account even during dry run to avoid getting the account into the window again
|
||||
@ -358,7 +346,7 @@ module Mastodon::CLI
|
||||
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?
|
||||
say('The following domains were not available during the check:', :yellow)
|
||||
@ -381,21 +369,19 @@ module Mastodon::CLI
|
||||
specified with space-separated USERNAMES.
|
||||
LONG_DESC
|
||||
def refresh(*usernames)
|
||||
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||
|
||||
if options[:domain] || options[:all]
|
||||
scope = Account.remote
|
||||
scope = scope.where(domain: options[:domain]) if options[:domain]
|
||||
|
||||
processed, = parallelize_with_progress(scope) do |account|
|
||||
next if options[:dry_run]
|
||||
next if dry_run?
|
||||
|
||||
account.reset_avatar!
|
||||
account.reset_header!
|
||||
account.save
|
||||
end
|
||||
|
||||
say("Refreshed #{processed} accounts#{dry_run}", :green, true)
|
||||
say("Refreshed #{processed} accounts#{dry_run_mode_suffix}", :green, true)
|
||||
elsif !usernames.empty?
|
||||
usernames.each do |user|
|
||||
user, domain = user.split('@')
|
||||
@ -406,7 +392,7 @@ module Mastodon::CLI
|
||||
exit(1)
|
||||
end
|
||||
|
||||
next if options[:dry_run]
|
||||
next if dry_run?
|
||||
|
||||
begin
|
||||
account.reset_avatar!
|
||||
@ -417,7 +403,7 @@ module Mastodon::CLI
|
||||
end
|
||||
end
|
||||
|
||||
say("OK#{dry_run}", :green)
|
||||
say("OK#{dry_run_mode_suffix}", :green)
|
||||
else
|
||||
say('No account(s) given', :red)
|
||||
exit(1)
|
||||
@ -568,8 +554,6 @@ module Mastodon::CLI
|
||||
- not muted/blocked by us
|
||||
LONG_DESC
|
||||
def prune
|
||||
dry_run = options[:dry_run] ? ' (dry run)' : ''
|
||||
|
||||
query = Account.remote.where.not(actor_type: %i(Application Service))
|
||||
query = query.where('NOT EXISTS (SELECT 1 FROM mentions WHERE account_id = accounts.id)')
|
||||
query = query.where('NOT EXISTS (SELECT 1 FROM favourites WHERE account_id = accounts.id)')
|
||||
@ -585,11 +569,11 @@ module Mastodon::CLI
|
||||
next if account.suspended?
|
||||
next if account.silenced?
|
||||
|
||||
account.destroy unless options[:dry_run]
|
||||
account.destroy unless dry_run?
|
||||
1
|
||||
end
|
||||
|
||||
say("OK, pruned #{deleted} accounts#{dry_run}", :green)
|
||||
say("OK, pruned #{deleted} accounts#{dry_run_mode_suffix}", :green)
|
||||
end
|
||||
|
||||
option :force, type: :boolean
|
||||
@ -667,6 +651,14 @@ module Mastodon::CLI
|
||||
|
||||
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)
|
||||
if account.nil?
|
||||
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.
|
||||
LONG_DESC
|
||||
def purge(*domains)
|
||||
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||
domains = domains.map { |domain| TagManager.instance.normalize_domain(domain) }
|
||||
account_scope = Account.none
|
||||
domain_block_scope = DomainBlock.none
|
||||
@ -79,23 +78,23 @@ module Mastodon::CLI
|
||||
|
||||
# Actually perform the deletions
|
||||
processed, = parallelize_with_progress(account_scope) do |account|
|
||||
DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless options[:dry_run]
|
||||
DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless dry_run?
|
||||
end
|
||||
|
||||
say("Removed #{processed} accounts#{dry_run}", :green)
|
||||
say("Removed #{processed} accounts#{dry_run_mode_suffix}", :green)
|
||||
|
||||
if options[:purge_domain_blocks]
|
||||
domain_block_count = domain_block_scope.count
|
||||
domain_block_scope.in_batches.destroy_all unless options[:dry_run]
|
||||
say("Removed #{domain_block_count} domain blocks#{dry_run}", :green)
|
||||
domain_block_scope.in_batches.destroy_all unless dry_run?
|
||||
say("Removed #{domain_block_count} domain blocks#{dry_run_mode_suffix}", :green)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
option :concurrency, type: :numeric, default: 50, aliases: [:c]
|
||||
|
@ -39,7 +39,7 @@ module Mastodon::CLI
|
||||
processed = 0
|
||||
|
||||
domains.each do |domain|
|
||||
if EmailDomainBlock.where(domain: domain).exists?
|
||||
if EmailDomainBlock.exists?(domain: domain)
|
||||
say("#{domain} is already blocked.", :yellow)
|
||||
skipped += 1
|
||||
next
|
||||
@ -60,7 +60,7 @@ module Mastodon::CLI
|
||||
(email_domain_block.other_domains || []).uniq.each do |hostname|
|
||||
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)
|
||||
skipped += 1
|
||||
next
|
||||
|
@ -18,14 +18,12 @@ module Mastodon::CLI
|
||||
Otherwise, a single user specified by USERNAME.
|
||||
LONG_DESC
|
||||
def build(username = nil)
|
||||
dry_run = options[:dry_run] ? '(DRY RUN)' : ''
|
||||
|
||||
if options[:all] || username.nil?
|
||||
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
|
||||
|
||||
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?
|
||||
account = Account.find_local(username)
|
||||
|
||||
@ -34,9 +32,9 @@ module Mastodon::CLI
|
||||
exit(1)
|
||||
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
|
||||
say('No account(s) given', :red)
|
||||
exit(1)
|
||||
|
@ -15,6 +15,10 @@ module Mastodon::CLI
|
||||
options[:dry_run]
|
||||
end
|
||||
|
||||
def dry_run_mode_suffix
|
||||
dry_run? ? ' (DRY RUN)' : ''
|
||||
end
|
||||
|
||||
def create_progress_bar(total = nil)
|
||||
ProgressBar.create(total: total, format: '%c/%u |%b%i| %e')
|
||||
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
|
||||
|
||||
unless options[:dry_run]
|
||||
unless dry_run?
|
||||
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('A running Sidekiq process is required. Do not shut it down until queues clear.')
|
||||
@ -104,12 +104,11 @@ module Mastodon::CLI
|
||||
|
||||
inboxes = Account.inboxes
|
||||
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?
|
||||
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('You can shut it down and delete it any time')
|
||||
return
|
||||
@ -126,7 +125,7 @@ module Mastodon::CLI
|
||||
|
||||
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|
|
||||
[json, account.id, inbox_url]
|
||||
end
|
||||
@ -140,7 +139,7 @@ module Mastodon::CLI
|
||||
Account.local.without_suspended.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')
|
||||
rescue TTY::Reader::InputInterrupt
|
||||
exit(1)
|
||||
|
@ -1,6 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'tty-prompt'
|
||||
require_relative 'base'
|
||||
|
||||
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.
|
||||
LONG_DESC
|
||||
def fix_duplicates
|
||||
@prompt = TTY::Prompt.new
|
||||
|
||||
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.'
|
||||
@prompt.error 'Please update to at least Mastodon 3.0.0 before running this script.'
|
||||
say 'Your version of the database schema is too old and is not supported by this script.', :red
|
||||
say 'Please update to at least Mastodon 3.0.0 before running this script.', :red
|
||||
exit(1)
|
||||
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.'
|
||||
exit(1) unless @prompt.yes?('Continue anyway? (Yes/No)')
|
||||
say 'Your version of the database schema is more recent than this script, this may cause unexpected errors.', :yellow
|
||||
exit(1) unless yes?('Continue anyway? (Yes/No)')
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
@prompt.warn 'This task will take a long time to run and is potentially destructive.'
|
||||
@prompt.warn 'Please make sure to stop Mastodon and have a backup.'
|
||||
exit(1) unless @prompt.yes?('Continue? (Yes/No)')
|
||||
say 'This task will take a long time to run and is potentially destructive.', :yellow
|
||||
say 'Please make sure to stop Mastodon and have a backup.', :yellow
|
||||
exit(1) unless yes?('Continue? (Yes/No)')
|
||||
|
||||
deduplicate_users!
|
||||
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
|
||||
Rails.cache.clear
|
||||
|
||||
@prompt.say 'Finished!'
|
||||
say 'Finished!'
|
||||
end
|
||||
|
||||
private
|
||||
@ -184,7 +181,7 @@ module Mastodon::CLI
|
||||
def deduplicate_accounts!
|
||||
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|
|
||||
accounts = Account.where(id: row['ids'].split(',')).to_a
|
||||
@ -196,14 +193,14 @@ module Mastodon::CLI
|
||||
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
|
||||
ActiveRecord::Base.connection.add_index :accounts, 'lower (username), lower(domain)', name: 'index_accounts_on_username_and_domain_lower', unique: true
|
||||
else
|
||||
ActiveRecord::Base.connection.add_index :accounts, "lower (username), COALESCE(lower(domain), '')", name: 'index_accounts_on_username_and_domain_lower', unique: true
|
||||
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 index_accounts_on_uri;')
|
||||
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_reset_password_token')
|
||||
|
||||
@prompt.say 'Deduplicating user records…'
|
||||
say 'Deduplicating user records…'
|
||||
|
||||
# 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|
|
||||
users = User.where(id: row['ids'].split(',')).sort_by(&:updated_at).reverse
|
||||
ref_user = users.shift
|
||||
@prompt.warn "Multiple users registered with e-mail address #{ref_user.email}."
|
||||
@prompt.warn "e-mail will be disabled for the following accounts: #{user.map(&:account).map(&:acct).join(', ')}"
|
||||
@prompt.warn 'Please reach out to them and set another address with `tootctl account modify` or delete them.'
|
||||
say "Multiple users registered with e-mail address #{ref_user.email}.", :yellow
|
||||
say "e-mail will be disabled for the following accounts: #{user.map(&:account).map(&:acct).join(', ')}", :yellow
|
||||
say 'Please reach out to them and set another address with `tootctl account modify` or delete them.', :yellow
|
||||
|
||||
i = 0
|
||||
users.each do |user|
|
||||
user.update!(email: "#{i} " + user.email)
|
||||
users.each_with_index do |user, index|
|
||||
user.update!(email: "#{index} " + user.email)
|
||||
end
|
||||
end
|
||||
|
||||
@ -235,7 +231,7 @@ module Mastodon::CLI
|
||||
deduplicate_users_process_remember_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, ['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
|
||||
@ -250,7 +246,7 @@ module Mastodon::CLI
|
||||
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|
|
||||
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|
|
||||
user.update!(confirmation_token: nil)
|
||||
@ -262,7 +258,7 @@ module Mastodon::CLI
|
||||
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|
|
||||
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|
|
||||
user.update!(remember_token: nil)
|
||||
@ -274,7 +270,7 @@ module Mastodon::CLI
|
||||
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|
|
||||
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|
|
||||
user.update!(reset_password_token: nil)
|
||||
@ -285,12 +281,12 @@ module Mastodon::CLI
|
||||
def deduplicate_account_domain_blocks!
|
||||
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|
|
||||
AccountDomainBlock.where(id: row['ids'].split(',').drop(1)).delete_all
|
||||
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
|
||||
end
|
||||
|
||||
@ -299,12 +295,12 @@ module Mastodon::CLI
|
||||
|
||||
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|
|
||||
AccountIdentityProof.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
|
||||
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
|
||||
end
|
||||
|
||||
@ -313,19 +309,19 @@ module Mastodon::CLI
|
||||
|
||||
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|
|
||||
AnnouncementReaction.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
|
||||
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
|
||||
end
|
||||
|
||||
def deduplicate_conversations!
|
||||
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|
|
||||
conversations = Conversation.where(id: row['ids'].split(',')).sort_by(&:id).reverse
|
||||
|
||||
@ -337,7 +333,7 @@ module Mastodon::CLI
|
||||
end
|
||||
end
|
||||
|
||||
@prompt.say 'Restoring conversations indexes…'
|
||||
say 'Restoring conversations indexes…'
|
||||
if ActiveRecord::Migrator.current_version < 2022_03_07_083603
|
||||
ActiveRecord::Base.connection.add_index :conversations, ['uri'], name: 'index_conversations_on_uri', unique: true
|
||||
else
|
||||
@ -348,7 +344,7 @@ module Mastodon::CLI
|
||||
def deduplicate_custom_emojis!
|
||||
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|
|
||||
emojis = CustomEmoji.where(id: row['ids'].split(',')).sort_by(&:id).reverse
|
||||
|
||||
@ -360,14 +356,14 @@ module Mastodon::CLI
|
||||
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
|
||||
end
|
||||
|
||||
def deduplicate_custom_emoji_categories!
|
||||
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|
|
||||
categories = CustomEmojiCategory.where(id: row['ids'].split(',')).sort_by(&:id).reverse
|
||||
|
||||
@ -379,26 +375,26 @@ module Mastodon::CLI
|
||||
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
|
||||
end
|
||||
|
||||
def deduplicate_domain_allows!
|
||||
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|
|
||||
DomainAllow.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
|
||||
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
|
||||
end
|
||||
|
||||
def deduplicate_domain_blocks!
|
||||
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|
|
||||
domain_blocks = DomainBlock.where(id: row['ids'].split(',')).by_severity.reverse.to_a
|
||||
|
||||
@ -415,7 +411,7 @@ module Mastodon::CLI
|
||||
domain_blocks.each(&:destroy)
|
||||
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
|
||||
end
|
||||
|
||||
@ -424,37 +420,37 @@ module Mastodon::CLI
|
||||
|
||||
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|
|
||||
UnavailableDomain.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
|
||||
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
|
||||
end
|
||||
|
||||
def deduplicate_email_domain_blocks!
|
||||
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|
|
||||
domain_blocks = EmailDomainBlock.where(id: row['ids'].split(',')).sort_by { |b| b.parent.nil? ? 1 : 0 }.to_a
|
||||
domain_blocks.drop(1).each(&:destroy)
|
||||
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
|
||||
end
|
||||
|
||||
def deduplicate_media_attachments!
|
||||
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|
|
||||
MediaAttachment.where(id: row['ids'].split(',').drop(1)).update_all(shortcode: nil)
|
||||
end
|
||||
|
||||
@prompt.say 'Restoring media_attachments indexes…'
|
||||
say 'Restoring media_attachments indexes…'
|
||||
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
|
||||
else
|
||||
@ -465,19 +461,19 @@ module Mastodon::CLI
|
||||
def deduplicate_preview_cards!
|
||||
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|
|
||||
PreviewCard.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
|
||||
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
|
||||
end
|
||||
|
||||
def deduplicate_statuses!
|
||||
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|
|
||||
statuses = Status.where(id: row['ids'].split(',')).sort_by(&:id)
|
||||
ref_status = statuses.shift
|
||||
@ -487,7 +483,7 @@ module Mastodon::CLI
|
||||
end
|
||||
end
|
||||
|
||||
@prompt.say 'Restoring statuses indexes…'
|
||||
say 'Restoring statuses indexes…'
|
||||
if ActiveRecord::Migrator.current_version < 2022_03_10_060706
|
||||
ActiveRecord::Base.connection.add_index :statuses, ['uri'], name: 'index_statuses_on_uri', unique: true
|
||||
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_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|
|
||||
tags = Tag.where(id: row['ids'].split(',')).sort_by { |t| [t.usable?, t.trendable?, t.listable?].count(false) }
|
||||
ref_tag = tags.shift
|
||||
@ -509,7 +505,7 @@ module Mastodon::CLI
|
||||
end
|
||||
end
|
||||
|
||||
@prompt.say 'Restoring tags indexes…'
|
||||
say 'Restoring tags indexes…'
|
||||
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
|
||||
else
|
||||
@ -522,12 +518,12 @@ module Mastodon::CLI
|
||||
|
||||
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|
|
||||
WebauthnCredential.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
|
||||
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
|
||||
end
|
||||
|
||||
@ -536,28 +532,37 @@ module Mastodon::CLI
|
||||
|
||||
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|
|
||||
Webhooks.where(id: row['ids'].split(',')).sort_by(&:id).reverse.drop(1).each(&:destroy)
|
||||
end
|
||||
|
||||
@prompt.say 'Restoring webhooks indexes…'
|
||||
say 'Restoring webhooks indexes…'
|
||||
ActiveRecord::Base.connection.add_index :webhooks, ['url'], name: 'index_webhooks_on_url', unique: true
|
||||
end
|
||||
|
||||
def deduplicate_local_accounts!(accounts)
|
||||
accounts = accounts.sort_by(&:id).reverse
|
||||
|
||||
@prompt.warn "Multiple local accounts were found for username '#{accounts.first.username}'."
|
||||
@prompt.warn 'All those accounts are distinct accounts but only the most recently-created one is fully-functional.'
|
||||
say "Multiple local accounts were found for username '#{accounts.first.username}'.", :yellow
|
||||
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|
|
||||
@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
|
||||
|
||||
@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.default 0
|
||||
q.convert :int
|
||||
|
@ -35,12 +35,12 @@ module Mastodon::CLI
|
||||
say('--prune-profiles and --remove-headers should not be specified simultaneously', :red, true)
|
||||
exit(1)
|
||||
end
|
||||
|
||||
if options[:include_follows] && !(options[:prune_profiles] || options[:remove_headers])
|
||||
say('--include-follows can only be used with --prune-profiles or --remove-headers', :red, true)
|
||||
exit(1)
|
||||
end
|
||||
time_ago = options[:days].days.ago
|
||||
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||
time_ago = options[:days].days.ago
|
||||
|
||||
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|
|
||||
@ -51,7 +51,7 @@ module Mastodon::CLI
|
||||
size = (account.header_file_size || 0)
|
||||
size += (account.avatar_file_size || 0) if options[:prune_profiles]
|
||||
|
||||
unless options[:dry_run]
|
||||
unless dry_run?
|
||||
account.header.destroy
|
||||
account.avatar.destroy if options[:prune_profiles]
|
||||
account.save!
|
||||
@ -60,7 +60,7 @@ module Mastodon::CLI
|
||||
size
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
unless options[:dry_run]
|
||||
unless dry_run?
|
||||
media_attachment.file.destroy
|
||||
media_attachment.thumbnail.destroy
|
||||
media_attachment.save
|
||||
@ -78,7 +78,7 @@ module Mastodon::CLI
|
||||
size
|
||||
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
|
||||
|
||||
@ -97,7 +97,6 @@ module Mastodon::CLI
|
||||
progress = create_progress_bar(nil)
|
||||
reclaimed_bytes = 0
|
||||
removed = 0
|
||||
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||
prefix = options[:prefix]
|
||||
|
||||
case Paperclip::Attachment.default_options[:storage]
|
||||
@ -123,7 +122,7 @@ module Mastodon::CLI
|
||||
record_map = preload_records_from_mixed_objects(objects)
|
||||
|
||||
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.delete('cache')
|
||||
@ -145,7 +144,7 @@ module Mastodon::CLI
|
||||
next unless attachment.blank? || !attachment.variant?(file_name)
|
||||
|
||||
begin
|
||||
object.delete unless options[:dry_run]
|
||||
object.delete unless dry_run?
|
||||
|
||||
reclaimed_bytes += object.size
|
||||
removed += 1
|
||||
@ -194,7 +193,7 @@ module Mastodon::CLI
|
||||
begin
|
||||
size = File.size(path)
|
||||
|
||||
unless options[:dry_run]
|
||||
unless dry_run?
|
||||
File.delete(path)
|
||||
begin
|
||||
FileUtils.rmdir(File.dirname(path), parents: true)
|
||||
@ -216,7 +215,7 @@ module Mastodon::CLI
|
||||
progress.total = progress.progress
|
||||
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
|
||||
|
||||
option :account, type: :string
|
||||
@ -246,8 +245,6 @@ module Mastodon::CLI
|
||||
not be re-downloaded. To force re-download of every URL, use --force.
|
||||
DESC
|
||||
def refresh
|
||||
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||
|
||||
if options[:status]
|
||||
scope = MediaAttachment.where(status_id: options[:status])
|
||||
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 DomainBlock.reject_media?(media_attachment.account.domain)
|
||||
|
||||
unless options[:dry_run]
|
||||
unless dry_run?
|
||||
media_attachment.reset_file!
|
||||
media_attachment.reset_thumbnail!
|
||||
media_attachment.save
|
||||
@ -283,7 +280,7 @@ module Mastodon::CLI
|
||||
media_attachment.file_file_size + (media_attachment.thumbnail_file_size || 0)
|
||||
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
|
||||
|
||||
desc 'usage', 'Calculate disk space consumed by Mastodon'
|
||||
|
@ -27,7 +27,6 @@ module Mastodon::CLI
|
||||
DESC
|
||||
def remove
|
||||
time_ago = options[:days].days.ago
|
||||
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
|
||||
link = options[:link] ? 'link-type ' : ''
|
||||
scope = PreviewCard.cached
|
||||
scope = scope.where(type: :link) if options[:link]
|
||||
@ -38,7 +37,7 @@ module Mastodon::CLI
|
||||
|
||||
size = preview_card.image_file_size
|
||||
|
||||
unless options[:dry_run]
|
||||
unless dry_run?
|
||||
preview_card.image.destroy
|
||||
preview_card.save
|
||||
end
|
||||
@ -46,7 +45,7 @@ module Mastodon::CLI
|
||||
size
|
||||
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
|
||||
|
@ -17,7 +17,6 @@ module Mastodon::CLI
|
||||
LONG_DESC
|
||||
def storage_schema
|
||||
progress = create_progress_bar(nil)
|
||||
dry_run = dry_run? ? ' (DRY RUN)' : ''
|
||||
records = 0
|
||||
|
||||
klasses = [
|
||||
@ -69,7 +68,7 @@ module Mastodon::CLI
|
||||
progress.total = progress.progress
|
||||
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
|
||||
|
||||
private
|
||||
|
44
package.json
44
package.json
@ -2,7 +2,7 @@
|
||||
"name": "@mastodon/mastodon",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=16"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "git push --tags",
|
||||
@ -26,14 +26,14 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.8",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
|
||||
"@babel/core": "^7.22.1",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.22.3",
|
||||
"@babel/plugin-transform-react-inline-elements": "^7.21.0",
|
||||
"@babel/plugin-transform-runtime": "^7.21.4",
|
||||
"@babel/preset-env": "^7.21.5",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/plugin-transform-runtime": "^7.22.4",
|
||||
"@babel/preset-env": "^7.22.4",
|
||||
"@babel/preset-react": "^7.22.3",
|
||||
"@babel/preset-typescript": "^7.21.5",
|
||||
"@babel/runtime": "^7.21.5",
|
||||
"@babel/runtime": "^7.22.3",
|
||||
"@gamestdio/websocket": "^0.3.2",
|
||||
"@github/webauthn-json": "^2.1.1",
|
||||
"@rails/ujs": "^6.1.7",
|
||||
@ -76,7 +76,7 @@
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"intl-relativeformat": "^6.4.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsdom": "^22.0.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mark-loader": "^0.1.6",
|
||||
"marky": "^1.2.5",
|
||||
@ -86,7 +86,7 @@
|
||||
"path-complete-extname": "^1.0.0",
|
||||
"pg": "^8.5.0",
|
||||
"pg-connection-string": "^2.6.0",
|
||||
"postcss": "^8.4.23",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-loader": "^4.3.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"punycode": "^2.3.0",
|
||||
@ -133,18 +133,18 @@
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-merge": "^5.9.0",
|
||||
"wicg-inert": "^3.1.2",
|
||||
"workbox-expiration": "^6.5.4",
|
||||
"workbox-precaching": "^6.5.4",
|
||||
"workbox-routing": "^6.5.4",
|
||||
"workbox-strategies": "^6.5.4",
|
||||
"workbox-webpack-plugin": "^6.5.4",
|
||||
"workbox-window": "^6.5.4",
|
||||
"workbox-expiration": "^6.6.0",
|
||||
"workbox-precaching": "^6.6.0",
|
||||
"workbox-routing": "^6.6.0",
|
||||
"workbox-strategies": "^6.6.0",
|
||||
"workbox-webpack-plugin": "^6.6.0",
|
||||
"workbox-window": "^6.6.0",
|
||||
"ws": "^8.12.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@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/escape-html": "^1.0.2",
|
||||
"@types/express": "^4.17.17",
|
||||
@ -152,18 +152,18 @@
|
||||
"@types/intl": "^1.2.0",
|
||||
"@types/jest": "^29.5.1",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@types/npmlog": "^4.1.4",
|
||||
"@types/object-assign": "^4.0.30",
|
||||
"@types/pg": "^8.6.6",
|
||||
"@types/prop-types": "^15.7.5",
|
||||
"@types/punycode": "^2.1.0",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react": "^18.2.7",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/react-immutable-proptypes": "^2.1.0",
|
||||
"@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-router-dom": "^5.3.3",
|
||||
"@types/react-select": "^5.0.1",
|
||||
@ -177,15 +177,15 @@
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@types/webpack": "^4.41.33",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.7",
|
||||
"@typescript-eslint/parser": "^5.59.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||
"@typescript-eslint/parser": "^5.59.8",
|
||||
"babel-jest": "^29.5.0",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.5",
|
||||
"eslint-plugin-formatjs": "^4.10.1",
|
||||
"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-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "~6.1.1",
|
||||
|
@ -18,4 +18,37 @@ describe Admin::IpBlocksController do
|
||||
expect(response).to have_http_status(:success)
|
||||
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
|
||||
|
@ -18,4 +18,42 @@ describe Admin::RelaysController do
|
||||
expect(response).to have_http_status(:success)
|
||||
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
|
||||
|
@ -18,4 +18,68 @@ describe Admin::RulesController do
|
||||
expect(response).to have_http_status(:success)
|
||||
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
|
||||
|
@ -18,4 +18,82 @@ describe Admin::WebhooksController do
|
||||
expect(response).to have_http_status(:success)
|
||||
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
|
||||
|
@ -4,9 +4,662 @@ require 'rails_helper'
|
||||
require 'mastodon/cli/accounts'
|
||||
|
||||
describe Mastodon::CLI::Accounts do
|
||||
let(:cli) { described_class.new }
|
||||
|
||||
describe '.exit_on_failure?' do
|
||||
it 'returns true' do
|
||||
expect(described_class.exit_on_failure?).to be true
|
||||
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
|
||||
|
@ -62,6 +62,10 @@ RSpec.configure do |config|
|
||||
config.infer_spec_type_from_file_location!
|
||||
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: :helper
|
||||
config.include Devise::Test::ControllerHelpers, type: :view
|
||||
@ -73,6 +77,10 @@ RSpec.configure do |config|
|
||||
config.include Redisable
|
||||
config.include SignedRequestHelpers, type: :request
|
||||
|
||||
config.before :each, type: :cli do
|
||||
stub_stdout
|
||||
end
|
||||
|
||||
config.before :each, type: :feature do
|
||||
https = ENV['LOCAL_HTTPS'] == 'true'
|
||||
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
|
||||
end
|
||||
|
||||
def stub_stdout
|
||||
allow($stdout).to receive(:write)
|
||||
end
|
||||
|
||||
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://w3id.org/identity/v1').to_return(request_fixture('json-ld.identity.txt'))
|
||||
|
Loading…
Reference in New Issue
Block a user