Browse Source

Merge remote-tracking branch 'origin/master' into closed-social-v3

pull/4/head
欧醚 4 years ago
parent
commit
04bfe40736
94 changed files with 1097 additions and 626 deletions
  1. +7
    -7
      Gemfile
  2. +66
    -68
      Gemfile.lock
  3. +2
    -9
      app/controllers/accounts_controller.rb
  4. +20
    -11
      app/controllers/admin/accounts_controller.rb
  5. +2
    -2
      app/controllers/api/base_controller.rb
  6. +2
    -3
      app/controllers/api/v1/accounts_controller.rb
  7. +8
    -1
      app/controllers/api/v1/admin/accounts_controller.rb
  8. +1
    -1
      app/controllers/api/v1/follow_requests_controller.rb
  9. +1
    -1
      app/controllers/api/v1/push/subscriptions_controller.rb
  10. +2
    -1
      app/controllers/api/web/push_subscriptions_controller.rb
  11. +1
    -1
      app/controllers/settings/deletes_controller.rb
  12. +2
    -2
      app/javascript/mastodon/actions/accounts.js
  13. +1
    -1
      app/javascript/mastodon/actions/markers.js
  14. +1
    -1
      app/javascript/mastodon/actions/notifications.js
  15. +17
    -3
      app/javascript/mastodon/components/error_boundary.js
  16. +11
    -1
      app/javascript/mastodon/features/account/components/header.js
  17. +5
    -0
      app/javascript/mastodon/features/account_timeline/components/header.js
  18. +10
    -2
      app/javascript/mastodon/features/account_timeline/containers/header_container.js
  19. +8
    -0
      app/javascript/mastodon/features/notifications/components/filter_bar.js
  20. +35
    -0
      app/javascript/mastodon/features/notifications/components/notification.js
  21. +1
    -1
      app/javascript/mastodon/features/notifications/index.js
  22. +5
    -0
      app/javascript/styles/contrast/diff.scss
  23. +4
    -0
      app/javascript/styles/mastodon/components.scss
  24. +2
    -2
      app/lib/activitypub/activity.rb
  25. +1
    -1
      app/lib/activitypub/activity/delete.rb
  26. +2
    -2
      app/lib/activitypub/activity/follow.rb
  27. +1
    -1
      app/lib/activitypub/activity/like.rb
  28. +9
    -7
      app/mailers/notification_mailer.rb
  29. +14
    -14
      app/mailers/user_mailer.rb
  30. +3
    -6
      app/models/account.rb
  31. +20
    -0
      app/models/account_deletion_request.rb
  32. +1
    -1
      app/models/admin/account_action.rb
  33. +3
    -0
      app/models/concerns/account_associations.rb
  34. +16
    -10
      app/models/concerns/account_interactions.rb
  35. +2
    -1
      app/models/follow.rb
  36. +2
    -1
      app/models/follow_request.rb
  37. +1
    -1
      app/models/form/account_batch.rb
  38. +1
    -1
      app/models/invite.rb
  39. +27
    -17
      app/models/notification.rb
  40. +2
    -2
      app/models/user.rb
  41. +1
    -1
      app/models/webauthn_credential.rb
  42. +4
    -0
      app/policies/account_policy.rb
  43. +1
    -1
      app/serializers/rest/notification_serializer.rb
  44. +9
    -3
      app/serializers/rest/relationship_serializer.rb
  45. +1
    -1
      app/services/after_unallow_domain_service.rb
  46. +1
    -1
      app/services/block_domain_service.rb
  47. +180
    -0
      app/services/delete_account_service.rb
  48. +1
    -1
      app/services/favourite_service.rb
  49. +8
    -7
      app/services/follow_service.rb
  50. +3
    -3
      app/services/import_service.rb
  51. +6
    -2
      app/services/notify_service.rb
  52. +1
    -1
      app/services/process_mentions_service.rb
  53. +1
    -1
      app/services/reblog_service.rb
  54. +30
    -153
      app/services/suspend_account_service.rb
  55. +52
    -0
      app/services/unsuspend_account_service.rb
  56. +58
    -56
      app/views/admin/accounts/show.html.haml
  57. +13
    -0
      app/workers/account_deletion_worker.rb
  58. +13
    -0
      app/workers/admin/account_deletion_worker.rb
  59. +4
    -2
      app/workers/admin/suspension_worker.rb
  60. +13
    -0
      app/workers/admin/unsuspension_worker.rb
  61. +14
    -1
      app/workers/feed_insert_worker.rb
  62. +2
    -2
      app/workers/local_notification_worker.rb
  63. +2
    -2
      app/workers/poll_expiration_notify_worker.rb
  64. +2
    -1
      app/workers/refollow_worker.rb
  65. +13
    -0
      app/workers/scheduler/user_cleanup_scheduler.rb
  66. +3
    -2
      app/workers/unfollow_follow_worker.rb
  67. +23
    -8
      config/locales/en.yml
  68. +4
    -4
      config/locales/simple_form.en.yml
  69. +2
    -2
      config/routes.rb
  70. +8
    -0
      db/migrate/20200908193330_create_account_deletion_requests.rb
  71. +19
    -0
      db/migrate/20200917192924_add_notify_to_follows.rb
  72. +5
    -0
      db/migrate/20200917193034_add_type_to_notifications.rb
  73. +7
    -0
      db/migrate/20200917222316_add_index_notifications_on_type.rb
  74. +22
    -0
      db/post_migrate/20200917193528_migrate_notifications_type.rb
  75. +15
    -0
      db/post_migrate/20200917222734_remove_index_notifications_on_account_activity.rb
  76. +13
    -3
      db/schema.rb
  77. +2
    -2
      lib/mastodon/accounts_cli.rb
  78. +1
    -1
      lib/mastodon/domains_cli.rb
  79. +8
    -8
      package.json
  80. +2
    -18
      spec/controllers/accounts_controller_spec.rb
  81. +57
    -27
      spec/controllers/api/v1/accounts_controller_spec.rb
  82. +2
    -1
      spec/controllers/auth/registrations_controller_spec.rb
  83. +1
    -0
      spec/controllers/concerns/export_controller_concern_spec.rb
  84. +3
    -0
      spec/fabricators/account_deletion_request_fabricator.rb
  85. +4
    -0
      spec/models/account_deletion_request_spec.rb
  86. +1
    -1
      spec/models/concerns/account_interactions_spec.rb
  87. +1
    -1
      spec/models/follow_request_spec.rb
  88. +1
    -1
      spec/models/invite_spec.rb
  89. +2
    -2
      spec/models/webauthn_credentials_spec.rb
  90. +1
    -1
      spec/services/delete_account_service_spec.rb
  91. +1
    -0
      spec/services/import_service_spec.rb
  92. +5
    -1
      spec/services/notify_service_spec.rb
  93. +2
    -2
      spec/workers/refollow_worker_spec.rb
  94. +130
    -119
      yarn.lock

+ 7
- 7
Gemfile View File

@ -6,9 +6,9 @@ ruby '>= 2.5.0', '< 3.0.0'
gem 'pkg-config', '~> 1.4' gem 'pkg-config', '~> 1.4'
gem 'puma', '~> 4.3' gem 'puma', '~> 4.3'
gem 'rails', '~> 5.2.4.3'
gem 'rails', '~> 5.2.4.4'
gem 'sprockets', '~> 3.7.2' gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 0.20'
gem 'thor', '~> 1.0'
gem 'rack', '~> 2.2.3' gem 'rack', '~> 2.2.3'
gem 'thwait', '~> 0.2.0' gem 'thwait', '~> 0.2.0'
@ -20,7 +20,7 @@ gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.7' gem 'pghero', '~> 2.7'
gem 'dotenv-rails', '~> 2.7' gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.79', require: false
gem 'aws-sdk-s3', '~> 1.81', require: false
gem 'fog-core', '<= 2.1.0' gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0' gem 'paperclip', '~> 6.0'
@ -121,12 +121,12 @@ end
group :test do group :test do
gem 'capybara', '~> 3.33' gem 'capybara', '~> 3.33'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.13'
gem 'faker', '~> 2.14'
gem 'microformats', '~> 4.2' gem 'microformats', '~> 4.2'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.1' gem 'rspec-sidekiq', '~> 3.1'
gem 'simplecov', '~> 0.19', require: false gem 'simplecov', '~> 0.19', require: false
gem 'webmock', '~> 3.8'
gem 'webmock', '~> 3.9'
gem 'parallel_tests', '~> 3.2' gem 'parallel_tests', '~> 3.2'
gem 'rspec_junit_formatter', '~> 0.4' gem 'rspec_junit_formatter', '~> 0.4'
end end
@ -134,13 +134,13 @@ end
group :development do group :development do
gem 'active_record_query_trace', '~> 1.7' gem 'active_record_query_trace', '~> 1.7'
gem 'annotate', '~> 3.1' gem 'annotate', '~> 3.1'
gem 'better_errors', '~> 2.7'
gem 'better_errors', '~> 2.8'
gem 'binding_of_caller', '~> 0.7' gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 6.1' gem 'bullet', '~> 6.1'
gem 'letter_opener', '~> 1.7' gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.4' gem 'letter_opener_web', '~> 1.4'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', '~> 0.90', require: false
gem 'rubocop', '~> 0.91', require: false
gem 'rubocop-rails', '~> 2.8', require: false gem 'rubocop-rails', '~> 2.8', require: false
gem 'brakeman', '~> 4.9', require: false gem 'brakeman', '~> 4.9', require: false
gem 'bundler-audit', '~> 0.7', require: false gem 'bundler-audit', '~> 0.7', require: false

+ 66
- 68
Gemfile.lock View File

@ -16,25 +16,25 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (5.2.4.3)
actionpack (= 5.2.4.3)
actioncable (5.2.4.4)
actionpack (= 5.2.4.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailer (5.2.4.3)
actionpack (= 5.2.4.3)
actionview (= 5.2.4.3)
activejob (= 5.2.4.3)
actionmailer (5.2.4.4)
actionpack (= 5.2.4.4)
actionview (= 5.2.4.4)
activejob (= 5.2.4.4)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (5.2.4.3)
actionview (= 5.2.4.3)
activesupport (= 5.2.4.3)
actionpack (5.2.4.4)
actionview (= 5.2.4.4)
activesupport (= 5.2.4.4)
rack (~> 2.0, >= 2.0.8) rack (~> 2.0, >= 2.0.8)
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.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.4.3)
activesupport (= 5.2.4.3)
actionview (5.2.4.4)
activesupport (= 5.2.4.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -45,20 +45,20 @@ 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.7) active_record_query_trace (1.7)
activejob (5.2.4.3)
activesupport (= 5.2.4.3)
activejob (5.2.4.4)
activesupport (= 5.2.4.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (5.2.4.3)
activesupport (= 5.2.4.3)
activerecord (5.2.4.3)
activemodel (= 5.2.4.3)
activesupport (= 5.2.4.3)
activemodel (5.2.4.4)
activesupport (= 5.2.4.4)
activerecord (5.2.4.4)
activemodel (= 5.2.4.4)
activesupport (= 5.2.4.4)
arel (>= 9.0) arel (>= 9.0)
activestorage (5.2.4.3)
actionpack (= 5.2.4.3)
activerecord (= 5.2.4.3)
activestorage (5.2.4.4)
actionpack (= 5.2.4.4)
activerecord (= 5.2.4.4)
marcel (~> 0.3.1) marcel (~> 0.3.1)
activesupport (5.2.4.3)
activesupport (5.2.4.4)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
minitest (~> 5.1) minitest (~> 5.1)
@ -79,23 +79,23 @@ GEM
cocaine (~> 0.5.3) cocaine (~> 0.5.3)
awrence (1.1.1) awrence (1.1.1)
aws-eventstream (1.1.0) aws-eventstream (1.1.0)
aws-partitions (1.365.0)
aws-sdk-core (3.105.0)
aws-partitions (1.373.0)
aws-sdk-core (3.107.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0) aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.37.0)
aws-sdk-kms (1.38.0)
aws-sdk-core (~> 3, >= 3.99.0) aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.79.1)
aws-sdk-s3 (1.81.0)
aws-sdk-core (~> 3, >= 3.104.3) aws-sdk-core (~> 3, >= 3.104.3)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.2) aws-sigv4 (1.2.2)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.16) bcrypt (3.1.16)
better_errors (2.7.1)
better_errors (2.8.1)
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
@ -160,13 +160,12 @@ GEM
cose (1.0.0) cose (1.0.0)
cbor (~> 0.5.9) cbor (~> 0.5.9)
openssl-signature_algorithm (~> 0.4.0) openssl-signature_algorithm (~> 0.4.0)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crack (0.4.4)
crass (1.0.6) crass (1.0.6)
css_parser (1.7.1) css_parser (1.7.1)
addressable addressable
debug_inspector (0.0.3) debug_inspector (0.0.3)
devise (4.7.2)
devise (4.7.3)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0) railties (>= 4.1.0)
@ -210,7 +209,7 @@ GEM
tzinfo tzinfo
excon (0.76.0) excon (0.76.0)
fabrication (2.21.1) fabrication (2.21.1)
faker (2.13.0)
faker (2.14.0)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
faraday (1.0.1) faraday (1.0.1)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
@ -233,7 +232,7 @@ GEM
fog-json (>= 1.0) fog-json (>= 1.0)
ipaddress (>= 0.8) ipaddress (>= 0.8)
formatador (0.2.5) formatador (0.2.5)
fugit (1.3.8)
fugit (1.3.9)
et-orbi (~> 1.1, >= 1.1.8) et-orbi (~> 1.1, >= 1.1.8)
raabro (~> 1.3) raabro (~> 1.3)
fuubar (2.5.0) fuubar (2.5.0)
@ -363,7 +362,7 @@ GEM
net-scp (3.0.0) net-scp (3.0.0)
net-ssh (>= 2.6.5, < 7.0.0) net-ssh (>= 2.6.5, < 7.0.0)
net-ssh (6.1.0) net-ssh (6.1.0)
nio4r (2.5.3)
nio4r (2.5.4)
nokogiri (1.10.10) nokogiri (1.10.10)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
nokogumbo (2.0.2) nokogumbo (2.0.2)
@ -387,7 +386,7 @@ GEM
openssl (2.2.0) openssl (2.2.0)
openssl-signature_algorithm (0.4.0) openssl-signature_algorithm (0.4.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ox (2.13.3)
ox (2.13.4)
paperclip (6.0.0) paperclip (6.0.0)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
@ -406,9 +405,9 @@ GEM
pastel (0.8.0) pastel (0.8.0)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.2.3) pg (1.2.3)
pghero (2.7.0)
pghero (2.7.2)
activerecord (>= 5) activerecord (>= 5)
pkg-config (1.4.2)
pkg-config (1.4.3)
posix-spawn (0.3.15) posix-spawn (0.3.15)
premailer (1.13.1) premailer (1.13.1)
addressable addressable
@ -441,18 +440,18 @@ GEM
rack rack
rack-test (1.1.0) rack-test (1.1.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rails (5.2.4.3)
actioncable (= 5.2.4.3)
actionmailer (= 5.2.4.3)
actionpack (= 5.2.4.3)
actionview (= 5.2.4.3)
activejob (= 5.2.4.3)
activemodel (= 5.2.4.3)
activerecord (= 5.2.4.3)
activestorage (= 5.2.4.3)
activesupport (= 5.2.4.3)
rails (5.2.4.4)
actioncable (= 5.2.4.4)
actionmailer (= 5.2.4.4)
actionpack (= 5.2.4.4)
actionview (= 5.2.4.4)
activejob (= 5.2.4.4)
activemodel (= 5.2.4.4)
activerecord (= 5.2.4.4)
activestorage (= 5.2.4.4)
activesupport (= 5.2.4.4)
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 5.2.4.3)
railties (= 5.2.4.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)
@ -468,9 +467,9 @@ GEM
railties (>= 5.0, < 6) railties (>= 5.0, < 6)
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
rails (>= 4.2.0) rails (>= 4.2.0)
railties (5.2.4.3)
actionpack (= 5.2.4.3)
activesupport (= 5.2.4.3)
railties (5.2.4.4)
actionpack (= 5.2.4.4)
activesupport (= 5.2.4.4)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0) thor (>= 0.19.0, < 2.0)
@ -481,7 +480,7 @@ GEM
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.4.0) rdf-normalize (0.4.0)
rdf (~> 3.1) rdf (~> 3.1)
redis (4.2.1)
redis (4.2.2)
redis-actionpack (5.2.0) redis-actionpack (5.2.0)
actionpack (>= 5, < 7) actionpack (>= 5, < 7)
redis-rack (>= 2.1.0, < 3) redis-rack (>= 2.1.0, < 3)
@ -500,7 +499,7 @@ GEM
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
redis-store (1.9.0) redis-store (1.9.0)
redis (>= 4, < 5) redis (>= 4, < 5)
regexp_parser (1.7.1)
regexp_parser (1.8.0)
request_store (1.5.0) request_store (1.5.0)
rack (>= 1.4) rack (>= 1.4)
responders (3.0.1) responders (3.0.1)
@ -535,18 +534,18 @@ GEM
rspec-support (3.9.3) rspec-support (3.9.3)
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 (0.90.0)
rubocop (0.91.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.7.1.1) parser (>= 2.7.1.1)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.7) regexp_parser (>= 1.7)
rexml rexml
rubocop-ast (>= 0.3.0, < 1.0)
rubocop-ast (>= 0.4.0, < 1.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0) unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (0.3.0)
rubocop-ast (0.4.2)
parser (>= 2.7.1.4) parser (>= 2.7.1.4)
rubocop-rails (2.8.0)
rubocop-rails (2.8.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 0.87.0) rubocop (>= 0.87.0)
@ -555,7 +554,6 @@ GEM
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
rufus-scheduler (3.6.0) rufus-scheduler (3.6.0)
fugit (~> 1.1, >= 1.1.6) fugit (~> 1.1, >= 1.1.6)
safe_yaml (1.0.5)
safety_net_attestation (0.4.0) safety_net_attestation (0.4.0)
jwt (~> 2.0) jwt (~> 2.0)
sanitize (5.2.1) sanitize (5.2.1)
@ -564,7 +562,7 @@ GEM
nokogumbo (~> 2.0) nokogumbo (~> 2.0)
securecompare (1.0.0) securecompare (1.0.0)
semantic_range (2.3.0) semantic_range (2.3.0)
sidekiq (6.1.1)
sidekiq (6.1.2)
connection_pool (>= 2.2.2) connection_pool (>= 2.2.2)
rack (~> 2.0) rack (~> 2.0)
redis (>= 4.2.0) redis (>= 4.2.0)
@ -593,7 +591,7 @@ GEM
sprockets (3.7.2) sprockets (3.7.2)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
sprockets-rails (3.2.1)
sprockets-rails (3.2.2)
actionpack (>= 4.0) actionpack (>= 4.0)
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
@ -612,7 +610,7 @@ GEM
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
terrapin (0.6.0) terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
thor (0.20.3)
thor (1.0.1)
thread_safe (0.3.6) thread_safe (0.3.6)
thwait (0.2.0) thwait (0.2.0)
e2mmap e2mmap
@ -653,7 +651,7 @@ GEM
safety_net_attestation (~> 0.4.0) safety_net_attestation (~> 0.4.0)
securecompare (~> 1.0) securecompare (~> 1.0)
tpm-key_attestation (~> 0.9.0) tpm-key_attestation (~> 0.9.0)
webmock (3.8.3)
webmock (3.9.1)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
@ -680,8 +678,8 @@ DEPENDENCIES
active_record_query_trace (~> 1.7) active_record_query_trace (~> 1.7)
addressable (~> 2.7) addressable (~> 2.7)
annotate (~> 3.1) annotate (~> 3.1)
aws-sdk-s3 (~> 1.79)
better_errors (~> 2.7)
aws-sdk-s3 (~> 1.81)
better_errors (~> 2.8)
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
blurhash (~> 0.1) blurhash (~> 0.1)
bootsnap (~> 1.4) bootsnap (~> 1.4)
@ -710,7 +708,7 @@ DEPENDENCIES
e2mmap (~> 0.1.0) e2mmap (~> 0.1.0)
ed25519 (~> 1.2) ed25519 (~> 1.2)
fabrication (~> 2.21) fabrication (~> 2.21)
faker (~> 2.13)
faker (~> 2.14)
fast_blank (~> 1.0) fast_blank (~> 1.0)
fastimage fastimage
fog-core (<= 2.1.0) fog-core (<= 2.1.0)
@ -766,7 +764,7 @@ DEPENDENCIES
rack (~> 2.2.3) rack (~> 2.2.3)
rack-attack (~> 6.3) rack-attack (~> 6.3)
rack-cors (~> 1.1) rack-cors (~> 1.1)
rails (~> 5.2.4.3)
rails (~> 5.2.4.4)
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
rails-i18n (~> 5.1) rails-i18n (~> 5.1)
rails-settings-cached (~> 0.6) rails-settings-cached (~> 0.6)
@ -778,7 +776,7 @@ DEPENDENCIES
rspec-rails (~> 4.0) rspec-rails (~> 4.0)
rspec-sidekiq (~> 3.1) rspec-sidekiq (~> 3.1)
rspec_junit_formatter (~> 0.4) rspec_junit_formatter (~> 0.4)
rubocop (~> 0.90)
rubocop (~> 0.91)
rubocop-rails (~> 2.8) rubocop-rails (~> 2.8)
ruby-progressbar (~> 1.10) ruby-progressbar (~> 1.10)
sanitize (~> 5.2) sanitize (~> 5.2)
@ -795,12 +793,12 @@ DEPENDENCIES
stoplight (~> 2.2.1) stoplight (~> 2.2.1)
streamio-ffmpeg (~> 3.0) streamio-ffmpeg (~> 3.0)
strong_migrations (~> 0.7) strong_migrations (~> 0.7)
thor (~> 0.20)
thor (~> 1.0)
thwait (~> 0.2.0) thwait (~> 0.2.0)
tty-prompt (~> 0.22) tty-prompt (~> 0.22)
twitter-text (~> 1.14) twitter-text (~> 1.14)
tzinfo-data (~> 1.2020) tzinfo-data (~> 1.2020)
webauthn (~> 3.0.0.alpha1) webauthn (~> 3.0.0.alpha1)
webmock (~> 3.8)
webmock (~> 3.9)
webpacker (~> 5.2) webpacker (~> 5.2)
webpush webpush

+ 2
- 9
app/controllers/accounts_controller.rb View File

@ -7,6 +7,7 @@ class AccountsController < ApplicationController
include AccountControllerConcern include AccountControllerConcern
include SignatureAuthentication include SignatureAuthentication
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers before_action :set_cache_headers
before_action :set_body_classes before_action :set_body_classes
@ -48,7 +49,7 @@ class AccountsController < ApplicationController
format.json do format.json do
expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?) expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter
end end
end end
end end
@ -153,12 +154,4 @@ class AccountsController < ApplicationController
def params_slice(*keys) def params_slice(*keys)
params.slice(*keys).permit(*keys) params.slice(*keys).permit(*keys)
end end
def restrict_fields_to
if signed_request_account.present? || public_fetch_mode?
# Return all fields
else
%i(id type preferred_username inbox public_key endpoints)
end
end
end end

+ 20
- 11
app/controllers/admin/accounts_controller.rb View File

@ -2,7 +2,7 @@
module Admin module Admin
class AccountsController < BaseController class AccountsController < BaseController
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
before_action :set_account, except: [:index]
before_action :require_remote_account!, only: [:redownload] before_action :require_remote_account!, only: [:redownload]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
@ -14,49 +14,58 @@ module Admin
def show def show
authorize @account, :show? authorize @account, :show?
@deletion_request = @account.deletion_request
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account) @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest @moderation_notes = @account.targeted_moderation_notes.latest
@warnings = @account.targeted_account_warnings.latest.custom @warnings = @account.targeted_account_warnings.latest.custom
@domain_block = DomainBlock.rule_for(@account.domain)
end end
def memorialize def memorialize
authorize @account, :memorialize? authorize @account, :memorialize?
@account.memorialize! @account.memorialize!
log_action :memorialize, @account log_action :memorialize, @account
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.memorialized_msg', username: @account.acct)
end end
def enable def enable
authorize @account.user, :enable? authorize @account.user, :enable?
@account.user.enable! @account.user.enable!
log_action :enable, @account.user log_action :enable, @account.user
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.enabled_msg', username: @account.acct)
end end
def approve def approve
authorize @account.user, :approve? authorize @account.user, :approve?
@account.user.approve! @account.user.approve!
redirect_to admin_pending_accounts_path
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
end end
def reject def reject
authorize @account.user, :reject? authorize @account.user, :reject?
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
redirect_to admin_pending_accounts_path
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
end
def destroy
authorize @account, :destroy?
Admin::AccountDeletionWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.destroyed_msg', username: @account.acct)
end end
def unsilence def unsilence
authorize @account, :unsilence? authorize @account, :unsilence?
@account.unsilence! @account.unsilence!
log_action :unsilence, @account log_action :unsilence, @account
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsilenced_msg', username: @account.acct)
end end
def unsuspend def unsuspend
authorize @account, :unsuspend? authorize @account, :unsuspend?
@account.unsuspend! @account.unsuspend!
Admin::UnsuspensionWorker.perform_async(@account.id)
log_action :unsuspend, @account log_action :unsuspend, @account
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsuspended_msg', username: @account.acct)
end end
def redownload def redownload
@ -65,7 +74,7 @@ module Admin
@account.update!(last_webfingered_at: nil) @account.update!(last_webfingered_at: nil)
ResolveAccountService.new.call(@account) ResolveAccountService.new.call(@account)
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.redownloaded_msg', username: @account.acct)
end end
def remove_avatar def remove_avatar
@ -76,7 +85,7 @@ module Admin
log_action :remove_avatar, @account.user log_action :remove_avatar, @account.user
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_avatar_msg', username: @account.acct)
end end
def remove_header def remove_header
@ -87,7 +96,7 @@ module Admin
log_action :remove_header, @account.user log_action :remove_header, @account.user
redirect_to admin_account_path(@account.id)
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct)
end end
private private

+ 2
- 2
app/controllers/api/base_controller.rb View File

@ -96,12 +96,12 @@ class Api::BaseController < ApplicationController
def require_user! def require_user!
if !current_user if !current_user
render json: { error: 'This method requires an authenticated user' }, status: 422 render json: { error: 'This method requires an authenticated user' }, status: 422
elsif current_user.disabled?
render json: { error: 'Your login is currently disabled' }, status: 403
elsif !current_user.confirmed? elsif !current_user.confirmed?
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403 render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
elsif !current_user.approved? elsif !current_user.approved?
render json: { error: 'Your login is currently pending approval' }, status: 403 render json: { error: 'Your login is currently pending approval' }, status: 403
elsif !current_user.functional?
render json: { error: 'Your login is currently disabled' }, status: 403
else else
set_user_activity set_user_activity
end end

+ 2
- 3
app/controllers/api/v1/accounts_controller.rb View File

@ -30,9 +30,8 @@ class Api::V1::AccountsController < Api::BaseController
end end
def follow def follow
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true)
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, with_rate_limit: true)
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } }
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
end end

+ 8
- 1
app/controllers/api/v1/admin/accounts_controller.rb View File

@ -58,7 +58,13 @@ class Api::V1::Admin::AccountsController < Api::BaseController
def reject def reject
authorize @account.user, :reject? authorize @account.user, :reject?
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
render json: @account, serializer: REST::Admin::AccountSerializer
end
def destroy
authorize @account, :destroy?
Admin::AccountDeletionWorker.perform_async(@account.id)
render json: @account, serializer: REST::Admin::AccountSerializer render json: @account, serializer: REST::Admin::AccountSerializer
end end
@ -72,6 +78,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
def unsuspend def unsuspend
authorize @account, :unsuspend? authorize @account, :unsuspend?
@account.unsuspend! @account.unsuspend!
Admin::UnsuspensionWorker.perform_async(@account.id)
log_action :unsuspend, @account log_action :unsuspend, @account
render json: @account, serializer: REST::Admin::AccountSerializer render json: @account, serializer: REST::Admin::AccountSerializer
end end

+ 1
- 1
app/controllers/api/v1/follow_requests_controller.rb View File

@ -13,7 +13,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
def authorize def authorize
AuthorizeFollowService.new.call(account, current_account) AuthorizeFollowService.new.call(account, current_account)
NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account))
NotifyService.new.call(current_account, :follow, Follow.find_by(account: account, target_account: current_account))
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
end end

+ 1
- 1
app/controllers/api/v1/push/subscriptions_controller.rb View File

@ -52,6 +52,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
def data_params def data_params
return {} if params[:data].blank? return {} if params[:data].blank?
params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
end end
end end

+ 2
- 1
app/controllers/api/web/push_subscriptions_controller.rb View File

@ -22,6 +22,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
reblog: alerts_enabled, reblog: alerts_enabled,
mention: alerts_enabled, mention: alerts_enabled,
poll: alerts_enabled, poll: alerts_enabled,
status: alerts_enabled,
}, },
} }
@ -57,6 +58,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
end end
def data_params def data_params
@data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
@data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
end end
end end

+ 1
- 1
app/controllers/settings/deletes_controller.rb View File

@ -43,7 +43,7 @@ class Settings::DeletesController < Settings::BaseController
def destroy_account! def destroy_account!
current_account.suspend! current_account.suspend!
Admin::SuspensionWorker.perform_async(current_user.account_id, true)
AccountDeletionWorker.perform_async(current_user.account_id)
sign_out sign_out
end end
end end

+ 2
- 2
app/javascript/mastodon/actions/accounts.js View File

@ -109,14 +109,14 @@ export function fetchAccountFail(id, error) {
}; };
}; };
export function followAccount(id, reblogs = true) {
export function followAccount(id, options = { reblogs: true }) {
return (dispatch, getState) => { return (dispatch, getState) => {
const alreadyFollowing = getState().getIn(['relationships', id, 'following']); const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
const locked = getState().getIn(['accounts', id, 'locked'], false); const locked = getState().getIn(['accounts', id, 'locked'], false);
dispatch(followAccountRequest(id, locked)); dispatch(followAccountRequest(id, locked));
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
dispatch(followAccountSuccess(response.data, alreadyFollowing)); dispatch(followAccountSuccess(response.data, alreadyFollowing));
}).catch(error => { }).catch(error => {
dispatch(followAccountFail(error, locked)); dispatch(followAccountFail(error, locked));

+ 1
- 1
app/javascript/mastodon/actions/markers.js View File

@ -57,7 +57,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const _buildParams = (state) => { const _buildParams = (state) => {
const params = {}; const params = {};
const lastHomeId = state.getIn(['timelines', 'home', 'items', 0]);
const lastHomeId = state.getIn(['timelines', 'home', 'items']).find(item => item !== null);
const lastNotificationId = state.getIn(['notifications', 'items', 0, 'id']); const lastNotificationId = state.getIn(['notifications', 'items', 0, 'id']);
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) { if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {

+ 1
- 1
app/javascript/mastodon/actions/notifications.js View File

@ -59,7 +59,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
let filtered = false; let filtered = false;
if (notification.type === 'mention') {
if (['mention', 'status'].includes(notification.type)) {
const dropRegex = filters[0]; const dropRegex = filters[0];
const regex = filters[1]; const regex = filters[1];
const searchIndex = searchTextFromRawStatus(notification.status); const searchIndex = searchTextFromRawStatus(notification.status);

+ 17
- 3
app/javascript/mastodon/components/error_boundary.js View File

@ -66,17 +66,31 @@ export default class ErrorBoundary extends React.PureComponent {
} }
render() { render() {
const { hasError, copied } = this.state;
const { hasError, copied, errorMessage } = this.state;
if (!hasError) { if (!hasError) {
return this.props.children; return this.props.children;
} }
const likelyBrowserAddonIssue = errorMessage && errorMessage.includes('NotFoundError');
return ( return (
<div className='error-boundary'> <div className='error-boundary'>
<div> <div>
<p className='error-boundary__error'><FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' /></p>
<p><FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' /></p>
<p className='error-boundary__error'>
{ likelyBrowserAddonIssue ? (
<FormattedMessage id='error.unexpected_crash.explanation_addons' defaultMessage='This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.' />
) : (
<FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' />
)}
</p>
<p>
{ likelyBrowserAddonIssue ? (
<FormattedMessage id='error.unexpected_crash.next_steps_addons' defaultMessage='Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
) : (
<FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
)}
</p>
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p> <p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
</div> </div>
</div> </div>

+ 11
- 1
app/javascript/mastodon/features/account/components/header.js View File

@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me, isStaff } from 'mastodon/initial_state'; import { autoPlayGif, me, isStaff } from 'mastodon/initial_state';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import IconButton from 'mastodon/components/icon_button';
import Avatar from 'mastodon/components/avatar'; import Avatar from 'mastodon/components/avatar';
import { counterRenderer } from 'mastodon/components/common_counter'; import { counterRenderer } from 'mastodon/components/common_counter';
import ShortNumber from 'mastodon/components/short_number'; import ShortNumber from 'mastodon/components/short_number';
@ -35,6 +36,8 @@ const messages = defineMessages({
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
@ -68,8 +71,9 @@ class Header extends ImmutablePureComponent {
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onReblogToggle: PropTypes.func.isRequired, onReblogToggle: PropTypes.func.isRequired,
onNotifyToggle: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired, onUnblockDomain: PropTypes.func.isRequired,
@ -144,6 +148,7 @@ class Header extends ImmutablePureComponent {
let info = []; let info = [];
let actionBtn = ''; let actionBtn = '';
let bellBtn = '';
let lockedIcon = ''; let lockedIcon = '';
let menu = []; let menu = [];
@ -173,6 +178,10 @@ class Header extends ImmutablePureComponent {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />; actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
} }
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
}
if (account.get('moved') && !account.getIn(['relationship', 'following'])) { if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
actionBtn = ''; actionBtn = '';
} }
@ -287,6 +296,7 @@ class Header extends ImmutablePureComponent {
{!suspended && ( {!suspended && (
<div className='account__header__tabs__buttons'> <div className='account__header__tabs__buttons'>
{actionBtn} {actionBtn}
{bellBtn}
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' /> <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
</div> </div>

+ 5
- 0
app/javascript/mastodon/features/account_timeline/components/header.js View File

@ -55,6 +55,10 @@ export default class Header extends ImmutablePureComponent {
this.props.onReblogToggle(this.props.account); this.props.onReblogToggle(this.props.account);
} }
handleNotifyToggle = () => {
this.props.onNotifyToggle(this.props.account);
}
handleMute = () => { handleMute = () => {
this.props.onMute(this.props.account); this.props.onMute(this.props.account);
} }
@ -106,6 +110,7 @@ export default class Header extends ImmutablePureComponent {
onMention={this.handleMention} onMention={this.handleMention}
onDirect={this.handleDirect} onDirect={this.handleDirect}
onReblogToggle={this.handleReblogToggle} onReblogToggle={this.handleReblogToggle}
onNotifyToggle={this.handleNotifyToggle}
onReport={this.handleReport} onReport={this.handleReport}
onMute={this.handleMute} onMute={this.handleMute}
onBlockDomain={this.handleBlockDomain} onBlockDomain={this.handleBlockDomain}

+ 10
- 2
app/javascript/mastodon/features/account_timeline/containers/header_container.js View File

@ -76,9 +76,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onReblogToggle (account) { onReblogToggle (account) {
if (account.getIn(['relationship', 'showing_reblogs'])) { if (account.getIn(['relationship', 'showing_reblogs'])) {
dispatch(followAccount(account.get('id'), false));
dispatch(followAccount(account.get('id'), { reblogs: false }));
} else { } else {
dispatch(followAccount(account.get('id'), true));
dispatch(followAccount(account.get('id'), { reblogs: true }));
} }
}, },
@ -90,6 +90,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
} }
}, },
onNotifyToggle (account) {
if (account.getIn(['relationship', 'notifying'])) {
dispatch(followAccount(account.get('id'), { notify: false }));
} else {
dispatch(followAccount(account.get('id'), { notify: true }));
}
},
onReport (account) { onReport (account) {
dispatch(initReport(account)); dispatch(initReport(account));
}, },

+ 8
- 0
app/javascript/mastodon/features/notifications/components/filter_bar.js View File

@ -9,6 +9,7 @@ const tooltips = defineMessages({
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' }, boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
}); });
export default @injectIntl export default @injectIntl
@ -87,6 +88,13 @@ class FilterBar extends React.PureComponent {
> >
<Icon id='tasks' fixedWidth /> <Icon id='tasks' fixedWidth />
</button> </button>
<button
className={selectedFilter === 'status' ? 'active' : ''}
onClick={this.onClick('status')}
title={intl.formatMessage(tooltips.statuses)}
>
<Icon id='home' fixedWidth />
</button>
<button <button
className={selectedFilter === 'follow' ? 'active' : ''} className={selectedFilter === 'follow' ? 'active' : ''}
onClick={this.onClick('follow')} onClick={this.onClick('follow')}

+ 35
- 0
app/javascript/mastodon/features/notifications/components/notification.js View File

@ -17,6 +17,7 @@ const messages = defineMessages({
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' }, ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' }, poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' }, reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
}); });
const notificationForScreenReader = (intl, message, timestamp) => { const notificationForScreenReader = (intl, message, timestamp) => {
@ -237,6 +238,38 @@ class Notification extends ImmutablePureComponent {
); );
} }
renderStatus (notification, link) {
const { intl } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-status focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.status, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='home' fixedWidth />
</div>
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.status' defaultMessage='{name} just posted' values={{ name: link }} />
</span>
</div>
<StatusContainer
id={notification.get('status')}
account={notification.get('account')}
muted
withDismiss
hidden={this.props.hidden}
getScrollPosition={this.props.getScrollPosition}
updateScrollBottom={this.props.updateScrollBottom}
cachedMediaWidth={this.props.cachedMediaWidth}
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
);
}
renderPoll (notification, account) { renderPoll (notification, account) {
const { intl } = this.props; const { intl } = this.props;
const ownPoll = me === account.get('id'); const ownPoll = me === account.get('id');
@ -292,6 +325,8 @@ class Notification extends ImmutablePureComponent {
return this.renderFavourite(notification, link); return this.renderFavourite(notification, link);
case 'reblog': case 'reblog':
return this.renderReblog(notification, link); return this.renderReblog(notification, link);
case 'status':
return this.renderStatus(notification, link);
case 'poll': case 'poll':
return this.renderPoll(notification, account); return this.renderPoll(notification, account);
} }

+ 1
- 1
app/javascript/mastodon/features/notifications/index.js View File

@ -32,7 +32,7 @@ const getNotifications = createSelector([
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category // we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type'))); return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
} }
return notifications.filter(item => item !== null && allowedType === item.get('type'));
return notifications.filter(item => item === null || allowedType === item.get('type'));
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({

+ 5
- 0
app/javascript/styles/contrast/diff.scss View File

@ -75,3 +75,8 @@
.public-layout .public-account-header__tabs__tabs .counter.active::after { .public-layout .public-account-header__tabs__tabs .counter.active::after {
border-bottom: 4px solid $ui-highlight-color; border-bottom: 4px solid $ui-highlight-color;
} }
.compose-form .autosuggest-textarea__textarea::placeholder,
.compose-form .spoiler-input__input::placeholder {
color: $inverted-text-color;
}

+ 4
- 0
app/javascript/styles/mastodon/components.scss View File

@ -6502,6 +6502,10 @@ noscript {
padding: 2px; padding: 2px;
} }
& > .icon-button {
margin-right: 8px;
}
.button { .button {
margin: 0 8px; margin: 0 8px;
} }

+ 2
- 2
app/lib/activitypub/activity.rb View File

@ -118,13 +118,13 @@ class ActivityPub::Activity
end end
def notify_about_reblog(status) def notify_about_reblog(status)
NotifyService.new.call(status.reblog.account, status)
NotifyService.new.call(status.reblog.account, :reblog, status)
end end
def notify_about_mentions(status) def notify_about_mentions(status)
status.active_mentions.includes(:account).each do |mention| status.active_mentions.includes(:account).each do |mention|
next unless mention.account.local? && audience_includes?(mention.account) next unless mention.account.local? && audience_includes?(mention.account)
NotifyService.new.call(mention.account, mention)
NotifyService.new.call(mention.account, :mention, mention)
end end
end end

+ 1
- 1
app/lib/activitypub/activity/delete.rb View File

@ -13,7 +13,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
def delete_person def delete_person
lock_or_return("delete_in_progress:#{@account.id}") do lock_or_return("delete_in_progress:#{@account.id}") do
SuspendAccountService.new.call(@account, reserve_username: false)
DeleteAccountService.new.call(@account, reserve_username: false)
end end
end end

+ 2
- 2
app/lib/activitypub/activity/follow.rb View File

@ -22,10 +22,10 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id']) follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id'])
if target_account.locked? || @account.silenced? if target_account.locked? || @account.silenced?
NotifyService.new.call(target_account, follow_request)
NotifyService.new.call(target_account, :follow_request, follow_request)
else else
AuthorizeFollowService.new.call(@account, target_account) AuthorizeFollowService.new.call(@account, target_account)
NotifyService.new.call(target_account, ::Follow.find_by(account: @account, target_account: target_account))
NotifyService.new.call(target_account, :follow, ::Follow.find_by(account: @account, target_account: target_account))
end end
end end

+ 1
- 1
app/lib/activitypub/activity/like.rb View File

@ -7,6 +7,6 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) || @account.favourited?(original_status) return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) || @account.favourited?(original_status)
favourite = original_status.favourites.create!(account: @account) favourite = original_status.favourites.create!(account: @account)
NotifyService.new.call(original_status.account, favourite)
NotifyService.new.call(original_status.account, :favourite, favourite)
end end
end end

+ 9
- 7
app/mailers/notification_mailer.rb View File

@ -10,7 +10,7 @@ class NotificationMailer < ApplicationMailer
@me = recipient @me = recipient
@status = notification.target_status @status = notification.target_status
return if @me.user.disabled? || @status.nil?
return unless @me.user.functional? && @status.present?
locale_for_account(@me) do locale_for_account(@me) do
thread_by_conversation(@status.conversation) thread_by_conversation(@status.conversation)
@ -22,7 +22,7 @@ class NotificationMailer < ApplicationMailer
@me = recipient @me = recipient
@account = notification.from_account @account = notification.from_account
return if @me.user.disabled?
return unless @me.user.functional?
locale_for_account(@me) do locale_for_account(@me) do
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct) mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
@ -34,7 +34,7 @@ class NotificationMailer < ApplicationMailer
@account = notification.from_account @account = notification.from_account
@status = notification.target_status @status = notification.target_status
return if @me.user.disabled? || @status.nil?
return unless @me.user.functional? && @status.present?
locale_for_account(@me) do locale_for_account(@me) do
thread_by_conversation(@status.conversation) thread_by_conversation(@status.conversation)
@ -47,7 +47,7 @@ class NotificationMailer < ApplicationMailer
@account = notification.from_account @account = notification.from_account
@status = notification.target_status @status = notification.target_status
return if @me.user.disabled? || @status.nil?
return unless @me.user.functional? && @status.present?
locale_for_account(@me) do locale_for_account(@me) do
thread_by_conversation(@status.conversation) thread_by_conversation(@status.conversation)
@ -59,7 +59,7 @@ class NotificationMailer < ApplicationMailer
@me = recipient @me = recipient
@account = notification.from_account @account = notification.from_account
return if @me.user.disabled?
return unless @me.user.functional?
locale_for_account(@me) do locale_for_account(@me) do
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct) mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
@ -67,7 +67,7 @@ class NotificationMailer < ApplicationMailer
end end
def digest(recipient, **opts) def digest(recipient, **opts)
return if recipient.user.disabled?
return unless recipient.user.functional?
@me = recipient @me = recipient
@since = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max @since = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max
@ -88,8 +88,10 @@ class NotificationMailer < ApplicationMailer
def thread_by_conversation(conversation) def thread_by_conversation(conversation)
return if conversation.nil? return if conversation.nil?
msg_id = "<conversation-#{conversation.id}.#{conversation.created_at.strftime('%Y-%m-%d')}@#{Rails.configuration.x.local_domain}>" msg_id = "<conversation-#{conversation.id}.#{conversation.created_at.strftime('%Y-%m-%d')}@#{Rails.configuration.x.local_domain}>"
headers['In-Reply-To'] = msg_id headers['In-Reply-To'] = msg_id
headers['References'] = msg_id
headers['References'] = msg_id
end end
end end

+ 14
- 14
app/mailers/user_mailer.rb View File

@ -15,7 +15,7 @@ class UserMailer < Devise::Mailer
@token = token @token = token
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
return if @resource.disabled?
return unless @resource.active_for_authentication?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.unconfirmed_email.presence || @resource.email, mail to: @resource.unconfirmed_email.presence || @resource.email,
@ -29,7 +29,7 @@ class UserMailer < Devise::Mailer
@token = token @token = token
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
return if @resource.disabled?
return unless @resource.active_for_authentication?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject') mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject')
@ -40,7 +40,7 @@ class UserMailer < Devise::Mailer
@resource = user @resource = user
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
return if @resource.disabled?
return unless @resource.active_for_authentication?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject') mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject')
@ -51,7 +51,7 @@ class UserMailer < Devise::Mailer
@resource = user @resource = user
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
return if @resource.disabled?
return unless @resource.active_for_authentication?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.email_changed.subject') mail to: @resource.email, subject: I18n.t('devise.mailer.email_changed.subject')
@ -62,7 +62,7 @@ class UserMailer < Devise::Mailer
@resource = user @resource = user
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
return if @resource.disabled?
return unless @resource.active_for_authentication?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_enabled.subject') mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_enabled.subject')
@ -73,7 +73,7 @@ class UserMailer < Devise::Mailer
@resource = user @resource = user
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
return if @resource.disabled?
return unless @resource.active_for_authentication?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_disabled.subject') mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_disabled.subject')
@ -84,7 +84,7 @@ class UserMailer < Devise::Mailer
@resource = user @resource = user
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
return if @resource.disabled?
return unless @resource.active_for_authentication?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject') mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject')
@ -95,7 +95,7 @@ class UserMailer < Devise::Mailer
@resource = user @resource = user
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
return if @resource.disabled?
return unless @resource.active_for_authentication?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_enabled.subject') mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_enabled.subject')
@ -106,7 +106,7 @@ class UserMailer < Devise::Mailer
@resource = user @resource = user
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
return if @resource.disabled?
return unless @resource.active_for_authentication?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_disabled.subject') mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_disabled.subject')
@ -118,7 +118,7 @@ class UserMailer < Devise::Mailer
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
@webauthn_credential = webauthn_credential @webauthn_credential = webauthn_credential
return if @resource.disabled?
return unless @resource.active_for_authentication?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.added.subject') mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.added.subject')
@ -130,7 +130,7 @@ class UserMailer < Devise::Mailer
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
@webauthn_credential = webauthn_credential @webauthn_credential = webauthn_credential
return if @resource.disabled?
return unless @resource.active_for_authentication?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.deleted.subject') mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.deleted.subject')
@ -141,7 +141,7 @@ class UserMailer < Devise::Mailer
@resource = user @resource = user
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
return if @resource.disabled?
return unless @resource.active_for_authentication?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('user_mailer.welcome.subject') mail to: @resource.email, subject: I18n.t('user_mailer.welcome.subject')
@ -153,7 +153,7 @@ class UserMailer < Devise::Mailer
@instance = Rails.configuration.x.local_domain @instance = Rails.configuration.x.local_domain
@backup = backup @backup = backup
return if @resource.disabled?
return unless @resource.active_for_authentication?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('user_mailer.backup_ready.subject') mail to: @resource.email, subject: I18n.t('user_mailer.backup_ready.subject')
@ -181,7 +181,7 @@ class UserMailer < Devise::Mailer
@detection = Browser.new(user_agent) @detection = Browser.new(user_agent)
@timestamp = timestamp.to_time.utc @timestamp = timestamp.to_time.utc
return if @resource.disabled?
return unless @resource.active_for_authentication?
I18n.with_locale(@resource.locale || I18n.default_locale) do I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, mail to: @resource.email,

+ 3
- 6
app/models/account.rb View File

@ -222,23 +222,20 @@ class Account < ApplicationRecord
def suspend!(date = Time.now.utc) def suspend!(date = Time.now.utc)
transaction do transaction do
user&.disable! if local?
create_deletion_request!
update!(suspended_at: date) update!(suspended_at: date)
end end
end end
def unsuspend! def unsuspend!
transaction do transaction do
user&.enable! if local?
deletion_request&.destroy!
update!(suspended_at: nil) update!(suspended_at: nil)
end end
end end
def memorialize! def memorialize!
transaction do
user&.disable! if local?
update!(memorial: true)
end
update!(memorial: true)
end end
def sign? def sign?

+ 20
- 0
app/models/account_deletion_request.rb View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_deletion_requests
#
# id :bigint(8) not null, primary key
# account_id :bigint(8)
# created_at :datetime not null
# updated_at :datetime not null
#
class AccountDeletionRequest < ApplicationRecord
DELAY_TO_DELETION = 30.days.freeze
belongs_to :account
def due_at
created_at + DELAY_TO_DELETION
end
end

+ 1
- 1
app/models/admin/account_action.rb View File

@ -134,7 +134,7 @@ class Admin::AccountAction
end end
def process_email! def process_email!
UserMailer.warning(target_account.user, warning, status_ids).deliver_now! if warnable?
UserMailer.warning(target_account.user, warning, status_ids).deliver_later! if warnable?
end end
def warnable? def warnable?

+ 3
- 0
app/models/concerns/account_associations.rb View File

@ -60,5 +60,8 @@ module AccountAssociations
# Hashtags # Hashtags
has_and_belongs_to_many :tags has_and_belongs_to_many :tags
has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
# Account deletion requests
has_one :deletion_request, class_name: 'AccountDeletionRequest', inverse_of: :account, dependent: :destroy
end end
end end

+ 16
- 10
app/models/concerns/account_interactions.rb View File

@ -8,6 +8,7 @@ module AccountInteractions
Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping| Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
mapping[follow.target_account_id] = { mapping[follow.target_account_id] = {
reblogs: follow.show_reblogs?, reblogs: follow.show_reblogs?,
notify: follow.notify?,
} }
end end
end end
@ -36,6 +37,7 @@ module AccountInteractions
FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping| FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping|
mapping[follow_request.target_account_id] = { mapping[follow_request.target_account_id] = {
reblogs: follow_request.show_reblogs?, reblogs: follow_request.show_reblogs?,
notify: follow_request.notify?,
} }
end end
end end
@ -95,25 +97,29 @@ module AccountInteractions
has_many :announcement_mutes, dependent: :destroy has_many :announcement_mutes, dependent: :destroy
end end
def follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
reblogs = true if reblogs.nil?
rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false)
rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit)
.find_or_create_by!(target_account: other_account) .find_or_create_by!(target_account: other_account)
rel.update!(show_reblogs: reblogs)
rel.show_reblogs = reblogs unless reblogs.nil?
rel.notify = notify unless notify.nil?
rel.save! if rel.changed?
remove_potential_friendship(other_account) remove_potential_friendship(other_account)
rel rel
end end
def request_follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
reblogs = true if reblogs.nil?
rel = follow_requests.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
def request_follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false)
rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit)
.find_or_create_by!(target_account: other_account) .find_or_create_by!(target_account: other_account)
rel.update!(show_reblogs: reblogs)
rel.show_reblogs = reblogs unless reblogs.nil?
rel.notify = notify unless notify.nil?
rel.save! if rel.changed?
remove_potential_friendship(other_account) remove_potential_friendship(other_account)
rel rel

+ 2
- 1
app/models/follow.rb View File

@ -10,6 +10,7 @@
# target_account_id :bigint(8) not null # target_account_id :bigint(8) not null
# show_reblogs :boolean default(TRUE), not null # show_reblogs :boolean default(TRUE), not null
# uri :string # uri :string
# notify :boolean default(FALSE), not null
# #
class Follow < ApplicationRecord class Follow < ApplicationRecord
@ -34,7 +35,7 @@ class Follow < ApplicationRecord
end end
def revoke_request! def revoke_request!
FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, uri: uri)
FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, notify: notify, uri: uri)
destroy! destroy!
end end

+ 2
- 1
app/models/follow_request.rb View File

@ -10,6 +10,7 @@
# target_account_id :bigint(8) not null # target_account_id :bigint(8) not null
# show_reblogs :boolean default(TRUE), not null # show_reblogs :boolean default(TRUE), not null
# uri :string # uri :string
# notify :boolean default(FALSE), not null
# #
class FollowRequest < ApplicationRecord class FollowRequest < ApplicationRecord
@ -28,7 +29,7 @@ class FollowRequest < ApplicationRecord
validates_with FollowLimitValidator, on: :create validates_with FollowLimitValidator, on: :create
def authorize! def authorize!
account.follow!(target_account, reblogs: show_reblogs, uri: uri)
account.follow!(target_account, reblogs: show_reblogs, notify: notify, uri: uri)
MergeWorker.perform_async(target_account.id, account.id) if account.local? MergeWorker.perform_async(target_account.id, account.id) if account.local?
destroy! destroy!
end end

+ 1
- 1
app/models/form/account_batch.rb View File

@ -69,6 +69,6 @@ class Form::AccountBatch
records = accounts.includes(:user) records = accounts.includes(:user)
records.each { |account| authorize(account.user, :reject?) } records.each { |account| authorize(account.user, :reject?) }
.each { |account| SuspendAccountService.new.call(account, reserve_email: false, reserve_username: false) }
.each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) }
end end
end end

+ 1
- 1
app/models/invite.rb View File

@ -28,7 +28,7 @@ class Invite < ApplicationRecord
before_validation :set_code before_validation :set_code
def valid_for_use? def valid_for_use?
(max_uses.nil? || uses < max_uses) && !expired? && !(user.nil? || user.disabled?)
(max_uses.nil? || uses < max_uses) && !expired? && user&.functional?
end end
private private

+ 27
- 17
app/models/notification.rb View File

@ -10,21 +10,34 @@
# updated_at :datetime not null # updated_at :datetime not null
# account_id :bigint(8) not null # account_id :bigint(8) not null
# from_account_id :bigint(8) not null # from_account_id :bigint(8) not null
# type :string
# #
class Notification < ApplicationRecord class Notification < ApplicationRecord
self.inheritance_column = nil
include Paginable include Paginable
include Cacheable include Cacheable
TYPE_CLASS_MAP = {
mention: 'Mention',
reblog: 'Status',
follow: 'Follow',
follow_request: 'FollowRequest',
favourite: 'Favourite',
poll: 'Poll',
LEGACY_TYPE_CLASS_MAP = {
'Mention' => :mention,
'Status' => :reblog,
'Follow' => :follow,
'FollowRequest' => :follow_request,
'Favourite' => :favourite,
'Poll' => :poll,
}.freeze }.freeze
TYPES = %i(
mention
status
reblog
follow
follow_request
favourite
poll
).freeze
STATUS_INCLUDES = [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account]].freeze STATUS_INCLUDES = [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account]].freeze
belongs_to :account, optional: true belongs_to :account, optional: true
@ -38,29 +51,30 @@ class Notification < ApplicationRecord
belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id', optional: true belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id', optional: true
belongs_to :poll, foreign_type: 'Poll', foreign_key: 'activity_id', optional: true belongs_to :poll, foreign_type: 'Poll', foreign_key: 'activity_id', optional: true
validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
validates :type, inclusion: { in: TYPES }
scope :without_suspended, -> { joins(:from_account).merge(Account.without_suspended) } scope :without_suspended, -> { joins(:from_account).merge(Account.without_suspended) }
scope :browserable, ->(exclude_types = [], account_id = nil) { scope :browserable, ->(exclude_types = [], account_id = nil) {
types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types)
types = TYPES - exclude_types.map(&:to_sym)
if account_id.nil? if account_id.nil?
where(activity_type: types)
where(type: types)
else else
where(activity_type: types, from_account_id: account_id)
where(type: types, from_account_id: account_id)
end end
} }
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, follow_request: :account, poll: [status: STATUS_INCLUDES] cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, follow_request: :account, poll: [status: STATUS_INCLUDES]
def type def type
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
@type ||= (super || LEGACY_TYPE_CLASS_MAP[activity_type]).to_sym
end end
def target_status def target_status
case type case type
when :status
status
when :reblog when :reblog
status&.reblog status&.reblog
when :favourite when :favourite
@ -89,10 +103,6 @@ class Notification < ApplicationRecord
item.target_status.account = accounts[item.target_status.account_id] if item.target_status item.target_status.account = accounts[item.target_status.account_id] if item.target_status
end end
end end
def activity_types_from_types(types)
types.map { |type| TYPE_CLASS_MAP[type.to_sym] }.compact
end
end end
after_initialize :set_from_account after_initialize :set_from_account

+ 2
- 2
app/models/user.rb View File

@ -168,7 +168,7 @@ class User < ApplicationRecord
end end
def active_for_authentication? def active_for_authentication?
true
!account.memorial?
end end
def suspicious_sign_in?(ip) def suspicious_sign_in?(ip)
@ -176,7 +176,7 @@ class User < ApplicationRecord
end end
def functional? def functional?
confirmed? && approved? && !disabled? && !account.suspended? && account.moved_to_account_id.nil?
confirmed? && approved? && !disabled? && !account.suspended? && !account.memorial? && account.moved_to_account_id.nil?
end end
def unconfirmed_or_pending? def unconfirmed_or_pending?

+ 1
- 1
app/models/webauthn_credential.rb View File

@ -18,5 +18,5 @@ class WebauthnCredential < ApplicationRecord
validates :external_id, uniqueness: true validates :external_id, uniqueness: true
validates :nickname, uniqueness: { scope: :user_id } validates :nickname, uniqueness: { scope: :user_id }
validates :sign_count, validates :sign_count,
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 }
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**63 - 1 }
end end

+ 4
- 0
app/policies/account_policy.rb View File

@ -17,6 +17,10 @@ class AccountPolicy < ApplicationPolicy
staff? && !record.user&.staff? staff? && !record.user&.staff?
end end
def destroy?
record.suspended? && record.deletion_request.present? && admin?
end
def unsuspend? def unsuspend?
staff? staff?
end end

+ 1
- 1
app/serializers/rest/notification_serializer.rb View File

@ -11,6 +11,6 @@ class REST::NotificationSerializer < ActiveModel::Serializer
end end
def status_type? def status_type?
[:favourite, :reblog, :mention, :poll].include?(object.type)
[:favourite, :reblog, :status, :mention, :poll].include?(object.type)
end end
end end

+ 9
- 3
app/serializers/rest/relationship_serializer.rb View File

@ -1,9 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class REST::RelationshipSerializer < ActiveModel::Serializer class REST::RelationshipSerializer < ActiveModel::Serializer
attributes :id, :following, :showing_reblogs, :followed_by, :blocking, :blocked_by,
:muting, :muting_notifications, :requested, :domain_blocking,
:endorsed, :note
attributes :id, :following, :showing_reblogs, :notifying, :followed_by,
:blocking, :blocked_by, :muting, :muting_notifications, :requested,
:domain_blocking, :endorsed, :note
def id def id
object.id.to_s object.id.to_s
@ -19,6 +19,12 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
false false
end end
def notifying
(instance_options[:relationships].following[object.id] || {})[:notify] ||
(instance_options[:relationships].requested[object.id] || {})[:notify] ||
false
end
def followed_by def followed_by
instance_options[:relationships].followed_by[object.id] || false instance_options[:relationships].followed_by[object.id] || false
end end

+ 1
- 1
app/services/after_unallow_domain_service.rb View File

@ -3,7 +3,7 @@
class AfterUnallowDomainService < BaseService class AfterUnallowDomainService < BaseService
def call(domain) def call(domain)
Account.where(domain: domain).find_each do |account| Account.where(domain: domain).find_each do |account|
SuspendAccountService.new.call(account, reserve_username: false)
DeleteAccountService.new.call(account, reserve_username: false)
end end
end end
end end

+ 1
- 1
app/services/block_domain_service.rb View File

@ -36,7 +36,7 @@ class BlockDomainService < BaseService
def suspend_accounts! def suspend_accounts!
blocked_domain_accounts.without_suspended.in_batches.update_all(suspended_at: @domain_block.created_at) blocked_domain_accounts.without_suspended.in_batches.update_all(suspended_at: @domain_block.created_at)
blocked_domain_accounts.where(suspended_at: @domain_block.created_at).reorder(nil).find_each do |account| blocked_domain_accounts.where(suspended_at: @domain_block.created_at).reorder(nil).find_each do |account|
SuspendAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at)
DeleteAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at)
end end
end end

+ 180
- 0
app/services/delete_account_service.rb View File

@ -0,0 +1,180 @@
# frozen_string_literal: true
class DeleteAccountService < BaseService
include Payloadable
ASSOCIATIONS_ON_SUSPEND = %w(
account_pins
active_relationships
block_relationships
blocked_by_relationships
conversation_mutes
conversations
custom_filters
domain_blocks
favourites
follow_requests
list_accounts
mute_relationships
muted_by_relationships
notifications
owned_lists
passive_relationships
report_notes
scheduled_statuses
status_pins
).freeze
ASSOCIATIONS_ON_DESTROY = %w(
reports
targeted_moderation_notes
targeted_reports
).freeze
# Suspend or remove an account and remove as much of its data
# as possible. If it's a local account and it has not been confirmed
# or never been approved, then side effects are skipped and both
# the user and account records are removed fully. Otherwise,
# it is controlled by options.
# @param [Account]
# @param [Hash] options
# @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts
# @option [Boolean] :reserve_username Keep account record
# @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads
# @option [Time] :suspended_at Only applicable when :reserve_username is true
def call(account, **options)
@account = account
@options = { reserve_username: true, reserve_email: true }.merge(options)
if @account.local? && @account.user_unconfirmed_or_pending?
@options[:reserve_email] = false
@options[:reserve_username] = false
@options[:skip_side_effects] = true
end
reject_follows!
purge_user!
purge_profile!
purge_content!
fulfill_deletion_request!
end
private
def reject_follows!
return if @account.local? || !@account.activitypub?
ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
[build_reject_json(follow), follow.target_account_id, follow.account.inbox_url]
end
end
def purge_user!
return if !@account.local? || @account.user.nil?
if @options[:reserve_email]
@account.user.disable!
@account.user.invites.where(uses: 0).destroy_all
else
@account.user.destroy
end
end
def purge_content!
distribute_delete_actor! if @account.local? && !@options[:skip_side_effects]
@account.statuses.reorder(nil).find_in_batches do |statuses|
statuses.reject! { |status| reported_status_ids.include?(status.id) } if @options[:reserve_username]
BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:skip_side_effects])
end
@account.media_attachments.reorder(nil).find_each do |media_attachment|
next if @options[:reserve_username] && reported_status_ids.include?(media_attachment.status_id)
media_attachment.destroy
end
@account.polls.reorder(nil).find_each do |poll|
next if @options[:reserve_username] && reported_status_ids.include?(poll.status_id)
poll.destroy
end
associations_for_destruction.each do |association_name|
destroy_all(@account.public_send(association_name))
end
@account.destroy unless @options[:reserve_username]
end
def purge_profile!
# If the account is going to be destroyed
# there is no point wasting time updating
# its values first
return unless @options[:reserve_username]
@account.silenced_at = nil
@account.suspended_at = @options[:suspended_at] || Time.now.utc
@account.locked = false
@account.memorial = false
@account.discoverable = false
@account.display_name = ''
@account.note = ''
@account.fields = []
@account.statuses_count = 0
@account.followers_count = 0
@account.following_count = 0
@account.moved_to_account = nil
@account.trust_level = :untrusted
@account.avatar.destroy
@account.header.destroy
@account.save!
end
def fulfill_deletion_request!
@account.deletion_request&.destroy
end
def destroy_all(association)
association.in_batches.destroy_all
end
def distribute_delete_actor!
ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url|
[delete_actor_json, @account.id, inbox_url]
end
ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes) do |inbox_url|
[delete_actor_json, @account.id, inbox_url]
end
end
def delete_actor_json
@delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account))
end
def build_reject_json(follow)
Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
end
def delivery_inboxes
@delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url)
end
def low_priority_delivery_inboxes
Account.inboxes - delivery_inboxes
end
def reported_status_ids
@reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq
end
def associations_for_destruction
if @options[:reserve_username]
ASSOCIATIONS_ON_SUSPEND
else
ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY
end
end
end

+ 1
- 1
app/services/favourite_service.rb View File

@ -29,7 +29,7 @@ class FavouriteService < BaseService
status = favourite.status status = favourite.status
if status.account.local? if status.account.local?
NotifyService.new.call(status.account, favourite)
NotifyService.new.call(status.account, :favourite, favourite)
elsif status.account.activitypub? elsif status.account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url) ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url)
end end

+ 8
- 7
app/services/follow_service.rb View File

@ -9,12 +9,13 @@ class FollowService < BaseService
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record) # @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
# @param [Hash] options # @param [Hash] options
# @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true # @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true
# @option [Boolean] :notify Whether to create notifications about new posts, defaults to false
# @option [Boolean] :bypass_locked # @option [Boolean] :bypass_locked
# @option [Boolean] :with_rate_limit # @option [Boolean] :with_rate_limit
def call(source_account, target_account, options = {}) def call(source_account, target_account, options = {})
@source_account = source_account @source_account = source_account
@target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true) @target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
@options = { reblogs: true, bypass_locked: false, with_rate_limit: false }.merge(options)
@options = { bypass_locked: false, with_rate_limit: false }.merge(options)
raise ActiveRecord::RecordNotFound if following_not_possible? raise ActiveRecord::RecordNotFound if following_not_possible?
raise Mastodon::NotPermittedError if following_not_allowed? raise Mastodon::NotPermittedError if following_not_allowed?
@ -45,18 +46,18 @@ class FollowService < BaseService
end end
def change_follow_options! def change_follow_options!
@source_account.follow!(@target_account, reblogs: @options[:reblogs])
@source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify])
end end
def change_follow_request_options! def change_follow_request_options!
@source_account.request_follow!(@target_account, reblogs: @options[:reblogs])
@source_account.request_follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify])
end end
def request_follow! def request_follow!
follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit])
follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit])
if @target_account.local? if @target_account.local?
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name)
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, :follow_request)
elsif @target_account.activitypub? elsif @target_account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url) ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url)
end end
@ -65,9 +66,9 @@ class FollowService < BaseService
end end
def direct_follow! def direct_follow!
follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit])
follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit])
LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name)
LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, :follow)
MergeWorker.perform_async(@target_account.id, @source_account.id) MergeWorker.perform_async(@target_account.id, @source_account.id)
follow follow

+ 3
- 3
app/services/import_service.rb View File

@ -25,7 +25,7 @@ class ImportService < BaseService
def import_follows! def import_follows!
parse_import_data!(['Account address']) parse_import_data!(['Account address'])
import_relationships!('follow', 'unfollow', @account.following, follow_limit, reblogs: 'Show boosts')
import_relationships!('follow', 'unfollow', @account.following, follow_limit, reblogs: { header: 'Show boosts', default: true })
end end
def import_blocks! def import_blocks!
@ -35,7 +35,7 @@ class ImportService < BaseService
def import_mutes! def import_mutes!
parse_import_data!(['Account address']) parse_import_data!(['Account address'])
import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT, notifications: 'Hide notifications')
import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT, notifications: { header: 'Hide notifications', default: true })
end end
def import_domain_blocks! def import_domain_blocks!
@ -65,7 +65,7 @@ class ImportService < BaseService
def import_relationships!(action, undo_action, overwrite_scope, limit, extra_fields = {}) def import_relationships!(action, undo_action, overwrite_scope, limit, extra_fields = {})
local_domain_suffix = "@#{Rails.configuration.x.local_domain}" local_domain_suffix = "@#{Rails.configuration.x.local_domain}"
items = @data.take(limit).map { |row| [row['Account address']&.strip&.delete_suffix(local_domain_suffix), Hash[extra_fields.map { |key, header| [key, row[header]&.strip] }]] }.reject { |(id, _)| id.blank? }
items = @data.take(limit).map { |row| [row['Account address']&.strip&.delete_suffix(local_domain_suffix), Hash[extra_fields.map { |key, field_settings| [key, row[field_settings[:header]]&.strip || field_settings[:default]] }]] }.reject { |(id, _)| id.blank? }
if @import.overwrite? if @import.overwrite?
presence_hash = items.each_with_object({}) { |(id, extra), mapping| mapping[id] = [true, extra] } presence_hash = items.each_with_object({}) { |(id, extra), mapping| mapping[id] = [true, extra] }

+ 6
- 2
app/services/notify_service.rb View File

@ -1,10 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class NotifyService < BaseService class NotifyService < BaseService
def call(recipient, activity)
def call(recipient, type, activity)
@recipient = recipient @recipient = recipient
@activity = activity @activity = activity
@notification = Notification.new(account: @recipient, activity: @activity)
@notification = Notification.new(account: @recipient, type: type, activity: @activity)
return if recipient.user.nil? || blocked? return if recipient.user.nil? || blocked?
@ -22,6 +22,10 @@ class NotifyService < BaseService
FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient) FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient)
end end
def blocked_status?
false
end
def blocked_favourite? def blocked_favourite?
false false
end end

+ 1
- 1
app/services/process_mentions_service.rb View File

@ -58,7 +58,7 @@ class ProcessMentionsService < BaseService
mentioned_account = mention.account mentioned_account = mention.account
if mentioned_account.local? if mentioned_account.local?
LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name)
LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name, :mention)
elsif mentioned_account.activitypub? elsif mentioned_account.activitypub?
ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url) ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url)
end end

+ 1
- 1
app/services/reblog_service.rb View File

@ -45,7 +45,7 @@ class ReblogService < BaseService
reblogged_status = reblog.reblog reblogged_status = reblog.reblog
if reblogged_status.account.local? if reblogged_status.account.local?
LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name)
LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name, :reblog)
elsif reblogged_status.account.activitypub? && !reblogged_status.account.following?(reblog.account) elsif reblogged_status.account.activitypub? && !reblogged_status.account.following?(reblog.account)
ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url) ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url)
end end

+ 30
- 153
app/services/suspend_account_service.rb View File

@ -1,175 +1,52 @@
# frozen_string_literal: true # frozen_string_literal: true
class SuspendAccountService < BaseService class SuspendAccountService < BaseService
include Payloadable
ASSOCIATIONS_ON_SUSPEND = %w(
account_pins
active_relationships
block_relationships
blocked_by_relationships
conversation_mutes
conversations
custom_filters
domain_blocks
favourites
follow_requests
list_accounts
mute_relationships
muted_by_relationships
notifications
owned_lists
passive_relationships
report_notes
scheduled_statuses
status_pins
).freeze
ASSOCIATIONS_ON_DESTROY = %w(
reports
targeted_moderation_notes
targeted_reports
).freeze
# Suspend or remove an account and remove as much of its data
# as possible. If it's a local account and it has not been confirmed
# or never been approved, then side effects are skipped and both
# the user and account records are removed fully. Otherwise,
# it is controlled by options.
# @param [Account]
# @param [Hash] options
# @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts
# @option [Boolean] :reserve_username Keep account record
# @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads
# @option [Time] :suspended_at Only applicable when :reserve_username is true
def call(account, **options)
def call(account)
@account = account @account = account
@options = { reserve_username: true, reserve_email: true }.merge(options)
if @account.local? && @account.user_unconfirmed_or_pending?
@options[:reserve_email] = false
@options[:reserve_username] = false
@options[:skip_side_effects] = true
end
reject_follows!
purge_user!
purge_profile!
purge_content!
suspend!
unmerge_from_home_timelines!
unmerge_from_list_timelines!
privatize_media_attachments!
end end
private private
def reject_follows!
return if @account.local? || !@account.activitypub?
ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
[build_reject_json(follow), follow.target_account_id, follow.account.inbox_url]
end
def suspend!
@account.suspend! unless @account.suspended?
end end
def purge_user!
return if !@account.local? || @account.user.nil?
if @options[:reserve_email]
@account.user.disable!
@account.user.invites.where(uses: 0).destroy_all
else
@account.user.destroy
def unmerge_from_home_timelines!
@account.followers_for_local_distribution.find_each do |follower|
FeedManager.instance.unmerge_from_timeline(@account, follower)
end end
end end
def purge_content!
distribute_delete_actor! if @account.local? && !@options[:skip_side_effects]
@account.statuses.reorder(nil).find_in_batches do |statuses|
statuses.reject! { |status| reported_status_ids.include?(status.id) } if @options[:reserve_username]
BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:skip_side_effects])
def unmerge_from_list_timelines!
@account.lists_for_local_distribution.find_each do |list|
FeedManager.instance.unmerge_from_list(@account, list)
end end
@account.media_attachments.reorder(nil).find_each do |media_attachment|
next if @options[:reserve_username] && reported_status_ids.include?(media_attachment.status_id)
media_attachment.destroy
end
@account.polls.reorder(nil).find_each do |poll|
next if @options[:reserve_username] && reported_status_ids.include?(poll.status_id)
poll.destroy
end
associations_for_destruction.each do |association_name|
destroy_all(@account.public_send(association_name))
end
@account.destroy unless @options[:reserve_username]
end end
def purge_profile!
# If the account is going to be destroyed
# there is no point wasting time updating
# its values first
return unless @options[:reserve_username]
def privatize_media_attachments!
attachment_names = MediaAttachment.attachment_definitions.keys
@account.silenced_at = nil
@account.suspended_at = @options[:suspended_at] || Time.now.utc
@account.locked = false
@account.memorial = false
@account.discoverable = false
@account.display_name = ''
@account.note = ''
@account.fields = []
@account.statuses_count = 0
@account.followers_count = 0
@account.following_count = 0
@account.moved_to_account = nil
@account.trust_level = :untrusted
@account.avatar.destroy
@account.header.destroy
@account.save!
end
def destroy_all(association)
association.in_batches.destroy_all
end
def distribute_delete_actor!
ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url|
[delete_actor_json, @account.id, inbox_url]
end
ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes) do |inbox_url|
[delete_actor_json, @account.id, inbox_url]
end
end
def delete_actor_json
@delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account))
end
def build_reject_json(follow)
Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
end
def delivery_inboxes
@delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url)
end
def low_priority_delivery_inboxes
Account.inboxes - delivery_inboxes
end
def reported_status_ids
@reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq
end
@account.media_attachments.find_each do |media_attachment|
attachment_names.each do |attachment_name|
attachment = media_attachment.public_send(attachment_name)
styles = [:original] | attachment.styles.keys
def associations_for_destruction
if @options[:reserve_username]
ASSOCIATIONS_ON_SUSPEND
else
ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY
styles.each do |style|
case Paperclip::Attachment.default_options[:storage]
when :s3
attachment.s3_object(style).acl.put(:private)
when :fog
# Not supported
when :filesystem
FileUtils.chmod(0o600 & ~File.umask, attachment.path(style))
end
end
end
end end
end end
end end

+ 52
- 0
app/services/unsuspend_account_service.rb View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
class UnsuspendAccountService < BaseService
def call(account)
@account = account
unsuspend!
merge_into_home_timelines!
merge_into_list_timelines!
publish_media_attachments!
end
private
def unsuspend!
@account.unsuspend! if @account.suspended?
end
def merge_into_home_timelines!
@account.followers_for_local_distribution.find_each do |follower|
FeedManager.instance.merge_into_timeline(@account, follower)
end
end
def merge_into_list_timelines!
@account.lists_for_local_distribution.find_each do |list|
FeedManager.instance.merge_into_list(@account, list)
end
end
def publish_media_attachments!
attachment_names = MediaAttachment.attachment_definitions.keys
@account.media_attachments.find_each do |media_attachment|
attachment_names.each do |attachment_name|
attachment = media_attachment.public_send(attachment_name)
styles = [:original] | attachment.styles.keys
styles.each do |style|
case Paperclip::Attachment.default_options[:storage]
when :s3
attachment.s3_object(style).acl.put(Paperclip::Attachment.default_options[:s3_permissions])
when :fog
# Not supported
when :filesystem
FileUtils.chmod(0o666 & ~File.umask, attachment.path(style))
end
end
end
end
end
end

+ 58
- 56
app/views/admin/accounts/show.html.haml View File

@ -56,19 +56,21 @@
= link_to admin_action_logs_path(target_account_id: @account.id) do = link_to admin_action_logs_path(target_account_id: @account.id) do
.dashboard__counters__text .dashboard__counters__text
- if @account.local? && @account.user.nil? - if @account.local? && @account.user.nil?
%span.neutral= t('admin.accounts.deleted')
= t('admin.accounts.deleted')
- elsif @account.memorial?
= t('admin.accounts.memorialized')
- elsif @account.suspended? - elsif @account.suspended?
%span.red= t('admin.accounts.suspended')
= t('admin.accounts.suspended')
- elsif @account.silenced? - elsif @account.silenced?
%span.red= t('admin.accounts.silenced')
= t('admin.accounts.silenced')
- elsif @account.local? && @account.user&.disabled? - elsif @account.local? && @account.user&.disabled?
%span.red= t('admin.accounts.disabled')
= t('admin.accounts.disabled')
- elsif @account.local? && !@account.user&.confirmed? - elsif @account.local? && !@account.user&.confirmed?
%span.neutral= t('admin.accounts.confirming')
= t('admin.accounts.confirming')
- elsif @account.local? && !@account.user_approved? - elsif @account.local? && !@account.user_approved?
%span.neutral= t('admin.accounts.pending')
= t('admin.accounts.pending')
- else - else
%span.neutral= t('admin.accounts.no_limits_imposed')
= t('admin.accounts.no_limits_imposed')
.dashboard__counters__label= t 'admin.accounts.login_status' .dashboard__counters__label= t 'admin.accounts.login_status'
- unless @account.local? && @account.user.nil? - unless @account.local? && @account.user.nil?
@ -122,19 +124,6 @@
= 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= t('admin.accounts.login_status')
%td
- if @account.user&.disabled?
= t('admin.accounts.disabled')
- else
= t('admin.accounts.enabled')
%td
- if @account.user&.disabled?
= table_link_to 'unlock', t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post if can?(:enable, @account.user)
- elsif @account.user_approved?
= table_link_to 'lock', t('admin.accounts.disable'), new_admin_account_action_path(@account.id, type: 'disable') if can?(:disable, @account.user)
%tr %tr
%th= t('simple_form.labels.defaults.locale') %th= t('simple_form.labels.defaults.locale')
%td= @account.user_locale %td= @account.user_locale
@ -172,49 +161,62 @@
%td %td
= @account.inbox_url = @account.inbox_url
= fa_icon DeliveryFailureTracker.available?(@account.inbox_url) ? 'check' : 'times' = fa_icon DeliveryFailureTracker.available?(@account.inbox_url) ? 'check' : 'times'
%td
= table_link_to 'search', @domain_block.present? ? t('admin.domain_blocks.view') : t('admin.accounts.view_domain'), admin_instance_path(@account.domain)
%tr %tr
%th= t('admin.accounts.shared_inbox_url') %th= t('admin.accounts.shared_inbox_url')
%td %td
= @account.shared_inbox_url = @account.shared_inbox_url
= fa_icon DeliveryFailureTracker.available?(@account.shared_inbox_url) ? 'check': 'times' = fa_icon DeliveryFailureTracker.available?(@account.shared_inbox_url) ? 'check': 'times'
%td
- if @domain_block.nil?
= table_link_to 'ban', t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain)
- if @account.suspended?
%hr.spacer/
%p.muted-hint= @deletion_request.present? ? t('admin.accounts.suspension_reversible_hint_html', date: content_tag(:strong, l(@deletion_request.due_at.to_date))) : t('admin.accounts.suspension_irreversible')
= link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account)
%div.action-buttons
%div
- if @account.local? && @account.user_approved?
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account)
- if @account.silenced?
= link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account)
- elsif !@account.local? || @account.user_approved?
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@account.id, type: 'silence'), class: 'button button--destructive' if can?(:silence, @account)
- if @account.local?
- if @account.user_pending?
= link_to t('admin.accounts.approve'), approve_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:approve, @account.user)
= link_to t('admin.accounts.reject'), reject_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:reject, @account.user)
- unless @account.user_confirmed?
= link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user)
- if @account.suspended?
= link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account)
- elsif !@account.local? || @account.user_approved?
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button button--destructive' if can?(:suspend, @account)
- unless @account.local?
- if DomainBlock.rule_for(@account.domain)
= link_to t('admin.domain_blocks.view'), admin_instance_path(@account.domain), class: 'button'
- if @deletion_request.present?
= link_to t('admin.accounts.delete'), admin_account_path(@account.id), method: :delete, class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, @account)
- else
%div.action-buttons
%div
- if @account.local? && @account.user_approved?
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account)
- if @account.user_disabled?
= link_to t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post, class: 'button' if can?(:enable, @account.user)
- else
= link_to t('admin.accounts.disable'), new_admin_account_action_path(@account.id, type: 'disable'), class: 'button' if can?(:disable, @account.user)
- if @account.silenced?
= link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account)
- elsif !@account.local? || @account.user_approved?
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@account.id, type: 'silence'), class: 'button' if can?(:silence, @account)
- if @account.local?
- if @account.user_pending?
= link_to t('admin.accounts.approve'), approve_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:approve, @account.user)
= link_to t('admin.accounts.reject'), reject_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:reject, @account.user)
- unless @account.user_confirmed?
= link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user)
- if !@account.local? || @account.user_approved?
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button' if can?(:suspend, @account)
%div
- 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?
= 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
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive'
%div
- 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?
= 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
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)
%hr.spacer/ %hr.spacer/

+ 13
- 0
app/workers/account_deletion_worker.rb View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class AccountDeletionWorker
include Sidekiq::Worker
sidekiq_options queue: 'pull'
def perform(account_id)
DeleteAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: false)
rescue ActiveRecord::RecordNotFound
true
end
end

+ 13
- 0
app/workers/admin/account_deletion_worker.rb View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class Admin::AccountDeletionWorker
include Sidekiq::Worker
sidekiq_options queue: 'pull'
def perform(account_id)
DeleteAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: true)
rescue ActiveRecord::RecordNotFound
true
end
end

+ 4
- 2
app/workers/admin/suspension_worker.rb View File

@ -5,7 +5,9 @@ class Admin::SuspensionWorker
sidekiq_options queue: 'pull' sidekiq_options queue: 'pull'
def perform(account_id, remove_user = false)
SuspendAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: !remove_user)
def perform(account_id)
SuspendAccountService.new.call(Account.find(account_id))
rescue ActiveRecord::RecordNotFound
true
end end
end end

+ 13
- 0
app/workers/admin/unsuspension_worker.rb View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class Admin::UnsuspensionWorker
include Sidekiq::Worker
sidekiq_options queue: 'pull'
def perform(account_id)
UnsuspendAccountService.new.call(Account.find(account_id))
rescue ActiveRecord::RecordNotFound
true
end
end

+ 14
- 1
app/workers/feed_insert_worker.rb View File

@ -23,7 +23,10 @@ class FeedInsertWorker
private private
def check_and_insert def check_and_insert
perform_push unless feed_filtered?
return if feed_filtered?
perform_push
perform_notify if notify?
end end
def feed_filtered? def feed_filtered?
@ -35,6 +38,12 @@ class FeedInsertWorker
end end
end end
def notify?
return false if @type != :home || @status.reblog? || (@status.reply? && @status.in_reply_to_account_id != @status.account_id)
Follow.find_by(account: @follower, target_account: @status.account)&.notify?
end
def perform_push def perform_push
case @type case @type
when :home when :home
@ -43,4 +52,8 @@ class FeedInsertWorker
FeedManager.instance.push_to_list(@list, @status) FeedManager.instance.push_to_list(@list, @status)
end end
end end
def perform_notify
NotifyService.new.call(@follower, :status, @status)
end
end end

+ 2
- 2
app/workers/local_notification_worker.rb View File

@ -3,7 +3,7 @@
class LocalNotificationWorker class LocalNotificationWorker
include Sidekiq::Worker include Sidekiq::Worker
def perform(receiver_account_id, activity_id = nil, activity_class_name = nil)
def perform(receiver_account_id, activity_id = nil, activity_class_name = nil, type = nil)
if activity_id.nil? && activity_class_name.nil? if activity_id.nil? && activity_class_name.nil?
activity = Mention.find(receiver_account_id) activity = Mention.find(receiver_account_id)
receiver = activity.account receiver = activity.account
@ -12,7 +12,7 @@ class LocalNotificationWorker
activity = activity_class_name.constantize.find(activity_id) activity = activity_class_name.constantize.find(activity_id)
end end
NotifyService.new.call(receiver, activity)
NotifyService.new.call(receiver, type || activity_class_name.underscore, activity)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
true true
end end

+ 2
- 2
app/workers/poll_expiration_notify_worker.rb View File

@ -11,12 +11,12 @@ class PollExpirationNotifyWorker
# Notify poll owner and remote voters # Notify poll owner and remote voters
if poll.local? if poll.local?
ActivityPub::DistributePollUpdateWorker.perform_async(poll.status.id) ActivityPub::DistributePollUpdateWorker.perform_async(poll.status.id)
NotifyService.new.call(poll.account, poll)
NotifyService.new.call(poll.account, :poll, poll)
end end
# Notify local voters # Notify local voters
poll.votes.includes(:account).map(&:account).select(&:local?).each do |account| poll.votes.includes(:account).map(&:account).select(&:local?).each do |account|
NotifyService.new.call(account, poll)
NotifyService.new.call(account, :poll, poll)
end end
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
true true

+ 2
- 1
app/workers/refollow_worker.rb View File

@ -11,6 +11,7 @@ class RefollowWorker
target_account.passive_relationships.where(account: Account.where(domain: nil)).includes(:account).reorder(nil).find_each do |follow| target_account.passive_relationships.where(account: Account.where(domain: nil)).includes(:account).reorder(nil).find_each do |follow|
reblogs = follow.show_reblogs? reblogs = follow.show_reblogs?
notify = follow.notify?
# Locally unfollow remote account # Locally unfollow remote account
follower = follow.account follower = follow.account
@ -18,7 +19,7 @@ class RefollowWorker
# Schedule re-follow # Schedule re-follow
begin begin
FollowService.new.call(follower, target_account, reblogs: reblogs)
FollowService.new.call(follower, target_account, reblogs: reblogs, notify: notify)
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError
next next
end end

+ 13
- 0
app/workers/scheduler/user_cleanup_scheduler.rb View File

@ -6,9 +6,22 @@ class Scheduler::UserCleanupScheduler
sidekiq_options lock: :until_executed, retry: 0 sidekiq_options lock: :until_executed, retry: 0
def perform def perform
clean_unconfirmed_accounts!
clean_suspended_accounts!
end
private
def clean_unconfirmed_accounts!
User.where('confirmed_at is NULL AND confirmation_sent_at <= ?', 2.days.ago).reorder(nil).find_in_batches do |batch| User.where('confirmed_at is NULL AND confirmation_sent_at <= ?', 2.days.ago).reorder(nil).find_in_batches do |batch|
Account.where(id: batch.map(&:account_id)).delete_all Account.where(id: batch.map(&:account_id)).delete_all
User.where(id: batch.map(&:id)).delete_all User.where(id: batch.map(&:id)).delete_all
end end
end end
def clean_suspended_accounts!
AccountDeletionRequest.where('created_at <= ?', AccountDeletionRequest::DELAY_TO_DELETION.ago).reorder(nil).find_each do |deletion_request|
Admin::AccountDeletionWorker.perform_async(deletion_request.account_id)
end
end
end end

+ 3
- 2
app/workers/unfollow_follow_worker.rb View File

@ -10,10 +10,11 @@ class UnfollowFollowWorker
old_target_account = Account.find(old_target_account_id) old_target_account = Account.find(old_target_account_id)
new_target_account = Account.find(new_target_account_id) new_target_account = Account.find(new_target_account_id)
follow = follower_account.active_relationships.find_by(target_account: old_target_account)
follow = follower_account.active_relationships.find_by(target_account: old_target_account)
reblogs = follow&.show_reblogs? reblogs = follow&.show_reblogs?
notify = follow&.notify?
FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, bypass_locked: bypass_locked)
FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, bypass_locked: bypass_locked)
UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true) UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true)
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
true true

+ 23
- 8
config/locales/en.yml View File

@ -98,6 +98,7 @@ en:
add_email_domain_block: Block e-mail domain add_email_domain_block: Block e-mail domain
approve: Approve approve: Approve
approve_all: Approve all approve_all: Approve all
approved_msg: Successfully approved %{username}'s sign-up application
are_you_sure: Are you sure? are_you_sure: Are you sure?
avatar: Avatar avatar: Avatar
by_domain: Domain by_domain: Domain
@ -111,18 +112,21 @@ en:
confirm: Confirm confirm: Confirm
confirmed: Confirmed confirmed: Confirmed
confirming: Confirming confirming: Confirming
delete: Delete data
deleted: Deleted deleted: Deleted
demote: Demote demote: Demote
disable: Disable
destroyed_msg: "%{username}'s data is now queued to be deleted imminently"
disable: Freeze
disable_two_factor_authentication: Disable 2FA disable_two_factor_authentication: Disable 2FA
disabled: Disabled
disabled: Frozen
display_name: Display name display_name: Display name
domain: Domain domain: Domain
edit: Edit edit: Edit
email: Email email: Email
email_status: Email status email_status: Email status
enable: Enable
enable: Unfreeze
enabled: Enabled enabled: Enabled
enabled_msg: Successfully unfroze %{username}'s account
followers: Followers followers: Followers
follows: Follows follows: Follows
header: Header header: Header
@ -138,6 +142,8 @@ en:
login_status: Login status login_status: Login status
media_attachments: Media attachments media_attachments: Media attachments
memorialize: Turn into memoriam memorialize: Turn into memoriam
memorialized: Memorialized
memorialized_msg: Successfully turned %{username} into a memorial account
moderation: moderation:
active: Active active: Active
all: All all: All
@ -158,10 +164,14 @@ en:
public: Public public: Public
push_subscription_expires: PuSH subscription expires push_subscription_expires: PuSH subscription expires
redownload: Refresh profile redownload: Refresh profile
redownloaded_msg: Successfully refreshed %{username}'s profile from origin
reject: Reject reject: Reject
reject_all: Reject all reject_all: Reject all
rejected_msg: Successfully rejected %{username}'s sign-up application
remove_avatar: Remove avatar remove_avatar: Remove avatar
remove_header: Remove header remove_header: Remove header
removed_avatar_msg: Successfully removed %{username}'s avatar image
removed_header_msg: Successfully removed %{username}'s header image
resend_confirmation: resend_confirmation:
already_confirmed: This user is already confirmed already_confirmed: This user is already confirmed
send: Resend confirmation email send: Resend confirmation email
@ -182,18 +192,23 @@ en:
show: show:
created_reports: Made reports created_reports: Made reports
targeted_reports: Reported by others targeted_reports: Reported by others
silence: Silence
silenced: Silenced
silence: Limit
silenced: Limited
statuses: Statuses statuses: Statuses
subscribe: Subscribe subscribe: Subscribe
suspended: Suspended suspended: Suspended
suspension_irreversible: The data of this account has been irreversibly deleted. You can unsuspend the account to make it usable but it will not recover any data it previously had.
suspension_reversible_hint_html: The account has been suspended, and the data will be fully removed on %{date}. Until then, the account can be restored without any ill effects. If you wish to remove all of the account's data immediately, you can do so below.
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_silenced: Undo silence undo_silenced: Undo silence
undo_suspension: Undo suspension undo_suspension: Undo suspension
unsilenced_msg: Successfully unlimited %{username}'s account
unsubscribe: Unsubscribe unsubscribe: Unsubscribe
unsuspended_msg: Successfully unsuspended %{username}'s account
username: Username username: Username
view_domain: View summary for domain
warn: Warn warn: Warn
web: Web web: Web
whitelisted: Allowed for federation whitelisted: Allowed for federation
@ -1304,9 +1319,9 @@ en:
title: Sign in attempt title: Sign in attempt
warning: warning:
explanation: explanation:
disable: While your account is frozen, your account data remains intact, but you cannot perform any actions until it is unlocked.
silence: While your account is limited, only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follow you.
suspend: Your account has been suspended, and all of your toots and your uploaded media files have been irreversibly removed from this server, and servers where you had followers.
disable: You can no longer login to your account or use it in any other way, but your profile and other data remains intact.
silence: You can still use your account but only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follow you.
suspend: You can no longer use your account, and your profile and other data are no longer accessible. You can still login to request a backup of your data until the data is fully removed, but we will retain some data to prevent you from evading the suspension.
get_in_touch: You can reply to this e-mail to get in touch with the staff of %{instance}. get_in_touch: You can reply to this e-mail to get in touch with the staff of %{instance}.
review_server_policies: Review server policies review_server_policies: Review server policies
statuses: 'Specifically, for:' statuses: 'Specifically, for:'

+ 4
- 4
config/locales/simple_form.en.yml View File

@ -90,10 +90,10 @@ en:
text: Custom warning text: Custom warning
type: Action type: Action
types: types:
disable: Disable login
none: Do nothing
silence: Silence
suspend: Suspend and irreversibly delete account data
disable: Freeze
none: Send a warning
silence: Limit
suspend: Suspend
warning_preset_id: Use a warning preset warning_preset_id: Use a warning preset
announcement: announcement:
all_day: All-day event all_day: All-day event

+ 2
- 2
config/routes.rb View File

@ -232,7 +232,7 @@ Rails.application.routes.draw do
resources :report_notes, only: [:create, :destroy] resources :report_notes, only: [:create, :destroy]
resources :accounts, only: [:index, :show] do
resources :accounts, only: [:index, :show, :destroy] do
member do member do
post :enable post :enable
post :unsilence post :unsilence
@ -466,7 +466,7 @@ Rails.application.routes.draw do
end end
namespace :admin do namespace :admin do
resources :accounts, only: [:index, :show] do
resources :accounts, only: [:index, :show, :destroy] do
member do member do
post :enable post :enable
post :unsilence post :unsilence

+ 8
- 0
db/migrate/20200908193330_create_account_deletion_requests.rb View File

@ -0,0 +1,8 @@
class CreateAccountDeletionRequests < ActiveRecord::Migration[5.2]
def change
create_table :account_deletion_requests do |t|
t.references :account, foreign_key: { on_delete: :cascade }
t.timestamps
end
end
end

+ 19
- 0
db/migrate/20200917192924_add_notify_to_follows.rb View File

@ -0,0 +1,19 @@
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
class AddNotifyToFollows < ActiveRecord::Migration[5.1]
include Mastodon::MigrationHelpers
disable_ddl_transaction!
def up
safety_assured do
add_column_with_default :follows, :notify, :boolean, default: false, allow_null: false
add_column_with_default :follow_requests, :notify, :boolean, default: false, allow_null: false
end
end
def down
remove_column :follows, :notify
remove_column :follow_requests, :notify
end
end

+ 5
- 0
db/migrate/20200917193034_add_type_to_notifications.rb View File

@ -0,0 +1,5 @@
class AddTypeToNotifications < ActiveRecord::Migration[5.2]
def change
add_column :notifications, :type, :string
end
end

+ 7
- 0
db/migrate/20200917222316_add_index_notifications_on_type.rb View File

@ -0,0 +1,7 @@
class AddIndexNotificationsOnType < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
add_index :notifications, [:account_id, :id, :type], order: { id: :desc }, algorithm: :concurrently
end
end

+ 22
- 0
db/post_migrate/20200917193528_migrate_notifications_type.rb View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class MigrateNotificationsType < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
TYPES_TO_MIGRATE = {
'Mention' => :mention,
'Status' => :reblog,
'Follow' => :follow,
'FollowRequest' => :follow_request,
'Favourite' => :favourite,
'Poll' => :poll,
}.freeze
def up
TYPES_TO_MIGRATE.each_pair do |activity_type, type|
Notification.where(activity_type: activity_type, type: nil).in_batches.update_all(type: type)
end
end
def down; end
end

+ 15
- 0
db/post_migrate/20200917222734_remove_index_notifications_on_account_activity.rb View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class RemoveIndexNotificationsOnAccountActivity < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
remove_index :notifications, name: :account_activity
remove_index :notifications, name: :index_notifications_on_account_id_and_id
end
def down
add_index :notifications, [:account_id, :activity_id, :activity_type], unique: true, name: 'account_activity', algorithm: :concurrently
add_index :notifications, [:account_id, :id], order: { id: :desc }, algorithm: :concurrently
end
end

+ 13
- 3
db/schema.rb View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_06_30_190544) do
ActiveRecord::Schema.define(version: 2020_09_17_222734) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -36,6 +36,13 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do
t.index ["conversation_id"], name: "index_account_conversations_on_conversation_id" t.index ["conversation_id"], name: "index_account_conversations_on_conversation_id"
end end
create_table "account_deletion_requests", force: :cascade do |t|
t.bigint "account_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_account_deletion_requests_on_account_id"
end
create_table "account_domain_blocks", force: :cascade do |t| create_table "account_domain_blocks", force: :cascade do |t|
t.string "domain" t.string "domain"
t.datetime "created_at", null: false t.datetime "created_at", null: false
@ -404,6 +411,7 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do
t.bigint "target_account_id", null: false t.bigint "target_account_id", null: false
t.boolean "show_reblogs", default: true, null: false t.boolean "show_reblogs", default: true, null: false
t.string "uri" t.string "uri"
t.boolean "notify", default: false, null: false
t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true
end end
@ -414,6 +422,7 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do
t.bigint "target_account_id", null: false t.bigint "target_account_id", null: false
t.boolean "show_reblogs", default: true, null: false t.boolean "show_reblogs", default: true, null: false
t.string "uri" t.string "uri"
t.boolean "notify", default: false, null: false
t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true
t.index ["target_account_id"], name: "index_follows_on_target_account_id" t.index ["target_account_id"], name: "index_follows_on_target_account_id"
end end
@ -538,8 +547,8 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.bigint "account_id", null: false t.bigint "account_id", null: false
t.bigint "from_account_id", null: false t.bigint "from_account_id", null: false
t.index ["account_id", "activity_id", "activity_type"], name: "account_activity", unique: true
t.index ["account_id", "id"], name: "index_notifications_on_account_id_and_id", order: { id: :desc }
t.string "type"
t.index ["account_id", "id", "type"], name: "index_notifications_on_account_id_and_id_and_type", order: { id: :desc }
t.index ["activity_id", "activity_type"], name: "index_notifications_on_activity_id_and_activity_type" t.index ["activity_id", "activity_type"], name: "index_notifications_on_activity_id_and_activity_type"
t.index ["from_account_id"], name: "index_notifications_on_from_account_id" t.index ["from_account_id"], name: "index_notifications_on_from_account_id"
end end
@ -950,6 +959,7 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do
add_foreign_key "account_aliases", "accounts", on_delete: :cascade add_foreign_key "account_aliases", "accounts", on_delete: :cascade
add_foreign_key "account_conversations", "accounts", on_delete: :cascade add_foreign_key "account_conversations", "accounts", on_delete: :cascade
add_foreign_key "account_conversations", "conversations", on_delete: :cascade add_foreign_key "account_conversations", "conversations", on_delete: :cascade
add_foreign_key "account_deletion_requests", "accounts", on_delete: :cascade
add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade
add_foreign_key "account_identity_proofs", "accounts", on_delete: :cascade add_foreign_key "account_identity_proofs", "accounts", on_delete: :cascade
add_foreign_key "account_migrations", "accounts", column: "target_account_id", on_delete: :nullify add_foreign_key "account_migrations", "accounts", column: "target_account_id", on_delete: :nullify

+ 2
- 2
lib/mastodon/accounts_cli.rb View File

@ -87,7 +87,7 @@ module Mastodon
say('Use --force to reattach it anyway and delete the other user') say('Use --force to reattach it anyway and delete the other user')
return return
elsif account.user.present? elsif account.user.present?
account.user.destroy!
DeleteAccountService.new.call(account, reserve_email: false)
end end
end end
@ -192,7 +192,7 @@ module Mastodon
end end
say("Deleting user with #{account.statuses_count} statuses, this might take a while...") say("Deleting user with #{account.statuses_count} statuses, this might take a while...")
SuspendAccountService.new.call(account, reserve_email: false)
DeleteAccountService.new.call(account, reserve_email: false)
say('OK', :green) say('OK', :green)
end end

+ 1
- 1
lib/mastodon/domains_cli.rb View File

@ -42,7 +42,7 @@ module Mastodon
end end
processed, = parallelize_with_progress(scope) do |account| processed, = parallelize_with_progress(scope) do |account|
SuspendAccountService.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 options[:dry_run]
end end
DomainBlock.where(domain: domains).destroy_all unless options[:dry_run] DomainBlock.where(domain: domains).destroy_all unless options[:dry_run]

+ 8
- 8
package.json View File

@ -64,17 +64,17 @@
"@babel/plugin-proposal-decorators": "^7.10.5", "@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-transform-react-inline-elements": "^7.10.4", "@babel/plugin-transform-react-inline-elements": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.11.5", "@babel/plugin-transform-runtime": "^7.11.5",
"@babel/preset-env": "^7.11.0",
"@babel/preset-env": "^7.11.5",
"@babel/preset-react": "^7.10.4", "@babel/preset-react": "^7.10.4",
"@babel/runtime": "^7.11.2", "@babel/runtime": "^7.11.2",
"@clusterws/cws": "^3.0.0", "@clusterws/cws": "^3.0.0",
"@gamestdio/websocket": "^0.3.2", "@gamestdio/websocket": "^0.3.2",
"@github/webauthn-json": "^0.4.2",
"@github/webauthn-json": "^0.5.4",
"@rails/ujs": "^6.0.3", "@rails/ujs": "^6.0.3",
"array-includes": "^3.1.1", "array-includes": "^3.1.1",
"arrow-key-navigation": "^1.2.0", "arrow-key-navigation": "^1.2.0",
"autoprefixer": "^9.8.6", "autoprefixer": "^9.8.6",
"axios": "^0.19.2",
"axios": "^0.20.0",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"babel-plugin-lodash": "^3.3.4", "babel-plugin-lodash": "^3.3.4",
"babel-plugin-preval": "^5.0.0", "babel-plugin-preval": "^5.0.0",
@ -85,16 +85,16 @@
"classnames": "^2.2.5", "classnames": "^2.2.5",
"compression-webpack-plugin": "^5.0.1", "compression-webpack-plugin": "^5.0.1",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"css-loader": "^4.2.2",
"css-loader": "^4.3.0",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"detect-passive-events": "^1.0.2",
"detect-passive-events": "^1.0.5",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"emoji-mart": "Gargron/emoji-mart#build", "emoji-mart": "Gargron/emoji-mart#build",
"es6-symbol": "^3.1.3", "es6-symbol": "^3.1.3",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"exif-js": "^2.3.0", "exif-js": "^2.3.0",
"express": "^4.17.1", "express": "^4.17.1",
"file-loader": "^6.0.0",
"file-loader": "^6.1.0",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"glob": "^7.1.6", "glob": "^7.1.6",
"history": "^4.10.1", "history": "^4.10.1",
@ -164,7 +164,7 @@
"throng": "^4.0.0", "throng": "^4.0.0",
"tiny-queue": "^0.2.1", "tiny-queue": "^0.2.1",
"uuid": "^8.2.0", "uuid": "^8.2.0",
"webpack": "^4.44.1",
"webpack": "^4.44.2",
"webpack-assets-manifest": "^3.1.1", "webpack-assets-manifest": "^3.1.1",
"webpack-bundle-analyzer": "^3.8.0", "webpack-bundle-analyzer": "^3.8.0",
"webpack-cli": "^3.3.12", "webpack-cli": "^3.3.12",
@ -187,7 +187,7 @@
"react-test-renderer": "^16.13.1", "react-test-renderer": "^16.13.1",
"sass-lint": "^1.13.1", "sass-lint": "^1.13.1",
"webpack-dev-server": "^3.11.0", "webpack-dev-server": "^3.11.0",
"yargs": "^15.4.1"
"yargs": "^16.0.3"
}, },
"resolutions": { "resolutions": {
"kind-of": "^6.0.3" "kind-of": "^6.0.3"

+ 2
- 18
spec/controllers/accounts_controller_spec.rb View File

@ -348,24 +348,8 @@ RSpec.describe AccountsController, type: :controller do
context 'in authorized fetch mode' do context 'in authorized fetch mode' do
let(:authorized_fetch_mode) { true } let(:authorized_fetch_mode) { true }
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns application/activity+json' do
expect(response.content_type).to eq 'application/activity+json'
end
it_behaves_like 'cachable response'
it 'returns Vary header with Signature' do
expect(response.headers['Vary']).to include 'Signature'
end
it 'renders bare minimum account' do
json = body_as_json
expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey)
expect(json).to_not include(:name, :summary)
it 'returns http unauthorized' do
expect(response).to have_http_status(401)
end end
end end
end end

+ 57
- 27
spec/controllers/api/v1/accounts_controller_spec.rb View File

@ -71,50 +71,80 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
let(:scopes) { 'write:follows' } let(:scopes) { 'write:follows' }
let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', locked: locked)).account } let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', locked: locked)).account }
before do
post :follow, params: { id: other_account.id }
end
context do
before do
post :follow, params: { id: other_account.id }
end
context 'with unlocked account' do
let(:locked) { false }
context 'with unlocked account' do
let(:locked) { false }
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns JSON with following=true and requested=false' do
json = body_as_json
it 'returns JSON with following=true and requested=false' do
json = body_as_json
expect(json[:following]).to be true
expect(json[:requested]).to be false
end
expect(json[:following]).to be true
expect(json[:requested]).to be false
end
it 'creates a following relation between user and target user' do
expect(user.account.following?(other_account)).to be true
end
it 'creates a following relation between user and target user' do
expect(user.account.following?(other_account)).to be true
it_behaves_like 'forbidden for wrong scope', 'read:accounts'
end end
it_behaves_like 'forbidden for wrong scope', 'read:accounts'
context 'with locked account' do
let(:locked) { true }
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns JSON with following=false and requested=true' do
json = body_as_json
expect(json[:following]).to be false
expect(json[:requested]).to be true
end
it 'creates a follow request relation between user and target user' do
expect(user.account.requested?(other_account)).to be true
end
it_behaves_like 'forbidden for wrong scope', 'read:accounts'
end
end end
context 'with locked account' do
let(:locked) { true }
context 'modifying follow options' do
let(:locked) { false }
it 'returns http success' do
expect(response).to have_http_status(200)
before do
user.account.follow!(other_account, reblogs: false, notify: false)
end end
it 'returns JSON with following=false and requested=true' do
it 'changes reblogs option' do
post :follow, params: { id: other_account.id, reblogs: true }
json = body_as_json json = body_as_json
expect(json[:following]).to be false
expect(json[:requested]).to be true
expect(json[:following]).to be true
expect(json[:showing_reblogs]).to be true
expect(json[:notifying]).to be false
end end
it 'creates a follow request relation between user and target user' do
expect(user.account.requested?(other_account)).to be true
end
it 'changes notify option' do
post :follow, params: { id: other_account.id, notify: true }
json = body_as_json
it_behaves_like 'forbidden for wrong scope', 'read:accounts'
expect(json[:following]).to be true
expect(json[:showing_reblogs]).to be false
expect(json[:notifying]).to be true
end
end end
end end

+ 2
- 1
spec/controllers/auth/registrations_controller_spec.rb View File

@ -199,9 +199,10 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
end end
subject do subject do
inviter = Fabricate(:user, confirmed_at: 2.days.ago)
Setting.registrations_mode = 'approved' Setting.registrations_mode = 'approved'
request.headers["Accept-Language"] = accept_language request.headers["Accept-Language"] = accept_language
invite = Fabricate(:invite, max_uses: nil, expires_at: 1.hour.from_now)
invite = Fabricate(:invite, user: inviter, max_uses: nil, expires_at: 1.hour.from_now)
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code, agreement: 'true' } } post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code, agreement: 'true' } }
end end

+ 1
- 0
spec/controllers/concerns/export_controller_concern_spec.rb View File

@ -5,6 +5,7 @@ require 'rails_helper'
describe ApplicationController, type: :controller do describe ApplicationController, type: :controller do
controller do controller do
include ExportControllerConcern include ExportControllerConcern
def index def index
send_export_file send_export_file
end end

+ 3
- 0
spec/fabricators/account_deletion_request_fabricator.rb View File

@ -0,0 +1,3 @@
Fabricator(:account_deletion_request) do
account
end

+ 4
- 0
spec/models/account_deletion_request_spec.rb View File

@ -0,0 +1,4 @@
require 'rails_helper'
RSpec.describe AccountDeletionRequest, type: :model do
end

+ 1
- 1
spec/models/concerns/account_interactions_spec.rb View File

@ -14,7 +14,7 @@ describe AccountInteractions do
context 'account with Follow' do context 'account with Follow' do
it 'returns { target_account_id => true }' do it 'returns { target_account_id => true }' do
Fabricate(:follow, account: account, target_account: target_account) Fabricate(:follow, account: account, target_account: target_account)
is_expected.to eq(target_account_id => { reblogs: true })
is_expected.to eq(target_account_id => { reblogs: true, notify: false })
end end
end end

+ 1
- 1
spec/models/follow_request_spec.rb View File

@ -7,7 +7,7 @@ RSpec.describe FollowRequest, type: :model do
let(:target_account) { Fabricate(:account) } let(:target_account) { Fabricate(:account) }
it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do
expect(account).to receive(:follow!).with(target_account, reblogs: true, uri: follow_request.uri)
expect(account).to receive(:follow!).with(target_account, reblogs: true, notify: false, uri: follow_request.uri)
expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id) expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id)
expect(follow_request).to receive(:destroy!) expect(follow_request).to receive(:destroy!)
follow_request.authorize! follow_request.authorize!

+ 1
- 1
spec/models/invite_spec.rb View File

@ -29,7 +29,7 @@ RSpec.describe Invite, type: :model do
it 'returns false when invite creator has been disabled' do it 'returns false when invite creator has been disabled' do
invite = Fabricate(:invite, max_uses: nil, expires_at: nil) invite = Fabricate(:invite, max_uses: nil, expires_at: nil)
SuspendAccountService.new.call(invite.user.account)
invite.user.account.suspend!
expect(invite.valid_for_use?).to be false expect(invite.valid_for_use?).to be false
end end
end end

+ 2
- 2
spec/models/webauthn_credentials_spec.rb View File

@ -69,8 +69,8 @@ RSpec.describe WebauthnCredential, type: :model do
expect(webauthn_credential).to model_have_error_on_field(:sign_count) expect(webauthn_credential).to model_have_error_on_field(:sign_count)
end end
it 'is invalid if sign_count is greater 2**32 - 1' do
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: 2**32)
it 'is invalid if sign_count is greater 2**63 - 1' do
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: 2**63)
webauthn_credential.valid? webauthn_credential.valid?

spec/services/suspend_account_service_spec.rb → spec/services/delete_account_service_spec.rb View File

@ -1,6 +1,6 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe SuspendAccountService, type: :service do
RSpec.describe DeleteAccountService, type: :service do
describe '#call on local account' do describe '#call on local account' do
before do before do
stub_request(:post, "https://alice.com/inbox").to_return(status: 201) stub_request(:post, "https://alice.com/inbox").to_return(status: 201)

+ 1
- 0
spec/services/import_service_spec.rb View File

@ -95,6 +95,7 @@ RSpec.describe ImportService, type: :service do
let(:import) { Import.create(account: account, type: 'following', data: csv) } let(:import) { Import.create(account: account, type: 'following', data: csv) }
it 'follows the listed accounts, including boosts' do it 'follows the listed accounts, including boosts' do
subject.call(import) subject.call(import)
expect(account.following.count).to eq 1 expect(account.following.count).to eq 1
expect(account.follow_requests.count).to eq 1 expect(account.follow_requests.count).to eq 1
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true

+ 5
- 1
spec/services/notify_service_spec.rb View File

@ -2,13 +2,14 @@ require 'rails_helper'
RSpec.describe NotifyService, type: :service do RSpec.describe NotifyService, type: :service do
subject do subject do
-> { described_class.new.call(recipient, activity) }
-> { described_class.new.call(recipient, type, activity) }
end end
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
let(:recipient) { user.account } let(:recipient) { user.account }
let(:sender) { Fabricate(:account, domain: 'example.com') } let(:sender) { Fabricate(:account, domain: 'example.com') }
let(:activity) { Fabricate(:follow, account: sender, target_account: recipient) } let(:activity) { Fabricate(:follow, account: sender, target_account: recipient) }
let(:type) { :follow }
it { is_expected.to change(Notification, :count).by(1) } it { is_expected.to change(Notification, :count).by(1) }
@ -50,6 +51,7 @@ RSpec.describe NotifyService, type: :service do
context 'for direct messages' do context 'for direct messages' do
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct)) } let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct)) }
let(:type) { :mention }
before do before do
user.settings.interactions = user.settings.interactions.merge('must_be_following_dm' => enabled) user.settings.interactions = user.settings.interactions.merge('must_be_following_dm' => enabled)
@ -93,6 +95,7 @@ RSpec.describe NotifyService, type: :service do
describe 'reblogs' do describe 'reblogs' do
let(:status) { Fabricate(:status, account: Fabricate(:account)) } let(:status) { Fabricate(:status, account: Fabricate(:account)) }
let(:activity) { Fabricate(:status, account: sender, reblog: status) } let(:activity) { Fabricate(:status, account: sender, reblog: status) }
let(:type) { :reblog }
it 'shows reblogs by default' do it 'shows reblogs by default' do
recipient.follow!(sender) recipient.follow!(sender)
@ -114,6 +117,7 @@ RSpec.describe NotifyService, type: :service do
let(:asshole) { Fabricate(:account, username: 'asshole') } let(:asshole) { Fabricate(:account, username: 'asshole') }
let(:reply_to) { Fabricate(:status, account: asshole) } let(:reply_to) { Fabricate(:status, account: asshole) }
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, thread: reply_to)) } let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, thread: reply_to)) }
let(:type) { :mention }
it 'does not notify when conversation is muted' do it 'does not notify when conversation is muted' do
recipient.mute_conversation!(activity.status.conversation) recipient.mute_conversation!(activity.status.conversation)

+ 2
- 2
spec/workers/refollow_worker_spec.rb View File

@ -23,8 +23,8 @@ describe RefollowWorker do
result = subject.perform(account.id) result = subject.perform(account.id)
expect(result).to be_nil expect(result).to be_nil
expect(service).to have_received(:call).with(alice, account, reblogs: true)
expect(service).to have_received(:call).with(bob, account, reblogs: false)
expect(service).to have_received(:call).with(alice, account, reblogs: true, notify: false)
expect(service).to have_received(:call).with(bob, account, reblogs: false, notify: false)
end end
end end
end end

+ 130
- 119
yarn.lock View File

@ -842,10 +842,10 @@
"@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-create-regexp-features-plugin" "^7.10.4"
"@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4"
"@babel/preset-env@^7.11.0":
version "7.11.0"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796"
integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg==
"@babel/preset-env@^7.11.5":
version "7.11.5"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.5.tgz#18cb4b9379e3e92ffea92c07471a99a2914e4272"
integrity sha512-kXqmW1jVcnB2cdueV+fyBM8estd5mlNfaQi6lwLgRwCby4edpavgbFhiBNjmWA3JpB/yZGSISa7Srf+TwxDQoA==
dependencies: dependencies:
"@babel/compat-data" "^7.11.0" "@babel/compat-data" "^7.11.0"
"@babel/helper-compilation-targets" "^7.10.4" "@babel/helper-compilation-targets" "^7.10.4"
@ -909,7 +909,7 @@
"@babel/plugin-transform-unicode-escapes" "^7.10.4" "@babel/plugin-transform-unicode-escapes" "^7.10.4"
"@babel/plugin-transform-unicode-regex" "^7.10.4" "@babel/plugin-transform-unicode-regex" "^7.10.4"
"@babel/preset-modules" "^0.1.3" "@babel/preset-modules" "^0.1.3"
"@babel/types" "^7.11.0"
"@babel/types" "^7.11.5"
browserslist "^4.12.0" browserslist "^4.12.0"
core-js-compat "^3.6.2" core-js-compat "^3.6.2"
invariant "^2.2.2" invariant "^2.2.2"
@ -1112,10 +1112,10 @@
resolved "https://registry.yarnpkg.com/@gamestdio/websocket/-/websocket-0.3.2.tgz#321ba0976ee30fd14e51dbf8faa85ce7b325f76a" resolved "https://registry.yarnpkg.com/@gamestdio/websocket/-/websocket-0.3.2.tgz#321ba0976ee30fd14e51dbf8faa85ce7b325f76a"
integrity sha512-J3n5SKim+ZoLbe44hRGI/VYAwSMCeIJuBy+FfP6EZaujEpNchPRFcIsVQLWAwpU1bP2Ji63rC+rEUOd1vjUB6Q== integrity sha512-J3n5SKim+ZoLbe44hRGI/VYAwSMCeIJuBy+FfP6EZaujEpNchPRFcIsVQLWAwpU1bP2Ji63rC+rEUOd1vjUB6Q==
"@github/webauthn-json@^0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@github/webauthn-json/-/webauthn-json-0.4.2.tgz#573bba7f30f035d82a6b6040430eb4e9729db849"
integrity sha512-10RwfEzpg0y68Coj480tYE4KajRO39ii0652bWbKM0OfXlV9szw6N5vMwnNzelvAMigcEDiAtFcvFCvB8GnTtA==
"@github/webauthn-json@^0.5.4":
version "0.5.4"
resolved "https://registry.yarnpkg.com/@github/webauthn-json/-/webauthn-json-0.5.4.tgz#fe4f47647c2b29e42e33f6f30484fec7e5adfeb1"
integrity sha512-lZi5cSZi2F08a2kmjxr/FU13ILenpxkZJUWo1p3hHAmMHLLr5GAVlkCJmvWeIsIiAp65AHh3dQIZSMPIylcClw==
"@istanbuljs/load-nyc-config@^1.0.0": "@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0" version "1.1.0"
@ -1482,9 +1482,9 @@
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/node@*": "@types/node@*":
version "14.6.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.0.tgz#7d4411bf5157339337d7cff864d9ff45f177b499"
integrity sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA==
version "14.11.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.1.tgz#56af902ad157e763f9ba63d671c39cda3193c835"
integrity sha512-oTQgnd0hblfLsJ6BvJzzSL+Inogp3lq9fGgqRkMB/ziKMgEUaFl801OncOzUmalfzt14N0oPHMK47ipl+wbTIw==
"@types/normalize-package-data@^2.4.0": "@types/normalize-package-data@^2.4.0":
version "2.4.0" version "2.4.0"
@ -1691,9 +1691,9 @@
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
abab@^2.0.3: abab@^2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.4.tgz#6dfa57b417ca06d21b2478f0e638302f99c2405c"
integrity sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==
version "2.0.5"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
version "1.3.7" version "1.3.7"
@ -1766,12 +1766,7 @@ ajv-keywords@^1.0.0:
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
integrity sha1-MU3QpLM2j609/NxU7eYXG4htrzw= integrity sha1-MU3QpLM2j609/NxU7eYXG4htrzw=
ajv-keywords@^3.1.0, ajv-keywords@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da"
integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==
ajv-keywords@^3.5.2:
ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2:
version "3.5.2" version "3.5.2"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
@ -2083,12 +2078,12 @@ axe-core@^3.5.4:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227"
integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q==
axios@^0.19.2:
version "0.19.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
axios@^0.20.0:
version "0.20.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.20.0.tgz#057ba30f04884694993a8cd07fa394cff11c50bd"
integrity sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA==
dependencies: dependencies:
follow-redirects "1.5.10"
follow-redirects "^1.10.0"
axobject-query@^2.1.2: axobject-query@^2.1.2:
version "2.2.0" version "2.2.0"
@ -2677,9 +2672,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0" lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001124: caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001124:
version "1.0.30001124"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001124.tgz#5d9998190258e11630d674fc50ea8e579ae0ced2"
integrity sha512-zQW8V3CdND7GHRH6rxm6s59Ww4g/qGWTheoboW9nfeMg7sUoopIfKCcNZUjwYRCOrvereh3kwDpZj4VLQ7zGtA==
version "1.0.30001133"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001133.tgz#ec564c5495311299eb05245e252d589a84acd95e"
integrity sha512-s3XAUFaC/ntDb1O3lcw9K8MPeOW7KO3z9+GzAoBxfz1B0VdacXPMKgFUtG4KIsgmnbexmi013s9miVu4h+qMHw==
capture-exit@^2.0.0: capture-exit@^2.0.0:
version "2.0.0" version "2.0.0"
@ -2873,6 +2868,15 @@ cliui@^6.0.0:
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi "^6.2.0" wrap-ansi "^6.2.0"
cliui@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.1.tgz#a4cb67aad45cd83d8d05128fc9f4d8fbb887e6b3"
integrity sha512-rcvHOWyGyid6I1WjT/3NatKj2kDt9OdSHSXpyLXaMWFbKpGACNW8pRhhdPUq9MWUOdwn8Rz9AVETjF4105rZZQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
clone@^2.1.1: clone@^2.1.1:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
@ -3258,10 +3262,10 @@ css-list-helpers@^1.0.1:
dependencies: dependencies:
tcomb "^2.5.0" tcomb "^2.5.0"
css-loader@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.2.2.tgz#b668b3488d566dc22ebcf9425c5f254a05808c89"
integrity sha512-omVGsTkZPVwVRpckeUnLshPp12KsmMSLqYxs12+RzM9jRR5Y+Idn/tBffjXRvOE+qW7if24cuceFJqYR5FmGBg==
css-loader@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.3.0.tgz#c888af64b2a5b2e85462c72c0f4a85c7e2e0821e"
integrity sha512-rdezjCjScIrsL8BSYszgT4s476IcNKt6yX69t0pHjJVnPUTDpn4WfIpDQTN3wCJvUvfsz/mFjuGOekf3PY3NUg==
dependencies: dependencies:
camelcase "^6.0.0" camelcase "^6.0.0"
cssesc "^3.0.0" cssesc "^3.0.0"
@ -3273,7 +3277,7 @@ css-loader@^4.2.2:
postcss-modules-scope "^2.2.0" postcss-modules-scope "^2.2.0"
postcss-modules-values "^3.0.0" postcss-modules-values "^3.0.0"
postcss-value-parser "^4.1.0" postcss-value-parser "^4.1.0"
schema-utils "^2.7.0"
schema-utils "^2.7.1"
semver "^7.3.2" semver "^7.3.2"
css-select-base-adapter@^0.1.1: css-select-base-adapter@^0.1.1:
@ -3484,14 +3488,7 @@ debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
dependencies: dependencies:
ms "2.0.0" ms "2.0.0"
debug@=3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debug@^3.0.0, debug@^3.1.1, debug@^3.2.5:
debug@^3.1.1, debug@^3.2.5:
version "3.2.6" version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
@ -3555,7 +3552,7 @@ default-gateway@^4.2.0:
execa "^1.0.0" execa "^1.0.0"
ip-regex "^2.1.0" ip-regex "^2.1.0"
define-properties@^1.1.2, define-properties@^1.1.3:
define-properties@^1.1.3:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
@ -3645,10 +3642,10 @@ detect-node@^2.0.4:
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
detect-passive-events@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-1.0.4.tgz#6ed477e6e5bceb79079735dcd357789d37f9a91a"
integrity sha1-btR35uW863kHlzXc01d4nTf5qRo=
detect-passive-events@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-1.0.5.tgz#ce324db665123bef9e368b8059ff95d95217cc05"
integrity sha512-foW7Q35wwOCxVzW0xLf5XeB5Fhe7oyRgvkBYdiP9IWgLMzjqUqTvsJv9ymuEWGjY6AoDXD3OC294+Z9iuOw0QA==
diff-sequences@^25.2.6: diff-sequences@^25.2.6:
version "25.2.6" version "25.2.6"
@ -3777,9 +3774,9 @@ domutils@^1.5.1, domutils@^1.7.0:
domelementtype "1" domelementtype "1"
dot-prop@^5.2.0: dot-prop@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb"
integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==
version "5.3.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==
dependencies: dependencies:
is-obj "^2.0.0" is-obj "^2.0.0"
@ -3822,9 +3819,9 @@ ejs@^2.3.4, ejs@^2.6.1:
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
electron-to-chromium@^1.3.562: electron-to-chromium@^1.3.562:
version "1.3.562"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.562.tgz#79c20277ee1c8d0173a22af00e38433b752bc70f"
integrity sha512-WhRe6liQ2q/w1MZc8mD8INkenHivuHdrr4r5EQHNomy3NJux+incP6M6lDMd0paShP3MD0WGe5R1TWmEClf+Bg==
version "1.3.567"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.567.tgz#7a404288952ac990e447a7a86470d460ea953b8f"
integrity sha512-1aKkw0Hha1Bw9JA5K5PT5eFXC/TXbkJvUfNSNEciPUMgSIsRJZM1hF2GUEAGZpAbgvd8En21EA+Lv820KOhvqA==
elliptic@^6.5.3: elliptic@^6.5.3:
version "6.5.3" version "6.5.3"
@ -4044,9 +4041,9 @@ es6-weak-map@^2.0.1:
es6-symbol "^3.1.1" es6-symbol "^3.1.1"
escalade@^3.0.2: escalade@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4"
integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==
version "3.1.0"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e"
integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig==
escape-html@^1.0.3, escape-html@~1.0.3: escape-html@^1.0.3, escape-html@~1.0.3:
version "1.0.3" version "1.0.3"
@ -4588,13 +4585,13 @@ file-entry-cache@^5.0.1:
dependencies: dependencies:
flat-cache "^2.0.1" flat-cache "^2.0.1"
file-loader@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.0.0.tgz#97bbfaab7a2460c07bcbd72d3a6922407f67649f"
integrity sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ==
file-loader@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.1.0.tgz#65b9fcfb0ea7f65a234a1f10cdd7f1ab9a33f253"
integrity sha512-26qPdHyTsArQ6gU4P1HJbAbnFTyT2r0pG7czh1GFAd9TZbj0n94wWbupgixZH/ET/meqi2/5+F7DhW4OAXD+Lg==
dependencies: dependencies:
loader-utils "^2.0.0" loader-utils "^2.0.0"
schema-utils "^2.6.5"
schema-utils "^2.7.1"
file-type@^12.4.1: file-type@^12.4.1:
version "12.4.2" version "12.4.2"
@ -4728,19 +4725,10 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3" inherits "^2.0.3"
readable-stream "^2.3.6" readable-stream "^2.3.6"
follow-redirects@1.5.10:
version "1.5.10"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
dependencies:
debug "=3.1.0"
follow-redirects@^1.0.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.11.0.tgz#afa14f08ba12a52963140fe43212658897bc0ecb"
integrity sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA==
dependencies:
debug "^3.0.0"
follow-redirects@^1.0.0, follow-redirects@^1.10.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
font-awesome@^4.7.0: font-awesome@^4.7.0:
version "4.7.0" version "4.7.0"
@ -4899,7 +4887,7 @@ gensync@^1.0.0-beta.1:
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
get-caller-file@^2.0.1:
get-caller-file@^2.0.1, get-caller-file@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
@ -5100,7 +5088,7 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-symbols@^1.0.0, has-symbols@^1.0.1:
has-symbols@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
@ -5681,9 +5669,9 @@ is-binary-path@~2.1.0:
binary-extensions "^2.0.0" binary-extensions "^2.0.0"
is-callable@^1.1.4, is-callable@^1.2.0: is-callable@^1.1.4, is-callable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb"
integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==
version "1.2.1"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.1.tgz#4d1e21a4f437509d25ce55f8184350771421c96d"
integrity sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==
is-ci@^2.0.0: is-ci@^2.0.0:
version "2.0.0" version "2.0.0"
@ -6410,16 +6398,7 @@ jest-watcher@^26.3.0:
jest-util "^26.3.0" jest-util "^26.3.0"
string-length "^4.0.1" string-length "^4.0.1"
jest-worker@^26.0.0:
version "26.2.1"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.2.1.tgz#5d630ab93f666b53f911615bc13e662b382bd513"
integrity sha512-+XcGMMJDTeEGncRb5M5Zq9P7K4sQ1sirhjdOxsN1462h6lFo9w59bl2LVQmdGEEeU3m+maZCkS2Tcc9SfCHO4A==
dependencies:
"@types/node" "*"
merge-stream "^2.0.0"
supports-color "^7.0.0"
jest-worker@^26.3.0:
jest-worker@^26.0.0, jest-worker@^26.3.0:
version "26.3.0" version "26.3.0"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f"
integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw== integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw==
@ -7224,10 +7203,10 @@ node-fetch@^2.6.0:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-forge@0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
node-forge@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==
node-int64@^0.4.0: node-int64@^0.4.0:
version "0.4.0" version "0.4.0"
@ -7281,9 +7260,9 @@ node-notifier@^8.0.0:
which "^2.0.2" which "^2.0.2"
node-releases@^1.1.60: node-releases@^1.1.60:
version "1.1.60"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084"
integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==
version "1.1.61"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e"
integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g==
normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
version "2.5.0" version "2.5.0"
@ -7415,7 +7394,7 @@ object-is@^1.0.1:
define-properties "^1.1.3" define-properties "^1.1.3"
es-abstract "^1.17.5" es-abstract "^1.17.5"
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
@ -7428,14 +7407,14 @@ object-visit@^1.0.0:
isobject "^3.0.0" isobject "^3.0.0"
object.assign@^4.1.0: object.assign@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
version "4.1.1"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd"
integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==
dependencies: dependencies:
define-properties "^1.1.2"
function-bind "^1.1.1"
has-symbols "^1.0.0"
object-keys "^1.0.11"
define-properties "^1.1.3"
es-abstract "^1.18.0-next.0"
has-symbols "^1.0.1"
object-keys "^1.1.1"
object.entries@^1.1.2: object.entries@^1.1.2:
version "1.1.2" version "1.1.2"
@ -9496,11 +9475,11 @@ select-hose@^2.0.0:
integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
selfsigned@^1.10.7: selfsigned@^1.10.7:
version "1.10.7"
resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b"
integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==
version "1.10.8"
resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30"
integrity sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==
dependencies: dependencies:
node-forge "0.9.0"
node-forge "^0.10.0"
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: "semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
version "5.7.1" version "5.7.1"
@ -9844,9 +9823,9 @@ spdx-expression-parse@^3.0.0:
spdx-license-ids "^3.0.0" spdx-license-ids "^3.0.0"
spdx-license-ids@^3.0.0: spdx-license-ids@^3.0.0:
version "3.0.5"
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
version "3.0.6"
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce"
integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==
spdy-transport@^3.0.0: spdy-transport@^3.0.0:
version "3.0.0" version "3.0.0"
@ -10218,9 +10197,9 @@ supports-color@^6.1.0:
has-flag "^3.0.0" has-flag "^3.0.0"
supports-color@^7.0.0, supports-color@^7.1.0: supports-color@^7.0.0, supports-color@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies: dependencies:
has-flag "^4.0.0" has-flag "^4.0.0"
@ -11070,10 +11049,10 @@ webpack-sources@^1.0.0, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-
source-list-map "^2.0.0" source-list-map "^2.0.0"
source-map "~0.6.1" source-map "~0.6.1"
webpack@^4.44.1:
version "4.44.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21"
integrity sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ==
webpack@^4.44.2:
version "4.44.2"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72"
integrity sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q==
dependencies: dependencies:
"@webassemblyjs/ast" "1.9.0" "@webassemblyjs/ast" "1.9.0"
"@webassemblyjs/helper-module-context" "1.9.0" "@webassemblyjs/helper-module-context" "1.9.0"
@ -11202,6 +11181,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrappy@1: wrappy@1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@ -11263,6 +11251,11 @@ y18n@^4.0.0:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
y18n@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.1.tgz#1ad2a7eddfa8bce7caa2e1f6b5da96c39d99d571"
integrity sha512-/jJ831jEs4vGDbYPQp4yGKDYPSCCEQ45uZWJHE1AoYBzqdZi8+LDWas0z4HrmJXmKdpFsTiowSHXdxyFhpmdMg==
yallist@^3.0.2: yallist@^3.0.2:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
@ -11294,6 +11287,11 @@ yargs-parser@^18.1.2:
camelcase "^5.0.0" camelcase "^5.0.0"
decamelize "^1.2.0" decamelize "^1.2.0"
yargs-parser@^20.0.0:
version "20.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.0.0.tgz#c65a1daaa977ad63cebdd52159147b789a4e19a9"
integrity sha512-8eblPHTL7ZWRkyjIZJjnGf+TijiKJSwA24svzLRVvtgoi/RZiKa9fFQTrlx0OKLnyHSdt/enrdadji6WFfESVA==
yargs@^13.3.2: yargs@^13.3.2:
version "13.3.2" version "13.3.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
@ -11310,7 +11308,7 @@ yargs@^13.3.2:
y18n "^4.0.0" y18n "^4.0.0"
yargs-parser "^13.1.2" yargs-parser "^13.1.2"
yargs@^15.3.1, yargs@^15.4.1:
yargs@^15.3.1:
version "15.4.1" version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
@ -11327,6 +11325,19 @@ yargs@^15.3.1, yargs@^15.4.1:
y18n "^4.0.0" y18n "^4.0.0"
yargs-parser "^18.1.2" yargs-parser "^18.1.2"
yargs@^16.0.3:
version "16.0.3"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.0.3.tgz#7a919b9e43c90f80d4a142a89795e85399a7e54c"
integrity sha512-6+nLw8xa9uK1BOEOykaiYAJVh6/CjxWXK/q9b5FpRgNslt8s22F2xMBqVIKgCRjNgGvGPBy8Vog7WN7yh4amtA==
dependencies:
cliui "^7.0.0"
escalade "^3.0.2"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.0"
y18n "^5.0.1"
yargs-parser "^20.0.0"
zlibjs@^0.3.1: zlibjs@^0.3.1:
version "0.3.1" version "0.3.1"
resolved "https://registry.yarnpkg.com/zlibjs/-/zlibjs-0.3.1.tgz#50197edb28a1c42ca659cc8b4e6a9ddd6d444554" resolved "https://registry.yarnpkg.com/zlibjs/-/zlibjs-0.3.1.tgz#50197edb28a1c42ca659cc8b4e6a9ddd6d444554"

Loading…
Cancel
Save