From 42b2fdb0acc41b06a397b25ca095fa222b96b9bf Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 22 Jan 2026 13:04:15 +0100 Subject: [PATCH 01/52] 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 85fed0cbd3b..4190f3248e3 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 417e47c18fe..00000000000 --- 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 20e9889ae80..2d3801613c9 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 20e9889ae80..e9df4cdd35d 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 00000000000..a83414784d1 --- /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 00000000000..a83414784d1 --- /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/52] 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 e673faca045..65db807d187 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 4ad393e3a57..425cf630557 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/52] 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 9c14c9e85e2..15f3d5048dc 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/52] 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 3ee1f77aa0b..646162cffe4 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/52] 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 eb91d9545fe..d5a176807b2 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 89a2f78c2e1..c9adaaff397 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 244d0d126f1..ab757d39683 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 4e3ab060a04..cc6558ea693 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 03914fa6b48..8a60f1dd491 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 e2d91835ec7..bc6e67d8bff 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 b21881b10fd..e33afa53c9b 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 88c0302f860..3c33f0d2d29 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 c6dd020cdff..9f488995606 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/52] 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 1411a222f81..bec46ab52ed 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/52] 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 3aa0bbf7da4..ed8484f5b80 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 8f05812d600..316bf831b6f 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 b9dcbe59fd2..c06bc84a339 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 0a343670240..a2c0b13b146 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 e64a6b1180a..032abb75250 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 a60a289d5b0..0be46788818 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 662ee7caf78..066c42cb6cd 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 8e0e13cdb94..644be2671a9 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 90cfe47428f..4e58bd9f6c2 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 e2aa202f274..00000000000 --- a/config/imagemagick/policy.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb index 520707e59ff..2c1057e5058 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 b444c5611b0..18d35a27f18 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 09210d60ebe..cd8d2410b92 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 b4ff4a12a0e..e9ff1dd9dd8 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 62daa077952..1c9ef4bd3d6 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 a712cdde1dc..cbabb1d0dd3 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 0fe76c37b07..bac29046eac 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 347ff4b2797..89e34998db7 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 04e48bc02c3..1f149b9634d 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/52] 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 15f3d5048dc..eaf9fb1eda3 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/52] 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 5dfdfb6e73c..cd0d9c1f5e5 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 e41b734f214..7fc56e42891 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/52] 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 00000000000..2aa87645144 --- /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 00000000000..30897adda1f --- /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 00000000000..aa058fc782e --- /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 `