Browse Source

Merge branch 'main' into glitch-soc/merge-upstream

Conflicts:
- `db/schema.rb`:
  Conflict due to glitch-soc adding the `content_type` column on status edits
  and thus having a different schema version number.
  Solved by taking upstream's schema version number, as it is higher than
  glitch-soc's.
glitch-soc
Claire 3 months ago
parent
commit
f224237862
  1. 2
      .circleci/config.yml
  2. 6
      .github/workflows/build-image.yml
  3. 2
      .github/workflows/check-i18n.yml
  4. 2
      Aptfile
  5. 4
      Dockerfile
  6. 8
      Gemfile
  7. 150
      Gemfile.lock
  8. 2
      Vagrantfile
  9. 4
      app/controllers/admin/accounts_controller.rb
  10. 1
      app/controllers/admin/dashboard_controller.rb
  11. 40
      app/controllers/admin/disputes/appeals_controller.rb
  12. 11
      app/controllers/auth/registrations_controller.rb
  13. 26
      app/controllers/disputes/appeals_controller.rb
  14. 18
      app/controllers/disputes/base_controller.rb
  15. 17
      app/controllers/disputes/strikes_controller.rb
  16. 4
      app/helpers/admin/account_moderation_notes_helper.rb
  17. 2
      app/helpers/admin/action_logs_helper.rb
  18. 20
      app/helpers/admin/trends/statuses_helper.rb
  19. 15
      app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
  20. 8
      app/javascript/packs/public.js
  21. 93
      app/javascript/styles/mastodon/admin.scss
  22. 17
      app/javascript/styles/mastodon/footer.scss
  23. 1
      app/lib/activitypub/activity/announce.rb
  24. 2
      app/lib/feed_manager.rb
  25. 37
      app/lib/search_query_transformer.rb
  26. 10
      app/mailers/admin_mailer.rb
  27. 20
      app/mailers/user_mailer.rb
  28. 4
      app/models/account.rb
  29. 4
      app/models/account_filter.rb
  30. 8
      app/models/account_warning.rb
  31. 2
      app/models/admin/action_log_filter.rb
  32. 49
      app/models/admin/appeal_filter.rb
  33. 2
      app/models/admin/status_filter.rb
  34. 60
      app/models/appeal.rb
  35. 6
      app/models/user.rb
  36. 17
      app/policies/account_warning_policy.rb
  37. 13
      app/policies/appeal_policy.rb
  38. 28
      app/services/appeal_service.rb
  39. 74
      app/services/approve_appeal_service.rb
  40. 7
      app/views/admin/account_moderation_notes/_account_moderation_note.html.haml
  41. 30
      app/views/admin/account_warnings/_account_warning.html.haml
  42. 21
      app/views/admin/accounts/show.html.haml
  43. 7
      app/views/admin/dashboard/index.html.haml
  44. 21
      app/views/admin/disputes/appeals/_appeal.html.haml
  45. 22
      app/views/admin/disputes/appeals/index.html.haml
  46. 2
      app/views/admin/report_notes/_report_note.html.haml
  47. 2
      app/views/admin/reports/show.html.haml
  48. 9
      app/views/admin_mailer/new_appeal.text.erb
  49. 20
      app/views/auth/registrations/_account_warning.html.haml
  50. 31
      app/views/auth/registrations/_status.html.haml
  51. 127
      app/views/disputes/strikes/show.html.haml
  52. 4
      app/views/layouts/public.html.haml
  53. 1
      app/views/settings/preferences/notifications/show.html.haml
  54. 2
      app/views/statuses/_detailed_status.html.haml
  55. 59
      app/views/user_mailer/appeal_approved.html.haml
  56. 7
      app/views/user_mailer/appeal_approved.text.erb
  57. 59
      app/views/user_mailer/appeal_rejected.html.haml
  58. 7
      app/views/user_mailer/appeal_rejected.text.erb
  59. 6
      app/views/user_mailer/warning.html.haml
  60. 64
      config/brakeman.ignore
  61. 87
      config/locales/en.yml
  62. 9
      config/locales/simple_form.en.yml
  63. 4
      config/navigation.rb
  64. 15
      config/routes.rb
  65. 1
      config/settings.yml
  66. 14
      db/migrate/20220124141035_create_appeals.rb
  67. 5
      db/migrate/20220210153119_add_overruled_at_to_account_warnings.rb
  68. 23
      db/schema.rb
  69. 12
      package.json
  70. 53
      spec/controllers/admin/disputes/appeals_controller_spec.rb
  71. 27
      spec/controllers/disputes/appeals_controller_spec.rb
  72. 30
      spec/controllers/disputes/strikes_controller_spec.rb
  73. 7
      spec/fabricators/account_warning_fabricator.rb
  74. 5
      spec/fabricators/appeal_fabricator.rb
  75. 32
      spec/lib/activitypub/activity/announce_spec.rb
  76. 5
      spec/mailers/previews/admin_mailer_preview.rb
  77. 5
      spec/mailers/previews/user_mailer_preview.rb
  78. 5
      spec/models/appeal_spec.rb
  79. 13
      spec/models/user_spec.rb
  80. 15
      streaming/index.js
  81. 178
      yarn.lock

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

6
.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

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

2
Aptfile

@ -4,10 +4,8 @@ libicu-dev
libidn11
libidn11-dev
libpq-dev
libprotobuf-dev
libxdamage1
libxfixes3
protobuf-compiler
zlib1g-dev
libcairo2
libcroco3

4
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 && \

8
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'

150
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)

2
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

4
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

1
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

40
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

11
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

26
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

18
app/controllers/disputes/base_controller.rb

@ -0,0 +1,18 @@
# frozen_string_literal: true
class Disputes::BaseController < ApplicationController
include Authorization
layout 'admin'
skip_before_action :require_functional!
before_action :set_body_classes
before_action :authenticate_user!
private
def set_body_classes
@body_classes = 'admin'
end
end

17
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

4
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'),

2
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

20
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

15
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) => {

8
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

93
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;
}
}
}
}
}

17
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%);

1
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)

2
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 = [

37
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]}:")

10
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

20
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

4
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

4
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'

8
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

2
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,

49
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

2
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

60
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

6
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

17
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

13
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

28
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

74
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

7
app/views/admin/account_moderation_notes/_account_moderation_note.html.haml

@ -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)

30
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')

21
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)