diff --git a/Gemfile b/Gemfile index 4ca62d76b4d..c79db13be0f 100644 --- a/Gemfile +++ b/Gemfile @@ -5,10 +5,10 @@ ruby '>= 2.5.0', '< 3.0.0' gem 'pkg-config', '~> 1.4' -gem 'puma', '~> 4.3' -gem 'rails', '~> 5.2.4.3' +gem 'puma', '~> 5.0' +gem 'rails', '~> 5.2.4.4' gem 'sprockets', '~> 3.7.2' -gem 'thor', '~> 0.20' +gem 'thor', '~> 1.0' gem 'rack', '~> 2.2.3' gem 'thwait', '~> 0.2.0' @@ -20,7 +20,7 @@ gem 'makara', '~> 0.4' gem 'pghero', '~> 2.7' gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.79', require: false +gem 'aws-sdk-s3', '~> 1.81', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' @@ -123,26 +123,26 @@ end group :test do gem 'capybara', '~> 3.33' gem 'climate_control', '~> 0.2' - gem 'faker', '~> 2.13' + gem 'faker', '~> 2.14' gem 'microformats', '~> 4.2' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.1' gem 'simplecov', '~> 0.19', require: false - gem 'webmock', '~> 3.8' - gem 'parallel_tests', '~> 3.2' + gem 'webmock', '~> 3.9' + gem 'parallel_tests', '~> 3.3' gem 'rspec_junit_formatter', '~> 0.4' end group :development do gem 'active_record_query_trace', '~> 1.7' gem 'annotate', '~> 3.1' - gem 'better_errors', '~> 2.7' + gem 'better_errors', '~> 2.8' gem 'binding_of_caller', '~> 0.7' gem 'bullet', '~> 6.1' gem 'letter_opener', '~> 1.7' gem 'letter_opener_web', '~> 1.4' gem 'memory_profiler' - gem 'rubocop', '~> 0.90', require: false + gem 'rubocop', '~> 0.91', require: false gem 'rubocop-rails', '~> 2.8', require: false gem 'brakeman', '~> 4.9', require: false gem 'bundler-audit', '~> 0.7', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 581d494ab6d..6f9c6a6f901 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,25 +16,25 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (5.2.4.3) - actionpack (= 5.2.4.3) + actioncable (5.2.4.4) + actionpack (= 5.2.4.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.4.3) - actionpack (= 5.2.4.3) - actionview (= 5.2.4.3) - activejob (= 5.2.4.3) + actionmailer (5.2.4.4) + actionpack (= 5.2.4.4) + actionview (= 5.2.4.4) + activejob (= 5.2.4.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.4.3) - actionview (= 5.2.4.3) - activesupport (= 5.2.4.3) + actionpack (5.2.4.4) + actionview (= 5.2.4.4) + activesupport (= 5.2.4.4) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.4.3) - activesupport (= 5.2.4.3) + actionview (5.2.4.4) + activesupport (= 5.2.4.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -45,20 +45,20 @@ GEM case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) active_record_query_trace (1.7) - activejob (5.2.4.3) - activesupport (= 5.2.4.3) + activejob (5.2.4.4) + activesupport (= 5.2.4.4) globalid (>= 0.3.6) - activemodel (5.2.4.3) - activesupport (= 5.2.4.3) - activerecord (5.2.4.3) - activemodel (= 5.2.4.3) - activesupport (= 5.2.4.3) + activemodel (5.2.4.4) + activesupport (= 5.2.4.4) + activerecord (5.2.4.4) + activemodel (= 5.2.4.4) + activesupport (= 5.2.4.4) arel (>= 9.0) - activestorage (5.2.4.3) - actionpack (= 5.2.4.3) - activerecord (= 5.2.4.3) + activestorage (5.2.4.4) + actionpack (= 5.2.4.4) + activerecord (= 5.2.4.4) marcel (~> 0.3.1) - activesupport (5.2.4.3) + activesupport (5.2.4.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -79,23 +79,23 @@ GEM cocaine (~> 0.5.3) awrence (1.1.1) aws-eventstream (1.1.0) - aws-partitions (1.365.0) - aws-sdk-core (3.105.0) + aws-partitions (1.373.0) + aws-sdk-core (3.107.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.37.0) + aws-sdk-kms (1.38.0) aws-sdk-core (~> 3, >= 3.99.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.79.1) + aws-sdk-s3 (1.81.0) aws-sdk-core (~> 3, >= 3.104.3) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) aws-sigv4 (1.2.2) aws-eventstream (~> 1, >= 1.0.2) bcrypt (3.1.16) - better_errors (2.7.1) + better_errors (2.8.1) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -160,13 +160,12 @@ GEM cose (1.0.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 0.4.0) - crack (0.4.3) - safe_yaml (~> 1.0.0) + crack (0.4.4) crass (1.0.6) css_parser (1.7.1) addressable debug_inspector (0.0.3) - devise (4.7.2) + devise (4.7.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -210,7 +209,7 @@ GEM tzinfo excon (0.76.0) fabrication (2.21.1) - faker (2.13.0) + faker (2.14.0) i18n (>= 1.6, < 2) faraday (1.0.1) multipart-post (>= 1.2, < 3) @@ -233,7 +232,7 @@ GEM fog-json (>= 1.0) ipaddress (>= 0.8) formatador (0.2.5) - fugit (1.3.8) + fugit (1.3.9) et-orbi (~> 1.1, >= 1.1.8) raabro (~> 1.3) fuubar (2.5.0) @@ -363,7 +362,7 @@ GEM net-scp (3.0.0) net-ssh (>= 2.6.5, < 7.0.0) net-ssh (6.1.0) - nio4r (2.5.3) + nio4r (2.5.4) nokogiri (1.10.10) mini_portile2 (~> 2.4.0) nokogumbo (2.0.2) @@ -387,7 +386,7 @@ GEM openssl (2.2.0) openssl-signature_algorithm (0.4.0) orm_adapter (0.5.0) - ox (2.13.3) + ox (2.13.4) paperclip (6.0.0) activemodel (>= 4.2.0) activesupport (>= 4.2.0) @@ -398,7 +397,7 @@ GEM av (~> 0.9.0) paperclip (>= 2.5.2) parallel (1.19.2) - parallel_tests (3.2.0) + parallel_tests (3.3.0) parallel parser (2.7.1.4) ast (~> 2.4.1) @@ -406,11 +405,11 @@ GEM pastel (0.8.0) tty-color (~> 0.5) pg (1.2.3) - pghero (2.7.0) + pghero (2.7.2) activerecord (>= 5) - pkg-config (1.4.2) + pkg-config (1.4.3) posix-spawn (0.3.15) - premailer (1.13.1) + premailer (1.14.2) addressable css_parser (>= 1.6.0) htmlentities (>= 4.0.0) @@ -427,7 +426,7 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.6) - puma (4.3.6) + puma (5.0.0) nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) @@ -441,18 +440,18 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (5.2.4.3) - actioncable (= 5.2.4.3) - actionmailer (= 5.2.4.3) - actionpack (= 5.2.4.3) - actionview (= 5.2.4.3) - activejob (= 5.2.4.3) - activemodel (= 5.2.4.3) - activerecord (= 5.2.4.3) - activestorage (= 5.2.4.3) - activesupport (= 5.2.4.3) + rails (5.2.4.4) + actioncable (= 5.2.4.4) + actionmailer (= 5.2.4.4) + actionpack (= 5.2.4.4) + actionview (= 5.2.4.4) + activejob (= 5.2.4.4) + activemodel (= 5.2.4.4) + activerecord (= 5.2.4.4) + activestorage (= 5.2.4.4) + activesupport (= 5.2.4.4) bundler (>= 1.3.0) - railties (= 5.2.4.3) + railties (= 5.2.4.4) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -468,9 +467,9 @@ GEM railties (>= 5.0, < 6) rails-settings-cached (0.6.6) rails (>= 4.2.0) - railties (5.2.4.3) - actionpack (= 5.2.4.3) - activesupport (= 5.2.4.3) + railties (5.2.4.4) + actionpack (= 5.2.4.4) + activesupport (= 5.2.4.4) method_source rake (>= 0.8.7) thor (>= 0.19.0, < 2.0) @@ -482,7 +481,7 @@ GEM rdf-normalize (0.4.0) rdf (~> 3.1) redcarpet (3.5.0) - redis (4.2.1) + redis (4.2.2) redis-actionpack (5.2.0) actionpack (>= 5, < 7) redis-rack (>= 2.1.0, < 3) @@ -501,7 +500,7 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.9.0) redis (>= 4, < 5) - regexp_parser (1.7.1) + regexp_parser (1.8.0) request_store (1.5.0) rack (>= 1.4) responders (3.0.1) @@ -536,18 +535,18 @@ GEM rspec-support (3.9.3) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (0.90.0) + rubocop (0.91.0) parallel (~> 1.10) parser (>= 2.7.1.1) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.7) rexml - rubocop-ast (>= 0.3.0, < 1.0) + rubocop-ast (>= 0.4.0, < 1.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 2.0) - rubocop-ast (0.3.0) + rubocop-ast (0.4.2) parser (>= 2.7.1.4) - rubocop-rails (2.8.0) + rubocop-rails (2.8.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 0.87.0) @@ -556,7 +555,6 @@ GEM nokogiri (>= 1.5.10) rufus-scheduler (3.6.0) fugit (~> 1.1, >= 1.1.6) - safe_yaml (1.0.5) safety_net_attestation (0.4.0) jwt (~> 2.0) sanitize (5.2.1) @@ -565,7 +563,7 @@ GEM nokogumbo (~> 2.0) securecompare (1.0.0) semantic_range (2.3.0) - sidekiq (6.1.1) + sidekiq (6.1.2) connection_pool (>= 2.2.2) rack (~> 2.0) redis (>= 4.2.0) @@ -594,7 +592,7 @@ GEM sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.1) + sprockets-rails (3.2.2) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) @@ -613,7 +611,7 @@ GEM unicode-display_width (~> 1.1, >= 1.1.1) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) - thor (0.20.3) + thor (1.0.1) thread_safe (0.3.6) thwait (0.2.0) e2mmap @@ -654,7 +652,7 @@ GEM safety_net_attestation (~> 0.4.0) securecompare (~> 1.0) tpm-key_attestation (~> 0.9.0) - webmock (3.8.3) + webmock (3.9.1) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -681,8 +679,8 @@ DEPENDENCIES active_record_query_trace (~> 1.7) addressable (~> 2.7) annotate (~> 3.1) - aws-sdk-s3 (~> 1.79) - better_errors (~> 2.7) + aws-sdk-s3 (~> 1.81) + better_errors (~> 2.8) binding_of_caller (~> 0.7) blurhash (~> 0.1) bootsnap (~> 1.4) @@ -711,7 +709,7 @@ DEPENDENCIES e2mmap (~> 0.1.0) ed25519 (~> 1.2) fabrication (~> 2.21) - faker (~> 2.13) + faker (~> 2.14) fast_blank (~> 1.0) fastimage fog-core (<= 2.1.0) @@ -752,7 +750,7 @@ DEPENDENCIES paperclip (~> 6.0) paperclip-av-transcoder (~> 0.6) parallel (~> 1.19) - parallel_tests (~> 3.2) + parallel_tests (~> 3.3) parslet pg (~> 1.2) pghero (~> 2.7) @@ -762,12 +760,12 @@ DEPENDENCIES private_address_check (~> 0.5) pry-byebug (~> 3.9) pry-rails (~> 0.3) - puma (~> 4.3) + puma (~> 5.0) pundit (~> 2.1) rack (~> 2.2.3) rack-attack (~> 6.3) rack-cors (~> 1.1) - rails (~> 5.2.4.3) + rails (~> 5.2.4.4) rails-controller-testing (~> 1.0) rails-i18n (~> 5.1) rails-settings-cached (~> 0.6) @@ -780,7 +778,7 @@ DEPENDENCIES rspec-rails (~> 4.0) rspec-sidekiq (~> 3.1) rspec_junit_formatter (~> 0.4) - rubocop (~> 0.90) + rubocop (~> 0.91) rubocop-rails (~> 2.8) ruby-progressbar (~> 1.10) sanitize (~> 5.2) @@ -797,12 +795,12 @@ DEPENDENCIES stoplight (~> 2.2.1) streamio-ffmpeg (~> 3.0) strong_migrations (~> 0.7) - thor (~> 0.20) + thor (~> 1.0) thwait (~> 0.2.0) tty-prompt (~> 0.22) twitter-text (~> 1.14) tzinfo-data (~> 1.2020) webauthn (~> 3.0.0.alpha1) - webmock (~> 3.8) + webmock (~> 3.9) webpacker (~> 5.2) webpush diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 54106933c74..3565427672b 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -7,6 +7,7 @@ class AccountsController < ApplicationController include AccountControllerConcern include SignatureAuthentication + before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers before_action :set_body_classes @@ -49,7 +50,7 @@ class AccountsController < ApplicationController format.json do expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?) - render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to + render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter end end end @@ -154,12 +155,4 @@ class AccountsController < ApplicationController def params_slice(*keys) params.slice(*keys).permit(*keys) end - - def restrict_fields_to - if signed_request_account.present? || public_fetch_mode? - # Return all fields - else - %i(id type preferred_username inbox public_key endpoints) - end - end end diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index e066860bfed..5fd735ad6af 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -57,9 +57,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def set_statuses return unless page_requested? - @statuses = @account.statuses.permitted_for(@account, signed_request_account) @statuses = cache_collection_paginated_by_id( - @statuses, + @account.statuses.permitted_for(@account, signed_request_account), Status, LIMIT, params_slice(:max_id, :min_id, :since_id) diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 7b178354299..b9b75727dd9 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -2,7 +2,7 @@ module Admin class AccountsController < BaseController - before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject] + before_action :set_account, except: [:index] before_action :require_remote_account!, only: [:redownload] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] @@ -14,49 +14,58 @@ module Admin def show authorize @account, :show? + @deletion_request = @account.deletion_request @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account) @moderation_notes = @account.targeted_moderation_notes.latest @warnings = @account.targeted_account_warnings.latest.custom + @domain_block = DomainBlock.rule_for(@account.domain) end def memorialize authorize @account, :memorialize? @account.memorialize! log_action :memorialize, @account - redirect_to admin_account_path(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.memorialized_msg', username: @account.acct) end def enable authorize @account.user, :enable? @account.user.enable! log_action :enable, @account.user - redirect_to admin_account_path(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.enabled_msg', username: @account.acct) end def approve authorize @account.user, :approve? @account.user.approve! - redirect_to admin_pending_accounts_path + redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct) end def reject authorize @account.user, :reject? - SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false) - redirect_to admin_pending_accounts_path + DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false) + redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct) + end + + def destroy + authorize @account, :destroy? + Admin::AccountDeletionWorker.perform_async(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.destroyed_msg', username: @account.acct) end def unsilence authorize @account, :unsilence? @account.unsilence! log_action :unsilence, @account - redirect_to admin_account_path(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsilenced_msg', username: @account.acct) end def unsuspend authorize @account, :unsuspend? @account.unsuspend! + Admin::UnsuspensionWorker.perform_async(@account.id) log_action :unsuspend, @account - redirect_to admin_account_path(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsuspended_msg', username: @account.acct) end def redownload @@ -65,7 +74,7 @@ module Admin @account.update!(last_webfingered_at: nil) ResolveAccountService.new.call(@account) - redirect_to admin_account_path(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.redownloaded_msg', username: @account.acct) end def remove_avatar @@ -76,7 +85,7 @@ module Admin log_action :remove_avatar, @account.user - redirect_to admin_account_path(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_avatar_msg', username: @account.acct) end def remove_header @@ -87,7 +96,7 @@ module Admin log_action :remove_header, @account.user - redirect_to admin_account_path(@account.id) + redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct) end private diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 4672255475f..e962c4e97f6 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -96,12 +96,12 @@ class Api::BaseController < ApplicationController def require_user! if !current_user render json: { error: 'This method requires an authenticated user' }, status: 422 - elsif current_user.disabled? - render json: { error: 'Your login is currently disabled' }, status: 403 elsif !current_user.confirmed? render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403 elsif !current_user.approved? render json: { error: 'Your login is currently pending approval' }, status: 403 + elsif !current_user.functional? + render json: { error: 'Your login is currently disabled' }, status: 403 else set_user_activity end diff --git a/app/controllers/api/v1/accounts/featured_tags_controller.rb b/app/controllers/api/v1/accounts/featured_tags_controller.rb index d6277261d45..014d7195671 100644 --- a/app/controllers/api/v1/accounts/featured_tags_controller.rb +++ b/app/controllers/api/v1/accounts/featured_tags_controller.rb @@ -17,6 +17,6 @@ class Api::V1::Accounts::FeaturedTagsController < Api::BaseController end def set_featured_tags - @featured_tags = @account.featured_tags + @featured_tags = @account.suspended? ? @account.featured_tags : [] end end diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 2277067c9f4..a665863ebf4 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController end def hide_results? - (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) + @account.suspended? || (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) end def default_accounts diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 93d4bd3a4a1..7d885a212f2 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController end def hide_results? - (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) + @account.suspended? || (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) end def default_accounts diff --git a/app/controllers/api/v1/accounts/identity_proofs_controller.rb b/app/controllers/api/v1/accounts/identity_proofs_controller.rb index 8dad6fee962..4b5f6902c7d 100644 --- a/app/controllers/api/v1/accounts/identity_proofs_controller.rb +++ b/app/controllers/api/v1/accounts/identity_proofs_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController before_action :set_account def index - @proofs = @account.identity_proofs.active + @proofs = @account.suspended? ? [] : @account.identity_proofs.active render json: @proofs, each_serializer: REST::IdentityProofSerializer end diff --git a/app/controllers/api/v1/accounts/lists_controller.rb b/app/controllers/api/v1/accounts/lists_controller.rb index ccb751f8f7d..c92f1f8a08d 100644 --- a/app/controllers/api/v1/accounts/lists_controller.rb +++ b/app/controllers/api/v1/accounts/lists_controller.rb @@ -6,7 +6,7 @@ class Api::V1::Accounts::ListsController < Api::BaseController before_action :set_account def index - @lists = @account.lists.where(account: current_account) + @lists = @account.suspended? ? [] : @account.lists.where(account: current_account) render json: @lists, each_serializer: REST::ListSerializer end diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index 1d3992a2857..503f85c97d7 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController before_action :require_user! def index - accounts = Account.where(id: account_ids).select('id') + accounts = Account.without_suspended.where(id: account_ids).select('id') # .where doesn't guarantee that our results are in the same order # we requested them, so return the "right" order to the requestor. @accounts = accounts.index_by(&:id).values_at(*account_ids).compact diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 85a9133e3ac..92ccb80615d 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -18,7 +18,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def load_statuses - cached_account_statuses + @account.suspended? ? [] : cached_account_statuses end def cached_account_statuses diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 0080faf3307..aef51a6479a 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -9,7 +9,6 @@ class Api::V1::AccountsController < Api::BaseController before_action :require_user!, except: [:show, :create] before_action :set_account, except: [:create] - before_action :check_account_suspension, only: [:show] before_action :check_enabled_registrations, only: [:create] skip_before_action :require_authenticated_user!, only: :create @@ -31,9 +30,8 @@ class Api::V1::AccountsController < Api::BaseController end def follow - FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true) - - options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } } + follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, with_rate_limit: true) + options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) end @@ -73,10 +71,6 @@ class Api::V1::AccountsController < Api::BaseController AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options) end - def check_account_suspension - gone if @account.suspended? - end - def account_params params.permit(:username, :email, :password, :agreement, :locale, :reason) end diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb index 24c7fbef12f..3af572f25ef 100644 --- a/app/controllers/api/v1/admin/accounts_controller.rb +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -58,7 +58,13 @@ class Api::V1::Admin::AccountsController < Api::BaseController def reject authorize @account.user, :reject? - SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false) + DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false) + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def destroy + authorize @account, :destroy? + Admin::AccountDeletionWorker.perform_async(@account.id) render json: @account, serializer: REST::Admin::AccountSerializer end @@ -72,6 +78,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController def unsuspend authorize @account, :unsuspend? @account.unsuspend! + Admin::UnsuspensionWorker.perform_async(@account.id) log_action :unsuspend, @account render json: @account, serializer: REST::Admin::AccountSerializer end diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb index a2baeef900c..586cdfca9d4 100644 --- a/app/controllers/api/v1/blocks_controller.rb +++ b/app/controllers/api/v1/blocks_controller.rb @@ -18,6 +18,8 @@ class Api::V1::BlocksController < Api::BaseController def paginated_blocks @paginated_blocks ||= Block.eager_load(target_account: :account_stat) + .joins(:target_account) + .merge(Account.without_suspended) .where(account: current_account) .paginate_by_max_id( limit_param(DEFAULT_ACCOUNTS_LIMIT), diff --git a/app/controllers/api/v1/endorsements_controller.rb b/app/controllers/api/v1/endorsements_controller.rb index c87dbc4ce83..9e80f468a78 100644 --- a/app/controllers/api/v1/endorsements_controller.rb +++ b/app/controllers/api/v1/endorsements_controller.rb @@ -25,7 +25,7 @@ class Api::V1::EndorsementsController < Api::BaseController end def endorsed_accounts - current_account.endorsed_accounts.includes(:account_stat) + current_account.endorsed_accounts.includes(:account_stat).without_suspended end def insert_pagination_headers diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index 0ee6e531f07..b34c76f29e9 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -13,7 +13,7 @@ class Api::V1::FollowRequestsController < Api::BaseController def authorize AuthorizeFollowService.new.call(account, current_account) - NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account)) + NotifyService.new.call(current_account, :follow, Follow.find_by(account: account, target_account: current_account)) render json: account, serializer: REST::RelationshipSerializer, relationships: relationships end @@ -37,7 +37,7 @@ class Api::V1::FollowRequestsController < Api::BaseController end def default_accounts - Account.includes(:follow_requests, :account_stat).references(:follow_requests) + Account.without_suspended.includes(:follow_requests, :account_stat).references(:follow_requests) end def paginated_follow_requests diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb index 23078263e7a..b66ea9bfe60 100644 --- a/app/controllers/api/v1/lists/accounts_controller.rb +++ b/app/controllers/api/v1/lists/accounts_controller.rb @@ -37,9 +37,9 @@ class Api::V1::Lists::AccountsController < Api::BaseController def load_accounts if unlimited? - @list.accounts.includes(:account_stat).all + @list.accounts.without_suspended.includes(:account_stat).all else - @list.accounts.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) + @list.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) end end diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index 5dc047b43d3..a89f3d7002c 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -27,6 +27,8 @@ class Api::V1::MutesController < Api::BaseController def paginated_mutes @paginated_mutes ||= Mute.eager_load(:target_account) + .joins(:target_account) + .merge(Account.without_suspended) .where(account: current_account) .paginate_by_max_id( limit_param(DEFAULT_ACCOUNTS_LIMIT), diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 9ff1683673d..fda34826510 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -14,7 +14,7 @@ class Api::V1::NotificationsController < Api::BaseController end def show - @notification = current_account.notifications.find(params[:id]) + @notification = current_account.notifications.without_suspended.find(params[:id]) render json: @notification, serializer: REST::NotificationSerializer end @@ -49,7 +49,7 @@ class Api::V1::NotificationsController < Api::BaseController end def browserable_account_notifications - current_account.notifications.browserable(exclude_types, from_account) + current_account.notifications.without_suspended.browserable(exclude_types, from_account) end def target_statuses_from_notifications diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index d34b333eb3a..0918c61e972 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -52,6 +52,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController def data_params return {} if params[:data].blank? - params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll]) + params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status]) end end diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index 8229786d6cc..2b614a83756 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -22,6 +22,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController def default_accounts Account + .without_suspended .includes(:favourites, :account_stat) .references(:favourites) .where(favourites: { status_id: @status.id }) diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index 6c9e49d903a..24db30fcc01 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -21,7 +21,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController end def default_accounts - Account.includes(:statuses, :account_stat).references(:statuses) + Account.without_suspended.includes(:statuses, :account_stat).references(:statuses) end def paginated_statuses diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index 7916b82fa08..1dce3e70f2a 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -22,6 +22,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController reblog: alerts_enabled, mention: alerts_enabled, poll: alerts_enabled, + status: alerts_enabled, }, } @@ -57,6 +58,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController end def data_params - @data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll]) + @data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status]) end end diff --git a/app/controllers/concerns/export_controller_concern.rb b/app/controllers/concerns/export_controller_concern.rb index bfe990c827d..24cfc7a0124 100644 --- a/app/controllers/concerns/export_controller_concern.rb +++ b/app/controllers/concerns/export_controller_concern.rb @@ -5,7 +5,6 @@ module ExportControllerConcern included do before_action :authenticate_user! - before_action :require_not_suspended! before_action :load_export skip_before_action :require_functional! @@ -30,8 +29,4 @@ module ExportControllerConcern def export_filename "#{controller_name}.csv" end - - def require_not_suspended! - forbidden if current_account.suspended? - end end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index c5ccece1347..b2564a7915a 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -6,6 +6,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :store_current_location before_action :authenticate_resource_owner! before_action :set_pack + before_action :require_not_suspended!, only: :destroy before_action :set_body_classes skip_before_action :require_functional! @@ -30,4 +31,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio def set_pack use_pack 'settings' end + + def require_not_suspended! + forbidden if current_account.suspended? + end end diff --git a/app/controllers/settings/aliases_controller.rb b/app/controllers/settings/aliases_controller.rb index b7c9a409d1f..a421b8ede31 100644 --- a/app/controllers/settings/aliases_controller.rb +++ b/app/controllers/settings/aliases_controller.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true class Settings::AliasesController < Settings::BaseController - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! + before_action :require_not_suspended! before_action :set_aliases, except: :destroy before_action :set_alias, only: :destroy diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index ed3f82a8e06..d3ac268d862 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::ApplicationsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :set_application, only: [:show, :update, :destroy, :regenerate] before_action :prepare_scopes, only: [:create, :update] diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index b97603af6f2..dee3922d801 100644 --- a/app/controllers/settings/base_controller.rb +++ b/app/controllers/settings/base_controller.rb @@ -2,6 +2,9 @@ class Settings::BaseController < ApplicationController before_action :set_pack + layout 'admin' + + before_action :authenticate_user! before_action :set_body_classes before_action :set_cache_headers @@ -18,4 +21,8 @@ class Settings::BaseController < ApplicationController def set_cache_headers response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate' end + + def require_not_suspended! + forbidden if current_account.suspended? + end end diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index 15a59c999df..f96c83b8025 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -1,14 +1,11 @@ # frozen_string_literal: true class Settings::DeletesController < Settings::BaseController - layout 'admin' - - before_action :check_enabled_deletion - before_action :authenticate_user! - before_action :require_not_suspended! - skip_before_action :require_functional! + before_action :require_not_suspended! + before_action :check_enabled_deletion + def show @confirmation = Form::DeleteConfirmation.new end @@ -46,7 +43,7 @@ class Settings::DeletesController < Settings::BaseController def destroy_account! current_account.suspend! - Admin::SuspensionWorker.perform_async(current_user.account_id, true) + AccountDeletionWorker.perform_async(current_user.account_id) sign_out end end diff --git a/app/controllers/settings/exports/blocked_accounts_controller.rb b/app/controllers/settings/exports/blocked_accounts_controller.rb index 2092104e01d..2190caa3619 100644 --- a/app/controllers/settings/exports/blocked_accounts_controller.rb +++ b/app/controllers/settings/exports/blocked_accounts_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class BlockedAccountsController < ApplicationController + class BlockedAccountsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports/blocked_domains_controller.rb b/app/controllers/settings/exports/blocked_domains_controller.rb index 6676ce34014..bee4b2431e3 100644 --- a/app/controllers/settings/exports/blocked_domains_controller.rb +++ b/app/controllers/settings/exports/blocked_domains_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class BlockedDomainsController < ApplicationController + class BlockedDomainsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports/following_accounts_controller.rb b/app/controllers/settings/exports/following_accounts_controller.rb index 74281ddca26..acefcb15da6 100644 --- a/app/controllers/settings/exports/following_accounts_controller.rb +++ b/app/controllers/settings/exports/following_accounts_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class FollowingAccountsController < ApplicationController + class FollowingAccountsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports/lists_controller.rb b/app/controllers/settings/exports/lists_controller.rb index cf5a9de44b3..bc65f56a0ef 100644 --- a/app/controllers/settings/exports/lists_controller.rb +++ b/app/controllers/settings/exports/lists_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class ListsController < ApplicationController + class ListsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports/muted_accounts_controller.rb b/app/controllers/settings/exports/muted_accounts_controller.rb index e511619ca6a..50b7bf1f791 100644 --- a/app/controllers/settings/exports/muted_accounts_controller.rb +++ b/app/controllers/settings/exports/muted_accounts_controller.rb @@ -2,7 +2,7 @@ module Settings module Exports - class MutedAccountsController < ApplicationController + class MutedAccountsController < BaseController include ExportControllerConcern def index diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 0e93d07a9b2..30138d29ed6 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -3,11 +3,6 @@ class Settings::ExportsController < Settings::BaseController include Authorization - layout 'admin' - - before_action :authenticate_user! - before_action :require_not_suspended! - skip_before_action :require_functional! def show @@ -16,8 +11,6 @@ class Settings::ExportsController < Settings::BaseController end def create - raise Mastodon::NotPermittedError unless user_signed_in? - backup = nil RedisLock.acquire(lock_options) do |lock| @@ -37,8 +30,4 @@ class Settings::ExportsController < Settings::BaseController def lock_options { redis: Redis.current, key: "backup:#{current_user.id}" } end - - def require_not_suspended! - forbidden if current_account.suspended? - end end diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb index e9861da56cf..e805527d07c 100644 --- a/app/controllers/settings/featured_tags_controller.rb +++ b/app/controllers/settings/featured_tags_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::FeaturedTagsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :set_featured_tags, only: :index before_action :set_featured_tag, except: [:index, :create] before_action :set_recently_used_tags, only: :index diff --git a/app/controllers/settings/identity_proofs_controller.rb b/app/controllers/settings/identity_proofs_controller.rb index b217b3c3bec..4618c78836b 100644 --- a/app/controllers/settings/identity_proofs_controller.rb +++ b/app/controllers/settings/identity_proofs_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::IdentityProofsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :check_required_params, only: :new before_action :check_enabled, only: :new diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index 7b8c4ae2350..d4516526eee 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::ImportsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :set_account def show diff --git a/app/controllers/settings/migration/redirects_controller.rb b/app/controllers/settings/migration/redirects_controller.rb index 97193ade02d..6d469f3842c 100644 --- a/app/controllers/settings/migration/redirects_controller.rb +++ b/app/controllers/settings/migration/redirects_controller.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true class Settings::Migration::RedirectsController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! - before_action :require_not_suspended! - skip_before_action :require_functional! + before_action :require_not_suspended! + def new @redirect = Form::Redirect.new end @@ -38,8 +35,4 @@ class Settings::Migration::RedirectsController < Settings::BaseController def resource_params params.require(:form_redirect).permit(:acct, :current_password, :current_username) end - - def require_not_suspended! - forbidden if current_account.suspended? - end end diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb index 68304bb513f..62603aba819 100644 --- a/app/controllers/settings/migrations_controller.rb +++ b/app/controllers/settings/migrations_controller.rb @@ -1,15 +1,12 @@ # frozen_string_literal: true class Settings::MigrationsController < Settings::BaseController - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! before_action :require_not_suspended! before_action :set_migrations before_action :set_cooldown - skip_before_action :require_functional! - def show @migration = current_account.migrations.build end @@ -44,8 +41,4 @@ class Settings::MigrationsController < Settings::BaseController def on_cooldown? @cooldown.present? end - - def require_not_suspended! - forbidden if current_account.suspended? - end end diff --git a/app/controllers/settings/pictures_controller.rb b/app/controllers/settings/pictures_controller.rb index df2a6eed3ec..28df65f8ffa 100644 --- a/app/controllers/settings/pictures_controller.rb +++ b/app/controllers/settings/pictures_controller.rb @@ -2,7 +2,6 @@ module Settings class PicturesController < BaseController - before_action :authenticate_user! before_action :set_account before_action :set_picture diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 75c3e2495d0..87431f8cf2d 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true class Settings::PreferencesController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! - def show; end def update diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 19a7ce157f0..0c15447a6c1 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true class Settings::ProfilesController < Settings::BaseController - layout 'admin' - - before_action :authenticate_user! before_action :set_account def show diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb index f8fb4036ef0..ee2fc5dc80f 100644 --- a/app/controllers/settings/sessions_controller.rb +++ b/app/controllers/settings/sessions_controller.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -# Intentionally does not inherit from BaseController -class Settings::SessionsController < ApplicationController - before_action :authenticate_user! - before_action :set_session, only: :destroy - +class Settings::SessionsController < Settings::BaseController skip_before_action :require_functional! + before_action :require_not_suspended! + before_action :set_session, only: :destroy + def destroy @session.destroy! flash[:notice] = I18n.t('sessions.revoke_success') diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb index 9f23011a7d4..1a0afe58b05 100644 --- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb +++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb @@ -5,14 +5,11 @@ module Settings class ConfirmationsController < BaseController include ChallengableConcern - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! before_action :require_challenge! before_action :ensure_otp_secret - skip_before_action :require_functional! - def new prepare_two_factor_form end diff --git a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb index 6836f7ef622..cbba842a980 100644 --- a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb +++ b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb @@ -5,14 +5,11 @@ module Settings class OtpAuthenticationController < BaseController include ChallengableConcern - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! before_action :verify_otp_not_enabled, only: [:show] before_action :require_challenge!, only: [:create] - skip_before_action :require_functional! - def show @confirmation = Form::TwoFactorConfirmation.new end diff --git a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb index 0c4f5bff762..6ec53224d38 100644 --- a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb +++ b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb @@ -5,13 +5,10 @@ module Settings class RecoveryCodesController < BaseController include ChallengableConcern - layout 'admin' - - before_action :authenticate_user! - before_action :require_challenge!, on: :create - skip_before_action :require_functional! + before_action :require_challenge!, on: :create + def create @recovery_codes = current_user.generate_otp_backup_codes! current_user.save! diff --git a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb index ee539278519..bd6f83134c3 100644 --- a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb +++ b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb @@ -3,9 +3,8 @@ module Settings module TwoFactorAuthentication class WebauthnCredentialsController < BaseController - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! before_action :require_otp_enabled before_action :require_webauthn_enabled, only: [:index, :destroy] diff --git a/app/controllers/settings/two_factor_authentication_methods_controller.rb b/app/controllers/settings/two_factor_authentication_methods_controller.rb index 224d3a45cac..205933ea814 100644 --- a/app/controllers/settings/two_factor_authentication_methods_controller.rb +++ b/app/controllers/settings/two_factor_authentication_methods_controller.rb @@ -4,14 +4,11 @@ module Settings class TwoFactorAuthenticationMethodsController < BaseController include ChallengableConcern - layout 'admin' + skip_before_action :require_functional! - before_action :authenticate_user! before_action :require_challenge!, only: :disable before_action :require_otp_enabled - skip_before_action :require_functional! - def index; end def disable diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index d28f7dad88b..723c04e554d 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -109,14 +109,14 @@ export function fetchAccountFail(id, error) { }; }; -export function followAccount(id, reblogs = true) { +export function followAccount(id, options = { reblogs: true }) { return (dispatch, getState) => { const alreadyFollowing = getState().getIn(['relationships', id, 'following']); const locked = getState().getIn(['accounts', id, 'locked'], false); dispatch(followAccountRequest(id, locked)); - api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => { + api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => { dispatch(followAccountSuccess(response.data, alreadyFollowing)); }).catch(error => { dispatch(followAccountFail(error, locked)); diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js index 37d1ddccfee..41a95503eb6 100644 --- a/app/javascript/mastodon/actions/markers.js +++ b/app/javascript/mastodon/actions/markers.js @@ -3,6 +3,9 @@ import { debounce } from 'lodash'; import compareId from '../compare_id'; import { showAlertForError } from './alerts'; +export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; +export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; +export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL'; export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS'; export const synchronouslySubmitMarkers = () => (dispatch, getState) => { @@ -57,8 +60,8 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => { const _buildParams = (state) => { const params = {}; - const lastHomeId = state.getIn(['timelines', 'home', 'items', 0]); - const lastNotificationId = state.getIn(['notifications', 'items', 0, 'id']); + const lastHomeId = state.getIn(['timelines', 'home', 'items']).find(item => item !== null); + const lastNotificationId = state.getIn(['notifications', 'lastReadId']); if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) { params.home = { @@ -100,3 +103,39 @@ export function submitMarkersSuccess({ home, notifications }) { export function submitMarkers() { return (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); }; + +export const fetchMarkers = () => (dispatch, getState) => { + const params = { timeline: ['notifications'] }; + + dispatch(fetchMarkersRequest()); + + api(getState).get('/api/v1/markers', { params }).then(response => { + dispatch(fetchMarkersSuccess(response.data)); + }).catch(error => { + dispatch(fetchMarkersFail(error)); + }); +}; + +export function fetchMarkersRequest() { + return { + type: MARKERS_FETCH_REQUEST, + skipLoading: true, + }; +}; + +export function fetchMarkersSuccess(markers) { + return { + type: MARKERS_FETCH_SUCCESS, + markers, + skipLoading: true, + }; +}; + +export function fetchMarkersFail(error) { + return { + type: MARKERS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, + }; +}; diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index a26844f8482..552def63b6d 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -33,6 +33,8 @@ export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT'; export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; +export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; + defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, @@ -59,7 +61,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { let filtered = false; - if (notification.type === 'mention') { + if (['mention', 'status'].includes(notification.type)) { const dropRegex = filters[0]; const regex = filters[1]; const searchIndex = searchTextFromRawStatus(notification.status); diff --git a/app/javascript/mastodon/actions/picture_in_picture.js b/app/javascript/mastodon/actions/picture_in_picture.js new file mode 100644 index 00000000000..4085cb59e00 --- /dev/null +++ b/app/javascript/mastodon/actions/picture_in_picture.js @@ -0,0 +1,38 @@ +// @ts-check + +export const PICTURE_IN_PICTURE_DEPLOY = 'PICTURE_IN_PICTURE_DEPLOY'; +export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE'; + +/** + * @typedef MediaProps + * @property {string} src + * @property {boolean} muted + * @property {number} volume + * @property {number} currentTime + * @property {string} poster + * @property {string} backgroundColor + * @property {string} foregroundColor + * @property {string} accentColor + */ + +/** + * @param {string} statusId + * @param {string} accountId + * @param {string} playerType + * @param {MediaProps} props + * @return {object} + */ +export const deployPictureInPicture = (statusId, accountId, playerType, props) => ({ + type: PICTURE_IN_PICTURE_DEPLOY, + statusId, + accountId, + playerType, + props, +}); + +/* + * @return {object} + */ +export const removePictureInPicture = () => ({ + type: PICTURE_IN_PICTURE_REMOVE, +}); diff --git a/app/javascript/mastodon/components/animated_number.js b/app/javascript/mastodon/components/animated_number.js index f3127c88ef1..fbe948c5b02 100644 --- a/app/javascript/mastodon/components/animated_number.js +++ b/app/javascript/mastodon/components/animated_number.js @@ -5,10 +5,21 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion'; import spring from 'react-motion/lib/spring'; import { reduceMotion } from 'mastodon/initial_state'; +const obfuscatedCount = count => { + if (count < 0) { + return 0; + } else if (count <= 1) { + return count; + } else { + return '1+'; + } +}; + export default class AnimatedNumber extends React.PureComponent { static propTypes = { value: PropTypes.number.isRequired, + obfuscate: PropTypes.bool, }; state = { @@ -36,11 +47,11 @@ export default class AnimatedNumber extends React.PureComponent { } render () { - const { value } = this.props; + const { value, obfuscate } = this.props; const { direction } = this.state; if (reduceMotion) { - return ; + return obfuscate ? obfuscatedCount(value) : ; } const styles = [{ @@ -54,7 +65,7 @@ export default class AnimatedNumber extends React.PureComponent { {items => ( {items.map(({ key, data, style }) => ( - 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}> + 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : } ))} )} diff --git a/app/javascript/mastodon/components/error_boundary.js b/app/javascript/mastodon/components/error_boundary.js index ca3012276bc..ca4a2cfe14b 100644 --- a/app/javascript/mastodon/components/error_boundary.js +++ b/app/javascript/mastodon/components/error_boundary.js @@ -66,17 +66,31 @@ export default class ErrorBoundary extends React.PureComponent { } render() { - const { hasError, copied } = this.state; + const { hasError, copied, errorMessage } = this.state; if (!hasError) { return this.props.children; } + const likelyBrowserAddonIssue = errorMessage && errorMessage.includes('NotFoundError'); + return (
-

-

+

+ { likelyBrowserAddonIssue ? ( + + ) : ( + + )} +

+

+ { likelyBrowserAddonIssue ? ( + + ) : ( + + )} +

Mastodon v{version} ยท ยท

diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index fd715bc3c83..7f83dc1b91e 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; +import AnimatedNumber from 'mastodon/components/animated_number'; export default class IconButton extends React.PureComponent { @@ -24,6 +25,8 @@ export default class IconButton extends React.PureComponent { animate: PropTypes.bool, overlay: PropTypes.bool, tabIndex: PropTypes.string, + counter: PropTypes.number, + obfuscateCount: PropTypes.bool, }; static defaultProps = { @@ -97,6 +100,8 @@ export default class IconButton extends React.PureComponent { pressed, tabIndex, title, + counter, + obfuscateCount, } = this.props; const { @@ -113,6 +118,10 @@ export default class IconButton extends React.PureComponent { overlayed: overlay, }); + if (typeof counter !== 'undefined') { + style.width = 'auto'; + } + return ( ); } diff --git a/app/javascript/mastodon/components/intersection_observer_article.js b/app/javascript/mastodon/components/intersection_observer_article.js index 124b34b02f9..2d87f19b535 100644 --- a/app/javascript/mastodon/components/intersection_observer_article.js +++ b/app/javascript/mastodon/components/intersection_observer_article.js @@ -2,10 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import scheduleIdleTask from '../features/ui/util/schedule_idle_task'; import getRectFromEntry from '../features/ui/util/get_rect_from_entry'; -import { is } from 'immutable'; -// Diff these props in the "rendered" state -const updateOnPropsForRendered = ['id', 'index', 'listLength']; // Diff these props in the "unrendered" state const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight']; @@ -33,9 +30,12 @@ export default class IntersectionObserverArticle extends React.Component { // If we're going from rendered to unrendered (or vice versa) then update return true; } - // Otherwise, diff based on props - const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered; - return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop])); + // If we are and remain hidden, diff based on props + if (isUnrendered) { + return !updateOnPropsForUnrendered.every(prop => nextProps[prop] === this.props[prop]); + } + // Else, assume the children have changed + return true; } componentDidMount () { diff --git a/app/javascript/mastodon/components/picture_in_picture_placeholder.js b/app/javascript/mastodon/components/picture_in_picture_placeholder.js new file mode 100644 index 00000000000..19d15c18b16 --- /dev/null +++ b/app/javascript/mastodon/components/picture_in_picture_placeholder.js @@ -0,0 +1,69 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Icon from 'mastodon/components/icon'; +import { removePictureInPicture } from 'mastodon/actions/picture_in_picture'; +import { connect } from 'react-redux'; +import { debounce } from 'lodash'; +import { FormattedMessage } from 'react-intl'; + +export default @connect() +class PictureInPicturePlaceholder extends React.PureComponent { + + static propTypes = { + width: PropTypes.number, + dispatch: PropTypes.func.isRequired, + }; + + state = { + width: this.props.width, + height: this.props.width && (this.props.width / (16/9)), + }; + + handleClick = () => { + const { dispatch } = this.props; + dispatch(removePictureInPicture()); + } + + setRef = c => { + this.node = c; + + if (this.node) { + this._setDimensions(); + } + } + + _setDimensions () { + const width = this.node.offsetWidth; + const height = width / (16/9); + + this.setState({ width, height }); + } + + componentDidMount () { + window.addEventListener('resize', this.handleResize, { passive: true }); + } + + componentWillUnmount () { + window.removeEventListener('resize', this.handleResize); + } + + handleResize = debounce(() => { + if (this.node) { + this._setDimensions(); + } + }, 250, { + trailing: true, + }); + + render () { + const { height } = this.state; + + return ( +
+ + +
+ ); + } + +} diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 174e401b725..c1e1cd172a1 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -17,6 +17,7 @@ import { HotKeys } from 'react-hotkeys'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; import { displayMedia } from '../initial_state'; +import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder'; // We use the component (and not the container) since we do not want // to use the progress bar to show download progress @@ -95,6 +96,8 @@ class Status extends ImmutablePureComponent { cacheMediaWidth: PropTypes.func, cachedMediaWidth: PropTypes.number, scrollKey: PropTypes.string, + deployPictureInPicture: PropTypes.func, + usingPiP: PropTypes.bool, }; // Avoid checking props that are functions (and whose equality will always @@ -104,6 +107,8 @@ class Status extends ImmutablePureComponent { 'account', 'muted', 'hidden', + 'unread', + 'usingPiP', ]; state = { @@ -205,6 +210,13 @@ class Status extends ImmutablePureComponent { } } + handleDeployPictureInPicture = (type, mediaProps) => { + const { deployPictureInPicture } = this.props; + const status = this._properStatus(); + + deployPictureInPicture(status, type, mediaProps); + } + handleHotkeyReply = e => { e.preventDefault(); this.props.onReply(this._properStatus(), this.context.router.history); @@ -265,7 +277,7 @@ class Status extends ImmutablePureComponent { let media = null; let statusAvatar, prepend, rebloggedByText; - const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey } = this.props; + const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey, usingPiP } = this.props; let { status, account, ...other } = this.props; @@ -336,7 +348,9 @@ class Status extends ImmutablePureComponent { status = status.get('reblog'); } - if (status.get('media_attachments').size > 0) { + if (usingPiP) { + media = ; + } else if (status.get('media_attachments').size > 0) { if (this.props.muted) { media = ( )} @@ -382,6 +397,7 @@ class Status extends ImmutablePureComponent { sensitive={status.get('sensitive')} onOpenVideo={this.handleOpenVideo} cacheWidth={this.props.cacheMediaWidth} + deployPictureInPicture={this.handleDeployPictureInPicture} visible={this.state.showMedia} onToggleVisibility={this.handleToggleMediaVisibility} /> @@ -438,10 +454,10 @@ class Status extends ImmutablePureComponent { return ( -
+
{prepend} -
+
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index b7babd4adc9..66b5a17ac22 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -43,16 +43,6 @@ const messages = defineMessages({ unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, }); -const obfuscatedCount = count => { - if (count < 0) { - return 0; - } else if (count <= 1) { - return count; - } else { - return '1+'; - } -}; - const mapStateToProps = (state, { status }) => ({ relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), }); @@ -329,9 +319,10 @@ class StatusActionBar extends ImmutablePureComponent { return (
-
{obfuscatedCount(status.get('replies_count'))}
+ + {shareButton}
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index decf7279f9a..7bfd66d3eaa 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -37,6 +37,7 @@ import { initMuteModal } from '../actions/mutes'; import { initBlockModal } from '../actions/blocks'; import { initReport } from '../actions/reports'; import { openModal } from '../actions/modal'; +import { deployPictureInPicture } from '../actions/picture_in_picture'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { boostModal, deleteModal } from '../initial_state'; import { showAlertForError } from '../actions/alerts'; @@ -56,6 +57,7 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => ({ status: getStatus(state, props), + usingPiP: state.get('picture_in_picture').statusId === props.id, }); return mapStateToProps; @@ -207,6 +209,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(unblockDomain(domain)); }, + deployPictureInPicture (status, type, mediaProps) { + dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps)); + }, + }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 61ecf045d18..2b97af4e675 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { autoPlayGif, me, isStaff } from 'mastodon/initial_state'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; +import IconButton from 'mastodon/components/icon_button'; import Avatar from 'mastodon/components/avatar'; import { counterRenderer } from 'mastodon/components/common_counter'; import ShortNumber from 'mastodon/components/short_number'; @@ -35,6 +36,8 @@ const messages = defineMessages({ unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, + enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' }, + disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, @@ -68,8 +71,9 @@ class Header extends ImmutablePureComponent { onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired, - onReport: PropTypes.func.isRequired, onReblogToggle: PropTypes.func.isRequired, + onNotifyToggle: PropTypes.func.isRequired, + onReport: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired, onUnblockDomain: PropTypes.func.isRequired, @@ -140,8 +144,11 @@ class Header extends ImmutablePureComponent { return null; } + const suspended = account.get('suspended'); + let info = []; let actionBtn = ''; + let bellBtn = ''; let lockedIcon = ''; let menu = []; @@ -171,6 +178,10 @@ class Header extends ImmutablePureComponent { actionBtn = + + ); + } + return ( diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.js b/app/javascript/mastodon/features/picture_in_picture/components/footer.js new file mode 100644 index 00000000000..086cda954eb --- /dev/null +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js @@ -0,0 +1,137 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import IconButton from 'mastodon/components/icon_button'; +import classNames from 'classnames'; +import { me, boostModal } from 'mastodon/initial_state'; +import { defineMessages, injectIntl } from 'react-intl'; +import { replyCompose } from 'mastodon/actions/compose'; +import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions'; +import { makeGetStatus } from 'mastodon/selectors'; +import { openModal } from 'mastodon/actions/modal'; + +const messages = defineMessages({ + reply: { id: 'status.reply', defaultMessage: 'Reply' }, + replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, + reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, + reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' }, + cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, + cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, + favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, + replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, + replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, +}); + +const makeMapStateToProps = () => { + const getStatus = makeGetStatus(); + + const mapStateToProps = (state, { statusId }) => ({ + status: getStatus(state, { id: statusId }), + askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, + }); + + return mapStateToProps; +}; + +export default @connect(makeMapStateToProps) +@injectIntl +class Footer extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + statusId: PropTypes.string.isRequired, + status: ImmutablePropTypes.map.isRequired, + intl: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + askReplyConfirmation: PropTypes.bool, + }; + + _performReply = () => { + const { dispatch, status } = this.props; + dispatch(replyCompose(status, this.context.router.history)); + }; + + handleReplyClick = () => { + const { dispatch, askReplyConfirmation, intl } = this.props; + + if (askReplyConfirmation) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: this._performReply, + })); + } else { + this._performReply(); + } + }; + + handleFavouriteClick = () => { + const { dispatch, status } = this.props; + + if (status.get('favourited')) { + dispatch(unfavourite(status)); + } else { + dispatch(favourite(status)); + } + }; + + _performReblog = () => { + const { dispatch, status } = this.props; + dispatch(reblog(status)); + } + + handleReblogClick = e => { + const { dispatch, status } = this.props; + + if (status.get('reblogged')) { + dispatch(unreblog(status)); + } else if ((e && e.shiftKey) || !boostModal) { + this._performReblog(); + } else { + dispatch(openModal('BOOST', { status, onReblog: this._performReblog })); + } + }; + + render () { + const { status, intl } = this.props; + + const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); + const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; + + let replyIcon, replyTitle; + + if (status.get('in_reply_to_id', null) === null) { + replyIcon = 'reply'; + replyTitle = intl.formatMessage(messages.reply); + } else { + replyIcon = 'reply-all'; + replyTitle = intl.formatMessage(messages.replyAll); + } + + let reblogTitle = ''; + + if (status.get('reblogged')) { + reblogTitle = intl.formatMessage(messages.cancel_reblog_private); + } else if (publicStatus) { + reblogTitle = intl.formatMessage(messages.reblog); + } else if (reblogPrivate) { + reblogTitle = intl.formatMessage(messages.reblog_private); + } else { + reblogTitle = intl.formatMessage(messages.cannot_reblog); + } + + return ( +
+ + + +
+ ); + } + +} diff --git a/app/javascript/mastodon/features/picture_in_picture/components/header.js b/app/javascript/mastodon/features/picture_in_picture/components/header.js new file mode 100644 index 00000000000..4cb6de1a40f --- /dev/null +++ b/app/javascript/mastodon/features/picture_in_picture/components/header.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import IconButton from 'mastodon/components/icon_button'; +import { Link } from 'react-router-dom'; +import Avatar from 'mastodon/components/avatar'; +import DisplayName from 'mastodon/components/display_name'; + +const mapStateToProps = (state, { accountId }) => ({ + account: state.getIn(['accounts', accountId]), +}); + +export default @connect(mapStateToProps) +class Header extends ImmutablePureComponent { + + static propTypes = { + accountId: PropTypes.string.isRequired, + statusId: PropTypes.string.isRequired, + account: ImmutablePropTypes.map.isRequired, + onClose: PropTypes.func.isRequired, + }; + + render () { + const { account, statusId, onClose } = this.props; + + return ( +
+ + + + + + +
+ ); + } + +} diff --git a/app/javascript/mastodon/features/picture_in_picture/index.js b/app/javascript/mastodon/features/picture_in_picture/index.js new file mode 100644 index 00000000000..1e59fbcd337 --- /dev/null +++ b/app/javascript/mastodon/features/picture_in_picture/index.js @@ -0,0 +1,85 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import Video from 'mastodon/features/video'; +import Audio from 'mastodon/features/audio'; +import { removePictureInPicture } from 'mastodon/actions/picture_in_picture'; +import Header from './components/header'; +import Footer from './components/footer'; + +const mapStateToProps = state => ({ + ...state.get('picture_in_picture'), +}); + +export default @connect(mapStateToProps) +class PictureInPicture extends React.Component { + + static propTypes = { + statusId: PropTypes.string, + accountId: PropTypes.string, + type: PropTypes.string, + src: PropTypes.string, + muted: PropTypes.bool, + volume: PropTypes.number, + currentTime: PropTypes.number, + poster: PropTypes.string, + backgroundColor: PropTypes.string, + foregroundColor: PropTypes.string, + accentColor: PropTypes.string, + dispatch: PropTypes.func.isRequired, + }; + + handleClose = () => { + const { dispatch } = this.props; + dispatch(removePictureInPicture()); + } + + render () { + const { type, src, currentTime, accountId, statusId } = this.props; + + if (!currentTime) { + return null; + } + + let player; + + if (type === 'video') { + player = ( +
- {(!onCloseVideo && !editable && !fullscreen) && } + {(!onCloseVideo && !editable && !fullscreen && !this.props.alwaysVisible) && } {(!fullscreen && onOpenVideo) && } {onCloseVideo && } diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 3823bb05e09..a8fb69c2748 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -36,6 +36,7 @@ import trends from './trends'; import missed_updates from './missed_updates'; import announcements from './announcements'; import markers from './markers'; +import picture_in_picture from './picture_in_picture'; const reducers = { announcements, @@ -75,6 +76,7 @@ const reducers = { trends, missed_updates, markers, + picture_in_picture, }; export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index ed1ba027267..b01db806fa5 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -9,6 +9,7 @@ import { NOTIFICATIONS_LOAD_PENDING, NOTIFICATIONS_MOUNT, NOTIFICATIONS_UNMOUNT, + NOTIFICATIONS_MARK_AS_READ, } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS, @@ -16,6 +17,13 @@ import { FOLLOW_REQUEST_AUTHORIZE_SUCCESS, FOLLOW_REQUEST_REJECT_SUCCESS, } from '../actions/accounts'; +import { + MARKERS_FETCH_SUCCESS, +} from '../actions/markers'; +import { + APP_FOCUS, + APP_UNFOCUS, +} from '../actions/app'; import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks'; import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; @@ -26,8 +34,11 @@ const initialState = ImmutableMap({ items: ImmutableList(), hasMore: true, top: false, - mounted: false, + mounted: 0, unread: 0, + lastReadId: '0', + readMarkerId: '0', + isTabVisible: true, isLoading: false, }); @@ -46,8 +57,10 @@ const normalizeNotification = (state, notification, usePendingItems) => { return state.update('pendingItems', list => list.unshift(notificationToMap(notification))).update('unread', unread => unread + 1); } - if (!top) { + if (shouldCountUnreadNotifications(state)) { state = state.update('unread', unread => unread + 1); + } else { + state = state.set('lastReadId', notification.id); } return state.update('items', list => { @@ -60,6 +73,7 @@ const normalizeNotification = (state, notification, usePendingItems) => { }; const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => { + const lastReadId = state.get('lastReadId'); let items = ImmutableList(); notifications.forEach((n, i) => { @@ -87,6 +101,15 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece mutable.set('hasMore', false); } + if (shouldCountUnreadNotifications(state)) { + mutable.update('unread', unread => unread + items.count(item => compareId(item.get('id'), lastReadId) > 0)); + } else { + const mostRecent = items.find(item => item !== null); + if (mostRecent && compareId(lastReadId, mostRecent.get('id')) < 0) { + mutable.set('lastReadId', mostRecent.get('id')); + } + } + mutable.set('isLoading', false); }); }; @@ -96,21 +119,92 @@ const filterNotifications = (state, accountIds, type) => { return state.update('items', helper).update('pendingItems', helper); }; +const clearUnread = (state) => { + state = state.set('unread', state.get('pendingItems').size); + const lastNotification = state.get('items').find(item => item !== null); + return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0'); +}; + const updateTop = (state, top) => { - if (top) { - state = state.set('unread', state.get('pendingItems').size); + state = state.set('top', top); + + if (!shouldCountUnreadNotifications(state)) { + state = clearUnread(state); } - return state.set('top', top); + return state; }; const deleteByStatus = (state, statusId) => { + const lastReadId = state.get('lastReadId'); + + if (shouldCountUnreadNotifications(state)) { + const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0); + state = state.update('unread', unread => unread - deletedUnread.size); + } + const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId); + const deletedUnread = state.get('pendingItems').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0); + state = state.update('unread', unread => unread - deletedUnread.size); return state.update('items', helper).update('pendingItems', helper); }; +const updateMounted = (state) => { + state = state.update('mounted', count => count + 1); + if (!shouldCountUnreadNotifications(state)) { + state = state.set('readMarkerId', state.get('lastReadId')); + state = clearUnread(state); + } + return state; +}; + +const updateVisibility = (state, visibility) => { + state = state.set('isTabVisible', visibility); + if (!shouldCountUnreadNotifications(state)) { + state = state.set('readMarkerId', state.get('lastReadId')); + state = clearUnread(state); + } + return state; +}; + +const shouldCountUnreadNotifications = (state) => { + const isTabVisible = state.get('isTabVisible'); + const isOnTop = state.get('top'); + const isMounted = state.get('mounted') > 0; + const lastReadId = state.get('lastReadId'); + const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (!state.get('items').isEmpty() && compareId(state.get('items').last().get('id'), lastReadId) <= 0); + + return !(isTabVisible && isOnTop && isMounted && lastItemReached); +}; + +const recountUnread = (state, last_read_id) => { + return state.withMutations(mutable => { + if (compareId(last_read_id, mutable.get('lastReadId')) > 0) { + mutable.set('lastReadId', last_read_id); + } + + if (compareId(last_read_id, mutable.get('readMarkerId')) > 0) { + mutable.set('readMarkerId', last_read_id); + } + + if (state.get('unread') > 0 || shouldCountUnreadNotifications(state)) { + mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), last_read_id) > 0)); + } + }); +}; + export default function notifications(state = initialState, action) { switch(action.type) { + case MARKERS_FETCH_SUCCESS: + return action.markers.notifications ? recountUnread(state, action.markers.notifications.last_read_id) : state; + case NOTIFICATIONS_MOUNT: + return updateMounted(state); + case NOTIFICATIONS_UNMOUNT: + return state.update('mounted', count => count - 1); + case APP_FOCUS: + return updateVisibility(state, true); + case APP_UNFOCUS: + return updateVisibility(state, false); case NOTIFICATIONS_LOAD_PENDING: return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0); case NOTIFICATIONS_EXPAND_REQUEST: @@ -144,10 +238,9 @@ export default function notifications(state = initialState, action) { return action.timeline === 'home' ? state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : state; - case NOTIFICATIONS_MOUNT: - return state.set('mounted', true); - case NOTIFICATIONS_UNMOUNT: - return state.set('mounted', false); + case NOTIFICATIONS_MARK_AS_READ: + const lastNotification = state.get('items').find(item => item !== null); + return lastNotification ? recountUnread(state, lastNotification.get('id')) : state; default: return state; } diff --git a/app/javascript/mastodon/reducers/picture_in_picture.js b/app/javascript/mastodon/reducers/picture_in_picture.js new file mode 100644 index 00000000000..06cd8c5e875 --- /dev/null +++ b/app/javascript/mastodon/reducers/picture_in_picture.js @@ -0,0 +1,22 @@ +import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'mastodon/actions/picture_in_picture'; + +const initialState = { + statusId: null, + accountId: null, + type: null, + src: null, + muted: false, + volume: 0, + currentTime: 0, +}; + +export default function pictureInPicture(state = initialState, action) { + switch(action.type) { + case PICTURE_IN_PICTURE_DEPLOY: + return { statusId: action.statusId, accountId: action.accountId, type: action.playerType, ...action.props }; + case PICTURE_IN_PICTURE_REMOVE: + return { ...initialState }; + default: + return state; + } +}; diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss index 5a40e7d79a9..841ed66480c 100644 --- a/app/javascript/styles/contrast/diff.scss +++ b/app/javascript/styles/contrast/diff.scss @@ -75,3 +75,8 @@ .public-layout .public-account-header__tabs__tabs .counter.active::after { border-bottom: 4px solid $ui-highlight-color; } + +.compose-form .autosuggest-textarea__textarea::placeholder, +.compose-form .spoiler-input__input::placeholder { + color: $inverted-text-color; +} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 4f4507b3738..ff4bb34289a 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -163,7 +163,8 @@ } .icon-button { - display: inline-block; + display: inline-flex; + align-items: center; padding: 0; color: $action-button-color; border: 0; @@ -245,6 +246,14 @@ background: rgba($base-overlay-background, 0.9); } } + + &__counter { + display: inline-block; + width: 14px; + margin-left: 4px; + font-size: 12px; + font-weight: 500; + } } .text-icon-button { @@ -1139,24 +1148,6 @@ align-items: center; display: flex; margin-top: 8px; - - &__counter { - display: inline-flex; - margin-right: 11px; - align-items: center; - - .status__action-bar-button { - margin-right: 4px; - } - - &__label { - display: inline-block; - width: 14px; - font-size: 12px; - font-weight: 500; - color: $action-button-color; - } - } } .status__action-bar-button { @@ -6502,6 +6493,10 @@ noscript { padding: 2px; } + & > .icon-button { + margin-right: 8px; + } + .button { margin: 0 8px; } @@ -7011,3 +7006,119 @@ noscript { } } } + +.notification, +.status__wrapper { + position: relative; + + &.unread { + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + pointer-events: 0; + width: 100%; + height: 100%; + border-left: 2px solid $highlight-text-color; + pointer-events: none; + } + } +} + +.picture-in-picture { + position: fixed; + bottom: 20px; + right: 20px; + width: 300px; + + &__footer { + border-radius: 0 0 4px 4px; + background: lighten($ui-base-color, 4%); + padding: 10px; + padding-top: 12px; + display: flex; + justify-content: space-between; + } + + &__header { + border-radius: 4px 4px 0 0; + background: lighten($ui-base-color, 4%); + padding: 10px; + display: flex; + justify-content: space-between; + + &__account { + display: flex; + text-decoration: none; + } + + .account__avatar { + margin-right: 10px; + } + + .display-name { + color: $primary-text-color; + text-decoration: none; + + strong, + span { + display: block; + text-overflow: ellipsis; + overflow: hidden; + } + + span { + color: $darker-text-color; + } + } + } + + .video-player, + .audio-player { + border-radius: 0; + } + + @media screen and (max-width: 415px) { + width: 210px; + bottom: 10px; + right: 10px; + + &__footer { + display: none; + } + + .video-player, + .audio-player { + border-radius: 0 0 4px 4px; + } + } +} + +.picture-in-picture-placeholder { + box-sizing: border-box; + border: 2px dashed lighten($ui-base-color, 8%); + background: $base-shadow-color; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-top: 10px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + color: $darker-text-color; + + i { + display: block; + font-size: 24px; + font-weight: 400; + margin-bottom: 10px; + } + + &:hover, + &:focus, + &:active { + border-color: lighten($ui-base-color, 12%); + } +} diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 94aee7939fc..224451f4173 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -118,13 +118,13 @@ class ActivityPub::Activity end def notify_about_reblog(status) - NotifyService.new.call(status.reblog.account, status) + NotifyService.new.call(status.reblog.account, :reblog, status) end def notify_about_mentions(status) status.active_mentions.includes(:account).each do |mention| next unless mention.account.local? && audience_includes?(mention.account) - NotifyService.new.call(mention.account, mention) + NotifyService.new.call(mention.account, :mention, mention) end end diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index dc9ff580c1a..09b9e5e0e83 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -13,7 +13,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity def delete_person lock_or_return("delete_in_progress:#{@account.id}") do - SuspendAccountService.new.call(@account, reserve_username: false) + DeleteAccountService.new.call(@account, reserve_username: false) end end diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index ec92f4255f6..0beec68ab38 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -22,10 +22,10 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id']) if target_account.locked? || @account.silenced? - NotifyService.new.call(target_account, follow_request) + NotifyService.new.call(target_account, :follow_request, follow_request) else AuthorizeFollowService.new.call(@account, target_account) - NotifyService.new.call(target_account, ::Follow.find_by(account: @account, target_account: target_account)) + NotifyService.new.call(target_account, :follow, ::Follow.find_by(account: @account, target_account: target_account)) end end diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index 674d5fe4723..c065f01f860 100644 --- a/app/lib/activitypub/activity/like.rb +++ b/app/lib/activitypub/activity/like.rb @@ -7,6 +7,6 @@ class ActivityPub::Activity::Like < ActivityPub::Activity return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) || @account.favourited?(original_status) favourite = original_status.favourites.create!(account: @account) - NotifyService.new.call(original_status.account, favourite) + NotifyService.new.call(original_status.account, :favourite, favourite) end end diff --git a/app/lib/request.rb b/app/lib/request.rb index 89fbeaf9529..38048dad7b5 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -253,7 +253,15 @@ class Request alias new open def check_private_address(address) - raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(IPAddr.new(address.to_s)) + addr = IPAddr.new(address.to_s) + return if private_address_exceptions.any? { |range| range.include?(addr) } + raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(addr) + end + + def private_address_exceptions + @private_address_exceptions = begin + (ENV['ALLOWED_PRIVATE_ADDRESSES'] || '').split(',').map { |addr| IPAddr.new(addr) } + end end end end diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 9d8a7886c27..54db892ccc6 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -10,7 +10,7 @@ class NotificationMailer < ApplicationMailer @me = recipient @status = notification.target_status - return if @me.user.disabled? || @status.nil? + return unless @me.user.functional? && @status.present? locale_for_account(@me) do thread_by_conversation(@status.conversation) @@ -22,7 +22,7 @@ class NotificationMailer < ApplicationMailer @me = recipient @account = notification.from_account - return if @me.user.disabled? + return unless @me.user.functional? locale_for_account(@me) do mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct) @@ -34,7 +34,7 @@ class NotificationMailer < ApplicationMailer @account = notification.from_account @status = notification.target_status - return if @me.user.disabled? || @status.nil? + return unless @me.user.functional? && @status.present? locale_for_account(@me) do thread_by_conversation(@status.conversation) @@ -47,7 +47,7 @@ class NotificationMailer < ApplicationMailer @account = notification.from_account @status = notification.target_status - return if @me.user.disabled? || @status.nil? + return unless @me.user.functional? && @status.present? locale_for_account(@me) do thread_by_conversation(@status.conversation) @@ -59,7 +59,7 @@ class NotificationMailer < ApplicationMailer @me = recipient @account = notification.from_account - return if @me.user.disabled? + return unless @me.user.functional? locale_for_account(@me) do mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct) @@ -67,7 +67,7 @@ class NotificationMailer < ApplicationMailer end def digest(recipient, **opts) - return if recipient.user.disabled? + return unless recipient.user.functional? @me = recipient @since = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max @@ -88,8 +88,10 @@ class NotificationMailer < ApplicationMailer def thread_by_conversation(conversation) return if conversation.nil? + msg_id = "" + headers['In-Reply-To'] = msg_id - headers['References'] = msg_id + headers['References'] = msg_id end end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index b557685516c..95996ba3ff9 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -15,7 +15,7 @@ class UserMailer < Devise::Mailer @token = token @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.unconfirmed_email.presence || @resource.email, @@ -29,7 +29,7 @@ class UserMailer < Devise::Mailer @token = token @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject') @@ -40,7 +40,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject') @@ -51,7 +51,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.email_changed.subject') @@ -62,7 +62,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_enabled.subject') @@ -73,7 +73,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_disabled.subject') @@ -84,7 +84,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject') @@ -95,7 +95,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_enabled.subject') @@ -106,7 +106,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_disabled.subject') @@ -118,7 +118,7 @@ class UserMailer < Devise::Mailer @instance = Rails.configuration.x.local_domain @webauthn_credential = webauthn_credential - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.added.subject') @@ -130,7 +130,7 @@ class UserMailer < Devise::Mailer @instance = Rails.configuration.x.local_domain @webauthn_credential = webauthn_credential - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.deleted.subject') @@ -141,7 +141,7 @@ class UserMailer < Devise::Mailer @resource = user @instance = Rails.configuration.x.local_domain - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('user_mailer.welcome.subject') @@ -153,7 +153,7 @@ class UserMailer < Devise::Mailer @instance = Rails.configuration.x.local_domain @backup = backup - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, subject: I18n.t('user_mailer.backup_ready.subject') @@ -181,7 +181,7 @@ class UserMailer < Devise::Mailer @detection = Browser.new(user_agent) @timestamp = timestamp.to_time.utc - return if @resource.disabled? + return unless @resource.active_for_authentication? I18n.with_locale(@resource.locale || I18n.default_locale) do mail to: @resource.email, diff --git a/app/models/account.rb b/app/models/account.rb index 0b3c48543d5..3a6b38181bb 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -226,23 +226,20 @@ class Account < ApplicationRecord def suspend!(date = Time.now.utc) transaction do - user&.disable! if local? + create_deletion_request! update!(suspended_at: date) end end def unsuspend! transaction do - user&.enable! if local? + deletion_request&.destroy! update!(suspended_at: nil) end end def memorialize! - transaction do - user&.disable! if local? - update!(memorial: true) - end + update!(memorial: true) end def sign? diff --git a/app/models/account_conversation.rb b/app/models/account_conversation.rb index 5e2ddd0839f..56fd13543c8 100644 --- a/app/models/account_conversation.rb +++ b/app/models/account_conversation.rb @@ -38,15 +38,16 @@ class AccountConversation < ApplicationRecord class << self def to_a_paginated_by_id(limit, options = {}) if options[:min_id] - paginate_by_min_id(limit, options[:min_id]).reverse + paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse else paginate_by_max_id(limit, options[:max_id], options[:since_id]).to_a end end - def paginate_by_min_id(limit, min_id = nil) + def paginate_by_min_id(limit, min_id = nil, max_id = nil) query = order(arel_table[:last_status_id].asc).limit(limit) query = query.where(arel_table[:last_status_id].gt(min_id)) if min_id.present? + query = query.where(arel_table[:last_status_id].lt(max_id)) if max_id.present? query end diff --git a/app/models/account_deletion_request.rb b/app/models/account_deletion_request.rb new file mode 100644 index 00000000000..7d0c346cc27 --- /dev/null +++ b/app/models/account_deletion_request.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: account_deletion_requests +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# created_at :datetime not null +# updated_at :datetime not null +# +class AccountDeletionRequest < ApplicationRecord + DELAY_TO_DELETION = 30.days.freeze + + belongs_to :account + + def due_at + created_at + DELAY_TO_DELETION + end +end diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb index b30a823699e..c4ac09520ef 100644 --- a/app/models/admin/account_action.rb +++ b/app/models/admin/account_action.rb @@ -134,7 +134,7 @@ class Admin::AccountAction end def process_email! - UserMailer.warning(target_account.user, warning, status_ids).deliver_now! if warnable? + UserMailer.warning(target_account.user, warning, status_ids).deliver_later! if warnable? end def warnable? @@ -142,7 +142,7 @@ class Admin::AccountAction end def status_ids - @report.status_ids if @report && include_statuses + report.status_ids if report && include_statuses end def reports diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index cca3a17fa41..98849f8fcfa 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -60,5 +60,8 @@ module AccountAssociations # Hashtags has_and_belongs_to_many :tags has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account + + # Account deletion requests + has_one :deletion_request, class_name: 'AccountDeletionRequest', inverse_of: :account, dependent: :destroy end end diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index be7211f2cad..427ebdae29c 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -8,6 +8,7 @@ module AccountInteractions Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping| mapping[follow.target_account_id] = { reblogs: follow.show_reblogs?, + notify: follow.notify?, } end end @@ -36,6 +37,7 @@ module AccountInteractions FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping| mapping[follow_request.target_account_id] = { reblogs: follow_request.show_reblogs?, + notify: follow_request.notify?, } end end @@ -95,25 +97,29 @@ module AccountInteractions has_many :announcement_mutes, dependent: :destroy end - def follow!(other_account, reblogs: nil, uri: nil, rate_limit: false) - reblogs = true if reblogs.nil? - - rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit) + def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false) + rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit) .find_or_create_by!(target_account: other_account) - rel.update!(show_reblogs: reblogs) + rel.show_reblogs = reblogs unless reblogs.nil? + rel.notify = notify unless notify.nil? + + rel.save! if rel.changed? + remove_potential_friendship(other_account) rel end - def request_follow!(other_account, reblogs: nil, uri: nil, rate_limit: false) - reblogs = true if reblogs.nil? - - rel = follow_requests.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit) + def request_follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false) + rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit) .find_or_create_by!(target_account: other_account) - rel.update!(show_reblogs: reblogs) + rel.show_reblogs = reblogs unless reblogs.nil? + rel.notify = notify unless notify.nil? + + rel.save! if rel.changed? + remove_potential_friendship(other_account) rel diff --git a/app/models/concerns/paginable.rb b/app/models/concerns/paginable.rb index 760cc3df4d4..62e39f6714a 100644 --- a/app/models/concerns/paginable.rb +++ b/app/models/concerns/paginable.rb @@ -14,15 +14,16 @@ module Paginable # Differs from :paginate_by_max_id in that it gives the results immediately following min_id, # whereas since_id gives the items with largest id, but with since_id as a cutoff. # Results will be in ascending order by id. - scope :paginate_by_min_id, ->(limit, min_id = nil) { + scope :paginate_by_min_id, ->(limit, min_id = nil, max_id = nil) { query = reorder(arel_table[:id]).limit(limit) query = query.where(arel_table[:id].gt(min_id)) if min_id.present? + query = query.where(arel_table[:id].lt(max_id)) if max_id.present? query } def self.to_a_paginated_by_id(limit, options = {}) if options[:min_id].present? - paginate_by_min_id(limit, options[:min_id]).reverse + paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse else paginate_by_max_id(limit, options[:max_id], options[:since_id]).to_a end diff --git a/app/models/feed.rb b/app/models/feed.rb index 36e0c1e0a09..f51dcfab1da 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -20,12 +20,12 @@ class Feed protected def from_redis(limit, max_id, since_id, min_id) + max_id = '+inf' if max_id.blank? if min_id.blank? - max_id = '+inf' if max_id.blank? since_id = '-inf' if since_id.blank? unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i) else - unhydrated = redis.zrangebyscore(key, "(#{min_id}", '+inf', limit: [0, limit], with_scores: true).map(&:first).map(&:to_i) + unhydrated = redis.zrangebyscore(key, "(#{min_id}", "(#{max_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i) end Status.where(id: unhydrated).cache_ids diff --git a/app/models/follow.rb b/app/models/follow.rb index f3e48a2ed7e..0b4ddbf3f8d 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -10,6 +10,7 @@ # target_account_id :bigint(8) not null # show_reblogs :boolean default(TRUE), not null # uri :string +# notify :boolean default(FALSE), not null # class Follow < ApplicationRecord @@ -34,7 +35,7 @@ class Follow < ApplicationRecord end def revoke_request! - FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, uri: uri) + FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, notify: notify, uri: uri) destroy! end diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index 3325e264cce..c1f19149b1f 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -10,6 +10,7 @@ # target_account_id :bigint(8) not null # show_reblogs :boolean default(TRUE), not null # uri :string +# notify :boolean default(FALSE), not null # class FollowRequest < ApplicationRecord @@ -28,7 +29,7 @@ class FollowRequest < ApplicationRecord validates_with FollowLimitValidator, on: :create def authorize! - account.follow!(target_account, reblogs: show_reblogs, uri: uri) + account.follow!(target_account, reblogs: show_reblogs, notify: notify, uri: uri) MergeWorker.perform_async(target_account.id, account.id) if account.local? destroy! end diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index 0b285fde92c..7b9e40f6855 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -69,6 +69,6 @@ class Form::AccountBatch records = accounts.includes(:user) records.each { |account| authorize(account.user, :reject?) } - .each { |account| SuspendAccountService.new.call(account, reserve_email: false, reserve_username: false) } + .each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) } end end diff --git a/app/models/invite.rb b/app/models/invite.rb index 29d25eae801..7ea4e2f9841 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -28,7 +28,7 @@ class Invite < ApplicationRecord before_validation :set_code def valid_for_use? - (max_uses.nil? || uses < max_uses) && !expired? && !(user.nil? || user.disabled?) + (max_uses.nil? || uses < max_uses) && !expired? && user&.functional? end private diff --git a/app/models/notification.rb b/app/models/notification.rb index ad7528f505c..e83123c9731 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -10,21 +10,34 @@ # updated_at :datetime not null # account_id :bigint(8) not null # from_account_id :bigint(8) not null +# type :string # class Notification < ApplicationRecord + self.inheritance_column = nil + include Paginable include Cacheable - TYPE_CLASS_MAP = { - mention: 'Mention', - reblog: 'Status', - follow: 'Follow', - follow_request: 'FollowRequest', - favourite: 'Favourite', - poll: 'Poll', + LEGACY_TYPE_CLASS_MAP = { + 'Mention' => :mention, + 'Status' => :reblog, + 'Follow' => :follow, + 'FollowRequest' => :follow_request, + 'Favourite' => :favourite, + 'Poll' => :poll, }.freeze + TYPES = %i( + mention + status + reblog + follow + follow_request + favourite + poll + ).freeze + STATUS_INCLUDES = [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account]].freeze belongs_to :account, optional: true @@ -38,26 +51,30 @@ class Notification < ApplicationRecord belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id', optional: true belongs_to :poll, foreign_type: 'Poll', foreign_key: 'activity_id', optional: true - validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] } - validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values } + validates :type, inclusion: { in: TYPES } + + scope :without_suspended, -> { joins(:from_account).merge(Account.without_suspended) } scope :browserable, ->(exclude_types = [], account_id = nil) { - types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types) + types = TYPES - exclude_types.map(&:to_sym) + if account_id.nil? - where(activity_type: types) + where(type: types) else - where(activity_type: types, from_account_id: account_id) + where(type: types, from_account_id: account_id) end } cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, follow_request: :account, poll: [status: STATUS_INCLUDES] def type - @type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym + @type ||= (super || LEGACY_TYPE_CLASS_MAP[activity_type]).to_sym end def target_status case type + when :status + status when :reblog status&.reblog when :favourite @@ -86,10 +103,6 @@ class Notification < ApplicationRecord item.target_status.account = accounts[item.target_status.account_id] if item.target_status end end - - def activity_types_from_types(types) - types.map { |type| TYPE_CLASS_MAP[type.to_sym] }.compact - end end after_initialize :set_from_account diff --git a/app/models/status.rb b/app/models/status.rb index 8495927afd7..d1ac2e4f248 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -93,19 +93,19 @@ class Status < ApplicationRecord scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') } scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') } scope :with_public_visibility, -> { where(visibility: :public) } - scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) } + scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) } scope :in_chosen_languages, ->(account) { where(language: nil).or where(language: account.chosen_languages) } scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) } scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) } scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) } scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) } - scope :tagged_with_all, ->(tags) { - Array(tags).map(&:id).map(&:to_i).reduce(self) do |result, id| + scope :tagged_with_all, ->(tag_ids) { + Array(tag_ids).reduce(self) do |result, id| result.joins("INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}") end } - scope :tagged_with_none, ->(tags) { - Array(tags).map(&:id).map(&:to_i).reduce(self) do |result, id| + scope :tagged_with_none, ->(tag_ids) { + Array(tag_ids).reduce(self) do |result, id| result.joins("LEFT OUTER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}") .where("t#{id}.tag_id IS NULL") end diff --git a/app/models/tag_feed.rb b/app/models/tag_feed.rb index baff5502037..a7d583a7e11 100644 --- a/app/models/tag_feed.rb +++ b/app/models/tag_feed.rb @@ -53,6 +53,6 @@ class TagFeed < PublicFeed end def tags_for(names) - Tag.matching_name(Array(names).take(LIMIT_PER_MODE)) if names.present? + Tag.matching_name(Array(names).take(LIMIT_PER_MODE)).pluck(:id) if names.present? end end diff --git a/app/models/user.rb b/app/models/user.rb index 4467362e1fb..6cd2ca6bdca 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -169,7 +169,7 @@ class User < ApplicationRecord end def active_for_authentication? - true + !account.memorial? end def suspicious_sign_in?(ip) @@ -177,7 +177,7 @@ class User < ApplicationRecord end def functional? - confirmed? && approved? && !disabled? && !account.suspended? + confirmed? && approved? && !disabled? && !account.suspended? && !account.memorial? end def unconfirmed_or_pending? diff --git a/app/models/webauthn_credential.rb b/app/models/webauthn_credential.rb index 4129ce53971..7d423e38d71 100644 --- a/app/models/webauthn_credential.rb +++ b/app/models/webauthn_credential.rb @@ -18,5 +18,5 @@ class WebauthnCredential < ApplicationRecord validates :external_id, uniqueness: true validates :nickname, uniqueness: { scope: :user_id } validates :sign_count, - numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 } + numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**63 - 1 } end diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb index 9c145979d6c..1b105e92aa8 100644 --- a/app/policies/account_policy.rb +++ b/app/policies/account_policy.rb @@ -17,6 +17,10 @@ class AccountPolicy < ApplicationPolicy staff? && !record.user&.staff? end + def destroy? + record.suspended? && record.deletion_request.present? && admin? + end + def unsuspend? staff? end diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index fa5c0dd9ced..d0359580df2 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -12,6 +12,7 @@ class StatusPolicy < ApplicationPolicy end def show? + return false if author.suspended? return false if local_only? && (current_account.nil? || !current_account.local?) if requires_mention? diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 4e497cdbdd2..5cc42c7cfb0 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -8,8 +8,11 @@ class REST::AccountSerializer < ActiveModel::Serializer :followers_count, :following_count, :statuses_count, :last_status_at has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested? + has_many :emojis, serializer: REST::CustomEmojiSerializer + attribute :suspended, if: :suspended? + class FieldSerializer < ActiveModel::Serializer attributes :name, :value, :verified_at @@ -29,7 +32,7 @@ class REST::AccountSerializer < ActiveModel::Serializer end def note - Formatter.instance.simplified_format(object) + object.suspended? ? '' : Formatter.instance.simplified_format(object) end def url @@ -37,23 +40,19 @@ class REST::AccountSerializer < ActiveModel::Serializer end def avatar - full_asset_url(object.avatar_original_url) + full_asset_url(object.suspended? ? object.avatar.default_url : object.avatar_original_url) end def avatar_static - full_asset_url(object.avatar_static_url) + full_asset_url(object.suspended? ? object.avatar.default_url : object.avatar_static_url) end def header - full_asset_url(object.header_original_url) + full_asset_url(object.suspended? ? object.header.default_url : object.header_original_url) end def header_static - full_asset_url(object.header_static_url) - end - - def moved_and_not_nested? - object.moved? && object.moved_to_account.moved_to_account_id.nil? + full_asset_url(object.suspended? ? object.header.default_url : object.header_static_url) end def last_status_at @@ -63,4 +62,42 @@ class REST::AccountSerializer < ActiveModel::Serializer def followers_count (Setting.hide_followers_count || object.user&.setting_hide_followers_count) ? -1 : object.followers_count end + + def display_name + object.suspended? ? '' : object.display_name + end + + def locked + object.suspended? ? false : object.locked + end + + def bot + object.suspended? ? false : object.bot + end + + def discoverable + object.suspended? ? false : object.discoverable + end + + def moved_to_account + object.suspended? ? nil : object.moved_to_account + end + + def emojis + object.suspended? ? [] : object.emojis + end + + def fields + object.suspended? ? [] : object.fields + end + + def suspended + object.suspended? + end + + delegate :suspended?, to: :object + + def moved_and_not_nested? + object.moved? && object.moved_to_account.moved_to_account_id.nil? + end end diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb index 80812ad0df9..27b031fcc77 100644 --- a/app/serializers/rest/notification_serializer.rb +++ b/app/serializers/rest/notification_serializer.rb @@ -11,6 +11,6 @@ class REST::NotificationSerializer < ActiveModel::Serializer end def status_type? - [:favourite, :reblog, :mention, :poll].include?(object.type) + [:favourite, :reblog, :status, :mention, :poll].include?(object.type) end end diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb index c2f3c9a1126..afd4cddf9b5 100644 --- a/app/serializers/rest/relationship_serializer.rb +++ b/app/serializers/rest/relationship_serializer.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true class REST::RelationshipSerializer < ActiveModel::Serializer - attributes :id, :following, :showing_reblogs, :followed_by, :blocking, :blocked_by, - :muting, :muting_notifications, :requested, :domain_blocking, - :endorsed, :note + attributes :id, :following, :showing_reblogs, :notifying, :followed_by, + :blocking, :blocked_by, :muting, :muting_notifications, :requested, + :domain_blocking, :endorsed, :note def id object.id.to_s @@ -19,6 +19,12 @@ class REST::RelationshipSerializer < ActiveModel::Serializer false end + def notifying + (instance_options[:relationships].following[object.id] || {})[:notify] || + (instance_options[:relationships].requested[object.id] || {})[:notify] || + false + end + def followed_by instance_options[:relationships].followed_by[object.id] || false end diff --git a/app/services/after_unallow_domain_service.rb b/app/services/after_unallow_domain_service.rb index ccd0b8ae919..d3008a1052f 100644 --- a/app/services/after_unallow_domain_service.rb +++ b/app/services/after_unallow_domain_service.rb @@ -3,7 +3,7 @@ class AfterUnallowDomainService < BaseService def call(domain) Account.where(domain: domain).find_each do |account| - SuspendAccountService.new.call(account, reserve_username: false) + DeleteAccountService.new.call(account, reserve_username: false) end end end diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index dc23ef8d8a8..1cf3382b352 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -36,7 +36,7 @@ class BlockDomainService < BaseService def suspend_accounts! blocked_domain_accounts.without_suspended.in_batches.update_all(suspended_at: @domain_block.created_at) blocked_domain_accounts.where(suspended_at: @domain_block.created_at).reorder(nil).find_each do |account| - SuspendAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at) + DeleteAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at) end end diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb new file mode 100644 index 00000000000..15bdd13e3d4 --- /dev/null +++ b/app/services/delete_account_service.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +class DeleteAccountService < BaseService + include Payloadable + + ASSOCIATIONS_ON_SUSPEND = %w( + account_pins + active_relationships + block_relationships + blocked_by_relationships + conversation_mutes + conversations + custom_filters + domain_blocks + favourites + follow_requests + list_accounts + mute_relationships + muted_by_relationships + notifications + owned_lists + passive_relationships + report_notes + scheduled_statuses + status_pins + ).freeze + + ASSOCIATIONS_ON_DESTROY = %w( + reports + targeted_moderation_notes + targeted_reports + ).freeze + + # Suspend or remove an account and remove as much of its data + # as possible. If it's a local account and it has not been confirmed + # or never been approved, then side effects are skipped and both + # the user and account records are removed fully. Otherwise, + # it is controlled by options. + # @param [Account] + # @param [Hash] options + # @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts + # @option [Boolean] :reserve_username Keep account record + # @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads + # @option [Time] :suspended_at Only applicable when :reserve_username is true + def call(account, **options) + @account = account + @options = { reserve_username: true, reserve_email: true }.merge(options) + + if @account.local? && @account.user_unconfirmed_or_pending? + @options[:reserve_email] = false + @options[:reserve_username] = false + @options[:skip_side_effects] = true + end + + reject_follows! + purge_user! + purge_profile! + purge_content! + fulfill_deletion_request! + end + + private + + def reject_follows! + return if @account.local? || !@account.activitypub? + + ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow| + [build_reject_json(follow), follow.target_account_id, follow.account.inbox_url] + end + end + + def purge_user! + return if !@account.local? || @account.user.nil? + + if @options[:reserve_email] + @account.user.disable! + @account.user.invites.where(uses: 0).destroy_all + else + @account.user.destroy + end + end + + def purge_content! + distribute_delete_actor! if @account.local? && !@options[:skip_side_effects] + + @account.statuses.reorder(nil).find_in_batches do |statuses| + statuses.reject! { |status| reported_status_ids.include?(status.id) } if @options[:reserve_username] + BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:skip_side_effects]) + end + + @account.media_attachments.reorder(nil).find_each do |media_attachment| + next if @options[:reserve_username] && reported_status_ids.include?(media_attachment.status_id) + + media_attachment.destroy + end + + @account.polls.reorder(nil).find_each do |poll| + next if @options[:reserve_username] && reported_status_ids.include?(poll.status_id) + + poll.destroy + end + + associations_for_destruction.each do |association_name| + destroy_all(@account.public_send(association_name)) + end + + @account.destroy unless @options[:reserve_username] + end + + def purge_profile! + # If the account is going to be destroyed + # there is no point wasting time updating + # its values first + + return unless @options[:reserve_username] + + @account.silenced_at = nil + @account.suspended_at = @options[:suspended_at] || Time.now.utc + @account.locked = false + @account.memorial = false + @account.discoverable = false + @account.display_name = '' + @account.note = '' + @account.fields = [] + @account.statuses_count = 0 + @account.followers_count = 0 + @account.following_count = 0 + @account.moved_to_account = nil + @account.trust_level = :untrusted + @account.avatar.destroy + @account.header.destroy + @account.save! + end + + def fulfill_deletion_request! + @account.deletion_request&.destroy + end + + def destroy_all(association) + association.in_batches.destroy_all + end + + def distribute_delete_actor! + ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url| + [delete_actor_json, @account.id, inbox_url] + end + + ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes) do |inbox_url| + [delete_actor_json, @account.id, inbox_url] + end + end + + def delete_actor_json + @delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account)) + end + + def build_reject_json(follow) + Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) + end + + def delivery_inboxes + @delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url) + end + + def low_priority_delivery_inboxes + Account.inboxes - delivery_inboxes + end + + def reported_status_ids + @reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq + end + + def associations_for_destruction + if @options[:reserve_username] + ASSOCIATIONS_ON_SUSPEND + else + ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY + end + end +end diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index 02b26458a6f..a0ab3b4b755 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -29,7 +29,7 @@ class FavouriteService < BaseService status = favourite.status if status.account.local? - NotifyService.new.call(status.account, favourite) + NotifyService.new.call(status.account, :favourite, favourite) elsif status.account.activitypub? ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url) end diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 311ae7fa688..9625728510c 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -9,12 +9,13 @@ class FollowService < BaseService # @param [String, Account] uri User URI to follow in the form of username@domain (or account record) # @param [Hash] options # @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true + # @option [Boolean] :notify Whether to create notifications about new posts, defaults to false # @option [Boolean] :bypass_locked # @option [Boolean] :with_rate_limit def call(source_account, target_account, options = {}) @source_account = source_account @target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true) - @options = { reblogs: true, bypass_locked: false, with_rate_limit: false }.merge(options) + @options = { bypass_locked: false, with_rate_limit: false }.merge(options) raise ActiveRecord::RecordNotFound if following_not_possible? raise Mastodon::NotPermittedError if following_not_allowed? @@ -45,18 +46,18 @@ class FollowService < BaseService end def change_follow_options! - @source_account.follow!(@target_account, reblogs: @options[:reblogs]) + @source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify]) end def change_follow_request_options! - @source_account.request_follow!(@target_account, reblogs: @options[:reblogs]) + @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify]) end def request_follow! - follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit]) + follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit]) if @target_account.local? - LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name) + LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, :follow_request) elsif @target_account.activitypub? ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url) end @@ -65,9 +66,9 @@ class FollowService < BaseService end def direct_follow! - follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit]) + follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit]) - LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name) + LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, :follow) MergeWorker.perform_async(@target_account.id, @source_account.id) follow diff --git a/app/services/import_service.rb b/app/services/import_service.rb index 4cad93767a4..7e55452de73 100644 --- a/app/services/import_service.rb +++ b/app/services/import_service.rb @@ -25,7 +25,7 @@ class ImportService < BaseService def import_follows! parse_import_data!(['Account address']) - import_relationships!('follow', 'unfollow', @account.following, follow_limit, reblogs: 'Show boosts') + import_relationships!('follow', 'unfollow', @account.following, follow_limit, reblogs: { header: 'Show boosts', default: true }) end def import_blocks! @@ -35,7 +35,7 @@ class ImportService < BaseService def import_mutes! parse_import_data!(['Account address']) - import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT, notifications: 'Hide notifications') + import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT, notifications: { header: 'Hide notifications', default: true }) end def import_domain_blocks! @@ -65,7 +65,7 @@ class ImportService < BaseService def import_relationships!(action, undo_action, overwrite_scope, limit, extra_fields = {}) local_domain_suffix = "@#{Rails.configuration.x.local_domain}" - items = @data.take(limit).map { |row| [row['Account address']&.strip&.delete_suffix(local_domain_suffix), Hash[extra_fields.map { |key, header| [key, row[header]&.strip] }]] }.reject { |(id, _)| id.blank? } + items = @data.take(limit).map { |row| [row['Account address']&.strip&.delete_suffix(local_domain_suffix), Hash[extra_fields.map { |key, field_settings| [key, row[field_settings[:header]]&.strip || field_settings[:default]] }]] }.reject { |(id, _)| id.blank? } if @import.overwrite? presence_hash = items.each_with_object({}) { |(id, extra), mapping| mapping[id] = [true, extra] } diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index e4ca10eb158..fc187db4030 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true class NotifyService < BaseService - def call(recipient, activity) + def call(recipient, type, activity) @recipient = recipient @activity = activity - @notification = Notification.new(account: @recipient, activity: @activity) + @notification = Notification.new(account: @recipient, type: type, activity: @activity) return if recipient.user.nil? || blocked? @@ -22,6 +22,10 @@ class NotifyService < BaseService FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient) end + def blocked_status? + false + end + def blocked_favourite? false end diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index f454229704d..feffb872bec 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -58,7 +58,7 @@ class ProcessMentionsService < BaseService mentioned_account = mention.account if mentioned_account.local? - LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name) + LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name, :mention) elsif mentioned_account.activitypub? && !@status.local_only? ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url) end diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 6cecb5ac428..6f018e24b40 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -45,7 +45,7 @@ class ReblogService < BaseService reblogged_status = reblog.reblog if reblogged_status.account.local? - LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name) + LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name, :reblog) elsif reblogged_status.account.activitypub? && !reblogged_status.account.following?(reblog.account) ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url) end diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index ecc893931d5..5a079c3acdd 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -1,175 +1,52 @@ # frozen_string_literal: true class SuspendAccountService < BaseService - include Payloadable - - ASSOCIATIONS_ON_SUSPEND = %w( - account_pins - active_relationships - block_relationships - blocked_by_relationships - conversation_mutes - conversations - custom_filters - domain_blocks - favourites - follow_requests - list_accounts - mute_relationships - muted_by_relationships - notifications - owned_lists - passive_relationships - report_notes - scheduled_statuses - status_pins - ).freeze - - ASSOCIATIONS_ON_DESTROY = %w( - reports - targeted_moderation_notes - targeted_reports - ).freeze - - # Suspend or remove an account and remove as much of its data - # as possible. If it's a local account and it has not been confirmed - # or never been approved, then side effects are skipped and both - # the user and account records are removed fully. Otherwise, - # it is controlled by options. - # @param [Account] - # @param [Hash] options - # @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts - # @option [Boolean] :reserve_username Keep account record - # @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads - # @option [Time] :suspended_at Only applicable when :reserve_username is true - def call(account, **options) + def call(account) @account = account - @options = { reserve_username: true, reserve_email: true }.merge(options) - if @account.local? && @account.user_unconfirmed_or_pending? - @options[:reserve_email] = false - @options[:reserve_username] = false - @options[:skip_side_effects] = true - end - - reject_follows! - purge_user! - purge_profile! - purge_content! + suspend! + unmerge_from_home_timelines! + unmerge_from_list_timelines! + privatize_media_attachments! end private - def reject_follows! - return if @account.local? || !@account.activitypub? + def suspend! + @account.suspend! unless @account.suspended? + end - ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow| - [build_reject_json(follow), follow.target_account_id, follow.account.inbox_url] + def unmerge_from_home_timelines! + @account.followers_for_local_distribution.find_each do |follower| + FeedManager.instance.unmerge_from_timeline(@account, follower) end end - def purge_user! - return if !@account.local? || @account.user.nil? - - if @options[:reserve_email] - @account.user.disable! - @account.user.invites.where(uses: 0).destroy_all - else - @account.user.destroy + def unmerge_from_list_timelines! + @account.lists_for_local_distribution.find_each do |list| + FeedManager.instance.unmerge_from_list(@account, list) end end - def purge_content! - distribute_delete_actor! if @account.local? && !@options[:skip_side_effects] + def privatize_media_attachments! + attachment_names = MediaAttachment.attachment_definitions.keys - @account.statuses.reorder(nil).find_in_batches do |statuses| - statuses.reject! { |status| reported_status_ids.include?(status.id) } if @options[:reserve_username] - BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:skip_side_effects]) - end + @account.media_attachments.find_each do |media_attachment| + attachment_names.each do |attachment_name| + attachment = media_attachment.public_send(attachment_name) + styles = [:original] | attachment.styles.keys - @account.media_attachments.reorder(nil).find_each do |media_attachment| - next if @options[:reserve_username] && reported_status_ids.include?(media_attachment.status_id) - - media_attachment.destroy - end - - @account.polls.reorder(nil).find_each do |poll| - next if @options[:reserve_username] && reported_status_ids.include?(poll.status_id) - - poll.destroy - end - - associations_for_destruction.each do |association_name| - destroy_all(@account.public_send(association_name)) - end - - @account.destroy unless @options[:reserve_username] - end - - def purge_profile! - # If the account is going to be destroyed - # there is no point wasting time updating - # its values first - - return unless @options[:reserve_username] - - @account.silenced_at = nil - @account.suspended_at = @options[:suspended_at] || Time.now.utc - @account.locked = false - @account.memorial = false - @account.discoverable = false - @account.display_name = '' - @account.note = '' - @account.fields = [] - @account.statuses_count = 0 - @account.followers_count = 0 - @account.following_count = 0 - @account.moved_to_account = nil - @account.trust_level = :untrusted - @account.avatar.destroy - @account.header.destroy - @account.save! - end - - def destroy_all(association) - association.in_batches.destroy_all - end - - def distribute_delete_actor! - ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url| - [delete_actor_json, @account.id, inbox_url] - end - - ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes) do |inbox_url| - [delete_actor_json, @account.id, inbox_url] - end - end - - def delete_actor_json - @delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account)) - end - - def build_reject_json(follow) - Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) - end - - def delivery_inboxes - @delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url) - end - - def low_priority_delivery_inboxes - Account.inboxes - delivery_inboxes - end - - def reported_status_ids - @reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq - end - - def associations_for_destruction - if @options[:reserve_username] - ASSOCIATIONS_ON_SUSPEND - else - ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY + styles.each do |style| + case Paperclip::Attachment.default_options[:storage] + when :s3 + attachment.s3_object(style).acl.put(:private) + when :fog + # Not supported + when :filesystem + FileUtils.chmod(0o600 & ~File.umask, attachment.path(style)) + end + end + end end end end diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb new file mode 100644 index 00000000000..3e731ddd9f7 --- /dev/null +++ b/app/services/unsuspend_account_service.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class UnsuspendAccountService < BaseService + def call(account) + @account = account + + unsuspend! + merge_into_home_timelines! + merge_into_list_timelines! + publish_media_attachments! + end + + private + + def unsuspend! + @account.unsuspend! if @account.suspended? + end + + def merge_into_home_timelines! + @account.followers_for_local_distribution.find_each do |follower| + FeedManager.instance.merge_into_timeline(@account, follower) + end + end + + def merge_into_list_timelines! + @account.lists_for_local_distribution.find_each do |list| + FeedManager.instance.merge_into_list(@account, list) + end + end + + def publish_media_attachments! + attachment_names = MediaAttachment.attachment_definitions.keys + + @account.media_attachments.find_each do |media_attachment| + attachment_names.each do |attachment_name| + attachment = media_attachment.public_send(attachment_name) + styles = [:original] | attachment.styles.keys + + styles.each do |style| + case Paperclip::Attachment.default_options[:storage] + when :s3 + attachment.s3_object(style).acl.put(Paperclip::Attachment.default_options[:s3_permissions]) + when :fog + # Not supported + when :filesystem + FileUtils.chmod(0o666 & ~File.umask, attachment.path(style)) + end + end + end + end + end +end diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index e6461aad04d..f0a216f6b8f 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -56,19 +56,21 @@ = link_to admin_action_logs_path(target_account_id: @account.id) do .dashboard__counters__text - if @account.local? && @account.user.nil? - %span.neutral= t('admin.accounts.deleted') + = t('admin.accounts.deleted') + - elsif @account.memorial? + = t('admin.accounts.memorialized') - elsif @account.suspended? - %span.red= t('admin.accounts.suspended') + = t('admin.accounts.suspended') - elsif @account.silenced? - %span.red= t('admin.accounts.silenced') + = t('admin.accounts.silenced') - elsif @account.local? && @account.user&.disabled? - %span.red= t('admin.accounts.disabled') + = t('admin.accounts.disabled') - elsif @account.local? && !@account.user&.confirmed? - %span.neutral= t('admin.accounts.confirming') + = t('admin.accounts.confirming') - elsif @account.local? && !@account.user_approved? - %span.neutral= t('admin.accounts.pending') + = t('admin.accounts.pending') - else - %span.neutral= t('admin.accounts.no_limits_imposed') + = t('admin.accounts.no_limits_imposed') .dashboard__counters__label= t 'admin.accounts.login_status' - unless @account.local? && @account.user.nil? @@ -122,19 +124,6 @@ = 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= t('admin.accounts.login_status') - %td - - if @account.user&.disabled? - = t('admin.accounts.disabled') - - else - = t('admin.accounts.enabled') - %td - - if @account.user&.disabled? - = table_link_to 'unlock', t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post if can?(:enable, @account.user) - - elsif @account.user_approved? - = table_link_to 'lock', t('admin.accounts.disable'), new_admin_account_action_path(@account.id, type: 'disable') if can?(:disable, @account.user) - %tr %th= t('simple_form.labels.defaults.locale') %td= @account.user_locale @@ -172,49 +161,62 @@ %td = @account.inbox_url = fa_icon DeliveryFailureTracker.available?(@account.inbox_url) ? 'check' : 'times' + %td + = table_link_to 'search', @domain_block.present? ? t('admin.domain_blocks.view') : t('admin.accounts.view_domain'), admin_instance_path(@account.domain) %tr %th= t('admin.accounts.shared_inbox_url') %td = @account.shared_inbox_url = fa_icon DeliveryFailureTracker.available?(@account.shared_inbox_url) ? 'check': 'times' + %td + - if @domain_block.nil? + = table_link_to 'ban', t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain) - %div.action-buttons - %div - - if @account.local? && @account.user_approved? - = link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account) - - if @account.silenced? - = link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account) - - elsif !@account.local? || @account.user_approved? - = link_to t('admin.accounts.silence'), new_admin_account_action_path(@account.id, type: 'silence'), class: 'button button--destructive' if can?(:silence, @account) + - if @account.suspended? + %hr.spacer/ - - if @account.local? - - if @account.user_pending? - = link_to t('admin.accounts.approve'), approve_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:approve, @account.user) - = link_to t('admin.accounts.reject'), reject_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:reject, @account.user) + %p.muted-hint= @deletion_request.present? ? t('admin.accounts.suspension_reversible_hint_html', date: content_tag(:strong, l(@deletion_request.due_at.to_date))) : t('admin.accounts.suspension_irreversible') - - unless @account.user_confirmed? - = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user) + = link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account) - - if @account.suspended? - = link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account) - - elsif !@account.local? || @account.user_approved? - = link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button button--destructive' if can?(:suspend, @account) + - if @deletion_request.present? + = link_to t('admin.accounts.delete'), admin_account_path(@account.id), method: :delete, class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, @account) + - else + %div.action-buttons + %div + - if @account.local? && @account.user_approved? + = link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account) - - unless @account.local? - - if DomainBlock.rule_for(@account.domain) - = link_to t('admin.domain_blocks.view'), admin_instance_path(@account.domain), class: 'button' + - if @account.user_disabled? + = link_to t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post, class: 'button' if can?(:enable, @account.user) + - else + = link_to t('admin.accounts.disable'), new_admin_account_action_path(@account.id, type: 'disable'), class: 'button' if can?(:disable, @account.user) + + - if @account.silenced? + = link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account) + - elsif !@account.local? || @account.user_approved? + = link_to t('admin.accounts.silence'), new_admin_account_action_path(@account.id, type: 'silence'), class: 'button' if can?(:silence, @account) + + - if @account.local? + - if @account.user_pending? + = link_to t('admin.accounts.approve'), approve_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:approve, @account.user) + = link_to t('admin.accounts.reject'), reject_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:reject, @account.user) + + - unless @account.user_confirmed? + = link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user) + + - if !@account.local? || @account.user_approved? + = link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button' if can?(:suspend, @account) + + %div + - if @account.local? + = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user) + - if @account.user&.otp_required_for_login? + = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user) + - if !@account.memorial? && @account.user_approved? + = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account) - else - = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive' - - %div - - if @account.local? - = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user) - - if @account.user&.otp_required_for_login? - = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user) - - if !@account.memorial? && @account.user_approved? - = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account) - - else - = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account) + = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account) %hr.spacer/ diff --git a/app/views/auth/registrations/_sessions.html.haml b/app/views/auth/registrations/_sessions.html.haml index 395e36a9fd6..d3a04c00e7e 100644 --- a/app/views/auth/registrations/_sessions.html.haml +++ b/app/views/auth/registrations/_sessions.html.haml @@ -27,5 +27,5 @@ - else %time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at) %td - - if current_session.session_id != session.session_id + - if current_session.session_id != session.session_id && !current_account.suspended? = table_link_to 'times', t('sessions.revoke'), settings_session_path(session), method: :delete diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index 4a46b27a93c..a3445b421aa 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -30,18 +30,19 @@ = render 'sessions' -%hr.spacer/ - -%h3= t('auth.migrate_account') -%p.muted-hint= t('auth.migrate_account_html', path: settings_migration_path) - -%hr.spacer/ - -%h3= t('migrations.incoming_migrations') -%p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path) - -- if open_deletion? && !current_account.suspended? +- unless current_account.suspended? %hr.spacer/ - %h3= t('auth.delete_account') - %p.muted-hint= t('auth.delete_account_html', path: settings_delete_path) + %h3= t('auth.migrate_account') + %p.muted-hint= t('auth.migrate_account_html', path: settings_migration_path) + + %hr.spacer/ + + %h3= t('migrations.incoming_migrations') + %p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path) + + - if open_deletion? + %hr.spacer/ + + %h3= t('auth.delete_account') + %p.muted-hint= t('auth.delete_account_html', path: settings_delete_path) diff --git a/app/views/oauth/authorized_applications/index.html.haml b/app/views/oauth/authorized_applications/index.html.haml index 7b77108a93c..fbb733db49f 100644 --- a/app/views/oauth/authorized_applications/index.html.haml +++ b/app/views/oauth/authorized_applications/index.html.haml @@ -20,5 +20,5 @@ %th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join(', ') %td= l application.created_at %td - - unless application.superapp? + - unless application.superapp? || current_account.suspended? = table_link_to 'times', t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), method: :delete, data: { confirm: t('doorkeeper.authorized_applications.confirmations.revoke') } diff --git a/app/views/statuses/show.html.haml b/app/views/statuses/show.html.haml index 873df7fbd1d..7ef7b09a2de 100644 --- a/app/views/statuses/show.html.haml +++ b/app/views/statuses/show.html.haml @@ -12,6 +12,7 @@ = opengraph 'og:type', 'article' = opengraph 'og:title', "#{display_name(@account)} (#{acct(@account)})" = opengraph 'og:url', short_account_status_url(@account, @status) + = opengraph 'og:published_time', @status.created_at.iso8601 = render 'og_description', activity: @status = render 'og_image', activity: @status, account: @account diff --git a/app/workers/account_deletion_worker.rb b/app/workers/account_deletion_worker.rb new file mode 100644 index 00000000000..0f6be71e193 --- /dev/null +++ b/app/workers/account_deletion_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AccountDeletionWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull' + + def perform(account_id) + DeleteAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: false) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/admin/account_deletion_worker.rb b/app/workers/admin/account_deletion_worker.rb new file mode 100644 index 00000000000..82f269ad6fb --- /dev/null +++ b/app/workers/admin/account_deletion_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class Admin::AccountDeletionWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull' + + def perform(account_id) + DeleteAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: true) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/admin/suspension_worker.rb b/app/workers/admin/suspension_worker.rb index 83c815efd7c..35c57033609 100644 --- a/app/workers/admin/suspension_worker.rb +++ b/app/workers/admin/suspension_worker.rb @@ -5,7 +5,9 @@ class Admin::SuspensionWorker sidekiq_options queue: 'pull' - def perform(account_id, remove_user = false) - SuspendAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: !remove_user) + def perform(account_id) + SuspendAccountService.new.call(Account.find(account_id)) + rescue ActiveRecord::RecordNotFound + true end end diff --git a/app/workers/admin/unsuspension_worker.rb b/app/workers/admin/unsuspension_worker.rb new file mode 100644 index 00000000000..7cb2349b16b --- /dev/null +++ b/app/workers/admin/unsuspension_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class Admin::UnsuspensionWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull' + + def perform(account_id) + UnsuspendAccountService.new.call(Account.find(account_id)) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/feed_insert_worker.rb b/app/workers/feed_insert_worker.rb index fd35af56212..45e6bb88dc9 100644 --- a/app/workers/feed_insert_worker.rb +++ b/app/workers/feed_insert_worker.rb @@ -25,7 +25,10 @@ class FeedInsertWorker private def check_and_insert - perform_push unless feed_filtered? + return if feed_filtered? + + perform_push + perform_notify if notify? end def feed_filtered? @@ -39,6 +42,12 @@ class FeedInsertWorker end end + def notify? + return false if @type != :home || @status.reblog? || (@status.reply? && @status.in_reply_to_account_id != @status.account_id) + + Follow.find_by(account: @follower, target_account: @status.account)&.notify? + end + def perform_push case @type when :home @@ -49,4 +58,8 @@ class FeedInsertWorker FeedManager.instance.push_to_direct(@account, @status) end end + + def perform_notify + NotifyService.new.call(@follower, :status, @status) + end end diff --git a/app/workers/local_notification_worker.rb b/app/workers/local_notification_worker.rb index 48635e498ff..6b08ca6fcf8 100644 --- a/app/workers/local_notification_worker.rb +++ b/app/workers/local_notification_worker.rb @@ -3,7 +3,7 @@ class LocalNotificationWorker include Sidekiq::Worker - def perform(receiver_account_id, activity_id = nil, activity_class_name = nil) + def perform(receiver_account_id, activity_id = nil, activity_class_name = nil, type = nil) if activity_id.nil? && activity_class_name.nil? activity = Mention.find(receiver_account_id) receiver = activity.account @@ -12,7 +12,7 @@ class LocalNotificationWorker activity = activity_class_name.constantize.find(activity_id) end - NotifyService.new.call(receiver, activity) + NotifyService.new.call(receiver, type || activity_class_name.underscore, activity) rescue ActiveRecord::RecordNotFound true end diff --git a/app/workers/poll_expiration_notify_worker.rb b/app/workers/poll_expiration_notify_worker.rb index 64b4cbd7e46..8a12fc075cc 100644 --- a/app/workers/poll_expiration_notify_worker.rb +++ b/app/workers/poll_expiration_notify_worker.rb @@ -11,12 +11,12 @@ class PollExpirationNotifyWorker # Notify poll owner and remote voters if poll.local? ActivityPub::DistributePollUpdateWorker.perform_async(poll.status.id) - NotifyService.new.call(poll.account, poll) + NotifyService.new.call(poll.account, :poll, poll) end # Notify local voters poll.votes.includes(:account).map(&:account).select(&:local?).each do |account| - NotifyService.new.call(account, poll) + NotifyService.new.call(account, :poll, poll) end rescue ActiveRecord::RecordNotFound true diff --git a/app/workers/refollow_worker.rb b/app/workers/refollow_worker.rb index 9b07ce1b56a..98940680d07 100644 --- a/app/workers/refollow_worker.rb +++ b/app/workers/refollow_worker.rb @@ -11,6 +11,7 @@ class RefollowWorker target_account.passive_relationships.where(account: Account.where(domain: nil)).includes(:account).reorder(nil).find_each do |follow| reblogs = follow.show_reblogs? + notify = follow.notify? # Locally unfollow remote account follower = follow.account @@ -18,7 +19,7 @@ class RefollowWorker # Schedule re-follow begin - FollowService.new.call(follower, target_account, reblogs: reblogs) + FollowService.new.call(follower, target_account, reblogs: reblogs, notify: notify) rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError next end diff --git a/app/workers/scheduler/user_cleanup_scheduler.rb b/app/workers/scheduler/user_cleanup_scheduler.rb index 6113edde17b..8571b59e140 100644 --- a/app/workers/scheduler/user_cleanup_scheduler.rb +++ b/app/workers/scheduler/user_cleanup_scheduler.rb @@ -6,9 +6,22 @@ class Scheduler::UserCleanupScheduler sidekiq_options lock: :until_executed, retry: 0 def perform + clean_unconfirmed_accounts! + clean_suspended_accounts! + end + + private + + def clean_unconfirmed_accounts! User.where('confirmed_at is NULL AND confirmation_sent_at <= ?', 2.days.ago).reorder(nil).find_in_batches do |batch| Account.where(id: batch.map(&:account_id)).delete_all User.where(id: batch.map(&:id)).delete_all end end + + def clean_suspended_accounts! + AccountDeletionRequest.where('created_at <= ?', AccountDeletionRequest::DELAY_TO_DELETION.ago).reorder(nil).find_each do |deletion_request| + Admin::AccountDeletionWorker.perform_async(deletion_request.account_id) + end + end end diff --git a/app/workers/unfollow_follow_worker.rb b/app/workers/unfollow_follow_worker.rb index b6e665a41fb..71b5a0e3fa5 100644 --- a/app/workers/unfollow_follow_worker.rb +++ b/app/workers/unfollow_follow_worker.rb @@ -10,10 +10,11 @@ class UnfollowFollowWorker old_target_account = Account.find(old_target_account_id) new_target_account = Account.find(new_target_account_id) - follow = follower_account.active_relationships.find_by(target_account: old_target_account) + follow = follower_account.active_relationships.find_by(target_account: old_target_account) reblogs = follow&.show_reblogs? + notify = follow&.notify? - FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, bypass_locked: bypass_locked) + FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, bypass_locked: bypass_locked) UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true) rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError true diff --git a/config/locales/en.yml b/config/locales/en.yml index ab96074fd56..427b2c3fca0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -98,6 +98,7 @@ en: add_email_domain_block: Block e-mail domain approve: Approve approve_all: Approve all + approved_msg: Successfully approved %{username}'s sign-up application are_you_sure: Are you sure? avatar: Avatar by_domain: Domain @@ -111,18 +112,21 @@ en: confirm: Confirm confirmed: Confirmed confirming: Confirming + delete: Delete data deleted: Deleted demote: Demote - disable: Disable + destroyed_msg: "%{username}'s data is now queued to be deleted imminently" + disable: Freeze disable_two_factor_authentication: Disable 2FA - disabled: Disabled + disabled: Frozen display_name: Display name domain: Domain edit: Edit email: Email email_status: Email status - enable: Enable + enable: Unfreeze enabled: Enabled + enabled_msg: Successfully unfroze %{username}'s account followers: Followers follows: Follows header: Header @@ -138,6 +142,8 @@ en: login_status: Login status media_attachments: Media attachments memorialize: Turn into memoriam + memorialized: Memorialized + memorialized_msg: Successfully turned %{username} into a memorial account moderation: active: Active all: All @@ -158,10 +164,14 @@ en: public: Public push_subscription_expires: PuSH subscription expires redownload: Refresh profile + redownloaded_msg: Successfully refreshed %{username}'s profile from origin reject: Reject reject_all: Reject all + rejected_msg: Successfully rejected %{username}'s sign-up application remove_avatar: Remove avatar remove_header: Remove header + removed_avatar_msg: Successfully removed %{username}'s avatar image + removed_header_msg: Successfully removed %{username}'s header image resend_confirmation: already_confirmed: This user is already confirmed send: Resend confirmation email @@ -182,18 +192,23 @@ en: show: created_reports: Made reports targeted_reports: Reported by others - silence: Silence - silenced: Silenced + silence: Limit + silenced: Limited statuses: Statuses subscribe: Subscribe suspended: Suspended + suspension_irreversible: The data of this account has been irreversibly deleted. You can unsuspend the account to make it usable but it will not recover any data it previously had. + suspension_reversible_hint_html: The account has been suspended, and the data will be fully removed on %{date}. Until then, the account can be restored without any ill effects. If you wish to remove all of the account's data immediately, you can do so below. time_in_queue: Waiting in queue %{time} title: Accounts unconfirmed_email: Unconfirmed email undo_silenced: Undo silence undo_suspension: Undo suspension + unsilenced_msg: Successfully unlimited %{username}'s account unsubscribe: Unsubscribe + unsuspended_msg: Successfully unsuspended %{username}'s account username: Username + view_domain: View summary for domain warn: Warn web: Web whitelisted: Allowed for federation @@ -1304,9 +1319,9 @@ en: title: Sign in attempt warning: explanation: - disable: While your account is frozen, your account data remains intact, but you cannot perform any actions until it is unlocked. - silence: While your account is limited, only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follow you. - suspend: Your account has been suspended, and all of your toots and your uploaded media files have been irreversibly removed from this server, and servers where you had followers. + disable: You can no longer login to your account or use it in any other way, but your profile and other data remains intact. + silence: You can still use your account but only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follow you. + suspend: You can no longer use your account, and your profile and other data are no longer accessible. You can still login to request a backup of your data until the data is fully removed, but we will retain some data to prevent you from evading the suspension. get_in_touch: You can reply to this e-mail to get in touch with the staff of %{instance}. review_server_policies: Review server policies statuses: 'Specifically, for:' diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 4ab0d1871d5..910e77ec2a3 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -90,10 +90,10 @@ en: text: Custom warning type: Action types: - disable: Disable login - none: Do nothing - silence: Silence - suspend: Suspend and irreversibly delete account data + disable: Freeze + none: Send a warning + silence: Limit + suspend: Suspend warning_preset_id: Use a warning preset announcement: all_day: All-day event diff --git a/config/navigation.rb b/config/navigation.rb index c82dfbb6db2..6ea267ab0e4 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -27,7 +27,7 @@ SimpleNavigation::Configuration.run do |navigation| 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 :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/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 end diff --git a/config/routes.rb b/config/routes.rb index 48d2718c873..25c9b93eca5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -234,7 +234,7 @@ Rails.application.routes.draw do resources :report_notes, only: [:create, :destroy] - resources :accounts, only: [:index, :show] do + resources :accounts, only: [:index, :show, :destroy] do member do post :enable post :unsilence @@ -474,7 +474,7 @@ Rails.application.routes.draw do end namespace :admin do - resources :accounts, only: [:index, :show] do + resources :accounts, only: [:index, :show, :destroy] do member do post :enable post :unsilence diff --git a/db/migrate/20200908193330_create_account_deletion_requests.rb b/db/migrate/20200908193330_create_account_deletion_requests.rb new file mode 100644 index 00000000000..e03183ae457 --- /dev/null +++ b/db/migrate/20200908193330_create_account_deletion_requests.rb @@ -0,0 +1,8 @@ +class CreateAccountDeletionRequests < ActiveRecord::Migration[5.2] + def change + create_table :account_deletion_requests do |t| + t.references :account, foreign_key: { on_delete: :cascade } + t.timestamps + end + end +end diff --git a/db/migrate/20200917192924_add_notify_to_follows.rb b/db/migrate/20200917192924_add_notify_to_follows.rb new file mode 100644 index 00000000000..d27471c447f --- /dev/null +++ b/db/migrate/20200917192924_add_notify_to_follows.rb @@ -0,0 +1,19 @@ +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddNotifyToFollows < ActiveRecord::Migration[5.1] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def up + safety_assured do + add_column_with_default :follows, :notify, :boolean, default: false, allow_null: false + add_column_with_default :follow_requests, :notify, :boolean, default: false, allow_null: false + end + end + + def down + remove_column :follows, :notify + remove_column :follow_requests, :notify + end +end diff --git a/db/migrate/20200917193034_add_type_to_notifications.rb b/db/migrate/20200917193034_add_type_to_notifications.rb new file mode 100644 index 00000000000..002be3aa00b --- /dev/null +++ b/db/migrate/20200917193034_add_type_to_notifications.rb @@ -0,0 +1,5 @@ +class AddTypeToNotifications < ActiveRecord::Migration[5.2] + def change + add_column :notifications, :type, :string + end +end diff --git a/db/migrate/20200917222316_add_index_notifications_on_type.rb b/db/migrate/20200917222316_add_index_notifications_on_type.rb new file mode 100644 index 00000000000..9bd23c1d3d9 --- /dev/null +++ b/db/migrate/20200917222316_add_index_notifications_on_type.rb @@ -0,0 +1,7 @@ +class AddIndexNotificationsOnType < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + add_index :notifications, [:account_id, :id, :type], order: { id: :desc }, algorithm: :concurrently + end +end diff --git a/db/post_migrate/20200917193528_migrate_notifications_type.rb b/db/post_migrate/20200917193528_migrate_notifications_type.rb new file mode 100644 index 00000000000..88e4230842b --- /dev/null +++ b/db/post_migrate/20200917193528_migrate_notifications_type.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class MigrateNotificationsType < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + TYPES_TO_MIGRATE = { + 'Mention' => :mention, + 'Status' => :reblog, + 'Follow' => :follow, + 'FollowRequest' => :follow_request, + 'Favourite' => :favourite, + 'Poll' => :poll, + }.freeze + + def up + TYPES_TO_MIGRATE.each_pair do |activity_type, type| + Notification.where(activity_type: activity_type, type: nil).in_batches.update_all(type: type) + end + end + + def down; end +end diff --git a/db/post_migrate/20200917222734_remove_index_notifications_on_account_activity.rb b/db/post_migrate/20200917222734_remove_index_notifications_on_account_activity.rb new file mode 100644 index 00000000000..cb7f78e53bf --- /dev/null +++ b/db/post_migrate/20200917222734_remove_index_notifications_on_account_activity.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class RemoveIndexNotificationsOnAccountActivity < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def up + remove_index :notifications, name: :account_activity + remove_index :notifications, name: :index_notifications_on_account_id_and_id + end + + def down + add_index :notifications, [:account_id, :activity_id, :activity_type], unique: true, name: 'account_activity', algorithm: :concurrently + add_index :notifications, [:account_id, :id], order: { id: :desc }, algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index b56aa109a52..9678b9d7978 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_06_30_190544) do +ActiveRecord::Schema.define(version: 2020_09_17_222734) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -36,6 +36,13 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do t.index ["conversation_id"], name: "index_account_conversations_on_conversation_id" end + create_table "account_deletion_requests", force: :cascade do |t| + t.bigint "account_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_account_deletion_requests_on_account_id" + end + create_table "account_domain_blocks", force: :cascade do |t| t.string "domain" t.datetime "created_at", null: false @@ -404,6 +411,7 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do t.bigint "target_account_id", null: false t.boolean "show_reblogs", default: true, null: false t.string "uri" + t.boolean "notify", default: false, null: false t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true end @@ -414,6 +422,7 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do t.bigint "target_account_id", null: false t.boolean "show_reblogs", default: true, null: false t.string "uri" + t.boolean "notify", default: false, null: false t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true t.index ["target_account_id"], name: "index_follows_on_target_account_id" end @@ -538,8 +547,8 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do t.datetime "updated_at", null: false t.bigint "account_id", null: false t.bigint "from_account_id", null: false - t.index ["account_id", "activity_id", "activity_type"], name: "account_activity", unique: true - t.index ["account_id", "id"], name: "index_notifications_on_account_id_and_id", order: { id: :desc } + t.string "type" + t.index ["account_id", "id", "type"], name: "index_notifications_on_account_id_and_id_and_type", order: { id: :desc } t.index ["activity_id", "activity_type"], name: "index_notifications_on_activity_id_and_activity_type" t.index ["from_account_id"], name: "index_notifications_on_from_account_id" end @@ -928,6 +937,7 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do add_foreign_key "account_aliases", "accounts", on_delete: :cascade add_foreign_key "account_conversations", "accounts", on_delete: :cascade add_foreign_key "account_conversations", "conversations", on_delete: :cascade + add_foreign_key "account_deletion_requests", "accounts", on_delete: :cascade add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade add_foreign_key "account_identity_proofs", "accounts", on_delete: :cascade add_foreign_key "account_migrations", "accounts", column: "target_account_id", on_delete: :nullify diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb index 8c91c301358..8f9279a3c05 100644 --- a/lib/mastodon/accounts_cli.rb +++ b/lib/mastodon/accounts_cli.rb @@ -87,7 +87,7 @@ module Mastodon say('Use --force to reattach it anyway and delete the other user') return elsif account.user.present? - account.user.destroy! + DeleteAccountService.new.call(account, reserve_email: false) end end @@ -192,7 +192,7 @@ module Mastodon end say("Deleting user with #{account.statuses_count} statuses, this might take a while...") - SuspendAccountService.new.call(account, reserve_email: false) + DeleteAccountService.new.call(account, reserve_email: false) say('OK', :green) end diff --git a/lib/mastodon/domains_cli.rb b/lib/mastodon/domains_cli.rb index 558737c273e..5433ddd9d7e 100644 --- a/lib/mastodon/domains_cli.rb +++ b/lib/mastodon/domains_cli.rb @@ -42,7 +42,7 @@ module Mastodon end processed, = parallelize_with_progress(scope) do |account| - SuspendAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless options[:dry_run] + DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless options[:dry_run] end DomainBlock.where(domain: domains).destroy_all unless options[:dry_run] diff --git a/lib/paperclip/attachment_extensions.rb b/lib/paperclip/attachment_extensions.rb index 93df0a326f0..752e79e65ed 100644 --- a/lib/paperclip/attachment_extensions.rb +++ b/lib/paperclip/attachment_extensions.rb @@ -35,6 +35,10 @@ module Paperclip formats.include?(other_extension.delete('.')) && File.basename(other_filename, other_extension) == File.basename(original_filename, File.extname(original_filename)) end + + def default_url(style_name = default_style) + @url_generator.for_as_default(style_name) + end end end diff --git a/lib/paperclip/response_with_limit_adapter.rb b/lib/paperclip/response_with_limit_adapter.rb index 8711b13497a..17a2abd25f2 100644 --- a/lib/paperclip/response_with_limit_adapter.rb +++ b/lib/paperclip/response_with_limit_adapter.rb @@ -16,7 +16,7 @@ module Paperclip private def cache_current_values - @original_filename = filename_from_content_disposition || filename_from_path || 'data' + @original_filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data' @size = @target.response.content_length @tempfile = copy_to_tempfile(@target) @content_type = ContentTypeDetector.new(@tempfile.path).detect diff --git a/lib/paperclip/url_generator_extensions.rb b/lib/paperclip/url_generator_extensions.rb index 1079efdbc4f..e1d6df2c299 100644 --- a/lib/paperclip/url_generator_extensions.rb +++ b/lib/paperclip/url_generator_extensions.rb @@ -11,6 +11,10 @@ module Paperclip Addressable::URI.parse(url).normalize.to_str.gsub(escape_regex) { |m| "%#{m.ord.to_s(16).upcase}" } end end + + def for_as_default(style_name) + attachment_options[:interpolator].interpolate(default_url, @attachment, style_name) + end end end diff --git a/lib/tasks/emojis.rake b/lib/tasks/emojis.rake index 2ac8bc05995..d0b8fa890b3 100644 --- a/lib/tasks/emojis.rake +++ b/lib/tasks/emojis.rake @@ -91,7 +91,7 @@ namespace :emojis do desc 'Generate emoji variants with white borders' task :generate_borders do src = Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json') - emojis = '๐ŸŽฑ๐Ÿœโšซ๐Ÿ–คโฌ›โ—ผ๏ธโ—พโ—ผ๏ธโœ’๏ธโ–ช๏ธ๐Ÿ’ฃ๐ŸŽณ๐Ÿ“ท๐Ÿ“ธโ™ฃ๏ธ๐Ÿ•ถ๏ธโœด๏ธ๐Ÿ”Œ๐Ÿ’‚โ€โ™€๏ธ๐Ÿ“ฝ๏ธ๐Ÿณ๐Ÿฆ๐Ÿ’‚๐Ÿ”ช๐Ÿ•ณ๏ธ๐Ÿ•น๏ธ๐Ÿ•‹๐Ÿ–Š๏ธ๐Ÿ–‹๏ธ๐Ÿ’‚โ€โ™‚๏ธ๐ŸŽค๐ŸŽ“๐ŸŽฅ๐ŸŽผโ™ ๏ธ๐ŸŽฉ๐Ÿฆƒ๐Ÿ“ผ๐Ÿ“น๐ŸŽฎ๐Ÿƒ๐Ÿด๐Ÿž๐Ÿ‘ฝโšพ๐Ÿ”โ˜๏ธ๐Ÿ’จ๐Ÿ•Š๏ธ๐Ÿ‘€๐Ÿฅ๐Ÿ‘ป๐Ÿโ•โ”โ›ธ๏ธ๐ŸŒฉ๏ธ๐Ÿ”Š๐Ÿ”‡๐Ÿ“ƒ๐ŸŒง๏ธ๐Ÿ๐Ÿš๐Ÿ™๐Ÿ“๐Ÿ‘๐Ÿ’€โ˜ ๏ธ๐ŸŒจ๏ธ๐Ÿ”‰๐Ÿ”ˆ๐Ÿ’ฌ๐Ÿ’ญ๐Ÿ๐Ÿณ๏ธโšชโฌœโ—ฝโ—ป๏ธโ–ซ๏ธ' + emojis = '๐ŸŽฑ๐Ÿœโšซ๐Ÿ–คโฌ›โ—ผ๏ธโ—พโ—ผ๏ธโœ’๏ธโ–ช๏ธ๐Ÿ’ฃ๐ŸŽณ๐Ÿ“ท๐Ÿ“ธโ™ฃ๏ธ๐Ÿ•ถ๏ธโœด๏ธ๐Ÿ”Œ๐Ÿ’‚โ€โ™€๏ธ๐Ÿ“ฝ๏ธ๐Ÿณ๐Ÿฆ๐Ÿ’‚๐Ÿ”ช๐Ÿ•ณ๏ธ๐Ÿ•น๏ธ๐Ÿ•‹๐Ÿ–Š๏ธ๐Ÿ–‹๏ธ๐Ÿ’‚โ€โ™‚๏ธ๐ŸŽค๐ŸŽ“๐ŸŽฅ๐ŸŽผโ™ ๏ธ๐ŸŽฉ๐Ÿฆƒ๐Ÿ“ผ๐Ÿ“น๐ŸŽฎ๐Ÿƒ๐Ÿด๐Ÿž๐Ÿ•บ๐Ÿ‘ฝโšพ๐Ÿ”โ˜๏ธ๐Ÿ’จ๐Ÿ•Š๏ธ๐Ÿ‘€๐Ÿฅ๐Ÿ‘ป๐Ÿโ•โ”โ›ธ๏ธ๐ŸŒฉ๏ธ๐Ÿ”Š๐Ÿ”‡๐Ÿ“ƒ๐ŸŒง๏ธ๐Ÿ๐Ÿš๐Ÿ™๐Ÿ“๐Ÿ‘๐Ÿ’€โ˜ ๏ธ๐ŸŒจ๏ธ๐Ÿ”‰๐Ÿ”ˆ๐Ÿ’ฌ๐Ÿ’ญ๐Ÿ๐Ÿณ๏ธโšชโฌœโ—ฝโ—ป๏ธโ–ซ๏ธ' map = Oj.load(File.read(src)) diff --git a/package.json b/package.json index 11e2b5c8461..30dfbc47125 100644 --- a/package.json +++ b/package.json @@ -65,18 +65,18 @@ "@babel/plugin-proposal-decorators": "^7.10.5", "@babel/plugin-transform-react-inline-elements": "^7.10.4", "@babel/plugin-transform-runtime": "^7.11.5", - "@babel/preset-env": "^7.11.0", + "@babel/preset-env": "^7.11.5", "@babel/preset-react": "^7.10.4", "@babel/runtime": "^7.11.2", "@clusterws/cws": "^3.0.0", "@gamestdio/websocket": "^0.3.2", - "@github/webauthn-json": "^0.4.2", + "@github/webauthn-json": "^0.5.4", "@rails/ujs": "^6.0.3", "array-includes": "^3.1.1", "atrament": "0.2.4", "arrow-key-navigation": "^1.2.0", "autoprefixer": "^9.8.6", - "axios": "^0.19.2", + "axios": "^0.20.0", "babel-loader": "^8.1.0", "babel-plugin-lodash": "^3.3.4", "babel-plugin-preval": "^5.0.0", @@ -87,9 +87,9 @@ "classnames": "^2.2.5", "compression-webpack-plugin": "^5.0.1", "cross-env": "^7.0.2", - "css-loader": "^4.2.2", + "css-loader": "^4.3.0", "cssnano": "^4.1.10", - "detect-passive-events": "^1.0.2", + "detect-passive-events": "^1.0.5", "dotenv": "^8.2.0", "emoji-mart": "Gargron/emoji-mart#build", "es6-symbol": "^3.1.3", @@ -97,7 +97,7 @@ "exif-js": "^2.3.0", "express": "^4.17.1", "favico.js": "^0.3.10", - "file-loader": "^6.0.0", + "file-loader": "^6.1.0", "font-awesome": "^4.7.0", "glob": "^7.1.6", "history": "^4.10.1", @@ -160,12 +160,12 @@ "stacktrace-js": "^2.0.2", "stringz": "^2.1.0", "substring-trie": "^1.0.2", - "terser-webpack-plugin": "^3.0.6", + "terser-webpack-plugin": "^4.2.2", "tesseract.js": "^2.1.1", "throng": "^4.0.0", "tiny-queue": "^0.2.1", "uuid": "^8.2.0", - "webpack": "^4.44.1", + "webpack": "^4.44.2", "webpack-assets-manifest": "^3.1.1", "webpack-bundle-analyzer": "^3.8.0", "webpack-cli": "^3.3.12", @@ -188,7 +188,7 @@ "react-test-renderer": "^16.13.1", "sass-lint": "^1.13.1", "webpack-dev-server": "^3.11.0", - "yargs": "^15.4.1" + "yargs": "^16.0.3" }, "resolutions": { "kind-of": "^6.0.3" diff --git a/public/emoji/1f57a_border.svg b/public/emoji/1f57a_border.svg new file mode 100644 index 00000000000..7d3729976cf --- /dev/null +++ b/public/emoji/1f57a_border.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index 93bf2c83f40..b04f4650bc4 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -348,24 +348,8 @@ RSpec.describe AccountsController, type: :controller do context 'in authorized fetch mode' do let(:authorized_fetch_mode) { true } - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'returns application/activity+json' do - expect(response.content_type).to eq 'application/activity+json' - end - - it_behaves_like 'cachable response' - - it 'returns Vary header with Signature' do - expect(response.headers['Vary']).to include 'Signature' - end - - it 'renders bare minimum account' do - json = body_as_json - expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey) - expect(json).to_not include(:name, :summary) + it 'returns http unauthorized' do + expect(response).to have_http_status(401) end end end diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 024409dab85..1e656503fbc 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -71,50 +71,80 @@ RSpec.describe Api::V1::AccountsController, type: :controller do let(:scopes) { 'write:follows' } let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', locked: locked)).account } - before do - post :follow, params: { id: other_account.id } - end - - context 'with unlocked account' do - let(:locked) { false } - - it 'returns http success' do - expect(response).to have_http_status(200) + context do + before do + post :follow, params: { id: other_account.id } end - it 'returns JSON with following=true and requested=false' do + context 'with unlocked account' do + let(:locked) { false } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns JSON with following=true and requested=false' do + json = body_as_json + + expect(json[:following]).to be true + expect(json[:requested]).to be false + end + + it 'creates a following relation between user and target user' do + expect(user.account.following?(other_account)).to be true + end + + it_behaves_like 'forbidden for wrong scope', 'read:accounts' + end + + context 'with locked account' do + let(:locked) { true } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns JSON with following=false and requested=true' do + json = body_as_json + + expect(json[:following]).to be false + expect(json[:requested]).to be true + end + + it 'creates a follow request relation between user and target user' do + expect(user.account.requested?(other_account)).to be true + end + + it_behaves_like 'forbidden for wrong scope', 'read:accounts' + end + end + + context 'modifying follow options' do + let(:locked) { false } + + before do + user.account.follow!(other_account, reblogs: false, notify: false) + end + + it 'changes reblogs option' do + post :follow, params: { id: other_account.id, reblogs: true } + json = body_as_json expect(json[:following]).to be true - expect(json[:requested]).to be false + expect(json[:showing_reblogs]).to be true + expect(json[:notifying]).to be false end - it 'creates a following relation between user and target user' do - expect(user.account.following?(other_account)).to be true - end + it 'changes notify option' do + post :follow, params: { id: other_account.id, notify: true } - it_behaves_like 'forbidden for wrong scope', 'read:accounts' - end - - context 'with locked account' do - let(:locked) { true } - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'returns JSON with following=false and requested=true' do json = body_as_json - expect(json[:following]).to be false - expect(json[:requested]).to be true + expect(json[:following]).to be true + expect(json[:showing_reblogs]).to be false + expect(json[:notifying]).to be true end - - it 'creates a follow request relation between user and target user' do - expect(user.account.requested?(other_account)).to be true - end - - it_behaves_like 'forbidden for wrong scope', 'read:accounts' end end diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb index c2e9f33a82a..bef82276372 100644 --- a/spec/controllers/auth/registrations_controller_spec.rb +++ b/spec/controllers/auth/registrations_controller_spec.rb @@ -199,9 +199,10 @@ RSpec.describe Auth::RegistrationsController, type: :controller do end subject do + inviter = Fabricate(:user, confirmed_at: 2.days.ago) Setting.registrations_mode = 'approved' request.headers["Accept-Language"] = accept_language - invite = Fabricate(:invite, max_uses: nil, expires_at: 1.hour.from_now) + invite = Fabricate(:invite, user: inviter, max_uses: nil, expires_at: 1.hour.from_now) post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code, agreement: 'true' } } end diff --git a/spec/controllers/concerns/export_controller_concern_spec.rb b/spec/controllers/concerns/export_controller_concern_spec.rb index e5861c801f5..fce129bee22 100644 --- a/spec/controllers/concerns/export_controller_concern_spec.rb +++ b/spec/controllers/concerns/export_controller_concern_spec.rb @@ -5,6 +5,7 @@ require 'rails_helper' describe ApplicationController, type: :controller do controller do include ExportControllerConcern + def index send_export_file end diff --git a/spec/controllers/settings/deletes_controller_spec.rb b/spec/controllers/settings/deletes_controller_spec.rb index 996872efd1a..8d5c4774fd8 100644 --- a/spec/controllers/settings/deletes_controller_spec.rb +++ b/spec/controllers/settings/deletes_controller_spec.rb @@ -77,6 +77,20 @@ describe Settings::DeletesController do expect(response).to redirect_to settings_delete_path end end + + context 'when account deletions are disabled' do + around do |example| + open_deletion = Setting.open_deletion + example.run + Setting.open_deletion = open_deletion + end + + it 'redirects' do + Setting.open_deletion = false + delete :destroy + expect(response).to redirect_to root_path + end + end end context 'when not signed in' do @@ -85,19 +99,5 @@ describe Settings::DeletesController do expect(response).to redirect_to '/auth/sign_in' end end - - context do - around do |example| - open_deletion = Setting.open_deletion - example.run - Setting.open_deletion = open_deletion - end - - it 'redirects' do - Setting.open_deletion = false - delete :destroy - expect(response).to redirect_to root_path - end - end end end diff --git a/spec/fabricators/account_deletion_request_fabricator.rb b/spec/fabricators/account_deletion_request_fabricator.rb new file mode 100644 index 00000000000..08a82ba3c33 --- /dev/null +++ b/spec/fabricators/account_deletion_request_fabricator.rb @@ -0,0 +1,3 @@ +Fabricator(:account_deletion_request) do + account +end diff --git a/spec/models/account_deletion_request_spec.rb b/spec/models/account_deletion_request_spec.rb new file mode 100644 index 00000000000..afaecbe2289 --- /dev/null +++ b/spec/models/account_deletion_request_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe AccountDeletionRequest, type: :model do +end diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb index 36e346f1400..68eb9c027e6 100644 --- a/spec/models/concerns/account_interactions_spec.rb +++ b/spec/models/concerns/account_interactions_spec.rb @@ -14,7 +14,7 @@ describe AccountInteractions do context 'account with Follow' do it 'returns { target_account_id => { reblogs: true } }' do Fabricate(:follow, account: account, target_account: target_account) - is_expected.to eq(target_account_id => { reblogs: true }) + is_expected.to eq(target_account_id => { reblogs: true, notify: false }) end end diff --git a/spec/models/follow_request_spec.rb b/spec/models/follow_request_spec.rb index 4b824c0db64..7c8e121d976 100644 --- a/spec/models/follow_request_spec.rb +++ b/spec/models/follow_request_spec.rb @@ -7,7 +7,7 @@ RSpec.describe FollowRequest, type: :model do let(:target_account) { Fabricate(:account) } it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do - expect(account).to receive(:follow!).with(target_account, reblogs: true, uri: follow_request.uri) + expect(account).to receive(:follow!).with(target_account, reblogs: true, notify: false, uri: follow_request.uri) expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id) expect(follow_request).to receive(:destroy!) follow_request.authorize! diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb index 30abfb86bf4..b0596c56123 100644 --- a/spec/models/invite_spec.rb +++ b/spec/models/invite_spec.rb @@ -29,7 +29,7 @@ RSpec.describe Invite, type: :model do it 'returns false when invite creator has been disabled' do invite = Fabricate(:invite, max_uses: nil, expires_at: nil) - SuspendAccountService.new.call(invite.user.account) + invite.user.account.suspend! expect(invite.valid_for_use?).to be false end end diff --git a/spec/models/webauthn_credentials_spec.rb b/spec/models/webauthn_credentials_spec.rb index 9289c371e65..a63ae6cd248 100644 --- a/spec/models/webauthn_credentials_spec.rb +++ b/spec/models/webauthn_credentials_spec.rb @@ -69,8 +69,8 @@ RSpec.describe WebauthnCredential, type: :model do expect(webauthn_credential).to model_have_error_on_field(:sign_count) end - it 'is invalid if sign_count is greater 2**32 - 1' do - webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: 2**32) + it 'is invalid if sign_count is greater 2**63 - 1' do + webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: 2**63) webauthn_credential.valid? diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/delete_account_service_spec.rb similarity index 98% rename from spec/services/suspend_account_service_spec.rb rename to spec/services/delete_account_service_spec.rb index 32726d76397..d208b25b849 100644 --- a/spec/services/suspend_account_service_spec.rb +++ b/spec/services/delete_account_service_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe SuspendAccountService, type: :service do +RSpec.describe DeleteAccountService, type: :service do describe '#call on local account' do before do stub_request(:post, "https://alice.com/inbox").to_return(status: 201) diff --git a/spec/services/import_service_spec.rb b/spec/services/import_service_spec.rb index 7618e907618..b1909d4fd14 100644 --- a/spec/services/import_service_spec.rb +++ b/spec/services/import_service_spec.rb @@ -95,6 +95,7 @@ RSpec.describe ImportService, type: :service do let(:import) { Import.create(account: account, type: 'following', data: csv) } it 'follows the listed accounts, including boosts' do subject.call(import) + expect(account.following.count).to eq 1 expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb index b09d335ba38..118436f8ba5 100644 --- a/spec/services/notify_service_spec.rb +++ b/spec/services/notify_service_spec.rb @@ -2,13 +2,14 @@ require 'rails_helper' RSpec.describe NotifyService, type: :service do subject do - -> { described_class.new.call(recipient, activity) } + -> { described_class.new.call(recipient, type, activity) } end let(:user) { Fabricate(:user) } let(:recipient) { user.account } let(:sender) { Fabricate(:account, domain: 'example.com') } let(:activity) { Fabricate(:follow, account: sender, target_account: recipient) } + let(:type) { :follow } it { is_expected.to change(Notification, :count).by(1) } @@ -50,6 +51,7 @@ RSpec.describe NotifyService, type: :service do context 'for direct messages' do let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct)) } + let(:type) { :mention } before do user.settings.interactions = user.settings.interactions.merge('must_be_following_dm' => enabled) @@ -93,6 +95,7 @@ RSpec.describe NotifyService, type: :service do describe 'reblogs' do let(:status) { Fabricate(:status, account: Fabricate(:account)) } let(:activity) { Fabricate(:status, account: sender, reblog: status) } + let(:type) { :reblog } it 'shows reblogs by default' do recipient.follow!(sender) @@ -114,6 +117,7 @@ RSpec.describe NotifyService, type: :service do let(:asshole) { Fabricate(:account, username: 'asshole') } let(:reply_to) { Fabricate(:status, account: asshole) } let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, thread: reply_to)) } + let(:type) { :mention } it 'does not notify when conversation is muted' do recipient.mute_conversation!(activity.status.conversation) diff --git a/spec/workers/refollow_worker_spec.rb b/spec/workers/refollow_worker_spec.rb index 29771aa59b7..6b4c042912b 100644 --- a/spec/workers/refollow_worker_spec.rb +++ b/spec/workers/refollow_worker_spec.rb @@ -23,8 +23,8 @@ describe RefollowWorker do result = subject.perform(account.id) expect(result).to be_nil - expect(service).to have_received(:call).with(alice, account, reblogs: true) - expect(service).to have_received(:call).with(bob, account, reblogs: false) + expect(service).to have_received(:call).with(alice, account, reblogs: true, notify: false) + expect(service).to have_received(:call).with(bob, account, reblogs: false, notify: false) end end end diff --git a/streaming/index.js b/streaming/index.js index 3a68e6de5f6..de2175144ce 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -81,6 +81,19 @@ const redisUrlToClient = (defaultConfig, redisUrl) => { const numWorkers = +process.env.STREAMING_CLUSTER_NUM || (env === 'development' ? 1 : Math.max(os.cpus().length - 1, 1)); +/** + * @param {string} json + * @return {Object.|null} + */ +const parseJSON = (json) => { + try { + return JSON.parse(json); + } catch (err) { + log.error(err); + return null; + } +}; + const startMaster = () => { if (!process.env.SOCKET && process.env.PORT && isNaN(+process.env.PORT)) { log.warn('UNIX domain socket is now supported by using SOCKET. Please migrate from PORT hack.'); @@ -524,7 +537,9 @@ const startWorker = (workerId) => { log.verbose(req.requestId, `Starting stream from ${ids.join(', ')} for ${accountId}${streamType}`); const listener = message => { - const { event, payload, queued_at } = JSON.parse(message); + const json = parseJSON(message); + if (!json) return; + const { event, payload, queued_at } = json; const transmit = () => { const now = new Date().getTime(); @@ -954,7 +969,9 @@ const startWorker = (workerId) => { ws.on('error', onEnd); ws.on('message', data => { - const { type, stream, ...params } = JSON.parse(data); + const json = parseJSON(data); + if (!json) return; + const { type, stream, ...params } = json; if (type === 'subscribe') { subscribeWebsocketToChannel(session, firstParam(stream), params); diff --git a/yarn.lock b/yarn.lock index 6978601ad31..04a3020ed59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -842,10 +842,10 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/preset-env@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796" - integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg== +"@babel/preset-env@^7.11.5": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.5.tgz#18cb4b9379e3e92ffea92c07471a99a2914e4272" + integrity sha512-kXqmW1jVcnB2cdueV+fyBM8estd5mlNfaQi6lwLgRwCby4edpavgbFhiBNjmWA3JpB/yZGSISa7Srf+TwxDQoA== dependencies: "@babel/compat-data" "^7.11.0" "@babel/helper-compilation-targets" "^7.10.4" @@ -909,7 +909,7 @@ "@babel/plugin-transform-unicode-escapes" "^7.10.4" "@babel/plugin-transform-unicode-regex" "^7.10.4" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.11.0" + "@babel/types" "^7.11.5" browserslist "^4.12.0" core-js-compat "^3.6.2" invariant "^2.2.2" @@ -1112,10 +1112,10 @@ resolved "https://registry.yarnpkg.com/@gamestdio/websocket/-/websocket-0.3.2.tgz#321ba0976ee30fd14e51dbf8faa85ce7b325f76a" integrity sha512-J3n5SKim+ZoLbe44hRGI/VYAwSMCeIJuBy+FfP6EZaujEpNchPRFcIsVQLWAwpU1bP2Ji63rC+rEUOd1vjUB6Q== -"@github/webauthn-json@^0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@github/webauthn-json/-/webauthn-json-0.4.2.tgz#573bba7f30f035d82a6b6040430eb4e9729db849" - integrity sha512-10RwfEzpg0y68Coj480tYE4KajRO39ii0652bWbKM0OfXlV9szw6N5vMwnNzelvAMigcEDiAtFcvFCvB8GnTtA== +"@github/webauthn-json@^0.5.4": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@github/webauthn-json/-/webauthn-json-0.5.4.tgz#fe4f47647c2b29e42e33f6f30484fec7e5adfeb1" + integrity sha512-lZi5cSZi2F08a2kmjxr/FU13ILenpxkZJUWo1p3hHAmMHLLr5GAVlkCJmvWeIsIiAp65AHh3dQIZSMPIylcClw== "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" @@ -1482,9 +1482,9 @@ integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== "@types/node@*": - version "14.6.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.0.tgz#7d4411bf5157339337d7cff864d9ff45f177b499" - integrity sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA== + version "14.11.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.1.tgz#56af902ad157e763f9ba63d671c39cda3193c835" + integrity sha512-oTQgnd0hblfLsJ6BvJzzSL+Inogp3lq9fGgqRkMB/ziKMgEUaFl801OncOzUmalfzt14N0oPHMK47ipl+wbTIw== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -1691,9 +1691,9 @@ integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== abab@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.4.tgz#6dfa57b417ca06d21b2478f0e638302f99c2405c" - integrity sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ== + version "2.0.5" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" + integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: version "1.3.7" @@ -1766,12 +1766,7 @@ ajv-keywords@^1.0.0: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" integrity sha1-MU3QpLM2j609/NxU7eYXG4htrzw= -ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" - integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== - -ajv-keywords@^3.5.2: +ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== @@ -2088,12 +2083,12 @@ axe-core@^3.5.4: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== -axios@^0.19.2: - version "0.19.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" - integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== +axios@^0.20.0: + version "0.20.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.20.0.tgz#057ba30f04884694993a8cd07fa394cff11c50bd" + integrity sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA== dependencies: - follow-redirects "1.5.10" + follow-redirects "^1.10.0" axobject-query@^2.1.2: version "2.2.0" @@ -2506,14 +2501,14 @@ browserify-zlib@^0.2.0: pako "~1.0.5" browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.8.5: - version "4.14.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.1.tgz#cb2b490ba881d45dc3039078c7ed04411eaf3fa3" - integrity sha512-zyBTIHydW37pnb63c7fHFXUG6EcqWOqoMdDx6cdyaDFriZ20EoVxcE95S54N+heRqY8m8IUgB5zYta/gCwSaaA== + version "4.14.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.3.tgz#381f9e7f13794b2eb17e1761b4f118e8ae665a53" + integrity sha512-GcZPC5+YqyPO4SFnz48/B0YaCwS47Q9iPChRGi6t7HhflKBcINzFrJvRfC+jp30sRMKxF+d4EHGs27Z0XP1NaQ== dependencies: - caniuse-lite "^1.0.30001124" - electron-to-chromium "^1.3.562" - escalade "^3.0.2" - node-releases "^1.1.60" + caniuse-lite "^1.0.30001131" + electron-to-chromium "^1.3.570" + escalade "^3.1.0" + node-releases "^1.1.61" bser@2.1.1: version "2.1.1" @@ -2587,7 +2582,7 @@ cacache@^12.0.2: unique-filename "^1.1.1" y18n "^4.0.0" -cacache@^15.0.4, cacache@^15.0.5: +cacache@^15.0.5: version "15.0.5" resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0" integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A== @@ -2681,10 +2676,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001124: - version "1.0.30001124" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001124.tgz#5d9998190258e11630d674fc50ea8e579ae0ced2" - integrity sha512-zQW8V3CdND7GHRH6rxm6s59Ww4g/qGWTheoboW9nfeMg7sUoopIfKCcNZUjwYRCOrvereh3kwDpZj4VLQ7zGtA== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001131: + version "1.0.30001133" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001133.tgz#ec564c5495311299eb05245e252d589a84acd95e" + integrity sha512-s3XAUFaC/ntDb1O3lcw9K8MPeOW7KO3z9+GzAoBxfz1B0VdacXPMKgFUtG4KIsgmnbexmi013s9miVu4h+qMHw== capture-exit@^2.0.0: version "2.0.0" @@ -2878,6 +2873,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.1.tgz#a4cb67aad45cd83d8d05128fc9f4d8fbb887e6b3" + integrity sha512-rcvHOWyGyid6I1WjT/3NatKj2kDt9OdSHSXpyLXaMWFbKpGACNW8pRhhdPUq9MWUOdwn8Rz9AVETjF4105rZZQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -3258,10 +3262,10 @@ css-list-helpers@^1.0.1: dependencies: tcomb "^2.5.0" -css-loader@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.2.2.tgz#b668b3488d566dc22ebcf9425c5f254a05808c89" - integrity sha512-omVGsTkZPVwVRpckeUnLshPp12KsmMSLqYxs12+RzM9jRR5Y+Idn/tBffjXRvOE+qW7if24cuceFJqYR5FmGBg== +css-loader@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-4.3.0.tgz#c888af64b2a5b2e85462c72c0f4a85c7e2e0821e" + integrity sha512-rdezjCjScIrsL8BSYszgT4s476IcNKt6yX69t0pHjJVnPUTDpn4WfIpDQTN3wCJvUvfsz/mFjuGOekf3PY3NUg== dependencies: camelcase "^6.0.0" cssesc "^3.0.0" @@ -3273,7 +3277,7 @@ css-loader@^4.2.2: postcss-modules-scope "^2.2.0" postcss-modules-values "^3.0.0" postcss-value-parser "^4.1.0" - schema-utils "^2.7.0" + schema-utils "^2.7.1" semver "^7.3.2" css-select-base-adapter@^0.1.1: @@ -3474,14 +3478,7 @@ debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" -debug@=3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -debug@^3.0.0, debug@^3.1.1, debug@^3.2.5: +debug@^3.1.1, debug@^3.2.5: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -3545,7 +3542,7 @@ default-gateway@^4.2.0: execa "^1.0.0" ip-regex "^2.1.0" -define-properties@^1.1.2, define-properties@^1.1.3: +define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== @@ -3635,10 +3632,10 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== -detect-passive-events@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-1.0.4.tgz#6ed477e6e5bceb79079735dcd357789d37f9a91a" - integrity sha1-btR35uW863kHlzXc01d4nTf5qRo= +detect-passive-events@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-1.0.5.tgz#ce324db665123bef9e368b8059ff95d95217cc05" + integrity sha512-foW7Q35wwOCxVzW0xLf5XeB5Fhe7oyRgvkBYdiP9IWgLMzjqUqTvsJv9ymuEWGjY6AoDXD3OC294+Z9iuOw0QA== diff-sequences@^25.2.6: version "25.2.6" @@ -3760,9 +3757,9 @@ domutils@^1.7.0: domelementtype "1" dot-prop@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" - integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== dependencies: is-obj "^2.0.0" @@ -3804,10 +3801,10 @@ ejs@^2.3.4, ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== -electron-to-chromium@^1.3.562: - version "1.3.562" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.562.tgz#79c20277ee1c8d0173a22af00e38433b752bc70f" - integrity sha512-WhRe6liQ2q/w1MZc8mD8INkenHivuHdrr4r5EQHNomy3NJux+incP6M6lDMd0paShP3MD0WGe5R1TWmEClf+Bg== +electron-to-chromium@^1.3.570: + version "1.3.570" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.570.tgz#3f5141cc39b4e3892a276b4889980dabf1d29c7f" + integrity sha512-Y6OCoVQgFQBP5py6A/06+yWxUZHDlNr/gNDGatjH8AZqXl8X0tE4LfjLJsXGz/JmWJz8a6K7bR1k+QzZ+k//fg== elliptic@^6.5.3: version "6.5.3" @@ -4021,10 +4018,10 @@ es6-weak-map@^2.0.1: es6-iterator "^2.0.3" es6-symbol "^3.1.1" -escalade@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" - integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ== +escalade@^3.0.2, escalade@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e" + integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig== escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" @@ -4571,13 +4568,13 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" -file-loader@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.0.0.tgz#97bbfaab7a2460c07bcbd72d3a6922407f67649f" - integrity sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ== +file-loader@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.1.0.tgz#65b9fcfb0ea7f65a234a1f10cdd7f1ab9a33f253" + integrity sha512-26qPdHyTsArQ6gU4P1HJbAbnFTyT2r0pG7czh1GFAd9TZbj0n94wWbupgixZH/ET/meqi2/5+F7DhW4OAXD+Lg== dependencies: loader-utils "^2.0.0" - schema-utils "^2.6.5" + schema-utils "^2.7.1" file-type@^12.4.1: version "12.4.2" @@ -4711,19 +4708,10 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" - integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== - dependencies: - debug "=3.1.0" - -follow-redirects@^1.0.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.11.0.tgz#afa14f08ba12a52963140fe43212658897bc0ecb" - integrity sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA== - dependencies: - debug "^3.0.0" +follow-redirects@^1.0.0, follow-redirects@^1.10.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" + integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== font-awesome@^4.7.0: version "4.7.0" @@ -4882,7 +4870,7 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== -get-caller-file@^2.0.1: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -5083,7 +5071,7 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.0, has-symbols@^1.0.1: +has-symbols@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== @@ -5652,9 +5640,9 @@ is-binary-path@~2.1.0: binary-extensions "^2.0.0" is-callable@^1.1.4, is-callable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" - integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.1.tgz#4d1e21a4f437509d25ce55f8184350771421c96d" + integrity sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg== is-ci@^2.0.0: version "2.0.0" @@ -6381,15 +6369,6 @@ jest-watcher@^26.3.0: jest-util "^26.3.0" string-length "^4.0.1" -jest-worker@^26.0.0: - version "26.2.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.2.1.tgz#5d630ab93f666b53f911615bc13e662b382bd513" - integrity sha512-+XcGMMJDTeEGncRb5M5Zq9P7K4sQ1sirhjdOxsN1462h6lFo9w59bl2LVQmdGEEeU3m+maZCkS2Tcc9SfCHO4A== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^7.0.0" - jest-worker@^26.3.0: version "26.3.0" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f" @@ -7191,14 +7170,14 @@ nice-try@^1.0.4: integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== node-fetch@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" - integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -node-forge@0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" - integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== +node-forge@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== node-int64@^0.4.0: version "0.4.0" @@ -7251,10 +7230,10 @@ node-notifier@^8.0.0: uuid "^8.3.0" which "^2.0.2" -node-releases@^1.1.60: - version "1.1.60" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" - integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== +node-releases@^1.1.61: + version "1.1.61" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e" + integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g== normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" @@ -7386,7 +7365,7 @@ object-is@^1.0.1: define-properties "^1.1.3" es-abstract "^1.17.5" -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: +object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -7399,14 +7378,14 @@ object-visit@^1.0.0: isobject "^3.0.0" object.assign@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + version "4.1.1" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" + integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.0" + has-symbols "^1.0.1" + object-keys "^1.1.1" object.entries@^1.1.2: version "1.1.2" @@ -7581,10 +7560,10 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.1.tgz#584784ac0722d1aed09f19f90ed2999af6ce2839" - integrity sha512-mw/p92EyOzl2MhauKodw54Rx5ZK4624rNfgNaBguFZkHzyUG9WsDzFF5/yQVEJinbJDdP4jEfMN+uBquiGnaLg== +p-limit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe" + integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg== dependencies: p-try "^2.0.0" @@ -9426,7 +9405,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.2.0, schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0, schema-utils@^2.7.1: +schema-utils@^2.2.0, schema-utils@^2.6.5, schema-utils@^2.7.0, schema-utils@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== @@ -9449,11 +9428,11 @@ select-hose@^2.0.0: integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= selfsigned@^1.10.7: - version "1.10.7" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b" - integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA== + version "1.10.8" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30" + integrity sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w== dependencies: - node-forge "0.9.0" + node-forge "^0.10.0" "semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: version "5.7.1" @@ -9511,6 +9490,13 @@ serialize-javascript@^4.0.0: dependencies: randombytes "^2.1.0" +serialize-javascript@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" + integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== + dependencies: + randombytes "^2.1.0" + serve-index@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" @@ -9797,9 +9783,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" - integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + version "3.0.6" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" + integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== spdy-transport@^3.0.0: version "3.0.0" @@ -10171,9 +10157,9 @@ supports-color@^6.1.0: has-flag "^3.0.0" supports-color@^7.0.0, supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" @@ -10281,22 +10267,22 @@ terser-webpack-plugin@^1.4.3: webpack-sources "^1.4.0" worker-farm "^1.7.0" -terser-webpack-plugin@^3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-3.0.6.tgz#db0a108bbdd3680d72c9b491fbabad09ba207b99" - integrity sha512-z3HLOOPUHkCNGkeEHqqiMAIy1pjpHwS1o+i6Zn0Ws3EAvHJj46737efNNEvJ0Vx9BdDQM83d56qySDJOSORA0A== +terser-webpack-plugin@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-4.2.2.tgz#d86200c700053bba637913fe4310ba1bdeb5568e" + integrity sha512-3qAQpykRTD5DReLu5/cwpsg7EZFzP3Q0Hp2XUWJUw2mpq2jfgOKTZr8IZKKnNieRVVo1UauROTdhbQJZveGKtQ== dependencies: - cacache "^15.0.4" + cacache "^15.0.5" find-cache-dir "^3.3.1" - jest-worker "^26.0.0" - p-limit "^3.0.1" - schema-utils "^2.6.6" - serialize-javascript "^4.0.0" + jest-worker "^26.3.0" + p-limit "^3.0.2" + schema-utils "^2.7.1" + serialize-javascript "^5.0.1" source-map "^0.6.1" - terser "^4.8.0" + terser "^5.3.2" webpack-sources "^1.4.3" -terser@^4.1.2, terser@^4.8.0: +terser@^4.1.2: version "4.8.0" resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== @@ -10305,6 +10291,15 @@ terser@^4.1.2, terser@^4.8.0: source-map "~0.6.1" source-map-support "~0.5.12" +terser@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.2.tgz#f4bea90eb92945b2a028ceef79181b9bb586e7af" + integrity sha512-H67sydwBz5jCUA32ZRL319ULu+Su1cAoZnnc+lXnenGRYWyLE3Scgkt8mNoAsMx0h5kdo758zdoS0LG9rYZXDQ== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + tesseract.js-core@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tesseract.js-core/-/tesseract.js-core-2.2.0.tgz#6ef78051272a381969fac3e45a226e85022cffef" @@ -11023,10 +11018,10 @@ webpack-sources@^1.0.0, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack- source-list-map "^2.0.0" source-map "~0.6.1" -webpack@^4.44.1: - version "4.44.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21" - integrity sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ== +webpack@^4.44.2: + version "4.44.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72" + integrity sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q== dependencies: "@webassemblyjs/ast" "1.9.0" "@webassemblyjs/helper-module-context" "1.9.0" @@ -11155,6 +11150,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -11216,6 +11220,11 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== +y18n@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.1.tgz#1ad2a7eddfa8bce7caa2e1f6b5da96c39d99d571" + integrity sha512-/jJ831jEs4vGDbYPQp4yGKDYPSCCEQ45uZWJHE1AoYBzqdZi8+LDWas0z4HrmJXmKdpFsTiowSHXdxyFhpmdMg== + yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" @@ -11247,6 +11256,11 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^20.0.0: + version "20.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.0.0.tgz#c65a1daaa977ad63cebdd52159147b789a4e19a9" + integrity sha512-8eblPHTL7ZWRkyjIZJjnGf+TijiKJSwA24svzLRVvtgoi/RZiKa9fFQTrlx0OKLnyHSdt/enrdadji6WFfESVA== + yargs@^13.3.2: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" @@ -11263,7 +11277,7 @@ yargs@^13.3.2: y18n "^4.0.0" yargs-parser "^13.1.2" -yargs@^15.3.1, yargs@^15.4.1: +yargs@^15.3.1: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== @@ -11280,6 +11294,19 @@ yargs@^15.3.1, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.0.3.tgz#7a919b9e43c90f80d4a142a89795e85399a7e54c" + integrity sha512-6+nLw8xa9uK1BOEOykaiYAJVh6/CjxWXK/q9b5FpRgNslt8s22F2xMBqVIKgCRjNgGvGPBy8Vog7WN7yh4amtA== + dependencies: + cliui "^7.0.0" + escalade "^3.0.2" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.1" + yargs-parser "^20.0.0" + zlibjs@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/zlibjs/-/zlibjs-0.3.1.tgz#50197edb28a1c42ca659cc8b4e6a9ddd6d444554"