diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-security.yml index d3cb4e5e0a..72729b544b 100644 --- a/.github/workflows/build-security.yml +++ b/.github/workflows/build-security.yml @@ -9,7 +9,6 @@ permissions: jobs: compute-suffix: runs-on: ubuntu-latest - if: github.repository == 'mastodon/mastodon' steps: - id: version_vars env: diff --git a/.storybook/static/mockServiceWorker.js b/.storybook/static/mockServiceWorker.js index de7bc0f292..be4527c7ee 100644 --- a/.storybook/static/mockServiceWorker.js +++ b/.storybook/static/mockServiceWorker.js @@ -7,7 +7,7 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.10.2' +const PACKAGE_VERSION = '2.10.4' const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() diff --git a/CHANGELOG.md b/CHANGELOG.md index b3af469bb3..a6684e2067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -583,7 +583,6 @@ The following changelog entries focus on changes visible to users, administrator You can now separately filter or drop notifications from people you don't follow, people who don't follow you, accounts created within the past 30 days, as well as unsolicited private mentions, and accounts limited by the moderation.\ Instead of being outright dropped, notifications that you chose to filter are put in a separate “Filtered notifications” box that you can review separately without it clogging your main notifications.\ This adds the following REST API endpoints: - - `GET /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#get-policy - `PATCH /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#update-the-filtering-policy-for-notifications - `GET /api/v1/notifications/requests`: https://docs.joinmastodon.org/methods/notifications/#get-requests @@ -595,7 +594,6 @@ The following changelog entries focus on changes visible to users, administrator - `GET /api/v1/notifications/requests/merged`: https://docs.joinmastodon.org/methods/notifications/#requests-merged In addition, accepting one or more notification requests generates a new streaming event: - - `notifications_merged`: an event of this type indicates accepted notification requests have finished merging, and the notifications list should be refreshed - **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\ diff --git a/Dockerfile b/Dockerfile index 23214c2483..3872322854 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,11 +17,11 @@ ARG RUBY_VERSION="3.4.5" # # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # renovate: datasource=node-version depName=node ARG NODE_MAJOR_VERSION="22" -# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] -ARG DEBIAN_VERSION="bookworm" -# Node.js image to use for base image based on combined variables (ex: 20-bookworm-slim) +# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="trixie"] +ARG DEBIAN_VERSION="trixie" +# Node.js image to use for base image based on combined variables (ex: 20-trixie-slim) FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node -# Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-bookworm) +# Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-trixie) FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby # Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA @@ -96,9 +96,6 @@ RUN \ # Set /opt/mastodon as working directory WORKDIR /opt/mastodon -# Add backport repository for some specific packages where we need the latest version -RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list - # hadolint ignore=DL3008,DL3005 RUN \ # Mount Apt cache and lib directories from Docker buildx caches @@ -161,11 +158,11 @@ RUN \ libexif-dev \ libexpat1-dev \ libgirepository1.0-dev \ - libheif-dev/bookworm-backports \ + libheif-dev \ + libhwy-dev \ libimagequant-dev \ libjpeg62-turbo-dev \ liblcms2-dev \ - liborc-dev \ libspng-dev \ libtiff-dev \ libwebp-dev \ @@ -209,7 +206,7 @@ FROM build AS ffmpeg # ffmpeg version to compile, change with [--build-arg FFMPEG_VERSION="7.0.x"] # renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg -ARG FFMPEG_VERSION=7.1 +ARG FFMPEG_VERSION=7.1.1 # ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"] ARG FFMPEG_URL=https://ffmpeg.org/releases @@ -327,28 +324,28 @@ RUN \ # Apt update install non-dev versions of necessary components apt-get install -y --no-install-recommends \ libexpat1 \ - libglib2.0-0 \ - libicu72 \ + libglib2.0-0t64 \ + libicu76 \ libidn12 \ libpq5 \ - libreadline8 \ - libssl3 \ + libreadline8t64 \ + libssl3t64 \ libyaml-0-2 \ # libvips components libcgif0 \ libexif12 \ - libheif1/bookworm-backports \ + libheif1 \ + libhwy1t64 \ libimagequant0 \ libjpeg62-turbo \ liblcms2-2 \ - liborc-0.4-0 \ libspng0 \ libtiff6 \ libwebp7 \ libwebpdemux2 \ libwebpmux3 \ # ffmpeg components - libdav1d6 \ + libdav1d7 \ libmp3lame0 \ libopencore-amrnb0 \ libopencore-amrwb0 \ @@ -358,9 +355,9 @@ RUN \ libvorbis0a \ libvorbisenc2 \ libvorbisfile3 \ - libvpx7 \ + libvpx9 \ libx264-164 \ - libx265-199 \ + libx265-215 \ ; # Copy Mastodon sources into final layer diff --git a/Gemfile b/Gemfile index ee2369921d..b6ecb0126d 100644 --- a/Gemfile +++ b/Gemfile @@ -82,13 +82,13 @@ gem 'rqrcode', '~> 3.0' gem 'ruby-progressbar', '~> 1.13' gem 'sanitize', '~> 7.0' gem 'scenic', '~> 1.7' -gem 'sidekiq', '< 8' +gem 'sidekiq', '< 9' gem 'sidekiq-bulk', '~> 0.2.0' gem 'sidekiq-scheduler', '~> 6.0' gem 'sidekiq-unique-jobs', '> 8' gem 'simple_form', '~> 5.2' gem 'simple-navigation', '~> 4.4' -gem 'stoplight', '~> 4.1' +gem 'stoplight' gem 'strong_migrations' gem 'tty-prompt', '~> 0.23', require: false gem 'twitter-text', '~> 3.1.0' @@ -102,17 +102,17 @@ gem 'rdf-normalize', '~> 0.5' gem 'prometheus_exporter', '~> 2.2', require: false -gem 'opentelemetry-api', '~> 1.5.0' +gem 'opentelemetry-api', '~> 1.6.0' group :opentelemetry do gem 'opentelemetry-exporter-otlp', '~> 0.30.0', require: false gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false - gem 'opentelemetry-instrumentation-excon', '~> 0.23.0', require: false - gem 'opentelemetry-instrumentation-faraday', '~> 0.27.0', require: false + gem 'opentelemetry-instrumentation-excon', '~> 0.24.0', require: false + gem 'opentelemetry-instrumentation-faraday', '~> 0.28.0', require: false gem 'opentelemetry-instrumentation-http', '~> 0.25.0', require: false - gem 'opentelemetry-instrumentation-http_client', '~> 0.23.0', require: false + gem 'opentelemetry-instrumentation-http_client', '~> 0.24.0', require: false gem 'opentelemetry-instrumentation-net_http', '~> 0.23.0', require: false gem 'opentelemetry-instrumentation-pg', '~> 0.30.0', require: false gem 'opentelemetry-instrumentation-rack', '~> 0.26.0', require: false @@ -146,7 +146,7 @@ group :test do gem 'climate_control' # Validate schemas in specs - gem 'json-schema', '~> 5.0' + gem 'json-schema', '~> 6.0' # Test harness fo rack components gem 'rack-test', '~> 2.1' @@ -223,7 +223,7 @@ gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' gem 'net-http', '~> 0.6.0' -gem 'rubyzip', '~> 2.3' +gem 'rubyzip', '~> 3.0' gem 'hcaptcha', '~> 7.1' diff --git a/Gemfile.lock b/Gemfile.lock index 943334b03d..85114a3f1e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,29 +10,29 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (8.0.2) - actionpack (= 8.0.2) - activesupport (= 8.0.2) + actioncable (8.0.2.1) + actionpack (= 8.0.2.1) + activesupport (= 8.0.2.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.2) - actionpack (= 8.0.2) - activejob (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) + actionmailbox (8.0.2.1) + actionpack (= 8.0.2.1) + activejob (= 8.0.2.1) + activerecord (= 8.0.2.1) + activestorage (= 8.0.2.1) + activesupport (= 8.0.2.1) mail (>= 2.8.0) - actionmailer (8.0.2) - actionpack (= 8.0.2) - actionview (= 8.0.2) - activejob (= 8.0.2) - activesupport (= 8.0.2) + actionmailer (8.0.2.1) + actionpack (= 8.0.2.1) + actionview (= 8.0.2.1) + activejob (= 8.0.2.1) + activesupport (= 8.0.2.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.2) - actionview (= 8.0.2) - activesupport (= 8.0.2) + actionpack (8.0.2.1) + actionview (= 8.0.2.1) + activesupport (= 8.0.2.1) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -40,15 +40,15 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.2) - actionpack (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) + actiontext (8.0.2.1) + actionpack (= 8.0.2.1) + activerecord (= 8.0.2.1) + activestorage (= 8.0.2.1) + activesupport (= 8.0.2.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.2) - activesupport (= 8.0.2) + actionview (8.0.2.1) + activesupport (= 8.0.2.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -58,22 +58,22 @@ GEM activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (8.0.2) - activesupport (= 8.0.2) + activejob (8.0.2.1) + activesupport (= 8.0.2.1) globalid (>= 0.3.6) - activemodel (8.0.2) - activesupport (= 8.0.2) - activerecord (8.0.2) - activemodel (= 8.0.2) - activesupport (= 8.0.2) + activemodel (8.0.2.1) + activesupport (= 8.0.2.1) + activerecord (8.0.2.1) + activemodel (= 8.0.2.1) + activesupport (= 8.0.2.1) timeout (>= 0.4.0) - activestorage (8.0.2) - actionpack (= 8.0.2) - activejob (= 8.0.2) - activerecord (= 8.0.2) - activesupport (= 8.0.2) + activestorage (8.0.2.1) + actionpack (= 8.0.2.1) + activejob (= 8.0.2.1) + activerecord (= 8.0.2.1) + activesupport (= 8.0.2.1) marcel (~> 1.0) - activesupport (8.0.2) + activesupport (8.0.2.1) base64 benchmark (>= 0.3) bigdecimal @@ -300,8 +300,8 @@ GEM highline (3.1.2) reline hiredis (0.6.3) - hiredis-client (0.25.1) - redis-client (= 0.25.1) + hiredis-client (0.25.2) + redis-client (= 0.25.2) hkdf (0.3.0) htmlentities (4.3.4) http (5.3.1) @@ -315,7 +315,7 @@ GEM http_accept_language (2.1.1) httpclient (2.9.0) mutex_m - httplog (1.7.2) + httplog (1.7.3) rack (>= 2.0) rainbow (>= 2.0.0) i18n (1.14.7) @@ -365,7 +365,7 @@ GEM json-ld-preloaded (3.3.2) json-ld (~> 3.3) rdf (~> 3.3) - json-schema (5.2.1) + json-schema (6.0.0) addressable (~> 2.8) bigdecimal (~> 3.1) jsonapi-renderer (0.2.2) @@ -497,7 +497,7 @@ GEM openssl (3.3.0) openssl-signature_algorithm (1.3.0) openssl (> 2.0) - opentelemetry-api (1.5.0) + opentelemetry-api (1.6.0) opentelemetry-common (0.22.0) opentelemetry-api (~> 1.0) opentelemetry-exporter-otlp (0.30.0) @@ -547,19 +547,19 @@ GEM opentelemetry-instrumentation-concurrent_ruby (0.22.0) opentelemetry-api (~> 1.0) opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-excon (0.23.0) + opentelemetry-instrumentation-excon (0.24.0) opentelemetry-api (~> 1.0) opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-faraday (0.27.0) + opentelemetry-instrumentation-faraday (0.28.0) opentelemetry-api (~> 1.0) opentelemetry-instrumentation-base (~> 0.23.0) opentelemetry-instrumentation-http (0.25.1) opentelemetry-api (~> 1.0) opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-http_client (0.23.0) + opentelemetry-instrumentation-http_client (0.24.0) opentelemetry-api (~> 1.0) opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-net_http (0.23.0) + opentelemetry-instrumentation-net_http (0.23.1) opentelemetry-api (~> 1.0) opentelemetry-instrumentation-base (~> 0.23.0) opentelemetry-instrumentation-pg (0.30.1) @@ -589,7 +589,7 @@ GEM opentelemetry-instrumentation-base (~> 0.23.0) opentelemetry-registry (0.4.0) opentelemetry-api (~> 1.1) - opentelemetry-sdk (1.8.0) + opentelemetry-sdk (1.8.1) opentelemetry-api (~> 1.1) opentelemetry-common (~> 0.20) opentelemetry-registry (~> 0.2) @@ -625,7 +625,7 @@ GEM premailer (~> 1.7, >= 1.7.9) prettyprint (0.2.0) prism (1.4.0) - prometheus_exporter (2.2.0) + prometheus_exporter (2.3.0) webrick propshaft (1.2.1) actionpack (>= 7.0.0) @@ -667,20 +667,20 @@ GEM rack (>= 1.3) rackup (2.2.1) rack (>= 3) - rails (8.0.2) - actioncable (= 8.0.2) - actionmailbox (= 8.0.2) - actionmailer (= 8.0.2) - actionpack (= 8.0.2) - actiontext (= 8.0.2) - actionview (= 8.0.2) - activejob (= 8.0.2) - activemodel (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) + rails (8.0.2.1) + actioncable (= 8.0.2.1) + actionmailbox (= 8.0.2.1) + actionmailer (= 8.0.2.1) + actionpack (= 8.0.2.1) + actiontext (= 8.0.2.1) + actionview (= 8.0.2.1) + activejob (= 8.0.2.1) + activemodel (= 8.0.2.1) + activerecord (= 8.0.2.1) + activestorage (= 8.0.2.1) + activesupport (= 8.0.2.1) bundler (>= 1.15.0) - railties (= 8.0.2) + railties (= 8.0.2.1) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -688,12 +688,12 @@ GEM rails-html-sanitizer (1.6.2) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - rails-i18n (8.0.1) + rails-i18n (8.0.2) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) - railties (8.0.2) - actionpack (= 8.0.2) - activesupport (= 8.0.2) + railties (8.0.2.1) + actionpack (= 8.0.2.1) + activesupport (= 8.0.2.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -717,11 +717,9 @@ GEM reline redcarpet (3.6.1) redis (4.8.1) - redis-client (0.25.1) + redis-client (0.25.2) connection_pool - redlock (1.3.2) - redis (>= 3.0.0, < 6.0) - regexp_parser (2.11.0) + regexp_parser (2.11.2) reline (0.6.2) io-console (~> 0.5) request_store (1.7.0) @@ -751,7 +749,7 @@ GEM rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (8.0.1) + rspec-rails (8.0.2) actionpack (>= 7.2) activesupport (>= 7.2) railties (>= 7.2) @@ -789,7 +787,7 @@ GEM lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0) - rubocop-rails (2.32.0) + rubocop-rails (2.33.3) activesupport (>= 4.2.0) lint_roller (~> 1.1) rack (>= 1.1) @@ -811,7 +809,7 @@ GEM ruby-vips (2.2.4) ffi (~> 1.12) logger - rubyzip (2.4.1) + rubyzip (3.0.1) rufus-scheduler (3.9.2) fugit (~> 1.1, >= 1.11.1) safety_net_attestation (0.4.0) @@ -825,12 +823,12 @@ GEM securerandom (0.4.1) shoulda-matchers (6.5.0) activesupport (>= 5.2.0) - sidekiq (7.3.9) - base64 - connection_pool (>= 2.3.0) - logger - rack (>= 2.2.4) - redis-client (>= 0.22.2) + sidekiq (8.0.7) + connection_pool (>= 2.5.0) + json (>= 2.9.0) + logger (>= 1.6.2) + rack (>= 3.1.0) + redis-client (>= 0.23.2) sidekiq-bulk (0.2.0) sidekiq sidekiq-scheduler (6.0.1) @@ -855,8 +853,8 @@ GEM stackprof (0.2.27) starry (0.2.0) base64 - stoplight (4.1.1) - redlock (~> 1.0) + stoplight (5.3.1) + zeitwerk stringio (3.1.7) strong_migrations (2.5.0) activerecord (>= 7.1) @@ -1005,7 +1003,7 @@ DEPENDENCIES jd-paperclip-azure (~> 3.0) json-ld json-ld-preloaded (~> 3.2) - json-schema (~> 5.0) + json-schema (~> 6.0) kaminari (~> 1.2) kt-paperclip (~> 7.2) letter_opener (~> 1.8) @@ -1027,15 +1025,15 @@ DEPENDENCIES omniauth-rails_csrf_protection (~> 1.0) omniauth-saml (~> 2.0) omniauth_openid_connect (~> 0.8.0) - opentelemetry-api (~> 1.5.0) + opentelemetry-api (~> 1.6.0) opentelemetry-exporter-otlp (~> 0.30.0) opentelemetry-instrumentation-active_job (~> 0.8.0) opentelemetry-instrumentation-active_model_serializers (~> 0.22.0) opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0) - opentelemetry-instrumentation-excon (~> 0.23.0) - opentelemetry-instrumentation-faraday (~> 0.27.0) + opentelemetry-instrumentation-excon (~> 0.24.0) + opentelemetry-instrumentation-faraday (~> 0.28.0) opentelemetry-instrumentation-http (~> 0.25.0) - opentelemetry-instrumentation-http_client (~> 0.23.0) + opentelemetry-instrumentation-http_client (~> 0.24.0) opentelemetry-instrumentation-net_http (~> 0.23.0) opentelemetry-instrumentation-pg (~> 0.30.0) opentelemetry-instrumentation-rack (~> 0.26.0) @@ -1075,11 +1073,11 @@ DEPENDENCIES ruby-prof ruby-progressbar (~> 1.13) ruby-vips (~> 2.2) - rubyzip (~> 2.3) + rubyzip (~> 3.0) sanitize (~> 7.0) scenic (~> 1.7) shoulda-matchers - sidekiq (< 8) + sidekiq (< 9) sidekiq-bulk (~> 0.2.0) sidekiq-scheduler (~> 6.0) sidekiq-unique-jobs (> 8) @@ -1088,7 +1086,7 @@ DEPENDENCIES simplecov (~> 0.22) simplecov-lcov (~> 0.8) stackprof - stoplight (~> 4.1) + stoplight strong_migrations test-prof thor (~> 1.2) diff --git a/README.md b/README.md index 1202728965..5c0e596b72 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Mastodon is a **free, open-source social network server** based on [ActivityPub] - **Ruby** 3.2+ - **PostgreSQL** 13+ -- **Redis** 6.2+ +- **Redis** 7.0+ - **Node.js** 20+ This repository includes deployment configurations for **Docker and docker-compose**, as well as for other environments like Heroku and Scalingo. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). A [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the main documentation. diff --git a/Vagrantfile b/Vagrantfile index ce456060cd..0a34367024 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -54,6 +54,7 @@ sudo apt-get install \ pkg-config \ protobuf-compiler \ zlib1g-dev \ + libvips42t64 \ -y # Install rvm @@ -134,7 +135,7 @@ VAGRANTFILE_API_VERSION = "2" Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - config.vm.box = "ubuntu/focal64" + config.vm.box = "bento/ubuntu-24.04" config.vm.provider :virtualbox do |vb| vb.name = "mastodon" diff --git a/app/controllers/api/v1/statuses/interaction_policies_controller.rb b/app/controllers/api/v1/statuses/interaction_policies_controller.rb new file mode 100644 index 0000000000..b8ec4fe140 --- /dev/null +++ b/app/controllers/api/v1/statuses/interaction_policies_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::BaseController + include Api::InteractionPoliciesConcern + + before_action -> { doorkeeper_authorize! :write, :'write:statuses' } + before_action -> { check_feature_enabled } + + def update + authorize @status, :update? + + @status.update!(quote_approval_policy: quote_approval_policy) + + broadcast_updates! if @status.quote_approval_policy_previously_changed? + + render json: @status, serializer: REST::StatusSerializer + end + + private + + def status_params + params.permit(:quote_approval_policy) + end + + def check_feature_enabled + raise ActionController::RoutingError unless Mastodon::Feature.outgoing_quotes_enabled? + end + + def broadcast_updates! + DistributionWorker.perform_async(@status.id, { 'update' => true }) + ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id, { 'updated_at' => Time.now.utc.iso8601 }) + end +end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index fdf1e7a468..93dbd8f9d1 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -3,6 +3,7 @@ class Api::V1::StatusesController < Api::BaseController include Authorization include AsyncRefreshesConcern + include Api::InteractionPoliciesConcern before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy] before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy] @@ -205,23 +206,6 @@ class Api::V1::StatusesController < Api::BaseController ) end - def quote_approval_policy - # TODO: handle `nil` separately - return nil unless Mastodon::Feature.outgoing_quotes_enabled? && status_params[:quote_approval_policy].present? - - case status_params[:quote_approval_policy] - when 'public' - Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16 - when 'followers' - Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16 - when 'nobody' - 0 - else - # TODO: raise more useful message - raise ActiveRecord::RecordInvalid - end - end - def serializer_for_status @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 3b42dc48ba..fc430544fb 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -23,11 +23,11 @@ class Auth::RegistrationsController < Devise::RegistrationsController super(&:build_invite_request) end - def edit # rubocop:disable Lint/UselessMethodDefinition + def edit super end - def create # rubocop:disable Lint/UselessMethodDefinition + def create super end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index c52bda67b0..182f242ae5 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -12,6 +12,8 @@ class Auth::SessionsController < Devise::SessionsController skip_before_action :require_functional! skip_before_action :update_user_sign_in + around_action :preserve_stored_location, only: :destroy, if: :continue_after? + prepend_before_action :check_suspicious!, only: [:create] include Auth::TwoFactorAuthenticationConcern @@ -31,11 +33,9 @@ class Auth::SessionsController < Devise::SessionsController end def destroy - tmp_stored_location = stored_location_for(:user) super session.delete(:challenge_passed_at) flash.delete(:notice) - store_location_for(:user, tmp_stored_location) if continue_after? end def webauthn_options @@ -96,6 +96,12 @@ class Auth::SessionsController < Devise::SessionsController private + def preserve_stored_location + original_stored_location = stored_location_for(:user) + yield + store_location_for(:user, original_stored_location) + end + def check_suspicious! user = find_user @login_is_suspicious = suspicious_sign_in?(user) unless user.nil? diff --git a/app/controllers/concerns/api/interaction_policies_concern.rb b/app/controllers/concerns/api/interaction_policies_concern.rb new file mode 100644 index 0000000000..21a4cf6c56 --- /dev/null +++ b/app/controllers/concerns/api/interaction_policies_concern.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Api::InteractionPoliciesConcern + extend ActiveSupport::Concern + + def quote_approval_policy + # TODO: handle `nil` separately + return nil unless Mastodon::Feature.outgoing_quotes_enabled? && status_params[:quote_approval_policy].present? + + case status_params[:quote_approval_policy] + when 'public' + Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16 + when 'followers' + Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16 + when 'nobody' + 0 + else + # TODO: raise more useful message + raise ActiveRecord::RecordInvalid + end + end +end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index b61a569860..2bdd355864 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -9,6 +9,8 @@ module SignatureVerification EXPIRATION_WINDOW_LIMIT = 12.hours CLOCK_SKEW_MARGIN = 1.hour + STOPLIGHT_COOL_OFF_TIME = 5.minutes.seconds + STOPLIGHT_THRESHOLD = 1 def require_account_signature! render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account @@ -107,10 +109,12 @@ module SignatureVerification end 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) } + Stoplight( + "source:#{request.remote_ip}", + cool_off_time: STOPLIGHT_COOL_OFF_TIME, + threshold: STOPLIGHT_THRESHOLD, + tracked_errors: [HTTP::Error, OpenSSL::SSL::SSLError] + ) end def actor_refresh_key!(actor) diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index d70834cec6..9dfa4041bd 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -183,7 +183,7 @@ export function directCompose(account) { }; } -export function submitCompose() { +export function submitCompose(successCallback) { return function (dispatch, getState) { const status = getState().getIn(['compose', 'text'], ''); const media = getState().getIn(['compose', 'media_attachments']); @@ -228,6 +228,8 @@ export function submitCompose() { visibility: getState().getIn(['compose', 'privacy']), poll: getState().getIn(['compose', 'poll'], null), language: getState().getIn(['compose', 'language']), + quoted_status_id: getState().getIn(['compose', 'quoted_status_id']), + quote_approval_policy: getState().getIn(['compose', 'quote_policy']), }, headers: { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), @@ -239,6 +241,9 @@ export function submitCompose() { dispatch(insertIntoTagHistory(response.data.tags, status)); dispatch(submitComposeSuccess({ ...response.data })); + if (typeof successCallback === 'function') { + successCallback(response.data); + } // To make the app more responsive, immediately push the status // into the columns diff --git a/app/javascript/mastodon/actions/compose_typed.ts b/app/javascript/mastodon/actions/compose_typed.ts index 97f0d68c51..7b1f5e688c 100644 --- a/app/javascript/mastodon/actions/compose_typed.ts +++ b/app/javascript/mastodon/actions/compose_typed.ts @@ -1,9 +1,18 @@ +import { createAction } from '@reduxjs/toolkit'; import type { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import { apiUpdateMedia } from 'mastodon/api/compose'; import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments'; import type { MediaAttachment } from 'mastodon/models/media_attachment'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; +import { + createDataLoadingThunk, + createAppThunk, +} from 'mastodon/store/typed_functions'; + +import type { ApiQuotePolicy } from '../api_types/quotes'; +import type { Status } from '../models/status'; + +import { ensureComposeIsVisible } from './compose'; type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & { unattached?: boolean; @@ -68,3 +77,26 @@ export const changeUploadCompose = createDataLoadingThunk( useLoadingBar: false, }, ); + +export const quoteComposeByStatus = createAppThunk( + 'compose/quoteComposeStatus', + (status: Status, { getState }) => { + ensureComposeIsVisible(getState); + return status; + }, +); + +export const quoteComposeById = createAppThunk( + (statusId: string, { dispatch, getState }) => { + const status = getState().statuses.get(statusId); + if (status) { + dispatch(quoteComposeByStatus(status)); + } + }, +); + +export const quoteComposeCancel = createAction('compose/quoteComposeCancel'); + +export const setQuotePolicy = createAction( + 'compose/setQuotePolicy', +); diff --git a/app/javascript/mastodon/actions/statuses_typed.ts b/app/javascript/mastodon/actions/statuses_typed.ts index cc9c389cda..f34d9f2bc3 100644 --- a/app/javascript/mastodon/actions/statuses_typed.ts +++ b/app/javascript/mastodon/actions/statuses_typed.ts @@ -1,8 +1,10 @@ import { createAction } from '@reduxjs/toolkit'; -import { apiGetContext } from 'mastodon/api/statuses'; +import { apiGetContext, apiSetQuotePolicy } from 'mastodon/api/statuses'; import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; +import type { ApiQuotePolicy } from '../api_types/quotes'; + import { importFetchedStatuses } from './importer'; export const fetchContext = createDataLoadingThunk( @@ -23,3 +25,10 @@ export const fetchContext = createDataLoadingThunk( export const completeContextRefresh = createAction<{ statusId: string }>( 'status/context/complete', ); + +export const setStatusQuotePolicy = createDataLoadingThunk( + 'status/setQuotePolicy', + ({ statusId, policy }: { statusId: string; policy: ApiQuotePolicy }) => { + return apiSetQuotePolicy(statusId, policy); + }, +); diff --git a/app/javascript/mastodon/api/statuses.ts b/app/javascript/mastodon/api/statuses.ts index 48eff2a692..123f2759d0 100644 --- a/app/javascript/mastodon/api/statuses.ts +++ b/app/javascript/mastodon/api/statuses.ts @@ -1,5 +1,10 @@ -import api, { getAsyncRefreshHeader } from 'mastodon/api'; -import type { ApiContextJSON } from 'mastodon/api_types/statuses'; +import api, { apiRequestPut, getAsyncRefreshHeader } from 'mastodon/api'; +import type { + ApiContextJSON, + ApiStatusJSON, +} from 'mastodon/api_types/statuses'; + +import type { ApiQuotePolicy } from '../api_types/quotes'; export const apiGetContext = async (statusId: string) => { const response = await api().request({ @@ -12,3 +17,15 @@ export const apiGetContext = async (statusId: string) => { refresh: getAsyncRefreshHeader(response), }; }; + +export const apiSetQuotePolicy = async ( + statusId: string, + policy: ApiQuotePolicy, +) => { + return apiRequestPut( + `v1/statuses/${statusId}/interaction_policy`, + { + quote_approval_policy: policy, + }, + ); +}; diff --git a/app/javascript/mastodon/api_types/quotes.ts b/app/javascript/mastodon/api_types/quotes.ts new file mode 100644 index 0000000000..981c047c13 --- /dev/null +++ b/app/javascript/mastodon/api_types/quotes.ts @@ -0,0 +1,33 @@ +import type { ApiStatusJSON } from './statuses'; + +export type ApiQuoteState = 'accepted' | 'pending' | 'revoked' | 'unauthorized'; +export type ApiQuotePolicy = 'public' | 'followers' | 'nobody' | 'unknown'; + +interface ApiQuoteEmptyJSON { + state: Exclude; + quoted_status: null; +} + +interface ApiNestedQuoteJSON { + state: 'accepted'; + quoted_status_id: string; +} + +interface ApiQuoteAcceptedJSON { + state: 'accepted'; + quoted_status: Omit & { + quote: ApiNestedQuoteJSON | ApiQuoteEmptyJSON; + }; +} + +export type ApiQuoteJSON = ApiQuoteAcceptedJSON | ApiQuoteEmptyJSON; + +export interface ApiQuotePolicyJSON { + automatic: ApiQuotePolicy[]; + manual: ApiQuotePolicy[]; + current_user: ApiQuotePolicy; +} + +export function isQuotePolicy(policy: string): policy is ApiQuotePolicy { + return ['public', 'followers', 'nobody'].includes(policy); +} diff --git a/app/javascript/mastodon/api_types/statuses.ts b/app/javascript/mastodon/api_types/statuses.ts index 09bd2349b3..0127f6334b 100644 --- a/app/javascript/mastodon/api_types/statuses.ts +++ b/app/javascript/mastodon/api_types/statuses.ts @@ -4,6 +4,7 @@ import type { ApiAccountJSON } from './accounts'; import type { ApiCustomEmojiJSON } from './custom_emoji'; import type { ApiMediaAttachmentJSON } from './media_attachments'; import type { ApiPollJSON } from './polls'; +import type { ApiQuoteJSON, ApiQuotePolicyJSON } from './quotes'; // See app/modals/status.rb export type StatusVisibility = @@ -118,9 +119,17 @@ export interface ApiStatusJSON { card?: ApiPreviewCardJSON; poll?: ApiPollJSON; + quote?: ApiQuoteJSON; + quote_approval?: ApiQuotePolicyJSON; } export interface ApiContextJSON { ancestors: ApiStatusJSON[]; descendants: ApiStatusJSON[]; } + +export interface ApiStatusSourceJSON { + id: string; + text: string; + spoiler_text: string; +} diff --git a/app/javascript/mastodon/components/autosuggest_textarea.jsx b/app/javascript/mastodon/components/autosuggest_textarea.jsx index c7ec3779f3..de5accc4b2 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.jsx +++ b/app/javascript/mastodon/components/autosuggest_textarea.jsx @@ -53,6 +53,7 @@ const AutosuggestTextarea = forwardRef(({ onFocus, autoFocus = true, lang, + className, }, textareaRef) => { const [suggestionsHidden, setSuggestionsHidden] = useState(true); @@ -192,7 +193,7 @@ const AutosuggestTextarea = forwardRef(({ }; return ( -
+