diff --git a/.circleci/config.yml b/.circleci/config.yml
index 751ca95b18..a9ad921457 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 5b7f2481ea..2d1df72112 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 2e8f230f31..9cb98dd125 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 b2cbad714d..9235141ad5 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 c6287b5a7a..1b3661561e 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 ae999d9643..c64cead60f 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 9d160dade0..d61f13c3ba 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 aeff2f233b..0d44b4d230 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 e7f56e243b..e0ae71b9f2 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 f0a9354110..e376baab22 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 0000000000..32e5e2f6fd
--- /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 6b1f3fa822..5d32fe66e2 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 0000000000..eefd92b5a8
--- /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 0000000000..7830c55247
--- /dev/null
+++ b/app/controllers/disputes/base_controller.rb
@@ -0,0 +1,23 @@
+# 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!
+ before_action :set_pack
+
+ private
+
+ def set_pack
+ use_pack 'admin'
+ end
+
+ 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 0000000000..d41c5c727e
--- /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 40b2a52892..2f08538ca6 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 f3aa4be4f2..47eeeaac3a 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 0000000000..d16e3dd121
--- /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/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js
index 78f691c980..5de9fe107f 100644
--- a/app/javascript/flavours/glitch/features/emoji_picker/index.js
+++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js
@@ -250,7 +250,7 @@ class EmojiPickerMenu extends React.PureComponent {
state = {
modifierOpen: false,
- placement: null,
+ readyToFocus: false,
};
handleDocumentClick = e => {
@@ -262,6 +262,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 () {
@@ -361,7 +371,7 @@ class EmojiPickerMenu extends React.PureComponent {
showSkinTones={false}
backgroundImageFn={backgroundImageFn}
notFound={notFoundFn}
- autoFocus
+ autoFocus={this.state.readyToFocus}
emojiTooltip
native={useSystemEmojiFont}
/>
@@ -396,6 +406,7 @@ class EmojiPickerDropdown extends React.PureComponent {
state = {
active: false,
loading: false,
+ placement: null,
};
setRef = (c) => {
diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js
index a92f3d5a84..84ec9fce74 100644
--- a/app/javascript/flavours/glitch/packs/public.js
+++ b/app/javascript/flavours/glitch/packs/public.js
@@ -147,13 +147,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/flavours/glitch/packs/settings.js b/app/javascript/flavours/glitch/packs/settings.js
index 9c4d119c1e..0a53e1c25a 100644
--- a/app/javascript/flavours/glitch/packs/settings.js
+++ b/app/javascript/flavours/glitch/packs/settings.js
@@ -7,13 +7,7 @@ function main() {
const { delegate } = require('@rails/ujs');
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');
});
}
diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index 66ce92ce2c..33e115c1a0 100644
--- a/app/javascript/flavours/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/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/flavours/glitch/styles/footer.scss b/app/javascript/flavours/glitch/styles/footer.scss
index 00d2908832..073ebda7e4 100644
--- a/app/javascript/flavours/glitch/styles/footer.scss
+++ b/app/javascript/flavours/glitch/styles/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/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
index 4a87714e64..f433e4de9f 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 7ebe8b4d0c..be467a8e25 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 66ce92ce2c..33e115c1a0 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 00d2908832..073ebda7e4 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 1f93192905..12fad8da40 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 0713aa471e..7f2bc42d3e 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 e07ebfffed..c685d7b6fd 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 b23bd1296f..a9d00c000d 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 5221a48928..583c948b0e 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 e41fdf0031..8f6663e7c1 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 dcb1741227..9da1522dd1 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 fc0d988fdc..05d01942df 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 12136223be..0f2f712a25 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 0000000000..b163d2e568
--- /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 ce5bb5f461..4fba612a65 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 0000000000..1f32cfa8b2
--- /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 ee20e293e8..a21e96ae57 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 0000000000..65707dfa7c
--- /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 0000000000..a25187172a
--- /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 0000000000..1397c50f5f
--- /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 0000000000..f76bf8943e
--- /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 432fb79a6e..0000000000
--- 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 8c9c9679ce..ef23c3b776 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 f3853d629e..9a1f07a066 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'
+
+ %p= t 'admin.reports.notes_description_html'
+
+ .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|
- = render 'shared/error_messages', object: @account_moderation_note
-
- = f.input :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6
= 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 2ee13b9e29..66e0c02514 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 0000000000..02b8777e13
--- /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 0000000000..42e9c4b1d7
--- /dev/null
+++ b/app/views/admin/disputes/appeals/index.html.haml
@@ -0,0 +1,19 @@
+- content_for :page_title do
+ = t('admin.disputes.appeals.title')
+
+.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 428b6cf59c..f9d57c2ae3 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 018a0c54ab..abcbec9499 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 0000000000..db4529eb7d
--- /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 0000000000..40e7e12968
--- /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 47112dae07..3546510b21 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 0000000000..3dcb19016e
--- /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 61198171d4..1a789cef84 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 d7cc1ed5d1..223e5d7407 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 cd5ed52af4..1922f53ce3 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 0000000000..962cab2e2c
--- /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 0000000000..290fa24c36
--- /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 0000000000..75cd9d023b
--- /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 0000000000..f47a768181
--- /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 bda1fef6cf..b308e18f7b 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 4245b71924..6ffe12ae06 100644
--- a/config/brakeman.ignore
+++ b/config/brakeman.ignore
@@ -1,25 +1,5 @@
{
"ignored_warnings": [
- {
- "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,
@@ -27,7 +7,7 @@
"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,
@@ -140,6 +120,36 @@
"confidence": "Medium",
"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,
@@ -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 1809f123ed..cf4c2cc37e 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 one strike.
+ other: This account has %{count} strikes.
+ zero: This account is in good standing.
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: "%{count} pending appeal"
+ other: "%{count} pending appeals"
pending_reports_html:
- one: "1 pending report"
+ one: "%{count} pending report"
other: "%{count} pending reports"
pending_tags_html:
- one: "1 pending hashtag"
+ one: "%{count} pending hashtag"
other: "%{count} pending hashtags"
pending_users_html:
- one: "1 pending user"
+ one: "%{count} pending user"
other: "%{count} 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. Keep the recovery codes safe. 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 d6376782d4..03eefd0d5a 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 a5590d2ea6..a85670500a 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 0eb7f1b0f1..a138fcbcc8 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 7d192f3691..d0946a668e 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 0000000000..afb3efbd53
--- /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 0000000000..a082da774c
--- /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 252373a7ce..e27e04b71d 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 4f876816fc..77328c4c1a 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 0000000000..6a06f94069
--- /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 0000000000..faa571fc9e
--- /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 0000000000..157f9ec3c7
--- /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 db161d4464..72fe835d9a 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 0000000000..339363822d
--- /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 b93fcbe665..41806b2582 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 75ffbbf40f..9c0372b47f 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 69b9b971ee..8de7d86696 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 0000000000..14062dc4f4
--- /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 406438c220..1645ab59e0 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 2dbb546c01..3fdc9615ef 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.|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 3d6b779cec..ca5f384be1 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"