Merge branch 'main' into glitch-soc/merge-upstream

Conflicts:
- `app/helpers/accounts_helper.rb`:
  Conflict due to upstream changing how followers count is displayed while we
  have an option to hide followers count.
  Ported upstream change.
- `app/views/accounts/_header.html.haml`:
  Conflict due to upstream changing how followers count is displayed while we
  have an option to hide followers count.
  Ported upstream change.
- `app/views/directories/index.html.haml`:
  Conflict due to upstream changing how followers count is displayed while we
  have an option to hide followers count.
  Ported upstream change.
master
Claire 3 years ago
commit 3160e050a9
  1. 4
      Gemfile
  2. 128
      Gemfile.lock
  3. 10
      app/controllers/activitypub/outboxes_controller.rb
  4. 4
      app/controllers/admin/resets_controller.rb
  5. 27
      app/controllers/admin/sign_in_token_authentications_controller.rb
  6. 2
      app/controllers/admin/two_factor_authentications_controller.rb
  7. 3
      app/controllers/well_known/webfinger_controller.rb
  8. 6
      app/helpers/accounts_helper.rb
  9. 11
      app/helpers/application_helper.rb
  10. 1
      app/javascript/styles/mastodon/components.scss
  11. 1
      app/models/account_stat.rb
  12. 25
      app/models/user.rb
  13. 8
      app/policies/user_policy.rb
  14. 4
      app/views/about/more.html.haml
  15. 4
      app/views/about/show.html.haml
  16. 10
      app/views/accounts/_header.html.haml
  17. 2
      app/views/accounts/show.html.haml
  18. 24
      app/views/admin/accounts/show.html.haml
  19. 16
      app/views/admin/dashboard/index.html.haml
  20. 4
      app/views/admin/follow_recommendations/_account.html.haml
  21. 2
      app/views/admin/instances/_instance.html.haml
  22. 2
      app/views/admin/tags/_tag.html.haml
  23. 4
      app/views/directories/index.html.haml
  24. 4
      app/views/relationships/_account.html.haml
  25. 2
      app/views/settings/featured_tags/index.html.haml
  26. 6
      app/views/statuses/_detailed_status.html.haml
  27. 42
      config/locales/en.yml
  28. 1
      config/routes.rb
  29. 5
      db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb
  30. 1
      db/schema.rb
  31. 1
      dist/mastodon-sidekiq.service
  32. 1
      dist/mastodon-web.service
  33. 15
      lib/mastodon/accounts_cli.rb
  34. 13
      lib/mastodon/domains_cli.rb
  35. 6
      package.json
  36. 16
      spec/controllers/activitypub/outboxes_controller_spec.rb
  37. 2
      spec/controllers/admin/resets_controller_spec.rb
  38. 8
      spec/controllers/admin/two_factor_authentications_controller_spec.rb
  39. 4
      spec/controllers/well_known/webfinger_controller_spec.rb
  40. 2
      spec/models/tag_feed_spec.rb
  41. 28
      spec/models/user_spec.rb
  42. 33
      spec/services/bootstrap_timeline_service_spec.rb
  43. 39
      yarn.lock

@ -6,7 +6,7 @@ ruby '>= 2.5.0', '< 3.1.0'
gem 'pkg-config', '~> 1.4' gem 'pkg-config', '~> 1.4'
gem 'puma', '~> 5.3' gem 'puma', '~> 5.3'
gem 'rails', '~> 6.1.3' gem 'rails', '~> 6.1.4'
gem 'sprockets', '~> 3.7.2' gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.1' gem 'thor', '~> 1.1'
gem 'rack', '~> 2.2.3' gem 'rack', '~> 2.2.3'
@ -24,7 +24,7 @@ gem 'paperclip', '~> 6.0'
gem 'blurhash', '~> 0.1' gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.7' gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.6.0', require: false gem 'bootsnap', '~> 1.6.0', require: false
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.7' gem 'charlock_holmes', '~> 0.7.7'

@ -1,40 +1,40 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (6.1.3.2) actioncable (6.1.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.1.3.2) actionmailbox (6.1.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4)
activejob (= 6.1.3.2) activejob (= 6.1.4)
activerecord (= 6.1.3.2) activerecord (= 6.1.4)
activestorage (= 6.1.3.2) activestorage (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.1.3.2) actionmailer (6.1.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4)
actionview (= 6.1.3.2) actionview (= 6.1.4)
activejob (= 6.1.3.2) activejob (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.1.3.2) actionpack (6.1.4)
actionview (= 6.1.3.2) actionview (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
rack (~> 2.0, >= 2.0.9) rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.3.2) actiontext (6.1.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4)
activerecord (= 6.1.3.2) activerecord (= 6.1.4)
activestorage (= 6.1.3.2) activestorage (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.1.3.2) actionview (6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -45,28 +45,28 @@ GEM
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.8) active_record_query_trace (1.8)
activejob (6.1.3.2) activejob (6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.1.3.2) activemodel (6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
activerecord (6.1.3.2) activerecord (6.1.4)
activemodel (= 6.1.3.2) activemodel (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
activestorage (6.1.3.2) activestorage (6.1.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4)
activejob (= 6.1.3.2) activejob (= 6.1.4)
activerecord (= 6.1.3.2) activerecord (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
marcel (~> 1.0.0) marcel (~> 1.0.0)
mini_mime (~> 1.0.2) mini_mime (>= 1.1.0)
activesupport (6.1.3.2) activesupport (6.1.4)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
tzinfo (~> 2.0) tzinfo (~> 2.0)
zeitwerk (~> 2.3) zeitwerk (~> 2.3)
addressable (2.7.0) addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
airbrussh (1.4.0) airbrussh (1.4.0)
sshkit (>= 1.6.1, != 1.7.0) sshkit (>= 1.6.1, != 1.7.0)
@ -353,7 +353,7 @@ GEM
mimemagic (0.3.10) mimemagic (0.3.10)
nokogiri (~> 1) nokogiri (~> 1)
rake rake
mini_mime (1.0.3) mini_mime (1.1.0)
mini_portile2 (2.5.3) mini_portile2 (2.5.3)
minitest (5.14.4) minitest (5.14.4)
msgpack (1.4.2) msgpack (1.4.2)
@ -374,7 +374,7 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5) sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0) statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.11.7) oj (3.11.8)
omniauth (1.9.1) omniauth (1.9.1)
hashie (>= 3.4.6) hashie (>= 3.4.6)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
@ -443,20 +443,20 @@ GEM
rack rack
rack-test (1.1.0) rack-test (1.1.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rails (6.1.3.2) rails (6.1.4)
actioncable (= 6.1.3.2) actioncable (= 6.1.4)
actionmailbox (= 6.1.3.2) actionmailbox (= 6.1.4)
actionmailer (= 6.1.3.2) actionmailer (= 6.1.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4)
actiontext (= 6.1.3.2) actiontext (= 6.1.4)
actionview (= 6.1.3.2) actionview (= 6.1.4)
activejob (= 6.1.3.2) activejob (= 6.1.4)
activemodel (= 6.1.3.2) activemodel (= 6.1.4)
activerecord (= 6.1.3.2) activerecord (= 6.1.4)
activestorage (= 6.1.3.2) activestorage (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 6.1.3.2) railties (= 6.1.4)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
@ -472,11 +472,11 @@ GEM
railties (>= 6.0.0, < 7) railties (>= 6.0.0, < 7)
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
rails (>= 4.2.0) rails (>= 4.2.0)
railties (6.1.3.2) railties (6.1.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
method_source method_source
rake (>= 0.8.7) rake (>= 0.13)
thor (~> 1.0) thor (~> 1.0)
rainbow (3.0.0) rainbow (3.0.0)
rake (13.0.3) rake (13.0.3)
@ -525,7 +525,7 @@ GEM
rspec-support (3.10.2) rspec-support (3.10.2)
rspec_junit_formatter (0.4.1) rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0) rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.18.1) rubocop (1.18.2)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.0.0.0) parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
@ -536,7 +536,7 @@ GEM
unicode-display_width (>= 1.4.0, < 3.0) unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.7.0) rubocop-ast (1.7.0)
parser (>= 3.0.1.1) parser (>= 3.0.1.1)
rubocop-rails (2.11.1) rubocop-rails (2.11.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
@ -570,7 +570,7 @@ GEM
sidekiq (>= 3) sidekiq (>= 3)
thwait thwait
tilt (>= 1.4.0) tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.1) sidekiq-unique-jobs (7.1.2)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0) brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 5.0, < 7.0) sidekiq (>= 5.0, < 7.0)
@ -659,7 +659,7 @@ GEM
webpush (0.3.8) webpush (0.3.8)
hkdf (~> 0.2) hkdf (~> 0.2)
jwt (~> 2.0) jwt (~> 2.0)
websocket-driver (0.7.3) websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
wisper (2.0.1) wisper (2.0.1)
@ -674,7 +674,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
active_model_serializers (~> 0.10) active_model_serializers (~> 0.10)
active_record_query_trace (~> 1.8) active_record_query_trace (~> 1.8)
addressable (~> 2.7) addressable (~> 2.8)
annotate (~> 3.1) annotate (~> 3.1)
aws-sdk-s3 (~> 1.96) aws-sdk-s3 (~> 1.96)
better_errors (~> 2.9) better_errors (~> 2.9)
@ -758,7 +758,7 @@ DEPENDENCIES
rack (~> 2.2.3) rack (~> 2.2.3)
rack-attack (~> 6.5) rack-attack (~> 6.5)
rack-cors (~> 1.1) rack-cors (~> 1.1)
rails (~> 6.1.3) rails (~> 6.1.4)
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
rails-i18n (~> 6.0) rails-i18n (~> 6.0)
rails-settings-cached (~> 0.6) rails-settings-cached (~> 0.6)

@ -11,7 +11,11 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
before_action :set_cache_headers before_action :set_cache_headers
def show def show
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?)) if page_requested?
expires_in(1.minute, public: public_fetch_mode? && signed_request_account.nil?)
else
expires_in(3.minutes, public: public_fetch_mode?)
end
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
@ -76,4 +80,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def set_account def set_account
@account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative @account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
end end
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode? || page_requested?
end
end end

@ -6,9 +6,9 @@ module Admin
def create def create
authorize @user, :reset_password? authorize @user, :reset_password?
@user.send_reset_password_instructions @user.reset_password!
log_action :reset_password, @user log_action :reset_password, @user
redirect_to admin_accounts_path redirect_to admin_account_path(@user.account_id)
end end
end end
end end

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Admin
class SignInTokenAuthenticationsController < BaseController
before_action :set_target_user
def create
authorize @user, :enable_sign_in_token_auth?
@user.update(skip_sign_in_token: false)
log_action :enable_sign_in_token_auth, @user
redirect_to admin_account_path(@user.account_id)
end
def destroy
authorize @user, :disable_sign_in_token_auth?
@user.update(skip_sign_in_token: true)
log_action :disable_sign_in_token_auth, @user
redirect_to admin_account_path(@user.account_id)
end
private
def set_target_user
@user = User.find(params[:user_id])
end
end
end

@ -9,7 +9,7 @@ module Admin
@user.disable_two_factor! @user.disable_two_factor!
log_action :disable_2fa, @user log_action :disable_2fa, @user
UserMailer.two_factor_disabled(@user).deliver_later! UserMailer.two_factor_disabled(@user).deliver_later!
redirect_to admin_accounts_path redirect_to admin_account_path(@user.account_id)
end end
private private

@ -4,7 +4,6 @@ module WellKnown
class WebfingerController < ActionController::Base class WebfingerController < ActionController::Base
include RoutingHelper include RoutingHelper
before_action { response.headers['Vary'] = 'Accept' }
before_action :set_account before_action :set_account
before_action :check_account_suspension before_action :check_account_suspension
@ -39,10 +38,12 @@ module WellKnown
end end
def bad_request def bad_request
expires_in(3.minutes, public: true)
head 400 head 400
end end
def not_found def not_found
expires_in(3.minutes, public: true)
head 404 head 404
end end

@ -84,19 +84,19 @@ module AccountsHelper
def account_description(account) def account_description(account)
prepend_stats = [ prepend_stats = [
[ [
number_to_human(account.statuses_count, strip_insignificant_zeros: true), number_to_human(account.statuses_count, precision: 3, strip_insignificant_zeros: true),
I18n.t('accounts.posts', count: account.statuses_count), I18n.t('accounts.posts', count: account.statuses_count),
].join(' '), ].join(' '),
[ [
number_to_human(account.following_count, strip_insignificant_zeros: true), number_to_human(account.following_count, precision: 3, strip_insignificant_zeros: true),
I18n.t('accounts.following', count: account.following_count), I18n.t('accounts.following', count: account.following_count),
].join(' '), ].join(' '),
] ]
unless hide_followers_count?(account) unless hide_followers_count?(account)
prepend_stats << [ prepend_stats << [
number_to_human(account.followers_count, strip_insignificant_zeros: true), number_to_human(account.followers_count, precision: 3, strip_insignificant_zeros: true),
I18n.t('accounts.followers', count: account.followers_count), I18n.t('accounts.followers', count: account.followers_count),
].join(' ') ].join(' ')
end end

@ -14,6 +14,17 @@ module ApplicationHelper
ku ku
).freeze ).freeze
def friendly_number_to_human(number, **options)
# By default, the number of precision digits used by number_to_human
# is looked up from the locales definition, and rails-i18n comes with
# values that don't seem to make much sense for many languages, so
# override these values with a default of 3 digits of precision.
options[:precision] = 3
options[:strip_insignificant_zeros] = true
number_to_human(number, **options)
end
def active_nav_class(*paths) def active_nav_class(*paths)
paths.any? { |path| current_page?(path) } ? 'active' : '' paths.any? { |path| current_page?(path) } ? 'active' : ''
end end

@ -7291,6 +7291,7 @@ noscript {
&__account { &__account {
display: flex; display: flex;
text-decoration: none; text-decoration: none;
overflow: hidden;
} }
.account__avatar { .account__avatar {

@ -15,6 +15,7 @@
class AccountStat < ApplicationRecord class AccountStat < ApplicationRecord
self.locking_column = nil self.locking_column = nil
self.ignored_columns = %w(lock_version)
belongs_to :account, inverse_of: :account_stat belongs_to :account, inverse_of: :account_stat

@ -42,6 +42,7 @@
# sign_in_token_sent_at :datetime # sign_in_token_sent_at :datetime
# webauthn_id :string # webauthn_id :string
# sign_up_ip :inet # sign_up_ip :inet
# skip_sign_in_token :boolean
# #
class User < ApplicationRecord class User < ApplicationRecord
@ -200,7 +201,7 @@ class User < ApplicationRecord
end end
def suspicious_sign_in?(ip) def suspicious_sign_in?(ip)
!otp_required_for_login? && current_sign_in_at.present? && current_sign_in_at < 2.weeks.ago && !recent_ip?(ip) !otp_required_for_login? && !skip_sign_in_token? && current_sign_in_at.present? && !recent_ip?(ip)
end end
def functional? def functional?
@ -329,12 +330,32 @@ class User < ApplicationRecord
super super
end end
def reset_password!(new_password, new_password_confirmation) def reset_password(new_password, new_password_confirmation)
return false if encrypted_password.blank? return false if encrypted_password.blank?
super super
end end
def reset_password!
# First, change password to something random, invalidate the remember-me token,
# and deactivate all sessions
transaction do
update(remember_token: nil, remember_created_at: nil, password: SecureRandom.hex)
session_activations.destroy_all
end
# Then, remove all authorized applications and connected push subscriptions
Doorkeeper::AccessGrant.by_resource_owner(self).in_batches.update_all(revoked_at: Time.now.utc)
Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch|
batch.update_all(revoked_at: Time.now.utc)
Web::PushSubscription.where(access_token_id: batch).delete_all
end
# Finally, send a reset password prompt to the user
send_reset_password_instructions
end
def show_all_media? def show_all_media?
setting_display_media == 'show_all' setting_display_media == 'show_all'
end end

@ -13,6 +13,14 @@ class UserPolicy < ApplicationPolicy
admin? && !record.staff? admin? && !record.staff?
end end
def disable_sign_in_token_auth?
staff?
end
def enable_sign_in_token_auth?
staff?
end
def confirm? def confirm?
staff? && !record.confirmed? staff? && !record.confirmed?
end end

@ -16,11 +16,11 @@
.row__information-board .row__information-board
.information-board__section .information-board__section
%span= t 'about.user_count_before' %span= t 'about.user_count_before'
%strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true %strong= friendly_number_to_human @instance_presenter.user_count
%span= t 'about.user_count_after', count: @instance_presenter.user_count %span= t 'about.user_count_after', count: @instance_presenter.user_count
.information-board__section .information-board__section
%span= t 'about.status_count_before' %span= t 'about.status_count_before'
%strong= number_to_human @instance_presenter.status_count, strip_insignificant_zeros: true %strong= friendly_number_to_human @instance_presenter.status_count
%span= t 'about.status_count_after', count: @instance_presenter.status_count %span= t 'about.status_count_after', count: @instance_presenter.status_count
.row__mascot .row__mascot
.landing-page__mascot .landing-page__mascot

@ -70,10 +70,10 @@
.hero-widget__counters__wrapper .hero-widget__counters__wrapper
.hero-widget__counter .hero-widget__counter
%strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true %strong= friendly_number_to_human @instance_presenter.user_count
%span= t 'about.user_count_after', count: @instance_presenter.user_count %span= t 'about.user_count_after', count: @instance_presenter.user_count
.hero-widget__counter .hero-widget__counter
%strong= number_to_human @instance_presenter.active_user_count, strip_insignificant_zeros: true %strong= friendly_number_to_human @instance_presenter.active_user_count
%span %span
= t 'about.active_count_after' = t 'about.active_count_after'
%abbr{ title: t('about.active_footnote') } * %abbr{ title: t('about.active_footnote') } *

@ -15,17 +15,17 @@
.details-counters .details-counters
.counter{ class: active_nav_class(short_account_url(account), short_account_with_replies_url(account), short_account_media_url(account)) } .counter{ class: active_nav_class(short_account_url(account), short_account_with_replies_url(account), short_account_media_url(account)) }
= link_to short_account_url(account), class: 'u-url u-uid', title: number_with_delimiter(account.statuses_count) do = link_to short_account_url(account), class: 'u-url u-uid', title: number_with_delimiter(account.statuses_count) do
%span.counter-number= number_to_human account.statuses_count, strip_insignificant_zeros: true %span.counter-number= friendly_number_to_human account.statuses_count
%span.counter-label= t('accounts.posts', count: account.statuses_count) %span.counter-label= t('accounts.posts', count: account.statuses_count)
.counter{ class: active_nav_class(account_following_index_url(account)) } .counter{ class: active_nav_class(account_following_index_url(account)) }
= link_to account_following_index_url(account), title: number_with_delimiter(account.following_count) do = link_to account_following_index_url(account), title: number_with_delimiter(account.following_count) do
%span.counter-number= number_to_human account.following_count, strip_insignificant_zeros: true %span.counter-number= friendly_number_to_human account.following_count
%span.counter-label= t('accounts.following', count: account.following_count) %span.counter-label= t('accounts.following', count: account.following_count)
.counter{ class: active_nav_class(account_followers_url(account)) } .counter{ class: active_nav_class(account_followers_url(account)) }
= link_to account_followers_url(account), title: hide_followers_count?(account) ? nil : number_with_delimiter(account.followers_count) do = link_to account_followers_url(account), title: hide_followers_count?(account) ? nil : number_with_delimiter(account.followers_count) do
%span.counter-number= hide_followers_count?(account) ? '-' : (number_to_human account.followers_count, strip_insignificant_zeros: true) %span.counter-number= hide_followers_count?(account) ? '-' : (friendly_number_to_human account.followers_count)
%span.counter-label= t('accounts.followers', count: account.followers_count) %span.counter-label= t('accounts.followers', count: account.followers_count)
.spacer .spacer
.public-account-header__tabs__tabs__buttons .public-account-header__tabs__tabs__buttons
@ -36,8 +36,8 @@
.public-account-header__extra__links .public-account-header__extra__links
= link_to account_following_index_url(account) do = link_to account_following_index_url(account) do
%strong= number_to_human account.following_count, strip_insignificant_zeros: true %strong= friendly_number_to_human account.following_count
= t('accounts.following', count: account.following_count) = t('accounts.following', count: account.following_count)
= link_to account_followers_url(account) do = link_to account_followers_url(account) do
%strong= hide_followers_count?(account) ? '-' : (number_to_human account.followers_count, strip_insignificant_zeros: true) %strong= hide_followers_count?(account) ? '-' : (friendly_number_to_human account.followers_count)
= t('accounts.followers', count: account.followers_count) = t('accounts.followers', count: account.followers_count)

@ -81,6 +81,6 @@
= t('accounts.nothing_here') = t('accounts.nothing_here')
- else - else
%time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at %time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
.trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true .trends__item__current= friendly_number_to_human featured_tag.statuses_count
= render 'application/sidebar' = render 'application/sidebar'

@ -129,6 +129,27 @@
- else - else
= t('admin.accounts.confirming') = t('admin.accounts.confirming')
%td= table_link_to 'refresh', t('admin.accounts.resend_confirmation.send'), resend_admin_account_confirmation_path(@account.id), method: :post if can?(:confirm, @account.user) %td= table_link_to 'refresh', t('admin.accounts.resend_confirmation.send'), resend_admin_account_confirmation_path(@account.id), method: :post if can?(:confirm, @account.user)
%tr
%th{ rowspan: can?(:reset_password, @account.user) ? 2 : 1 }= t('admin.accounts.security')
%td{ rowspan: can?(:reset_password, @account.user) ? 2 : 1 }
- if @account.user&.two_factor_enabled?
= t 'admin.accounts.security_measures.password_and_2fa'
- elsif @account.user&.skip_sign_in_token?
= t 'admin.accounts.security_measures.only_password'
- else
= t 'admin.accounts.security_measures.password_and_sign_in_token'
%td
- if @account.user&.two_factor_enabled?
= table_link_to 'unlock', t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete if can?(:disable_2fa, @account.user)
- elsif @account.user&.skip_sign_in_token?
= table_link_to 'lock', t('admin.accounts.enable_sign_in_token_auth'), admin_user_sign_in_token_authentication_path(@account.user.id), method: :post if can?(:enable_sign_in_token_auth, @account.user)
- else
= table_link_to 'unlock', t('admin.accounts.disable_sign_in_token_auth'), admin_user_sign_in_token_authentication_path(@account.user.id), method: :delete if can?(:disable_sign_in_token_auth, @account.user)
- if can?(:reset_password, @account.user)
%tr
%td
= table_link_to 'key', t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, data: { confirm: t('admin.accounts.are_you_sure') }
%tr %tr
%th= t('simple_form.labels.defaults.locale') %th= t('simple_form.labels.defaults.locale')
@ -221,9 +242,6 @@
%div %div
- if @account.local? - if @account.local?
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
- if @account.user&.otp_required_for_login?
= link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
- if !@account.memorial? && @account.user_approved? - if !@account.memorial? && @account.user_approved?
= link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account) = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
- else - else

@ -13,42 +13,42 @@
%div %div
= link_to admin_accounts_url(local: 1, recent: 1) do = link_to admin_accounts_url(local: 1, recent: 1) do
.dashboard__counters__num{ title: number_with_delimiter(@users_count, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@users_count, strip_insignificant_zeros: true) }
= number_to_human @users_count, strip_insignificant_zeros: true = friendly_number_to_human @users_count
.dashboard__counters__label= t 'admin.dashboard.total_users' .dashboard__counters__label= t 'admin.dashboard.total_users'
%div %div
%div %div
.dashboard__counters__num{ title: number_with_delimiter(@registrations_week, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@registrations_week, strip_insignificant_zeros: true) }
= number_to_human @registrations_week, strip_insignificant_zeros: true = friendly_number_to_human @registrations_week
.dashboard__counters__label= t 'admin.dashboard.week_users_new' .dashboard__counters__label= t 'admin.dashboard.week_users_new'
%div %div
%div %div
.dashboard__counters__num{ title: number_with_delimiter(@logins_week, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@logins_week, strip_insignificant_zeros: true) }
= number_to_human @logins_week, strip_insignificant_zeros: true = friendly_number_to_human @logins_week
.dashboard__counters__label= t 'admin.dashboard.week_users_active' .dashboard__counters__label= t 'admin.dashboard.week_users_active'
%div %div
= link_to admin_pending_accounts_path do = link_to admin_pending_accounts_path do
.dashboard__counters__num{ title: number_with_delimiter(@pending_users_count, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@pending_users_count, strip_insignificant_zeros: true) }
= number_to_human @pending_users_count, strip_insignificant_zeros: true = friendly_number_to_human @pending_users_count
.dashboard__counters__label= t 'admin.dashboard.pending_users' .dashboard__counters__label= t 'admin.dashboard.pending_users'
%div %div
= link_to admin_reports_url do = link_to admin_reports_url do
.dashboard__counters__num{ title: number_with_delimiter(@reports_count, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@reports_count, strip_insignificant_zeros: true) }
= number_to_human @reports_count, strip_insignificant_zeros: true = friendly_number_to_human @reports_count
.dashboard__counters__label= t 'admin.dashboard.open_reports' .dashboard__counters__label= t 'admin.dashboard.open_reports'
%div %div
= link_to admin_tags_path(pending_review: '1') do = link_to admin_tags_path(pending_review: '1') do
.dashboard__counters__num{ title: number_with_delimiter(@pending_tags_count, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@pending_tags_count, strip_insignificant_zeros: true) }
= number_to_human @pending_tags_count, strip_insignificant_zeros: true = friendly_number_to_human @pending_tags_count
.dashboard__counters__label= t 'admin.dashboard.pending_tags' .dashboard__counters__label= t 'admin.dashboard.pending_tags'
%div %div
%div %div
.dashboard__counters__num{ title: number_with_delimiter(@interactions_week, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@interactions_week, strip_insignificant_zeros: true) }
= number_to_human @interactions_week, strip_insignificant_zeros: true = friendly_number_to_human @interactions_week
.dashboard__counters__label= t 'admin.dashboard.week_interactions' .dashboard__counters__label= t 'admin.dashboard.week_interactions'
%div %div
= link_to sidekiq_url do = link_to sidekiq_url do
.dashboard__counters__num{ title: number_with_delimiter(@queue_backlog, strip_insignificant_zeros: true) } .dashboard__counters__num{ title: number_with_delimiter(@queue_backlog, strip_insignificant_zeros: true) }
= number_to_human @queue_backlog, strip_insignificant_zeros: true = friendly_number_to_human @queue_backlog
.dashboard__counters__label= t 'admin.dashboard.backlog' .dashboard__counters__label= t 'admin.dashboard.backlog'
.dashboard__widgets .dashboard__widgets

@ -7,10 +7,10 @@
%tr %tr
%td= account_link_to account %td= account_link_to account
%td.accounts-table__count.optional %td.accounts-table__count.optional
= number_to_human account.statuses_count, strip_insignificant_zeros: true = friendly_number_to_human account.statuses_count
%small= t('accounts.posts', count: account.statuses_count).downcase %small= t('accounts.posts', count: account.statuses_count).downcase
%td.accounts-table__count.optional %td.accounts-table__count.optional
= number_to_human account.followers_count, strip_insignificant_zeros: true = friendly_number_to_human account.followers_count
%small= t('accounts.followers', count: account.followers_count).downcase %small= t('accounts.followers', count: account.followers_count).downcase
%td.accounts-table__count %td.accounts-table__count
- if account.last_status_at.present? - if account.last_status_at.present?

@ -30,4 +30,4 @@
= ' / ' = ' / '
%span.negative-hint %span.negative-hint
= t('admin.instances.delivery.unavailable_message') = t('admin.instances.delivery.unavailable_message')
.trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true .trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= friendly_number_to_human instance.accounts_count

@ -16,4 +16,4 @@
= fa_icon 'fire fw' = fa_icon 'fire fw'
= t('admin.tags.trending_right_now') = t('admin.tags.trending_right_now')
.trends__item__current= number_to_human tag.history.first[:uses], strip_insignificant_zeros: true .trends__item__current= friendly_number_to_human tag.history.first[:uses]

@ -39,10 +39,10 @@
.directory__card__extra .directory__card__extra
.accounts-table__count .accounts-table__count
= number_to_human account.statuses_count, strip_insignificant_zeros: true = friendly_number_to_human account.statuses_count
%small= t('accounts.posts', count: account.statuses_count).downcase %small= t('accounts.posts', count: account.statuses_count).downcase
.accounts-table__count .accounts-table__count
= hide_followers_count?(account) ? '-' : (number_to_human account.followers_count, strip_insignificant_zeros: true) = hide_followers_count?(account) ? '-' : (friendly_number_to_human account.followers_count)
%small= t('accounts.followers', count: account.followers_count).downcase %small= t('accounts.followers', count: account.followers_count).downcase
.accounts-table__count .accounts-table__count
- if account.last_status_at.present? - if account.last_status_at.present?

@ -9,10 +9,10 @@
= interrelationships_icon(@relationships, account.id) = interrelationships_icon(@relationships, account.id)
%td= account_link_to account %td= account_link_to account
%td.accounts-table__count.optional %td.accounts-table__count.optional
= number_to_human account.statuses_count, strip_insignificant_zeros: true = friendly_number_to_human account.statuses_count
%small= t('accounts.posts', count: account.statuses_count).downcase %small= t('accounts.posts', count: account.statuses_count).downcase
%td.accounts-table__count.optional %td.accounts-table__count.optional
= number_to_human account.followers_count, strip_insignificant_zeros: true = friendly_number_to_human account.followers_count
%small= t('accounts.followers', count: account.followers_count).downcase %small= t('accounts.followers', count: account.followers_count).downcase
%td.accounts-table__count %td.accounts-table__count
- if account.last_status_at.present? - if account.last_status_at.present?

@ -28,4 +28,4 @@
- else - else
%time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
= table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } = table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
.trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true .trends__item__current= friendly_number_to_human featured_tag.statuses_count

@ -55,18 +55,18 @@
= fa_icon('reply') = fa_icon('reply')
- else - else
= fa_icon('reply-all') = fa_icon('reply-all')
%span.detailed-status__reblogs>= number_to_human status.replies_count, strip_insignificant_zeros: true %span.detailed-status__reblogs>= friendly_number_to_human status.replies_count
= " " = " "
· ·
- if status.public_visibility? || status.unlisted_visibility? - if status.public_visibility? || status.unlisted_visibility?
= link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do = link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do
= fa_icon('retweet') = fa_icon('retweet')
%span.detailed-status__reblogs>= number_to_human status.reblogs_count, strip_insignificant_zeros: true %span.detailed-status__reblogs>= friendly_number_to_human status.reblogs_count
= " " = " "
· ·
= link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do = link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do
= fa_icon('star') = fa_icon('star')
%span.detailed-status__favorites>= number_to_human status.favourites_count, strip_insignificant_zeros: true %span.detailed-status__favorites>= friendly_number_to_human status.favourites_count
= " " = " "
- if user_signed_in? - if user_signed_in?

@ -44,7 +44,7 @@ en:
rejecting_media: 'Media files from these servers will not be processed or stored, and no thumbnails will be displayed, requiring manual click-through to the original file:' rejecting_media: 'Media files from these servers will not be processed or stored, and no thumbnails will be displayed, requiring manual click-through to the original file:'
rejecting_media_title: Filtered media rejecting_media_title: Filtered media
silenced: 'Posts from these servers will be hidden in public timelines and conversations, and no notifications will be generated from their users interactions, unless you are following them:' silenced: 'Posts from these servers will be hidden in public timelines and conversations, and no notifications will be generated from their users interactions, unless you are following them:'
silenced_title: Silenced servers silenced_title: Limited servers
suspended: 'No data from these servers will be processed, stored or exchanged, making any interaction or communication with users from these servers impossible:' suspended: 'No data from these servers will be processed, stored or exchanged, making any interaction or communication with users from these servers impossible:'
suspended_title: Suspended servers suspended_title: Suspended servers
unavailable_content_html: Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server. unavailable_content_html: Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.
@ -119,6 +119,7 @@ en:
demote: Demote demote: Demote
destroyed_msg: "%{username}'s data is now queued to be deleted imminently" destroyed_msg: "%{username}'s data is now queued to be deleted imminently"
disable: Freeze disable: Freeze
disable_sign_in_token_auth: Disable e-mail token authentication
disable_two_factor_authentication: Disable 2FA disable_two_factor_authentication: Disable 2FA
disabled: Frozen disabled: Frozen
display_name: Display name display_name: Display name
@ -127,6 +128,7 @@ en:
email: Email email: Email
email_status: Email status email_status: Email status
enable: Unfreeze enable: Unfreeze
enable_sign_in_token_auth: Enable e-mail token authentication
enabled: Enabled enabled: Enabled
enabled_msg: Successfully unfroze %{username}'s account enabled_msg: Successfully unfroze %{username}'s account
followers: Followers followers: Followers
@ -151,7 +153,7 @@ en:
active: Active active: Active
all: All all: All
pending: Pending pending: Pending
silenced: Silenced silenced: Limited
suspended: Suspended suspended: Suspended
title: Moderation title: Moderation
moderation_notes: Moderation notes moderation_notes: Moderation notes
@ -191,8 +193,12 @@ en:
search: Search search: Search
search_same_email_domain: Other users with the same e-mail domain search_same_email_domain: Other users with the same e-mail domain
search_same_ip: Other users with the same IP search_same_ip: Other users with the same IP
sensitive: Sensitive security_measures:
sensitized: marked as sensitive only_password: Only password
password_and_2fa: Password and 2FA
password_and_sign_in_token: Password and e-mail token
sensitive: Force-sensitive
sensitized: Marked as sensitive
shared_inbox_url: Shared inbox URL shared_inbox_url: Shared inbox URL
show: show:
created_reports: Made reports created_reports: Made reports
@ -207,10 +213,10 @@ en:
time_in_queue: Waiting in queue %{time} time_in_queue: Waiting in queue %{time}
title: Accounts title: Accounts
unconfirmed_email: Unconfirmed email unconfirmed_email: Unconfirmed email
undo_sensitized: Undo sensitive undo_sensitized: Undo force-sensitive
undo_silenced: Undo silence undo_silenced: Undo limit
undo_suspension: Undo suspension undo_suspension: Undo suspension
unsilenced_msg: Successfully unlimited %{username}'s account unsilenced_msg: Successfully undid limit of %{username}'s account
unsubscribe: Unsubscribe unsubscribe: Unsubscribe
unsuspended_msg: Successfully unsuspended %{username}'s account unsuspended_msg: Successfully unsuspended %{username}'s account
username: Username username: Username
@ -236,14 +242,16 @@ en:
destroy_custom_emoji: Delete Custom Emoji destroy_custom_emoji: Delete Custom Emoji
destroy_domain_allow: Delete Domain Allow destroy_domain_allow: Delete Domain Allow
destroy_domain_block: Delete Domain Block destroy_domain_block: Delete Domain Block
destroy_email_domain_block: Delete e-mail domain block destroy_email_domain_block: Delete E-mail Domain Block
destroy_ip_block: Delete IP rule destroy_ip_block: Delete IP rule
destroy_status: Delete Post destroy_status: Delete Post
destroy_unavailable_domain: Delete Unavailable Domain destroy_unavailable_domain: Delete Unavailable Domain
disable_2fa_user: Disable 2FA disable_2fa_user: Disable 2FA
disable_custom_emoji: Disable Custom Emoji disable_custom_emoji: Disable Custom Emoji
disable_sign_in_token_auth_user: Disable E-mail Token Authentication for User
disable_user: Disable User disable_user: Disable User
enable_custom_emoji: Enable Custom Emoji enable_custom_emoji: Enable Custom Emoji
enable_sign_in_token_auth_user: Enable E-mail Token Authentication for User
enable_user: Enable User enable_user: Enable User
memorialize_account: Memorialize Account memorialize_account: Memorialize Account
promote_user: Promote User promote_user: Promote User
@ -251,12 +259,12 @@ en:
reopen_report: Reopen Report reopen_report: Reopen Report
reset_password_user: Reset Password reset_password_user: Reset Password
resolve_report: Resolve Report resolve_report: Resolve Report
sensitive_account: Mark the media in your account as sensitive sensitive_account: Force-Sensitive Account
silence_account: Silence Account silence_account: Limit Account
suspend_account: Suspend Account suspend_account: Suspend Account
unassigned_report: Unassign Report unassigned_report: Unassign Report
unsensitive_account: Unmark the media in your account as sensitive unsensitive_account: Undo Force-Sensitive Account
unsilence_account: Unsilence Account unsilence_account: Undo Limit Account
unsuspend_account: Unsuspend Account unsuspend_account: Unsuspend Account
update_announcement: Update Announcement update_announcement: Update Announcement
update_custom_emoji: Update Custom Emoji update_custom_emoji: Update Custom Emoji
@ -285,8 +293,10 @@ en:
destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}" destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}"
disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}" disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}"
disable_custom_emoji_html: "%{name} disabled emoji %{target}" disable_custom_emoji_html: "%{name} disabled emoji %{target}"
disable_sign_in_token_auth_user_html: "%{name} disabled e-mail token authentication for %{target}"
disable_user_html: "%{name} disabled login for user %{target}" disable_user_html: "%{name} disabled login for user %{target}"
enable_custom_emoji_html: "%{name} enabled emoji %{target}" enable_custom_emoji_html: "%{name} enabled emoji %{target}"
enable_sign_in_token_auth_user_html: "%{name} enabled e-mail token authentication for %{target}"
enable_user_html: "%{name} enabled login for user %{target}" enable_user_html: "%{name} enabled login for user %{target}"
memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page" memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page"
promote_user_html: "%{name} promoted user %{target}" promote_user_html: "%{name} promoted user %{target}"
@ -295,11 +305,11 @@ en:
reset_password_user_html: "%{name} reset password of user %{target}" reset_password_user_html: "%{name} reset password of user %{target}"
resolve_report_html: "%{name} resolved report %{target}" resolve_report_html: "%{name} resolved report %{target}"
sensitive_account_html: "%{name} marked %{target}'s media as sensitive" sensitive_account_html: "%{name} marked %{target}'s media as sensitive"
silence_account_html: "%{name} silenced %{target}'s account" silence_account_html: "%{name} limited %{target}'s account"
suspend_account_html: "%{name} suspended %{target}'s account" suspend_account_html: "%{name} suspended %{target}'s account"
unassigned_report_html: "%{name} unassigned report %{target}" unassigned_report_html: "%{name} unassigned report %{target}"
unsensitive_account_html: "%{name} unmarked %{target}'s media as sensitive" unsensitive_account_html: "%{name} unmarked %{target}'s media as sensitive"
unsilence_account_html: "%{name} unsilenced %{target}'s account" unsilence_account_html: "%{name} undid limit of %{target}'s account"
unsuspend_account_html: "%{name} unsuspended %{target}'s account" unsuspend_account_html: "%{name} unsuspended %{target}'s account"
update_announcement_html: "%{name} updated announcement %{target}" update_announcement_html: "%{name} updated announcement %{target}"
update_custom_emoji_html: "%{name} updated emoji %{target}" update_custom_emoji_html: "%{name} updated emoji %{target}"
@ -421,14 +431,14 @@ en:
rejecting_media: rejecting media files rejecting_media: rejecting media files
rejecting_reports: rejecting reports rejecting_reports: rejecting reports
severity: severity:
silence: silenced silence: limited
suspend: suspended suspend: suspended
show: show:
affected_accounts: affected_accounts:
one: One account in the database affected one: One account in the database affected
other: "%{count} accounts in the database affected" other: "%{count} accounts in the database affected"
retroactive: retroactive:
silence: Unsilence existing affected accounts from this domain silence: Undo limit of existing affected accounts from this domain
suspend: Unsuspend existing affected accounts from this domain suspend: Unsuspend existing affected accounts from this domain
title: Undo domain block for %{domain} title: Undo domain block for %{domain}
undo: Undo undo: Undo

@ -285,6 +285,7 @@ Rails.application.routes.draw do
resources :users, only: [] do resources :users, only: [] do
resource :two_factor_authentication, only: [:destroy] resource :two_factor_authentication, only: [:destroy]
resource :sign_in_token_authentication, only: [:create, :destroy]
end end
resources :custom_emojis, only: [:index, :new, :create] do resources :custom_emojis, only: [:index, :new, :create] do

@ -0,0 +1,5 @@
class AddSkipSignInTokenToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :skip_sign_in_token, :boolean
end
end

@ -929,6 +929,7 @@ ActiveRecord::Schema.define(version: 2021_06_30_000137) do
t.datetime "sign_in_token_sent_at" t.datetime "sign_in_token_sent_at"
t.string "webauthn_id" t.string "webauthn_id"
t.inet "sign_up_ip" t.inet "sign_up_ip"
t.boolean "skip_sign_in_token"
t.index ["account_id"], name: "index_users_on_account_id" t.index ["account_id"], name: "index_users_on_account_id"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id" t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id"

@ -9,6 +9,7 @@ WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production" Environment="RAILS_ENV=production"
Environment="DB_POOL=25" Environment="DB_POOL=25"
Environment="MALLOC_ARENA_MAX=2" Environment="MALLOC_ARENA_MAX=2"
Environment="LD_PRELOAD=libjemalloc.so"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 25 ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 25
TimeoutSec=15 TimeoutSec=15
Restart=always Restart=always

@ -8,6 +8,7 @@ User=mastodon
WorkingDirectory=/home/mastodon/live WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production" Environment="RAILS_ENV=production"
Environment="PORT=3000" Environment="PORT=3000"
Environment="LD_PRELOAD=libjemalloc.so"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec puma -C config/puma.rb ExecStart=/home/mastodon/.rbenv/shims/bundle exec puma -C config/puma.rb
ExecReload=/bin/kill -SIGUSR1 $MAINPID ExecReload=/bin/kill -SIGUSR1 $MAINPID
TimeoutSec=15 TimeoutSec=15

@ -54,7 +54,8 @@ module Mastodon
option :email, required: true option :email, required: true
option :confirmed, type: :boolean option :confirmed, type: :boolean
option :role, default: 'user' option :role, default: 'user', enum: %w(user moderator admin)
option :skip_sign_in_token, type: :boolean
option :reattach, type: :boolean option :reattach, type: :boolean
option :force, type: :boolean option :force, type: :boolean
desc 'create USERNAME', 'Create a new user' desc 'create USERNAME', 'Create a new user'
@ -68,6 +69,9 @@ module Mastodon
With the --role option one of "user", "admin" or "moderator" With the --role option one of "user", "admin" or "moderator"
can be supplied. Defaults to "user" can be supplied. Defaults to "user"
With the --skip-sign-in-token option, you can ensure that
the user is never asked for an e-mailed security code.
With the --reattach option, the new user will be reattached With the --reattach option, the new user will be reattached
to a given existing username of an old account. If the old to a given existing username of an old account. If the old
account is still in use by someone else, you can supply account is still in use by someone else, you can supply
@ -77,7 +81,7 @@ module Mastodon
def create(username) def create(username)
account = Account.new(username: username) account = Account.new(username: username)
password = SecureRandom.hex password = SecureRandom.hex
user = User.new(email: options[:email], password: password, agreement: true, approved: true, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: options[:confirmed] ? Time.now.utc : nil, bypass_invite_request_check: true) user = User.new(email: options[:email], password: password, agreement: true, approved: true, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: options[:confirmed] ? Time.now.utc : nil, bypass_invite_request_check: true, skip_sign_in_token: options[:skip_sign_in_token])
if options[:reattach] if options[:reattach]
account = Account.find_local(username) || Account.new(username: username) account = Account.find_local(username) || Account.new(username: username)
@ -113,7 +117,7 @@ module Mastodon
end end
end end
option :role option :role, enum: %w(user moderator admin)
option :email option :email
option :confirm, type: :boolean option :confirm, type: :boolean
option :enable, type: :boolean option :enable, type: :boolean
@ -121,6 +125,7 @@ module Mastodon
option :disable_2fa, type: :boolean option :disable_2fa, type: :boolean
option :approve, type: :boolean option :approve, type: :boolean
option :reset_password, type: :boolean option :reset_password, type: :boolean
option :skip_sign_in_token, type: :boolean
desc 'modify USERNAME', 'Modify a user' desc 'modify USERNAME', 'Modify a user'
long_desc <<-LONG_DESC long_desc <<-LONG_DESC
Modify a user account. Modify a user account.
@ -142,6 +147,9 @@ module Mastodon
With the --reset-password option, the user's password is replaced by With the --reset-password option, the user's password is replaced by
a randomly-generated one, printed in the output. a randomly-generated one, printed in the output.
With the --skip-sign-in-token option, you can ensure that
the user is never asked for an e-mailed security code.
LONG_DESC LONG_DESC
def modify(username) def modify(username)
user = Account.find_local(username)&.user user = Account.find_local(username)&.user
@ -163,6 +171,7 @@ module Mastodon
user.disabled = true if options[:disable] user.disabled = true if options[:disable]
user.approved = true if options[:approve] user.approved = true if options[:approve]
user.otp_required_for_login = false if options[:disable_2fa] user.otp_required_for_login = false if options[:disable_2fa]
user.skip_sign_in_token = options[:skip_sign_in_token] unless options[:skip_sign_in_token].nil?
user.confirm if options[:confirm] user.confirm if options[:confirm]
if user.save if user.save

@ -17,6 +17,7 @@ module Mastodon
option :verbose, type: :boolean, aliases: [:v] option :verbose, type: :boolean, aliases: [:v]
option :dry_run, type: :boolean option :dry_run, type: :boolean
option :limited_federation_mode, type: :boolean option :limited_federation_mode, type: :boolean
option :by_uri, type: :boolean
desc 'purge [DOMAIN...]', 'Remove accounts from a DOMAIN without a trace' desc 'purge [DOMAIN...]', 'Remove accounts from a DOMAIN without a trace'
long_desc <<-LONG_DESC long_desc <<-LONG_DESC
Remove all accounts from a given DOMAIN without leaving behind any Remove all accounts from a given DOMAIN without leaving behind any
@ -26,6 +27,12 @@ module Mastodon
When the --limited-federation-mode option is given, instead of purging accounts When the --limited-federation-mode option is given, instead of purging accounts
from a single domain, all accounts from domains that have not been explicitly allowed from a single domain, all accounts from domains that have not been explicitly allowed
are removed from the database. are removed from the database.
When the --by-uri option is given, DOMAIN is used to match the domain part of actor
URIs rather than the domain part of the webfinger handle. For instance, an account
that has the handle `foo@bar.com` but whose profile is at the URL
`https://mastodon-bar.com/users/foo`, would be purged by either
`tootctl domains purge bar.com` or `tootctl domains purge --by-uri mastodon-bar.com`.
LONG_DESC LONG_DESC
def purge(*domains) def purge(*domains)
dry_run = options[:dry_run] ? ' (DRY RUN)' : '' dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
@ -34,7 +41,11 @@ module Mastodon
if options[:limited_federation_mode] if options[:limited_federation_mode]
Account.remote.where.not(domain: DomainAllow.pluck(:domain)) Account.remote.where.not(domain: DomainAllow.pluck(:domain))
elsif !domains.empty? elsif !domains.empty?
Account.remote.where(domain: domains) if options[:by_uri]
domains.map { |domain| Account.remote.where(Account.arel_table[:uri].matches("https://#{domain}/%", false, true)) }.reduce(:or)
else
Account.remote.where(domain: domains)
end
else else
say('No domain(s) given', :red) say('No domain(s) given', :red)
exit(1) exit(1)

@ -69,7 +69,7 @@
"@babel/runtime": "^7.14.6", "@babel/runtime": "^7.14.6",
"@gamestdio/websocket": "^0.3.2", "@gamestdio/websocket": "^0.3.2",
"@github/webauthn-json": "^0.5.7", "@github/webauthn-json": "^0.5.7",
"@rails/ujs": "^6.1.3", "@rails/ujs": "^6.1.4",
"array-includes": "^3.1.3", "array-includes": "^3.1.3",
"atrament": "0.2.4", "atrament": "0.2.4",
"arrow-key-navigation": "^1.2.0", "arrow-key-navigation": "^1.2.0",
@ -171,14 +171,14 @@
"webpack-cli": "^3.3.12", "webpack-cli": "^3.3.12",
"webpack-merge": "^5.8.0", "webpack-merge": "^5.8.0",
"wicg-inert": "^3.1.1", "wicg-inert": "^3.1.1",
"ws": "^7.5.1" "ws": "^7.5.2"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7", "@testing-library/react": "^11.2.7",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-jest": "^27.0.6", "babel-jest": "^27.0.6",
"eslint": "^7.29.0", "eslint": "^7.30.0",
"eslint-plugin-import": "~2.23.4", "eslint-plugin-import": "~2.23.4",
"eslint-plugin-jsx-a11y": "~6.4.1", "eslint-plugin-jsx-a11y": "~6.4.1",
"eslint-plugin-promise": "~5.1.0", "eslint-plugin-promise": "~5.1.0",

@ -55,6 +55,10 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
it_behaves_like 'cachable response' it_behaves_like 'cachable response'
it 'does not have a Vary header' do
expect(response.headers['Vary']).to be_nil
end
context 'when account is permanently suspended' do context 'when account is permanently suspended' do
before do before do
account.suspend! account.suspend!
@ -96,6 +100,10 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
it_behaves_like 'cachable response' it_behaves_like 'cachable response'
it 'returns Vary header with Signature' do
expect(response.headers['Vary']).to include 'Signature'
end
context 'when account is permanently suspended' do context 'when account is permanently suspended' do
before do before do
account.suspend! account.suspend!
@ -144,7 +152,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end end
it 'returns private Cache-Control header' do it 'returns private Cache-Control header' do
expect(response.headers['Cache-Control']).to eq 'max-age=0, private' expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
end end
end end
@ -170,7 +178,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end end
it 'returns private Cache-Control header' do it 'returns private Cache-Control header' do
expect(response.headers['Cache-Control']).to eq 'max-age=0, private' expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
end end
end end
@ -195,7 +203,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end end
it 'returns private Cache-Control header' do it 'returns private Cache-Control header' do
expect(response.headers['Cache-Control']).to eq 'max-age=0, private' expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
end end
end end
@ -220,7 +228,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end end
it 'returns private Cache-Control header' do it 'returns private Cache-Control header' do
expect(response.headers['Cache-Control']).to eq 'max-age=0, private' expect(response.headers['Cache-Control']).to eq 'max-age=60, private'
end end
end end
end end

@ -16,7 +16,7 @@ describe Admin::ResetsController do
post :create, params: { account_id: account.id } post :create, params: { account_id: account.id }
expect(response).to redirect_to(admin_accounts_path) expect(response).to redirect_to(admin_account_path(account.id))
end end
end end
end end

@ -15,12 +15,12 @@ describe Admin::TwoFactorAuthenticationsController do
user.update(otp_required_for_login: true) user.update(otp_required_for_login: true)
end end
it 'redirects to admin accounts page' do it 'redirects to admin account page' do
delete :destroy, params: { user_id: user.id } delete :destroy, params: { user_id: user.id }
user.reload user.reload
expect(user.otp_enabled?).to eq false expect(user.otp_enabled?).to eq false
expect(response).to redirect_to(admin_accounts_path) expect(response).to redirect_to(admin_account_path(user.account_id))
end end
end end
@ -38,13 +38,13 @@ describe Admin::TwoFactorAuthenticationsController do
nickname: 'Security Key') nickname: 'Security Key')
end end
it 'redirects to admin accounts page' do it 'redirects to admin account page' do
delete :destroy, params: { user_id: user.id } delete :destroy, params: { user_id: user.id }
user.reload user.reload
expect(user.otp_enabled?).to eq false expect(user.otp_enabled?).to eq false
expect(user.webauthn_enabled?).to eq false expect(user.webauthn_enabled?).to eq false
expect(response).to redirect_to(admin_accounts_path) expect(response).to redirect_to(admin_account_path(user.account_id))
end end
end end
end end

@ -24,6 +24,10 @@ describe WellKnown::WebfingerController, type: :controller do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it 'does not set a Vary header' do
expect(response.headers['Vary']).to be_nil
end
it 'returns application/jrd+json' do it 'returns application/jrd+json' do
expect(response.media_type).to eq 'application/jrd+json' expect(response.media_type).to eq 'application/jrd+json'
end end

@ -37,7 +37,7 @@ describe TagFeed, type: :service do
expect(results).to include both expect(results).to include both
end end
it 'handles being passed non existant tag names' do it 'handles being passed non existent tag names' do
results = described_class.new(tag1, nil, any: ['wark']).get(20) results = described_class.new(tag1, nil, any: ['wark']).get(20)
expect(results).to include status1 expect(results).to include status1
expect(results).to_not include status2 expect(results).to_not include status2

@ -344,6 +344,34 @@ RSpec.describe User, type: :model do
end end
end end
describe '#reset_password!' do
subject(:user) { Fabricate(:user, password: 'foobar12345') }
let!(:session_activation) { Fabricate(:session_activation, user: user) }
let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) }
let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) }
before do
user.reset_password!
end
it 'changes the password immediately' do
expect(user.external_or_valid_password?('foobar12345')).to be false
end
it 'deactivates all sessions' do
expect(user.session_activations.count).to eq 0
end
it 'revokes all access tokens' do
expect(Doorkeeper::AccessToken.active_for(user).count).to eq 0
end
it 'removes push subscriptions' do
expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0
end
end
describe '#confirm!' do describe '#confirm!' do
subject(:user) { Fabricate(:user, confirmed_at: confirmed_at) } subject(:user) { Fabricate(:user, confirmed_at: confirmed_at) }

@ -1,4 +1,37 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe BootstrapTimelineService, type: :service do RSpec.describe BootstrapTimelineService, type: :service do
subject { BootstrapTimelineService.new }
context 'when the new user has registered from an invite' do
let(:service) { double }
let(:autofollow) { false }
let(:inviter) { Fabricate(:user, confirmed_at: 2.days.ago) }
let(:invite) { Fabricate(:invite, user: inviter, max_uses: nil, expires_at: 1.hour.from_now, autofollow: autofollow) }
let(:new_user) { Fabricate(:user, invite_code: invite.code) }
before do
allow(FollowService).to receive(:new).and_return(service)
allow(service).to receive(:call)
end
context 'when the invite has auto-follow enabled' do
let(:autofollow) { true }
it 'calls FollowService to follow the inviter' do
subject.call(new_user.account)
expect(service).to have_received(:call).with(new_user.account, inviter.account)
end
end
context 'when the invite does not have auto-follow enable' do
let(:autofollow) { false }
it 'calls FollowService to follow the inviter' do
subject.call(new_user.account)
expect(service).to_not have_received(:call)
end
end
end
end end

@ -1129,6 +1129,20 @@
resolved "https://registry.yarnpkg.com/@github/webauthn-json/-/webauthn-json-0.5.7.tgz#143bc67f6e0f75f8d188e565741507bb08c31214" resolved "https://registry.yarnpkg.com/@github/webauthn-json/-/webauthn-json-0.5.7.tgz#143bc67f6e0f75f8d188e565741507bb08c31214"
integrity sha512-SUYsttDxFSvWvvJssJpwzjmRCqYfdfqC9VCmAHQYfdKCVelyJteCHo9/lK1CB72mx/jrl6cFNY08aua4J2jIyg== integrity sha512-SUYsttDxFSvWvvJssJpwzjmRCqYfdfqC9VCmAHQYfdKCVelyJteCHo9/lK1CB72mx/jrl6cFNY08aua4J2jIyg==
"@humanwhocodes/config-array@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==
dependencies:
"@humanwhocodes/object-schema" "^1.2.0"
debug "^4.1.1"
minimatch "^3.0.4"
"@humanwhocodes/object-schema@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
"@istanbuljs/load-nyc-config@^1.0.0": "@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@ -1370,10 +1384,10 @@
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71"
integrity sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA== integrity sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA==
"@rails/ujs@^6.1.3": "@rails/ujs@^6.1.4":
version "6.1.3" version "6.1.4"
resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-6.1.3.tgz#90ef26caa0925492b1a3b1495db09cfbe49e745e" resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-6.1.4.tgz#093d5341595a02089ed309dec40f3c37da7b1b10"
integrity sha512-9mip5o+LVouWAqLMNJWhxda+D5uP+4RziNECgOGJlL6k3rc5SC/ljCHpV9Cym4i3oeGZkpZJ2tu4frCwt84kzQ== integrity sha512-O3lEzL5DYbxppMdsFSw36e4BHIlfz/xusynwXGv3l2lhSlvah41qviRpsoAlKXxl37nZAqK+UUF5cnGGK45Mfw==
"@sinonjs/commons@^1.7.0": "@sinonjs/commons@^1.7.0":
version "1.8.1" version "1.8.1"
@ -4441,13 +4455,14 @@ eslint@^2.7.0:
text-table "~0.2.0" text-table "~0.2.0"
user-home "^2.0.0" user-home "^2.0.0"
eslint@^7.29.0: eslint@^7.30.0:
version "7.29.0" version "7.30.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.29.0.tgz#ee2a7648f2e729485e4d0bd6383ec1deabc8b3c0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.30.0.tgz#6d34ab51aaa56112fd97166226c9a97f505474f8"
integrity sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA== integrity sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==
dependencies: dependencies:
"@babel/code-frame" "7.12.11" "@babel/code-frame" "7.12.11"
"@eslint/eslintrc" "^0.4.2" "@eslint/eslintrc" "^0.4.2"
"@humanwhocodes/config-array" "^0.5.0"
ajv "^6.10.0" ajv "^6.10.0"
chalk "^4.0.0" chalk "^4.0.0"
cross-spawn "^7.0.2" cross-spawn "^7.0.2"
@ -11715,10 +11730,10 @@ ws@^6.2.1:
dependencies: dependencies:
async-limiter "~1.0.0" async-limiter "~1.0.0"
ws@^7.2.3, ws@^7.3.1, ws@^7.5.1: ws@^7.2.3, ws@^7.3.1, ws@^7.5.2:
version "7.5.1" version "7.5.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.1.tgz#44fc000d87edb1d9c53e51fbc69a0ac1f6871d66" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.2.tgz#09cc8fea3bec1bc5ed44ef51b42f945be36900f6"
integrity sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow== integrity sha512-lkF7AWRicoB9mAgjeKbGqVUekLnSNO4VjKVnuPHpQeOxZOErX6BPXwJk70nFslRCEEA8EVW7ZjKwXaP9N+1sKQ==
xml-name-validator@^3.0.0: xml-name-validator@^3.0.0:
version "3.0.0" version "3.0.0"

Loading…
Cancel
Save