From 42b2fdb0acc41b06a397b25ca095fa222b96b9bf Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 22 Jan 2026 13:04:15 +0100 Subject: [PATCH 01/24] Re-download Material Icons (#37571) --- app/javascript/mastodon/features/compose/components/upload.tsx | 2 +- app/javascript/material-icons/400-24px/audio.svg | 1 - app/javascript/material-icons/400-24px/block-fill.svg | 2 +- app/javascript/material-icons/400-24px/block.svg | 2 +- app/javascript/material-icons/400-24px/graphic_eq-fill.svg | 1 + app/javascript/material-icons/400-24px/graphic_eq.svg | 1 + 6 files changed, 5 insertions(+), 4 deletions(-) delete mode 100644 app/javascript/material-icons/400-24px/audio.svg create mode 100644 app/javascript/material-icons/400-24px/graphic_eq-fill.svg create mode 100644 app/javascript/material-icons/400-24px/graphic_eq.svg diff --git a/app/javascript/mastodon/features/compose/components/upload.tsx b/app/javascript/mastodon/features/compose/components/upload.tsx index 85fed0cbd3..4190f3248e 100644 --- a/app/javascript/mastodon/features/compose/components/upload.tsx +++ b/app/javascript/mastodon/features/compose/components/upload.tsx @@ -10,8 +10,8 @@ import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import CloseIcon from '@/material-icons/400-20px/close.svg?react'; -import SoundIcon from '@/material-icons/400-24px/audio.svg?react'; import EditIcon from '@/material-icons/400-24px/edit.svg?react'; +import SoundIcon from '@/material-icons/400-24px/graphic_eq.svg?react'; import WarningIcon from '@/material-icons/400-24px/warning.svg?react'; import { undoUploadCompose } from 'mastodon/actions/compose'; import { openModal } from 'mastodon/actions/modal'; diff --git a/app/javascript/material-icons/400-24px/audio.svg b/app/javascript/material-icons/400-24px/audio.svg deleted file mode 100644 index 417e47c18f..0000000000 --- a/app/javascript/material-icons/400-24px/audio.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/block-fill.svg b/app/javascript/material-icons/400-24px/block-fill.svg index 20e9889ae8..2d3801613c 100644 --- a/app/javascript/material-icons/400-24px/block-fill.svg +++ b/app/javascript/material-icons/400-24px/block-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/block.svg b/app/javascript/material-icons/400-24px/block.svg index 20e9889ae8..e9df4cdd35 100644 --- a/app/javascript/material-icons/400-24px/block.svg +++ b/app/javascript/material-icons/400-24px/block.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/graphic_eq-fill.svg b/app/javascript/material-icons/400-24px/graphic_eq-fill.svg new file mode 100644 index 0000000000..a83414784d --- /dev/null +++ b/app/javascript/material-icons/400-24px/graphic_eq-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/graphic_eq.svg b/app/javascript/material-icons/400-24px/graphic_eq.svg new file mode 100644 index 0000000000..a83414784d --- /dev/null +++ b/app/javascript/material-icons/400-24px/graphic_eq.svg @@ -0,0 +1 @@ + \ No newline at end of file From 958103368eaa3ab2a044087ca8cffac6ec9f4454 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 22 Jan 2026 13:38:00 +0100 Subject: [PATCH 02/24] Shorten caching of quote posts pending approval (#37570) --- app/controllers/statuses_controller.rb | 2 +- app/models/quote.rb | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index e673faca04..65db807d18 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -29,7 +29,7 @@ class StatusesController < ApplicationController end format.json do - expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode? + expires_in @status.quote&.pending? ? 5.seconds : 3.minutes, public: true if @status.distributable? && public_fetch_mode? render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter end end diff --git a/app/models/quote.rb b/app/models/quote.rb index 4ad393e3a5..425cf63055 100644 --- a/app/models/quote.rb +++ b/app/models/quote.rb @@ -47,6 +47,8 @@ class Quote < ApplicationRecord def accept! update!(state: :accepted) + + reset_parent_cache! if attribute_changed?(:state) end def reject! @@ -75,6 +77,15 @@ class Quote < ApplicationRecord private + def reset_parent_cache! + return if status_id.nil? + + Rails.cache.delete("v3:statuses/#{status_id}") + + # This clears the web cache for the ActivityPub representation + Rails.cache.delete("statuses/show:v3:statuses/#{status_id}") + end + def set_accounts self.account = status.account self.quoted_account = quoted_status&.account From 52ca91c43d0134fd6a3600a208ce019fa0e22159 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:50:23 +0100 Subject: [PATCH 03/24] Update dependency pg-connection-string to v2.10.1 (#37558) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9c14c9e85e..15f3d5048d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10512,9 +10512,9 @@ __metadata: linkType: hard "pg-connection-string@npm:^2.10.0, pg-connection-string@npm:^2.6.0": - version: 2.10.0 - resolution: "pg-connection-string@npm:2.10.0" - checksum: 10c0/6639d3e12f66bc3cc5fa1e5794b4b22a4212e30424cbf001422e041c77c598ee0cd19395a9d79cb99a962da612b03c021e8148c3437cf01a29a18437bad193eb + version: 2.10.1 + resolution: "pg-connection-string@npm:2.10.1" + checksum: 10c0/f218a72b59c661022caca9a7f2116655632b1d7e7d6dc9a8ee9f238744e0927e0d6f44e12f50d9767c6d9cd47d9b3766aa054b77504b15c6bf503400530e053e languageName: node linkType: hard From 8dcd3881890a70c0df396c5d69ab8ed9d5494150 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:50:59 +0000 Subject: [PATCH 04/24] Update dependency aws-sdk-s3 to v1.212.0 (#37536) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3ee1f77aa0..646162cffe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -96,8 +96,8 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1201.0) - aws-sdk-core (3.241.3) + aws-partitions (1.1206.0) + aws-sdk-core (3.241.4) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -105,11 +105,11 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.120.0) - aws-sdk-core (~> 3, >= 3.241.3) + aws-sdk-kms (1.121.0) + aws-sdk-core (~> 3, >= 3.241.4) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.211.0) - aws-sdk-core (~> 3, >= 3.241.3) + aws-sdk-s3 (1.212.0) + aws-sdk-core (~> 3, >= 3.241.4) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) From 7b9479239aa52fddba1d4949ceebebe274b194f3 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 22 Jan 2026 08:59:36 -0500 Subject: [PATCH 05/24] Typo fix in federation document (#37564) --- FEDERATION.md | 4 ++-- spec/models/account_spec.rb | 2 ++ spec/models/custom_emoji_spec.rb | 8 ++++++- spec/models/custom_filter_keyword_spec.rb | 5 +++++ spec/models/custom_filter_spec.rb | 3 ++- spec/models/list_spec.rb | 1 + spec/requests/activitypub/inboxes_spec.rb | 13 +++++++++++ .../api/web/push_subscriptions_spec.rb | 11 ++++++++++ .../services/fan_out_on_write_service_spec.rb | 22 +++++++++++++++++-- 9 files changed, 63 insertions(+), 6 deletions(-) diff --git a/FEDERATION.md b/FEDERATION.md index eb91d9545f..d5a176807b 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -52,8 +52,8 @@ Mastodon requires all `POST` requests to be signed, and MAY require `GET` reques ## Size limits Mastodon imposes a few hard limits on federated content. -These limits are intended to be very generous and way above what the Mastodon user experience is optimized for, so as to accomodate future changes and unusual or unforeseen usage patterns, while still providing some limits for performance reasons. -The following table attempts to summary those limits. +These limits are intended to be very generous and way above what the Mastodon user experience is optimized for, so as to accommodate future changes and unusual or unforeseen usage patterns, while still providing some limits for performance reasons. +The following table summarizes those limits. | Limited property | Size limit | Consequence of exceeding the limit | | ------------------------------------------------------------- | ---------- | ---------------------------------- | diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 89a2f78c2e..c9adaaff39 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -564,6 +564,8 @@ RSpec.describe Account do it { is_expected.to_not allow_values('username', 'Username').for(:username) } end + it { is_expected.to validate_length_of(:username).is_at_most(described_class::USERNAME_LENGTH_HARD_LIMIT) } + it { is_expected.to allow_values('the-doctor', username_over_limit).for(:username) } it { is_expected.to_not allow_values('the doctor').for(:username) } diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb index 244d0d126f..ab757d3968 100644 --- a/spec/models/custom_emoji_spec.rb +++ b/spec/models/custom_emoji_spec.rb @@ -90,8 +90,14 @@ RSpec.describe CustomEmoji, :attachment_processing do subject { Fabricate.build :custom_emoji } it { is_expected.to validate_uniqueness_of(:shortcode).scoped_to(:domain) } - it { is_expected.to validate_length_of(:shortcode).is_at_least(described_class::MINIMUM_SHORTCODE_SIZE) } + it { is_expected.to validate_length_of(:shortcode).is_at_least(described_class::MINIMUM_SHORTCODE_SIZE).is_at_most(described_class::MAX_SHORTCODE_SIZE) } it { is_expected.to allow_values('cats').for(:shortcode) } it { is_expected.to_not allow_values('@#$@#$', 'X').for(:shortcode) } + + context 'when remote' do + subject { Fabricate.build :custom_emoji, domain: 'host.example' } + + it { is_expected.to validate_length_of(:shortcode).is_at_most(described_class::MAX_FEDERATED_SHORTCODE_SIZE) } + end end end diff --git a/spec/models/custom_filter_keyword_spec.rb b/spec/models/custom_filter_keyword_spec.rb index 4e3ab060a0..cc6558ea69 100644 --- a/spec/models/custom_filter_keyword_spec.rb +++ b/spec/models/custom_filter_keyword_spec.rb @@ -3,6 +3,11 @@ require 'rails_helper' RSpec.describe CustomFilterKeyword do + describe 'Validations' do + it { is_expected.to validate_length_of(:keyword).is_at_most(described_class::KEYWORD_LENGTH_LIMIT) } + it { is_expected.to validate_presence_of(:keyword) } + end + describe '#to_regex' do context 'when whole_word is true' do it 'builds a regex with boundaries and the keyword' do diff --git a/spec/models/custom_filter_spec.rb b/spec/models/custom_filter_spec.rb index 03914fa6b4..8a60f1dd49 100644 --- a/spec/models/custom_filter_spec.rb +++ b/spec/models/custom_filter_spec.rb @@ -6,8 +6,9 @@ RSpec.describe CustomFilter do it_behaves_like 'Expireable' describe 'Validations' do - it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_length_of(:title).is_at_most(described_class::TITLE_LENGTH_LIMIT) } it { is_expected.to validate_presence_of(:context) } + it { is_expected.to validate_presence_of(:title) } it { is_expected.to_not allow_values([], %w(invalid)).for(:context) } it { is_expected.to allow_values(%w(home)).for(:context) } diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index e2d91835ec..bc6e67d8bf 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -6,6 +6,7 @@ RSpec.describe List do describe 'Validations' do subject { Fabricate.build :list } + it { is_expected.to validate_length_of(:title).is_at_most(described_class::TITLE_LENGTH_LIMIT) } it { is_expected.to validate_presence_of(:title) } context 'when account has hit max list limit' do diff --git a/spec/requests/activitypub/inboxes_spec.rb b/spec/requests/activitypub/inboxes_spec.rb index b21881b10f..e33afa53c9 100644 --- a/spec/requests/activitypub/inboxes_spec.rb +++ b/spec/requests/activitypub/inboxes_spec.rb @@ -20,6 +20,19 @@ RSpec.describe 'ActivityPub Inboxes' do end end + context 'with an excessively large payload' do + subject { post inbox_path, params: { this: :that, those: :these }.to_json, sign_with: remote_account } + + before { stub_const('ActivityPub::Activity::MAX_JSON_SIZE', 1.byte) } + + it 'returns http content too large' do + subject + + expect(response) + .to have_http_status(413) + end + end + context 'with a specific account' do subject { post account_inbox_path(account_username: account.username), params: {}.to_json, sign_with: remote_account } diff --git a/spec/requests/api/web/push_subscriptions_spec.rb b/spec/requests/api/web/push_subscriptions_spec.rb index 88c0302f86..3c33f0d2d2 100644 --- a/spec/requests/api/web/push_subscriptions_spec.rb +++ b/spec/requests/api/web/push_subscriptions_spec.rb @@ -190,6 +190,17 @@ RSpec.describe 'API Web Push Subscriptions' do .to eq(alerts_payload[:data][:alerts][type.to_sym].to_s) end end + + context 'when using other user subscription' do + let(:subscription) { Fabricate(:web_push_subscription) } + + it 'does not change settings' do + put api_web_push_subscription_path(subscription), params: alerts_payload + + expect(response) + .to have_http_status(404) + end + end end def created_push_subscription diff --git a/spec/services/fan_out_on_write_service_spec.rb b/spec/services/fan_out_on_write_service_spec.rb index c6dd020cdf..9f48899560 100644 --- a/spec/services/fan_out_on_write_service_spec.rb +++ b/spec/services/fan_out_on_write_service_spec.rb @@ -23,18 +23,30 @@ RSpec.describe FanOutOnWriteService do Fabricate(:media_attachment, status: status, account: alice) allow(redis).to receive(:publish) - - subject.call(status) end def home_feed_of(account) HomeFeed.new(account).get(10).map(&:id) end + context 'when status account is suspended' do + let(:visibility) { 'public' } + + before { alice.suspend! } + + it 'does not execute or broadcast' do + expect(subject.call(status)) + .to be_nil + expect_no_broadcasting + end + end + context 'when status is public' do let(:visibility) { 'public' } it 'adds status to home feed of author and followers and broadcasts', :inline_jobs do + subject.call(status) + expect(status.id) .to be_in(home_feed_of(alice)) .and be_in(home_feed_of(bob)) @@ -52,6 +64,8 @@ RSpec.describe FanOutOnWriteService do let(:visibility) { 'limited' } it 'adds status to home feed of author and mentioned followers and does not broadcast', :inline_jobs do + subject.call(status) + expect(status.id) .to be_in(home_feed_of(alice)) .and be_in(home_feed_of(bob)) @@ -66,6 +80,8 @@ RSpec.describe FanOutOnWriteService do let(:visibility) { 'private' } it 'adds status to home feed of author and followers and does not broadcast', :inline_jobs do + subject.call(status) + expect(status.id) .to be_in(home_feed_of(alice)) .and be_in(home_feed_of(bob)) @@ -79,6 +95,8 @@ RSpec.describe FanOutOnWriteService do let(:visibility) { 'direct' } it 'is added to the home feed of its author and mentioned followers and does not broadcast', :inline_jobs do + subject.call(status) + expect(status.id) .to be_in(home_feed_of(alice)) .and be_in(home_feed_of(bob)) From 3a84b73d80b2ad8eeecf882f0c735e1182f40404 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:26:33 +0100 Subject: [PATCH 06/24] New Crowdin Translations (automated) (#37569) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/nl.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 1411a222f8..bec46ab52e 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Ga naar profiel", "account.hide_reblogs": "Boosts van @{name} verbergen", "account.in_memoriam": "In memoriam.", + "account.joined_long": "Lid geworden op {date}", "account.joined_short": "Geregistreerd op", "account.languages": "Getoonde talen wijzigen", "account.link_verified_on": "Eigendom van deze link is gecontroleerd op {date}", @@ -90,6 +91,8 @@ "account.unmute": "@{name} niet langer negeren", "account.unmute_notifications_short": "Meldingen niet langer negeren", "account.unmute_short": "Niet langer negeren", + "account_fields_modal.close": "Sluiten", + "account_fields_modal.title": "{name}'s info", "account_note.placeholder": "Klik om een opmerking toe te voegen", "admin.dashboard.daily_retention": "Retentiegraad van gebruikers per dag, vanaf registratie", "admin.dashboard.monthly_retention": "Retentiegraad van gebruikers per maand, vanaf registratie", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nieuw item} other {# nieuwe items}}", "loading_indicator.label": "Laden…", "media_gallery.hide": "Verberg", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Omdat je naar {movedToAccount} bent verhuisd is jouw account {disabledAccount} momenteel uitgeschakeld.", "mute_modal.hide_from_notifications": "Onder meldingen verbergen", "mute_modal.hide_options": "Opties verbergen", From 157d8c0d9912c62c0e533d51ff41b2c552c2f5cd Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 22 Jan 2026 09:57:19 -0500 Subject: [PATCH 07/24] Remove deprecated usage of imagemagick (#37488) --- .devcontainer/Dockerfile | 2 +- .github/workflows/test-ruby.yml | 87 ------------------- Dockerfile | 2 - Vagrantfile | 1 - .../dimension/software_versions_dimension.rb | 26 +----- app/models/concerns/account/avatar.rb | 2 +- app/models/concerns/account/header.rb | 2 +- app/models/preview_card.rb | 4 +- config/application.rb | 8 +- config/imagemagick/policy.xml | 27 ------ config/initializers/deprecations.rb | 6 -- config/initializers/paperclip.rb | 7 -- config/initializers/vips.rb | 50 +++++------ lib/paperclip/blurhash_transcoder.rb | 10 +-- lib/paperclip/color_extractor.rb | 13 +-- spec/models/media_attachment_spec.rb | 4 +- spec/models/preview_card_spec.rb | 6 -- spec/requests/api/v1/media_spec.rb | 2 +- spec/requests/api/v2/media_spec.rb | 2 +- 19 files changed, 37 insertions(+), 224 deletions(-) delete mode 100644 config/imagemagick/policy.xml diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 3aa0bbf7da..ed8484f5b8 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -9,7 +9,7 @@ RUN /bin/bash --login -i -c "nvm install" # Install additional OS packages RUN apt-get update && \ export DEBIAN_FRONTEND=noninteractive && \ - apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libvips42 libpam-dev + apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg libvips42 libpam-dev # Disable download prompt for Corepack ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index 8f05812d60..316bf831b6 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -173,93 +173,6 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - test-imagemagick: - name: ImageMagick tests - runs-on: ubuntu-latest - - needs: - - build - - services: - postgres: - image: postgres:14-alpine - env: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - options: >- - --health-cmd pg_isready - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 5432:5432 - - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 6379:6379 - - env: - DB_HOST: localhost - DB_USER: postgres - DB_PASS: postgres - COVERAGE: ${{ matrix.ruby-version == '.ruby-version' }} - RAILS_ENV: test - ALLOW_NOPAM: true - PAM_ENABLED: true - PAM_DEFAULT_SERVICE: pam_test - PAM_CONTROLLED_SERVICE: pam_test_controlled - OIDC_ENABLED: true - OIDC_SCOPE: read - SAML_ENABLED: true - CAS_ENABLED: true - BUNDLE_WITH: 'pam_authentication test' - GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }} - MASTODON_USE_LIBVIPS: false - - strategy: - fail-fast: false - matrix: - ruby-version: - - '3.2' - - '3.3' - - '.ruby-version' - steps: - - uses: actions/checkout@v5 - - - uses: actions/download-artifact@v6 - with: - path: './' - name: ${{ github.sha }} - - - name: Expand archived asset artifacts - run: | - tar xvzf artifacts.tar.gz - - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - with: - ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick libpam-dev - - - name: Load database schema - run: './bin/rails db:create db:schema:load db:seed' - - - run: bin/rspec --tag attachment_processing - - - name: Upload coverage reports to Codecov - if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@v5 - with: - files: coverage/lcov/mastodon.lcov - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - test-e2e: name: End to End testing runs-on: ubuntu-latest diff --git a/Dockerfile b/Dockerfile index b9dcbe59fd..c06bc84a33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -70,8 +70,6 @@ ENV \ PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \ # Optimize jemalloc 5.x performance MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" \ - # Enable libvips, should not be changed - MASTODON_USE_LIBVIPS=true \ # Sidekiq will touch tmp/sidekiq_process_has_started_and_will_begin_processing_jobs to indicate it is ready. This can be used for a readiness check in Kubernetes MASTODON_SIDEKIQ_READY_FILENAME=sidekiq_process_has_started_and_will_begin_processing_jobs diff --git a/Vagrantfile b/Vagrantfile index 0a34367024..a2c0b13b14 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -29,7 +29,6 @@ sudo apt-get install \ libpq-dev \ libxml2-dev \ libxslt1-dev \ - imagemagick \ nodejs \ redis-server \ redis-tools \ diff --git a/app/lib/admin/metrics/dimension/software_versions_dimension.rb b/app/lib/admin/metrics/dimension/software_versions_dimension.rb index e64a6b1180..032abb7525 100644 --- a/app/lib/admin/metrics/dimension/software_versions_dimension.rb +++ b/app/lib/admin/metrics/dimension/software_versions_dimension.rb @@ -10,7 +10,7 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim protected def perform_query - [mastodon_version, ruby_version, postgresql_version, redis_version, elasticsearch_version, libvips_version, imagemagick_version, ffmpeg_version].compact + [mastodon_version, ruby_version, postgresql_version, redis_version, elasticsearch_version, libvips_version, ffmpeg_version].compact end def mastodon_version @@ -70,8 +70,6 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim end def libvips_version - return unless Rails.configuration.x.use_vips - { key: 'libvips', human_key: 'libvips', @@ -80,28 +78,6 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim } end - def imagemagick_version - return if Rails.configuration.x.use_vips - - imagemagick_binary = Paperclip.options[:is_windows] ? 'magick convert' : 'convert' - - version_output = Terrapin::CommandLine.new(imagemagick_binary, '-version').run - version_match = version_output.match(/Version: ImageMagick (\S+)/)[1].strip - - return nil unless version_match - - version = version_match - - { - key: 'imagemagick', - human_key: 'ImageMagick', - value: version, - human_value: version, - } - rescue Terrapin::CommandNotFoundError, Terrapin::ExitStatusError, Paperclip::Errors::CommandNotFoundError, Paperclip::Errors::CommandFailedError - nil - end - def ffmpeg_version version_output = Terrapin::CommandLine.new(Rails.configuration.x.ffprobe_binary, '-show_program_version -v 0 -of json').run version = Oj.load(version_output, mode: :strict, symbol_keys: true).dig(:program_version, :version) diff --git a/app/models/concerns/account/avatar.rb b/app/models/concerns/account/avatar.rb index a60a289d5b..0be4678881 100644 --- a/app/models/concerns/account/avatar.rb +++ b/app/models/concerns/account/avatar.rb @@ -4,7 +4,7 @@ module Account::Avatar extend ActiveSupport::Concern AVATAR_IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze - AVATAR_LIMIT = Rails.configuration.x.use_vips ? 8.megabytes : 2.megabytes + AVATAR_LIMIT = 8.megabytes AVATAR_DIMENSIONS = [400, 400].freeze AVATAR_GEOMETRY = [AVATAR_DIMENSIONS.first, AVATAR_DIMENSIONS.last].join('x') diff --git a/app/models/concerns/account/header.rb b/app/models/concerns/account/header.rb index 662ee7caf7..066c42cb6c 100644 --- a/app/models/concerns/account/header.rb +++ b/app/models/concerns/account/header.rb @@ -4,7 +4,7 @@ module Account::Header extend ActiveSupport::Concern HEADER_IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze - HEADER_LIMIT = Rails.configuration.x.use_vips ? 8.megabytes : 2.megabytes + HEADER_LIMIT = 8.megabytes HEADER_DIMENSIONS = [1500, 500].freeze HEADER_GEOMETRY = [HEADER_DIMENSIONS.first, HEADER_DIMENSIONS.last].join('x') HEADER_MAX_PIXELS = HEADER_DIMENSIONS.first * HEADER_DIMENSIONS.last diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index 8e0e13cdb9..644be2671a 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -39,7 +39,7 @@ class PreviewCard < ApplicationRecord include Attachmentable IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze - LIMIT = Rails.configuration.x.use_vips ? 8.megabytes : 2.megabytes + LIMIT = 8.megabytes BLURHASH_OPTIONS = { x_comp: 4, @@ -63,7 +63,7 @@ class PreviewCard < ApplicationRecord belongs_to :author_account, class_name: 'Account', optional: true has_attached_file :image, - processors: [Rails.configuration.x.use_vips ? :lazy_thumbnail : :thumbnail, :blurhash_transcoder], + processors: [:lazy_thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 90 +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, validate_media_type: false diff --git a/config/application.rb b/config/application.rb index 90cfe47428..4e58bd9f6c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -94,13 +94,7 @@ module Mastodon require 'mastodon/redis_configuration' ::REDIS_CONFIGURATION = Mastodon::RedisConfiguration.new - config.x.use_vips = ENV['MASTODON_USE_LIBVIPS'] != 'false' - - if config.x.use_vips - require_relative '../lib/paperclip/vips_lazy_thumbnail' - else - require_relative '../lib/paperclip/lazy_thumbnail' - end + require_relative '../lib/paperclip/vips_lazy_thumbnail' end config.x.cache_buster = config_for(:cache_buster) diff --git a/config/imagemagick/policy.xml b/config/imagemagick/policy.xml deleted file mode 100644 index e2aa202f27..0000000000 --- a/config/imagemagick/policy.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb index 520707e59f..2c1057e505 100644 --- a/config/initializers/deprecations.rb +++ b/config/initializers/deprecations.rb @@ -17,12 +17,6 @@ if ENV['REDIS_NAMESPACE'] abort message # rubocop:disable Rails/Exit end -if ENV['MASTODON_USE_LIBVIPS'] == 'false' - warn <<~MESSAGE - WARNING: Mastodon support for ImageMagick is deprecated and will be removed in future versions. Please consider using libvips instead. - MESSAGE -end - if ENV.key?('WHITELIST_MODE') warn(<<~MESSAGE.squish) WARNING: The environment variable WHITELIST_MODE has been replaced with diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index b444c5611b..18d35a27f1 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -182,10 +182,3 @@ unless defined?(Seahorse) end end end - -# Set our ImageMagick security policy, but allow admins to override it -ENV['MAGICK_CONFIGURE_PATH'] = begin - imagemagick_config_paths = ENV.fetch('MAGICK_CONFIGURE_PATH', '').split(File::PATH_SEPARATOR) - imagemagick_config_paths << Rails.root.join('config', 'imagemagick').expand_path.to_s - imagemagick_config_paths.join(File::PATH_SEPARATOR) -end diff --git a/config/initializers/vips.rb b/config/initializers/vips.rb index 09210d60eb..cd8d2410b9 100644 --- a/config/initializers/vips.rb +++ b/config/initializers/vips.rb @@ -1,35 +1,33 @@ # frozen_string_literal: true -if Rails.configuration.x.use_vips - ENV['VIPS_BLOCK_UNTRUSTED'] = 'true' +ENV['VIPS_BLOCK_UNTRUSTED'] = 'true' - require 'vips' +require 'vips' - unless Vips.at_least_libvips?(8, 13) - abort <<~ERROR.squish # rubocop:disable Rails/Exit - Incompatible libvips version (#{Vips.version_string}), please install libvips >= 8.13 - ERROR - end - - Vips.block('VipsForeign', true) - - %w( - VipsForeignLoadNsgif - VipsForeignLoadJpeg - VipsForeignLoadPng - VipsForeignLoadWebp - VipsForeignLoadHeif - VipsForeignSavePng - VipsForeignSaveSpng - VipsForeignSaveJpeg - VipsForeignSaveWebp - ).each do |operation| - Vips.block(operation, false) - end - - Vips.block_untrusted(true) +unless Vips.at_least_libvips?(8, 13) + abort <<~ERROR.squish # rubocop:disable Rails/Exit + Incompatible libvips version (#{Vips.version_string}), please install libvips >= 8.13 + ERROR end +Vips.block('VipsForeign', true) + +%w( + VipsForeignLoadNsgif + VipsForeignLoadJpeg + VipsForeignLoadPng + VipsForeignLoadWebp + VipsForeignLoadHeif + VipsForeignSavePng + VipsForeignSaveSpng + VipsForeignSaveJpeg + VipsForeignSaveWebp +).each do |operation| + Vips.block(operation, false) +end + +Vips.block_untrusted(true) + # In some places of the code, we rescue this exception, but we don't always # load libvips, so it may be an undefined constant: unless defined?(Vips) diff --git a/lib/paperclip/blurhash_transcoder.rb b/lib/paperclip/blurhash_transcoder.rb index b4ff4a12a0..e9ff1dd9dd 100644 --- a/lib/paperclip/blurhash_transcoder.rb +++ b/lib/paperclip/blurhash_transcoder.rb @@ -19,14 +19,8 @@ module Paperclip private def blurhash_params - if Rails.configuration.x.use_vips - image = Vips::Image.thumbnail(@file.path, 100) - [image.width, image.height, image.colourspace(:srgb).extract_band(0, n: 3).to_a.flatten] - else - pixels = convert(':source -depth 8 RGB:-', source: File.expand_path(@file.path)).unpack('C*') - geometry = options.fetch(:file_geometry_parser).from_file(@file) - [geometry.width, geometry.height, pixels] - end + image = Vips::Image.thumbnail(@file.path, 100) + [image.width, image.height, image.colourspace(:srgb).extract_band(0, n: 3).to_a.flatten] end end end diff --git a/lib/paperclip/color_extractor.rb b/lib/paperclip/color_extractor.rb index 62daa07795..1c9ef4bd3d 100644 --- a/lib/paperclip/color_extractor.rb +++ b/lib/paperclip/color_extractor.rb @@ -10,7 +10,7 @@ module Paperclip BINS = 10 def make - background_palette, foreground_palette = Rails.configuration.x.use_vips ? palettes_from_libvips : palettes_from_imagemagick + background_palette, foreground_palette = palettes_from_libvips background_color = background_palette.first || foreground_palette.first foreground_colors = [] @@ -93,17 +93,6 @@ module Paperclip [background_palette, foreground_palette] end - def palettes_from_imagemagick - depth = 8 - - # Determine background palette by getting colors close to the image's edge only - background_palette = palette_from_im_histogram(convert(':source -alpha set -gravity Center -region 75%x75% -fill None -colorize 100% -alpha transparent +region -format %c -colors :quantity -depth :depth histogram:info:', source: File.expand_path(@file.path), quantity: 10, depth: depth), 10) - - # Determine foreground palette from the whole image - foreground_palette = palette_from_im_histogram(convert(':source -format %c -colors :quantity -depth :depth histogram:info:', source: File.expand_path(@file.path), quantity: 10, depth: depth), 10) - [background_palette, foreground_palette] - end - def downscaled_image image = Vips::Image.new_from_file(@file.path, access: :random).thumbnail_image(100) diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index a712cdde1d..cbabb1d0dd 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -219,9 +219,7 @@ RSpec.describe MediaAttachment, :attachment_processing do describe 'ogg with cover art' do let(:media) { Fabricate(:media_attachment, file: attachment_fixture('boop.ogg')) } let(:expected_media_duration) { 0.235102 } - - # The libvips and ImageMagick implementations produce different results - let(:expected_background_color) { Rails.configuration.x.use_vips ? '#268cd9' : '#3088d4' } + let(:expected_background_color) { '#268cd9' } it 'sets correct file metadata' do expect(media) diff --git a/spec/models/preview_card_spec.rb b/spec/models/preview_card_spec.rb index 0fe76c37b0..bac29046ea 100644 --- a/spec/models/preview_card_spec.rb +++ b/spec/models/preview_card_spec.rb @@ -3,12 +3,6 @@ require 'rails_helper' RSpec.describe PreviewCard do - describe 'file size limit', :attachment_processing do - it 'is set differently whether vips is enabled or not' do - expect(described_class::LIMIT).to eq(Rails.configuration.x.use_vips ? 8.megabytes : 2.megabytes) - end - end - describe 'Validations' do describe 'url' do it { is_expected.to allow_values('http://example.host/path', 'https://example.host/path').for(:url) } diff --git a/spec/requests/api/v1/media_spec.rb b/spec/requests/api/v1/media_spec.rb index 347ff4b279..89e34998db 100644 --- a/spec/requests/api/v1/media_spec.rb +++ b/spec/requests/api/v1/media_spec.rb @@ -102,7 +102,7 @@ RSpec.describe 'Media' do allow(user.account).to receive(:media_attachments).and_return(media_attachments) end - context 'when imagemagick cannot identify the file type' do + context 'when file type cannot be identified' do it 'returns http unprocessable entity' do allow(media_attachments).to receive(:create!).and_raise(Paperclip::Errors::NotIdentifiedByImageMagickError) diff --git a/spec/requests/api/v2/media_spec.rb b/spec/requests/api/v2/media_spec.rb index 04e48bc02c..1f149b9634 100644 --- a/spec/requests/api/v2/media_spec.rb +++ b/spec/requests/api/v2/media_spec.rb @@ -71,7 +71,7 @@ RSpec.describe 'Media API', :attachment_processing do allow(user.account).to receive(:media_attachments).and_return(media_attachments) end - context 'when imagemagick cannot identify the file type' do + context 'when file type cannot be identified' do before do allow(media_attachments).to receive(:create!).and_raise(Paperclip::Errors::NotIdentifiedByImageMagickError) end From 3806d15d99c468394f56494fcf1d3a152e23f780 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:00:53 +0100 Subject: [PATCH 08/24] Update dependency pg to v8.17.2 (#37557) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 15f3d5048d..eaf9fb1eda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10511,7 +10511,7 @@ __metadata: languageName: node linkType: hard -"pg-connection-string@npm:^2.10.0, pg-connection-string@npm:^2.6.0": +"pg-connection-string@npm:^2.10.1, pg-connection-string@npm:^2.6.0": version: 2.10.1 resolution: "pg-connection-string@npm:2.10.1" checksum: 10c0/f218a72b59c661022caca9a7f2116655632b1d7e7d6dc9a8ee9f238744e0927e0d6f44e12f50d9767c6d9cd47d9b3766aa054b77504b15c6bf503400530e053e @@ -10555,11 +10555,11 @@ __metadata: linkType: hard "pg@npm:^8.5.0": - version: 8.17.1 - resolution: "pg@npm:8.17.1" + version: 8.17.2 + resolution: "pg@npm:8.17.2" dependencies: pg-cloudflare: "npm:^1.3.0" - pg-connection-string: "npm:^2.10.0" + pg-connection-string: "npm:^2.10.1" pg-pool: "npm:^3.11.0" pg-protocol: "npm:^1.11.0" pg-types: "npm:2.2.0" @@ -10572,7 +10572,7 @@ __metadata: peerDependenciesMeta: pg-native: optional: true - checksum: 10c0/39a92391adfc73f793d195b4062bc2d21aa3537073e3973f3979d72901d92a59e129f31f42577ff916038a6c3f9fe423b6024717529609ae8548fda21248cfe7 + checksum: 10c0/74b022587f92953f498dba747ccf9c7c90767af70326595d30c7ab0e2f00b2b468226c8abae54caef63ab444a8ac6f1597d859174386c7ba7c318c225d711c5f languageName: node linkType: hard From 1809048105a7e04095f6a0d1d466adeb0fdcffa5 Mon Sep 17 00:00:00 2001 From: Shlee Date: Fri, 23 Jan 2026 02:31:44 +1030 Subject: [PATCH 09/24] Safefy: Updated Admin::AccountDeletionWorker to match AccountDeletionWorker (#37577) --- app/workers/admin/account_deletion_worker.rb | 5 ++++- spec/workers/admin/account_deletion_worker_spec.rb | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/workers/admin/account_deletion_worker.rb b/app/workers/admin/account_deletion_worker.rb index 5dfdfb6e73..cd0d9c1f5e 100644 --- a/app/workers/admin/account_deletion_worker.rb +++ b/app/workers/admin/account_deletion_worker.rb @@ -6,7 +6,10 @@ class Admin::AccountDeletionWorker sidekiq_options queue: 'pull', lock: :until_executed, lock_ttl: 1.week.to_i def perform(account_id) - DeleteAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: true) + delete_account = Account.find(account_id) + return unless delete_account.unavailable? + + DeleteAccountService.new.call(delete_account, reserve_username: true, reserve_email: true) rescue ActiveRecord::RecordNotFound true end diff --git a/spec/workers/admin/account_deletion_worker_spec.rb b/spec/workers/admin/account_deletion_worker_spec.rb index e41b734f21..7fc56e4289 100644 --- a/spec/workers/admin/account_deletion_worker_spec.rb +++ b/spec/workers/admin/account_deletion_worker_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Admin::AccountDeletionWorker do let(:worker) { described_class.new } describe 'perform' do - let(:account) { Fabricate(:account) } + let(:account) { Fabricate(:account, suspended: true) } let(:service) { instance_double(DeleteAccountService, call: true) } it 'calls delete account service' do From 0924171c0f108f59c858c73362367f0f13cccc08 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Thu, 22 Jan 2026 17:08:57 +0100 Subject: [PATCH 10/24] Add form field components: `TextInputField`, `TextAreaField`, `SelectField` (#37578) --- .../mastodon/components/form_fields/index.ts | 3 + .../form_fields/select_field.stories.tsx | 55 ++++++++++ .../components/form_fields/select_field.tsx | 38 +++++++ .../form_fields/text_area_field.stories.tsx | 45 ++++++++ .../form_fields/text_area_field.tsx | 31 ++++++ .../form_fields/text_input_field.stories.tsx | 45 ++++++++ .../form_fields/text_input_field.tsx | 36 +++++++ .../components/form_fields/wrapper.tsx | 100 ++++++++++++++++++ .../mastodon/features/lists/new.tsx | 100 +++++++----------- .../mastodon/features/onboarding/profile.tsx | 60 ++++------- app/javascript/mastodon/locales/en.json | 1 + 11 files changed, 418 insertions(+), 96 deletions(-) create mode 100644 app/javascript/mastodon/components/form_fields/index.ts create mode 100644 app/javascript/mastodon/components/form_fields/select_field.stories.tsx create mode 100644 app/javascript/mastodon/components/form_fields/select_field.tsx create mode 100644 app/javascript/mastodon/components/form_fields/text_area_field.stories.tsx create mode 100644 app/javascript/mastodon/components/form_fields/text_area_field.tsx create mode 100644 app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx create mode 100644 app/javascript/mastodon/components/form_fields/text_input_field.tsx create mode 100644 app/javascript/mastodon/components/form_fields/wrapper.tsx diff --git a/app/javascript/mastodon/components/form_fields/index.ts b/app/javascript/mastodon/components/form_fields/index.ts new file mode 100644 index 0000000000..2aa8764514 --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/index.ts @@ -0,0 +1,3 @@ +export { TextInputField } from './text_input_field'; +export { TextAreaField } from './text_area_field'; +export { SelectField } from './select_field'; diff --git a/app/javascript/mastodon/components/form_fields/select_field.stories.tsx b/app/javascript/mastodon/components/form_fields/select_field.stories.tsx new file mode 100644 index 0000000000..30897adda1 --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/select_field.stories.tsx @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { SelectField } from './select_field'; + +const meta = { + title: 'Components/Form Fields/SelectField', + component: SelectField, + args: { + label: 'Fruit preference', + hint: 'Select your favourite fruit or not. Up to you.', + }, + render(args) { + // Component styles require a wrapper class at the moment + return ( +
+ + + + + + + + + + + +
+ ); + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Simple: Story = {}; + +export const Required: Story = { + args: { + required: true, + }, +}; + +export const Optional: Story = { + args: { + required: false, + }, +}; + +export const WithError: Story = { + args: { + required: false, + hasError: true, + }, +}; diff --git a/app/javascript/mastodon/components/form_fields/select_field.tsx b/app/javascript/mastodon/components/form_fields/select_field.tsx new file mode 100644 index 0000000000..aa058fc782 --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/select_field.tsx @@ -0,0 +1,38 @@ +import type { ComponentPropsWithoutRef } from 'react'; +import { forwardRef } from 'react'; + +import { FormFieldWrapper } from './wrapper'; +import type { CommonFieldWrapperProps } from './wrapper'; + +interface Props + extends ComponentPropsWithoutRef<'select'>, CommonFieldWrapperProps {} + +/** + * A simple form field for single-item selections. + * Provide selectable items via nested `