diff --git a/.codeclimate.yml b/.codeclimate.yml index 9c9b4517a..dc8ca9a6f 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -30,7 +30,7 @@ plugins: channel: eslint-7 rubocop: enabled: true - channel: rubocop-0-88 + channel: rubocop-0-92 sass-lint: enabled: true exclude_patterns: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6b47350a4..c4cd48878 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,7 +11,7 @@ updates: interval: weekly open-pull-requests-limit: 99 allow: - - dependency-type: all + - dependency-type: direct - package-ecosystem: bundler directory: "/" @@ -19,4 +19,4 @@ updates: interval: weekly open-pull-requests-limit: 99 allow: - - dependency-type: all + - dependency-type: direct diff --git a/Gemfile b/Gemfile index ee0f71d56..33e467732 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ ruby '>= 2.5.0', '< 3.0.0' gem 'pkg-config', '~> 1.4' -gem 'puma', '~> 4.3' +gem 'puma', '~> 5.0' gem 'rails', '~> 5.2.4.4' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.0' @@ -20,7 +20,7 @@ gem 'makara', '~> 0.4' gem 'pghero', '~> 2.7' gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.81', require: false +gem 'aws-sdk-s3', '~> 1.83', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' @@ -54,7 +54,6 @@ gem 'doorkeeper', '~> 5.4' gem 'ed25519', '~> 1.2' gem 'fast_blank', '~> 1.0' gem 'fastimage' -gem 'goldfinger', '~> 2.1' gem 'hiredis', '~> 0.6' gem 'redis-namespace', '~> 1.8' gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b' @@ -127,12 +126,12 @@ group :test do gem 'rspec-sidekiq', '~> 3.1' gem 'simplecov', '~> 0.19', require: false gem 'webmock', '~> 3.9' - gem 'parallel_tests', '~> 3.2' + gem 'parallel_tests', '~> 3.3' gem 'rspec_junit_formatter', '~> 0.4' end group :development do - gem 'active_record_query_trace', '~> 1.7' + gem 'active_record_query_trace', '~> 1.8' gem 'annotate', '~> 3.1' gem 'better_errors', '~> 2.8' gem 'binding_of_caller', '~> 0.7' @@ -140,9 +139,9 @@ group :development do gem 'letter_opener', '~> 1.7' gem 'letter_opener_web', '~> 1.4' gem 'memory_profiler' - gem 'rubocop', '~> 0.90', require: false + gem 'rubocop', '~> 0.93', require: false gem 'rubocop-rails', '~> 2.8', require: false - gem 'brakeman', '~> 4.9', require: false + gem 'brakeman', '~> 4.10', require: false gem 'bundler-audit', '~> 0.7', require: false gem 'capistrano', '~> 3.14' @@ -160,3 +159,6 @@ end gem 'concurrent-ruby', require: false gem 'connection_pool', require: false + +gem 'xorcist', '~> 1.1' +gem 'pluck_each', '~> 0.1.3' diff --git a/Gemfile.lock b/Gemfile.lock index dc4bec2f9..0a298d1a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -44,7 +44,7 @@ GEM activemodel (>= 4.1, < 6.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - active_record_query_trace (1.7) + active_record_query_trace (1.8) activejob (5.2.4.4) activesupport (= 5.2.4.4) globalid (>= 0.3.6) @@ -79,23 +79,23 @@ GEM cocaine (~> 0.5.3) awrence (1.1.1) aws-eventstream (1.1.0) - aws-partitions (1.373.0) - aws-sdk-core (3.107.0) + aws-partitions (1.385.0) + aws-sdk-core (3.109.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.38.0) - aws-sdk-core (~> 3, >= 3.99.0) + aws-sdk-kms (1.39.0) + aws-sdk-core (~> 3, >= 3.109.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.81.0) - aws-sdk-core (~> 3, >= 3.104.3) + aws-sdk-s3 (1.83.1) + aws-sdk-core (~> 3, >= 3.109.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) aws-sigv4 (1.2.2) aws-eventstream (~> 1, >= 1.0.2) bcrypt (3.1.16) - better_errors (2.8.1) + better_errors (2.8.3) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -106,7 +106,7 @@ GEM ffi (~> 1.10.0) bootsnap (1.4.8) msgpack (~> 1.0) - brakeman (4.9.1) + brakeman (4.10.0) browser (4.2.0) builder (3.2.4) bullet (6.1.0) @@ -160,13 +160,12 @@ GEM cose (1.0.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 0.4.0) - crack (0.4.3) - safe_yaml (~> 1.0.0) + crack (0.4.4) crass (1.0.6) css_parser (1.7.1) addressable debug_inspector (0.0.3) - devise (4.7.2) + devise (4.7.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -233,7 +232,7 @@ GEM fog-json (>= 1.0) ipaddress (>= 0.8) formatador (0.2.5) - fugit (1.3.8) + fugit (1.3.9) et-orbi (~> 1.1, >= 1.1.8) raabro (~> 1.3) fuubar (2.5.0) @@ -241,12 +240,7 @@ GEM ruby-progressbar (~> 1.4) globalid (0.4.2) activesupport (>= 4.2.0) - goldfinger (2.1.1) - addressable (~> 2.5) - http (~> 4.0) - nokogiri (~> 1.8) - oj (~> 3.0) - hamlit (2.11.1) + hamlit (2.13.0) temple (>= 0.8.2) thor tilt @@ -363,7 +357,7 @@ GEM net-scp (3.0.0) net-ssh (>= 2.6.5, < 7.0.0) net-ssh (6.1.0) - nio4r (2.5.3) + nio4r (2.5.4) nokogiri (1.10.10) mini_portile2 (~> 2.4.0) nokogumbo (2.0.2) @@ -373,7 +367,7 @@ GEM concurrent-ruby (~> 1.0, >= 1.0.2) sidekiq (>= 3.5) statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.10.14) + oj (3.10.15) omniauth (1.9.1) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -381,7 +375,7 @@ GEM addressable (~> 2.3) nokogiri (~> 1.5) omniauth (~> 1.2) - omniauth-saml (1.10.2) + omniauth-saml (1.10.3) omniauth (~> 1.3, >= 1.3.2) ruby-saml (~> 1.9) openssl (2.2.0) @@ -398,9 +392,9 @@ GEM av (~> 0.9.0) paperclip (>= 2.5.2) parallel (1.19.2) - parallel_tests (3.2.0) + parallel_tests (3.3.0) parallel - parser (2.7.1.4) + parser (2.7.2.0) ast (~> 2.4.1) parslet (2.0.0) pastel (0.8.0) @@ -408,9 +402,12 @@ GEM pg (1.2.3) pghero (2.7.2) activerecord (>= 5) - pkg-config (1.4.2) + pkg-config (1.4.4) + pluck_each (0.1.3) + activerecord (> 3.2.0) + activesupport (> 3.0.0) posix-spawn (0.3.15) - premailer (1.13.1) + premailer (1.14.2) addressable css_parser (>= 1.6.0) htmlentities (>= 4.0.0) @@ -427,11 +424,11 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.6) - puma (4.3.6) + puma (5.0.2) nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) - raabro (1.3.1) + raabro (1.3.3) rack (2.2.3) rack-attack (6.3.1) rack (>= 1.0, < 3) @@ -500,7 +497,7 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.9.0) redis (>= 4, < 5) - regexp_parser (1.7.1) + regexp_parser (1.8.2) request_store (1.5.0) rack (>= 1.4) responders (3.0.1) @@ -513,7 +510,7 @@ GEM chunky_png (~> 1.0) rqrcode_core (~> 0.1) rqrcode_core (0.1.2) - rspec-core (3.9.2) + rspec-core (3.9.3) rspec-support (~> 3.9.3) rspec-expectations (3.9.2) diff-lcs (>= 1.2.0, < 2.0) @@ -535,18 +532,18 @@ GEM rspec-support (3.9.3) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (0.90.0) + rubocop (0.93.1) parallel (~> 1.10) - parser (>= 2.7.1.1) + parser (>= 2.7.1.5) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.7) + regexp_parser (>= 1.8) rexml - rubocop-ast (>= 0.3.0, < 1.0) + rubocop-ast (>= 0.6.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 2.0) - rubocop-ast (0.4.0) - parser (>= 2.7.1.4) - rubocop-rails (2.8.0) + rubocop-ast (0.8.0) + parser (>= 2.7.1.5) + rubocop-rails (2.8.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 0.87.0) @@ -555,7 +552,6 @@ GEM nokogiri (>= 1.5.10) rufus-scheduler (3.6.0) fugit (~> 1.1, >= 1.1.6) - safe_yaml (1.0.5) safety_net_attestation (0.4.0) jwt (~> 2.0) sanitize (5.2.1) @@ -577,19 +573,19 @@ GEM sidekiq (>= 3) thwait tilt (>= 1.4.0) - sidekiq-unique-jobs (6.0.23) + sidekiq-unique-jobs (6.0.24) concurrent-ruby (~> 1.0, >= 1.0.5) sidekiq (>= 4.0, < 7.0) thor (>= 0.20, < 2.0) simple-navigation (4.1.0) activesupport (>= 2.3.2) - simple_form (5.0.2) + simple_form (5.0.3) actionpack (>= 5.0) activemodel (>= 5.0) - simplecov (0.19.0) + simplecov (0.19.1) docile (~> 1.1) simplecov-html (~> 0.11) - simplecov-html (0.12.2) + simplecov-html (0.12.3) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -600,12 +596,12 @@ GEM sshkit (1.21.0) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - stackprof (0.2.15) + stackprof (0.2.16) statsd-ruby (1.4.0) stoplight (2.2.1) streamio-ffmpeg (3.0.2) multi_json (~> 1.8) - strong_migrations (0.7.1) + strong_migrations (0.7.2) activerecord (>= 5) temple (0.8.2) terminal-table (1.8.0) @@ -634,7 +630,7 @@ GEM unf (~> 0.1.0) tzinfo (1.2.7) thread_safe (~> 0.1) - tzinfo-data (1.2020.1) + tzinfo-data (1.2020.4) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext @@ -653,7 +649,7 @@ GEM safety_net_attestation (~> 0.4.0) securecompare (~> 1.0) tpm-key_attestation (~> 0.9.0) - webmock (3.9.1) + webmock (3.9.3) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -669,6 +665,7 @@ GEM websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) wisper (2.0.1) + xorcist (1.1.2) xpath (3.2.0) nokogiri (~> 1.8) @@ -677,15 +674,15 @@ PLATFORMS DEPENDENCIES active_model_serializers (~> 0.10) - active_record_query_trace (~> 1.7) + active_record_query_trace (~> 1.8) addressable (~> 2.7) annotate (~> 3.1) - aws-sdk-s3 (~> 1.81) + aws-sdk-s3 (~> 1.83) better_errors (~> 2.8) binding_of_caller (~> 0.7) blurhash (~> 0.1) bootsnap (~> 1.4) - brakeman (~> 4.9) + brakeman (~> 4.10) browser bullet (~> 6.1) bundler-audit (~> 0.7) @@ -716,7 +713,6 @@ DEPENDENCIES fog-core (<= 2.1.0) fog-openstack (~> 0.3) fuubar (~> 2.5) - goldfinger (~> 2.1) hamlit-rails (~> 0.2) health_check! hiredis (~> 0.6) @@ -751,17 +747,18 @@ DEPENDENCIES paperclip (~> 6.0) paperclip-av-transcoder (~> 0.6) parallel (~> 1.19) - parallel_tests (~> 3.2) + parallel_tests (~> 3.3) parslet pg (~> 1.2) pghero (~> 2.7) pkg-config (~> 1.4) + pluck_each (~> 0.1.3) posix-spawn premailer-rails private_address_check (~> 0.5) pry-byebug (~> 3.9) pry-rails (~> 0.3) - puma (~> 4.3) + puma (~> 5.0) pundit (~> 2.1) rack (~> 2.2.3) rack-attack (~> 6.3) @@ -778,7 +775,7 @@ DEPENDENCIES rspec-rails (~> 4.0) rspec-sidekiq (~> 3.1) rspec_junit_formatter (~> 0.4) - rubocop (~> 0.90) + rubocop (~> 0.93) rubocop-rails (~> 2.8) ruby-progressbar (~> 1.10) sanitize (~> 5.2) @@ -804,9 +801,4 @@ DEPENDENCIES webmock (~> 3.9) webpacker (~> 5.2) webpush - -RUBY VERSION - ruby 2.6.6p146 - -BUNDLED WITH - 2.1.4 + xorcist (~> 1.1) diff --git a/app/controllers/activitypub/followers_synchronizations_controller.rb b/app/controllers/activitypub/followers_synchronizations_controller.rb new file mode 100644 index 000000000..525031105 --- /dev/null +++ b/app/controllers/activitypub/followers_synchronizations_controller.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseController + include SignatureVerification + include AccountOwnedConcern + + before_action :require_signature! + before_action :set_items + before_action :set_cache_headers + + def show + expires_in 0, public: false + render json: collection_presenter, + serializer: ActivityPub::CollectionSerializer, + adapter: ActivityPub::Adapter, + content_type: 'application/activity+json' + end + + private + + def uri_prefix + signed_request_account.uri[/http(s?):\/\/[^\/]+\//] + end + + def set_items + @items = @account.followers.where(Account.arel_table[:uri].matches(uri_prefix + '%', false, true)).pluck(:uri) + end + + def collection_presenter + ActivityPub::CollectionPresenter.new( + id: account_followers_synchronization_url(@account), + type: :ordered, + items: @items + ) + end +end diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 0a561e7f0..fdb60d590 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -11,6 +11,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController def create upgrade_account + process_collection_synchronization process_payload head 202 end @@ -52,6 +53,19 @@ class ActivityPub::InboxesController < ActivityPub::BaseController DeliveryFailureTracker.reset!(signed_request_account.inbox_url) end + def process_collection_synchronization + raw_params = request.headers['Collection-Synchronization'] + return if raw_params.blank? || ENV['DISABLE_FOLLOWERS_SYNCHRONIZATION'] == 'true' + + # Re-using the syntax for signature parameters + tree = SignatureParamsParser.new.parse(raw_params) + params = SignatureParamsTransformer.new.apply(tree) + + ActivityPub::PrepareFollowersSynchronizationService.new.call(signed_request_account, params) + rescue Parslet::ParseFailed + Rails.logger.warn 'Error parsing Collection-Synchronization header' + end + def process_payload ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id) end diff --git a/app/controllers/admin/ip_blocks_controller.rb b/app/controllers/admin/ip_blocks_controller.rb new file mode 100644 index 000000000..92b8b0d2b --- /dev/null +++ b/app/controllers/admin/ip_blocks_controller.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Admin + class IpBlocksController < BaseController + def index + authorize :ip_block, :index? + + @ip_blocks = IpBlock.page(params[:page]) + @form = Form::IpBlockBatch.new + end + + def new + authorize :ip_block, :create? + + @ip_block = IpBlock.new(ip: '', severity: :no_access, expires_in: 1.year) + end + + def create + authorize :ip_block, :create? + + @ip_block = IpBlock.new(resource_params) + + if @ip_block.save + log_action :create, @ip_block + redirect_to admin_ip_blocks_path, notice: I18n.t('admin.ip_blocks.created_msg') + else + render :new + end + end + + def batch + @form = Form::IpBlockBatch.new(form_ip_block_batch_params.merge(current_account: current_account, action: action_from_button)) + @form.save + rescue ActionController::ParameterMissing + flash[:alert] = I18n.t('admin.ip_blocks.no_ip_block_selected') + rescue Mastodon::NotPermittedError + flash[:alert] = I18n.t('admin.custom_emojis.not_permitted') + ensure + redirect_to admin_ip_blocks_path + end + + private + + def resource_params + params.require(:ip_block).permit(:ip, :severity, :comment, :expires_in) + end + + def action_from_button + 'delete' if params[:delete] + end + + def form_ip_block_batch_params + params.require(:form_ip_block_batch).permit(ip_block_ids: []) + end + end +end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index aef51a647..3e66ff212 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -20,7 +20,7 @@ class Api::V1::AccountsController < Api::BaseController end def create - token = AppSignUpService.new.call(doorkeeper_token.application, account_params) + token = AppSignUpService.new.call(doorkeeper_token.application, request.remote_ip, account_params) response = Doorkeeper::OAuth::TokenResponse.new(token) headers.merge!(response.headers) @@ -42,7 +42,7 @@ class Api::V1::AccountsController < Api::BaseController end def mute - MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications)) + MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), duration: (params[:duration] || 0)) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index 805d0dee2..fd52511d7 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -7,7 +7,7 @@ class Api::V1::MutesController < Api::BaseController def index @accounts = load_accounts - render json: @accounts, each_serializer: REST::AccountSerializer + render json: @accounts, each_serializer: REST::MutedAccountSerializer end private diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index d34b333eb..0918c61e9 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -52,6 +52,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController def data_params return {} if params[:data].blank? - params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll]) + params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status]) end end diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index 7916b82fa..1dce3e70f 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -22,6 +22,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController reblog: alerts_enabled, mention: alerts_enabled, poll: alerts_enabled, + status: alerts_enabled, }, } @@ -57,6 +58,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController end def data_params - @data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll]) + @data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status]) end end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index d31966248..eb0924190 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -45,9 +45,9 @@ class Auth::RegistrationsController < Devise::RegistrationsController def build_resource(hash = nil) super(hash) - resource.locale = I18n.locale - resource.invite_code = params[:invite_code] if resource.invite_code.blank? - resource.current_sign_in_ip = request.remote_ip + resource.locale = I18n.locale + resource.invite_code = params[:invite_code] if resource.invite_code.blank? + resource.sign_up_ip = request.remote_ip resource.build_account if resource.account.nil? end diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index 63d9d9cd3..79a1ab02b 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -9,7 +9,7 @@ class FiltersController < ApplicationController before_action :set_body_classes def index - @filters = current_account.custom_filters + @filters = current_account.custom_filters.order(:phrase) end def new diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index be4dc904d..32b5d7948 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -43,6 +43,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_display_media, :setting_expand_spoilers, :setting_reduce_motion, + :setting_disable_swiping, :setting_system_font_ui, :setting_noindex, :setting_theme, diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 8e398c3b2..0f3ca36e2 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -29,6 +29,8 @@ module Admin::ActionLogsHelper link_to record.target_account.acct, admin_account_path(record.target_account_id) when 'Announcement' link_to truncate(record.text), edit_admin_announcement_path(record.id) + when 'IpBlock' + "#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})" end end @@ -48,6 +50,8 @@ module Admin::ActionLogsHelper end when 'Announcement' truncate(attributes['text'].is_a?(Array) ? attributes['text'].last : attributes['text']) + when 'IpBlock' + "#{attributes['ip']}/#{attributes['ip'].prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{attributes['severity']}")})" end end end diff --git a/app/helpers/webfinger_helper.rb b/app/helpers/webfinger_helper.rb index ab7ca4698..482f4e19e 100644 --- a/app/helpers/webfinger_helper.rb +++ b/app/helpers/webfinger_helper.rb @@ -1,38 +1,7 @@ # frozen_string_literal: true -# Monkey-patch on monkey-patch. -# Because it conflicts with the request.rb patch. -class HTTP::Timeout::PerOperationOriginal < HTTP::Timeout::PerOperation - def connect(socket_class, host, port, nodelay = false) - ::Timeout.timeout(@connect_timeout, HTTP::TimeoutError) do - @socket = socket_class.open(host, port) - @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay - end - end -end - module WebfingerHelper def webfinger!(uri) - hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri) - - raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && hidden_service_uri - - opts = { - ssl: !hidden_service_uri, - - headers: { - 'User-Agent': Mastodon::Version.user_agent, - }, - - timeout_class: HTTP::Timeout::PerOperationOriginal, - - timeout_options: { - write_timeout: 10, - connect_timeout: 5, - read_timeout: 10, - }, - } - - Goldfinger::Client.new(uri, opts.merge(Rails.configuration.x.http_client_proxy)).finger + Webfinger.new(uri).perform end end diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index 46046ded1..b941de8cc 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -257,11 +257,11 @@ export function unblockAccountFail(error) { } -export function muteAccount(id, notifications) { +export function muteAccount(id, notifications, duration=0) { return (dispatch, getState) => { dispatch(muteAccountRequest(id)); - api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => { + api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); }).catch(error => { diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js index 6cb09fe96..c4b61effd 100644 --- a/app/javascript/mastodon/actions/markers.js +++ b/app/javascript/mastodon/actions/markers.js @@ -3,6 +3,9 @@ import { debounce } from 'lodash'; import compareId from '../compare_id'; import { showAlertForError } from './alerts'; +export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; +export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; +export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL'; export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS'; export const synchronouslySubmitMarkers = () => (dispatch, getState) => { @@ -58,7 +61,7 @@ const _buildParams = (state) => { const params = {}; const lastHomeId = state.getIn(['timelines', 'home', 'items']).find(item => item !== null); - const lastNotificationId = state.getIn(['notifications', 'items', 0, 'id']); + const lastNotificationId = state.getIn(['notifications', 'lastReadId']); if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) { params.home = { @@ -97,6 +100,46 @@ export function submitMarkersSuccess({ home, notifications }) { }; }; -export function submitMarkers() { - return (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); +export function submitMarkers(params = {}) { + const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); + if (params.immediate === true) { + debouncedSubmitMarkers.flush(); + } + return result; +}; + +export const fetchMarkers = () => (dispatch, getState) => { + const params = { timeline: ['notifications'] }; + + dispatch(fetchMarkersRequest()); + + api(getState).get('/api/v1/markers', { params }).then(response => { + dispatch(fetchMarkersSuccess(response.data)); + }).catch(error => { + dispatch(fetchMarkersFail(error)); + }); +}; + +export function fetchMarkersRequest() { + return { + type: MARKERS_FETCH_REQUEST, + skipLoading: true, + }; +}; + +export function fetchMarkersSuccess(markers) { + return { + type: MARKERS_FETCH_SUCCESS, + markers, + skipLoading: true, + }; +}; + +export function fetchMarkersFail(error) { + return { + type: MARKERS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, + }; }; diff --git a/app/javascript/mastodon/actions/mutes.js b/app/javascript/mastodon/actions/mutes.js index 9f645faee..d8874f353 100644 --- a/app/javascript/mastodon/actions/mutes.js +++ b/app/javascript/mastodon/actions/mutes.js @@ -13,6 +13,7 @@ export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL'; export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS'; +export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION'; export function fetchMutes() { return (dispatch, getState) => { @@ -104,3 +105,12 @@ export function toggleHideNotifications() { dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS }); }; } + +export function changeMuteDuration(duration) { + return dispatch => { + dispatch({ + type: MUTES_CHANGE_DURATION, + duration, + }); + }; +} diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 099e42f6c..c4fa66428 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -16,6 +16,7 @@ import { getFiltersRegex } from '../selectors'; import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; import compareId from 'mastodon/compare_id'; import { searchTextFromRawStatus } from 'mastodon/actions/importer/normalizer'; +import { requestNotificationPermission } from '../utils/notifications'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -33,6 +34,12 @@ export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT'; export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; + +export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; + +export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; +export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; + defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, @@ -232,3 +239,47 @@ export const mountNotifications = () => ({ export const unmountNotifications = () => ({ type: NOTIFICATIONS_UNMOUNT, }); + + +export const markNotificationsAsRead = () => ({ + type: NOTIFICATIONS_MARK_AS_READ, +}); + +// Browser support +export function setupBrowserNotifications() { + return dispatch => { + dispatch(setBrowserSupport('Notification' in window)); + if ('Notification' in window) { + dispatch(setBrowserPermission(Notification.permission)); + } + + if ('Notification' in window && 'permissions' in navigator) { + navigator.permissions.query({ name: 'notifications' }).then((status) => { + status.onchange = () => dispatch(setBrowserPermission(Notification.permission)); + }); + } + }; +} + +export function requestBrowserPermission(callback = noOp) { + return dispatch => { + requestNotificationPermission((permission) => { + dispatch(setBrowserPermission(permission)); + callback(permission); + }); + }; +}; + +export function setBrowserSupport (value) { + return { + type: NOTIFICATIONS_SET_BROWSER_SUPPORT, + value, + }; +} + +export function setBrowserPermission (value) { + return { + type: NOTIFICATIONS_SET_BROWSER_PERMISSION, + value, + }; +} diff --git a/app/javascript/mastodon/actions/onboarding.js b/app/javascript/mastodon/actions/onboarding.js index a1dd3a731..42d8ea33f 100644 --- a/app/javascript/mastodon/actions/onboarding.js +++ b/app/javascript/mastodon/actions/onboarding.js @@ -1,8 +1,21 @@ import { changeSetting, saveSettings } from './settings'; +import { requestBrowserPermission } from './notifications'; export const INTRODUCTION_VERSION = 20181216044202; export const closeOnboarding = () => dispatch => { dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION)); dispatch(saveSettings()); + + dispatch(requestBrowserPermission((permission) => { + if (permission === 'granted') { + dispatch(changeSetting(['notifications', 'alerts', 'follow'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'favourite'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'mention'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'poll'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'status'], true)); + dispatch(saveSettings()); + } + })); }; diff --git a/app/javascript/mastodon/actions/picture_in_picture.js b/app/javascript/mastodon/actions/picture_in_picture.js new file mode 100644 index 000000000..4085cb59e --- /dev/null +++ b/app/javascript/mastodon/actions/picture_in_picture.js @@ -0,0 +1,38 @@ +// @ts-check + +export const PICTURE_IN_PICTURE_DEPLOY = 'PICTURE_IN_PICTURE_DEPLOY'; +export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE'; + +/** + * @typedef MediaProps + * @property {string} src + * @property {boolean} muted + * @property {number} volume + * @property {number} currentTime + * @property {string} poster + * @property {string} backgroundColor + * @property {string} foregroundColor + * @property {string} accentColor + */ + +/** + * @param {string} statusId + * @param {string} accountId + * @param {string} playerType + * @param {MediaProps} props + * @return {object} + */ +export const deployPictureInPicture = (statusId, accountId, playerType, props) => ({ + type: PICTURE_IN_PICTURE_DEPLOY, + statusId, + accountId, + playerType, + props, +}); + +/* + * @return {object} + */ +export const removePictureInPicture = () => ({ + type: PICTURE_IN_PICTURE_REMOVE, +}); diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js index 2705a6001..0e40ee1d6 100644 --- a/app/javascript/mastodon/components/account.js +++ b/app/javascript/mastodon/components/account.js @@ -8,6 +8,7 @@ import IconButton from './icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { me } from '../initial_state'; +import RelativeTimestamp from './relative_timestamp'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, @@ -107,11 +108,17 @@ class Account extends ImmutablePureComponent { } } + let mute_expires_at; + if (account.get('mute_expires_at')) { + mute_expires_at =
; + } + return (
+ {mute_expires_at}
diff --git a/app/javascript/mastodon/components/animated_number.js b/app/javascript/mastodon/components/animated_number.js index f3127c88e..fbe948c5b 100644 --- a/app/javascript/mastodon/components/animated_number.js +++ b/app/javascript/mastodon/components/animated_number.js @@ -5,10 +5,21 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion'; import spring from 'react-motion/lib/spring'; import { reduceMotion } from 'mastodon/initial_state'; +const obfuscatedCount = count => { + if (count < 0) { + return 0; + } else if (count <= 1) { + return count; + } else { + return '1+'; + } +}; + export default class AnimatedNumber extends React.PureComponent { static propTypes = { value: PropTypes.number.isRequired, + obfuscate: PropTypes.bool, }; state = { @@ -36,11 +47,11 @@ export default class AnimatedNumber extends React.PureComponent { } render () { - const { value } = this.props; + const { value, obfuscate } = this.props; const { direction } = this.state; if (reduceMotion) { - return ; + return obfuscate ? obfuscatedCount(value) : ; } const styles = [{ @@ -54,7 +65,7 @@ export default class AnimatedNumber extends React.PureComponent { {items => ( {items.map(({ key, data, style }) => ( - 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}> + 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : } ))} )} diff --git a/app/javascript/mastodon/components/autosuggest_emoji.js b/app/javascript/mastodon/components/autosuggest_emoji.js index ce4383a60..4937e4d98 100644 --- a/app/javascript/mastodon/components/autosuggest_emoji.js +++ b/app/javascript/mastodon/components/autosuggest_emoji.js @@ -1,8 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light'; - -const assetHost = process.env.CDN_HOST || ''; +import { assetHost } from 'mastodon/utils/config'; export default class AutosuggestEmoji extends React.PureComponent { diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index bef10624a..236e92296 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -2,13 +2,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import { createPortal } from 'react-dom'; import classNames from 'classnames'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import Icon from 'mastodon/components/icon'; const messages = defineMessages({ - show : { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, - hide : { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, - moveLeft : { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' }, + show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, + hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, + moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' }, moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, }); @@ -20,19 +20,21 @@ class ColumnHeader extends React.PureComponent { }; static propTypes = { - intl : PropTypes.object.isRequired, - title : PropTypes.node, - icon : PropTypes.string, - active : PropTypes.bool, - multiColumn : PropTypes.bool, - extraButton : PropTypes.node, + intl: PropTypes.object.isRequired, + title: PropTypes.node, + icon: PropTypes.string, + active: PropTypes.bool, + multiColumn: PropTypes.bool, + extraButton: PropTypes.node, showBackButton: PropTypes.bool, - children : PropTypes.node, - pinned : PropTypes.bool, - placeholder : PropTypes.bool, - onPin : PropTypes.func, - onMove : PropTypes.func, - onClick : PropTypes.func, + children: PropTypes.node, + pinned: PropTypes.bool, + placeholder: PropTypes.bool, + onPin: PropTypes.func, + onMove: PropTypes.func, + onClick: PropTypes.func, + appendContent: PropTypes.node, + collapseIssues: PropTypes.bool, }; state = { @@ -46,32 +48,32 @@ class ColumnHeader extends React.PureComponent { } else { this.context.router.history.goBack(); } - }; + } handleToggleClick = (e) => { e.stopPropagation(); this.setState({ collapsed: !this.state.collapsed, animating: true }); - }; + } handleTitleClick = () => { this.props.onClick(); - }; + } handleMoveLeft = () => { this.props.onMove(-1); - }; + } handleMoveRight = () => { this.props.onMove(1); - }; + } handleBackClick = () => { this.historyBack(); - }; + } handleTransitionEnd = () => { this.setState({ animating: false }); - }; + } handlePin = () => { if (!this.props.pinned) { @@ -79,10 +81,10 @@ class ColumnHeader extends React.PureComponent { } this.props.onPin(); - }; + } - render() { - const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder } = this.props; + render () { + const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props; const { collapsed, animating } = this.state; const wrapperClassName = classNames('column-header__wrapper', { @@ -106,82 +108,31 @@ class ColumnHeader extends React.PureComponent { if (children) { extraContent = ( -
+
{children} -
+
); } if (multiColumn && pinned) { - pinButton = - (); + pinButton = ; moveButtons = ( -
- - -
+
+ + +
); } else if (multiColumn && this.props.onPin) { - pinButton = - (); + pinButton = ; } if (!pinned && (multiColumn || showBackButton)) { backButton = ( - + ); } @@ -195,16 +146,20 @@ class ColumnHeader extends React.PureComponent { } if (children || (multiColumn && this.props.onPin)) { - collapseButton = - (); + + + {collapseIssues && } + + + ); } const hasTitle = icon && title; @@ -214,13 +169,9 @@ class ColumnHeader extends React.PureComponent {

{hasTitle && ( + )} {!hasTitle && backButton} @@ -229,19 +180,17 @@ class ColumnHeader extends React.PureComponent { {hasTitle && backButton} {extraButton} {collapseButton} -

- +
+ -
+
{(!collapsed || animating) && collapsedContent} -
-
-
+ + + + {appendContent} + ); if (multiColumn || placeholder) { diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index 47ad42e78..7ec39198a 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -2,45 +2,48 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; +import AnimatedNumber from 'mastodon/components/animated_number'; export default class IconButton extends React.PureComponent { static propTypes = { - className : PropTypes.string, - title : PropTypes.string.isRequired, - icon : PropTypes.string.isRequired, - onClick : PropTypes.func, + className: PropTypes.string, + title: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + onClick: PropTypes.func, onMouseDown: PropTypes.func, - onKeyDown : PropTypes.func, - onKeyPress : PropTypes.func, - size : PropTypes.number, - active : PropTypes.bool, - pressed : PropTypes.bool, - expanded : PropTypes.bool, - style : PropTypes.object, + onKeyDown: PropTypes.func, + onKeyPress: PropTypes.func, + size: PropTypes.number, + active: PropTypes.bool, + pressed: PropTypes.bool, + expanded: PropTypes.bool, + style: PropTypes.object, activeStyle: PropTypes.object, - disabled : PropTypes.bool, - inverted : PropTypes.bool, - animate : PropTypes.bool, - overlay : PropTypes.bool, - tabIndex : PropTypes.string, + disabled: PropTypes.bool, + inverted: PropTypes.bool, + animate: PropTypes.bool, + overlay: PropTypes.bool, + tabIndex: PropTypes.string, + counter: PropTypes.number, + obfuscateCount: PropTypes.bool, }; static defaultProps = { - size : 18, - active : false, + size: 18, + active: false, disabled: false, - animate : false, - overlay : false, + animate: false, + overlay: false, tabIndex: '0', }; state = { - activate : false, + activate: false, deactivate: false, - }; + } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps (nextProps) { if (!nextProps.animate) return; if (this.props.active && !nextProps.active) { @@ -50,38 +53,38 @@ export default class IconButton extends React.PureComponent { } } - handleClick = (e) => { + handleClick = (e) => { e.preventDefault(); if (!this.props.disabled) { this.props.onClick(e); } - }; + } handleKeyPress = (e) => { if (this.props.onKeyPress && !this.props.disabled) { this.props.onKeyPress(e); } - }; + } handleMouseDown = (e) => { if (!this.props.disabled && this.props.onMouseDown) { this.props.onMouseDown(e); } - }; + } handleKeyDown = (e) => { if (!this.props.disabled && this.props.onKeyDown) { this.props.onKeyDown(e); } - }; + } - render() { + render () { const style = { - // fontSize: `${this.props.size}px`, - // width: `${this.props.size * 1.28571429}px`, - // height: `${this.props.size * 1.28571429}px`, - // lineHeight: `${this.props.size}px`, + fontSize: `${this.props.size}px`, + width: `${this.props.size * 1.28571429}px`, + height: `${this.props.size * 1.28571429}px`, + lineHeight: `${this.props.size}px`, ...this.props.style, ...(this.props.active ? this.props.activeStyle : {}), }; @@ -97,6 +100,8 @@ export default class IconButton extends React.PureComponent { pressed, tabIndex, title, + counter, + obfuscateCount, } = this.props; const { @@ -111,8 +116,13 @@ export default class IconButton extends React.PureComponent { activate, deactivate, overlayed: overlay, + 'icon-button--with-counter': typeof counter !== 'undefined', }); + if (typeof counter !== 'undefined') { + style.width = 'auto'; + } + return ( +