Merge Upstream

master
Tuan 3 years ago
commit ef9cb2468d
  1. 42
      .github/ISSUE_TEMPLATE/1.bug_report.yml
  2. 21
      .github/ISSUE_TEMPLATE/2.feature_request.yml
  3. 2
      .github/ISSUE_TEMPLATE/3.support.md
  4. 12
      .github/ISSUE_TEMPLATE/bug_report.md
  5. 16
      .github/ISSUE_TEMPLATE/feature_request.md
  6. 2
      .ruby-version
  7. 2
      Dockerfile
  8. 12
      Gemfile
  9. 138
      Gemfile.lock
  10. 10
      app/controllers/activitypub/outboxes_controller.rb
  11. 4
      app/controllers/admin/resets_controller.rb
  12. 27
      app/controllers/admin/sign_in_token_authentications_controller.rb
  13. 2
      app/controllers/admin/two_factor_authentications_controller.rb
  14. 9
      app/controllers/auth/omniauth_callbacks_controller.rb
  15. 44
      app/controllers/auth/sessions_controller.rb
  16. 5
      app/controllers/concerns/sign_in_token_authentication_concern.rb
  17. 10
      app/controllers/concerns/two_factor_authentication_concern.rb
  18. 2
      app/controllers/follower_accounts_controller.rb
  19. 2
      app/controllers/following_accounts_controller.rb
  20. 13
      app/controllers/settings/login_activities_controller.rb
  21. 3
      app/controllers/well_known/webfinger_controller.rb
  22. 6
      app/helpers/accounts_helper.rb
  23. 11
      app/helpers/application_helper.rb
  24. 21
      app/javascript/flavours/glitch/actions/picture_in_picture.js
  25. 4
      app/javascript/flavours/glitch/components/status_content.js
  26. 16
      app/javascript/flavours/glitch/features/compose/components/search_results.js
  27. 7
      app/javascript/flavours/glitch/features/ui/components/link_footer.js
  28. 3
      app/javascript/flavours/glitch/reducers/picture_in_picture.js
  29. 12
      app/javascript/flavours/glitch/styles/components/boost.scss
  30. 78
      app/javascript/flavours/glitch/styles/components/drawer.scss
  31. 7
      app/javascript/flavours/glitch/styles/components/media.scss
  32. 11
      app/javascript/flavours/glitch/styles/components/search.scss
  33. 1
      app/javascript/flavours/glitch/styles/components/status.scss
  34. 18
      app/javascript/flavours/glitch/styles/forms.scss
  35. 1
      app/javascript/flavours/glitch/util/initial_state.js
  36. 21
      app/javascript/mastodon/actions/picture_in_picture.js
  37. 7
      app/javascript/mastodon/features/ui/components/link_footer.js
  38. 1
      app/javascript/mastodon/initial_state.js
  39. 3
      app/javascript/mastodon/reducers/picture_in_picture.js
  40. 4
      app/javascript/skins/glitch/metulayf/variables.scss
  41. 12
      app/javascript/styles/mastodon/boost.scss
  42. 8
      app/javascript/styles/mastodon/components.scss
  43. 18
      app/javascript/styles/mastodon/forms.scss
  44. 6
      app/models/account.rb
  45. 1
      app/models/account_stat.rb
  46. 2
      app/models/concerns/ldap_authenticable.rb
  47. 35
      app/models/login_activity.rb
  48. 14
      app/models/report_filter.rb
  49. 25
      app/models/user.rb
  50. 8
      app/policies/user_policy.rb
  51. 1
      app/serializers/initial_state_serializer.rb
  52. 29
      app/serializers/rest/instance_serializer.rb
  53. 3
      app/validators/status_length_validator.rb
  54. 4
      app/views/about/more.html.haml
  55. 4
      app/views/about/show.html.haml
  56. 10
      app/views/accounts/_header.html.haml
  57. 2
      app/views/accounts/show.html.haml
  58. 24
      app/views/admin/accounts/show.html.haml
  59. 16
      app/views/admin/dashboard/index.html.haml
  60. 4
      app/views/admin/follow_recommendations/_account.html.haml
  61. 2
      app/views/admin/instances/_instance.html.haml
  62. 6
      app/views/admin/reports/index.html.haml
  63. 2
      app/views/admin/tags/_tag.html.haml
  64. 5
      app/views/auth/registrations/_sessions.html.haml
  65. 4
      app/views/directories/index.html.haml
  66. 3
      app/views/layouts/public.html.haml
  67. 4
      app/views/relationships/_account.html.haml
  68. 2
      app/views/settings/featured_tags/index.html.haml
  69. 17
      app/views/settings/login_activities/_login_activity.html.haml
  70. 15
      app/views/settings/login_activities/index.html.haml
  71. 6
      app/views/statuses/_detailed_status.html.haml
  72. 16
      app/workers/move_worker.rb
  73. 1
      app/workers/scheduler/ip_cleanup_scheduler.rb
  74. 57
      config/locales/en.yml
  75. 2
      config/locales/tr.yml
  76. 2
      config/navigation.rb
  77. 4
      config/routes.rb
  78. 2
      config/webpack/production.js
  79. 2
      db/migrate/20210416200740_create_canonical_email_blocks.rb
  80. 14
      db/migrate/20210609202149_create_login_activities.rb
  81. 5
      db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb
  82. 13
      db/migrate/20210630000137_fix_canonical_email_blocks_foreign_key.rb
  83. 18
      db/schema.rb
  84. 1
      dist/mastodon-sidekiq.service
  85. 1
      dist/mastodon-web.service
  86. 2
      docker-compose.yml
  87. 15
      lib/mastodon/accounts_cli.rb
  88. 13
      lib/mastodon/domains_cli.rb
  89. 26
      package.json
  90. 16
      spec/controllers/activitypub/outboxes_controller_spec.rb
  91. 2
      spec/controllers/admin/resets_controller_spec.rb
  92. 8
      spec/controllers/admin/two_factor_authentications_controller_spec.rb
  93. 19
      spec/controllers/follower_accounts_controller_spec.rb
  94. 19
      spec/controllers/following_accounts_controller_spec.rb
  95. 4
      spec/controllers/well_known/webfinger_controller_spec.rb
  96. 8
      spec/fabricators/login_activity_fabricator.rb
  97. 5
      spec/models/login_activity_spec.rb
  98. 2
      spec/models/tag_feed_spec.rb
  99. 28
      spec/models/user_spec.rb
  100. 33
      spec/services/bootstrap_timeline_service_spec.rb
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,42 @@
name: Bug Report
description: If something isn't working as expected
labels: bug
body:
- type: markdown
attributes:
value: |
Make sure that you are submitting a new bug that was not previously reported or already fixed.
Please use a concise and distinct title for the issue.
- type: input
attributes:
label: Expected behaviour
description: What should have happened?
validations:
required: true
- type: input
attributes:
label: Actual behaviour
description: What happened?
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce the problem
description: What were you trying to do?
value: |
1.
2.
3.
...
validations:
required: true
- type: textarea
attributes:
label: Specifications
description: |
What version or commit hash of Mastodon did you find this bug in?
If a front-end issue, what browser and operating systems were you using?
validations:
required: true

@ -0,0 +1,21 @@
name: Feature Request
description: I have a suggestion
body:
- type: markdown
attributes:
value: |
Please use a concise and distinct title for the issue.
Consider: Could it be implemented as a 3rd party app using the REST API instead?
- type: textarea
attributes:
label: Pitch
description: Describe your idea for a feature. Make sure it has not already been suggested/implemented/turned down before.
validations:
required: true
- type: textarea
attributes:
label: Motivation
description: Why do you think this feature is needed? Who would benefit from it?
validations:
required: true

@ -1,7 +1,7 @@
--- ---
name: Support name: Support
about: Ask for help with your deployment about: Ask for help with your deployment
title: DO NOT CREATE THIS ISSUE
--- ---
We primarily use GitHub as a bug and feature tracker. For usage questions, troubleshooting of deployments and other individual technical assistance, please use one of the resources below: We primarily use GitHub as a bug and feature tracker. For usage questions, troubleshooting of deployments and other individual technical assistance, please use one of the resources below:

@ -1,12 +0,0 @@
---
name: Bug Report
about: If something isn't working as expected
labels: bug
---
[Issue text goes here].
* * * *
- [ ] I searched or browsed the repo’s other issues to ensure this is not a duplicate.
- [ ] This bugs also occur on vanilla Mastodon

@ -1,16 +0,0 @@
---
name: Feature Request
about: I have a suggestion
---
<!-- Please use a concise and distinct title for the issue -->
<!-- Consider: Could it be implemented as a 3rd party app using the REST API instead? -->
### Pitch
<!-- Describe your idea for a feature. Make sure it has not already been suggested/implemented/turned down before -->
### Motivation
<!-- Why do you think this feature is needed? Who would benefit from it? -->

@ -1 +1 @@
2.7.2 2.7.4

@ -26,7 +26,7 @@ RUN ARCH= && \
mv node-v$NODE_VER-linux-$ARCH /opt/node mv node-v$NODE_VER-linux-$ARCH /opt/node
# Install Ruby # Install Ruby
ENV RUBY_VER="2.7.2" ENV RUBY_VER="2.7.4"
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential \ apt-get install -y --no-install-recommends build-essential \
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \ bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \

@ -6,7 +6,7 @@ ruby '>= 2.5.0', '< 3.1.0'
gem 'pkg-config', '~> 1.4' gem 'pkg-config', '~> 1.4'
gem 'puma', '~> 5.3' gem 'puma', '~> 5.3'
gem 'rails', '~> 6.1.3' gem 'rails', '~> 6.1.4'
gem 'sprockets', '~> 3.7.2' gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.1' gem 'thor', '~> 1.1'
gem 'rack', '~> 2.2.3' gem 'rack', '~> 2.2.3'
@ -24,7 +24,7 @@ gem 'paperclip', '~> 6.0'
gem 'blurhash', '~> 0.1' gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.7' gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.6.0', require: false gem 'bootsnap', '~> 1.6.0', require: false
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.7' gem 'charlock_holmes', '~> 0.7.7'
@ -81,7 +81,7 @@ gem 'sanitize', '~> 5.2'
gem 'scenic', '~> 1.5' gem 'scenic', '~> 1.5'
gem 'sidekiq', '~> 6.2' gem 'sidekiq', '~> 6.2'
gem 'sidekiq-scheduler', '~> 3.1' gem 'sidekiq-scheduler', '~> 3.1'
gem 'sidekiq-unique-jobs', '~> 7.0' gem 'sidekiq-unique-jobs', '~> 7.1'
gem 'sidekiq-bulk', '~>0.2.0' gem 'sidekiq-bulk', '~>0.2.0'
gem 'simple-navigation', '~> 4.3' gem 'simple-navigation', '~> 4.3'
gem 'simple_form', '~> 5.1' gem 'simple_form', '~> 5.1'
@ -136,8 +136,8 @@ group :development do
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', '~> 1.16', require: false gem 'rubocop', '~> 1.18', require: false
gem 'rubocop-rails', '~> 2.10', require: false gem 'rubocop-rails', '~> 2.11', require: false
gem 'brakeman', '~> 5.0', require: false gem 'brakeman', '~> 5.0', require: false
gem 'bundler-audit', '~> 0.8', require: false gem 'bundler-audit', '~> 0.8', require: false
@ -157,5 +157,3 @@ gem 'concurrent-ruby', require: false
gem 'connection_pool', require: false gem 'connection_pool', require: false
gem 'xorcist', '~> 1.1' gem 'xorcist', '~> 1.1'
gem 'resolv', '~> 0.1.0'

@ -1,40 +1,40 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (6.1.3.2) actioncable (6.1.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.1.3.2) actionmailbox (6.1.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4)
activejob (= 6.1.3.2) activejob (= 6.1.4)
activerecord (= 6.1.3.2) activerecord (= 6.1.4)
activestorage (= 6.1.3.2) activestorage (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.1.3.2) actionmailer (6.1.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4)
actionview (= 6.1.3.2) actionview (= 6.1.4)
activejob (= 6.1.3.2) activejob (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.1.3.2) actionpack (6.1.4)
actionview (= 6.1.3.2) actionview (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
rack (~> 2.0, >= 2.0.9) rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.3.2) actiontext (6.1.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4)
activerecord (= 6.1.3.2) activerecord (= 6.1.4)
activestorage (= 6.1.3.2) activestorage (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.1.3.2) actionview (6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -45,28 +45,28 @@ GEM
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.8) active_record_query_trace (1.8)
activejob (6.1.3.2) activejob (6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.1.3.2) activemodel (6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
activerecord (6.1.3.2) activerecord (6.1.4)
activemodel (= 6.1.3.2) activemodel (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
activestorage (6.1.3.2) activestorage (6.1.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4)
activejob (= 6.1.3.2) activejob (= 6.1.4)
activerecord (= 6.1.3.2) activerecord (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
marcel (~> 1.0.0) marcel (~> 1.0.0)
mini_mime (~> 1.0.2) mini_mime (>= 1.1.0)
activesupport (6.1.3.2) activesupport (6.1.4)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
tzinfo (~> 2.0) tzinfo (~> 2.0)
zeitwerk (~> 2.3) zeitwerk (~> 2.3)
addressable (2.7.0) addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
airbrussh (1.4.0) airbrussh (1.4.0)
sshkit (>= 1.6.1, != 1.7.0) sshkit (>= 1.6.1, != 1.7.0)
@ -99,7 +99,7 @@ GEM
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
bindata (2.4.8) bindata (2.4.10)
binding_of_caller (1.0.0) binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
blurhash (0.1.5) blurhash (0.1.5)
@ -353,7 +353,7 @@ GEM
mimemagic (0.3.10) mimemagic (0.3.10)
nokogiri (~> 1) nokogiri (~> 1)
rake rake
mini_mime (1.0.3) mini_mime (1.1.0)
mini_portile2 (2.5.3) mini_portile2 (2.5.3)
minitest (5.14.4) minitest (5.14.4)
msgpack (1.4.2) msgpack (1.4.2)
@ -374,7 +374,7 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5) sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0) statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.11.5) oj (3.11.8)
omniauth (1.9.1) omniauth (1.9.1)
hashie (>= 3.4.6) hashie (>= 3.4.6)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
@ -443,20 +443,20 @@ GEM
rack rack
rack-test (1.1.0) rack-test (1.1.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rails (6.1.3.2) rails (6.1.4)
actioncable (= 6.1.3.2) actioncable (= 6.1.4)
actionmailbox (= 6.1.3.2) actionmailbox (= 6.1.4)
actionmailer (= 6.1.3.2) actionmailer (= 6.1.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4)
actiontext (= 6.1.3.2) actiontext (= 6.1.4)
actionview (= 6.1.3.2) actionview (= 6.1.4)
activejob (= 6.1.3.2) activejob (= 6.1.4)
activemodel (= 6.1.3.2) activemodel (= 6.1.4)
activerecord (= 6.1.3.2) activerecord (= 6.1.4)
activestorage (= 6.1.3.2) activestorage (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 6.1.3.2) railties (= 6.1.4)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
@ -472,11 +472,11 @@ GEM
railties (>= 6.0.0, < 7) railties (>= 6.0.0, < 7)
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
rails (>= 4.2.0) rails (>= 4.2.0)
railties (6.1.3.2) railties (6.1.4)
actionpack (= 6.1.3.2) actionpack (= 6.1.4)
activesupport (= 6.1.3.2) activesupport (= 6.1.4)
method_source method_source
rake (>= 0.8.7) rake (>= 0.13)
thor (~> 1.0) thor (~> 1.0)
rainbow (3.0.0) rainbow (3.0.0)
rake (13.0.3) rake (13.0.3)
@ -492,7 +492,6 @@ GEM
regexp_parser (2.1.1) regexp_parser (2.1.1)
request_store (1.5.0) request_store (1.5.0)
rack (>= 1.4) rack (>= 1.4)
resolv (0.1.0)
responders (3.0.1) responders (3.0.1)
actionpack (>= 5.0) actionpack (>= 5.0)
railties (>= 5.0) railties (>= 5.0)
@ -525,7 +524,7 @@ GEM
rspec-support (3.10.2) rspec-support (3.10.2)
rspec_junit_formatter (0.4.1) rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0) rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.16.1) rubocop (1.18.3)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.0.0.0) parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
@ -536,7 +535,7 @@ GEM
unicode-display_width (>= 1.4.0, < 3.0) unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.7.0) rubocop-ast (1.7.0)
parser (>= 3.0.1.1) parser (>= 3.0.1.1)
rubocop-rails (2.10.1) rubocop-rails (2.11.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
@ -570,7 +569,7 @@ GEM
sidekiq (>= 3) sidekiq (>= 3)
thwait thwait
tilt (>= 1.4.0) tilt (>= 1.4.0)
sidekiq-unique-jobs (7.0.12) sidekiq-unique-jobs (7.1.2)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0) brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 5.0, < 7.0) sidekiq (>= 5.0, < 7.0)
@ -659,7 +658,7 @@ GEM
webpush (0.3.8) webpush (0.3.8)
hkdf (~> 0.2) hkdf (~> 0.2)
jwt (~> 2.0) jwt (~> 2.0)
websocket-driver (0.7.3) websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
wisper (2.0.1) wisper (2.0.1)
@ -674,7 +673,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
active_model_serializers (~> 0.10) active_model_serializers (~> 0.10)
active_record_query_trace (~> 1.8) active_record_query_trace (~> 1.8)
addressable (~> 2.7) addressable (~> 2.8)
annotate (~> 3.1) annotate (~> 3.1)
aws-sdk-s3 (~> 1.96) aws-sdk-s3 (~> 1.96)
better_errors (~> 2.9) better_errors (~> 2.9)
@ -758,7 +757,7 @@ DEPENDENCIES
rack (~> 2.2.3) rack (~> 2.2.3)
rack-attack (~> 6.5) rack-attack (~> 6.5)
rack-cors (~> 1.1) rack-cors (~> 1.1)
rails (~> 6.1.3) rails (~> 6.1.4)
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
rails-i18n (~> 6.0) rails-i18n (~> 6.0)
rails-settings-cached (~> 0.6) rails-settings-cached (~> 0.6)
@ -766,20 +765,19 @@ DEPENDENCIES
redcarpet (~> 3.5) redcarpet (~> 3.5)
redis (~> 4.3) redis (~> 4.3)
redis-namespace (~> 1.8) redis-namespace (~> 1.8)
resolv (~> 0.1.0)
rqrcode (~> 2.0) rqrcode (~> 2.0)
rspec-rails (~> 5.0) rspec-rails (~> 5.0)
rspec-sidekiq (~> 3.1) rspec-sidekiq (~> 3.1)
rspec_junit_formatter (~> 0.4) rspec_junit_formatter (~> 0.4)
rubocop (~> 1.16) rubocop (~> 1.18)
rubocop-rails (~> 2.10) rubocop-rails (~> 2.11)
ruby-progressbar (~> 1.11) ruby-progressbar (~> 1.11)
sanitize (~> 5.2) sanitize (~> 5.2)
scenic (~> 1.5) scenic (~> 1.5)
sidekiq (~> 6.2) sidekiq (~> 6.2)
sidekiq-bulk (~> 0.2.0) sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 3.1) sidekiq-scheduler (~> 3.1)
sidekiq-unique-jobs (~> 7.0) sidekiq-unique-jobs (~> 7.1)
simple-navigation (~> 4.3) simple-navigation (~> 4.3)
simple_form (~> 5.1) simple_form (~> 5.1)
simplecov (~> 0.21) simplecov (~> 0.21)

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

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

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

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

@ -10,6 +10,15 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user) @user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
if @user.persisted? if @user.persisted?
LoginActivity.create(
user: user,
success: true,
authentication_method: :omniauth,
provider: provider,
ip: request.remote_ip,
user_agent: request.user_agent
)
sign_in_and_redirect @user, event: :authentication sign_in_and_redirect @user, event: :authentication
set_flash_message(:notice, :success, kind: provider_id.capitalize) if is_navigational_format? set_flash_message(:notice, :success, kind: provider_id.capitalize) if is_navigational_format?
else else

@ -27,9 +27,11 @@ class Auth::SessionsController < Devise::SessionsController
def create def create
super do |resource| super do |resource|
resource.update_sign_in!(request, new_sign_in: true) # We only need to call this if this hasn't already been
remember_me(resource) # called from one of the two-factor or sign-in token
flash.delete(:notice) # authentication methods
on_authentication_success(resource, :password) unless @on_authentication_success_called
end end
end end
@ -44,10 +46,8 @@ class Auth::SessionsController < Devise::SessionsController
def webauthn_options def webauthn_options
user = find_user user = find_user
if user.webauthn_enabled? if user&.webauthn_enabled?
options_for_get = WebAuthn::Credential.options_for_get( options_for_get = WebAuthn::Credential.options_for_get(allow: user.webauthn_credentials.pluck(:external_id))
allow: user.webauthn_credentials.pluck(:external_id)
)
session[:webauthn_challenge] = options_for_get.challenge session[:webauthn_challenge] = options_for_get.challenge
@ -142,4 +142,34 @@ class Auth::SessionsController < Devise::SessionsController
session.delete(:attempt_user_id) session.delete(:attempt_user_id)
session.delete(:attempt_user_updated_at) session.delete(:attempt_user_updated_at)
end end
def on_authentication_success(user, security_measure)
@on_authentication_success_called = true
clear_attempt_from_session
user.update_sign_in!(request, new_sign_in: true)
remember_me(user)
sign_in(user)
flash.delete(:notice)
LoginActivity.create(
user: user,
success: true,
authentication_method: security_measure,
ip: request.remote_ip,
user_agent: request.user_agent
)
end
def on_authentication_failure(user, security_measure, failure_reason)
LoginActivity.create(
user: user,
success: false,
authentication_method: security_measure,
failure_reason: failure_reason,
ip: request.remote_ip,
user_agent: request.user_agent
)
end
end end

@ -29,10 +29,9 @@ module SignInTokenAuthenticationConcern
def authenticate_with_sign_in_token_attempt(user) def authenticate_with_sign_in_token_attempt(user)
if valid_sign_in_token_attempt?(user) if valid_sign_in_token_attempt?(user)
clear_attempt_from_session on_authentication_success(user, :sign_in_token)
remember_me(user)
sign_in(user)
else else
on_authentication_failure(user, :sign_in_token, :invalid_sign_in_token)
flash.now[:alert] = I18n.t('users.invalid_sign_in_token') flash.now[:alert] = I18n.t('users.invalid_sign_in_token')
prompt_for_sign_in_token(user) prompt_for_sign_in_token(user)
end end

@ -52,21 +52,19 @@ module TwoFactorAuthenticationConcern
webauthn_credential = WebAuthn::Credential.from_get(user_params[:credential]) webauthn_credential = WebAuthn::Credential.from_get(user_params[:credential])
if valid_webauthn_credential?(user, webauthn_credential) if valid_webauthn_credential?(user, webauthn_credential)
clear_attempt_from_session on_authentication_success(user, :webauthn)
remember_me(user)
sign_in(user)
render json: { redirect_path: root_path }, status: :ok render json: { redirect_path: root_path }, status: :ok
else else
on_authentication_failure(user, :webauthn, :invalid_credential)
render json: { error: t('webauthn_credentials.invalid_credential') }, status: :unprocessable_entity render json: { error: t('webauthn_credentials.invalid_credential') }, status: :unprocessable_entity
end end
end end
def authenticate_with_two_factor_via_otp(user) def authenticate_with_two_factor_via_otp(user)
if valid_otp_attempt?(user) if valid_otp_attempt?(user)
clear_attempt_from_session on_authentication_success(user, :otp)
remember_me(user)
sign_in(user)
else else
on_authentication_failure(user, :otp, :invalid_otp_token)
flash.now[:alert] = I18n.t('users.invalid_otp_token') flash.now[:alert] = I18n.t('users.invalid_otp_token')
prompt_for_two_factor(user) prompt_for_two_factor(user)
end end

@ -86,7 +86,7 @@ class FollowerAccountsController < ApplicationController
if page_requested? || !@account.user_hides_network? if page_requested? || !@account.user_hides_network?
# Return all fields # Return all fields
else else
%i(id type totalItems) %i(id type total_items)
end end
end end
end end

@ -86,7 +86,7 @@ class FollowingAccountsController < ApplicationController
if page_requested? || !@account.user_hides_network? if page_requested? || !@account.user_hides_network?
# Return all fields # Return all fields
else else
%i(id type totalItems) %i(id type total_items)
end end
end end
end end

@ -0,0 +1,13 @@
# frozen_string_literal: true
class Settings::LoginActivitiesController < Settings::BaseController
def index
@login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page])
end
private
def set_pack
use_pack 'settings'
end
end

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

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

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

@ -22,13 +22,20 @@ export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
* @param {MediaProps} props * @param {MediaProps} props
* @return {object} * @return {object}
*/ */
export const deployPictureInPicture = (statusId, accountId, playerType, props) => ({ export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
type: PICTURE_IN_PICTURE_DEPLOY, return (dispatch, getState) => {
statusId, // Do not open a player for a toot that does not exist
accountId, if (getState().hasIn(['statuses', statusId])) {
playerType, dispatch({
props, type: PICTURE_IN_PICTURE_DEPLOY,
}); statusId,
accountId,
playerType,
props,
});
}
};
};
/* /*
* @return {object} * @return {object}

@ -224,8 +224,8 @@ export default class StatusContent extends React.PureComponent {
const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)]; const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
let element = e.target; let element = e.target;
while (element) { while (element !== e.currentTarget) {
if (['button', 'video', 'a', 'label', 'canvas'].includes(element.localName)) { if (['button', 'video', 'a', 'label', 'canvas'].includes(element.localName) || element.getAttribute('role') === 'button') {
return; return;
} }
element = element.parentNode; element = element.parentNode;

@ -71,7 +71,7 @@ class SearchResults extends ImmutablePureComponent {
); );
} else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) { } else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) {
statuses = ( statuses = (
<section> <section className='search-results__section'>
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> <h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
<div className='search-results__info'> <div className='search-results__info'>
@ -87,7 +87,7 @@ class SearchResults extends ImmutablePureComponent {
if (results.get('accounts') && results.get('accounts').size > 0) { if (results.get('accounts') && results.get('accounts').size > 0) {
count += results.get('accounts').size; count += results.get('accounts').size;
accounts = ( accounts = (
<section> <section className='search-results__section'>
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5> <h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
{results.get('accounts').map(accountId => <AccountContainer id={accountId} key={accountId} />)} {results.get('accounts').map(accountId => <AccountContainer id={accountId} key={accountId} />)}
@ -100,7 +100,7 @@ class SearchResults extends ImmutablePureComponent {
if (results.get('statuses') && results.get('statuses').size > 0) { if (results.get('statuses') && results.get('statuses').size > 0) {
count += results.get('statuses').size; count += results.get('statuses').size;
statuses = ( statuses = (
<section> <section className='search-results__section'>
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> <h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
{results.get('statuses').map(statusId => <StatusContainer id={statusId} key={statusId}/>)} {results.get('statuses').map(statusId => <StatusContainer id={statusId} key={statusId}/>)}
@ -113,7 +113,7 @@ class SearchResults extends ImmutablePureComponent {
if (results.get('hashtags') && results.get('hashtags').size > 0) { if (results.get('hashtags') && results.get('hashtags').size > 0) {
count += results.get('hashtags').size; count += results.get('hashtags').size;
hashtags = ( hashtags = (
<section> <section className='search-results__section'>
<h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> <h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
{results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)} {results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
@ -131,11 +131,9 @@ class SearchResults extends ImmutablePureComponent {
<FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} /> <FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} />
</header> </header>
<div className='search-results__contents'> {accounts}
{accounts} {statuses}
{statuses} {hashtags}
{hashtags}
</div>
</div> </div>
); );
}; };

@ -3,7 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { invitesEnabled, version, repository, source_url } from 'flavours/glitch/util/initial_state'; import { invitesEnabled, version, limitedFederationMode, repository, source_url } from 'flavours/glitch/util/initial_state';
import { signOutLink, securityLink } from 'flavours/glitch/util/backend_links'; import { signOutLink, securityLink } from 'flavours/glitch/util/backend_links';
import { logOut } from 'flavours/glitch/util/log_out'; import { logOut } from 'flavours/glitch/util/log_out';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
@ -45,8 +45,9 @@ class LinkFooter extends React.PureComponent {
return ( return (
<div className='getting-started__footer'> <div className='getting-started__footer'>
<ul> <ul>
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite hocams' /></a> · </li>}
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li> {!!securityLink && <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>}
{!limitedFederationMode && <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>}
<li><a href='/apps/index.html' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li> <li><a href='/apps/index.html' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li> <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
<li><a href={signOutLink} onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li> <li><a href={signOutLink} onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>

@ -1,4 +1,5 @@
import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'flavours/glitch/actions/picture_in_picture'; import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'flavours/glitch/actions/picture_in_picture';
import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines';
const initialState = { const initialState = {
statusId: null, statusId: null,
@ -16,6 +17,8 @@ export default function pictureInPicture(state = initialState, action) {
return { statusId: action.statusId, accountId: action.accountId, type: action.playerType, ...action.props }; return { statusId: action.statusId, accountId: action.accountId, type: action.playerType, ...action.props };
case PICTURE_IN_PICTURE_REMOVE: case PICTURE_IN_PICTURE_REMOVE:
return { ...initialState }; return { ...initialState };
case TIMELINE_DELETE:
return (state.statusId === action.id) ? { ...initialState } : state;
default: default:
return state; return state;
} }

File diff suppressed because one or more lines are too long

@ -120,20 +120,22 @@
} }
.drawer--results { .drawer--results {
background: $ui-base-color; overflow-x: hidden;
overflow: hidden; overflow-y: scroll;
display: flex; }
flex-direction: column;
flex: 1 1 auto;
& > header { .search-results__section {
color: $dark-text-color; margin-bottom: 5px;
background: lighten($ui-base-color, 2%);
h5 {
background: darken($ui-base-color, 4%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: default;
display: flex;
padding: 15px; padding: 15px;
font-weight: 500; font-weight: 500;
font-size: 16px; font-size: 16px;
cursor: default; color: $dark-text-color;
flex: 0 0 auto;
.fa { .fa {
display: inline-block; display: inline-block;
@ -141,48 +143,22 @@
} }
} }
& > .search-results__contents { .account:last-child,
overflow-x: hidden; & > div:last-child .status {
overflow-y: scroll; border-bottom: 0;
flex: 1 1 auto; }
& > .hashtag {
display: block;
padding: 10px;
color: $secondary-text-color;
text-decoration: none;
& > section { &:hover,
margin-bottom: 5px; &:active,
&:focus {
h5 { color: lighten($secondary-text-color, 4%);
background: darken($ui-base-color, 4%); text-decoration: underline;
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: default;
display: flex;
padding: 15px;
font-weight: 500;
font-size: 16px;
color: $dark-text-color;
.fa {
display: inline-block;
margin-right: 5px;
}
}
.account:last-child,
& > div:last-child .status {
border-bottom: 0;
}
& > .hashtag {
display: block;
padding: 10px;
color: $secondary-text-color;
text-decoration: none;
&:hover,
&:active,
&:focus {
color: lighten($secondary-text-color, 4%);
text-decoration: underline;
}
}
} }
} }
} }

@ -320,6 +320,13 @@
background: rgba($gold-star, 0.3); background: rgba($gold-star, 0.3);
} }
} }
&.disabled {
color: $white;
background-color: transparent;
cursor: default;
opacity: 0.4;
}
} }
} }
} }

@ -94,10 +94,15 @@
.search-results__header { .search-results__header {
color: $dark-text-color; color: $dark-text-color;
background: lighten($ui-base-color, 2%); background: lighten($ui-base-color, 2%);
border-bottom: 1px solid darken($ui-base-color, 4%); padding: 15px;
padding: 15px 10px;
font-size: 14px;
font-weight: 500; font-weight: 500;
font-size: 16px;
cursor: default;
.fa {
display: inline-block;
margin-right: 5px;
}
} }
.search-results__info { .search-results__info {

@ -1095,6 +1095,7 @@ a.status-card.compact:hover {
&__account { &__account {
display: flex; display: flex;
text-decoration: none; text-decoration: none;
overflow: hidden;
} }
.account__avatar { .account__avatar {

@ -11,6 +11,24 @@ code {
margin: 0 auto; margin: 0 auto;
} }
.indicator-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
color: $primary-text-color;
&.success {
background: $success-green;
}
&.failure {
background: $error-red;
}
}
.simple_form { .simple_form {
&.hidden { &.hidden {
display: none; display: none;

@ -24,6 +24,7 @@ export const searchEnabled = getMeta('search_enabled');
export const maxChars = (initialState && initialState.max_toot_chars) || 500; export const maxChars = (initialState && initialState.max_toot_chars) || 500;
export const pollLimits = (initialState && initialState.poll_limits); export const pollLimits = (initialState && initialState.poll_limits);
export const invitesEnabled = getMeta('invites_enabled'); export const invitesEnabled = getMeta('invites_enabled');
export const limitedFederationMode = getMeta('limited_federation_mode');
export const version = getMeta('version'); export const version = getMeta('version');
export const mascot = getMeta('mascot'); export const mascot = getMeta('mascot');
export const profile_directory = getMeta('profile_directory'); export const profile_directory = getMeta('profile_directory');

@ -22,13 +22,20 @@ export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
* @param {MediaProps} props * @param {MediaProps} props
* @return {object} * @return {object}
*/ */
export const deployPictureInPicture = (statusId, accountId, playerType, props) => ({ export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
type: PICTURE_IN_PICTURE_DEPLOY, return (dispatch, getState) => {
statusId, // Do not open a player for a toot that does not exist
accountId, if (getState().hasIn(['statuses', statusId])) {
playerType, dispatch({
props, type: PICTURE_IN_PICTURE_DEPLOY,
}); statusId,
accountId,
playerType,
props,
});
}
};
};
/* /*
* @return {object} * @return {object}

@ -3,7 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { invitesEnabled, version, repository, source_url } from 'mastodon/initial_state'; import { invitesEnabled, limitedFederationMode, version, repository, source_url } from 'mastodon/initial_state';
import { logOut } from 'mastodon/utils/log_out'; import { logOut } from 'mastodon/utils/log_out';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
@ -47,11 +47,12 @@ class LinkFooter extends React.PureComponent {
return ( return (
<div className='getting-started__footer'> <div className='getting-started__footer'>
<ul> <ul>
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite hocams' /></a> · </li>}
{withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>} {withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li> <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
<li><a href='/apps/index.html' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li> <li><a href='/apps/index.html' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
{!limitedFederationMode && <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>}
>>>>>>> remotes/upstream/main
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li> <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li> <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li> <li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>

@ -14,6 +14,7 @@ export const me = getMeta('me');
export const searchEnabled = getMeta('search_enabled'); export const searchEnabled = getMeta('search_enabled');
export const maxChars = (initialState && initialState.max_toot_chars) || 500; export const maxChars = (initialState && initialState.max_toot_chars) || 500;
export const invitesEnabled = getMeta('invites_enabled'); export const invitesEnabled = getMeta('invites_enabled');
export const limitedFederationMode = getMeta('limited_federation_mode');
export const repository = getMeta('repository'); export const repository = getMeta('repository');
export const source_url = getMeta('source_url'); export const source_url = getMeta('source_url');
export const version = getMeta('version'); export const version = getMeta('version');

@ -1,4 +1,5 @@
import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'mastodon/actions/picture_in_picture'; import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'mastodon/actions/picture_in_picture';
import { TIMELINE_DELETE } from '../actions/timelines';
const initialState = { const initialState = {
statusId: null, statusId: null,
@ -16,6 +17,8 @@ export default function pictureInPicture(state = initialState, action) {
return { statusId: action.statusId, accountId: action.accountId, type: action.playerType, ...action.props }; return { statusId: action.statusId, accountId: action.accountId, type: action.playerType, ...action.props };
case PICTURE_IN_PICTURE_REMOVE: case PICTURE_IN_PICTURE_REMOVE:
return { ...initialState }; return { ...initialState };
case TIMELINE_DELETE:
return (state.statusId === action.id) ? { ...initialState } : state;
default: default:
return state; return state;
} }

@ -10,9 +10,9 @@ $red-bookmark: $warning-red;
// Kafuka Theme Unique Colors. Color names may come later. // Kafuka Theme Unique Colors. Color names may come later.
$classic-base-color: #0c0c0c; $classic-base-color: #0c0c0c;
$classic-primary-color: #a81717; //ee7487; $classic-primary-color: #e6464b; //a81717; //ee7487;
$classic-secondary-color: #edfcf5; $classic-secondary-color: #edfcf5;
$classic-highlight-color: #df1717; //e31837; $classic-highlight-color: #a81717; //df1717; //e31837;
// Variables for defaults in UI // Variables for defaults in UI
$base-shadow-color: $black !default; $base-shadow-color: $black !default;

File diff suppressed because one or more lines are too long

@ -4732,6 +4732,13 @@ a.status-card.compact:hover {
background: rgba($gold-star, 0.3); background: rgba($gold-star, 0.3);
} }
} }
&.disabled {
color: $white;
background-color: transparent;
cursor: default;
opacity: 0.4;
}
} }
} }
} }
@ -7284,6 +7291,7 @@ noscript {
&__account { &__account {
display: flex; display: flex;
text-decoration: none; text-decoration: none;
overflow: hidden;
} }
.account__avatar { .account__avatar {

@ -11,6 +11,24 @@ code {
margin: 0 auto; margin: 0 auto;
} }
.indicator-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
color: $primary-text-color;
&.success {
background: $success-green;
}
&.failure {
background: $error-red;
}
}
.simple_form { .simple_form {
&.hidden { &.hidden {
display: none; display: none;

@ -572,7 +572,11 @@ class Account < ApplicationRecord
def create_canonical_email_block! def create_canonical_email_block!
return unless local? && user_email.present? return unless local? && user_email.present?
CanonicalEmailBlock.create(reference_account: self, email: user_email) begin
CanonicalEmailBlock.create(reference_account: self, email: user_email)
rescue ActiveRecord::RecordNotUnique
# A canonical e-mail block may already exist for the same e-mail
end
end end
def destroy_canonical_email_block! def destroy_canonical_email_block!

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

@ -15,10 +15,10 @@ module LdapAuthenticable
def ldap_get_user(attributes = {}) def ldap_get_user(attributes = {})
safe_username = attributes[Devise.ldap_uid.to_sym].first safe_username = attributes[Devise.ldap_uid.to_sym].first
if Devise.ldap_uid_conversion_enabled if Devise.ldap_uid_conversion_enabled
keys = Regexp.union(Devise.ldap_uid_conversion_search.chars) keys = Regexp.union(Devise.ldap_uid_conversion_search.chars)
replacement = Devise.ldap_uid_conversion_replace replacement = Devise.ldap_uid_conversion_replace
safe_username = safe_username.gsub(keys, replacement) safe_username = safe_username.gsub(keys, replacement)
end end

@ -0,0 +1,35 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: login_activities
#
# id :bigint(8) not null, primary key
# user_id :bigint(8) not null
# authentication_method :string
# provider :string
# success :boolean
# failure_reason :string
# ip :inet
# user_agent :string
# created_at :datetime
#
class LoginActivity < ApplicationRecord
enum authentication_method: { password: 'password', otp: 'otp', webauthn: 'webauthn', sign_in_token: 'sign_in_token', omniauth: 'omniauth' }
belongs_to :user
validates :authentication_method, inclusion: { in: authentication_methods.keys }
def detection
@detection ||= Browser.new(user_agent)
end
def browser
detection.id
end
def platform
detection.platform.id
end
end

@ -6,6 +6,7 @@ class ReportFilter
account_id account_id
target_account_id target_account_id
by_target_domain by_target_domain
target_origin
).freeze ).freeze
attr_reader :params attr_reader :params
@ -34,8 +35,21 @@ class ReportFilter
Report.where(account_id: value) Report.where(account_id: value)
when :target_account_id when :target_account_id
Report.where(target_account_id: value) Report.where(target_account_id: value)
when :target_origin
target_origin_scope(value)
else else
raise "Unknown filter: #{key}" raise "Unknown filter: #{key}"
end end
end end
def target_origin_scope(value)
case value.to_sym
when :local
Report.where(target_account: Account.local)
when :remote
Report.where(target_account: Account.remote)
else
raise "Unknown value: #{value}"
end
end
end end

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

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

@ -33,6 +33,7 @@ class InitialStateSerializer < ActiveModel::Serializer
source_url: Mastodon::Version.source_url, source_url: Mastodon::Version.source_url,
version: Mastodon::Version.to_s, version: Mastodon::Version.to_s,
invites_enabled: Setting.min_invite_role == 'user', invites_enabled: Setting.min_invite_role == 'user',
limited_federation_mode: Rails.configuration.x.whitelist_mode,
mascot: instance_presenter.mascot&.file&.url, mascot: instance_presenter.mascot&.file&.url,
profile_directory: Setting.profile_directory, profile_directory: Setting.profile_directory,
trends: Setting.trends, trends: Setting.trends,

@ -5,7 +5,8 @@ class REST::InstanceSerializer < ActiveModel::Serializer
attributes :uri, :title, :short_description, :description, :email, attributes :uri, :title, :short_description, :description, :email,
:version, :urls, :stats, :thumbnail, :max_toot_chars, :poll_limits, :version, :urls, :stats, :thumbnail, :max_toot_chars, :poll_limits,
:languages, :registrations, :approval_required, :invites_enabled :languages, :registrations, :approval_required, :invites_enabled,
:configuration
has_one :contact_account, serializer: REST::AccountSerializer has_one :contact_account, serializer: REST::AccountSerializer
@ -66,6 +67,32 @@ class REST::InstanceSerializer < ActiveModel::Serializer
{ streaming_api: Rails.configuration.x.streaming_api_base_url } { streaming_api: Rails.configuration.x.streaming_api_base_url }
end end
def configuration
{
statuses: {
max_characters: StatusLengthValidator::MAX_CHARS,
max_media_attachments: 4,
characters_reserved_per_url: StatusLengthValidator::URL_PLACEHOLDER_CHARS,
},
media_attachments: {
supported_mime_types: MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES + MediaAttachment::AUDIO_MIME_TYPES,
image_size_limit: MediaAttachment::IMAGE_LIMIT,
image_matrix_limit: Attachmentable::MAX_MATRIX_LIMIT,
video_size_limit: MediaAttachment::VIDEO_LIMIT,
video_frame_rate_limit: MediaAttachment::MAX_VIDEO_FRAME_RATE,
video_matrix_limit: MediaAttachment::MAX_VIDEO_MATRIX_LIMIT,
},
polls: {
max_options: PollValidator::MAX_OPTIONS,
max_characters_per_option: PollValidator::MAX_OPTION_CHARS,
min_expiration: PollValidator::MIN_EXPIRATION,
max_expiration: PollValidator::MAX_EXPIRATION,
},
}
end
def languages def languages
[I18n.default_locale] [I18n.default_locale]
end end

@ -2,7 +2,8 @@
class StatusLengthValidator < ActiveModel::Validator class StatusLengthValidator < ActiveModel::Validator
MAX_CHARS = (ENV['MAX_TOOT_CHARS'] || 1956).to_i MAX_CHARS = (ENV['MAX_TOOT_CHARS'] || 1956).to_i
URL_PLACEHOLDER = "\1#{'x' * 23}" URL_PLACEHOLDER_CHARS = 23
URL_PLACEHOLDER = "\1#{'x' * URL_PLACEHOLDER_CHARS}"
def validate(status) def validate(status)
return unless status.local? && !status.reblog? return unless status.local? && !status.reblog?

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

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

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

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

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

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

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

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

@ -7,6 +7,12 @@
%ul %ul
%li= filter_link_to t('admin.reports.unresolved'), resolved: nil %li= filter_link_to t('admin.reports.unresolved'), resolved: nil
%li= filter_link_to t('admin.reports.resolved'), resolved: '1' %li= filter_link_to t('admin.reports.resolved'), resolved: '1'
.filter-subset
%strong= t('admin.reports.target_origin')
%ul
%li= filter_link_to t('admin.accounts.location.all'), target_origin: nil
%li= filter_link_to t('admin.accounts.location.local'), target_origin: 'local'
%li= filter_link_to t('admin.accounts.location.remote'), target_origin: 'remote'
= form_tag admin_reports_url, method: 'GET', class: 'simple_form' do = form_tag admin_reports_url, method: 'GET', class: 'simple_form' do
.fields-group .fields-group

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

@ -1,5 +1,7 @@
%h3= t 'sessions.title' %h3= t 'sessions.title'
%p.muted-hint= t 'sessions.explanation' %p.muted-hint
= t 'sessions.explanation'
= link_to t('sessions.view_authentication_history'), settings_login_activities_path
%hr.spacer/ %hr.spacer/
@ -29,3 +31,4 @@
%td %td
- if current_session.session_id != session.session_id && !current_account.suspended? - if current_session.session_id != session.session_id && !current_account.suspended?
= table_link_to 'times', t('sessions.revoke'), settings_session_path(session), method: :delete = table_link_to 'times', t('sessions.revoke'), settings_session_path(session), method: :delete

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

@ -45,7 +45,8 @@
.column-3 .column-3
%h4= site_hostname %h4= site_hostname
%ul %ul
%li= link_to t('about.about_this'), about_more_path - unless whitelist_mode?
%li= link_to t('about.about_this'), about_more_path
%li= "v#{Mastodon::Version.to_s}" %li= "v#{Mastodon::Version.to_s}"
.column-4 .column-4
%h4= t 'footer.more' %h4= t 'footer.more'

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

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

@ -0,0 +1,17 @@
- method_str = content_tag(:span, login_activity.omniauth? ? t(login_activity.provider, scope: 'auth.providers') : t(login_activity.authentication_method, scope: 'login_activities.authentication_methods'), class: 'target')
- ip_str = content_tag(:span, login_activity.ip, class: 'target')
- browser_str = content_tag(:span, t('sessions.description', browser: t("sessions.browsers.#{login_activity.browser}", default: "#{login_activity.browser}"), platform: t("sessions.platforms.#{login_activity.platform}", default: "#{login_activity.platform}")), class: 'target', title: login_activity.user_agent)
.log-entry
.log-entry__header
.log-entry__avatar
.indicator-icon{ class: login_activity.success? ? 'success' : 'failure' }
= fa_icon login_activity.success? ? 'check' : 'times'
.log-entry__content
.log-entry__title
- if login_activity.success?
= t('login_activities.successful_sign_in_html', method: method_str, ip: ip_str, browser: browser_str)
- else
= t('login_activities.failed_sign_in_html', method: method_str, ip: ip_str, browser: browser_str)
.log-entry__timestamp
%time.formatted{ datetime: login_activity.created_at.iso8601 }= l(login_activity.created_at)

@ -0,0 +1,15 @@
- content_for :page_title do
= t 'login_activities.title'
%p= t('login_activities.description_html')
%hr.spacer/
- if @login_activities.empty?
%div.muted-hint.center-text
= t 'login_activities.empty'
- else
.announcements-list
= render partial: 'login_activity', collection: @login_activities
= paginate @login_activities

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

@ -13,9 +13,13 @@ class MoveWorker
queue_follow_unfollows! queue_follow_unfollows!
end end
@deferred_error = nil
copy_account_notes! copy_account_notes!
carry_blocks_over! carry_blocks_over!
carry_mutes_over! carry_mutes_over!
raise @deferred_error unless @deferred_error.nil?
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
true true
end end
@ -36,6 +40,8 @@ class MoveWorker
@source_account.followers.local.select(:id).find_in_batches do |accounts| @source_account.followers.local.select(:id).find_in_batches do |accounts|
UnfollowFollowWorker.push_bulk(accounts.map(&:id)) { |follower_id| [follower_id, @source_account.id, @target_account.id, bypass_locked] } UnfollowFollowWorker.push_bulk(accounts.map(&:id)) { |follower_id| [follower_id, @source_account.id, @target_account.id, bypass_locked] }
rescue => e
@deferred_error = e
end end
end end
@ -47,10 +53,12 @@ class MoveWorker
new_note = AccountNote.find_by(account: note.account, target_account: @target_account) new_note = AccountNote.find_by(account: note.account, target_account: @target_account)
if new_note.nil? if new_note.nil?
AccountNote.create!(account: note.account, target_account: @target_account, comment: [text, note.comment].join('\n')) AccountNote.create!(account: note.account, target_account: @target_account, comment: [text, note.comment].join("\n"))
else else
new_note.update!(comment: [text, note.comment, '\n', new_note.comment].join('\n')) new_note.update!(comment: [text, note.comment, "\n", new_note.comment].join("\n"))
end end
rescue => e
@deferred_error = e
end end
end end
@ -60,6 +68,8 @@ class MoveWorker
BlockService.new.call(block.account, @target_account) BlockService.new.call(block.account, @target_account)
add_account_note_if_needed!(block.account, 'move_handler.carry_blocks_over_text') add_account_note_if_needed!(block.account, 'move_handler.carry_blocks_over_text')
end end
rescue => e
@deferred_error = e
end end
end end
@ -67,6 +77,8 @@ class MoveWorker
@source_account.muted_by_relationships.where(account: Account.local).find_each do |mute| @source_account.muted_by_relationships.where(account: Account.local).find_each do |mute|
MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications) unless mute.account.muting?(@target_account) || mute.account.following?(@target_account) MuteService.new.call(mute.account, @target_account, notifications: mute.hide_notifications) unless mute.account.muting?(@target_account) || mute.account.following?(@target_account)
add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text') add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text')
rescue => e
@deferred_error = e
end end
end end

@ -17,6 +17,7 @@ class Scheduler::IpCleanupScheduler
def clean_ip_columns! def clean_ip_columns!
SessionActivation.where('updated_at < ?', IP_RETENTION_PERIOD.ago).in_batches.destroy_all SessionActivation.where('updated_at < ?', IP_RETENTION_PERIOD.ago).in_batches.destroy_all
User.where('current_sign_in_at < ?', IP_RETENTION_PERIOD.ago).in_batches.update_all(last_sign_in_ip: nil, current_sign_in_ip: nil, sign_up_ip: nil) User.where('current_sign_in_at < ?', IP_RETENTION_PERIOD.ago).in_batches.update_all(last_sign_in_ip: nil, current_sign_in_ip: nil, sign_up_ip: nil)
LoginActivity.where('created_at < ?', IP_RETENTION_PERIOD.ago).in_batches.destroy_all
end end
def clean_expired_ip_blocks! def clean_expired_ip_blocks!

@ -45,7 +45,7 @@ en:
rejecting_media: 'Media files from these servers will not be processed or stored, and no thumbnails will be displayed, requiring manual click-through to the original file:' rejecting_media: 'Media files from these servers will not be processed or stored, and no thumbnails will be displayed, requiring manual click-through to the original file:'
rejecting_media_title: Filtered media rejecting_media_title: Filtered media
silenced: 'Posts from these servers will be hidden in public timelines and conversations, and no notifications will be generated from their users interactions, unless you are following them:' silenced: 'Posts from these servers will be hidden in public timelines and conversations, and no notifications will be generated from their users interactions, unless you are following them:'
silenced_title: Silenced servers silenced_title: Limited servers
suspended: 'No data from these servers will be processed, stored or exchanged, making any interaction or communication with users from these servers impossible:' suspended: 'No data from these servers will be processed, stored or exchanged, making any interaction or communication with users from these servers impossible:'
suspended_title: Suspended servers suspended_title: Suspended servers
unavailable_content_html: Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server. unavailable_content_html: Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.
@ -120,6 +120,7 @@ en:
demote: Demote demote: Demote
destroyed_msg: "%{username}'s data is now queued to be deleted imminently" destroyed_msg: "%{username}'s data is now queued to be deleted imminently"
disable: Freeze disable: Freeze
disable_sign_in_token_auth: Disable e-mail token authentication
disable_two_factor_authentication: Disable 2FA disable_two_factor_authentication: Disable 2FA
disabled: Frozen disabled: Frozen
display_name: Display name display_name: Display name
@ -128,6 +129,7 @@ en:
email: Email email: Email
email_status: Email status email_status: Email status
enable: Unfreeze enable: Unfreeze
enable_sign_in_token_auth: Enable e-mail token authentication
enabled: Enabled enabled: Enabled
enabled_msg: Successfully unfroze %{username}'s account enabled_msg: Successfully unfroze %{username}'s account
followers: Followers followers: Followers
@ -152,7 +154,7 @@ en:
active: Active active: Active
all: All all: All
pending: Pending pending: Pending
silenced: Silenced silenced: Limited
suspended: Suspended suspended: Suspended
title: Moderation title: Moderation
moderation_notes: Moderation notes moderation_notes: Moderation notes
@ -192,8 +194,12 @@ en:
search: Search search: Search
search_same_email_domain: Other users with the same e-mail domain search_same_email_domain: Other users with the same e-mail domain
search_same_ip: Other users with the same IP search_same_ip: Other users with the same IP
sensitive: Sensitive security_measures:
sensitized: marked as sensitive only_password: Only password
password_and_2fa: Password and 2FA
password_and_sign_in_token: Password and e-mail token
sensitive: Force-sensitive
sensitized: Marked as sensitive
shared_inbox_url: Shared inbox URL shared_inbox_url: Shared inbox URL
show: show:
created_reports: Made reports created_reports: Made reports
@ -208,10 +214,10 @@ en:
time_in_queue: Waiting in queue %{time} time_in_queue: Waiting in queue %{time}
title: Accounts title: Accounts
unconfirmed_email: Unconfirmed email unconfirmed_email: Unconfirmed email
undo_sensitized: Undo sensitive undo_sensitized: Undo force-sensitive
undo_silenced: Undo silence undo_silenced: Undo limit
undo_suspension: Undo suspension undo_suspension: Undo suspension
unsilenced_msg: Successfully unlimited %{username}'s account unsilenced_msg: Successfully undid limit of %{username}'s account
unsubscribe: Unsubscribe unsubscribe: Unsubscribe
unsuspended_msg: Successfully unsuspended %{username}'s account unsuspended_msg: Successfully unsuspended %{username}'s account
username: Username username: Username
@ -237,14 +243,16 @@ en:
destroy_custom_emoji: Delete Custom Emoji destroy_custom_emoji: Delete Custom Emoji
destroy_domain_allow: Delete Domain Allow destroy_domain_allow: Delete Domain Allow
destroy_domain_block: Delete Domain Block destroy_domain_block: Delete Domain Block
destroy_email_domain_block: Delete e-mail domain block destroy_email_domain_block: Delete E-mail Domain Block
destroy_ip_block: Delete IP rule destroy_ip_block: Delete IP rule
destroy_status: Delete Post destroy_status: Delete Post
destroy_unavailable_domain: Delete Unavailable Domain destroy_unavailable_domain: Delete Unavailable Domain
disable_2fa_user: Disable 2FA disable_2fa_user: Disable 2FA
disable_custom_emoji: Disable Custom Emoji disable_custom_emoji: Disable Custom Emoji
disable_sign_in_token_auth_user: Disable E-mail Token Authentication for User
disable_user: Disable User disable_user: Disable User
enable_custom_emoji: Enable Custom Emoji enable_custom_emoji: Enable Custom Emoji
enable_sign_in_token_auth_user: Enable E-mail Token Authentication for User
enable_user: Enable User enable_user: Enable User
memorialize_account: Memorialize Account memorialize_account: Memorialize Account
promote_user: Promote User promote_user: Promote User
@ -252,12 +260,12 @@ en:
reopen_report: Reopen Report reopen_report: Reopen Report
reset_password_user: Reset Password reset_password_user: Reset Password
resolve_report: Resolve Report resolve_report: Resolve Report
sensitive_account: Mark the media in your account as sensitive sensitive_account: Force-Sensitive Account
silence_account: Silence Account silence_account: Limit Account
suspend_account: Suspend Account suspend_account: Suspend Account
unassigned_report: Unassign Report unassigned_report: Unassign Report
unsensitive_account: Unmark the media in your account as sensitive unsensitive_account: Undo Force-Sensitive Account
unsilence_account: Unsilence Account unsilence_account: Undo Limit Account
unsuspend_account: Unsuspend Account unsuspend_account: Unsuspend Account
update_announcement: Update Announcement update_announcement: Update Announcement
update_custom_emoji: Update Custom Emoji update_custom_emoji: Update Custom Emoji
@ -286,8 +294,10 @@ en:
destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}" destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}"
disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}" disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}"
disable_custom_emoji_html: "%{name} disabled emoji %{target}" disable_custom_emoji_html: "%{name} disabled emoji %{target}"
disable_sign_in_token_auth_user_html: "%{name} disabled e-mail token authentication for %{target}"
disable_user_html: "%{name} disabled login for user %{target}" disable_user_html: "%{name} disabled login for user %{target}"
enable_custom_emoji_html: "%{name} enabled emoji %{target}" enable_custom_emoji_html: "%{name} enabled emoji %{target}"
enable_sign_in_token_auth_user_html: "%{name} enabled e-mail token authentication for %{target}"
enable_user_html: "%{name} enabled login for user %{target}" enable_user_html: "%{name} enabled login for user %{target}"
memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page" memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page"
promote_user_html: "%{name} promoted user %{target}" promote_user_html: "%{name} promoted user %{target}"
@ -296,11 +306,11 @@ en:
reset_password_user_html: "%{name} reset password of user %{target}" reset_password_user_html: "%{name} reset password of user %{target}"
resolve_report_html: "%{name} resolved report %{target}" resolve_report_html: "%{name} resolved report %{target}"
sensitive_account_html: "%{name} marked %{target}'s media as sensitive" sensitive_account_html: "%{name} marked %{target}'s media as sensitive"
silence_account_html: "%{name} silenced %{target}'s account" silence_account_html: "%{name} limited %{target}'s account"
suspend_account_html: "%{name} suspended %{target}'s account" suspend_account_html: "%{name} suspended %{target}'s account"
unassigned_report_html: "%{name} unassigned report %{target}" unassigned_report_html: "%{name} unassigned report %{target}"
unsensitive_account_html: "%{name} unmarked %{target}'s media as sensitive" unsensitive_account_html: "%{name} unmarked %{target}'s media as sensitive"
unsilence_account_html: "%{name} unsilenced %{target}'s account" unsilence_account_html: "%{name} undid limit of %{target}'s account"
unsuspend_account_html: "%{name} unsuspended %{target}'s account" unsuspend_account_html: "%{name} unsuspended %{target}'s account"
update_announcement_html: "%{name} updated announcement %{target}" update_announcement_html: "%{name} updated announcement %{target}"
update_custom_emoji_html: "%{name} updated emoji %{target}" update_custom_emoji_html: "%{name} updated emoji %{target}"
@ -422,14 +432,14 @@ en:
rejecting_media: rejecting media files rejecting_media: rejecting media files
rejecting_reports: rejecting reports rejecting_reports: rejecting reports
severity: severity:
silence: silenced silence: limited
suspend: suspended suspend: suspended
show: show:
affected_accounts: affected_accounts:
one: One account in the database affected one: One account in the database affected
other: "%{count} accounts in the database affected" other: "%{count} accounts in the database affected"
retroactive: retroactive:
silence: Unsilence existing affected accounts from this domain silence: Undo limit of existing affected accounts from this domain
suspend: Unsuspend existing affected accounts from this domain suspend: Unsuspend existing affected accounts from this domain
title: Undo domain block for %{domain} title: Undo domain block for %{domain}
undo: Undo undo: Undo
@ -570,6 +580,7 @@ en:
resolved: Resolved resolved: Resolved
resolved_msg: Report successfully resolved! resolved_msg: Report successfully resolved!
status: Status status: Status
target_origin: Origin of reported account
title: Reports title: Reports
unassign: Unassign unassign: Unassign
unresolved: Unresolved unresolved: Unresolved
@ -1004,10 +1015,21 @@ en:
table: table:
expires_at: Expires expires_at: Expires
uses: Uses uses: Uses
title: Invite people title: Invite hocams
lists: lists:
errors: errors:
limit: You have reached the maximum amount of lists limit: You have reached the maximum amount of lists
login_activities:
authentication_methods:
otp: two-factor authentication app
password: password
sign_in_token: e-mail security code
webauthn: security keys
description_html: If you see activity that you don't recognize, consider changing your password and enabling two-factor authentication.
empty: No authentication history available
failed_sign_in_html: Failed sign-in attempt with %{method} from %{ip} (%{browser})
successful_sign_in_html: Successful sign-in with %{method} from %{ip} (%{browser})
title: Authentication history
media_attachments: media_attachments:
validations: validations:
images_and_video: Cannot attach a video to a post that already contains images images_and_video: Cannot attach a video to a post that already contains images
@ -1215,6 +1237,7 @@ en:
revoke: Revoke revoke: Revoke
revoke_success: Session successfully revoked revoke_success: Session successfully revoked
title: Sessions title: Sessions
view_authentication_history: View authentication history of your account
settings: settings:
account: Account account: Account
account_settings: Account settings account_settings: Account settings

@ -1003,7 +1003,7 @@ tr:
table: table:
expires_at: Bitiş tarihi expires_at: Bitiş tarihi
uses: Kullanım uses: Kullanım
title: İnsanları davet et title: Hocamları davet et
lists: lists:
errors: errors:
limit: En yüksek liste sayısına ulaştınız limit: En yüksek liste sayısına ulaştınız

@ -26,7 +26,7 @@ SimpleNavigation::Configuration.run do |navigation|
n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? } n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? }
n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s| n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s|
s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases} s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities}
s.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_methods_url, highlights_on: %r{/settings/two_factor_authentication|/settings/otp_authentication|/settings/security_keys} s.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_methods_url, highlights_on: %r{/settings/two_factor_authentication|/settings/otp_authentication|/settings/security_keys}
s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
end end

@ -166,6 +166,7 @@ Rails.application.routes.draw do
resources :aliases, only: [:index, :create, :destroy] resources :aliases, only: [:index, :create, :destroy]
resources :sessions, only: [:destroy] resources :sessions, only: [:destroy]
resources :featured_tags, only: [:index, :create, :destroy] resources :featured_tags, only: [:index, :create, :destroy]
resources :login_activities, only: [:index]
end end
resources :media, only: [:show] do resources :media, only: [:show] do
@ -224,7 +225,7 @@ Rails.application.routes.draw do
post :stop_delivery post :stop_delivery
end end
end end
resources :rules resources :rules
resources :reports, only: [:index, :show] do resources :reports, only: [:index, :show] do
@ -284,6 +285,7 @@ Rails.application.routes.draw do
resources :users, only: [] do resources :users, only: [] do
resource :two_factor_authentication, only: [:destroy] resource :two_factor_authentication, only: [:destroy]
resource :sign_in_token_authentication, only: [:create, :destroy]
end end
resources :custom_emojis, only: [:index, :new, :create] do resources :custom_emojis, only: [:index, :new, :create] do

@ -43,7 +43,7 @@ module.exports = merge(sharedConfig, {
plugins: [ plugins: [
new CompressionPlugin({ new CompressionPlugin({
filename: '[path].gz[query]', filename: '[path][base].gz[query]',
cache: true, cache: true,
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/, test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/,
}), }),

@ -2,7 +2,7 @@ class CreateCanonicalEmailBlocks < ActiveRecord::Migration[6.1]
def change def change
create_table :canonical_email_blocks do |t| create_table :canonical_email_blocks do |t|
t.string :canonical_email_hash, null: false, default: '', index: { unique: true } t.string :canonical_email_hash, null: false, default: '', index: { unique: true }
t.belongs_to :reference_account, null: false, foreign_key: { on_cascade: :delete, to_table: 'accounts' } t.belongs_to :reference_account, null: false, foreign_key: { to_table: 'accounts' }
t.timestamps t.timestamps
end end

@ -0,0 +1,14 @@
class CreateLoginActivities < ActiveRecord::Migration[6.1]
def change
create_table :login_activities do |t|
t.belongs_to :user, null: false, foreign_key: { on_delete: :cascade }
t.string :authentication_method
t.string :provider
t.boolean :success
t.string :failure_reason
t.inet :ip
t.string :user_agent
t.datetime :created_at
end
end
end

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

@ -0,0 +1,13 @@
class FixCanonicalEmailBlocksForeignKey < ActiveRecord::Migration[6.1]
def up
safety_assured do
execute 'ALTER TABLE canonical_email_blocks DROP CONSTRAINT fk_rails_1ecb262096, ADD CONSTRAINT fk_rails_1ecb262096 FOREIGN KEY (reference_account_id) REFERENCES accounts(id) ON DELETE CASCADE;'
end
end
def down
safety_assured do
execute 'ALTER TABLE canonical_email_blocks DROP CONSTRAINT fk_rails_1ecb262096, ADD CONSTRAINT fk_rails_1ecb262096 FOREIGN KEY (reference_account_id) REFERENCES accounts(id);'
end
end
end

@ -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: 2021_05_26_193025) do ActiveRecord::Schema.define(version: 2021_06_30_000137) 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"
@ -494,6 +494,18 @@ ActiveRecord::Schema.define(version: 2021_05_26_193025) do
t.index ["account_id"], name: "index_lists_on_account_id" t.index ["account_id"], name: "index_lists_on_account_id"
end end
create_table "login_activities", force: :cascade do |t|
t.bigint "user_id", null: false
t.string "authentication_method"
t.string "provider"
t.boolean "success"
t.string "failure_reason"
t.inet "ip"
t.string "user_agent"
t.datetime "created_at"
t.index ["user_id"], name: "index_login_activities_on_user_id"
end
create_table "markers", force: :cascade do |t| create_table "markers", force: :cascade do |t|
t.bigint "user_id" t.bigint "user_id"
t.string "timeline", default: "", null: false t.string "timeline", default: "", null: false
@ -917,6 +929,7 @@ ActiveRecord::Schema.define(version: 2021_05_26_193025) do
t.datetime "sign_in_token_sent_at" t.datetime "sign_in_token_sent_at"
t.string "webauthn_id" t.string "webauthn_id"
t.inet "sign_up_ip" t.inet "sign_up_ip"
t.boolean "skip_sign_in_token"
t.index ["account_id"], name: "index_users_on_account_id" t.index ["account_id"], name: "index_users_on_account_id"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id" t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id"
@ -987,7 +1000,7 @@ ActiveRecord::Schema.define(version: 2021_05_26_193025) do
add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade
add_foreign_key "bookmarks", "accounts", on_delete: :cascade add_foreign_key "bookmarks", "accounts", on_delete: :cascade
add_foreign_key "bookmarks", "statuses", on_delete: :cascade add_foreign_key "bookmarks", "statuses", on_delete: :cascade
add_foreign_key "canonical_email_blocks", "accounts", column: "reference_account_id" add_foreign_key "canonical_email_blocks", "accounts", column: "reference_account_id", on_delete: :cascade
add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade
add_foreign_key "custom_filters", "accounts", on_delete: :cascade add_foreign_key "custom_filters", "accounts", on_delete: :cascade
@ -1012,6 +1025,7 @@ ActiveRecord::Schema.define(version: 2021_05_26_193025) do
add_foreign_key "list_accounts", "follows", on_delete: :cascade add_foreign_key "list_accounts", "follows", on_delete: :cascade
add_foreign_key "list_accounts", "lists", on_delete: :cascade add_foreign_key "list_accounts", "lists", on_delete: :cascade
add_foreign_key "lists", "accounts", on_delete: :cascade add_foreign_key "lists", "accounts", on_delete: :cascade
add_foreign_key "login_activities", "users", on_delete: :cascade
add_foreign_key "markers", "users", on_delete: :cascade add_foreign_key "markers", "users", on_delete: :cascade
add_foreign_key "media_attachments", "accounts", name: "fk_96dd81e81b", on_delete: :nullify add_foreign_key "media_attachments", "accounts", name: "fk_96dd81e81b", on_delete: :nullify
add_foreign_key "media_attachments", "scheduled_statuses", on_delete: :nullify add_foreign_key "media_attachments", "scheduled_statuses", on_delete: :nullify

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

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

@ -14,7 +14,7 @@ services:
redis: redis:
restart: always restart: always
image: redis:6.0-alpine image: redis:6-alpine
networks: networks:
- internal_network - internal_network
healthcheck: healthcheck:

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

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

@ -60,16 +60,16 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@babel/core": "^7.14.5", "@babel/core": "^7.14.6",
"@babel/plugin-proposal-decorators": "^7.14.2", "@babel/plugin-proposal-decorators": "^7.14.5",
"@babel/plugin-transform-react-inline-elements": "^7.12.13", "@babel/plugin-transform-react-inline-elements": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.14.3", "@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.14.4", "@babel/preset-env": "^7.14.7",
"@babel/preset-react": "^7.13.13", "@babel/preset-react": "^7.14.5",
"@babel/runtime": "^7.14.5", "@babel/runtime": "^7.14.6",
"@gamestdio/websocket": "^0.3.2", "@gamestdio/websocket": "^0.3.2",
"@github/webauthn-json": "^0.5.7", "@github/webauthn-json": "^0.5.7",
"@rails/ujs": "^6.1.3", "@rails/ujs": "^6.1.4",
"array-includes": "^3.1.3", "array-includes": "^3.1.3",
"atrament": "0.2.4", "atrament": "0.2.4",
"arrow-key-navigation": "^1.2.0", "arrow-key-navigation": "^1.2.0",
@ -113,7 +113,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mark-loader": "^0.1.6", "mark-loader": "^0.1.6",
"marky": "^1.2.2", "marky": "^1.2.2",
"mini-css-extract-plugin": "^1.6.0", "mini-css-extract-plugin": "^1.6.2",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"npmlog": "^4.1.2", "npmlog": "^4.1.2",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
@ -156,7 +156,7 @@
"requestidlecallback": "^0.3.0", "requestidlecallback": "^0.3.0",
"reselect": "^4.0.0", "reselect": "^4.0.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sass": "^1.34.1", "sass": "^1.35.2",
"sass-loader": "^10.2.0", "sass-loader": "^10.2.0",
"stacktrace-js": "^2.0.2", "stacktrace-js": "^2.0.2",
"stringz": "^2.1.0", "stringz": "^2.1.0",
@ -173,14 +173,14 @@
"webpack-cli": "^3.3.12", "webpack-cli": "^3.3.12",
"webpack-merge": "^5.8.0", "webpack-merge": "^5.8.0",
"wicg-inert": "^3.1.1", "wicg-inert": "^3.1.1",
"ws": "^7.4.6" "ws": "^7.5.2"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7", "@testing-library/react": "^11.2.7",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-jest": "^27.0.2", "babel-jest": "^27.0.6",
"eslint": "^7.28.0", "eslint": "^7.30.0",
"eslint-plugin-import": "~2.23.4", "eslint-plugin-import": "~2.23.4",
"eslint-plugin-jsx-a11y": "~6.4.1", "eslint-plugin-jsx-a11y": "~6.4.1",
"eslint-plugin-promise": "~5.1.0", "eslint-plugin-promise": "~5.1.0",

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

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

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

@ -3,7 +3,7 @@ require 'rails_helper'
describe FollowerAccountsController do describe FollowerAccountsController do
render_views render_views
let(:alice) { Fabricate(:account, username: 'alice') } let(:alice) { Fabricate(:user).account }
let(:follower0) { Fabricate(:account) } let(:follower0) { Fabricate(:account) }
let(:follower1) { Fabricate(:account) } let(:follower1) { Fabricate(:account) }
@ -101,6 +101,23 @@ describe FollowerAccountsController do
expect(body['partOf']).to be_blank expect(body['partOf']).to be_blank
end end
context 'when account hides their network' do
before do
alice.user.settings.hide_network = true
end
it 'returns followers count' do
expect(body['totalItems']).to eq 2
end
it 'does not return items' do
expect(body['items']).to be_blank
expect(body['orderedItems']).to be_blank
expect(body['first']).to be_blank
expect(body['last']).to be_blank
end
end
context 'when account is permanently suspended' do context 'when account is permanently suspended' do
before do before do
alice.suspend! alice.suspend!

@ -3,7 +3,7 @@ require 'rails_helper'
describe FollowingAccountsController do describe FollowingAccountsController do
render_views render_views
let(:alice) { Fabricate(:account, username: 'alice') } let(:alice) { Fabricate(:user).account }
let(:followee0) { Fabricate(:account) } let(:followee0) { Fabricate(:account) }
let(:followee1) { Fabricate(:account) } let(:followee1) { Fabricate(:account) }
@ -101,6 +101,23 @@ describe FollowingAccountsController do
expect(body['partOf']).to be_blank expect(body['partOf']).to be_blank
end end
context 'when account hides their network' do
before do
alice.user.settings.hide_network = true
end
it 'returns followers count' do
expect(body['totalItems']).to eq 2
end
it 'does not return items' do
expect(body['items']).to be_blank
expect(body['orderedItems']).to be_blank
expect(body['first']).to be_blank
expect(body['last']).to be_blank
end
end
context 'when account is permanently suspended' do context 'when account is permanently suspended' do
before do before do
alice.suspend! alice.suspend!

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

@ -0,0 +1,8 @@
Fabricator(:login_activity) do
user
strategy 'password'
success true
failure_reason nil
ip { Faker::Internet.ip_v4_address }
user_agent { Faker::Internet.user_agent }
end

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

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

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

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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save