diff --git a/.nvmrc b/.nvmrc
index 403f75d03823be..f666621e50063d 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-22.20
+24.10
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bebb2a3b15834b..f634505428b5c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,11 +6,12 @@ All notable changes to this project will be documented in this file.
### Added
-- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, and #36461 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
+- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, #36461, #36516 and #36528 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
This includes a revamp of the composer interface.\
See https://blog.joinmastodon.org/2025/09/introducing-quote-posts/ for a user-centric overview of the feature, and https://docs.joinmastodon.org/client/quotes/ for API documentation.
-- **Add support for fetching and refreshing replies to the web UI** (#35210, #35496, #35575, #35500, #35577, #35602, #35603, #35654, #36141, #36237, #36172, #36256, #36271, #36334, #36382, and #36239 by @ClearlyClaire, @Gargron, and @diondiondion)
+- **Add support for fetching and refreshing replies to the web UI** (#35210, #35496, #35575, #35500, #35577, #35602, #35603, #35654, #36141, #36237, #36172, #36256, #36271, #36334, #36382, #36239, #36484 and #36481 by @ClearlyClaire, @Gargron, and @diondiondion)
- **Add ability to block words in usernames** (#35407, #35655, and #35806 by @ClearlyClaire and @Gargron)
+- Add support for displaying of quote posts in Moderator UI (#35964 by @ThisIsMissEm)
- Add support for displaying link previews for Admin UI (#35958 by @ThisIsMissEm)
- Add support for dynamic viewport height (#36272 by @e1berd)
- Add support for numeric-based URIs for new local accounts (#32724, #36304, #36316, and #36365 by @ClearlyClaire)
@@ -26,18 +27,20 @@ All notable changes to this project will be documented in this file.
- Add delivery failure tracking and handling to FASP jobs (#35625, #35628, and #35723 by @oneiros)
- Add example of quote post with a preview card to development sample data (#35616 by @ClearlyClaire)
- Add second set of blocked text that applies to accounts regardless of account age for spam-blocking (#35563 by @ClearlyClaire)
-- Add experimental feature to select custom emoji rendering (#35229, #35282, #35253, #35424, #35473, #35483, #35505, #35568, #35605, #35659, #35664, #35739, #35985, #36051, #36071, #36137, #36165, #36248, #36262, #36275, #36293, #36341, #36342, #36366, #36377, #36378, #36385, #36393, #36397, #36403, #36413, #36410, #36454, and #36402 by @ChaosExAnima and @braddunbar)\
+- Added emoji from Twemoji v16 (#36501 and #36530 by @ChaosExAnima)
+- Add experimental feature to select custom emoji rendering (#35229, #35282, #35253, #35424, #35473, #35483, #35505, #35568, #35605, #35659, #35664, #35739, #35985, #36051, #36071, #36137, #36165, #36248, #36262, #36275, #36293, #36341, #36342, #36366, #36377, #36378, #36385, #36393, #36397, #36403, #36413, #36410, #36454, #36402, #36503, #36502 and #36532 by @ChaosExAnima and @braddunbar)\
This also completely reworks the processing and rendering of emojis and server-rendered HTML in statuses and other places.
### Changed
- Change confirmation dialogs for follow button actions “unfollow”, “unblock”, and “withdraw request” (#36289 by @diondiondion)
- Change “Follow” button labels (#36264 by @diondiondion)
+- Change appearance settings to introduce new Advanced settings section (#36496 and #36506 by @diondiondion)
- Change display of content warnings in Admin UI (#35935 by @ThisIsMissEm)
- Change index on `follows` table to improve performance of some queries (#36374 by @ClearlyClaire)
- Change links to accounts in settings and moderation views to link to local view unless account is suspended (#36340 by @diondiondion)
- Change redirection for denied registration from web app to sign-in page with error message (#36384 by @ClearlyClaire)
-- Change `timeline_preview` setting into four more granular settings (#36338 and #36467 by @ClearlyClaire)
+- Change `timeline_preview` setting into four more granular settings (#36338, #36467 and #36497 by @ClearlyClaire)
- Change wording and design of interaction dialog to simplify it (#36124 by @diondiondion)
- Change dropdown menus to allow disabled items to be focused (#36078 by @diondiondion)
- Change modal background colours in light mode (#36069 by @diondiondion)
@@ -49,9 +52,11 @@ All notable changes to this project will be documented in this file.
- Change auditable accounts to be sorted by username in admin action logs interface (#35272 by @breadtk)
- Change order of translation restoration and service credit on post card (#33619 by @colindean)
- Change position of ‘add more’ to be inside table toolbar on reports (#35963 by @ThisIsMissEm)
+- Change docker-compose.yml sidekiq health check to work for both 4.4 and 4.5 (#36498 by @ClearlyClaire)
### Fixed
+- Fix relationship not being fetched to evaluate whether to show a quote post (#36517 by @ClearlyClaire)
- Fix rendering of poll options in status history modal (#35633 by @ThisIsMissEm)
- Fix “mute” button being displayed to unauthenticated visitors in hashtag dropdown (#36353 by @mkljczk)
- Fix overflow handling of `.more-from-author` (#36310 by @edent)
@@ -76,6 +81,12 @@ All notable changes to this project will be documented in this file.
- Fix glitchy status keyboard navigation (#35455 and #35504 by @diondiondion)
- Fix post being submitted when pressing “Enter” in the CW field (#35445 by @diondiondion)
+## [4.4.8] - 2025-10-21
+
+### Security
+
+- Fix quote control bypass ([GHSA-8h43-rcqj-wpc6](https://github.com/mastodon/mastodon/security/advisories/GHSA-8h43-rcqj-wpc6))
+
## [4.4.7] - 2025-10-15
### Fixed
diff --git a/Dockerfile b/Dockerfile
index e457ae3623b316..1c9c956b72c92f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,9 +14,9 @@ ARG BASE_REGISTRY="docker.io"
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
# renovate: datasource=docker depName=docker.io/ruby
ARG RUBY_VERSION="3.4.7"
-# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
+# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="22"]
# renovate: datasource=node-version depName=node
-ARG NODE_MAJOR_VERSION="22"
+ARG NODE_MAJOR_VERSION="24"
# 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)
diff --git a/app/lib/activitypub/activity/quote_request.rb b/app/lib/activitypub/activity/quote_request.rb
index 27dea05bf64313..12f48ebb2b3da0 100644
--- a/app/lib/activitypub/activity/quote_request.rb
+++ b/app/lib/activitypub/activity/quote_request.rb
@@ -7,7 +7,7 @@ def perform
return if non_matching_uri_hosts?(@account.uri, @json['id'])
quoted_status = status_from_uri(object_uri)
- return if quoted_status.nil? || !quoted_status.account.local? || !quoted_status.distributable?
+ return if quoted_status.nil? || !quoted_status.account.local? || !quoted_status.distributable? || quoted_status.reblog?
if StatusPolicy.new(@account, quoted_status).quote?
accept_quote_request!(quoted_status)
diff --git a/app/models/concerns/status/interaction_policy_concern.rb b/app/models/concerns/status/interaction_policy_concern.rb
index 5f4fd086b312a0..7de73986c3f7cb 100644
--- a/app/models/concerns/status/interaction_policy_concern.rb
+++ b/app/models/concerns/status/interaction_policy_concern.rb
@@ -29,7 +29,7 @@ def quote_policy_as_keys(kind)
# Returns `:automatic`, `:manual`, `:unknown` or `:denied`
def quote_policy_for_account(other_account, preloaded_relations: {})
- return :denied if other_account.nil? || direct_visibility? || limited_visibility?
+ return :denied if other_account.nil? || direct_visibility? || limited_visibility? || reblog?
following_author = nil
followed_by_author = nil
diff --git a/app/models/quote.rb b/app/models/quote.rb
index 0d24cb239a8fd4..e81d427089da8e 100644
--- a/app/models/quote.rb
+++ b/app/models/quote.rb
@@ -39,6 +39,7 @@ class Quote < ApplicationRecord
validates :activity_uri, presence: true, if: -> { account.local? && quoted_account&.remote? }
validates :approval_uri, absence: true, if: -> { quoted_account&.local? }
validate :validate_visibility
+ validate :validate_original_quoted_status
after_create_commit :increment_counter_caches!
after_destroy_commit :decrement_counter_caches!
@@ -85,6 +86,10 @@ def validate_visibility
errors.add(:quoted_status_id, :visibility_mismatch)
end
+ def validate_original_quoted_status
+ errors.add(:quoted_status_id, :reblog_unallowed) if quoted_status&.reblog?
+ end
+
def set_activity_uri
self.activity_uri = [ActivityPub::TagManager.instance.uri_for(account), '/quote_requests/', SecureRandom.uuid].join
end
diff --git a/app/serializers/rest/base_quote_serializer.rb b/app/serializers/rest/base_quote_serializer.rb
index be9d5cbe6f238e..2637014b697316 100644
--- a/app/serializers/rest/base_quote_serializer.rb
+++ b/app/serializers/rest/base_quote_serializer.rb
@@ -14,7 +14,7 @@ def state
end
def quoted_status
- object.quoted_status if object.accepted? && object.quoted_status.present? && !status_filter.filtered_for_quote?
+ object.quoted_status if object.accepted? && object.quoted_status.present? && !object.quoted_status&.reblog? && !status_filter.filtered_for_quote?
end
private
diff --git a/app/serializers/rest/scheduled_status_serializer.rb b/app/serializers/rest/scheduled_status_serializer.rb
index 7c54f39c0d137d..71ddb7b3e1707b 100644
--- a/app/serializers/rest/scheduled_status_serializer.rb
+++ b/app/serializers/rest/scheduled_status_serializer.rb
@@ -8,4 +8,11 @@ class REST::ScheduledStatusSerializer < ActiveModel::Serializer
def id
object.id.to_s
end
+
+ def params
+ object.params.merge(
+ quoted_status_id: object.params['quoted_status_id']&.to_s,
+ quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS.keys.find { |key| object.params['quote_approval_policy']&.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[key] << 16) }&.to_s || 'nobody'
+ )
+ end
end
diff --git a/app/services/activitypub/verify_quote_service.rb b/app/services/activitypub/verify_quote_service.rb
index 5ed516cde87d75..6e0a225fde0b90 100644
--- a/app/services/activitypub/verify_quote_service.rb
+++ b/app/services/activitypub/verify_quote_service.rb
@@ -81,7 +81,7 @@ def fetch_quoted_post_if_needed!(uri, prefetched_body: nil)
status ||= ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: @quote.account.followers.local.first, prefetched_body:, request_id: @request_id, depth: @depth + 1)
- @quote.update(quoted_status: status) if status.present?
+ @quote.update(quoted_status: status) if status.present? && !status.reblog?
rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e
@fetching_error = e
end
@@ -99,7 +99,7 @@ def import_quoted_post_if_needed!(uri)
status = ActivityPub::FetchRemoteStatusService.new.call(object['id'], prefetched_body: object, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id, depth: @depth)
- if status.present?
+ if status.present? && !status.reblog?
@quote.update(quoted_status: status)
true
else
diff --git a/app/workers/publish_scheduled_status_worker.rb b/app/workers/publish_scheduled_status_worker.rb
index bcf20b49431639..0e34aa77916c90 100644
--- a/app/workers/publish_scheduled_status_worker.rb
+++ b/app/workers/publish_scheduled_status_worker.rb
@@ -23,6 +23,7 @@ def options_with_objects(options)
options.tap do |options_hash|
options_hash[:application] = Doorkeeper::Application.find(options_hash.delete(:application_id)) if options[:application_id]
options_hash[:thread] = Status.find(options_hash.delete(:in_reply_to_id)) if options_hash[:in_reply_to_id]
+ options_hash[:quoted_status] = Status.find(options_hash.delete(:quoted_status_id)) if options_hash[:quoted_status_id]
end
end
end
diff --git a/config/initializers/strong_migrations.rb b/config/initializers/strong_migrations.rb
index 59053ca18738ee..d722b16a0f4ffc 100644
--- a/config/initializers/strong_migrations.rb
+++ b/config/initializers/strong_migrations.rb
@@ -1,4 +1,4 @@
# frozen_string_literal: true
StrongMigrations.start_after = 2017_09_24_022025
-StrongMigrations.target_version = 13
+StrongMigrations.target_version = 14
diff --git a/config/locales/simple_form.be.yml b/config/locales/simple_form.be.yml
index 9ee1b3873c9a06..cee48c7dedcfcb 100644
--- a/config/locales/simple_form.be.yml
+++ b/config/locales/simple_form.be.yml
@@ -65,6 +65,7 @@ be:
setting_display_media_hide_all: Заўсёды хаваць медыя
setting_display_media_show_all: Заўсёды паказваць медыя
setting_emoji_style: Як паказваць эмодзі. "Аўтаматычны" будзе намагацца выкарыстоўваць мясцовыя эмодзі, але для састарэлых браўзераў — Twemoji.
+ setting_quick_boosting_html: Калі ўключана, клік па %{boost_icon} іконцы пашырэння адразу пашырыць допіс, без адкрыцця меню "пашырыць/цытаваць". Перасоўвае дзеянне цытавання ў меню %{options_icon} (выбару).
setting_system_scrollbars_ui: Працуе толькі ў камп'ютарных браўзерах на аснове Safari і Chrome
setting_use_blurhash: Градыенты заснаваны на колерах схаваных выяў, але размываюць дэталі
setting_use_pending_items: Схаваць абнаўленні стужкі за клікам замест аўтаматычнага пракручвання стужкі
@@ -254,6 +255,7 @@ be:
setting_expand_spoilers: Заўжды разгортваць допісы з папярэджаннем аб змесціве
setting_hide_network: Схаваць вашы сувязі
setting_missing_alt_text_modal: Папярэджваць перад публікацыяй допісу без альтэрнатыўнага тэксту
+ setting_quick_boosting: Уключыць хуткае пашырэнне
setting_reduce_motion: Памяншэнне руху ў анімацыях
setting_system_font_ui: Выкарыстоўваць прадвызначаны сістэмны шрыфт
setting_system_scrollbars_ui: Паказваць паласу пракручвання па змаўчанні
diff --git a/config/locales/simple_form.nn.yml b/config/locales/simple_form.nn.yml
index 889f59dbed4ce9..a536fdf99b4fb5 100644
--- a/config/locales/simple_form.nn.yml
+++ b/config/locales/simple_form.nn.yml
@@ -65,6 +65,7 @@ nn:
setting_display_media_hide_all: Alltid skjul alt media
setting_display_media_show_all: Vis alltid media
setting_emoji_style: Korleis du skal visa smilefjes. «Auto» prøver å visa innebygde smilefjes, men bruker Twemoji som reserveløysing for eldre nettlesarar.
+ setting_quick_boosting_html: Når dette er skrudd på og du klikkar på %{boost_icon} framhev-ikonet, vil du framheva innlegget med ein gong i staden for å opna framhev/siter-menyen. Du finn siteringa i %{options_icon} (Val)-menyen.
setting_system_scrollbars_ui: Gjeld berre skrivebordsnettlesarar som er bygde på Safari og Chrome
setting_use_blurhash: Overgangar er basert på fargane til skjulte grafikkelement, men gjer detaljar utydelege
setting_use_pending_items: Gøym tidslineoppdateringar bak eit klikk, i staden for å rulla ned automatisk
@@ -252,6 +253,7 @@ nn:
setting_expand_spoilers: Vid alltid ut tut som er merka med innhaldsåtvaringar
setting_hide_network: Gøym nettverket ditt
setting_missing_alt_text_modal: Åtvar meg før eg legg ut media utan alternativ tekst
+ setting_quick_boosting: Skru på rask framheving
setting_reduce_motion: Minsk rørsle i animasjonar
setting_system_font_ui: Bruk standardskrifttypen på systemet
setting_system_scrollbars_ui: Bruk standardrullefeltet til systemet
diff --git a/config/locales/simple_form.tr.yml b/config/locales/simple_form.tr.yml
index 34618a3193e729..6809bb3f509cd4 100644
--- a/config/locales/simple_form.tr.yml
+++ b/config/locales/simple_form.tr.yml
@@ -65,6 +65,7 @@ tr:
setting_display_media_hide_all: Medyayı her zaman gizle
setting_display_media_show_all: Medyayı her zaman göster
setting_emoji_style: Emojiler nasıl görüntülensin. "Otomatik" seçeneği yerel emojileri kullanmaya çalışır, ancak eski tarayıcılar için Twemoji'yi kullanır.
+ setting_quick_boosting_html: Etkinleştirildiğinde, %{boost_icon} Öne Çıkar simgesine tıklandığında, öne çıkar/alıntı açılır menüsünü görüntüleme yerine hemen öne çıkarma işlemi gerçekleştirilir. Alıntı işlevi %{options_icon} (Seçenekler) menüsüne taşınır.
setting_system_scrollbars_ui: Yalnızca Safari ve Chrome tabanlı masaüstü tarayıcılar için geçerlidir
setting_use_blurhash: Gradyenler gizli görsellerin renklerine dayanır, ancak detayları gizler
setting_use_pending_items: Akışı otomatik olarak kaydırmak yerine, zaman çizelgesi güncellemelerini tek bir tıklamayla gizleyin
@@ -252,6 +253,7 @@ tr:
setting_expand_spoilers: İçerik uyarılarıyla işaretli gönderileri her zaman genişlet
setting_hide_network: Sosyal grafiğini gizle
setting_missing_alt_text_modal: Alternatif metni olmayan bir medya göndermeden önce beni uyar
+ setting_quick_boosting: Hızlı öne çıkarmayı etkinleştir
setting_reduce_motion: Animasyonlarda hareketi azalt
setting_system_font_ui: Sistemin varsayılan yazı tipini kullan
setting_system_scrollbars_ui: Sistemin varsayılan kaydırma çubuğunu kullan
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index f6438918887a32..8f2894e0c15310 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -1191,6 +1191,7 @@ tr:
advanced_settings: Gelişmiş ayarlar
animations_and_accessibility: Animasyonlar ve erişilebilirlik
boosting_preferences: Öne çıkarma seçenekleri
+ boosting_preferences_info_html: "İpucu: Ayarlardan bağımsız olarak, %{icon} Öne Çıkar simgesine Shift + Tıklama uygulandığından öne çıkarma hemen gerçekleştirilir."
discovery: Keşfet
localization:
body: Mastodon, gönüllüler tarafından çevrilmektedir.
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index dc2ee319229916..8ae82f4d07953d 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -35,7 +35,7 @@ def patch
end
def default_prerelease
- 'beta.1'
+ 'beta.2'
end
def prerelease
diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake
index 054f8b0177b641..e5ca6ac2ac5b1b 100644
--- a/lib/tasks/db.rake
+++ b/lib/tasks/db.rake
@@ -63,7 +63,7 @@ namespace :db do
task pre_migration_check: :environment do
pg_version = ActiveRecord::Base.connection.database_version
- abort 'This version of Mastodon requires PostgreSQL 13.0 or newer. Please update PostgreSQL before updating Mastodon.' if pg_version < 130_000
+ abort 'This version of Mastodon requires PostgreSQL 14.0 or newer. Please update PostgreSQL before updating Mastodon.' if pg_version < 140_000
schema_version = ActiveRecord::Migrator.current_version
abort <<~MESSAGE if ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS'] && schema_version < 2023_09_07_150100
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index 999dff3ad1d095..4fb308c8399d4a 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -1879,6 +1879,60 @@ def activity_for_object(json)
end
end
+ context 'with a quote of a known reblog that is otherwise valid' do
+ let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
+ let(:quoted_status) { Fabricate(:status, account: quoted_account, reblog: Fabricate(:status)) }
+ let(:approval_uri) { 'https://quoted.example.com/quote-approval' }
+
+ let(:object_json) do
+ build_object(
+ type: 'Note',
+ content: 'woah what she said is amazing',
+ quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
+ quoteAuthorization: approval_uri
+ )
+ end
+
+ before do
+ stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
+ '@context': [
+ 'https://www.w3.org/ns/activitystreams',
+ {
+ QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization',
+ gts: 'https://gotosocial.org/ns#',
+ interactionPolicy: {
+ '@id': 'gts:interactionPolicy',
+ '@type': '@id',
+ },
+ interactingObject: {
+ '@id': 'gts:interactingObject',
+ '@type': '@id',
+ },
+ interactionTarget: {
+ '@id': 'gts:interactionTarget',
+ '@type': '@id',
+ },
+ },
+ ],
+ type: 'QuoteAuthorization',
+ id: approval_uri,
+ attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account),
+ interactingObject: object_json[:id],
+ interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status),
+ }))
+ end
+
+ it 'creates a status without the verified quote' do
+ expect { subject.perform }.to change(sender.statuses, :count).by(1)
+
+ status = sender.statuses.first
+ expect(status).to_not be_nil
+ expect(status.quote).to_not be_nil
+ expect(status.quote.state).to_not eq 'accepted'
+ expect(status.quote.quoted_status).to be_nil
+ end
+ end
+
context 'when a vote to a local poll' do
let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) }
let!(:local_status) { Fabricate(:status, poll: poll) }
diff --git a/spec/models/concerns/status/interaction_policy_concern_spec.rb b/spec/models/concerns/status/interaction_policy_concern_spec.rb
index b59a1186d9bd62..ebc261fc76d5f8 100644
--- a/spec/models/concerns/status/interaction_policy_concern_spec.rb
+++ b/spec/models/concerns/status/interaction_policy_concern_spec.rb
@@ -15,6 +15,22 @@
describe '#quote_policy_for_account' do
let(:account) { Fabricate(:account) }
+ context 'when the account is the author' do
+ let(:status) { Fabricate(:status, account: account, quote_approval_policy: 0) }
+
+ it 'returns :automatic' do
+ expect(status.quote_policy_for_account(account)).to eq :automatic
+ end
+
+ context 'when it is a reblog' do
+ let(:status) { Fabricate(:status, account: account, quote_approval_policy: 0, reblog: Fabricate(:status)) }
+
+ it 'returns :automatic' do
+ expect(status.quote_policy_for_account(account)).to eq :denied
+ end
+ end
+ end
+
context 'when the account is not following the user' do
it 'returns :manual because of the public entry in the manual policy' do
expect(status.quote_policy_for_account(account)).to eq :manual
diff --git a/spec/requests/api/v1/accounts/credentials_spec.rb b/spec/requests/api/v1/accounts/credentials_spec.rb
index 84bea97e800c7d..4316c1409d0574 100644
--- a/spec/requests/api/v1/accounts/credentials_spec.rb
+++ b/spec/requests/api/v1/accounts/credentials_spec.rb
@@ -91,6 +91,11 @@
expect(response).to have_http_status(422)
expect(response.content_type)
.to start_with('application/json')
+ expect(response.parsed_body)
+ .to include(
+ error: /Validation failed/,
+ details: include(note: contain_exactly(include(error: 'ERR_TOO_LONG', description: /character limit/)))
+ )
end
end
diff --git a/spec/requests/api/v1/accounts_spec.rb b/spec/requests/api/v1/accounts_spec.rb
index 4604f8ccb8533b..2412181cfbf164 100644
--- a/spec/requests/api/v1/accounts_spec.rb
+++ b/spec/requests/api/v1/accounts_spec.rb
@@ -118,6 +118,11 @@
.to have_http_status(422)
expect(response.content_type)
.to start_with('application/json')
+ expect(response.parsed_body)
+ .to include(
+ error: /Validation failed/,
+ details: include(date_of_birth: contain_exactly(include(error: 'ERR_BELOW_LIMIT', description: /below the age limit/)))
+ )
end
end
diff --git a/spec/serializers/rest/scheduled_status_serializer_spec.rb b/spec/serializers/rest/scheduled_status_serializer_spec.rb
index 2cf00986542c4e..6fc2f2eca9cd67 100644
--- a/spec/serializers/rest/scheduled_status_serializer_spec.rb
+++ b/spec/serializers/rest/scheduled_status_serializer_spec.rb
@@ -10,14 +10,18 @@
)
end
- let(:scheduled_status) { Fabricate.build(:scheduled_status, scheduled_at: 4.minutes.from_now, params: { application_id: 123 }) }
+ let(:scheduled_status) { Fabricate.build(:scheduled_status, scheduled_at: 4.minutes.from_now, params: { application_id: 123, quoted_status_id: 456, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16 }) }
describe 'serialization' do
it 'returns expected values and removes application_id from params' do
expect(subject.deep_symbolize_keys)
.to include(
scheduled_at: be_a(String).and(match_api_datetime_format),
- params: include(:application_id)
+ params: a_hash_including(
+ application_id: 123,
+ quoted_status_id: '456',
+ quote_approval_policy: 'public'
+ )
)
end
end
diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb
index 39990aa2c6df48..aa0c714aa68c83 100644
--- a/spec/services/activitypub/process_status_update_service_spec.rb
+++ b/spec/services/activitypub/process_status_update_service_spec.rb
@@ -1130,6 +1130,72 @@
end
end
+ context 'when the status adds a verifiable quote of a reblog through an explicit update' do
+ let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
+ let(:quoted_status) { Fabricate(:status, account: quoted_account, reblog: Fabricate(:status)) }
+ let(:approval_uri) { 'https://quoted.example.com/approvals/1' }
+
+ let(:payload) do
+ {
+ '@context': [
+ 'https://www.w3.org/ns/activitystreams',
+ {
+ '@id': 'https://w3id.org/fep/044f#quote',
+ '@type': '@id',
+ },
+ {
+ '@id': 'https://w3id.org/fep/044f#quoteAuthorization',
+ '@type': '@id',
+ },
+ ],
+ id: 'foo',
+ type: 'Note',
+ summary: 'Show more',
+ content: 'Hello universe',
+ updated: '2021-09-08T22:39:25Z',
+ quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
+ quoteAuthorization: approval_uri,
+ }
+ end
+
+ before do
+ stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
+ '@context': [
+ 'https://www.w3.org/ns/activitystreams',
+ {
+ QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization',
+ gts: 'https://gotosocial.org/ns#',
+ interactionPolicy: {
+ '@id': 'gts:interactionPolicy',
+ '@type': '@id',
+ },
+ interactingObject: {
+ '@id': 'gts:interactingObject',
+ '@type': '@id',
+ },
+ interactionTarget: {
+ '@id': 'gts:interactionTarget',
+ '@type': '@id',
+ },
+ },
+ ],
+ type: 'QuoteAuthorization',
+ id: approval_uri,
+ attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account),
+ interactingObject: ActivityPub::TagManager.instance.uri_for(status),
+ interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status),
+ }))
+ end
+
+ it 'updates the approval URI but does not verify the quote' do
+ expect { subject.call(status, json, json) }
+ .to change(status, :quote).from(nil)
+ expect(status.quote.approval_uri).to eq approval_uri
+ expect(status.quote.state).to_not eq 'accepted'
+ expect(status.quote.quoted_status).to be_nil
+ end
+ end
+
context 'when the status adds a unverifiable quote through an implicit update' do
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
diff --git a/spec/workers/publish_scheduled_status_worker_spec.rb b/spec/workers/publish_scheduled_status_worker_spec.rb
index a91e66596582c5..81480f7e54cb5a 100644
--- a/spec/workers/publish_scheduled_status_worker_spec.rb
+++ b/spec/workers/publish_scheduled_status_worker_spec.rb
@@ -13,8 +13,12 @@
end
context 'when the account is not disabled' do
+ let(:user) { Fabricate(:user) }
+ let(:scheduled_status) { Fabricate(:scheduled_status, account: user.account, params: { text: 'Hello world, future!', quoted_status_id: Fabricate(:status, account: user.account).id }) }
+
it 'creates a status and removes scheduled record' do
expect(scheduled_status.account.statuses.first.text).to eq 'Hello world, future!'
+ expect(scheduled_status.account.statuses.first.quote).to_not be_nil
expect(ScheduledStatus.find_by(id: scheduled_status.id)).to be_nil
end
diff --git a/streaming/Dockerfile b/streaming/Dockerfile
index 679425dfcc2944..3a12007f68b19b 100644
--- a/streaming/Dockerfile
+++ b/streaming/Dockerfile
@@ -8,9 +8,9 @@ ARG TARGETPLATFORM=${TARGETPLATFORM}
ARG BUILDPLATFORM=${BUILDPLATFORM}
ARG BASE_REGISTRY="docker.io"
-# Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
+# Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="22"]
# renovate: datasource=node-version depName=node
-ARG NODE_MAJOR_VERSION="22"
+ARG NODE_MAJOR_VERSION="24"
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="trixie"]
ARG DEBIAN_VERSION="trixie"
# Node image to use for base image based on combined variables (ex: 20-trixie-slim)
@@ -32,20 +32,20 @@ ARG GID="991"
# Apply Mastodon build options based on options above
ENV \
-# Apply Mastodon version information
+ # Apply Mastodon version information
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \
-# Apply timezone
+ # Apply timezone
TZ=${TZ}
ENV \
-# Configure the IP to bind Mastodon to when serving traffic
+ # Configure the IP to bind Mastodon to when serving traffic
BIND="0.0.0.0" \
-# Explicitly set PORT to match the exposed port
+ # Explicitly set PORT to match the exposed port
PORT=4000 \
-# Use production settings for Yarn, Node and related nodejs based tools
+ # Use production settings for Yarn, Node and related nodejs based tools
NODE_ENV="production" \
-# Add Ruby and Mastodon installation to the PATH
+ # Add Ruby and Mastodon installation to the PATH
DEBIAN_FRONTEND="noninteractive"
# Set default shell used for running commands
@@ -56,29 +56,29 @@ ARG TARGETPLATFORM
RUN echo "Target platform is ${TARGETPLATFORM}"
RUN \
-# Remove automatic apt cache Docker cleanup scripts
+ # Remove automatic apt cache Docker cleanup scripts
rm -f /etc/apt/apt.conf.d/docker-clean; \
-# Sets timezone
+ # Sets timezone
echo "${TZ}" > /etc/localtime; \
-# Creates mastodon user/group and sets home directory
+ # Creates mastodon user/group and sets home directory
groupadd -g "${GID}" mastodon; \
useradd -l -u "${UID}" -g "${GID}" -m -d /opt/mastodon mastodon; \
-# Creates symlink for /mastodon folder
+ # Creates symlink for /mastodon folder
ln -s /opt/mastodon /mastodon;
# hadolint ignore=DL3008,DL3005
RUN \
-# Mount Apt cache and lib directories from Docker buildx caches
---mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
---mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
-# Upgrade to check for security updates to Debian image
+ # Mount Apt cache and lib directories from Docker buildx caches
+ --mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \
+ --mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
+ # Upgrade to check for security updates to Debian image
apt-get update; \
apt-get dist-upgrade -yq; \
apt-get install -y --no-install-recommends \
- ca-certificates \
- curl \
- tzdata \
- wget \
+ ca-certificates \
+ curl \
+ tzdata \
+ wget \
;
# Set /opt/mastodon as working directory
@@ -91,19 +91,19 @@ COPY .yarn /opt/mastodon/.yarn
COPY ./streaming /opt/mastodon/streaming
RUN \
-# Mount local Corepack and Yarn caches from Docker buildx caches
---mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
---mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
+ # Mount local Corepack and Yarn caches from Docker buildx caches
+ --mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
+ --mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
# Configure Corepack
rm /usr/local/bin/yarn*; \
corepack enable; \
corepack prepare --activate;
RUN \
-# Mount Corepack and Yarn caches from Docker buildx caches
---mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
---mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
-# Install Node packages
+ # Mount Corepack and Yarn caches from Docker buildx caches
+ --mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
+ --mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
+ # Install Node packages
yarn workspaces focus --production @mastodon/streaming;
# Set the running user for resulting container
diff --git a/yarn.lock b/yarn.lock
index 98e18a89567946..a243be793029ae 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10424,27 +10424,27 @@ __metadata:
languageName: node
linkType: hard
-"playwright-core@npm:1.55.0":
- version: 1.55.0
- resolution: "playwright-core@npm:1.55.0"
+"playwright-core@npm:1.56.1":
+ version: 1.56.1
+ resolution: "playwright-core@npm:1.56.1"
bin:
playwright-core: cli.js
- checksum: 10c0/c39d6aa30e7a4e73965942ca5e13405ae05c9cb49f755a35f04248c864c0b24cf662d9767f1797b3ec48d1cf4e54774dce4a19c16534bd5cfd2aa3da81c9dc3a
+ checksum: 10c0/ffd40142b99c68678b387445d5b42f1fee4ab0b65d983058c37f342e5629f9cdbdac0506ea80a0dfd41a8f9f13345bad54e9a8c35826ef66dc765f4eb3db8da7
languageName: node
linkType: hard
"playwright@npm:^1.54.1":
- version: 1.55.0
- resolution: "playwright@npm:1.55.0"
+ version: 1.56.1
+ resolution: "playwright@npm:1.56.1"
dependencies:
fsevents: "npm:2.3.2"
- playwright-core: "npm:1.55.0"
+ playwright-core: "npm:1.56.1"
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
- checksum: 10c0/51605b7e57a5650e57972c5fdfc09d7a9934cca1cbee5beacca716fa801e25cb5bb7c1663de90c22b300fde884e5545a2b13a0505a93270b660687791c478304
+ checksum: 10c0/8e9965aede86df0f4722063385748498977b219630a40a10d1b82b8bd8d4d4e9b6b65ecbfa024331a30800163161aca292fb6dd7446c531a1ad25f4155625ab4
languageName: node
linkType: hard