From 35dff48edf19a344cbf192b510033ba6785c367b Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 2 Mar 2023 09:30:40 -0500 Subject: [PATCH 01/23] Add spec coverage for Admin::Trends::StatusesHelper (#23898) --- .../admin/trends/statuses_helper_spec.rb | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 spec/helpers/admin/trends/statuses_helper_spec.rb diff --git a/spec/helpers/admin/trends/statuses_helper_spec.rb b/spec/helpers/admin/trends/statuses_helper_spec.rb new file mode 100644 index 0000000000..92caae6909 --- /dev/null +++ b/spec/helpers/admin/trends/statuses_helper_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::Trends::StatusesHelper do + describe '.one_line_preview' do + before do + allow(helper).to receive(:current_user).and_return(Fabricate.build(:user)) + end + + context 'with a local status' do + let(:status) { Fabricate.build(:status, text: 'Test local status') } + + it 'renders a correct preview text' do + result = helper.one_line_preview(status) + + expect(result).to eq 'Test local status' + end + end + + context 'with a remote status' do + let(:status) { Fabricate.build(:status, uri: 'https://sfd.sdf', text: '

Test remote status

text

') } + + it 'renders a correct preview text' do + result = helper.one_line_preview(status) + + expect(result).to eq 'Test remote status' + end + end + + context 'with a status that has empty text' do + let(:status) { Fabricate.build(:status, text: '') } + + it 'renders a correct preview text' do + result = helper.one_line_preview(status) + + expect(result).to eq '' + end + end + + context 'with a status that has emoji' do + before { Fabricate(:custom_emoji, shortcode: 'florpy') } + + let(:status) { Fabricate(:status, text: 'hello there :florpy:') } + + it 'renders a correct preview text' do + result = helper.one_line_preview(status) + + expect(result).to match 'hello there' + expect(result).to match ' Date: Thu, 2 Mar 2023 09:55:37 -0500 Subject: [PATCH 02/23] Update rspec-rails to version 6.0.1 (#23908) --- Gemfile | 2 +- Gemfile.lock | 34 ++++++++++++++-------------- spec/services/search_service_spec.rb | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Gemfile b/Gemfile index 17b0b287de..354f6bd6cf 100644 --- a/Gemfile +++ b/Gemfile @@ -106,7 +106,7 @@ group :development, :test do gem 'i18n-tasks', '~> 1.0', require: false gem 'pry-byebug', '~> 3.10' gem 'pry-rails', '~> 0.3' - gem 'rspec-rails', '~> 5.1' + gem 'rspec-rails', '~> 6.0' gem 'rubocop-performance', require: false gem 'rubocop-rails', require: false gem 'rubocop-rspec', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 965832094e..78774ac9cc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -193,7 +193,7 @@ GEM cocoon (1.2.15) coderay (1.1.3) color_diff (0.1) - concurrent-ruby (1.2.0) + concurrent-ruby (1.2.2) connection_pool (2.3.0) cose (1.3.0) cbor (~> 0.5.9) @@ -585,26 +585,26 @@ GEM chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.0) + rspec-core (3.12.1) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.1) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-rails (5.1.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) + rspec-support (~> 3.12.0) + rspec-rails (6.0.1) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.11) + rspec-expectations (~> 3.11) + rspec-mocks (~> 3.11) + rspec-support (~> 3.11) rspec-sidekiq (3.1.0) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) - rspec-support (3.11.1) + rspec-support (3.12.0) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) rubocop (1.45.1) @@ -869,7 +869,7 @@ DEPENDENCIES redis-namespace (~> 1.10) rexml (~> 3.2) rqrcode (~> 2.1) - rspec-rails (~> 5.1) + rspec-rails (~> 6.0) rspec-sidekiq (~> 3.1) rspec_junit_formatter (~> 0.6) rubocop diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 7ec334a56c..1ad0efe0af 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -77,7 +77,7 @@ describe SearchService, type: :service do it 'includes the tag in the results' do query = '#tag' tag = Tag.new - allow(Tag).to receive(:search_for).with('tag', 10, 0, exclude_unreviewed: nil).and_return([tag]) + allow(Tag).to receive(:search_for).with('tag', 10, 0, { exclude_unreviewed: nil }).and_return([tag]) results = subject.call(query, nil, 10) expect(Tag).to have_received(:search_for).with('tag', 10, 0, exclude_unreviewed: nil) From af578e8ce0aabdbe9c0cd3d72d6fa2cc30b7fc66 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 2 Mar 2023 10:21:04 -0500 Subject: [PATCH 03/23] Fix deprecation warning about merging conditions (#23618) --- app/models/account_filter.rb | 24 +++++++++++++++++++++--- spec/models/account_filter_spec.rb | 26 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index d27bb46fcf..1666ea883a 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -17,13 +17,13 @@ class AccountFilter attr_reader :params def initialize(params) - @params = params + @params = params.to_h.symbolize_keys end def results scope = Account.includes(:account_stat, user: [:ips, :invite_request]).without_instance_actor.reorder(nil) - params.each do |key, value| + relevant_params.each do |key, value| next if key.to_s == 'page' scope.merge!(scope_for(key, value)) if value.present? @@ -34,6 +34,16 @@ class AccountFilter private + def relevant_params + params.tap do |args| + args.delete(:origin) if origin_is_remote_and_domain_present? + end + end + + def origin_is_remote_and_domain_present? + params[:origin] == 'remote' && params[:by_domain].present? + end + def scope_for(key, value) case key.to_s when 'origin' @@ -94,7 +104,15 @@ class AccountFilter def order_scope(value) case value.to_s when 'active' - accounts_with_users.left_joins(:account_stat).order(Arel.sql('coalesce(users.current_sign_in_at, account_stats.last_status_at, to_timestamp(0)) desc, accounts.id desc')) + accounts_with_users + .left_joins(:account_stat) + .order( + Arel.sql( + <<~SQL.squish + COALESCE(users.current_sign_in_at, account_stats.last_status_at, to_timestamp(0)) DESC, accounts.id DESC + SQL + ) + ) when 'recent' Account.recent else diff --git a/spec/models/account_filter_spec.rb b/spec/models/account_filter_spec.rb index 853d20a0cb..3032260fef 100644 --- a/spec/models/account_filter_spec.rb +++ b/spec/models/account_filter_spec.rb @@ -18,4 +18,30 @@ describe AccountFilter do expect { filter.results }.to raise_error(/wrong/) end end + + describe 'with origin and by_domain interacting' do + let!(:local_account) { Fabricate(:account, domain: nil) } + let!(:remote_account_one) { Fabricate(:account, domain: 'example.org') } + let(:remote_account_two) { Fabricate(:account, domain: 'other.domain') } + + it 'works with domain first and origin remote' do + filter = described_class.new(by_domain: 'example.org', origin: 'remote') + expect(filter.results).to match_array [remote_account_one] + end + + it 'works with domain last and origin remote' do + filter = described_class.new(origin: 'remote', by_domain: 'example.org') + expect(filter.results).to match_array [remote_account_one] + end + + it 'works with domain first and origin local' do + filter = described_class.new(by_domain: 'example.org', origin: 'local') + expect(filter.results).to match_array [local_account] + end + + it 'works with domain last and origin local' do + filter = described_class.new(origin: 'local', by_domain: 'example.org') + expect(filter.results).to match_array [remote_account_one] + end + end end From 3a6451c867595fd58998ee1706589b15a69d993b Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:19:29 +0100 Subject: [PATCH 04/23] Add support for incoming rich text (#23913) --- app/javascript/styles/application.scss | 1 + app/javascript/styles/mastodon/rich_text.scss | 64 +++++++++++++++++++ lib/sanitize_ext/sanitize_config.rb | 19 ++---- spec/lib/sanitize_config_spec.rb | 28 ++++---- 4 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 app/javascript/styles/mastodon/rich_text.scss diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 81a040108e..1b2969c234 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -23,3 +23,4 @@ @import 'mastodon/dashboard'; @import 'mastodon/rtl'; @import 'mastodon/accessibility'; +@import 'mastodon/rich_text'; diff --git a/app/javascript/styles/mastodon/rich_text.scss b/app/javascript/styles/mastodon/rich_text.scss new file mode 100644 index 0000000000..35901984b4 --- /dev/null +++ b/app/javascript/styles/mastodon/rich_text.scss @@ -0,0 +1,64 @@ +.status__content__text, +.e-content, +.reply-indicator__content { + pre, + blockquote { + margin-bottom: 20px; + white-space: pre-wrap; + unicode-bidi: plaintext; + + &:last-child { + margin-bottom: 0; + } + } + + blockquote { + padding-left: 10px; + border-left: 3px solid $darker-text-color; + color: $darker-text-color; + white-space: normal; + + p:last-child { + margin-bottom: 0; + } + } + + & > ul, + & > ol { + margin-bottom: 20px; + } + + b, + strong { + font-weight: 700; + } + + em, + i { + font-style: italic; + } + + ul, + ol { + margin-left: 2em; + + p { + margin: 0; + } + } + + ul { + list-style-type: disc; + } + + ol { + list-style-type: decimal; + } +} + +.reply-indicator__content { + blockquote { + border-left-color: $inverted-text-color; + color: $inverted-text-color; + } +} diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb index dc39e9c90f..9cc500c36e 100644 --- a/lib/sanitize_ext/sanitize_config.rb +++ b/lib/sanitize_ext/sanitize_config.rb @@ -51,29 +51,22 @@ class Sanitize end UNSUPPORTED_ELEMENTS_TRANSFORMER = lambda do |env| - return unless %w(h1 h2 h3 h4 h5 h6 blockquote pre ul ol li).include?(env[:node_name]) + return unless %w(h1 h2 h3 h4 h5 h6).include?(env[:node_name]) current_node = env[:node] - case env[:node_name] - when 'li' - current_node.traverse do |node| - next unless %w(p ul ol li).include?(node.name) - - node.add_next_sibling('
') if node.next_sibling - node.replace(node.children) unless node.text? - end - else - current_node.name = 'p' - end + current_node.name = 'strong' + current_node.wrap('

') end MASTODON_STRICT ||= freeze_config( - elements: %w(p br span a), + elements: %w(p br span a del pre blockquote code b strong u i em ul ol li), attributes: { 'a' => %w(href rel class), 'span' => %w(class), + 'ol' => %w(start reversed), + 'li' => %w(value), }, add_attributes: { diff --git a/spec/lib/sanitize_config_spec.rb b/spec/lib/sanitize_config_spec.rb index c9543ceb0c..a01122bed0 100644 --- a/spec/lib/sanitize_config_spec.rb +++ b/spec/lib/sanitize_config_spec.rb @@ -6,24 +6,16 @@ describe Sanitize::Config do describe '::MASTODON_STRICT' do subject { Sanitize::Config::MASTODON_STRICT } - it 'converts h1 to p' do - expect(Sanitize.fragment('

Foo

', subject)).to eq '

Foo

' + it 'converts h1 to p strong' do + expect(Sanitize.fragment('

Foo

', subject)).to eq '

Foo

' end - it 'converts ul to p' do - expect(Sanitize.fragment('

Check out:

  • Foo
  • Bar
', subject)).to eq '

Check out:

Foo
Bar

' + it 'keeps ul' do + expect(Sanitize.fragment('

Check out:

  • Foo
  • Bar
', subject)).to eq '

Check out:

  • Foo
  • Bar
' end - it 'converts p inside ul' do - expect(Sanitize.fragment('
  • Foo

    Bar

  • Baz
', subject)).to eq '

Foo
Bar
Baz

' - end - - it 'converts ul inside ul' do - expect(Sanitize.fragment('
  • Foo
    • Bar
    • Baz
', subject)).to eq '

Foo
Bar
Baz

' - end - - it 'keep links in lists' do - expect(Sanitize.fragment('

Check out:

', subject)).to eq '

Check out:

joinmastodon.org
Bar

' + it 'keeps start and reversed attributes of ol' do + expect(Sanitize.fragment('

Check out:

  1. Foo
  2. Bar
', subject)).to eq '

Check out:

  1. Foo
  2. Bar
' end it 'removes a without href' do @@ -45,5 +37,13 @@ describe Sanitize::Config do it 'keeps a with href' do expect(Sanitize.fragment('Test', subject)).to eq 'Test' end + + it 'removes a with unparsable href' do + expect(Sanitize.fragment('Test', subject)).to eq 'Test' + end + + it 'keeps a with supported scheme and no host' do + expect(Sanitize.fragment('Test', subject)).to eq 'Test' + end end end From c2a046ded1d47e2504df05568e34bc6a2a6dc810 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:25:15 +0100 Subject: [PATCH 05/23] =?UTF-8?q?Fix=20=E2=80=9CRemove=20all=20followers?= =?UTF-8?q?=20from=20the=20selected=20domains=E2=80=9D=20being=20more=20de?= =?UTF-8?q?structive=20than=20it=20claims=20(#23805)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/relationships_controller.rb | 4 ++-- app/models/form/account_batch.rb | 10 ++++---- .../remove_domains_from_followers_service.rb | 23 +++++++++++++++++++ app/views/relationships/show.html.haml | 2 +- .../relationships_controller_spec.rb | 11 ++++++++- 5 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 app/services/remove_domains_from_followers_service.rb diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index 96cce55e9e..baa34da22a 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -60,8 +60,8 @@ class RelationshipsController < ApplicationController 'unfollow' elsif params[:remove_from_followers] 'remove_from_followers' - elsif params[:block_domains] - 'block_domains' + elsif params[:block_domains] || params[:remove_domains_from_followers] + 'remove_domains_from_followers' end end diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index 473622edf4..5a7fc7ed1b 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -17,8 +17,8 @@ class Form::AccountBatch unfollow! when 'remove_from_followers' remove_from_followers! - when 'block_domains' - block_domains! + when 'remove_domains_from_followers' + remove_domains_from_followers! when 'approve' approve! when 'reject' @@ -50,10 +50,8 @@ class Form::AccountBatch RemoveFromFollowersService.new.call(current_account, account_ids) end - def block_domains! - AfterAccountDomainBlockWorker.push_bulk(account_domains) do |domain| - [current_account.id, domain] - end + def remove_domains_from_followers! + RemoveDomainsFromFollowersService.new.call(current_account, account_domains) end def account_domains diff --git a/app/services/remove_domains_from_followers_service.rb b/app/services/remove_domains_from_followers_service.rb new file mode 100644 index 0000000000..d76763409d --- /dev/null +++ b/app/services/remove_domains_from_followers_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class RemoveDomainsFromFollowersService < BaseService + include Payloadable + + def call(source_account, target_domains) + source_account.passive_relationships.where(account_id: Account.where(domain: target_domains)).find_each do |follow| + follow.destroy + + create_notification(follow) if source_account.local? && !follow.account.local? && follow.account.activitypub? + end + end + + private + + def create_notification(follow) + ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.target_account_id, follow.account.inbox_url) + end + + def build_json(follow) + Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) + end +end diff --git a/app/views/relationships/show.html.haml b/app/views/relationships/show.html.haml index 2899cd5140..f08e9c1df8 100644 --- a/app/views/relationships/show.html.haml +++ b/app/views/relationships/show.html.haml @@ -48,7 +48,7 @@ = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('relationships.confirm_remove_selected_followers') } unless following_relationship? - = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship? + = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :remove_domains_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship? .batch-table__body - if @accounts.empty? = nothing_here 'nothing-here--under-tabs' diff --git a/spec/controllers/relationships_controller_spec.rb b/spec/controllers/relationships_controller_spec.rb index 39f455e038..53a5daa517 100644 --- a/spec/controllers/relationships_controller_spec.rb +++ b/spec/controllers/relationships_controller_spec.rb @@ -58,7 +58,7 @@ describe RelationshipsController do end context 'when select parameter is provided' do - subject { patch :update, params: { form_account_batch: { account_ids: [poopfeast.id] }, block_domains: '' } } + subject { patch :update, params: { form_account_batch: { account_ids: [poopfeast.id] }, remove_domains_from_followers: '' } } it 'soft-blocks followers from selected domains' do poopfeast.follow!(user.account) @@ -69,6 +69,15 @@ describe RelationshipsController do expect(poopfeast.following?(user.account)).to be false end + it 'does not unfollow users from selected domains' do + user.account.follow!(poopfeast) + + sign_in user, scope: :user + subject + + expect(user.account.following?(poopfeast)).to be true + end + include_examples 'authenticate user' include_examples 'redirects back to followers page' end From b55fc883b6181fb8080fbafd53bc7a9f1896b295 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:25:36 +0100 Subject: [PATCH 06/23] =?UTF-8?q?Fix=20duplicate=20=E2=80=9CPublish?= =?UTF-8?q?=E2=80=9D=20button=20on=20mobile=20(#23804)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/mastodon/features/ui/components/header.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/features/ui/components/header.jsx b/app/javascript/mastodon/features/ui/components/header.jsx index 1384bebda0..92adc47a9c 100644 --- a/app/javascript/mastodon/features/ui/components/header.jsx +++ b/app/javascript/mastodon/features/ui/components/header.jsx @@ -22,8 +22,8 @@ const mapDispatchToProps = (dispatch) => ({ }, }); -export default @connect(null, mapDispatchToProps) -@withRouter +export default @withRouter +@connect(null, mapDispatchToProps) class Header extends React.PureComponent { static contextTypes = { From f8bb4d0d6b1050de481187e9f034b8bbb649d931 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:36:18 +0100 Subject: [PATCH 07/23] Fix server error when failing to follow back followers from `/relationships` (#23787) --- app/controllers/relationships_controller.rb | 2 ++ app/models/form/account_batch.rb | 6 ++++++ config/locales/en.yml | 1 + 3 files changed, 9 insertions(+) diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index baa34da22a..de5dc58792 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -19,6 +19,8 @@ class RelationshipsController < ApplicationController @form.save rescue ActionController::ParameterMissing # Do nothing + rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound + flash[:alert] = I18n.t('relationships.follow_failure') if action_from_button == 'follow' ensure redirect_to relationships_path(filter_params) end diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index 5a7fc7ed1b..6a05f8163a 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -35,9 +35,15 @@ class Form::AccountBatch private def follow! + error = nil + accounts.each do |target_account| FollowService.new.call(current_account, target_account) + rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound => e + error ||= e end + + raise error if error.present? end def unfollow! diff --git a/config/locales/en.yml b/config/locales/en.yml index 9f8ba7ce78..d142962b5a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1408,6 +1408,7 @@ en: confirm_remove_selected_followers: Are you sure you want to remove selected followers? confirm_remove_selected_follows: Are you sure you want to remove selected follows? dormant: Dormant + follow_failure: Could not follow some of the selected accounts. follow_selected_followers: Follow selected followers followers: Followers following: Following From 02c6bad3ca7b59a8be6403fbbddd7d46346532dc Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:37:22 +0100 Subject: [PATCH 08/23] Change unintended SMTP read timeout from 5 seconds to 20 seconds (#23750) --- config/environments/production.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/environments/production.rb b/config/environments/production.rb index 99c9bb40c5..345a255a74 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -128,6 +128,7 @@ Rails.application.configure do enable_starttls_auto: enable_starttls_auto, tls: ENV['SMTP_TLS'].presence && ENV['SMTP_TLS'] == 'true', ssl: ENV['SMTP_SSL'].presence && ENV['SMTP_SSL'] == 'true', + read_timeout: 20, } config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym From a1347f456e47e085ef1031b607cabd0683c496c2 Mon Sep 17 00:00:00 2001 From: Terry Garcia <10190993+TerryGarcia@users.noreply.github.com> Date: Fri, 3 Mar 2023 13:37:49 -0600 Subject: [PATCH 09/23] Switched bookmark and favourites around (#23701) --- .../mastodon/features/ui/components/navigation_panel.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx index 9a9309be05..755b19349d 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx @@ -82,8 +82,8 @@ class NavigationPanel extends React.Component { {signedIn && ( - + From f94aa70b814159964de660ac33bc797a1e01af30 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:44:02 +0100 Subject: [PATCH 10/23] Fix error when displaying post history of a trendable post in the admin interface (#23574) --- app/views/admin/statuses/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/statuses/show.html.haml b/app/views/admin/statuses/show.html.haml index 62b49de8c8..4631e97f16 100644 --- a/app/views/admin/statuses/show.html.haml +++ b/app/views/admin/statuses/show.html.haml @@ -34,7 +34,7 @@ %td - if @status.trend.allowed? %abbr{ title: t('admin.trends.tags.current_score', score: @status.trend.score) }= t('admin.trends.tags.trending_rank', rank: @status.trend.rank) - - elsif @status.trend.requires_review? + - elsif @status.requires_review? = t('admin.trends.pending_review') - else = t('admin.trends.not_allowed_to_trend') From d6679d175181e425f52e5a6b062bc2c5545ce0cc Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:44:46 +0100 Subject: [PATCH 11/23] Add mail headers to avoid auto-replies (#23597) --- app/mailers/application_mailer.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 73b623576c..35f0b5fee1 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -7,9 +7,17 @@ class ApplicationMailer < ActionMailer::Base helper :instance helper :formatting + after_action :set_autoreply_headers! + protected def locale_for_account(account, &block) I18n.with_locale(account.user_locale || I18n.default_locale, &block) end + + def set_autoreply_headers! + headers['Precedence'] = 'list' + headers['X-Auto-Response-Suppress'] = 'All' + headers['Auto-Submitted'] = 'auto-generated' + end end From 8784498ebfc5508034f447d92645551d9d2c5907 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:45:12 +0100 Subject: [PATCH 12/23] Fix tootctl accounts migrate error due to typo (#23567) --- lib/mastodon/accounts_cli.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb index db379eb853..98855cbd06 100644 --- a/lib/mastodon/accounts_cli.rb +++ b/lib/mastodon/accounts_cli.rb @@ -627,7 +627,7 @@ module Mastodon exit(1) end - unless options[:force] || migration.target_acount_id == account.moved_to_account_id + unless options[:force] || migration.target_account_id == account.moved_to_account_id say('The specified account is not redirecting to its last migration target. Use --force if you want to replay the migration anyway', :red) exit(1) end From 6b16b77ab0f347e688f009c5a04961142b8dd203 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:45:55 +0100 Subject: [PATCH 13/23] Fix external authentication not running onboarding code for new users (#23458) --- app/models/concerns/omniauthable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb index b0aa5be6f1..41eae215b4 100644 --- a/app/models/concerns/omniauthable.rb +++ b/app/models/concerns/omniauthable.rb @@ -61,7 +61,7 @@ module Omniauthable user.account.avatar_remote_url = nil end - user.skip_confirmation! if email_is_verified + user.confirm! if email_is_verified user.save! user end From 3f52e717fa22e89a0eaf5f12f0fb9d2b121c7944 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:50:46 +0100 Subject: [PATCH 14/23] Add tests for moderation actions without custom text (#23184) --- .../admin/reports/actions_controller_spec.rb | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/spec/controllers/admin/reports/actions_controller_spec.rb b/spec/controllers/admin/reports/actions_controller_spec.rb index 3e42e4cb19..4c2624a408 100644 --- a/spec/controllers/admin/reports/actions_controller_spec.rb +++ b/spec/controllers/admin/reports/actions_controller_spec.rb @@ -57,6 +57,9 @@ describe Admin::Reports::ActionsController do let!(:media) { Fabricate(:media_attachment, account: target_account, status: statuses[0]) } let(:report) { Fabricate(:report, target_account: target_account, status_ids: statuses.map(&:id)) } let(:text) { 'hello' } + let(:common_params) do + { report_id: report.id, text: text } + end shared_examples 'common behavior' do it 'closes the report' do @@ -72,6 +75,26 @@ describe Admin::Reports::ActionsController do subject expect(response).to redirect_to(admin_reports_path) end + + context 'when text is unset' do + let(:common_params) do + { report_id: report.id } + end + + it 'closes the report' do + expect { subject }.to change { report.reload.action_taken? }.from(false).to(true) + end + + it 'creates a strike with the expected text' do + expect { subject }.to change { report.target_account.strikes.count }.by(1) + expect(report.target_account.strikes.last.text).to eq '' + end + + it 'redirects' do + subject + expect(response).to redirect_to(admin_reports_path) + end + end end shared_examples 'all action types' do @@ -124,13 +147,13 @@ describe Admin::Reports::ActionsController do end context 'action as submit button' do - subject { post :create, params: { report_id: report.id, text: text, action => '' } } + subject { post :create, params: common_params.merge({ action => '' }) } it_behaves_like 'all action types' end context 'action as submit button' do - subject { post :create, params: { report_id: report.id, text: text, moderation_action: action } } + subject { post :create, params: common_params.merge({ moderation_action: action }) } it_behaves_like 'all action types' end From de137e6bb0d69354b4be5d4a8d845f8bd90551ff Mon Sep 17 00:00:00 2001 From: Jamie Hoyle Date: Fri, 3 Mar 2023 19:53:37 +0000 Subject: [PATCH 15/23] Added support for specifying S3 storage classes in environment (#22480) --- config/initializers/paperclip.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index a2285427c8..9282c941da 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -90,6 +90,12 @@ if ENV['S3_ENABLED'] == 'true' ) end + if ENV.has_key?('S3_STORAGE_CLASS') + Paperclip::Attachment.default_options[:s3_headers].merge!( + 'X-Amz-Storage-Class' => ENV['S3_STORAGE_CLASS'] + ) + end + # Some S3-compatible providers might not actually be compatible with some APIs # used by kt-paperclip, see https://github.com/mastodon/mastodon/issues/16822 if ENV['S3_FORCE_SINGLE_REQUEST'] == 'true' From 0872f3e3d743a16533ac4fad4cd83b103047808c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C5=ABns=20Usovs?= Date: Fri, 3 Mar 2023 21:01:18 +0100 Subject: [PATCH 16/23] Allow streaming to connect to postgress with self-signed certs (#21431) --- package.json | 1 + streaming/index.js | 38 +------------------------------------- yarn.lock | 5 +++++ 3 files changed, 7 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 1ab297d041..15bf30925c 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "object.values": "^1.1.6", "path-complete-extname": "^1.0.0", "pg": "^8.5.0", + "pg-connection-string": "^2.5.0", "postcss": "^8.4.21", "postcss-loader": "^3.0.0", "promise.prototype.finally": "^3.1.4", diff --git a/streaming/index.js b/streaming/index.js index 32e3babaa4..ba7cfea191 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -7,6 +7,7 @@ const express = require('express'); const http = require('http'); const redis = require('redis'); const pg = require('pg'); +const dbUrlToConfig = require('pg-connection-string').parse; const log = require('npmlog'); const url = require('url'); const uuid = require('uuid'); @@ -23,43 +24,6 @@ dotenv.config({ log.level = process.env.LOG_LEVEL || 'verbose'; -/** - * @param {string} dbUrl - * @return {Object.} - */ -const dbUrlToConfig = (dbUrl) => { - if (!dbUrl) { - return {}; - } - - const params = url.parse(dbUrl, true); - const config = {}; - - if (params.auth) { - [config.user, config.password] = params.auth.split(':'); - } - - if (params.hostname) { - config.host = params.hostname; - } - - if (params.port) { - config.port = params.port; - } - - if (params.pathname) { - config.database = params.pathname.split('/')[1]; - } - - const ssl = params.query && params.query.ssl; - - if (ssl && ssl === 'true' || ssl === '1') { - config.ssl = true; - } - - return config; -}; - /** * @param {Object.} defaultConfig * @param {string} redisUrl diff --git a/yarn.lock b/yarn.lock index a856cf91dd..68cef79651 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8322,6 +8322,11 @@ pg-connection-string@^2.4.0: resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.4.0.tgz#c979922eb47832999a204da5dbe1ebf2341b6a10" integrity sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ== +pg-connection-string@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" + integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== + pg-int8@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" From 5a8c651e8f0252c7135042e79396f782361302d9 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Fri, 3 Mar 2023 21:06:31 +0100 Subject: [PATCH 17/23] Only offer translation for supported languages (#23879) --- .rubocop.yml | 4 + .../mastodon/components/status_content.jsx | 4 +- app/javascript/mastodon/initial_state.js | 2 - app/lib/translation_service.rb | 4 + app/lib/translation_service/deepl.rb | 46 +++++--- .../translation_service/libre_translate.rb | 38 +++++-- app/models/status.rb | 10 ++ app/serializers/initial_state_serializer.rb | 1 - app/serializers/rest/status_serializer.rb | 6 +- app/services/translate_status_service.rb | 2 +- spec/lib/translation_service/deepl_spec.rb | 100 ++++++++++++++++++ .../libre_translate_spec.rb | 71 +++++++++++++ spec/models/status_spec.rb | 79 ++++++++++++++ 13 files changed, 336 insertions(+), 31 deletions(-) create mode 100644 spec/lib/translation_service/deepl_spec.rb create mode 100644 spec/lib/translation_service/libre_translate_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 27d778edfb..0a41c54b90 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -97,6 +97,10 @@ Rails/Exit: - 'lib/mastodon/cli_helper.rb' - 'lib/cli.rb' +RSpec/FilePath: + CustomTransform: + DeepL: deepl + RSpec/NotToNot: EnforcedStyle: to_not diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index a1c38171f2..f9c9fe0791 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -6,7 +6,7 @@ import { Link } from 'react-router-dom'; import classnames from 'classnames'; import PollContainer from 'mastodon/containers/poll_container'; import Icon from 'mastodon/components/icon'; -import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'mastodon/initial_state'; +import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state'; const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) @@ -220,7 +220,7 @@ class StatusContent extends React.PureComponent { const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const renderReadMore = this.props.onClick && status.get('collapsed'); - const renderTranslate = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('language'); + const renderTranslate = this.props.onTranslate && status.get('translatable'); const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') }; const spoilerContent = { __html: status.get('spoilerHtml') }; diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index d04c4a42d2..919e0fc282 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -80,7 +80,6 @@ * @property {boolean} use_blurhash * @property {boolean=} use_pending_items * @property {string} version - * @property {boolean} translation_enabled */ /** @@ -132,7 +131,6 @@ export const unfollowModal = getMeta('unfollow_modal'); export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); export const version = getMeta('version'); -export const translationEnabled = getMeta('translation_enabled'); export const languages = initialState?.languages; export const statusPageUrl = getMeta('status_page_url'); diff --git a/app/lib/translation_service.rb b/app/lib/translation_service.rb index 285f309393..5ff93674a4 100644 --- a/app/lib/translation_service.rb +++ b/app/lib/translation_service.rb @@ -21,6 +21,10 @@ class TranslationService ENV['DEEPL_API_KEY'].present? || ENV['LIBRE_TRANSLATE_ENDPOINT'].present? end + def supported?(_source_language, _target_language) + false + end + def translate(_text, _source_language, _target_language) raise NotImplementedError end diff --git a/app/lib/translation_service/deepl.rb b/app/lib/translation_service/deepl.rb index 151d33d909..deff95a1db 100644 --- a/app/lib/translation_service/deepl.rb +++ b/app/lib/translation_service/deepl.rb @@ -11,33 +11,53 @@ class TranslationService::DeepL < TranslationService end def translate(text, source_language, target_language) - request(text, source_language, target_language).perform do |res| + form = { text: text, source_lang: source_language&.upcase, target_lang: target_language, tag_handling: 'html' } + request(:post, '/v2/translate', form: form) do |res| + transform_response(res.body_with_limit) + end + end + + def supported?(source_language, target_language) + source_language.in?(languages('source')) && target_language.in?(languages('target')) + end + + private + + def languages(type) + Rails.cache.fetch("translation_service/deepl/languages/#{type}", expires_in: 7.days, race_condition_ttl: 1.minute) do + request(:get, "/v2/languages?type=#{type}") do |res| + # In DeepL, EN and PT are deprecated in favor of EN-GB/EN-US and PT-BR/PT-PT, so + # they are supported but not returned by the API. + extra = type == 'source' ? [nil] : %w(en pt) + languages = Oj.load(res.body_with_limit).map { |language| language['language'].downcase } + + languages + extra + end + end + end + + def request(verb, path, **options) + req = Request.new(verb, "#{base_url}#{path}", **options) + req.add_headers(Authorization: "DeepL-Auth-Key #{@api_key}") + req.perform do |res| case res.code when 429 raise TooManyRequestsError when 456 raise QuotaExceededError when 200...300 - transform_response(res.body_with_limit) + yield res else raise UnexpectedResponseError end end end - private - - def request(text, source_language, target_language) - req = Request.new(:post, endpoint_url, form: { text: text, source_lang: source_language&.upcase, target_lang: target_language, tag_handling: 'html' }) - req.add_headers(Authorization: "DeepL-Auth-Key #{@api_key}") - req - end - - def endpoint_url + def base_url if @plan == 'free' - 'https://api-free.deepl.com/v2/translate' + 'https://api-free.deepl.com' else - 'https://api.deepl.com/v2/translate' + 'https://api.deepl.com' end end diff --git a/app/lib/translation_service/libre_translate.rb b/app/lib/translation_service/libre_translate.rb index 4ebe21e454..743e4d77f7 100644 --- a/app/lib/translation_service/libre_translate.rb +++ b/app/lib/translation_service/libre_translate.rb @@ -9,29 +9,45 @@ class TranslationService::LibreTranslate < TranslationService end def translate(text, source_language, target_language) - request(text, source_language, target_language).perform do |res| + body = Oj.dump(q: text, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key) + request(:post, '/translate', body: body) do |res| + transform_response(res.body_with_limit, source_language) + end + end + + def supported?(source_language, target_language) + languages.key?(source_language) && languages[source_language].include?(target_language) + end + + private + + def languages + Rails.cache.fetch('translation_service/libre_translate/languages', expires_in: 7.days, race_condition_ttl: 1.minute) do + request(:get, '/languages') do |res| + languages = Oj.load(res.body_with_limit).to_h { |language| [language['code'], language['targets']] } + languages[nil] = languages.values.flatten.uniq + languages + end + end + end + + def request(verb, path, **options) + req = Request.new(verb, "#{@base_url}#{path}", allow_local: true, **options) + req.add_headers('Content-Type': 'application/json') + req.perform do |res| case res.code when 429 raise TooManyRequestsError when 403 raise QuotaExceededError when 200...300 - transform_response(res.body_with_limit, source_language) + yield res else raise UnexpectedResponseError end end end - private - - def request(text, source_language, target_language) - body = Oj.dump(q: text, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key) - req = Request.new(:post, "#{@base_url}/translate", body: body, allow_local: true) - req.add_headers('Content-Type': 'application/json') - req - end - def transform_response(str, source_language) json = Oj.load(str, mode: :strict) diff --git a/app/models/status.rb b/app/models/status.rb index e7ea191a80..dd7ac2edb1 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -232,6 +232,16 @@ class Status < ApplicationRecord public_visibility? || unlisted_visibility? end + def translatable? + translate_target_locale = I18n.locale.to_s.split(/[_-]/).first + + distributable? && + content.present? && + language != translate_target_locale && + TranslationService.configured? && + TranslationService.configured.supported?(language, translate_target_locale) + end + alias sign? distributable? def with_media? diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 7905444e98..769ba653ed 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -30,7 +30,6 @@ class InitialStateSerializer < ActiveModel::Serializer timeline_preview: Setting.timeline_preview, activity_api_enabled: Setting.activity_api_enabled, single_user_mode: Rails.configuration.x.single_user_mode, - translation_enabled: TranslationService.configured?, trends_as_landing_page: Setting.trends_as_landing_page, status_page_url: Setting.status_page_url, } diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index e0b8f32a68..a422f5b258 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -4,7 +4,7 @@ class REST::StatusSerializer < ActiveModel::Serializer include FormattingHelper attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, - :sensitive, :spoiler_text, :visibility, :language, + :sensitive, :spoiler_text, :visibility, :language, :translatable, :uri, :url, :replies_count, :reblogs_count, :favourites_count, :edited_at @@ -50,6 +50,10 @@ class REST::StatusSerializer < ActiveModel::Serializer object.account.user_shows_application? || (current_user? && current_user.account_id == object.account_id) end + def translatable + current_user? && object.translatable? + end + def visibility # This visibility is masked behind "private" # to avoid API changes because there are no diff --git a/app/services/translate_status_service.rb b/app/services/translate_status_service.rb index 539a0d9db5..92d8b62a05 100644 --- a/app/services/translate_status_service.rb +++ b/app/services/translate_status_service.rb @@ -6,7 +6,7 @@ class TranslateStatusService < BaseService include FormattingHelper def call(status, target_language) - raise Mastodon::NotPermittedError unless status.public_visibility? || status.unlisted_visibility? + raise Mastodon::NotPermittedError unless status.translatable? @status = status @content = status_content_format(@status) diff --git a/spec/lib/translation_service/deepl_spec.rb b/spec/lib/translation_service/deepl_spec.rb new file mode 100644 index 0000000000..aa24731860 --- /dev/null +++ b/spec/lib/translation_service/deepl_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe TranslationService::DeepL do + subject(:service) { described_class.new(plan, 'my-api-key') } + + let(:plan) { 'advanced' } + + before do + stub_request(:get, 'https://api.deepl.com/v2/languages?type=source').to_return( + body: '[{"language":"EN","name":"English"},{"language":"UK","name":"Ukrainian"}]' + ) + stub_request(:get, 'https://api.deepl.com/v2/languages?type=target').to_return( + body: '[{"language":"EN-GB","name":"English (British)"},{"language":"ZH","name":"Chinese"}]' + ) + end + + describe '#supported?' do + it 'supports included languages as source and target languages' do + expect(service.supported?('uk', 'en')).to be true + end + + it 'supports auto-detecting source language' do + expect(service.supported?(nil, 'en')).to be true + end + + it 'supports "en" and "pt" as target languages though not included in language list' do + expect(service.supported?('uk', 'en')).to be true + expect(service.supported?('uk', 'pt')).to be true + end + + it 'does not support non-included language as target language' do + expect(service.supported?('uk', 'nl')).to be false + end + + it 'does not support non-included language as source language' do + expect(service.supported?('da', 'en')).to be false + end + end + + describe '#translate' do + it 'returns translation with specified source language' do + stub_request(:post, 'https://api.deepl.com/v2/translate') + .with(body: 'text=Hasta+la+vista&source_lang=ES&target_lang=en&tag_handling=html') + .to_return(body: '{"translations":[{"detected_source_language":"ES","text":"See you soon"}]}') + + translation = service.translate('Hasta la vista', 'es', 'en') + expect(translation.detected_source_language).to eq 'es' + expect(translation.provider).to eq 'DeepL.com' + expect(translation.text).to eq 'See you soon' + end + + it 'returns translation with auto-detected source language' do + stub_request(:post, 'https://api.deepl.com/v2/translate') + .with(body: 'text=Guten+Tag&source_lang&target_lang=en&tag_handling=html') + .to_return(body: '{"translations":[{"detected_source_language":"DE","text":"Good Morning"}]}') + + translation = service.translate('Guten Tag', nil, 'en') + expect(translation.detected_source_language).to eq 'de' + expect(translation.provider).to eq 'DeepL.com' + expect(translation.text).to eq 'Good Morning' + end + end + + describe '#languages?' do + it 'returns source languages' do + expect(service.send(:languages, 'source')).to eq ['en', 'uk', nil] + end + + it 'returns target languages' do + expect(service.send(:languages, 'target')).to eq %w(en-gb zh en pt) + end + end + + describe '#request' do + before do + stub_request(:any, //) + # rubocop:disable Lint/EmptyBlock + service.send(:request, :get, '/v2/languages') { |res| } + # rubocop:enable Lint/EmptyBlock + end + + it 'uses paid plan base URL' do + expect(a_request(:get, 'https://api.deepl.com/v2/languages')).to have_been_made.once + end + + context 'with free plan' do + let(:plan) { 'free' } + + it 'uses free plan base URL' do + expect(a_request(:get, 'https://api-free.deepl.com/v2/languages')).to have_been_made.once + end + end + + it 'sends API key' do + expect(a_request(:get, 'https://api.deepl.com/v2/languages').with(headers: { Authorization: 'DeepL-Auth-Key my-api-key' })).to have_been_made.once + end + end +end diff --git a/spec/lib/translation_service/libre_translate_spec.rb b/spec/lib/translation_service/libre_translate_spec.rb new file mode 100644 index 0000000000..a6cb01884a --- /dev/null +++ b/spec/lib/translation_service/libre_translate_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe TranslationService::LibreTranslate do + subject(:service) { described_class.new('https://libretranslate.example.com', 'my-api-key') } + + before do + stub_request(:get, 'https://libretranslate.example.com/languages').to_return( + body: '[{"code": "en","name": "English","targets": ["de","es"]},{"code": "da","name": "Danish","targets": ["en","de"]}]' + ) + end + + describe '#supported?' do + it 'supports included language pair' do + expect(service.supported?('en', 'de')).to be true + end + + it 'does not support reversed language pair' do + expect(service.supported?('de', 'en')).to be false + end + + it 'supports auto-detecting source language' do + expect(service.supported?(nil, 'de')).to be true + end + + it 'does not support auto-detecting for unsupported target language' do + expect(service.supported?(nil, 'pt')).to be false + end + end + + describe '#languages' do + subject(:languages) { service.send(:languages) } + + it 'includes supported source languages' do + expect(languages.keys).to eq ['en', 'da', nil] + end + + it 'includes supported target languages for source language' do + expect(languages['en']).to eq %w(de es) + end + + it 'includes supported target languages for auto-detected language' do + expect(languages[nil]).to eq %w(de es en) + end + end + + describe '#translate' do + it 'returns translation with specified source language' do + stub_request(:post, 'https://libretranslate.example.com/translate') + .with(body: '{"q":"Hasta la vista","source":"es","target":"en","format":"html","api_key":"my-api-key"}') + .to_return(body: '{"translatedText": "See you"}') + + translation = service.translate('Hasta la vista', 'es', 'en') + expect(translation.detected_source_language).to eq 'es' + expect(translation.provider).to eq 'LibreTranslate' + expect(translation.text).to eq 'See you' + end + + it 'returns translation with auto-detected source language' do + stub_request(:post, 'https://libretranslate.example.com/translate') + .with(body: '{"q":"Guten Morgen","source":"auto","target":"en","format":"html","api_key":"my-api-key"}') + .to_return(body: '{"detectedLanguage":{"confidence":92,"language":"de"},"translatedText":"Good morning"}') + + translation = service.translate('Guten Morgen', nil, 'en') + expect(translation.detected_source_language).to be_nil + expect(translation.provider).to eq 'LibreTranslate' + expect(translation.text).to eq 'Good morning' + end + end +end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 1e58c6d0d1..1f6cfc7967 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -114,6 +114,85 @@ RSpec.describe Status, type: :model do end end + describe '#translatable?' do + before do + allow(TranslationService).to receive(:configured?).and_return(true) + allow(TranslationService).to receive(:configured).and_return(TranslationService.new) + allow(TranslationService.configured).to receive(:supported?).with('es', 'en').and_return(true) + + subject.language = 'es' + subject.visibility = :public + end + + context 'all conditions are satisfied' do + it 'returns true' do + expect(subject.translatable?).to be true + end + end + + context 'translation service is not configured' do + it 'returns false' do + allow(TranslationService).to receive(:configured?).and_return(false) + allow(TranslationService).to receive(:configured).and_raise(TranslationService::NotConfiguredError) + expect(subject.translatable?).to be false + end + end + + context 'status language is nil' do + it 'returns true' do + subject.language = nil + allow(TranslationService.configured).to receive(:supported?).with(nil, 'en').and_return(true) + expect(subject.translatable?).to be true + end + end + + context 'status language is same as default locale' do + it 'returns false' do + subject.language = I18n.locale + expect(subject.translatable?).to be false + end + end + + context 'status language is unsupported' do + it 'returns false' do + subject.language = 'af' + allow(TranslationService.configured).to receive(:supported?).with('af', 'en').and_return(false) + expect(subject.translatable?).to be false + end + end + + context 'default locale is unsupported' do + it 'returns false' do + allow(TranslationService.configured).to receive(:supported?).with('es', 'af').and_return(false) + I18n.with_locale('af') do + expect(subject.translatable?).to be false + end + end + end + + context 'default locale has region' do + it 'returns true' do + I18n.with_locale('en-GB') do + expect(subject.translatable?).to be true + end + end + end + + context 'status text is blank' do + it 'returns false' do + subject.text = ' ' + expect(subject.translatable?).to be false + end + end + + context 'status visiblity is hidden' do + it 'returns false' do + subject.visibility = 'limited' + expect(subject.translatable?).to be false + end + end + end + describe '#content' do it 'returns the text of the status if it is not a reblog' do expect(subject.content).to eql subject.text From ddde4e0d954fbb1338774d61f1130e4af6924496 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 21:08:22 +0100 Subject: [PATCH 18/23] Change `ActivityPub::DeliveryWorker` retries to be spread out more (#21956) --- app/workers/activitypub/delivery_worker.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index d9153132b3..7c1c14766b 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -10,6 +10,16 @@ class ActivityPub::DeliveryWorker sidekiq_options queue: 'push', retry: 16, dead: false + # Unfortunately, we cannot control Sidekiq's jitter, so add our own + sidekiq_retry_in do |count| + # This is Sidekiq's default delay + delay = (count**4) + 15 + # Our custom jitter, that will be added to Sidekiq's built-in one. + # Sidekiq's built-in jitter is `rand(10) * (count + 1)` + jitter = rand(0.5 * (count**4)) + delay + jitter + end + HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze def perform(json, source_account_id, inbox_url, options = {}) From 050f1669c6fc02d7a917261d16d9264512955bc6 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 21:13:55 +0100 Subject: [PATCH 19/23] Fix original account being unfollowed on migration before the follow request could be sent (#21957) --- app/services/follow_migration_service.rb | 40 +++++++++++++++++++ .../migrated_follow_delivery_worker.rb | 17 ++++++++ app/workers/unfollow_follow_worker.rb | 8 +--- 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 app/services/follow_migration_service.rb create mode 100644 app/workers/activitypub/migrated_follow_delivery_worker.rb diff --git a/app/services/follow_migration_service.rb b/app/services/follow_migration_service.rb new file mode 100644 index 0000000000..cfe9093cbe --- /dev/null +++ b/app/services/follow_migration_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class FollowMigrationService < FollowService + # Follow an account with the same settings as another account, and unfollow the old account once the request is sent + # @param [Account] source_account From which to follow + # @param [Account] target_account Account to follow + # @param [Account] old_target_account Account to unfollow once the follow request has been sent to the new one + # @option [Boolean] bypass_locked Whether to immediately follow the new account even if it is locked + def call(source_account, target_account, old_target_account, bypass_locked: false) + @old_target_account = old_target_account + + follow = source_account.active_relationships.find_by(target_account: old_target_account) + reblogs = follow&.show_reblogs? + notify = follow&.notify? + languages = follow&.languages + + super(source_account, target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true) + end + + private + + def request_follow! + follow_request = @source_account.request_follow!(@target_account, **follow_options.merge(rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit])) + + if @target_account.local? + LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request') + UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true) + elsif @target_account.activitypub? + ActivityPub::MigratedFollowDeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, @old_target_account.id) + end + + follow_request + end + + def direct_follow! + follow = super + UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true) + follow + end +end diff --git a/app/workers/activitypub/migrated_follow_delivery_worker.rb b/app/workers/activitypub/migrated_follow_delivery_worker.rb new file mode 100644 index 0000000000..17a9e515ef --- /dev/null +++ b/app/workers/activitypub/migrated_follow_delivery_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class ActivityPub::MigratedFollowDeliveryWorker < ActivityPub::DeliveryWorker + def perform(json, source_account_id, inbox_url, old_target_account_id, options = {}) + super(json, source_account_id, inbox_url, options) + unfollow_old_account!(old_target_account_id) + end + + private + + def unfollow_old_account!(old_target_account_id) + old_target_account = Account.find(old_target_account_id) + UnfollowService.new.call(@source_account, old_target_account, skip_unmerge: true) + rescue StandardError + true + end +end diff --git a/app/workers/unfollow_follow_worker.rb b/app/workers/unfollow_follow_worker.rb index 7203b4888f..a4d57839de 100644 --- a/app/workers/unfollow_follow_worker.rb +++ b/app/workers/unfollow_follow_worker.rb @@ -10,13 +10,7 @@ class UnfollowFollowWorker old_target_account = Account.find(old_target_account_id) new_target_account = Account.find(new_target_account_id) - follow = follower_account.active_relationships.find_by(target_account: old_target_account) - reblogs = follow&.show_reblogs? - notify = follow&.notify? - languages = follow&.languages - - FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true) - UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true) + FollowMigrationService.new.call(follower_account, new_target_account, old_target_account, bypass_locked: bypass_locked) rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError true end From c65c34dfd11be9b307e9049fb58194c4cfa76a73 Mon Sep 17 00:00:00 2001 From: Nick Schonning Date: Fri, 3 Mar 2023 16:48:48 -0500 Subject: [PATCH 20/23] Remove climate_control gem (#23886) --- Gemfile | 1 - Gemfile.lock | 1 - 2 files changed, 2 deletions(-) diff --git a/Gemfile b/Gemfile index 354f6bd6cf..ede81af163 100644 --- a/Gemfile +++ b/Gemfile @@ -119,7 +119,6 @@ end group :test do gem 'capybara', '~> 3.38' - gem 'climate_control', '~> 0.2' gem 'faker', '~> 3.1' gem 'json-schema', '~> 3.0' gem 'rack-test', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 78774ac9cc..73d45fb547 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -792,7 +792,6 @@ DEPENDENCIES capybara (~> 3.38) charlock_holmes (~> 0.7.7) chewy (~> 7.2) - climate_control (~> 0.2) cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby From b00f945d92b6fff58c8a8ed702a9511ddb56a3da Mon Sep 17 00:00:00 2001 From: Nick Schonning Date: Fri, 3 Mar 2023 16:49:16 -0500 Subject: [PATCH 21/23] Remove implied StandardError rescue (#23942) --- app/workers/activitypub/migrated_follow_delivery_worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/workers/activitypub/migrated_follow_delivery_worker.rb b/app/workers/activitypub/migrated_follow_delivery_worker.rb index 17a9e515ef..daf30e0ae7 100644 --- a/app/workers/activitypub/migrated_follow_delivery_worker.rb +++ b/app/workers/activitypub/migrated_follow_delivery_worker.rb @@ -11,7 +11,7 @@ class ActivityPub::MigratedFollowDeliveryWorker < ActivityPub::DeliveryWorker def unfollow_old_account!(old_target_account_id) old_target_account = Account.find(old_target_account_id) UnfollowService.new.call(@source_account, old_target_account, skip_unmerge: true) - rescue StandardError + rescue true end end From 1840d5d50c68a7fa85ea026cdd53c0b2c5cda83f Mon Sep 17 00:00:00 2001 From: Nick Schonning Date: Fri, 3 Mar 2023 16:53:08 -0500 Subject: [PATCH 22/23] Remove pry gems (#23884) --- Gemfile | 2 -- Gemfile.lock | 11 ----------- 2 files changed, 13 deletions(-) diff --git a/Gemfile b/Gemfile index ede81af163..d3eefb224c 100644 --- a/Gemfile +++ b/Gemfile @@ -104,8 +104,6 @@ group :development, :test do gem 'fabrication', '~> 2.30' gem 'fuubar', '~> 2.5' gem 'i18n-tasks', '~> 1.0', require: false - gem 'pry-byebug', '~> 3.10' - gem 'pry-rails', '~> 0.3' gem 'rspec-rails', '~> 6.0' gem 'rubocop-performance', require: false gem 'rubocop-rails', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 73d45fb547..51cf8147b0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -155,7 +155,6 @@ GEM bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) - byebug (11.1.3) capistrano (3.17.2) airbrussh (>= 1.0.0) i18n @@ -497,14 +496,6 @@ GEM net-smtp premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) - pry (0.14.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.10.1) - byebug (~> 11.0) - pry (>= 0.13, < 0.15) - pry-rails (0.3.9) - pry (>= 0.10.4) public_suffix (5.0.1) puma (6.1.0) nio4r (~> 2.0) @@ -849,8 +840,6 @@ DEPENDENCIES posix-spawn premailer-rails private_address_check (~> 0.5) - pry-byebug (~> 3.10) - pry-rails (~> 0.3) public_suffix (~> 5.0) puma (~> 6.1) pundit (~> 2.3) From aa98c8fbeb02fecac2681464fd7c0445deb466b1 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 22:55:43 +0100 Subject: [PATCH 23/23] Disable Style/SymbolArray (#23921) --- .rubocop.yml | 3 ++ .rubocop_todo.yml | 131 ---------------------------------------------- 2 files changed, 3 insertions(+), 131 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 0a41c54b90..f7ed79b76d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -127,3 +127,6 @@ Style/TrailingCommaInArrayLiteral: Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: 'comma' + +Style/SymbolArray: + Enabled: false diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 22e1a99c12..b53f655bd0 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -2235,134 +2235,3 @@ Style/SlicingWithRange: - 'lib/active_record/batches.rb' - 'lib/mastodon/premailer_webpack_strategy.rb' - 'lib/tasks/repo.rake' - -# Offense count: 272 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MinSize. -# SupportedStyles: percent, brackets -Style/SymbolArray: - Exclude: - - 'app/controllers/accounts_controller.rb' - - 'app/controllers/activitypub/replies_controller.rb' - - 'app/controllers/admin/accounts_controller.rb' - - 'app/controllers/admin/announcements_controller.rb' - - 'app/controllers/admin/domain_blocks_controller.rb' - - 'app/controllers/admin/email_domain_blocks_controller.rb' - - 'app/controllers/admin/relationships_controller.rb' - - 'app/controllers/admin/relays_controller.rb' - - 'app/controllers/admin/roles_controller.rb' - - 'app/controllers/admin/rules_controller.rb' - - 'app/controllers/admin/statuses_controller.rb' - - 'app/controllers/admin/trends/statuses_controller.rb' - - 'app/controllers/admin/warning_presets_controller.rb' - - 'app/controllers/admin/webhooks_controller.rb' - - 'app/controllers/api/v1/accounts/credentials_controller.rb' - - 'app/controllers/api/v1/accounts_controller.rb' - - 'app/controllers/api/v1/admin/accounts_controller.rb' - - 'app/controllers/api/v1/admin/canonical_email_blocks_controller.rb' - - 'app/controllers/api/v1/admin/domain_allows_controller.rb' - - 'app/controllers/api/v1/admin/domain_blocks_controller.rb' - - 'app/controllers/api/v1/admin/email_domain_blocks_controller.rb' - - 'app/controllers/api/v1/admin/ip_blocks_controller.rb' - - 'app/controllers/api/v1/admin/reports_controller.rb' - - 'app/controllers/api/v1/crypto/deliveries_controller.rb' - - 'app/controllers/api/v1/crypto/keys/claims_controller.rb' - - 'app/controllers/api/v1/crypto/keys/uploads_controller.rb' - - 'app/controllers/api/v1/featured_tags_controller.rb' - - 'app/controllers/api/v1/filters_controller.rb' - - 'app/controllers/api/v1/lists_controller.rb' - - 'app/controllers/api/v1/notifications_controller.rb' - - 'app/controllers/api/v1/push/subscriptions_controller.rb' - - 'app/controllers/api/v1/scheduled_statuses_controller.rb' - - 'app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb' - - 'app/controllers/api/v1/statuses_controller.rb' - - 'app/controllers/api/v2/filters/keywords_controller.rb' - - 'app/controllers/api/v2/filters/statuses_controller.rb' - - 'app/controllers/api/v2/filters_controller.rb' - - 'app/controllers/api/web/push_subscriptions_controller.rb' - - 'app/controllers/application_controller.rb' - - 'app/controllers/auth/registrations_controller.rb' - - 'app/controllers/filters_controller.rb' - - 'app/controllers/settings/applications_controller.rb' - - 'app/controllers/settings/featured_tags_controller.rb' - - 'app/controllers/settings/profiles_controller.rb' - - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb' - - 'app/controllers/statuses_controller.rb' - - 'app/lib/feed_manager.rb' - - 'app/models/account.rb' - - 'app/models/account_filter.rb' - - 'app/models/admin/status_filter.rb' - - 'app/models/announcement.rb' - - 'app/models/concerns/ldap_authenticable.rb' - - 'app/models/concerns/status_threading_concern.rb' - - 'app/models/custom_filter.rb' - - 'app/models/domain_block.rb' - - 'app/models/import.rb' - - 'app/models/list.rb' - - 'app/models/media_attachment.rb' - - 'app/models/preview_card.rb' - - 'app/models/relay.rb' - - 'app/models/report.rb' - - 'app/models/site_upload.rb' - - 'app/models/status.rb' - - 'app/serializers/initial_state_serializer.rb' - - 'app/serializers/rest/notification_serializer.rb' - - 'db/migrate/20160220174730_create_accounts.rb' - - 'db/migrate/20160221003621_create_follows.rb' - - 'db/migrate/20160223171800_create_favourites.rb' - - 'db/migrate/20160224223247_create_mentions.rb' - - 'db/migrate/20160314164231_add_owner_to_application.rb' - - 'db/migrate/20160316103650_add_missing_indices.rb' - - 'db/migrate/20160926213048_remove_owner_from_application.rb' - - 'db/migrate/20161003145426_create_blocks.rb' - - 'db/migrate/20161006213403_rails_settings_migration.rb' - - 'db/migrate/20161105130633_create_statuses_tags_join_table.rb' - - 'db/migrate/20161119211120_create_notifications.rb' - - 'db/migrate/20161128103007_create_subscriptions.rb' - - 'db/migrate/20161222204147_create_follow_requests.rb' - - 'db/migrate/20170112154826_migrate_settings.rb' - - 'db/migrate/20170301222600_create_mutes.rb' - - 'db/migrate/20170406215816_add_notifications_and_favourites_indices.rb' - - 'db/migrate/20170424003227_create_account_domain_blocks.rb' - - 'db/migrate/20170427011934_re_add_owner_to_application.rb' - - 'db/migrate/20170507141759_optimize_index_subscriptions.rb' - - 'db/migrate/20170508230434_create_conversation_mutes.rb' - - 'db/migrate/20170720000000_add_index_favourites_on_account_id_and_id.rb' - - 'db/migrate/20170823162448_create_status_pins.rb' - - 'db/migrate/20170901142658_create_join_table_preview_cards_statuses.rb' - - 'db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb' - - 'db/migrate/20170917153509_create_custom_emojis.rb' - - 'db/migrate/20170918125918_ids_to_bigints.rb' - - 'db/migrate/20171116161857_create_list_accounts.rb' - - 'db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb' - - 'db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb' - - 'db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb' - - 'db/migrate/20171129172043_add_index_on_stream_entries.rb' - - 'db/migrate/20171226094803_more_faster_index_on_notifications.rb' - - 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb' - - 'db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb' - - 'db/migrate/20180808175627_create_account_pins.rb' - - 'db/migrate/20180831171112_create_bookmarks.rb' - - 'db/migrate/20180929222014_create_account_conversations.rb' - - 'db/migrate/20181007025445_create_pghero_space_stats.rb' - - 'db/migrate/20181203003808_create_accounts_tags_join_table.rb' - - 'db/migrate/20190316190352_create_account_identity_proofs.rb' - - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' - - 'db/migrate/20190820003045_update_statuses_index.rb' - - 'db/migrate/20190823221802_add_local_index_to_statuses.rb' - - 'db/migrate/20190904222339_create_markers.rb' - - 'db/migrate/20200113125135_create_announcement_mutes.rb' - - 'db/migrate/20200114113335_create_announcement_reactions.rb' - - 'db/migrate/20200119112504_add_public_index_to_statuses.rb' - - 'db/migrate/20200628133322_create_account_notes.rb' - - 'db/migrate/20200917222316_add_index_notifications_on_type.rb' - - 'db/migrate/20210425135952_add_index_on_media_attachments_account_id_status_id.rb' - - 'db/migrate/20220714171049_create_tag_follows.rb' - - 'db/migrate/20221021055441_add_index_featured_tags_on_account_id_and_tag_id.rb' - - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb' - - 'db/post_migrate/20200917222734_remove_index_notifications_on_account_activity.rb' - - 'spec/controllers/api/v1/streaming_controller_spec.rb' - - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' - - 'spec/controllers/concerns/signature_verification_spec.rb' - - 'spec/fabricators/notification_fabricator.rb' - - 'spec/models/public_feed_spec.rb'