Compare commits

..

No commits in common. "main" and "v4.5.5-bgme" have entirely different histories.

17 changed files with 35 additions and 109 deletions

View file

@ -2,25 +2,6 @@
All notable changes to this project will be documented in this file.
## [4.5.6] - 2026-02-03
### Security
- Fix ActivityPub collection caching logic for pinned posts and featured tags not checking blocked accounts ([GHSA-ccpr-m53r-mfwr](https://github.com/mastodon/mastodon/security/advisories/GHSA-ccpr-m53r-mfwr))
### Changed
- Shorten caching of quote posts pending approval (#37570 and #37592 by @ClearlyClaire)
### Fixed
- Fix relationship cache not being cleared when handling account migrations (#37664 by @ClearlyClaire)
- Fix quote cancel button not appearing after edit then delete-and-redraft (#37066 by @PGrayCS)
- Fix followers with profile subscription (bell icon) being notified of post edits (#37646 by @ClearlyClaire)
- Fix error when encountering invalid tag in updated object (#37635 by @ClearlyClaire)
- Fix cross-server conversation tracking (#37559 by @ClearlyClaire)
- Fix recycled connections not being immediately closed (#37335 and #37674 by @ClearlyClaire and @shleeable)
## [4.5.5] - 2026-01-20
### Security

View file

@ -4,31 +4,17 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :check_authorization
before_action :set_items
before_action :set_size
before_action :set_type
def show
expires_in 3.minutes, public: public_fetch_mode?
if @unauthorized
render json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
else
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
end
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
end
private
def check_authorization
# Because in public fetch mode we cache the response, there would be no
# benefit from performing the check below, since a blocked account or domain
# would likely be served the cache from the reverse proxy anyway
@unauthorized = authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
end
def set_items
case params[:id]
when 'featured'
@ -71,7 +57,11 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
end
def for_signed_account
if @unauthorized
# Because in public fetch mode we cache the response, there would be no
# benefit from performing the check below, since a blocked account or domain
# would likely be served the cache from the reverse proxy anyway
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
[]
else
yield

View file

@ -29,7 +29,7 @@ class StatusesController < ApplicationController
end
format.json do
expires_in @status.quote&.pending? ? 5.seconds : 3.minutes, public: true if @status.distributable? && public_fetch_mode?
expires_in 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

View file

@ -145,7 +145,6 @@ class Status extends ImmutablePureComponent {
'hidden',
'unread',
'pictureInPicture',
'onQuoteCancel',
];
state = {

View file

@ -379,7 +379,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def conversation_from_uri(uri)
return nil if uri.nil?
return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri)
return ActivityPub::TagManager.instance.uri_to_resource(uri, Conversation) if ActivityPub::TagManager.instance.local_uri?(uri)
begin
Conversation.find_or_create_by!(uri: uri)

View file

@ -241,6 +241,12 @@ class ActivityPub::TagManager
!host.nil? && (::TagManager.instance.local_domain?(host) || ::TagManager.instance.web_domain?(host))
end
def uri_to_local_id(uri, param = :id)
path_params = Rails.application.routes.recognize_path(uri)
path_params[:username] = Rails.configuration.x.local_domain if path_params[:controller] == 'instance_actors'
path_params[param]
end
def uris_to_local_accounts(uris)
usernames = []
ids = []
@ -258,14 +264,6 @@ class ActivityPub::TagManager
uri_to_resource(uri, Account)
end
def uri_to_local_conversation(uri)
path_params = Rails.application.routes.recognize_path(uri)
return unless path_params[:controller] == 'activitypub/contexts'
account_id, conversation_id = path_params[:id].split('-')
Conversation.find_by(parent_account_id: account_id, id: conversation_id)
end
def uri_to_resource(uri, klass)
return if uri.nil?
@ -273,8 +271,6 @@ class ActivityPub::TagManager
case klass.name
when 'Account'
uris_to_local_accounts([uri]).first
when 'Conversation'
uri_to_local_conversation(uri)
else
StatusFinder.new(uri).status
end

View file

@ -70,7 +70,6 @@ class ConnectionPool::SharedTimedStack
if @created == @max && !@queue.empty?
throw_away_connection = @queue.pop
@tagged_queue[throw_away_connection.site].delete(throw_away_connection)
throw_away_connection.close
@create_block.call(preferred_tag)
elsif @created != @max
connection = @create_block.call(preferred_tag)

View file

@ -11,12 +11,16 @@ class OStatus::TagManager
def unique_tag_to_local_id(tag, expected_type)
return nil unless local_id?(tag)
matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag)
matches[1] unless matches.nil?
if ActivityPub::TagManager.instance.local_uri?(tag)
ActivityPub::TagManager.instance.uri_to_local_id(tag)
else
matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag)
matches[1] unless matches.nil?
end
end
def local_id?(id)
id.start_with?("tag:#{Rails.configuration.x.local_domain}")
id.start_with?("tag:#{Rails.configuration.x.local_domain}") || ActivityPub::TagManager.instance.local_uri?(id)
end
def uri_for(target)

View file

@ -47,8 +47,6 @@ class Quote < ApplicationRecord
def accept!
update!(state: :accepted)
reset_parent_cache! if attribute_previously_changed?(:state)
end
def reject!
@ -77,15 +75,6 @@ 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

View file

@ -204,11 +204,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
def update_tags!
previous_tags = @status.tags.to_a
current_tags = @status.tags = @raw_tags.flat_map do |tag|
Tag.find_or_create_by_names([tag]).filter(&:valid?)
rescue ActiveRecord::RecordInvalid
[]
end
current_tags = @status.tags = Tag.find_or_create_by_names(@raw_tags)
return unless @status.distributable?

View file

@ -53,7 +53,7 @@ class FeedInsertWorker
def notify?(filter_result)
return false if @type != :home || @status.reblog? || (@status.reply? && @status.in_reply_to_account_id != @status.account_id) ||
update? || filter_result == :filter
filter_result == :filter
Follow.find_by(account: @follower, target_account: @status.account)&.notify?
end

View file

@ -64,16 +64,6 @@ class MoveWorker
.in_batches do |follows|
ListAccount.where(follow: follows).in_batches.update_all(account_id: @target_account.id)
num_moved += follows.update_all(target_account_id: @target_account.id)
# Clear any relationship cache, since callbacks are not called
Rails.cache.delete_multi(follows.flat_map do |follow|
[
['relationship', follow.account_id, follow.target_account_id],
['relationship', follow.target_account_id, follow.account_id],
['relationship', follow.account_id, @target_account.id],
['relationship', @target_account.id, follow.account_id],
]
end)
end
num_moved

View file

@ -59,7 +59,7 @@ services:
web:
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
# build: .
image: ghcr.io/mastodon/mastodon:v4.5.6
image: ghcr.io/mastodon/mastodon:v4.5.5
restart: always
env_file: .env.production
command: bundle exec puma -C config/puma.rb
@ -83,7 +83,7 @@ services:
# build:
# dockerfile: ./streaming/Dockerfile
# context: .
image: ghcr.io/mastodon/mastodon-streaming:v4.5.6
image: ghcr.io/mastodon/mastodon-streaming:v4.5.5
restart: always
env_file: .env.production
command: node ./streaming/index.js
@ -102,7 +102,7 @@ services:
sidekiq:
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
# build: .
image: ghcr.io/mastodon/mastodon:v4.5.6
image: ghcr.io/mastodon/mastodon:v4.5.5
restart: always
env_file: .env.production
command: bundle exec sidekiq

View file

@ -13,7 +13,7 @@ module Mastodon
end
def patch
6
5
end
def default_prerelease

View file

@ -471,7 +471,7 @@ RSpec.describe ActivityPub::Activity::Create do
end
end
context 'with a reply without explicitly setting a conversation' do
context 'with a reply' do
let(:original_status) { Fabricate(:status) }
let(:object_json) do
@ -493,30 +493,6 @@ RSpec.describe ActivityPub::Activity::Create do
end
end
context 'with a reply explicitly setting a conversation' do
let(:original_status) { Fabricate(:status) }
let(:object_json) do
build_object(
inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status),
conversation: ActivityPub::TagManager.instance.uri_for(original_status.conversation),
context: ActivityPub::TagManager.instance.uri_for(original_status.conversation)
)
end
it 'creates status' do
expect { subject.perform }.to change(sender.statuses, :count).by(1)
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.thread).to eq original_status
expect(status.reply?).to be true
expect(status.in_reply_to_account).to eq original_status.account
expect(status.conversation).to eq original_status.conversation
end
end
context 'with mentions' do
let(:recipient) { Fabricate(:account) }

View file

@ -612,6 +612,14 @@ RSpec.describe ActivityPub::TagManager do
end
end
describe '#uri_to_local_id' do
let(:account) { Fabricate(:account, id_scheme: :username_ap_id) }
it 'returns the local ID' do
expect(subject.uri_to_local_id(subject.uri_for(account), :username)).to eq account.username
end
end
describe '#uris_to_local_accounts' do
it 'returns the expected local accounts' do
account = Fabricate(:account)

View file

@ -258,7 +258,6 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
tag: [
{ type: 'Hashtag', name: 'foo' },
{ type: 'Hashtag', name: 'bar' },
{ type: 'Hashtag', name: '#2024' },
],
}
end