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
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:

@ -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
# Install Ruby
ENV RUBY_VER="2.7.2"
ENV RUBY_VER="2.7.4"
RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential \
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 'puma', '~> 5.3'
gem 'rails', '~> 6.1.3'
gem 'rails', '~> 6.1.4'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.1'
gem 'rack', '~> 2.2.3'
@ -24,7 +24,7 @@ gem 'paperclip', '~> 6.0'
gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.7'
gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.6.0', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
@ -81,7 +81,7 @@ gem 'sanitize', '~> 5.2'
gem 'scenic', '~> 1.5'
gem 'sidekiq', '~> 6.2'
gem 'sidekiq-scheduler', '~> 3.1'
gem 'sidekiq-unique-jobs', '~> 7.0'
gem 'sidekiq-unique-jobs', '~> 7.1'
gem 'sidekiq-bulk', '~>0.2.0'
gem 'simple-navigation', '~> 4.3'
gem 'simple_form', '~> 5.1'
@ -136,8 +136,8 @@ group :development do
gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.4'
gem 'memory_profiler'
gem 'rubocop', '~> 1.16', require: false
gem 'rubocop-rails', '~> 2.10', require: false
gem 'rubocop', '~> 1.18', require: false
gem 'rubocop-rails', '~> 2.11', require: false
gem 'brakeman', '~> 5.0', require: false
gem 'bundler-audit', '~> 0.8', require: false
@ -157,5 +157,3 @@ gem 'concurrent-ruby', require: false
gem 'connection_pool', require: false
gem 'xorcist', '~> 1.1'
gem 'resolv', '~> 0.1.0'

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

@ -11,7 +11,11 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
before_action :set_cache_headers
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'
end
@ -76,4 +80,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def set_account
@account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
end
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode? || page_requested?
end
end

@ -6,9 +6,9 @@ module Admin
def create
authorize @user, :reset_password?
@user.send_reset_password_instructions
@user.reset_password!
log_action :reset_password, @user
redirect_to admin_accounts_path
redirect_to admin_account_path(@user.account_id)
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!
log_action :disable_2fa, @user
UserMailer.two_factor_disabled(@user).deliver_later!
redirect_to admin_accounts_path
redirect_to admin_account_path(@user.account_id)
end
private

@ -10,6 +10,15 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
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
set_flash_message(:notice, :success, kind: provider_id.capitalize) if is_navigational_format?
else

@ -27,9 +27,11 @@ class Auth::SessionsController < Devise::SessionsController
def create
super do |resource|
resource.update_sign_in!(request, new_sign_in: true)
remember_me(resource)
flash.delete(:notice)
# We only need to call this if this hasn't already been
# called from one of the two-factor or sign-in token
# authentication methods
on_authentication_success(resource, :password) unless @on_authentication_success_called
end
end
@ -44,10 +46,8 @@ class Auth::SessionsController < Devise::SessionsController
def webauthn_options
user = find_user
if user.webauthn_enabled?
options_for_get = WebAuthn::Credential.options_for_get(
allow: user.webauthn_credentials.pluck(:external_id)
)
if user&.webauthn_enabled?
options_for_get = WebAuthn::Credential.options_for_get(allow: user.webauthn_credentials.pluck(:external_id))
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_updated_at)
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

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

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

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

@ -86,7 +86,7 @@ class FollowingAccountsController < ApplicationController
if page_requested? || !@account.user_hides_network?
# Return all fields
else
%i(id type totalItems)
%i(id type total_items)
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
include RoutingHelper
before_action { response.headers['Vary'] = 'Accept' }
before_action :set_account
before_action :check_account_suspension
@ -39,10 +38,12 @@ module WellKnown
end
def bad_request
expires_in(3.minutes, public: true)
head 400
end
def not_found
expires_in(3.minutes, public: true)
head 404
end

@ -84,19 +84,19 @@ module AccountsHelper
def account_description(account)
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),
].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),
].join(' '),
]
unless hide_followers_count?(account)
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),
].join(' ')
end

@ -14,6 +14,17 @@ module ApplicationHelper
ku
).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)
paths.any? { |path| current_page?(path) } ? 'active' : ''
end

@ -22,13 +22,20 @@ export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
* @param {MediaProps} props
* @return {object}
*/
export const deployPictureInPicture = (statusId, accountId, playerType, props) => ({
type: PICTURE_IN_PICTURE_DEPLOY,
statusId,
accountId,
playerType,
props,
});
export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
return (dispatch, getState) => {
// Do not open a player for a toot that does not exist
if (getState().hasIn(['statuses', statusId])) {
dispatch({
type: PICTURE_IN_PICTURE_DEPLOY,
statusId,
accountId,
playerType,
props,
});
}
};
};
/*
* @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)];
let element = e.target;
while (element) {
if (['button', 'video', 'a', 'label', 'canvas'].includes(element.localName)) {
while (element !== e.currentTarget) {
if (['button', 'video', 'a', 'label', 'canvas'].includes(element.localName) || element.getAttribute('role') === 'button') {
return;
}
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(' '))) {
statuses = (
<section>
<section className='search-results__section'>
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
<div className='search-results__info'>
@ -87,7 +87,7 @@ class SearchResults extends ImmutablePureComponent {
if (results.get('accounts') && results.get('accounts').size > 0) {
count += results.get('accounts').size;
accounts = (
<section>
<section className='search-results__section'>
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
{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) {
count += results.get('statuses').size;
statuses = (
<section>
<section className='search-results__section'>
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
{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) {
count += results.get('hashtags').size;
hashtags = (
<section>
<section className='search-results__section'>
<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} />)}
@ -131,11 +131,9 @@ class SearchResults extends ImmutablePureComponent {
<FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} />
</header>
<div className='search-results__contents'>
{accounts}
{statuses}
{hashtags}
</div>
{accounts}
{statuses}
{hashtags}
</div>
);
};

@ -3,7 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
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 { logOut } from 'flavours/glitch/util/log_out';
import { openModal } from 'flavours/glitch/actions/modal';
@ -45,8 +45,9 @@ class LinkFooter extends React.PureComponent {
return (
<div className='getting-started__footer'>
<ul>
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite hocams' /></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='/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>

@ -1,4 +1,5 @@
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 = {
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 };
case PICTURE_IN_PICTURE_REMOVE:
return { ...initialState };
case TIMELINE_DELETE:
return (state.statusId === action.id) ? { ...initialState } : state;
default:
return state;
}

File diff suppressed because one or more lines are too long

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

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

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

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

@ -11,6 +11,24 @@ code {
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 {
&.hidden {
display: none;

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

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

@ -3,7 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
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 { openModal } from 'mastodon/actions/modal';
@ -47,11 +47,12 @@ class LinkFooter extends React.PureComponent {
return (
<div className='getting-started__footer'>
<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>}
<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>
{!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='/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>

@ -14,6 +14,7 @@ export const me = getMeta('me');
export const searchEnabled = getMeta('search_enabled');
export const maxChars = (initialState && initialState.max_toot_chars) || 500;
export const invitesEnabled = getMeta('invites_enabled');
export const limitedFederationMode = getMeta('limited_federation_mode');
export const repository = getMeta('repository');
export const source_url = getMeta('source_url');
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 { TIMELINE_DELETE } from '../actions/timelines';
const initialState = {
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 };
case PICTURE_IN_PICTURE_REMOVE:
return { ...initialState };
case TIMELINE_DELETE:
return (state.statusId === action.id) ? { ...initialState } : state;
default:
return state;
}

@ -10,9 +10,9 @@ $red-bookmark: $warning-red;
// Kafuka Theme Unique Colors. Color names may come later.
$classic-base-color: #0c0c0c;
$classic-primary-color: #a81717; //ee7487;
$classic-primary-color: #e6464b; //a81717; //ee7487;
$classic-secondary-color: #edfcf5;
$classic-highlight-color: #df1717; //e31837;
$classic-highlight-color: #a81717; //df1717; //e31837;
// Variables for defaults in UI
$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);
}
}
&.disabled {
color: $white;
background-color: transparent;
cursor: default;
opacity: 0.4;
}
}
}
}
@ -7284,6 +7291,7 @@ noscript {
&__account {
display: flex;
text-decoration: none;
overflow: hidden;
}
.account__avatar {

@ -11,6 +11,24 @@ code {
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 {
&.hidden {
display: none;

@ -572,7 +572,11 @@ class Account < ApplicationRecord
def create_canonical_email_block!
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
def destroy_canonical_email_block!

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

@ -15,10 +15,10 @@ module LdapAuthenticable
def ldap_get_user(attributes = {})
safe_username = attributes[Devise.ldap_uid.to_sym].first
if Devise.ldap_uid_conversion_enabled
keys = Regexp.union(Devise.ldap_uid_conversion_search.chars)
replacement = Devise.ldap_uid_conversion_replace
safe_username = safe_username.gsub(keys, replacement)
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
target_account_id
by_target_domain
target_origin
).freeze
attr_reader :params
@ -34,8 +35,21 @@ class ReportFilter
Report.where(account_id: value)
when :target_account_id
Report.where(target_account_id: value)
when :target_origin
target_origin_scope(value)
else
raise "Unknown filter: #{key}"
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

@ -42,6 +42,7 @@
# sign_in_token_sent_at :datetime
# webauthn_id :string
# sign_up_ip :inet
# skip_sign_in_token :boolean
#
class User < ApplicationRecord
@ -200,7 +201,7 @@ class User < ApplicationRecord
end
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
def functional?
@ -329,12 +330,32 @@ class User < ApplicationRecord
super
end
def reset_password!(new_password, new_password_confirmation)
def reset_password(new_password, new_password_confirmation)
return false if encrypted_password.blank?
super
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?
setting_display_media == 'show_all'
end

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

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

@ -5,7 +5,8 @@ class REST::InstanceSerializer < ActiveModel::Serializer
attributes :uri, :title, :short_description, :description, :email,
: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
@ -66,6 +67,32 @@ class REST::InstanceSerializer < ActiveModel::Serializer
{ streaming_api: Rails.configuration.x.streaming_api_base_url }
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
[I18n.default_locale]
end

@ -2,7 +2,8 @@
class StatusLengthValidator < ActiveModel::Validator
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)
return unless status.local? && !status.reblog?

@ -16,11 +16,11 @@
.row__information-board
.information-board__section
%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
.information-board__section
%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
.row__mascot
.landing-page__mascot

@ -70,10 +70,10 @@
.hero-widget__counters__wrapper
.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
.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
= t 'about.active_count_after'
%abbr{ title: t('about.active_footnote') } *

@ -15,17 +15,17 @@
.details-counters
.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
%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)
.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
%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)
.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
%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)
.spacer
.public-account-header__tabs__tabs__buttons
@ -36,8 +36,8 @@
.public-account-header__extra__links
= 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)
= 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)

@ -83,6 +83,6 @@
= t('accounts.nothing_here')
- else
%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'

@ -129,6 +129,27 @@
- else
= 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)
%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
%th= t('simple_form.labels.defaults.locale')
@ -221,9 +242,6 @@
%div
- if @account.local?
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
- if @account.user&.otp_required_for_login?
= link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
- if !@account.memorial? && @account.user_approved?
= link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
- else

@ -13,42 +13,42 @@
%div
= link_to admin_accounts_url(local: 1, recent: 1) do
.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'
%div
%div
.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'
%div
%div
.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'
%div
= link_to admin_pending_accounts_path do
.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'
%div
= link_to admin_reports_url do
.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'
%div
= link_to admin_tags_path(pending_review: '1') do
.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'
%div
%div
.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'
%div
= link_to sidekiq_url do
.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__widgets

@ -7,10 +7,10 @@
%tr
%td= account_link_to account
%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
%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
%td.accounts-table__count
- if account.last_status_at.present?

@ -30,4 +30,4 @@
= ' / '
%span.negative-hint
= 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
%li= filter_link_to t('admin.reports.unresolved'), resolved: nil
%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
.fields-group

@ -16,4 +16,4 @@
= fa_icon 'fire fw'
= 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'
%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/
@ -29,3 +31,4 @@
%td
- if current_session.session_id != session.session_id && !current_account.suspended?
= table_link_to 'times', t('sessions.revoke'), settings_session_path(session), method: :delete

@ -39,10 +39,10 @@
.directory__card__extra
.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
.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
.accounts-table__count
- if account.last_status_at.present?

@ -45,7 +45,8 @@
.column-3
%h4= site_hostname
%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}"
.column-4
%h4= t 'footer.more'

@ -9,10 +9,10 @@
= interrelationships_icon(@relationships, account.id)
%td= account_link_to account
%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
%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
%td.accounts-table__count
- if account.last_status_at.present?

@ -28,4 +28,4 @@
- else
%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') }
.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')
- else
= 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?
= link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do
= 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
= 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?

@ -13,9 +13,13 @@ class MoveWorker
queue_follow_unfollows!
end
@deferred_error = nil
copy_account_notes!
carry_blocks_over!
carry_mutes_over!
raise @deferred_error unless @deferred_error.nil?
rescue ActiveRecord::RecordNotFound
true
end
@ -36,6 +40,8 @@ class MoveWorker
@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] }
rescue => e
@deferred_error = e
end
end
@ -47,10 +53,12 @@ class MoveWorker
new_note = AccountNote.find_by(account: note.account, target_account: @target_account)
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
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
rescue => e
@deferred_error = e
end
end
@ -60,6 +68,8 @@ class MoveWorker
BlockService.new.call(block.account, @target_account)
add_account_note_if_needed!(block.account, 'move_handler.carry_blocks_over_text')
end
rescue => e
@deferred_error = e
end
end
@ -67,6 +77,8 @@ class MoveWorker
@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)
add_account_note_if_needed!(mute.account, 'move_handler.carry_mutes_over_text')
rescue => e
@deferred_error = e
end
end

@ -17,6 +17,7 @@ class Scheduler::IpCleanupScheduler
def clean_ip_columns!
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)
LoginActivity.where('created_at < ?', IP_RETENTION_PERIOD.ago).in_batches.destroy_all
end
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_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_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_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.
@ -120,6 +120,7 @@ en:
demote: Demote
destroyed_msg: "%{username}'s data is now queued to be deleted imminently"
disable: Freeze
disable_sign_in_token_auth: Disable e-mail token authentication
disable_two_factor_authentication: Disable 2FA
disabled: Frozen
display_name: Display name
@ -128,6 +129,7 @@ en:
email: Email
email_status: Email status
enable: Unfreeze
enable_sign_in_token_auth: Enable e-mail token authentication
enabled: Enabled
enabled_msg: Successfully unfroze %{username}'s account
followers: Followers
@ -152,7 +154,7 @@ en:
active: Active
all: All
pending: Pending
silenced: Silenced
silenced: Limited
suspended: Suspended
title: Moderation
moderation_notes: Moderation notes
@ -192,8 +194,12 @@ en:
search: Search
search_same_email_domain: Other users with the same e-mail domain
search_same_ip: Other users with the same IP
sensitive: Sensitive
sensitized: marked as sensitive
security_measures:
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
show:
created_reports: Made reports
@ -208,10 +214,10 @@ en:
time_in_queue: Waiting in queue %{time}
title: Accounts
unconfirmed_email: Unconfirmed email
undo_sensitized: Undo sensitive
undo_silenced: Undo silence
undo_sensitized: Undo force-sensitive
undo_silenced: Undo limit
undo_suspension: Undo suspension
unsilenced_msg: Successfully unlimited %{username}'s account
unsilenced_msg: Successfully undid limit of %{username}'s account
unsubscribe: Unsubscribe
unsuspended_msg: Successfully unsuspended %{username}'s account
username: Username
@ -237,14 +243,16 @@ en:
destroy_custom_emoji: Delete Custom Emoji
destroy_domain_allow: Delete Domain Allow
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_status: Delete Post
destroy_unavailable_domain: Delete Unavailable Domain
disable_2fa_user: Disable 2FA
disable_custom_emoji: Disable Custom Emoji
disable_sign_in_token_auth_user: Disable E-mail Token Authentication for User
disable_user: Disable User
enable_custom_emoji: Enable Custom Emoji
enable_sign_in_token_auth_user: Enable E-mail Token Authentication for User
enable_user: Enable User
memorialize_account: Memorialize Account
promote_user: Promote User
@ -252,12 +260,12 @@ en:
reopen_report: Reopen Report
reset_password_user: Reset Password
resolve_report: Resolve Report
sensitive_account: Mark the media in your account as sensitive
silence_account: Silence Account
sensitive_account: Force-Sensitive Account
silence_account: Limit Account
suspend_account: Suspend Account
unassigned_report: Unassign Report
unsensitive_account: Unmark the media in your account as sensitive
unsilence_account: Unsilence Account
unsensitive_account: Undo Force-Sensitive Account
unsilence_account: Undo Limit Account
unsuspend_account: Unsuspend Account
update_announcement: Update Announcement
update_custom_emoji: Update Custom Emoji
@ -286,8 +294,10 @@ en:
destroy_unavailable_domain_html: "%{name} resumed delivery to domain %{target}"
disable_2fa_user_html: "%{name} disabled two factor requirement for user %{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}"
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}"
memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page"
promote_user_html: "%{name} promoted user %{target}"
@ -296,11 +306,11 @@ en:
reset_password_user_html: "%{name} reset password of user %{target}"
resolve_report_html: "%{name} resolved report %{target}"
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"
unassigned_report_html: "%{name} unassigned report %{target}"
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"
update_announcement_html: "%{name} updated announcement %{target}"
update_custom_emoji_html: "%{name} updated emoji %{target}"
@ -422,14 +432,14 @@ en:
rejecting_media: rejecting media files
rejecting_reports: rejecting reports
severity:
silence: silenced
silence: limited
suspend: suspended
show:
affected_accounts:
one: One account in the database affected
other: "%{count} accounts in the database affected"
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
title: Undo domain block for %{domain}
undo: Undo
@ -570,6 +580,7 @@ en:
resolved: Resolved
resolved_msg: Report successfully resolved!
status: Status
target_origin: Origin of reported account
title: Reports
unassign: Unassign
unresolved: Unresolved
@ -1004,10 +1015,21 @@ en:
table:
expires_at: Expires
uses: Uses
title: Invite people
title: Invite hocams
lists:
errors:
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:
validations:
images_and_video: Cannot attach a video to a post that already contains images
@ -1215,6 +1237,7 @@ en:
revoke: Revoke
revoke_success: Session successfully revoked
title: Sessions
view_authentication_history: View authentication history of your account
settings:
account: Account
account_settings: Account settings

@ -1003,7 +1003,7 @@ tr:
table:
expires_at: Bitiş tarihi
uses: Kullanım
title: İnsanları davet et
title: Hocamları davet et
lists:
errors:
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 :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 :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
end

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

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

@ -2,7 +2,7 @@ class CreateCanonicalEmailBlocks < ActiveRecord::Migration[6.1]
def change
create_table :canonical_email_blocks do |t|
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
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.
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
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"
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|
t.bigint "user_id"
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.string "webauthn_id"
t.inet "sign_up_ip"
t.boolean "skip_sign_in_token"
t.index ["account_id"], name: "index_users_on_account_id"
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"
@ -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 "bookmarks", "accounts", 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", "conversations", 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", "lists", 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 "media_attachments", "accounts", name: "fk_96dd81e81b", 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="DB_POOL=25"
Environment="MALLOC_ARENA_MAX=2"
Environment="LD_PRELOAD=libjemalloc.so"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 25
TimeoutSec=15
Restart=always

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

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

@ -54,7 +54,8 @@ module Mastodon
option :email, required: true
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 :force, type: :boolean
desc 'create USERNAME', 'Create a new user'
@ -68,6 +69,9 @@ module Mastodon
With the --role option one of "user", "admin" or "moderator"
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
to a given existing username of an old account. If the old
account is still in use by someone else, you can supply
@ -77,7 +81,7 @@ module Mastodon
def create(username)
account = Account.new(username: username)
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]
account = Account.find_local(username) || Account.new(username: username)
@ -113,7 +117,7 @@ module Mastodon
end
end
option :role
option :role, enum: %w(user moderator admin)
option :email
option :confirm, type: :boolean
option :enable, type: :boolean
@ -121,6 +125,7 @@ module Mastodon
option :disable_2fa, type: :boolean
option :approve, type: :boolean
option :reset_password, type: :boolean
option :skip_sign_in_token, type: :boolean
desc 'modify USERNAME', 'Modify a user'
long_desc <<-LONG_DESC
Modify a user account.
@ -142,6 +147,9 @@ module Mastodon
With the --reset-password option, the user's password is replaced by
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
def modify(username)
user = Account.find_local(username)&.user
@ -163,6 +171,7 @@ module Mastodon
user.disabled = true if options[:disable]
user.approved = true if options[:approve]
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]
if user.save

@ -17,6 +17,7 @@ module Mastodon
option :verbose, type: :boolean, aliases: [:v]
option :dry_run, type: :boolean
option :limited_federation_mode, type: :boolean
option :by_uri, type: :boolean
desc 'purge [DOMAIN...]', 'Remove accounts from a DOMAIN without a trace'
long_desc <<-LONG_DESC
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
from a single domain, all accounts from domains that have not been explicitly allowed
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
def purge(*domains)
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
@ -34,7 +41,11 @@ module Mastodon
if options[:limited_federation_mode]
Account.remote.where.not(domain: DomainAllow.pluck(:domain))
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
say('No domain(s) given', :red)
exit(1)

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

@ -55,6 +55,10 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
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
before do
account.suspend!
@ -96,6 +100,10 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
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
before do
account.suspend!
@ -144,7 +152,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end
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
@ -170,7 +178,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end
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
@ -195,7 +203,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end
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
@ -220,7 +228,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
end
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

@ -16,7 +16,7 @@ describe Admin::ResetsController do
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

@ -15,12 +15,12 @@ describe Admin::TwoFactorAuthenticationsController do
user.update(otp_required_for_login: true)
end
it 'redirects to admin accounts page' do
it 'redirects to admin account page' do
delete :destroy, params: { user_id: user.id }
user.reload
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
@ -38,13 +38,13 @@ describe Admin::TwoFactorAuthenticationsController do
nickname: 'Security Key')
end
it 'redirects to admin accounts page' do
it 'redirects to admin account page' do
delete :destroy, params: { user_id: user.id }
user.reload
expect(user.otp_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

@ -3,7 +3,7 @@ require 'rails_helper'
describe FollowerAccountsController do
render_views
let(:alice) { Fabricate(:account, username: 'alice') }
let(:alice) { Fabricate(:user).account }
let(:follower0) { Fabricate(:account) }
let(:follower1) { Fabricate(:account) }
@ -101,6 +101,23 @@ describe FollowerAccountsController do
expect(body['partOf']).to be_blank
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
before do
alice.suspend!

@ -3,7 +3,7 @@ require 'rails_helper'
describe FollowingAccountsController do
render_views
let(:alice) { Fabricate(:account, username: 'alice') }
let(:alice) { Fabricate(:user).account }
let(:followee0) { Fabricate(:account) }
let(:followee1) { Fabricate(:account) }
@ -101,6 +101,23 @@ describe FollowingAccountsController do
expect(body['partOf']).to be_blank
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
before do
alice.suspend!

@ -24,6 +24,10 @@ describe WellKnown::WebfingerController, type: :controller do
expect(response).to have_http_status(200)
end
it 'does not set a Vary header' do
expect(response.headers['Vary']).to be_nil
end
it 'returns application/jrd+json' do
expect(response.media_type).to eq 'application/jrd+json'
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
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)
expect(results).to include status1
expect(results).to_not include status2

@ -344,6 +344,34 @@ RSpec.describe User, type: :model do
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
subject(:user) { Fabricate(:user, confirmed_at: confirmed_at) }

@ -1,4 +1,37 @@
require 'rails_helper'
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

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

Loading…
Cancel
Save