diff --git a/.haml-lint.yml b/.haml-lint.yml index 2b553ca56c..e493afb85f 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -11,6 +11,6 @@ linters: MiddleDot: enabled: true LineLength: - max: 320 + max: 300 ViewLength: max: 200 # Override default value of 100 inherited from rubocop diff --git a/.rubocop.yml b/.rubocop.yml index 2aa72d9e5e..7fb8ab0c55 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -39,7 +39,7 @@ Layout/FirstHashElementIndentation: # Reason: Currently disabled in .rubocop_todo.yml # https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength Layout/LineLength: - Max: 320 # Default of 120 causes a duplicate entry in generated todo file + Max: 300 # Default of 120 causes a duplicate entry in generated todo file ## Disable most Metrics/*Length cops # Reason: those are often triggered and force significant refactors when this happend diff --git a/Gemfile b/Gemfile index 5dfaeaba9a..32b05207f3 100644 --- a/Gemfile +++ b/Gemfile @@ -69,7 +69,6 @@ gem 'nsa' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' -gem 'posix-spawn' gem 'public_suffix', '~> 5.0' gem 'pundit', '~> 2.3' gem 'premailer-rails' @@ -89,7 +88,7 @@ gem 'sidekiq-unique-jobs', '~> 7.1' gem 'sidekiq-bulk', '~> 0.2.0' gem 'simple-navigation', '~> 4.4' gem 'simple_form', '~> 5.2' -gem 'stoplight', '~> 3.0.1' +gem 'stoplight', '~> 4.1' gem 'strong_migrations', '1.8.0' gem 'tty-prompt', '~> 0.23', require: false gem 'twitter-text', '~> 3.1.0' diff --git a/Gemfile.lock b/Gemfile.lock index 913164d18f..702eb39584 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -245,7 +245,7 @@ GEM tzinfo excon (0.110.0) fabrication (2.31.0) - faker (3.3.0) + faker (3.3.1) i18n (>= 1.8.11, < 2) faraday (1.10.3) faraday-em_http (~> 1.0) @@ -509,7 +509,6 @@ GEM pg (1.5.6) pghero (3.4.1) activerecord (>= 6) - posix-spawn (0.3.15) premailer (1.23.0) addressable css_parser (>= 1.12.0) @@ -733,7 +732,7 @@ GEM smart_properties (1.17.0) stackprof (0.2.26) statsd-ruby (1.5.0) - stoplight (3.0.2) + stoplight (4.1.0) redlock (~> 1.0) stringio (3.1.0) strong_migrations (1.8.0) @@ -899,7 +898,6 @@ DEPENDENCIES parslet pg (~> 1.5) pghero - posix-spawn premailer-rails private_address_check (~> 0.5) propshaft @@ -941,7 +939,7 @@ DEPENDENCIES simplecov (~> 0.22) simplecov-lcov (~> 0.8) stackprof - stoplight (~> 3.0.1) + stoplight (~> 4.1) strong_migrations (= 1.8.0) test-prof thor (~> 1.2) diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 3155866271..68f09ee023 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -66,7 +66,7 @@ module SignatureVerification compare_signed_string = build_signed_string(include_query_string: false) return actor unless verify_signature(actor, signature, compare_signed_string).nil? - actor = stoplight_wrap_request { actor_refresh_key!(actor) } + actor = stoplight_wrapper.run { actor_refresh_key!(actor) } raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? @@ -226,10 +226,10 @@ module SignatureVerification end if key_id.start_with?('acct:') - stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } + stoplight_wrapper.run { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } elsif !ActivityPub::TagManager.instance.local_uri?(key_id) account = ActivityPub::TagManager.instance.uri_to_actor(key_id) - account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) } + account ||= stoplight_wrapper.run { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) } account end rescue Mastodon::PrivateNetworkAddressError => e @@ -238,12 +238,11 @@ module SignatureVerification raise SignatureVerificationError, e.message end - def stoplight_wrap_request(&block) - Stoplight("source:#{request.remote_ip}", &block) + def stoplight_wrapper + Stoplight("source:#{request.remote_ip}") .with_threshold(1) .with_cool_off_time(5.minutes.seconds) .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) } - .run end def actor_refresh_key!(actor) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 83b3ec61fc..1e08abc950 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -10104,9 +10104,10 @@ noscript { } .filtered-notifications-banner__badge { - background-color: $highlight-text-color; + background: $ui-button-background-color; border-radius: 4px; padding: 1px 6px; + color: $white; } } diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index cdf4cdd228..962269b830 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -491,10 +491,10 @@ class FeedManager # @param [List] list # @return [Boolean] def filter_from_list?(status, list) - if status.reply? && status.in_reply_to_account_id != status.account_id - should_filter = status.in_reply_to_account_id != list.account_id - should_filter &&= !list.show_followed? - should_filter &&= !(list.show_list? && ListAccount.exists?(list_id: list.id, account_id: status.in_reply_to_account_id)) + if status.reply? && status.in_reply_to_account_id != status.account_id # Status is a reply to account other than status account + should_filter = status.in_reply_to_account_id != list.account_id # Status replies to account id other than list account + should_filter &&= !list.show_followed? # List show_followed? is false + should_filter &&= !(list.show_list? && ListAccount.exists?(list_id: list.id, account_id: status.in_reply_to_account_id)) # If show_list? true, check for a ListAccount with list and reply to account return !!should_filter end @@ -509,7 +509,11 @@ class FeedManager # @param [Hash] crutches # @return [Boolean] def filter_from_tags?(status, receiver_id, crutches) - receiver_id == status.account_id || ((crutches[:active_mentions][status.id] || []) + [status.account_id]).any? { |target_account_id| crutches[:blocking][target_account_id] || crutches[:muting][target_account_id] } || crutches[:blocked_by][status.account_id] || crutches[:domain_blocking][status.account.domain] + receiver_id == status.account_id || # Receiver is status account? + ((crutches[:active_mentions][status.id] || []) + [status.account_id]) # For mentioned accounts or status account: + .any? { |target_account_id| crutches[:blocking][target_account_id] || crutches[:muting][target_account_id] } || # - Target account is muted or blocked? + crutches[:blocked_by][status.account_id] || # Blocked by status account? + crutches[:domain_blocking][status.account.domain] # Blocking domain of status account? end # Adds a status to an account's feed, returning true if a status was diff --git a/app/models/user_ip.rb b/app/models/user_ip.rb index 87b86a24d4..a6da2c0740 100644 --- a/app/models/user_ip.rb +++ b/app/models/user_ip.rb @@ -15,4 +15,6 @@ class UserIp < ApplicationRecord self.primary_key = :user_id belongs_to :user + + scope :by_latest_used, -> { order(used_at: :desc) } end diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index e7d4a646d4..4f886a7b56 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -54,6 +54,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer accounts: { max_featured_tags: FeaturedTag::LIMIT, + max_pinned_statuses: StatusPinValidator::PIN_LIMIT, }, statuses: { diff --git a/app/services/bulk_import_row_service.rb b/app/services/bulk_import_row_service.rb index ef4c18e789..26909dfe04 100644 --- a/app/services/bulk_import_row_service.rb +++ b/app/services/bulk_import_row_service.rb @@ -10,7 +10,7 @@ class BulkImportRowService when :following, :blocking, :muting, :lists target_acct = @data['acct'] target_domain = domain(target_acct) - @target_account = stoplight_wrap_request(target_domain) { ResolveAccountService.new.call(target_acct, { check_delivery_availability: true }) } + @target_account = stoplight_wrapper(target_domain).run { ResolveAccountService.new.call(target_acct, { check_delivery_availability: true }) } return false if @target_account.nil? when :bookmarks target_uri = @data['uri'] @@ -18,7 +18,7 @@ class BulkImportRowService @target_status = ActivityPub::TagManager.instance.uri_to_resource(target_uri, Status) return false if @target_status.nil? && ActivityPub::TagManager.instance.local_uri?(target_uri) - @target_status ||= stoplight_wrap_request(target_domain) { ActivityPub::FetchRemoteStatusService.new.call(target_uri) } + @target_status ||= stoplight_wrapper(target_domain).run { ActivityPub::FetchRemoteStatusService.new.call(target_uri) } return false if @target_status.nil? end @@ -51,16 +51,15 @@ class BulkImportRowService TagManager.instance.local_domain?(domain) ? nil : TagManager.instance.normalize_domain(domain) end - def stoplight_wrap_request(domain, &block) + def stoplight_wrapper(domain) if domain.present? - Stoplight("source:#{domain}", &block) + Stoplight("source:#{domain}") .with_fallback { nil } .with_threshold(1) .with_cool_off_time(5.minutes.seconds) .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) } - .run else - yield + Stoplight('domain-blank') end end end diff --git a/app/views/admin/accounts/_local_account.html.haml b/app/views/admin/accounts/_local_account.html.haml index 82197cda43..3ed392cd1a 100644 --- a/app/views/admin/accounts/_local_account.html.haml +++ b/app/views/admin/accounts/_local_account.html.haml @@ -62,13 +62,7 @@ %td %time.formatted{ datetime: account.created_at.iso8601, title: l(account.created_at) }= l account.created_at %td -- recent_ips = account.user.ips.order(used_at: :desc).to_a -- recent_ips.each_with_index do |recent_ip, i| - %tr - - if i.zero? - %th{ rowspan: recent_ips.size }= t('admin.accounts.most_recent_ip') - %td= recent_ip.ip - %td= table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: recent_ip.ip) + = render partial: 'admin/accounts/user_ip', collection: account.user.ips.by_latest_used %tr %th= t('admin.accounts.most_recent_activity') %td diff --git a/app/views/admin/accounts/_user_ip.html.haml b/app/views/admin/accounts/_user_ip.html.haml new file mode 100644 index 0000000000..1938cf7edf --- /dev/null +++ b/app/views/admin/accounts/_user_ip.html.haml @@ -0,0 +1,5 @@ +%tr + - if user_ip_iteration.first? + %th{ rowspan: user_ip_iteration.size }= t('admin.accounts.most_recent_ip') + %td= user_ip.ip + %td= table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: user_ip.ip) diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index 376c237a98..0c6ca026bb 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -59,7 +59,7 @@ class ActivityPub::DeliveryWorker end def perform_request - light = Stoplight(@inbox_url) do + stoplight_wrapper.run do request_pool.with(@host) do |http_client| build_request(http_client).perform do |response| raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) @@ -68,10 +68,12 @@ class ActivityPub::DeliveryWorker end end end + end - light.with_threshold(STOPLIGHT_FAILURE_THRESHOLD) - .with_cool_off_time(STOPLIGHT_COOLDOWN) - .run + def stoplight_wrapper + Stoplight(@inbox_url) + .with_threshold(STOPLIGHT_FAILURE_THRESHOLD) + .with_cool_off_time(STOPLIGHT_COOLDOWN) end def failure_tracker diff --git a/app/workers/import/relationship_worker.rb b/app/workers/import/relationship_worker.rb index a411ab5876..2298b095a7 100644 --- a/app/workers/import/relationship_worker.rb +++ b/app/workers/import/relationship_worker.rb @@ -11,7 +11,7 @@ class Import::RelationshipWorker def perform(account_id, target_account_uri, relationship, options) from_account = Account.find(account_id) target_domain = domain(target_account_uri) - target_account = stoplight_wrap_request(target_domain) { ResolveAccountService.new.call(target_account_uri, { check_delivery_availability: true }) } + target_account = stoplight_wrapper(target_domain).run { ResolveAccountService.new.call(target_account_uri, { check_delivery_availability: true }) } options.symbolize_keys! return if target_account.nil? @@ -43,16 +43,15 @@ class Import::RelationshipWorker TagManager.instance.local_domain?(domain) ? nil : TagManager.instance.normalize_domain(domain) end - def stoplight_wrap_request(domain, &block) + def stoplight_wrapper(domain) if domain.present? - Stoplight("source:#{domain}", &block) + Stoplight("source:#{domain}") .with_fallback { nil } .with_threshold(1) .with_cool_off_time(5.minutes.seconds) .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) } - .run else - yield + Stoplight('domain-blank') end end end diff --git a/config/initializers/stoplight.rb b/config/initializers/stoplight.rb index 72fe40600e..7c13d50637 100644 --- a/config/initializers/stoplight.rb +++ b/config/initializers/stoplight.rb @@ -3,6 +3,6 @@ require 'stoplight' Rails.application.reloader.to_prepare do - Stoplight::Light.default_data_store = Stoplight::DataStore::Redis.new(RedisConfiguration.new.connection) - Stoplight::Light.default_notifiers = [Stoplight::Notifier::Logger.new(Rails.logger)] + Stoplight.default_data_store = Stoplight::DataStore::Redis.new(RedisConfiguration.new.connection) + Stoplight.default_notifiers = [Stoplight::Notifier::Logger.new(Rails.logger)] end diff --git a/lib/paperclip/attachment_extensions.rb b/lib/paperclip/attachment_extensions.rb index 7f82138aa8..401da11290 100644 --- a/lib/paperclip/attachment_extensions.rb +++ b/lib/paperclip/attachment_extensions.rb @@ -84,13 +84,11 @@ module Paperclip # Don't go through Stoplight if we don't have anything object-storage-oriented to do return super if @queued_for_delete.empty? && @queued_for_write.empty? && !dirty? - Stoplight('object-storage') { super }.with_threshold(STOPLIGHT_THRESHOLD).with_cool_off_time(STOPLIGHT_COOLDOWN).with_error_handler do |error, handle| - if error.is_a?(Seahorse::Client::NetworkingError) - handle.call(error) - else - raise error - end - end.run + Stoplight('object-storage') + .with_threshold(STOPLIGHT_THRESHOLD) + .with_cool_off_time(STOPLIGHT_COOLDOWN) + .with_error_handler { |error, handle| error.is_a?(Seahorse::Client::NetworkingError) ? handle.call(error) : raise(error) } + .run { super } end end end diff --git a/spec/requests/api/v1/timelines/public_spec.rb b/spec/requests/api/v1/timelines/public_spec.rb index d6e1b69759..a16abb7d3f 100644 --- a/spec/requests/api/v1/timelines/public_spec.rb +++ b/spec/requests/api/v1/timelines/public_spec.rb @@ -22,13 +22,15 @@ describe 'Public' do get '/api/v1/timelines/public', headers: headers, params: params end - let!(:private_status) { Fabricate(:status, visibility: :private) } # rubocop:disable RSpec/LetSetup let!(:local_status) { Fabricate(:status, account: Fabricate.build(:account, domain: nil)) } let!(:remote_status) { Fabricate(:status, account: Fabricate.build(:account, domain: 'example.com')) } let!(:media_status) { Fabricate(:status, media_attachments: [Fabricate.build(:media_attachment)]) } - let(:params) { {} } + before do + Fabricate(:status, visibility: :private) + end + context 'when the instance allows public preview' do let(:expected_statuses) { [local_status, remote_status, media_status] } diff --git a/spec/requests/api/v2/instance_spec.rb b/spec/requests/api/v2/instance_spec.rb index bbe8299ac5..ba702359d4 100644 --- a/spec/requests/api/v2/instance_spec.rb +++ b/spec/requests/api/v2/instance_spec.rb @@ -18,6 +18,7 @@ describe 'Instances' do expect(body_as_json) .to be_present .and include(title: 'Mastodon Glitch Edition') + .and include_configuration_limits end end @@ -31,7 +32,26 @@ describe 'Instances' do expect(body_as_json) .to be_present .and include(title: 'Mastodon Glitch Edition') + .and include_configuration_limits end end + + def include_configuration_limits + include( + configuration: include( + accounts: include( + max_featured_tags: FeaturedTag::LIMIT, + max_pinned_statuses: StatusPinValidator::PIN_LIMIT + ), + statuses: include( + max_characters: StatusLengthValidator::MAX_CHARS, + max_media_attachments: 4 # TODO, move to constant somewhere + ), + polls: include( + max_options: PollValidator::MAX_OPTIONS + ) + ) + ) + end end end diff --git a/spec/serializers/rest/instance_serializer_spec.rb b/spec/serializers/rest/instance_serializer_spec.rb index d8f2536d20..39e6b3820b 100644 --- a/spec/serializers/rest/instance_serializer_spec.rb +++ b/spec/serializers/rest/instance_serializer_spec.rb @@ -10,11 +10,22 @@ describe REST::InstanceSerializer do it 'returns recent usage data' do expect(serialization['usage']).to eq({ 'users' => { 'active_month' => 0 } }) end + end + describe 'configuration' do it 'returns the VAPID public key' do expect(serialization['configuration']['vapid']).to eq({ 'public_key' => Rails.configuration.x.vapid_public_key, }) end + + it 'returns the max pinned statuses limit' do + expect(serialization.deep_symbolize_keys) + .to include( + configuration: include( + accounts: include(max_pinned_statuses: StatusPinValidator::PIN_LIMIT) + ) + ) + end end end