diff options
author | Claire <claire.github-309c@sitedethib.com> | 2022-02-17 10:58:25 +0100 |
---|---|---|
committer | Claire <claire.github-309c@sitedethib.com> | 2022-02-17 10:58:44 +0100 |
commit | f224237862b009ad4b008a8730c58111f103145b (patch) | |
tree | 097c08663c6348914fdf95d2ac9ce57ee2a3307c | |
parent | ec4f9066189fbab4368a275e9cd654dc7ad48217 (diff) | |
parent | ac99f586bb4138e083676579097d951434e90515 (diff) |
Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `db/schema.rb`: Conflict due to glitch-soc adding the `content_type` column on status edits and thus having a different schema version number. Solved by taking upstream's schema version number, as it is higher than glitch-soc's.
81 files changed, 1461 insertions, 381 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml index 751ca95b1..a9ad92145 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -32,7 +32,7 @@ commands: name: Install system dependencies command: | sudo apt-get update - sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler + sudo apt-get install -y libicu-dev libidn11-dev install-ruby-dependencies: parameters: ruby-version: diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 5b7f2481e..2d1df7211 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -6,6 +6,10 @@ on: - "main" tags: - "*" + pull_request: + paths: + - .github/workflows/build-image.yml + - Dockerfile jobs: build-image: runs-on: ubuntu-latest @@ -31,7 +35,7 @@ jobs: with: context: . platforms: linux/amd64,linux/arm64 - push: true + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/mastodon:latest cache-to: type=inline diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml index 2e8f230f3..9cb98dd12 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -18,7 +18,7 @@ jobs: - name: Install system dependencies run: | sudo apt-get update - sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler + sudo apt-get install -y libicu-dev libidn11-dev - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/Aptfile b/Aptfile index b2cbad714..9235141ad 100644 --- a/Aptfile +++ b/Aptfile @@ -4,10 +4,8 @@ libicu-dev libidn11 libidn11-dev libpq-dev -libprotobuf-dev libxdamage1 libxfixes3 -protobuf-compiler zlib1g-dev libcairo2 libcroco3 diff --git a/Dockerfile b/Dockerfile index c6287b5a7..1b3661561 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,7 +51,7 @@ RUN npm install -g npm@latest && \ gem install bundler && \ apt-get update && \ apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \ - libpq-dev libprotobuf-dev protobuf-compiler shared-mime-info + libpq-dev shared-mime-info COPY Gemfile* package.json yarn.lock /opt/mastodon/ @@ -88,7 +88,7 @@ RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selectio RUN apt-get update && \ apt-get -y --no-install-recommends install \ libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \ - libicu66 libprotobuf17 libidn11 libyaml-0-2 \ + libicu66 libidn11 libyaml-0-2 \ file ca-certificates tzdata libreadline8 gcc tini apt-utils && \ ln -s /opt/mastodon /mastodon && \ gem install bundler && \ diff --git a/Gemfile b/Gemfile index ae999d964..c64cead60 100644 --- a/Gemfile +++ b/Gemfile @@ -21,7 +21,7 @@ gem 'dotenv-rails', '~> 2.7' gem 'aws-sdk-s3', '~> 1.112', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false -gem 'kt-paperclip', '~> 7.0' +gem 'kt-paperclip', '~> 7.1' gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' @@ -76,7 +76,7 @@ gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'rqrcode', '~> 2.1' gem 'ruby-progressbar', '~> 1.11' gem 'sanitize', '~> 6.0' -gem 'scenic', '~> 1.5' +gem 'scenic', '~> 1.6' gem 'sidekiq', '~> 6.4' gem 'sidekiq-scheduler', '~> 3.1' gem 'sidekiq-unique-jobs', '~> 7.1' @@ -105,7 +105,7 @@ group :development, :test do gem 'i18n-tasks', '~> 0.9', require: false gem 'pry-byebug', '~> 3.9' gem 'pry-rails', '~> 0.3' - gem 'rspec-rails', '~> 5.0' + gem 'rspec-rails', '~> 5.1' end group :production, :test do @@ -126,7 +126,7 @@ end group :development do gem 'active_record_query_trace', '~> 1.8' - gem 'annotate', '~> 3.1' + gem 'annotate', '~> 3.2' gem 'better_errors', '~> 2.9' gem 'binding_of_caller', '~> 1.0' gem 'bullet', '~> 7.0' diff --git a/Gemfile.lock b/Gemfile.lock index 9d160dade..d61f13c3b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,40 +1,40 @@ GEM remote: https://rubygems.org/ specs: - actioncable (6.1.4.4) - actionpack (= 6.1.4.4) - activesupport (= 6.1.4.4) + actioncable (6.1.4.6) + actionpack (= 6.1.4.6) + activesupport (= 6.1.4.6) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.4) - actionpack (= 6.1.4.4) - activejob (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + actionmailbox (6.1.4.6) + actionpack (= 6.1.4.6) + activejob (= 6.1.4.6) + activerecord (= 6.1.4.6) + activestorage (= 6.1.4.6) + activesupport (= 6.1.4.6) mail (>= 2.7.1) - actionmailer (6.1.4.4) - actionpack (= 6.1.4.4) - actionview (= 6.1.4.4) - activejob (= 6.1.4.4) - activesupport (= 6.1.4.4) + actionmailer (6.1.4.6) + actionpack (= 6.1.4.6) + actionview (= 6.1.4.6) + activejob (= 6.1.4.6) + activesupport (= 6.1.4.6) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.4.4) - actionview (= 6.1.4.4) - activesupport (= 6.1.4.4) + actionpack (6.1.4.6) + actionview (= 6.1.4.6) + activesupport (= 6.1.4.6) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.4) - actionpack (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + actiontext (6.1.4.6) + actionpack (= 6.1.4.6) + activerecord (= 6.1.4.6) + activestorage (= 6.1.4.6) + activesupport (= 6.1.4.6) nokogiri (>= 1.8.5) - actionview (6.1.4.4) - activesupport (= 6.1.4.4) + actionview (6.1.4.6) + activesupport (= 6.1.4.6) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -45,22 +45,22 @@ GEM case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) active_record_query_trace (1.8) - activejob (6.1.4.4) - activesupport (= 6.1.4.4) + activejob (6.1.4.6) + activesupport (= 6.1.4.6) globalid (>= 0.3.6) - activemodel (6.1.4.4) - activesupport (= 6.1.4.4) - activerecord (6.1.4.4) - activemodel (= 6.1.4.4) - activesupport (= 6.1.4.4) - activestorage (6.1.4.4) - actionpack (= 6.1.4.4) - activejob (= 6.1.4.4) - activerecord (= 6.1.4.4) - activesupport (= 6.1.4.4) + activemodel (6.1.4.6) + activesupport (= 6.1.4.6) + activerecord (6.1.4.6) + activemodel (= 6.1.4.6) + activesupport (= 6.1.4.6) + activestorage (6.1.4.6) + actionpack (= 6.1.4.6) + activejob (= 6.1.4.6) + activerecord (= 6.1.4.6) + activesupport (= 6.1.4.6) marcel (~> 1.0.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.4) + activesupport (6.1.4.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -71,8 +71,8 @@ GEM airbrussh (1.4.0) sshkit (>= 1.6.1, != 1.7.0) android_key_attestation (0.3.0) - annotate (3.1.1) - activerecord (>= 3.2, < 7.0) + annotate (3.2.0) + activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) ast (2.4.2) attr_encrypted (3.1.0) @@ -181,7 +181,7 @@ GEM devise_pam_authenticatable2 (9.2.0) devise (>= 4.0.0) rpam2 (~> 4.0) - diff-lcs (1.4.4) + diff-lcs (1.5.0) discard (1.2.1) activerecord (>= 4.2, < 8) docile (1.3.4) @@ -332,7 +332,7 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - kt-paperclip (7.0.1) + kt-paperclip (7.1.1) activemodel (>= 4.2.0) activesupport (>= 4.2.0) marcel (~> 1.0.1) @@ -356,14 +356,14 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.13.0) + loofah (2.14.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) mini_mime (>= 0.1.1) makara (0.5.1) activerecord (>= 5.2.0) - marcel (1.0.1) + marcel (1.0.2) mario-redis-lock (1.2.1) redis (>= 3.0.5) matrix (0.4.2) @@ -374,7 +374,7 @@ GEM nokogiri (~> 1.10) mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2021.1115) + mime-types-data (3.2022.0105) mini_mime (1.1.2) mini_portile2 (2.7.1) minitest (5.15.0) @@ -418,7 +418,7 @@ GEM parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.3.1) + pg (1.3.2) pghero (2.8.2) activerecord (>= 5) pkg-config (1.4.7) @@ -455,20 +455,20 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.1.4.4) - actioncable (= 6.1.4.4) - actionmailbox (= 6.1.4.4) - actionmailer (= 6.1.4.4) - actionpack (= 6.1.4.4) - actiontext (= 6.1.4.4) - actionview (= 6.1.4.4) - activejob (= 6.1.4.4) - activemodel (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + rails (6.1.4.6) + actioncable (= 6.1.4.6) + actionmailbox (= 6.1.4.6) + actionmailer (= 6.1.4.6) + actionpack (= 6.1.4.6) + actiontext (= 6.1.4.6) + actionview (= 6.1.4.6) + activejob (= 6.1.4.6) + activemodel (= 6.1.4.6) + activerecord (= 6.1.4.6) + activestorage (= 6.1.4.6) + activesupport (= 6.1.4.6) bundler (>= 1.15.0) - railties (= 6.1.4.4) + railties (= 6.1.4.6) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -484,9 +484,9 @@ GEM railties (>= 6.0.0, < 7) rails-settings-cached (0.6.6) rails (>= 4.2.0) - railties (6.1.4.4) - actionpack (= 6.1.4.4) - activesupport (= 6.1.4.4) + railties (6.1.4.6) + actionpack (= 6.1.4.6) + activesupport (= 6.1.4.6) method_source rake (>= 0.13) thor (~> 1.0) @@ -509,19 +509,19 @@ GEM rexml (3.2.5) rotp (6.2.0) rpam2 (4.0.2) - rqrcode (2.1.0) + rqrcode (2.1.1) chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) + rspec-core (3.11.0) + rspec-support (~> 3.11.0) + rspec-expectations (3.11.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.2) + rspec-support (~> 3.11.0) + rspec-mocks (3.11.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-rails (5.0.2) + rspec-support (~> 3.11.0) + rspec-rails (5.1.0) actionpack (>= 5.2) activesupport (>= 5.2) railties (>= 5.2) @@ -532,7 +532,7 @@ GEM rspec-sidekiq (3.1.0) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) - rspec-support (3.10.3) + rspec-support (3.11.0) rspec_junit_formatter (0.5.1) rspec-core (>= 2, < 4, != 2.12.0) rubocop (1.25.1) @@ -562,7 +562,7 @@ GEM sanitize (6.0.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - scenic (1.5.5) + scenic (1.6.0) activerecord (>= 4.0.0) railties (>= 4.0.0) securecompare (1.0.0) @@ -685,7 +685,7 @@ DEPENDENCIES active_model_serializers (~> 0.10) active_record_query_trace (~> 1.8) addressable (~> 2.8) - annotate (~> 3.1) + annotate (~> 3.2) aws-sdk-s3 (~> 1.112) better_errors (~> 2.9) binding_of_caller (~> 1.0) @@ -732,7 +732,7 @@ DEPENDENCIES json-ld json-ld-preloaded (~> 3.2) kaminari (~> 1.2) - kt-paperclip (~> 7.0) + kt-paperclip (~> 7.1) letter_opener (~> 1.7) letter_opener_web (~> 2.0) link_header (~> 0.0) @@ -775,14 +775,14 @@ DEPENDENCIES redis-namespace (~> 1.8) rexml (~> 3.2) rqrcode (~> 2.1) - rspec-rails (~> 5.0) + rspec-rails (~> 5.1) rspec-sidekiq (~> 3.1) rspec_junit_formatter (~> 0.5) rubocop (~> 1.25) rubocop-rails (~> 2.13) ruby-progressbar (~> 1.11) sanitize (~> 6.0) - scenic (~> 1.5) + scenic (~> 1.6) sidekiq (~> 6.4) sidekiq-bulk (~> 0.2.0) sidekiq-scheduler (~> 3.1) diff --git a/Vagrantfile b/Vagrantfile index aeff2f233..0d44b4d23 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -33,11 +33,9 @@ sudo apt-get install \ redis-tools \ postgresql \ postgresql-contrib \ - protobuf-compiler \ yarn \ libicu-dev \ libidn11-dev \ - libprotobuf-dev \ libreadline-dev \ libpam0g-dev \ -y diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index e7f56e243..e0ae71b9f 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -28,7 +28,7 @@ module Admin @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.strikes.custom.latest + @warnings = @account.strikes.includes(:target_account, :account, :appeal).latest @domain_block = DomainBlock.rule_for(@account.domain) end @@ -146,7 +146,7 @@ module Admin end def filter_params - params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS) + params.slice(:page, *AccountFilter::KEYS).permit(:page, *AccountFilter::KEYS) end def form_account_batch_params diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index f0a935411..e376baab2 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -8,6 +8,7 @@ module Admin @pending_users_count = User.pending.count @pending_reports_count = Report.unresolved.count @pending_tags_count = Tag.pending_review.count + @pending_appeals_count = Appeal.pending.count end private diff --git a/app/controllers/admin/disputes/appeals_controller.rb b/app/controllers/admin/disputes/appeals_controller.rb new file mode 100644 index 000000000..32e5e2f6f --- /dev/null +++ b/app/controllers/admin/disputes/appeals_controller.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class Admin::Disputes::AppealsController < Admin::BaseController + before_action :set_appeal, except: :index + + def index + authorize :appeal, :index? + + @appeals = filtered_appeals.page(params[:page]) + end + + def approve + authorize @appeal, :approve? + log_action :approve, @appeal + ApproveAppealService.new.call(@appeal, current_account) + redirect_to disputes_strike_path(@appeal.strike) + end + + def reject + authorize @appeal, :approve? + log_action :reject, @appeal + @appeal.reject!(current_account) + UserMailer.appeal_rejected(@appeal.account.user, @appeal) + redirect_to disputes_strike_path(@appeal.strike) + end + + private + + def filtered_appeals + Admin::AppealFilter.new(filter_params.with_defaults(status: 'pending')).results.includes(strike: :account) + end + + def filter_params + params.slice(:page, *Admin::AppealFilter::KEYS).permit(:page, *Admin::AppealFilter::KEYS) + end + + def set_appeal + @appeal = Appeal.find(params[:id]) + end +end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 6b1f3fa82..5d32fe66e 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -10,6 +10,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController before_action :configure_sign_up_params, only: [:create] before_action :set_pack before_action :set_sessions, only: [:edit, :update] + before_action :set_strikes, only: [:edit, :update] before_action :set_instance_presenter, only: [:new, :create, :update] before_action :set_body_classes, only: [:new, :create, :edit, :update] before_action :require_not_suspended!, only: [:update] @@ -116,8 +117,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_invite - invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil - @invite = invite&.valid_for_use? ? invite : nil + @invite = begin + invite = Invite.find_by(code: invite_code) if invite_code.present? + invite if invite&.valid_for_use? + end end def determine_layout @@ -128,6 +131,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController @sessions = current_user.session_activations end + def set_strikes + @strikes = current_account.strikes.active.latest + end + def require_not_suspended! forbidden if current_account.suspended? end diff --git a/app/controllers/disputes/appeals_controller.rb b/app/controllers/disputes/appeals_controller.rb new file mode 100644 index 000000000..eefd92b5a --- /dev/null +++ b/app/controllers/disputes/appeals_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Disputes::AppealsController < Disputes::BaseController + before_action :set_strike + + def create + authorize @strike, :appeal? + + @appeal = AppealService.new.call(@strike, appeal_params[:text]) + + redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg') + rescue ActiveRecord::RecordInvalid => e + @appeal = e.record + render template: 'disputes/strikes/show' + end + + private + + def set_strike + @strike = current_account.strikes.find(params[:strike_id]) + end + + def appeal_params + params.require(:appeal).permit(:text) + end +end diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb new file mode 100644 index 000000000..865146b5c --- /dev/null +++ b/app/controllers/disputes/base_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Disputes::BaseController < ApplicationController + include Authorization + + layout 'admin' + + skip_before_action :require_functional! + + before_action :set_body_classes + before_action :authenticate_user! + + private + + def set_body_classes + @body_classes = 'admin' + end +end diff --git a/app/controllers/disputes/strikes_controller.rb b/app/controllers/disputes/strikes_controller.rb new file mode 100644 index 000000000..d41c5c727 --- /dev/null +++ b/app/controllers/disputes/strikes_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Disputes::StrikesController < Disputes::BaseController + before_action :set_strike + + def show + authorize @strike, :show? + + @appeal = @strike.appeal || @strike.build_appeal + end + + private + + def set_strike + @strike = AccountWarning.find(params[:id]) + end +end diff --git a/app/helpers/admin/account_moderation_notes_helper.rb b/app/helpers/admin/account_moderation_notes_helper.rb index 40b2a5289..2f08538ca 100644 --- a/app/helpers/admin/account_moderation_notes_helper.rb +++ b/app/helpers/admin/account_moderation_notes_helper.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true module Admin::AccountModerationNotesHelper - def admin_account_link_to(account) + def admin_account_link_to(account, path: nil) return if account.nil? - link_to admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do + link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do safe_join([ image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username'), diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index f3aa4be4f..47eeeaac3 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -33,6 +33,8 @@ module Admin::ActionLogsHelper "#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})" when 'Instance' record.domain + when 'Appeal' + link_to record.account.acct, disputes_strike_path(record.strike) end end diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb new file mode 100644 index 000000000..d16e3dd12 --- /dev/null +++ b/app/helpers/admin/trends/statuses_helper.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Admin::Trends::StatusesHelper + def one_line_preview(status) + text = begin + if status.local? + status.text.split("\n").first + else + Nokogiri::HTML(status.text).css('html > body > *').first&.text + end + end + + return '' if text.blank? + + html = Formatter.instance.send(:encode, text) + html = Formatter.instance.send(:encode_custom_emojis, html, status.emojis, prefers_autoplay?) + + html.html_safe # rubocop:disable Rails/OutputSafety + end +end diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index 4a87714e6..f433e4de9 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -170,7 +170,7 @@ class EmojiPickerMenu extends React.PureComponent { state = { modifierOpen: false, - placement: null, + readyToFocus: false, }; handleDocumentClick = e => { @@ -182,6 +182,16 @@ class EmojiPickerMenu extends React.PureComponent { componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + + // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need + // to wait for a frame before focusing + requestAnimationFrame(() => { + this.setState({ readyToFocus: true }); + if (this.node) { + const element = this.node.querySelector('input[type="search"]'); + if (element) element.focus(); + } + }); } componentWillUnmount () { @@ -281,7 +291,7 @@ class EmojiPickerMenu extends React.PureComponent { showSkinTones={false} backgroundImageFn={backgroundImageFn} notFound={notFoundFn} - autoFocus + autoFocus={this.state.readyToFocus} emojiTooltip /> @@ -314,6 +324,7 @@ class EmojiPickerDropdown extends React.PureComponent { state = { active: false, loading: false, + placement: null, }; setRef = (c) => { diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 7ebe8b4d0..be467a8e2 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -151,13 +151,7 @@ function main() { }); delegate(document, '.sidebar__toggle__icon', 'click', () => { - const target = document.querySelector('.sidebar ul'); - - if (target.style.display === 'block') { - target.style.display = 'none'; - } else { - target.style.display = 'block'; - } + document.querySelector('.sidebar ul').classList.toggle('visible'); }); // Empty the honeypot fields in JS in case something like an extension diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 66ce92ce2..33e115c1a 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -322,6 +322,10 @@ $content-width: 840px; & > ul { display: none; + + &.visible { + display: block; + } } ul a, @@ -594,12 +598,16 @@ body, } .log-entry { + display: block; line-height: 20px; padding: 15px; padding-left: 15px * 2 + 40px; background: $ui-base-color; border-bottom: 1px solid darken($ui-base-color, 8%); position: relative; + text-decoration: none; + color: $darker-text-color; + font-size: 14px; &:first-child { border-top-left-radius: 4px; @@ -612,15 +620,12 @@ body, border-bottom: 0; } - &:hover { + &:hover, + &:focus, + &:active { background: lighten($ui-base-color, 4%); } - &__header { - color: $darker-text-color; - font-size: 14px; - } - &__avatar { position: absolute; left: 15px; @@ -656,6 +661,18 @@ body, text-decoration: underline; } } + + &--inactive { + .log-entry__title { + text-decoration: line-through; + } + + a, + .username, + .target { + color: $darker-text-color; + } + } } a.name-tag, @@ -1191,6 +1208,17 @@ a.sparkline { font-weight: 600; padding: 4px 0; } + + a { + color: $ui-highlight-color; + text-decoration: none; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } } &--horizontal { @@ -1467,3 +1495,56 @@ a.sparkline { } } } + +.strike-card { + padding: 15px; + border-radius: 4px; + background: $ui-base-color; + font-size: 15px; + line-height: 20px; + word-wrap: break-word; + font-weight: 400; + color: $primary-text-color; + + p { + margin-bottom: 20px; + unicode-bidi: plaintext; + + &:last-child { + margin-bottom: 0; + } + } + + &__statuses-list { + border-radius: 4px; + border: 1px solid darken($ui-base-color, 8%); + font-size: 13px; + line-height: 18px; + overflow: hidden; + + &__item { + padding: 16px; + background: lighten($ui-base-color, 2%); + border-bottom: 1px solid darken($ui-base-color, 8%); + + &:last-child { + border-bottom: 0; + } + + &__meta { + color: $darker-text-color; + } + + a { + color: inherit; + text-decoration: none; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + } + } +} diff --git a/app/javascript/styles/mastodon/footer.scss b/app/javascript/styles/mastodon/footer.scss index 00d290883..073ebda7e 100644 --- a/app/javascript/styles/mastodon/footer.scss +++ b/app/javascript/styles/mastodon/footer.scss @@ -90,6 +90,20 @@ .column-4 { display: none; } + + .column-2 h4 { + display: none; + } + } + } + + .legal-xs { + display: none; + text-align: center; + padding-top: 20px; + + @media screen and (max-width: $no-gap-breakpoint) { + display: block; } } @@ -105,7 +119,8 @@ } } - ul a { + ul a, + .legal-xs a { text-decoration: none; color: lighten($ui-base-color, 34%); diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index 1f9319290..12fad8da4 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -8,6 +8,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity original_status = status_from_object return reject_payload! if original_status.nil? || !announceable?(original_status) + return if requested_through_relay? @status = Status.find_by(account: @account, reblog: original_status) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 0713aa471..7f2bc42d3 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -499,7 +499,7 @@ class FeedManager return false if active_filters.empty? - combined_regex = active_filters.reduce { |memo, obj| Regexp.union(memo, obj) } + combined_regex = Regexp.union(active_filters) status = status.reblog if status.reblog? combined_text = [ diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb index e07ebfffe..c685d7b6f 100644 --- a/app/lib/search_query_transformer.rb +++ b/app/lib/search_query_transformer.rb @@ -2,19 +2,21 @@ class SearchQueryTransformer < Parslet::Transform class Query - attr_reader :should_clauses, :must_not_clauses, :must_clauses + attr_reader :should_clauses, :must_not_clauses, :must_clauses, :filter_clauses def initialize(clauses) grouped = clauses.chunk(&:operator).to_h @should_clauses = grouped.fetch(:should, []) @must_not_clauses = grouped.fetch(:must_not, []) @must_clauses = grouped.fetch(:must, []) + @filter_clauses = grouped.fetch(:filter, []) end def apply(search) should_clauses.each { |clause| search = search.query.should(clause_to_query(clause)) } must_clauses.each { |clause| search = search.query.must(clause_to_query(clause)) } must_not_clauses.each { |clause| search = search.query.must_not(clause_to_query(clause)) } + filter_clauses.each { |clause| search = search.filter(**clause_to_filter(clause)) } search.query.minimum_should_match(1) end @@ -30,6 +32,15 @@ class SearchQueryTransformer < Parslet::Transform raise "Unexpected clause type: #{clause}" end end + + def clause_to_filter(clause) + case clause + when PrefixClause + { term: { clause.filter => clause.term } } + else + raise "Unexpected clause type: #{clause}" + end + end end class Operator @@ -69,11 +80,33 @@ class SearchQueryTransformer < Parslet::Transform end end + class PrefixClause + attr_reader :filter, :operator, :term + + def initialize(prefix, term) + @operator = :filter + case prefix + when 'from' + @filter = :account_id + username, domain = term.split('@') + account = Account.find_remote(username, domain) + + raise "Account not found: #{term}" unless account + + @term = account.id + else + raise "Unknown prefix: #{prefix}" + end + end + end + rule(clause: subtree(:clause)) do prefix = clause[:prefix][:term].to_s if clause[:prefix] operator = clause[:operator]&.to_s - if clause[:term] + if clause[:prefix] + PrefixClause.new(prefix, clause[:term].to_s) + elsif clause[:term] TermClause.new(prefix, operator, clause[:term].to_s) elsif clause[:shortcode] TermClause.new(prefix, operator, ":#{clause[:term]}:") diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index b23bd1296..a9d00c000 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -15,6 +15,16 @@ class AdminMailer < ApplicationMailer end end + def new_appeal(recipient, appeal) + @appeal = appeal + @me = recipient + @instance = Rails.configuration.x.local_domain + + locale_for_account(@me) do + mail to: @me.user_email, subject: I18n.t('admin_mailer.new_appeal.subject', instance: @instance, username: @appeal.account.username) + end + end + def new_pending_account(recipient, user) @account = user.account @me = recipient diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 5221a4892..583c948b0 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -173,6 +173,26 @@ class UserMailer < Devise::Mailer end end + def appeal_approved(user, appeal) + @resource = user + @instance = Rails.configuration.x.local_domain + @appeal = appeal + + I18n.with_locale(@resource.locale || I18n.default_locale) do + mail to: @resource.email, subject: I18n.t('user_mailer.appeal_approved.subject', date: l(@appeal.created_at)) + end + end + + def appeal_rejected(user, appeal) + @resource = user + @instance = Rails.configuration.x.local_domain + @appeal = appeal + + I18n.with_locale(@resource.locale || I18n.default_locale) do + mail to: @resource.email, subject: I18n.t('user_mailer.appeal_rejected.subject', date: l(@appeal.created_at)) + end + end + def sign_in_token(user, remote_ip, user_agent, timestamp) @resource = user @instance = Rails.configuration.x.local_domain diff --git a/app/models/account.rb b/app/models/account.rb index e41fdf003..8f6663e7c 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -274,6 +274,10 @@ class Account < ApplicationRecord true end + def previous_strikes_count + strikes.where(overruled_at: nil).count + end + def keypair @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key) end diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index dcb174122..9da1522dd 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -24,6 +24,8 @@ class AccountFilter scope = Account.includes(:account_stat, user: [:ips, :invite_request]).without_instance_actor.reorder(nil) params.each do |key, value| + next if key.to_s == 'page' + scope.merge!(scope_for(key, value.to_s.strip)) if value.present? end @@ -49,7 +51,7 @@ class AccountFilter when 'email' accounts_with_users.merge(User.matches_email(value)) when 'ip' - valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value)) : Account.none + valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value).group('users.id, accounts.id')) : Account.none when 'invited_by' invited_by_scope(value) when 'order' diff --git a/app/models/account_warning.rb b/app/models/account_warning.rb index fc0d988fd..05d01942d 100644 --- a/app/models/account_warning.rb +++ b/app/models/account_warning.rb @@ -12,6 +12,7 @@ # updated_at :datetime not null # report_id :bigint(8) # status_ids :string is an Array +# overruled_at :datetime # class AccountWarning < ApplicationRecord @@ -28,12 +29,17 @@ class AccountWarning < ApplicationRecord belongs_to :target_account, class_name: 'Account', inverse_of: :strikes belongs_to :report, optional: true - has_one :appeal, dependent: :destroy + has_one :appeal, dependent: :destroy, inverse_of: :strike scope :latest, -> { order(id: :desc) } scope :custom, -> { where.not(text: '') } + scope :active, -> { where(overruled_at: nil).or(where('account_warnings.overruled_at >= ?', 30.days.ago)) } def statuses Status.with_discarded.where(id: status_ids || []) end + + def overruled? + overruled_at.present? + end end diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb index 12136223b..0f2f712a2 100644 --- a/app/models/admin/action_log_filter.rb +++ b/app/models/admin/action_log_filter.rb @@ -8,6 +8,8 @@ class Admin::ActionLogFilter ).freeze ACTION_TYPE_MAP = { + approve_appeal: { target_type: 'Appeal', action: 'approve' }.freeze, + reject_appeal: { target_type: 'Appeal', action: 'reject' }.freeze, assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze, change_email_user: { target_type: 'User', action: 'change_email' }.freeze, confirm_user: { target_type: 'User', action: 'confirm' }.freeze, diff --git a/app/models/admin/appeal_filter.rb b/app/models/admin/appeal_filter.rb new file mode 100644 index 000000000..b163d2e56 --- /dev/null +++ b/app/models/admin/appeal_filter.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +class Admin::AppealFilter + KEYS = %i( + status + ).freeze + + attr_reader :params + + def initialize(params) + @params = params + end + + def results + scope = Appeal.order(id: :desc) + + params.each do |key, value| + next if %w(page).include?(key.to_s) + + scope.merge!(scope_for(key, value.to_s.strip)) if value.present? + end + + scope + end + + private + + def scope_for(key, value) + case key.to_s + when 'status' + status_scope(value) + else + raise "Unknown filter: #{key}" + end + end + + def status_scope(value) + case value + when 'approved' + Appeal.approved + when 'rejected' + Appeal.rejected + when 'pending' + Appeal.pending + else + raise "Unknown status: #{value}" + end + end +end diff --git a/app/models/admin/status_filter.rb b/app/models/admin/status_filter.rb index ce5bb5f46..4fba612a6 100644 --- a/app/models/admin/status_filter.rb +++ b/app/models/admin/status_filter.rb @@ -31,7 +31,7 @@ class Admin::StatusFilter def scope_for(key, value) case key.to_s when 'media' - Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id) + Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id).reorder('statuses.id desc') when 'id' Status.where(id: value) else diff --git a/app/models/appeal.rb b/app/models/appeal.rb new file mode 100644 index 000000000..1f32cfa8b --- /dev/null +++ b/app/models/appeal.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: appeals +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) not null +# account_warning_id :bigint(8) not null +# text :text default(""), not null +# approved_at :datetime +# approved_by_account_id :bigint(8) +# rejected_at :datetime +# rejected_by_account_id :bigint(8) +# created_at :datetime not null +# updated_at :datetime not null +# +class Appeal < ApplicationRecord + MAX_STRIKE_AGE = 20.days + + belongs_to :account + belongs_to :strike, class_name: 'AccountWarning', foreign_key: 'account_warning_id' + belongs_to :approved_by_account, class_name: 'Account', optional: true + belongs_to :rejected_by_account, class_name: 'Account', optional: true + + validates :text, presence: true, length: { maximum: 2_000 } + validates :account_warning_id, uniqueness: true + + validate :validate_time_frame, on: :create + + scope :approved, -> { where.not(approved_at: nil) } + scope :rejected, -> { where.not(rejected_at: nil) } + scope :pending, -> { where(approved_at: nil, rejected_at: nil) } + + def pending? + !approved? && !rejected? + end + + def approved? + approved_at.present? + end + + def rejected? + rejected_at.present? + end + + def approve!(current_account) + update!(approved_at: Time.now.utc, approved_by_account: current_account) + end + + def reject!(current_account) + update!(rejected_at: Time.now.utc, rejected_by_account: current_account) + end + + private + + def validate_time_frame + errors.add(:base, I18n.t('strikes.errors.too_late')) if strike.created_at < MAX_STRIKE_AGE.ago + end +end diff --git a/app/models/user.rb b/app/models/user.rb index ee20e293e..a21e96ae5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -111,7 +111,7 @@ class User < ApplicationRecord scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) } scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended_at: nil }) } scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) } - scope :matches_ip, ->(value) { left_joins(:ips).where('user_ips.ip <<= ?', value) } + scope :matches_ip, ->(value) { left_joins(:ips).where('user_ips.ip <<= ?', value).group('users.id') } scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) } before_validation :sanitize_languages @@ -265,6 +265,10 @@ class User < ApplicationRecord settings.notification_emails['pending_account'] end + def allows_appeal_emails? + settings.notification_emails['appeal'] + end + def allows_trending_tag_emails? settings.notification_emails['trending_tag'] end diff --git a/app/policies/account_warning_policy.rb b/app/policies/account_warning_policy.rb new file mode 100644 index 000000000..65707dfa7 --- /dev/null +++ b/app/policies/account_warning_policy.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AccountWarningPolicy < ApplicationPolicy + def show? + target? || staff? + end + + def appeal? + target? && record.created_at >= Appeal::MAX_STRIKE_AGE.ago + end + + private + + def target? + record.target_account_id == current_account&.id + end +end diff --git a/app/policies/appeal_policy.rb b/app/policies/appeal_policy.rb new file mode 100644 index 000000000..a25187172 --- /dev/null +++ b/app/policies/appeal_policy.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AppealPolicy < ApplicationPolicy + def index? + staff? + end + + def approve? + record.pending? && staff? + end + + alias reject? approve? +end diff --git a/app/services/appeal_service.rb b/app/services/appeal_service.rb new file mode 100644 index 000000000..1397c50f5 --- /dev/null +++ b/app/services/appeal_service.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class AppealService < BaseService + def call(strike, text) + @strike = strike + @text = text + + create_appeal! + notify_staff! + + @appeal + end + + private + + def create_appeal! + @appeal = @strike.create_appeal!( + text: @text, + account: @strike.target_account + ) + end + + def notify_staff! + User.staff.includes(:account).each do |u| + AdminMailer.new_appeal(u.account, @appeal).deliver_later if u.allows_appeal_emails? + end + end +end diff --git a/app/services/approve_appeal_service.rb b/app/services/approve_appeal_service.rb new file mode 100644 index 000000000..f76bf8943 --- /dev/null +++ b/app/services/approve_appeal_service.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +class ApproveAppealService < BaseService + def call(appeal, current_account) + @appeal = appeal + @strike = appeal.strike + @current_account = current_account + + ApplicationRecord.transaction do + undo_strike_action! + mark_strike_as_appealed! + end + + queue_workers! + notify_target_account! + end + + private + + def target_account + @strike.target_account + end + + def undo_strike_action! + case @strike.action + when 'disable' + undo_disable! + when 'delete_statuses' + undo_delete_statuses! + when 'sensitive' + undo_sensitive! + when 'silence' + undo_silence! + when 'suspend' + undo_suspend! + end + end + + def mark_strike_as_appealed! + @appeal.approve!(@current_account) + @strike.touch(:overruled_at) + end + + def undo_disable! + target_account.user.enable! + end + + def undo_delete_statuses! + # Cannot be undone + end + + def undo_sensitive! + target_account.unsensitize! + end + + def undo_silence! + target_account.unsilence! + end + + def undo_suspend! + target_account.unsuspend! + end + + def queue_workers! + case @strike.action + when 'suspend' + Admin::UnsuspensionWorker.perform_async(target_account.id) + end + end + + def notify_target_account! + UserMailer.appeal_approved(target_account.user, @appeal).deliver_later + end +end diff --git a/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml b/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml deleted file mode 100644 index 432fb79a6..000000000 --- a/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -.speech-bubble - .speech-bubble__bubble - = simple_format(h(account_moderation_note.content)) - .speech-bubble__owner - = admin_account_link_to account_moderation_note.account - %time.formatted{ datetime: account_moderation_note.created_at.iso8601 }= l account_moderation_note.created_at - = table_link_to 'trash', t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete if can?(:destroy, account_moderation_note) diff --git a/app/views/admin/account_warnings/_account_warning.html.haml b/app/views/admin/account_warnings/_account_warning.html.haml index 8c9c9679c..ef23c3b77 100644 --- a/app/views/admin/account_warnings/_account_warning.html.haml +++ b/app/views/admin/account_warnings/_account_warning.html.haml @@ -1,6 +1,24 @@ -.speech-bubble.warning - .speech-bubble__bubble - = Formatter.instance.linkify(account_warning.text) - .speech-bubble__owner - = admin_account_link_to account_warning.account - %time.formatted{ datetime: account_warning.created_at.iso8601 }= l account_warning.created_at += link_to disputes_strike_path(account_warning), class: ['log-entry', account_warning.overruled? && 'log-entry--inactive'] do + .log-entry__header + .log-entry__avatar + = image_tag account_warning.target_account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar' + .log-entry__content + .log-entry__title + = t(account_warning.action, scope: 'admin.strikes.actions', name: content_tag(:span, account_warning.account.username, class: 'username'), target: content_tag(:span, account_warning.target_account.acct, class: 'target')).html_safe + .log-entry__timestamp + %time.formatted{ datetime: account_warning.created_at.iso8601 } + = l(account_warning.created_at) + + - if account_warning.report_id.present? + · + = t('admin.reports.title', id: account_warning.report_id) + + - if account_warning.overruled? + · + %span.positive-hint= t('admin.strikes.appeal_approved') + - elsif account_warning.appeal&.pending? + · + %span.warning-hint= t('admin.strikes.appeal_pending') + - elsif account_warning.appeal&.rejected? + · + %span.negative-hint= t('admin.strikes.appeal_rejected') diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index f3853d629..9a1f07a06 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -246,18 +246,29 @@ %hr.spacer/ - unless @warnings.empty? - = render @warnings + + %h3= t 'admin.accounts.previous_strikes' + + %p= t('admin.accounts.previous_strikes_description_html', count: @account.previous_strikes_count) + + .account-strikes + = render @warnings %hr.spacer/ - = render @moderation_notes + %h3= t 'admin.reports.notes.title' - = simple_form_for @account_moderation_note, url: admin_account_moderation_notes_path do |f| - = render 'shared/error_messages', object: @account_moderation_note + %p= t 'admin.reports.notes_description_html' - = f.input :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6 + .report-notes + = render partial: 'admin/report_notes/report_note', collection: @moderation_notes + + = simple_form_for @account_moderation_note, url: admin_account_moderation_notes_path do |f| = f.hidden_field :target_account_id + .field-group + = f.input :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6 + .actions = f.button :button, t('admin.account_moderation_notes.create'), type: :submit diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 2ee13b9e2..66e0c0251 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -16,10 +16,10 @@ .dashboard .dashboard__item - = react_admin_component :counter, measure: 'new_users', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.new_users'), href: admin_accounts_path + = react_admin_component :counter, measure: 'new_users', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.new_users'), href: admin_accounts_path(origin: 'local') .dashboard__item - = react_admin_component :counter, measure: 'active_users', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.active_users'), href: admin_accounts_path + = react_admin_component :counter, measure: 'active_users', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.active_users'), href: admin_accounts_path(origin: 'local') .dashboard__item = react_admin_component :counter, measure: 'interactions', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.interactions') @@ -43,6 +43,9 @@ %span= t('admin.dashboard.pending_tags_html', count: @pending_tags_count) = fa_icon 'chevron-right fw' + = link_to admin_disputes_appeals_path(status: 'pending'), class: 'dashboard__quick-access' do + %span= t('admin.dashboard.pending_appeals_html', count: @pending_appeals_count) + = fa_icon 'chevron-right fw' .dashboard__item = react_admin_component :dimension, dimension: 'sources', start_at: @time_period.first, end_at: @time_period.last, limit: 8, label: t('admin.dashboard.sources') diff --git a/app/views/admin/disputes/appeals/_appeal.html.haml b/app/views/admin/disputes/appeals/_appeal.html.haml new file mode 100644 index 000000000..02b8777e1 --- /dev/null +++ b/app/views/admin/disputes/appeals/_appeal.html.haml @@ -0,0 +1,21 @@ += link_to disputes_strike_path(appeal.strike), class: ['log-entry', appeal.approved? && 'log-entry--inactive'] do + .log-entry__header + .log-entry__avatar + = image_tag appeal.account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar' + .log-entry__content + .log-entry__title + = t(appeal.strike.action, scope: 'admin.strikes.actions', name: content_tag(:span, appeal.strike.account.username, class: 'username'), target: content_tag(:span, appeal.account.acct, class: 'target')).html_safe + .log-entry__timestamp + %time.formatted{ datetime: appeal.strike.created_at.iso8601 } + = l(appeal.strike.created_at) + + - if appeal.strike.report_id.present? + · + = t('admin.reports.title', id: appeal.strike.report_id) + · + - if appeal.approved? + %span.positive-hint= t('admin.strikes.appeal_approved') + - elsif appeal.rejected? + %span.negative-hint= t('admin.strikes.appeal_rejected') + - else + %span.warning-hint= t('admin.strikes.appeal_pending') diff --git a/app/views/admin/disputes/appeals/index.html.haml b/app/views/admin/disputes/appeals/index.html.haml new file mode 100644 index 000000000..dd6a6f403 --- /dev/null +++ b/app/views/admin/disputes/appeals/index.html.haml @@ -0,0 +1,22 @@ +- content_for :page_title do + = t('admin.disputes.appeals.title') + +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + +.filters + .filter-subset + %strong= t('admin.tags.review') + %ul + %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{Appeal.pending.count})"], ' '), status: 'pending' + %li= filter_link_to t('admin.trends.approved'), status: 'approved' + %li= filter_link_to t('admin.trends.rejected'), status: 'rejected' + +- if @appeals.empty? + %div.muted-hint.center-text + = t 'admin.disputes.appeals.empty' +- else + .announcements-list + = render partial: 'appeal', collection: @appeals + += paginate @appeals diff --git a/app/views/admin/report_notes/_report_note.html.haml b/app/views/admin/report_notes/_report_note.html.haml index 428b6cf59..f9d57c2ae 100644 --- a/app/views/admin/report_notes/_report_note.html.haml +++ b/app/views/admin/report_notes/_report_note.html.haml @@ -3,7 +3,7 @@ .report-notes__item__header %span.username - = link_to display_name(report_note.account), admin_account_path(report_note.account_id) + = link_to report_note.account.username, admin_account_path(report_note.account_id) %time{ datetime: report_note.created_at.iso8601, title: l(report_note.created_at) } - if report_note.created_at.today? = t('admin.report_notes.today_at', time: l(report_note.created_at, format: :time)) diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 018a0c54a..abcbec949 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -53,7 +53,7 @@ .report-header__details__item__header %strong= t('admin.accounts.strikes') .report-header__details__item__content - = @report.target_account.strikes.count + = @report.target_account.previous_strikes_count .report-header__details .report-header__details__item diff --git a/app/views/admin_mailer/new_appeal.text.erb b/app/views/admin_mailer/new_appeal.text.erb new file mode 100644 index 000000000..db4529eb7 --- /dev/null +++ b/app/views/admin_mailer/new_appeal.text.erb @@ -0,0 +1,9 @@ +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> + +<%= raw t('admin_mailer.new_appeal.body', target: @appeal.account.username, action_taken_by: @appeal.strike.account.username, date: l(@appeal.strike.created_at), type: t(@appeal.strike.action, scope: 'admin_mailer.new_appeal.actions')) %> + +> <%= raw word_wrap(@appeal.text, break_sequence: "\n> ") %> + +<%= raw t('admin_mailer.new_appeal.next_steps') %> + +<%= raw t('application_mailer.view')%> <%= disputes_strike_url(@appeal.strike) %> diff --git a/app/views/auth/registrations/_account_warning.html.haml b/app/views/auth/registrations/_account_warning.html.haml new file mode 100644 index 000000000..40e7e1296 --- /dev/null +++ b/app/views/auth/registrations/_account_warning.html.haml @@ -0,0 +1,20 @@ += link_to disputes_strike_path(account_warning), class: 'log-entry' do + .log-entry__header + .log-entry__avatar + .indicator-icon{ class: account_warning.overruled? ? 'success' : 'failure' } + = fa_icon 'warning' + .log-entry__content + .log-entry__title + = t('disputes.strikes.title', action: t(account_warning.action, scope: 'disputes.strikes.title_actions'), date: l(account_warning.created_at.to_date)) + .log-entry__timestamp + %time.formatted{ datetime: account_warning.created_at.iso8601 }= l(account_warning.created_at) + + - if account_warning.overruled? + · + %span.positive-hint= t('disputes.strikes.your_appeal_approved') + - elsif account_warning.appeal&.pending? + · + %span.warning-hint= t('disputes.strikes.your_appeal_pending') + - elsif account_warning.appeal&.rejected? + · + %span.negative-hint= t('disputes.strikes.your_appeal_rejected') diff --git a/app/views/auth/registrations/_status.html.haml b/app/views/auth/registrations/_status.html.haml index 47112dae0..3546510b2 100644 --- a/app/views/auth/registrations/_status.html.haml +++ b/app/views/auth/registrations/_status.html.haml @@ -1,22 +1,17 @@ +- if !@user.confirmed? + .flash-message.warning + = t('auth.status.confirming') + = link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path +- elsif !@user.approved? + .flash-message.warning + = t('auth.status.pending') +- elsif @user.account.moved_to_account_id.present? + .flash-message.warning + = t('auth.status.redirecting_to', acct: @user.account.moved_to_account.acct) + = link_to t('migrations.cancel'), settings_migration_path + %h3= t('auth.status.account_status') -.simple_form - %p.hint - - if @user.account.suspended? - %span.negative-hint= t('user_mailer.warning.explanation.suspend') - - elsif @user.disabled? - %span.negative-hint= t('user_mailer.warning.explanation.disable') - - elsif @user.account.silenced? - %span.warning-hint= t('user_mailer.warning.explanation.silence') - - elsif !@user.confirmed? - %span.warning-hint= t('auth.status.confirming') - = link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path - - elsif !@user.approved? - %span.warning-hint= t('auth.status.pending') - - elsif @user.account.moved_to_account_id.present? - %span.positive-hint= t('auth.status.redirecting_to', acct: @user.account.moved_to_account.acct) - = link_to t('migrations.cancel'), settings_migration_path - - else - %span.positive-hint= t('auth.status.functional') += render partial: 'account_warning', collection: @strikes %hr.spacer/ diff --git a/app/views/disputes/strikes/show.html.haml b/app/views/disputes/strikes/show.html.haml new file mode 100644 index 000000000..3dcb19016 --- /dev/null +++ b/app/views/disputes/strikes/show.html.haml @@ -0,0 +1,127 @@ +- content_for :page_title do + = t('disputes.strikes.title', action: t(@strike.action, scope: 'disputes.strikes.title_actions'), date: l(@strike.created_at.to_date)) + +- content_for :heading_actions do + - if @appeal.persisted? + = link_to t('admin.accounts.approve'), approve_admin_disputes_appeal_path(@appeal), method: :post, class: 'button' if can?(:approve, @appeal) + = link_to t('admin.accounts.reject'), reject_admin_disputes_appeal_path(@appeal), method: :post, class: 'button button--destructive' if can?(:reject, @appeal) + +- if @strike.overruled? + %p.hint + %span.positive-hint + = fa_icon 'check' + = ' ' + = t 'disputes.strikes.appeal_approved' +- elsif @appeal.persisted? && @appeal.rejected? + %p.hint + %span.negative-hint + = fa_icon 'times' + = ' ' + = t 'disputes.strikes.appeal_rejected' + +.report-header + .report-header__card + .strike-card + - unless @strike.none_action? + %p= t "user_mailer.warning.explanation.#{@strike.action}" + + - unless @strike.text.blank? + = Formatter.instance.linkify(@strike.text) + + - if @strike.report && !@strike.report.other? + %p + %strong= t('user_mailer.warning.reason') + = t("user_mailer.warning.categories.#{@strike.report.category}") + + - if @strike.report.violation? && @strike.report.rule_ids.present? + %ul.rules-list + - @strike.report.rules.each do |rule| + %li= rule.text + + - if @strike.status_ids.present? && !@strike.status_ids.empty? + %p + %strong= t('user_mailer.warning.statuses') + + .strike-card__statuses-list + - status_map = @strike.statuses.includes(:application, :media_attachments).index_by(&:id) + + - @strike.status_ids.each do |status_id| + .strike-card__statuses-list__item + - if (status = status_map[status_id.to_i]) + .one-liner + = link_to short_account_status_url(@strike.target_account, status_id), class: 'emojify' do + = one_line_preview(status) + + - status.media_attachments.each do |media_attachment| + %abbr{ title: media_attachment.description } + = fa_icon 'link' + = media_attachment.file_file_name + .strike-card__statuses-list__item__meta + %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) + · + = status.application.name + - else + .one-liner= t('disputes.strikes.status', id: status_id) + .strike-card__statuses-list__item__meta + = t('disputes.strikes.status_removed') + + .report-header__details + .report-header__details__item + .report-header__details__item__header + %strong= t('disputes.strikes.created_at') + .report-header__details__item__content + %time.formatted{ datetime: @strike.created_at.iso8601, title: l(@strike.created_at) }= l(@strike.created_at) + .report-header__details__item + .report-header__details__item__header + %strong= t('disputes.strikes.recipient') + .report-header__details__item__content + = admin_account_link_to @strike.target_account, path: can?(:show, @strike.target_account) ? admin_account_path(@strike.target_account_id) : ActivityPub::TagManager.instance.url_for(@strike.target_account) + .report-header__details__item + .report-header__details__item__header + %strong= t('disputes.strikes.action_taken') + .report-header__details__item__content + - if @strike.overruled? + %del= t(@strike.action, scope: 'user_mailer.warning.title') + - else + = t(@strike.action, scope: 'user_mailer.warning.title') + - if @strike.report && can?(:show, @strike.report) + .report-header__details__item + .report-header__details__item__header + %strong= t('disputes.strikes.associated_report') + .report-header__details__item__content + = link_to t('admin.reports.report', id: @strike.report.id), admin_report_path(@strike.report) + - if @appeal.persisted? + .report-header__details__item + .report-header__details__item__header + %strong= t('disputes.strikes.appeal_submitted_at') + .report-header__details__item__content + %time.formatted{ datetime: @appeal.created_at.iso8601, title: l(@appeal.created_at) }= l(@appeal.created_at) +%hr.spacer/ + +- if @appeal.persisted? + %h3= t('disputes.strikes.appeal') + + .report-notes + .report-notes__item + = image_tag @appeal.account.avatar.url, class: 'report-notes__item__avatar' + + .report-notes__item__header + %span.username + = link_to @appeal.account.username, can?(:show, @appeal.account) ? admin_account_path(@appeal.account_id) : short_account_url(@appeal.account) + %time{ datetime: @appeal.created_at.iso8601, title: l(@appeal.created_at) } + - if @appeal.created_at.today? + = t('admin.report_notes.today_at', time: l(@appeal.created_at, format: :time)) + - else + = l @appeal.created_at.to_date + + .report-notes__item__content + = simple_format(h(@appeal.text)) +- elsif can?(:appeal, @strike) + %h3= t('disputes.strikes.appeals.submit') + + = simple_form_for(@appeal, url: disputes_strike_appeal_path(@strike)) do |f| + .fields-group + = f.input :text, wrapper: :with_label, input_html: { maxlength: 500 } + + .actions + = f.button :button, t('disputes.strikes.appeals.submit'), type: :submit diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index 61198171d..1a789cef8 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -53,5 +53,9 @@ %ul %li= link_to t('about.source_code'), Mastodon::Version.source_url %li= link_to t('about.apps'), 'https://joinmastodon.org/apps' + .legal-xs + = link_to "v#{Mastodon::Version.to_s}", Mastodon::Version.source_url + · + = link_to t('about.privacy_policy'), terms_path = render template: 'layouts/application' diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml index d7cc1ed5d..223e5d740 100644 --- a/app/views/settings/preferences/notifications/show.html.haml +++ b/app/views/settings/preferences/notifications/show.html.haml @@ -21,6 +21,7 @@ - if current_user.staff? = ff.input :report, as: :boolean, wrapper: :with_label + = ff.input :appeal, as: :boolean, wrapper: :with_label = ff.input :pending_account, as: :boolean, wrapper: :with_label = ff.input :trending_tag, as: :boolean, wrapper: :with_label diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml index cd5ed52af..1922f53ce 100644 --- a/app/views/statuses/_detailed_status.html.haml +++ b/app/views/statuses/_detailed_status.html.haml @@ -49,7 +49,7 @@ %span.detailed-status__visibility-icon = visibility_icon status · - - if status.application && @account.user&.setting_show_application + - if status.application && status.account.user&.setting_show_application - if status.application.website.blank? %strong.detailed-status__application= status.application.name - else diff --git a/app/views/user_mailer/appeal_approved.html.haml b/app/views/user_mailer/appeal_approved.html.haml new file mode 100644 index 000000000..962cab2e2 --- /dev/null +++ b/app/views/user_mailer/appeal_approved.html.haml @@ -0,0 +1,59 @@ +%table.email-table{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.email-body + .email-container + %table.content-section{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.content-cell.hero + .email-row + .col-6 + %table.column{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.column-cell.text-center.padded + %table.hero-icon{ align: 'center', cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td + = image_tag full_pack_url('media/images/mailer/icon_done.png'), alt: '' + + %h1= t 'user_mailer.appeal_approved.title' + +%table.email-table{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.email-body + .email-container + %table.content-section{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.content-cell.content-start + .email-row + .col-6 + %table.column{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.column-cell.text-center + %p= t 'user_mailer.appeal_approved.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at) + +%table.email-table{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.email-body + .email-container + %table.content-section{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.content-cell + %table.column{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.column-cell.button-cell + %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.button-primary + = link_to root_url do + %span= t 'user_mailer.appeal_approved.action' diff --git a/app/views/user_mailer/appeal_approved.text.erb b/app/views/user_mailer/appeal_approved.text.erb new file mode 100644 index 000000000..290fa24c3 --- /dev/null +++ b/app/views/user_mailer/appeal_approved.text.erb @@ -0,0 +1,7 @@ +<%= t 'user_mailer.appeal_approved.title' %> + +=== + +<%= t 'user_mailer.appeal_approved.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at) %> + +=> <%= root_url %> diff --git a/app/views/user_mailer/appeal_rejected.html.haml b/app/views/user_mailer/appeal_rejected.html.haml new file mode 100644 index 000000000..75cd9d023 --- /dev/null +++ b/app/views/user_mailer/appeal_rejected.html.haml @@ -0,0 +1,59 @@ +%table.email-table{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.email-body + .email-container + %table.content-section{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.content-cell.hero + .email-row + .col-6 + %table.column{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.column-cell.text-center.padded + %table.hero-icon{ align: 'center', cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td + = image_tag full_pack_url('media/images/mailer/icon_warning.png'), alt: '' + + %h1= t 'user_mailer.appeal_rejected.title' + +%table.email-table{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.email-body + .email-container + %table.content-section{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.content-cell.content-start + .email-row + .col-6 + %table.column{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.column-cell.text-center + %p= t 'user_mailer.appeal_rejected.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at) + +%table.email-table{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.email-body + .email-container + %table.content-section{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.content-cell + %table.column{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.column-cell.button-cell + %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.button-primary + = link_to root_url do + %span= t 'user_mailer.appeal_approved.action' diff --git a/app/views/user_mailer/appeal_rejected.text.erb b/app/views/user_mailer/appeal_rejected.text.erb new file mode 100644 index 000000000..f47a76818 --- /dev/null +++ b/app/views/user_mailer/appeal_rejected.text.erb @@ -0,0 +1,7 @@ +<%= t 'user_mailer.appeal_rejected.title' %> + +=== + +<%= t 'user_mailer.appeal_rejected.explanation', appeal_date: l(@appeal.created_at), strike_date: l(@appeal.strike.created_at) %> + +=> <%= root_url %> diff --git a/app/views/user_mailer/warning.html.haml b/app/views/user_mailer/warning.html.haml index bda1fef6c..b308e18f7 100644 --- a/app/views/user_mailer/warning.html.haml +++ b/app/views/user_mailer/warning.html.haml @@ -77,8 +77,8 @@ %tbody %tr %td.button-primary - = link_to about_more_url do - %span= t 'user_mailer.warning.review_server_policies' + = link_to disputes_strike_url(@warning) do + %span= t 'user_mailer.warning.appeal' %table.email-table{ cellspacing: 0, cellpadding: 0 } %tbody @@ -95,4 +95,4 @@ %tbody %tr %td.column-cell.text-center - %p= t 'user_mailer.warning.get_in_touch', instance: @instance + %p= t 'user_mailer.warning.appeal_description', instance: @instance diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 4245b7192..6ffe12ae0 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -3,31 +3,11 @@ { "warning_type": "SQL Injection", "warning_code": 0, - "fingerprint": "04dbbc249b989db2e0119bbb0f59c9818e12889d2b97c529cdc0b1526002ba4b", - "check_name": "SQL", - "message": "Possible SQL injection", - "file": "app/models/report.rb", - "line": 113, - "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", - "code": "Admin::ActionLog.from(\"(#{[Admin::ActionLog.where(:target_type => \"Report\", :target_id => id, :created_at => ((created_at..updated_at))).unscope(:order), Admin::ActionLog.where(:target_type => \"Account\", :target_id => target_account_id, :created_at => ((created_at..updated_at))).unscope(:order), Admin::ActionLog.where(:target_type => \"Status\", :target_id => status_ids, :created_at => ((created_at..updated_at))).unscope(:order)].map do\n \"(#{query.to_sql})\"\n end.join(\" UNION ALL \")}) AS admin_action_logs\")", - "render_path": null, - "location": { - "type": "method", - "class": "Report", - "method": "history" - }, - "user_input": "Admin::ActionLog.where(:target_type => \"Status\", :target_id => status_ids, :created_at => ((created_at..updated_at))).unscope(:order)", - "confidence": "High", - "note": "" - }, - { - "warning_type": "SQL Injection", - "warning_code": 0, "fingerprint": "19df3740b8d02a9fe0eb52c939b4b87d3a2a591162a6adfa8d64e9c26aeebe6d", "check_name": "SQL", "message": "Possible SQL injection", "file": "app/models/status.rb", - "line": 100, + "line": 104, "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", "code": "result.joins(\"INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}\")", "render_path": null, @@ -107,7 +87,7 @@ "check_name": "PermitAttributes", "message": "Potentially dangerous key allowed for mass assignment", "file": "app/controllers/api/v1/admin/reports_controller.rb", - "line": 78, + "line": 90, "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/", "code": "params.permit(:resolved, :account_id, :target_account_id)", "render_path": null, @@ -141,6 +121,36 @@ "note": "" }, { + "warning_type": "Cross-Site Scripting", + "warning_code": 2, + "fingerprint": "afad51718ae373b2f19d2513029fd2afccf58b9148e475934bc6a162ee33c352", + "check_name": "CrossSiteScripting", + "message": "Unescaped model attribute", + "file": "app/views/admin/disputes/appeals/_appeal.html.haml", + "line": 7, + "link": "https://brakemanscanner.org/docs/warning_types/cross_site_scripting", + "code": "t((Unresolved Model).new.strike.action, :scope => \"admin.strikes.actions\", :name => content_tag(:span, (Unresolved Model).new.strike.account.username, :class => \"username\"), :target => content_tag(:span, (Unresolved Model).new.account.acct, :class => \"target\"))", + "render_path": [ + { + "type": "template", + "name": "admin/disputes/appeals/index", + "line": 16, + "file": "app/views/admin/disputes/appeals/index.html.haml", + "rendered": { + "name": "admin/disputes/appeals/_appeal", + "file": "app/views/admin/disputes/appeals/_appeal.html.haml" + } + } + ], + "location": { + "type": "template", + "template": "admin/disputes/appeals/_appeal" + }, + "user_input": "(Unresolved Model).new.strike", + "confidence": "Weak", + "note": "" + }, + { "warning_type": "Redirect", "warning_code": 18, "fingerprint": "ba568ac09683f98740f663f3d850c31785900215992e8c090497d359a2563d50", @@ -194,7 +204,7 @@ { "type": "template", "name": "admin/trends/links/index", - "line": 37, + "line": 39, "file": "app/views/admin/trends/links/index.html.haml", "rendered": { "name": "admin/trends/links/_preview_card", @@ -213,13 +223,13 @@ { "warning_type": "Mass Assignment", "warning_code": 105, - "fingerprint": "e867661b2c9812bc8b75a5df12b28e2a53ab97015de0638b4e732fe442561b28", + "fingerprint": "f9de0ca4b04ae4b51b74d98db14dcbb6dae6809e627b58e711019cf9b4a47866", "check_name": "PermitAttributes", "message": "Potentially dangerous key allowed for mass assignment", "file": "app/controllers/api/v1/reports_controller.rb", "line": 36, "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/", - "code": "params.permit(:account_id, :comment, :forward, :status_ids => ([]))", + "code": "params.permit(:account_id, :comment, :category, :forward, :status_ids => ([]), :rule_ids => ([]))", "render_path": null, "location": { "type": "method", @@ -231,6 +241,6 @@ "note": "" } ], - "updated": "2021-11-14 05:26:09 +0100", - "brakeman_version": "5.1.2" + "updated": "2022-02-13 02:24:12 +0100", + "brakeman_version": "5.2.1" } diff --git a/config/locales/en.yml b/config/locales/en.yml index 1809f123e..cf4c2cc37 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -94,7 +94,6 @@ en: account_moderation_notes: create: Leave note created_msg: Moderation note successfully created! - delete: Delete destroyed_msg: Moderation note successfully destroyed! accounts: add_email_domain_block: Block e-mail domain @@ -163,6 +162,11 @@ en: not_subscribed: Not subscribed pending: Pending review perform_full_suspension: Suspend + previous_strikes: Previous strikes + previous_strikes_description_html: + one: This account has <strong>one</strong> strike. + other: This account has <strong>%{count}</strong> strikes. + zero: This account is <strong>in good standing</strong>. promote: Promote protocol: Protocol public: Public @@ -227,6 +231,7 @@ en: whitelisted: Allowed for federation action_logs: action_types: + approve_appeal: Approve Appeal approve_user: Approve User assigned_to_self_report: Assign Report change_email_user: Change E-mail for User @@ -258,6 +263,7 @@ en: enable_user: Enable User memorialize_account: Memorialize Account promote_user: Promote User + reject_appeal: Reject Appeal reject_user: Reject User remove_avatar_user: Remove Avatar reopen_report: Reopen Report @@ -276,6 +282,7 @@ en: update_domain_block: Update Domain Block update_status: Update Post actions: + approve_appeal_html: "%{name} approved moderation decision appeal from %{target}" approve_user_html: "%{name} approved sign-up from %{target}" assigned_to_self_report_html: "%{name} assigned report %{target} to themselves" change_email_user_html: "%{name} changed the e-mail address of user %{target}" @@ -307,6 +314,7 @@ en: enable_user_html: "%{name} enabled login for user %{target}" memorialize_account_html: "%{name} turned %{target}'s account into a memoriam page" promote_user_html: "%{name} promoted user %{target}" + reject_appeal_html: "%{name} rejected moderation decision appeal from %{target}" reject_user_html: "%{name} rejected sign-up from %{target}" remove_avatar_user_html: "%{name} removed %{target}'s avatar" reopen_report_html: "%{name} reopened report %{target}" @@ -385,14 +393,17 @@ en: media_storage: Media storage new_users: new users opened_reports: reports opened + pending_appeals_html: + one: "<strong>%{count}</strong> pending appeal" + other: "<strong>%{count}</strong> pending appeals" pending_reports_html: - one: "<strong>1</strong> pending report" + one: "<strong>%{count}</strong> pending report" other: "<strong>%{count}</strong> pending reports" pending_tags_html: - one: "<strong>1</strong> pending hashtag" + one: "<strong>%{count}</strong> pending hashtag" other: "<strong>%{count}</strong> pending hashtags" pending_users_html: - one: "<strong>1</strong> pending user" + one: "<strong>%{count}</strong> pending user" other: "<strong>%{count}</strong> pending users" resolved_reports: reports resolved software: Software @@ -402,6 +413,10 @@ en: top_languages: Top active languages top_servers: Top active servers website: Website + disputes: + appeals: + empty: No appeals found. + title: Appeals domain_allows: add_new: Allow federation with domain created_msg: Domain has been successfully allowed for federation @@ -442,6 +457,7 @@ en: affected_accounts: one: One account in the database affected other: "%{count} accounts in the database affected" + zero: No account in the database is affected retroactive: silence: Undo limit of existing affected accounts from this domain suspend: Unsuspend existing affected accounts from this domain @@ -495,6 +511,7 @@ en: known_accounts: one: "%{count} known account" other: "%{count} known accounts" + zero: No known account moderation: all: All limited: Limited @@ -720,6 +737,16 @@ en: no_status_selected: No posts were changed as none were selected title: Account posts with_media: With media + strikes: + actions: + delete_statuses: "%{name} deleted %{target}'s posts" + disable: "%{name} froze %{target}'s account" + none: "%{name} sent a warning to %{target}" + sensitive: "%{name} marked %{target}'s account as sensitive" + silence: "%{name} limited %{target}'s account" + suspend: "%{name} suspended %{target}'s account" + appeal_approved: Appealed + appeal_pending: Appeal pending system_checks: database_schema_check: message_html: There are pending database migrations. Please run them to ensure the application behaves as expected @@ -744,6 +771,7 @@ en: shared_by_over_week: one: Shared by one person over the last week other: Shared by %{count} people over the last week + zero: Shared by noone over the last week title: Trending links usage_comparison: Shared %{today} times today, compared to %{yesterday} yesterday pending_review: Pending review @@ -773,6 +801,7 @@ en: used_by_over_week: one: Used by one person over the last week other: Used by %{count} people over the last week + zero: Used by noone over the last week title: Trends warning_presets: add_new: Add new @@ -781,6 +810,17 @@ en: empty: You haven't defined any warning presets yet. title: Manage warning presets admin_mailer: + new_appeal: + actions: + delete_statuses: to delete their posts + disable: to freeze their account + none: a warning + sensitive: to mark their account as sensitive + silence: to limit their account + suspend: to suspend their account + body: "%{target} is appealing a moderation decision by %{action_taken_by} from %{date}, which was %{type}. They wrote:" + next_steps: You can approve the appeal to undo the moderation decision, or ignore it. + subject: "%{username} is appealing a moderation decision on %{instance}" new_pending_account: body: The details of the new account are below. You can approve or reject this application. subject: New account up for review on %{instance} (%{username}) @@ -871,7 +911,6 @@ en: status: account_status: Account status confirming: Waiting for e-mail confirmation to be completed. - functional: Your account is fully operational. pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved. redirecting_to: Your account is inactive because it is currently redirecting to %{acct}. too_fast: Form submitted too fast, try again. @@ -937,6 +976,32 @@ en: directory: Profile directory explanation: Discover users based on their interests explore_mastodon: Explore %{title} + disputes: + strikes: + action_taken: Action taken + appeal: Appeal + appeal_approved: This strike has been successfully appealed and is no longer valid + appeal_rejected: The appeal has been rejected + appeal_submitted_at: Appeal submitted + appealed_msg: Your appeal has been submitted. If it is approved, you will be notified. + appeals: + submit: Submit appeal + associated_report: Associated report + created_at: Dated + recipient: Addressed to + status: 'Post #%{id}' + status_removed: Post already removed from system + title: "%{action} from %{date}" + title_actions: + delete_statuses: Post removal + disable: Freezing of account + none: Warning + sensitive: Marking as sensitive of account + silence: Limitation of account + suspend: Suspension of account + your_appeal_approved: Your appeal has been approved + your_appeal_pending: You have submitted an appeal + your_appeal_rejected: Your appeal has been rejected domain_validator: invalid_domain: is not a valid domain name errors: @@ -1501,6 +1566,15 @@ en: recovery_instructions_html: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. <strong>Keep the recovery codes safe</strong>. For example, you may print them and store them with other important documents. webauthn: Security keys user_mailer: + appeal_approved: + action: Go to your account + explanation: The appeal of the strike against your account on %{strike_date} that you submitted on %{appeal_date} has been approved. Your account is once again in good standing. + subject: Your appeal from %{date} has been approved + title: Appeal approved + appeal_rejected: + explanation: The appeal of the strike against your account on %{strike_date} that you submitted on %{appeal_date} has been rejected. + subject: Your appeal from %{date} has been rejected + title: Appeal rejected backup_ready: explanation: You requested a full backup of your Mastodon account. It's now ready for download! subject: Your archive is ready for download @@ -1512,6 +1586,8 @@ en: subject: Please confirm attempted sign in title: Sign in attempt warning: + appeal: Submit an appeal + appeal_description: If you believe this is an error, you can submit an appeal to the staff of %{instance}. categories: spam: Spam violation: Content violates the following community guidelines @@ -1523,7 +1599,6 @@ en: 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 in about 30 days, but we will retain some basic data to prevent you from evading the suspension. get_in_touch: If you believe this is an error, you can reply to this e-mail to get in touch with the staff of %{instance}. reason: 'Reason:' - review_server_policies: Review server policies statuses: 'Posts that have been found in violation:' subject: delete_statuses: Your posts on %{acct} have been removed diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index d6376782d..03eefd0d5 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -27,6 +27,8 @@ en: scheduled_at: Leave blank to publish the announcement immediately starts_at: Optional. In case your announcement is bound to a specific time range text: You can use post syntax. Please be mindful of the space the announcement will take up on the user's screen + appeal: + text: You can only appeal a strike once defaults: autofollow: People who sign up through the invite will automatically follow you avatar: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px @@ -119,6 +121,8 @@ en: scheduled_at: Schedule publication starts_at: Start of event text: Announcement + appeal: + text: Explain why this decision should be reversed defaults: autofollow: Invite to follow your account avatar: Avatar @@ -197,6 +201,7 @@ en: sign_up_requires_approval: Limit sign-ups severity: Rule notification_emails: + appeal: Someone appeals a moderator decision digest: Send digest e-mails favourite: Someone favourited your post follow: Someone followed you @@ -204,8 +209,8 @@ en: mention: Someone mentioned you pending_account: New account needs review reblog: Someone boosted your post - report: A new report is submitted - trending_tag: A new trend requires approval + report: New report is submitted + trending_tag: New trend requires review rule: text: Rule tag: diff --git a/config/navigation.rb b/config/navigation.rb index a5590d2ea..a85670500 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -26,7 +26,7 @@ SimpleNavigation::Configuration.run do |navigation| n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_url, if: -> { current_user.functional? } n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s| - s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities} + s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities|^/disputes} 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 @@ -47,7 +47,7 @@ SimpleNavigation::Configuration.run do |navigation| n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), admin_reports_url, if: proc { current_user.staff? } do |s| s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports} - s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts} + s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts|/admin/disputes} s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path s.item :follow_recommendations, safe_join([fa_icon('user-plus fw'), t('admin.follow_recommendations.title')]), admin_follow_recommendations_path, highlights_on: %r{/admin/follow_recommendations} s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? } diff --git a/config/routes.rb b/config/routes.rb index 0eb7f1b0f..a138fcbcc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -167,6 +167,12 @@ Rails.application.routes.draw do resources :login_activities, only: [:index] end + namespace :disputes do + resources :strikes, only: [:show] do + resource :appeal, only: [:create] + end + end + resources :media, only: [:show] do get :player end @@ -327,6 +333,15 @@ Rails.application.routes.draw do end end end + + namespace :disputes do + resources :appeals, only: [:index] do + member do + post :approve + post :reject + end + end + end end get '/admin', to: redirect('/admin/dashboard', status: 302) diff --git a/config/settings.yml b/config/settings.yml index 7d192f369..d0946a668 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -52,6 +52,7 @@ defaults: &defaults report: true pending_account: true trending_tag: true + appeal: true interactions: must_be_follower: false must_be_following: false diff --git a/db/migrate/20220124141035_create_appeals.rb b/db/migrate/20220124141035_create_appeals.rb new file mode 100644 index 000000000..afb3efbd5 --- /dev/null +++ b/db/migrate/20220124141035_create_appeals.rb @@ -0,0 +1,14 @@ +class CreateAppeals < ActiveRecord::Migration[6.1] + def change + create_table :appeals do |t| + t.belongs_to :account, null: false, foreign_key: { on_delete: :cascade } + t.belongs_to :account_warning, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true } + t.text :text, null: false, default: '' + t.datetime :approved_at + t.belongs_to :approved_by_account, foreign_key: { to_table: :accounts, on_delete: :nullify } + t.datetime :rejected_at + t.belongs_to :rejected_by_account, foreign_key: { to_table: :accounts, on_delete: :nullify } + t.timestamps + end + end +end diff --git a/db/migrate/20220210153119_add_overruled_at_to_account_warnings.rb b/db/migrate/20220210153119_add_overruled_at_to_account_warnings.rb new file mode 100644 index 000000000..a082da774 --- /dev/null +++ b/db/migrate/20220210153119_add_overruled_at_to_account_warnings.rb @@ -0,0 +1,5 @@ +class AddOverruledAtToAccountWarnings < ActiveRecord::Migration[6.1] + def change + add_column :account_warnings, :overruled_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 252373a7c..e27e04b71 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: 2022_02_09_175231) do +ActiveRecord::Schema.define(version: 2022_02_10_153119) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -135,6 +135,7 @@ ActiveRecord::Schema.define(version: 2022_02_09_175231) do t.datetime "updated_at", null: false t.bigint "report_id" t.string "status_ids", array: true + t.datetime "overruled_at" t.index ["account_id"], name: "index_account_warnings_on_account_id" t.index ["target_account_id"], name: "index_account_warnings_on_target_account_id" end @@ -243,6 +244,22 @@ ActiveRecord::Schema.define(version: 2022_02_09_175231) do t.bigint "status_ids", array: true end + create_table "appeals", force: :cascade do |t| + t.bigint "account_id", null: false + t.bigint "account_warning_id", null: false + t.text "text", default: "", null: false + t.datetime "approved_at" + t.bigint "approved_by_account_id" + t.datetime "rejected_at" + t.bigint "rejected_by_account_id" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["account_id"], name: "index_appeals_on_account_id" + t.index ["account_warning_id"], name: "index_appeals_on_account_warning_id", unique: true + t.index ["approved_by_account_id"], name: "index_appeals_on_approved_by_account_id" + t.index ["rejected_by_account_id"], name: "index_appeals_on_rejected_by_account_id" + end + create_table "backups", force: :cascade do |t| t.bigint "user_id" t.string "dump_file_name" @@ -1034,6 +1051,10 @@ ActiveRecord::Schema.define(version: 2022_02_09_175231) do add_foreign_key "announcement_reactions", "accounts", on_delete: :cascade add_foreign_key "announcement_reactions", "announcements", on_delete: :cascade add_foreign_key "announcement_reactions", "custom_emojis", on_delete: :cascade + add_foreign_key "appeals", "account_warnings", on_delete: :cascade + add_foreign_key "appeals", "accounts", column: "approved_by_account_id", on_delete: :nullify + add_foreign_key "appeals", "accounts", column: "rejected_by_account_id", on_delete: :nullify + add_foreign_key "appeals", "accounts", on_delete: :cascade add_foreign_key "backups", "users", on_delete: :nullify add_foreign_key "blocks", "accounts", column: "target_account_id", name: "fk_9571bfabc1", on_delete: :cascade add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade diff --git a/package.json b/package.json index 4f876816f..77328c4c1 100644 --- a/package.json +++ b/package.json @@ -62,12 +62,12 @@ "private": true, "dependencies": { "@babel/core": "^7.17.2", - "@babel/plugin-proposal-decorators": "^7.17.0", + "@babel/plugin-proposal-decorators": "^7.17.2", "@babel/plugin-transform-react-inline-elements": "^7.16.7", "@babel/plugin-transform-runtime": "^7.17.0", "@babel/preset-env": "^7.16.11", "@babel/preset-react": "^7.16.7", - "@babel/runtime": "^7.17.0", + "@babel/runtime": "^7.17.2", "@gamestdio/websocket": "^0.3.2", "@github/webauthn-json": "^0.5.7", "@rails/ujs": "^6.1.4", @@ -75,7 +75,7 @@ "atrament": "0.2.4", "arrow-key-navigation": "^1.2.0", "autoprefixer": "^9.8.8", - "axios": "^0.25.0", + "axios": "^0.26.0", "babel-loader": "^8.2.3", "babel-plugin-lodash": "^3.3.4", "babel-plugin-preval": "^5.1.0", @@ -90,7 +90,7 @@ "css-loader": "^5.2.7", "cssnano": "^4.1.11", "detect-passive-events": "^2.0.3", - "dotenv": "^10.0.0", + "dotenv": "^16.0.0", "emoji-mart": "npm:emoji-mart-lazyload", "es6-symbol": "^3.1.3", "escape-html": "^1.0.3", @@ -115,7 +115,7 @@ "marky": "^1.2.2", "mini-css-extract-plugin": "^1.6.2", "mkdirp": "^1.0.4", - "npmlog": "^6.0.0", + "npmlog": "^6.0.1", "object-assign": "^4.1.1", "object-fit-images": "^3.2.3", "object.values": "^1.1.5", @@ -178,7 +178,7 @@ "@testing-library/jest-dom": "^5.16.2", "@testing-library/react": "^12.1.2", "babel-eslint": "^10.1.0", - "babel-jest": "^27.5.0", + "babel-jest": "^27.5.1", "eslint": "^7.32.0", "eslint-plugin-import": "~2.25.4", "eslint-plugin-jsx-a11y": "~6.5.1", diff --git a/spec/controllers/admin/disputes/appeals_controller_spec.rb b/spec/controllers/admin/disputes/appeals_controller_spec.rb new file mode 100644 index 000000000..6a06f9406 --- /dev/null +++ b/spec/controllers/admin/disputes/appeals_controller_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +RSpec.describe Admin::Disputes::AppealsController, type: :controller do + render_views + + before { sign_in current_user, scope: :user } + + let(:target_account) { Fabricate(:account) } + let(:strike) { Fabricate(:account_warning, target_account: target_account, action: :suspend) } + let(:appeal) { Fabricate(:appeal, strike: strike, account: target_account) } + + before do + target_account.suspend! + end + + describe 'POST #approve' do + let(:current_user) { Fabricate(:user, admin: true) } + + before do + allow(UserMailer).to receive(:appeal_approved).and_return(double('email', deliver_later: nil)) + post :approve, params: { id: appeal.id } + end + + it 'unsuspends a suspended account' do + expect(target_account.reload.suspended?).to be false + end + + it 'redirects back to the strike page' do + expect(response).to redirect_to(disputes_strike_path(appeal.strike)) + end + + it 'notifies target account about approved appeal' do + expect(UserMailer).to have_received(:appeal_approved).with(target_account.user, appeal) + end + end + + describe 'POST #reject' do + let(:current_user) { Fabricate(:user, admin: true) } + + before do + allow(UserMailer).to receive(:appeal_rejected).and_return(double('email', deliver_later: nil)) + post :reject, params: { id: appeal.id } + end + + it 'redirects back to the strike page' do + expect(response).to redirect_to(disputes_strike_path(appeal.strike)) + end + + it 'notifies target account about rejected appeal' do + expect(UserMailer).to have_received(:appeal_rejected).with(target_account.user, appeal) + end + end +end diff --git a/spec/controllers/disputes/appeals_controller_spec.rb b/spec/controllers/disputes/appeals_controller_spec.rb new file mode 100644 index 000000000..faa571fc9 --- /dev/null +++ b/spec/controllers/disputes/appeals_controller_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +RSpec.describe Disputes::AppealsController, type: :controller do + render_views + + before { sign_in current_user, scope: :user } + + let!(:admin) { Fabricate(:user, admin: true) } + + describe '#create' do + let(:current_user) { Fabricate(:user) } + let(:strike) { Fabricate(:account_warning, target_account: current_user.account) } + + before do + allow(AdminMailer).to receive(:new_appeal).and_return(double('email', deliver_later: nil)) + post :create, params: { strike_id: strike.id, appeal: { text: 'Foo' } } + end + + it 'notifies staff about new appeal' do + expect(AdminMailer).to have_received(:new_appeal).with(admin.account, Appeal.last) + end + + it 'redirects back to the strike page' do + expect(response).to redirect_to(disputes_strike_path(strike.id)) + end + end +end diff --git a/spec/controllers/disputes/strikes_controller_spec.rb b/spec/controllers/disputes/strikes_controller_spec.rb new file mode 100644 index 000000000..157f9ec3c --- /dev/null +++ b/spec/controllers/disputes/strikes_controller_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +RSpec.describe Disputes::StrikesController, type: :controller do + render_views + + before { sign_in current_user, scope: :user } + + describe '#show' do + let(:current_user) { Fabricate(:user) } + let(:strike) { Fabricate(:account_warning, target_account: current_user.account) } + + before do + get :show, params: { id: strike.id } + end + + context 'when meant for the user' do + it 'returns http success' do + expect(response).to have_http_status(:success) + end + end + + context 'when meant for a different user' do + let(:strike) { Fabricate(:account_warning) } + + it 'returns http forbidden' do + expect(response).to have_http_status(:forbidden) + end + end + end +end diff --git a/spec/fabricators/account_warning_fabricator.rb b/spec/fabricators/account_warning_fabricator.rb index db161d446..72fe835d9 100644 --- a/spec/fabricators/account_warning_fabricator.rb +++ b/spec/fabricators/account_warning_fabricator.rb @@ -1,5 +1,6 @@ Fabricator(:account_warning) do - account nil - target_account nil - text "MyText" + account + target_account(fabricator: :account) + text { Faker::Lorem.paragraph } + action 'suspend' end diff --git a/spec/fabricators/appeal_fabricator.rb b/spec/fabricators/appeal_fabricator.rb new file mode 100644 index 000000000..339363822 --- /dev/null +++ b/spec/fabricators/appeal_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:appeal) do + strike(fabricator: :account_warning) + account { |attrs| attrs[:strike].target_account } + text { Faker::Lorem.paragraph } +end diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb index b93fcbe66..41806b258 100644 --- a/spec/lib/activitypub/activity/announce_spec.rb +++ b/spec/lib/activitypub/activity/announce_spec.rb @@ -113,26 +113,23 @@ RSpec.describe ActivityPub::Activity::Announce do let!(:relay_account) { Fabricate(:account, inbox_url: 'https://relay.example.com/inbox') } let!(:relay) { Fabricate(:relay, inbox_url: 'https://relay.example.com/inbox') } + let(:object_json) { 'https://example.com/actor/hello-world' } + subject { described_class.new(json, sender, relayed_through_account: relay_account) } + before do + stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json)) + end + context 'and the relay is enabled' do before do relay.update(state: :accepted) subject.perform end - let(:object_json) do - { - id: 'https://example.com/actor#bar', - type: 'Note', - content: 'Lorem ipsum', - to: 'http://example.com/followers', - attributedTo: 'https://example.com/actor', - } - end - - it 'creates a reblog by sender of status' do - expect(sender.statuses.count).to eq 2 + it 'fetches the remote status' do + expect(a_request(:get, 'https://example.com/actor/hello-world')).to have_been_made + expect(Status.find_by(uri: 'https://example.com/actor/hello-world').text).to eq 'Hello world' end end @@ -141,14 +138,9 @@ RSpec.describe ActivityPub::Activity::Announce do subject.perform end - let(:object_json) do - { - id: 'https://example.com/actor#bar', - type: 'Note', - content: 'Lorem ipsum', - to: 'http://example.com/followers', - attributedTo: 'https://example.com/actor', - } + it 'does not fetch the remote status' do + expect(a_request(:get, 'https://example.com/actor/hello-world')).not_to have_been_made + expect(Status.find_by(uri: 'https://example.com/actor/hello-world')).to be_nil end it 'does not create anything' do diff --git a/spec/mailers/previews/admin_mailer_preview.rb b/spec/mailers/previews/admin_mailer_preview.rb index 75ffbbf40..9c0372b47 100644 --- a/spec/mailers/previews/admin_mailer_preview.rb +++ b/spec/mailers/previews/admin_mailer_preview.rb @@ -15,4 +15,9 @@ class AdminMailerPreview < ActionMailer::Preview def new_trending_links AdminMailer.new_trending_links(Account.first, PreviewCard.limit(3)) end + + # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_appeal + def new_appeal + AdminMailer.new_appeal(Account.first, Appeal.first) + end end diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index 69b9b971e..8de7d8669 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -82,6 +82,11 @@ class UserMailerPreview < ActionMailer::Preview UserMailer.warning(User.first, AccountWarning.last) end + # Preview this email at http://localhost:3000/rails/mailers/user_mailer/appeal_approved + def appeal_approved + UserMailer.appeal_approved(User.first, Appeal.last) + end + # Preview this email at http://localhost:3000/rails/mailers/user_mailer/sign_in_token def sign_in_token UserMailer.sign_in_token(User.first.tap { |user| user.generate_sign_in_token }, '127.0.0.1', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0', Time.now.utc) diff --git a/spec/models/appeal_spec.rb b/spec/models/appeal_spec.rb new file mode 100644 index 000000000..14062dc4f --- /dev/null +++ b/spec/models/appeal_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Appeal, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 406438c22..1645ab59e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -89,6 +89,19 @@ RSpec.describe User, type: :model do expect(User.matches_email('specified')).to match_array([specified]) end end + + describe 'matches_ip' do + it 'returns a relation of users whose ip address is matching with the given CIDR' do + user1 = Fabricate(:user) + user2 = Fabricate(:user) + Fabricate(:session_activation, user: user1, ip: '2160:2160::22', session_id: '1') + Fabricate(:session_activation, user: user1, ip: '2160:2160::23', session_id: '2') + Fabricate(:session_activation, user: user2, ip: '2160:8888::24', session_id: '3') + Fabricate(:session_activation, user: user2, ip: '2160:8888::25', session_id: '4') + + expect(User.matches_ip('2160:2160::/32')).to match_array([user1]) + end + end end let(:account) { Fabricate(:account, username: 'alice') } diff --git a/streaming/index.js b/streaming/index.js index 2dbb546c0..3fdc9615e 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -92,13 +92,18 @@ const numWorkers = +process.env.STREAMING_CLUSTER_NUM || (env === 'development' /** * @param {string} json + * @param {any} req * @return {Object.<string, any>|null} */ -const parseJSON = (json) => { +const parseJSON = (json, req) => { try { return JSON.parse(json); } catch (err) { - log.error(err); + if (req.accountId) { + log.warn(req.requestId, `Error parsing message from user ${req.accountId}: ${err}`); + } else { + log.silly(req.requestId, `Error parsing message from ${req.remoteAddress}: ${err}`); + } return null; } }; @@ -451,7 +456,7 @@ const startWorker = async (workerId) => { */ const createSystemMessageListener = (req, eventHandlers) => { return message => { - const json = parseJSON(message); + const json = parseJSON(message, req); if (!json) return; @@ -575,7 +580,7 @@ const startWorker = async (workerId) => { log.verbose(req.requestId, `Starting stream from ${ids.join(', ')} for ${accountId}`); const listener = message => { - const json = parseJSON(message); + const json = parseJSON(message, req); if (!json) return; @@ -1059,7 +1064,7 @@ const startWorker = async (workerId) => { ws.on('error', onEnd); ws.on('message', data => { - const json = parseJSON(data); + const json = parseJSON(data, session.request); if (!json) return; diff --git a/yarn.lock b/yarn.lock index 3d6b779ce..ca5f384be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -91,7 +91,7 @@ browserslist "^4.17.5" semver "^6.3.0" -"@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.17.0": +"@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.17.1": version "7.17.1" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.1.tgz#9699f14a88833a7e055ce57dcd3ffdcd25186b21" integrity sha512-JBdSr/LtyYIno/pNnJ75lBcqc3Z1XXujzPanHqjvvrhOA+DTceTFuJi8XjmWTZh4r3fsdfqaCMN0iZemdkxZHQ== @@ -357,12 +357,12 @@ "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-class-static-block" "^7.14.5" -"@babel/plugin-proposal-decorators@^7.17.0": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.17.0.tgz#fc0f689fe2535075056c587bc10c176fa9990443" - integrity sha512-JR8HTf3T1CsdMqfENrZ9pqncwsH4sPcvsyDLpvmv8iIbpDmeyBD7HPfGAIqkQph2j5d3B84hTm+m3qHPAedaPw== +"@babel/plugin-proposal-decorators@^7.17.2": + version "7.17.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.17.2.tgz#c36372ddfe0360cac1ee331a238310bddca11493" + integrity sha512-WH8Z95CwTq/W8rFbMqb9p3hicpt4RX4f0K659ax2VHxgOyT6qQmUaEVEjIh4WR9Eh9NymkVn5vwsrE68fAQNUw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.17.0" + "@babel/helper-create-class-features-plugin" "^7.17.1" "@babel/helper-plugin-utils" "^7.16.7" "@babel/helper-replace-supers" "^7.16.7" "@babel/plugin-syntax-decorators" "^7.17.0" @@ -1024,10 +1024,10 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.0", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.0.tgz#b8d142fc0f7664fb3d9b5833fd40dcbab89276c0" - integrity sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": + version "7.17.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" + integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== dependencies: regenerator-runtime "^0.13.4" @@ -1338,27 +1338,6 @@ jest-haste-map "^27.5.1" jest-runtime "^27.5.1" -"@jest/transform@^27.5.0": - version "27.5.0" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.0.tgz#a4941e69ac51e8aa9a255ff4855b564c228c400b" - integrity sha512-yXUy/iO3TH1itxJ9BF7LLjuXt8TtgtjAl0PBQbUaCvRa+L0yYBob6uayW9dFRX/CDQweouLhvmXh44zRiaB+yA== - dependencies: - "@babel/core" "^7.1.0" - "@jest/types" "^27.5.0" - babel-plugin-istanbul "^6.1.1" - chalk "^4.0.0" - convert-source-map "^1.4.0" - fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.2.9" - jest-haste-map "^27.5.0" - jest-regex-util "^27.5.0" - jest-util "^27.5.0" - micromatch "^4.0.4" - pirates "^4.0.4" - slash "^3.0.0" - source-map "^0.6.1" - write-file-atomic "^3.0.0" - "@jest/transform@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" @@ -1390,18 +1369,7 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" -"@jest/types@^27.0.2", "@jest/types@^27.5.0": - version "27.5.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.0.tgz#6ad04a5c5355fd9f46e5cf761850e0edb3c209dd" - integrity sha512-oDHEp7gwSgA82RZ6pzUL3ugM2njP/lVB1MsxRZNOBk+CoNvh9SpH1lQixPFc/kDlV50v59csiW4HLixWmhmgPQ== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^16.0.0" - chalk "^4.0.0" - -"@jest/types@^27.5.1": +"@jest/types@^27.0.2", "@jest/types@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== @@ -2148,10 +2116,10 @@ aproba@^1.1.1: resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -are-we-there-yet@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" - integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== +are-we-there-yet@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.0.tgz#ba20bd6b553e31d62fc8c31bd23d22b95734390d" + integrity sha512-0GWpv50YSOcLXaN6/FAKY3vfRbllXWV2xvfA/oKJF8pzFhWXPV+yjhJXDBbjscDYowv7Yw1A3uigpzn5iEGTyw== dependencies: delegates "^1.0.0" readable-stream "^3.6.0" @@ -2340,12 +2308,12 @@ axe-core@^4.3.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5" integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA== -axios@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" - integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== +axios@^0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.0.tgz#9a318f1c69ec108f8cd5f3c3d390366635e13928" + integrity sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og== dependencies: - follow-redirects "^1.14.7" + follow-redirects "^1.14.8" axobject-query@^2.2.0: version "2.2.0" @@ -2364,20 +2332,6 @@ babel-eslint@^10.1.0: eslint-visitor-keys "^1.0.0" resolve "^1.12.0" -babel-jest@^27.5.0: - version "27.5.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.0.tgz#c653985241af3c76f59d70d65a570860c2594a50" - integrity sha512-puhCyvBTNLevhbd1oyw6t3gWBicWoUARQYKCBB/B1moif17NbyhxbsfadqZIw8zfJJD+W7Vw0Nb20pEjLxkXqQ== - dependencies: - "@jest/transform" "^27.5.0" - "@jest/types" "^27.5.0" - "@types/babel__core" "^7.1.14" - babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^27.5.0" - chalk "^4.0.0" - graceful-fs "^4.2.9" - slash "^3.0.0" - babel-jest@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" @@ -2420,16 +2374,6 @@ babel-plugin-istanbul@^6.1.1: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^27.5.0: - version "27.5.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.0.tgz#8fdf07835f2165a068de3ce95fd7749a89801b51" - integrity sha512-ztwNkHl+g1GaoQcb8f2BER4C3LMvSXuF7KVqtUioXQgScSEnkl6lLgCILUYIR+CPTwL8H3F/PNLze64HPWF9JA== - dependencies: - "@babel/template" "^7.3.3" - "@babel/types" "^7.3.3" - "@types/babel__core" "^7.0.0" - "@types/babel__traverse" "^7.0.6" - babel-plugin-jest-hoist@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" @@ -2530,14 +2474,6 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^27.5.0: - version "27.5.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.0.tgz#4e308711c3d2ff1f45cf5d9a23646e37b621fc9f" - integrity sha512-7bfu1cJBlgK/nKfTvMlElzA3jpi6GzDWX3fntnyP2cQSzoi/KUz6ewGlcb3PSRYZGyv+uPnVHY0Im3JbsViqgA== - dependencies: - babel-plugin-jest-hoist "^27.5.0" - babel-preset-current-node-syntax "^1.0.0" - babel-preset-jest@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" @@ -4078,10 +4014,10 @@ dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" -dotenv@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" - integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== +dotenv@^16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411" + integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q== duplexer@^0.1.2: version "0.1.2" @@ -4999,7 +4935,7 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0, follow-redirects@^1.14.7: +follow-redirects@^1.0.0, follow-redirects@^1.14.8: version "1.14.8" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== @@ -6476,26 +6412,6 @@ jest-get-type@^27.5.1: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== -jest-haste-map@^27.5.0: - version "27.5.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.0.tgz#7cc3a920caf304c89fbfceb5d5717b929873f175" - integrity sha512-0KfckSBEKV+D6e0toXmIj4zzp72EiBnvkC0L+xYxenkLhAdkp2/8tye4AgMzz7Fqb1r8SWtz7+s1UQLrxMBang== - dependencies: - "@jest/types" "^27.5.0" - "@types/graceful-fs" "^4.1.2" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.9" - jest-regex-util "^27.5.0" - jest-serializer "^27.5.0" - jest-util "^27.5.0" - jest-worker "^27.5.0" - micromatch "^4.0.4" - walker "^1.0.7" - optionalDependencies: - fsevents "^2.3.2" - jest-haste-map@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" @@ -6585,11 +6501,6 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== -jest-regex-util@^27.5.0: - version "27.5.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.0.tgz#26c26cf15a73edba13cb8930e261443d25ed8608" - integrity sha512-e9LqSd6HsDsqd7KS3rNyYwmQAaG9jq4U3LbnwVxN/y3nNlDzm2OFs596uo9zrUY+AV1opXq6ome78tRDUCRWfA== - jest-regex-util@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" @@ -6675,14 +6586,6 @@ jest-runtime@^27.5.1: slash "^3.0.0" strip-bom "^4.0.0" -jest-serializer@^27.5.0: - version "27.5.0" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.0.tgz#439a110df27f97a40c114a429b708c2ada15a81f" - integrity sha512-aSDFqQlVXtBH+Zb5dl9mCvTSFkabixk/9P9cpngL4yJKpmEi9USxfDhONFMzJrtftPvZw3PcltUVmtFZTB93rg== - dependencies: - "@types/node" "*" - graceful-fs "^4.2.9" - jest-serializer@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" @@ -6719,18 +6622,6 @@ jest-snapshot@^27.5.1: pretty-format "^27.5.1" semver "^7.3.2" -jest-util@^27.5.0: - version "27.5.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.0.tgz#0b9540d91b0de65d288f235fa9899e6eeeab8d35" - integrity sha512-FUUqOx0gAzJy3ytatT1Ss372M1kmhczn8x7aE0++11oPGW1FyD/5NjYBI8w1KOXFm6IVjtaZm2szfJJL+CHs0g== - dependencies: - "@jest/types" "^27.5.0" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - jest-util@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" @@ -6777,15 +6668,6 @@ jest-worker@^26.5.0: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^27.5.0: - version "27.5.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.0.tgz#99ee77e4d06168107c27328bd7f54e74c3a48d59" - integrity sha512-8OEHiPNOPTfaWnJ2SUHM8fmgeGq37uuGsQBvGKQJl1f+6WIy6g7G3fE2ruI5294bUKUI9FaCWt5hDvO8HSwsSg== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - jest-worker@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" @@ -7690,12 +7572,12 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -npmlog@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.0.tgz#ba9ef39413c3d936ea91553db7be49c34ad0520c" - integrity sha512-03ppFRGlsyUaQFbGC2C8QWJN/C/K7PsfyD9aQdhVKAQIH4sQBc8WASqFBP7O+Ut4d2oo5LoeoboB3cGdBZSp6Q== +npmlog@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.1.tgz#06f1344a174c06e8de9c6c70834cfba2964bba17" + integrity sha512-BTHDvY6nrRHuRfyjt1MAufLxYdVXZfd099H4+i1f0lPywNQyI4foeNXJRObB/uy+TYqUW0vAD9gbdSOXPst7Eg== dependencies: - are-we-there-yet "^2.0.0" + are-we-there-yet "^3.0.0" console-control-strings "^1.1.0" gauge "^4.0.0" set-blocking "^2.0.0" |