Browse Source

Merge branch 'mastodon_4_5_5' into mastodon_4_3_0

pull/1371/head
Misty De Meo 2 months ago
parent
commit
b644d0077a
No known key found for this signature in database
GPG Key ID: 76CF846A2F674B2C
  1. 27
      CHANGELOG.md
  2. 19
      FEDERATION.md
  3. 3
      SECURITY.md
  4. 5
      app/controllers/activitypub/inboxes_controller.rb
  5. 11
      app/controllers/api/v1/statuses_controller.rb
  6. 2
      app/controllers/api/web/push_subscriptions_controller.rb
  7. 2
      app/controllers/concerns/cache_concern.rb
  8. 1
      app/javascript/mastodon/features/emoji/normalize.test.ts
  9. 6
      app/javascript/mastodon/features/emoji/normalize.ts
  10. 1
      app/javascript/styles/mastodon/admin.scss
  11. 8
      app/lib/activitypub/activity.rb
  12. 2
      app/lib/activitypub/activity/accept.rb
  13. 2
      app/lib/activitypub/activity/delete.rb
  14. 2
      app/lib/activitypub/activity/quote_request.rb
  15. 3
      app/lib/activitypub/activity/update.rb
  16. 6
      app/lib/activitypub/parser/poll_parser.rb
  17. 2
      app/lib/activitypub/tag_manager.rb
  18. 13
      app/lib/connection_pool/shared_connection_pool.rb
  19. 1
      app/lib/feed_manager.rb
  20. 6
      app/lib/signature_parser.rb
  21. 9
      app/models/account.rb
  22. 5
      app/models/custom_emoji.rb
  23. 3
      app/models/custom_filter.rb
  24. 4
      app/models/custom_filter_keyword.rb
  25. 3
      app/models/list.rb
  26. 4
      app/models/quote.rb
  27. 1
      app/services/activitypub/fetch_remote_status_service.rb
  28. 14
      app/services/activitypub/process_account_service.rb
  29. 2
      app/services/batched_remove_status_service.rb
  30. 2
      app/services/fan_out_on_write_service.rb
  31. 6
      docker-compose.yml
  32. 2
      lib/mastodon/version.rb
  33. 22
      spec/lib/activitypub/tag_manager_spec.rb
  34. 9
      spec/requests/api/v1/statuses_spec.rb
  35. 5
      spec/requests/api/web/push_subscriptions_spec.rb

27
CHANGELOG.md

@ -2,6 +2,33 @@
All notable changes to this project will be documented in this file.
## [4.5.5] - 2026-01-20
### Security
- Fix missing limits on various federated properties [GHSA-gg8q-rcg7-p79g](https://github.com/mastodon/mastodon/security/advisories/GHSA-gg8q-rcg7-p79g)
- Fix remote user suspension bypass [GHSA-5h2f-wg8j-xqwp](https://github.com/mastodon/mastodon/security/advisories/GHSA-5h2f-wg8j-xqwp)
- Fix missing length limits on some user-provided fields [GHSA-6x3w-9g92-gvf3](https://github.com/mastodon/mastodon/security/advisories/GHSA-6x3w-9g92-gvf3)
- Fix missing access check for push notification settings update [GHSA-f3q8-7vw3-69v4](https://github.com/mastodon/mastodon/security/advisories/GHSA-f3q8-7vw3-69v4)
### Changed
- Skip tombstone creation on deleting from 404 (#37533 by @ClearlyClaire)
### Fixed
- Fix potential duplicate handling of quote accept/reject/delete (#37537 by @ClearlyClaire)
- Fix `FeedManager#filter_from_home` error when handling a reblog of a deleted status (#37486 by @ClearlyClaire)
- Fix needlessly complicated SQL query in status batch removal (#37469 by @ClearlyClaire)
- Fix `quote_approval_policy` being reset to user defaults when omitted in status update (#37436 and #37474 by @mjankowski and @shleeable)
- Fix `Vary` parsing in cache control enforcement (#37426 by @MegaManSec)
- Fix missing URI scheme test in `QuoteRequest` handling (#37425 by @MegaManSec)
- Fix thread-unsafe ActivityPub activity dispatch (#37423 by @MegaManSec)
- Fix URI generation for reblogs by accounts with numerical ActivityPub identifiers (#37415 by @oneiros)
- Fix SignatureParser accepting duplicate parameters in HTTP Signature header (#37375 by @shleeable)
- Fix emoji with variant selector not being rendered properly (#37320 by @ChaosExAnima)
- Fix mobile admin sidebar displaying under batch table toolbar (#37307 by @diondiondion)
## [4.5.4] - 2026-01-07
### Security

19
FEDERATION.md

@ -52,3 +52,22 @@ Mastodon requires all `POST` requests to be signed, and MAY require `GET` reques
### Additional documentation
- [Mastodon documentation](https://docs.joinmastodon.org/)
## 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.
| Limited property | Size limit | Consequence of exceeding the limit |
| ------------------------------------------------------------- | ---------- | ---------------------------------- |
| Serialized JSON-LD | 1MB | **Activity is rejected/dropped** |
| Profile fields (actor `PropertyValue` attachments) name/value | 2047 | Field name/value is truncated |
| Number of profile fields (actor `PropertyValue` attachments) | 50 | Fields list is truncated |
| Poll options (number of `anyOf`/`oneOf` in a `Question`) | 500 | Items list is truncated |
| Account username (actor `preferredUsername`) length | 2048 | **Actor will be rejected** |
| Account display name (actor `name`) length | 2048 | Display name will be truncated |
| Account note (actor `summary`) length | 20kB | Account note will be truncated |
| Account `attributionDomains` | 256 | List will be truncated |
| Account aliases (actor `alsoKnownAs`) | 256 | List will be truncated |
| Custom emoji shortcode (`Emoji` `name`) | 2048 | Emoji will be rejected |

3
SECURITY.md

@ -18,5 +18,4 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| 4.5.x | Yes |
| 4.4.x | Yes |
| 4.3.x | Until 2026-05-06 |
| 4.2.x | Until 2026-01-08 |
| < 4.2 | No |
| < 4.3 | No |

5
app/controllers/activitypub/inboxes_controller.rb

@ -3,6 +3,7 @@
class ActivityPub::InboxesController < ActivityPub::BaseController
include JsonLdHelper
before_action :skip_large_payload
before_action :skip_unknown_actor_activity
before_action :require_actor_signature!
skip_before_action :authenticate_user!
@ -16,6 +17,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
private
def skip_large_payload
head 413 if request.content_length > ActivityPub::Activity::MAX_JSON_SIZE
end
def skip_unknown_actor_activity
head 202 if unknown_affected_account?
end

11
app/controllers/api/v1/statuses_controller.rb

@ -107,9 +107,7 @@ class Api::V1::StatusesController < Api::BaseController
@status = Status.where(account: current_account).find(params[:id])
authorize @status, :update?
UpdateStatusService.new.call(
@status,
current_account.id,
update_options = {
text: status_params[:status],
media_ids: status_params[:media_ids],
media_attributes: status_params[:media_attributes],
@ -117,8 +115,11 @@ class Api::V1::StatusesController < Api::BaseController
language: status_params[:language],
spoiler_text: status_params[:spoiler_text],
poll: status_params[:poll],
quote_approval_policy: quote_approval_policy
)
}
update_options[:quote_approval_policy] = quote_approval_policy if status_params[:quote_approval_policy].present?
UpdateStatusService.new.call(@status, current_account.id, update_options)
render json: @status, serializer: REST::StatusSerializer
end

2
app/controllers/api/web/push_subscriptions_controller.rb

@ -62,7 +62,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
end
def set_push_subscription
@push_subscription = ::Web::PushSubscription.find(params[:id])
@push_subscription = ::Web::PushSubscription.where(user_id: active_session.user_id).find(params[:id])
end
def subscription_params

2
app/controllers/concerns/cache_concern.rb

@ -19,7 +19,7 @@ module CacheConcern
# from being used as cache keys, while allowing to `Vary` on them (to not serve
# anonymous cached data to authenticated requests when authentication matters)
def enforce_cache_control!
vary = response.headers['Vary']&.split&.map { |x| x.strip.downcase }
vary = response.headers['Vary'].to_s.split(',').map { |x| x.strip.downcase }.reject(&:empty?)
return unless vary.present? && %w(cookie authorization signature).any? { |header| vary.include?(header) && request.headers[header].present? }
response.cache_control.replace(private: true, no_store: true)

1
app/javascript/mastodon/features/emoji/normalize.test.ts

@ -33,6 +33,7 @@ describe('emojiToUnicodeHex', () => {
['⚫', '26AB'],
['🖤', '1F5A4'],
['💀', '1F480'],
['❤', '2764'], // Checks for trailing variation selector removal.
['💂', '1F482-200D-2642-FE0F'],
] as const)(
'emojiToUnicodeHex converts %s to %s',

6
app/javascript/mastodon/features/emoji/normalize.ts

@ -30,6 +30,12 @@ export function emojiToUnicodeHex(emoji: string): string {
codes.push(code);
}
}
// Handles how Emojibase removes the variation selector for single code emojis.
// See: https://emojibase.dev/docs/spec/#merged-variation-selectors
if (codes.at(1) === VARIATION_SELECTOR_CODE && codes.length === 2) {
codes.pop();
}
return hexNumbersToString(codes);
}

1
app/javascript/styles/mastodon/admin.scss

@ -170,6 +170,7 @@ $content-width: 840px;
width: 100%;
max-width: $content-width;
flex: 1 1 auto;
isolation: isolate;
}
@media screen and (max-width: ($content-width + $sidebar-width)) {

8
app/lib/activitypub/activity.rb

@ -5,6 +5,7 @@ class ActivityPub::Activity
include Redisable
include Lockable
MAX_JSON_SIZE = 1.megabyte
SUPPORTED_TYPES = %w(Note Question Article).freeze
CONVERTED_TYPES = %w(Image Audio Video Page Event).freeze
@ -21,14 +22,13 @@ class ActivityPub::Activity
class << self
def factory(json, account, **)
@json = json
klass&.new(json, account, **)
klass_for(json)&.new(json, account, **)
end
private
def klass
case @json['type']
def klass_for(json)
case json['type']
when 'Create'
ActivityPub::Activity::Create
when 'Announce'

2
app/lib/activitypub/activity/accept.rb

@ -46,7 +46,7 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
def accept_quote!(quote)
approval_uri = value_or_id(first_of_value(@json['result']))
return if unsupported_uri_scheme?(approval_uri) || quote.quoted_account != @account || !quote.status.local?
return if unsupported_uri_scheme?(approval_uri) || quote.quoted_account != @account || !quote.status.local? || !quote.pending?
# NOTE: we are not going through `ActivityPub::VerifyQuoteService` as the `Accept` is as authoritative
# as the stamp, but this means we are not checking the stamp, which may lead to inconsistencies

2
app/lib/activitypub/activity/delete.rb

@ -56,7 +56,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
end
def revoke_quote
@quote = Quote.find_by(approval_uri: object_uri, quoted_account: @account)
@quote = Quote.find_by(approval_uri: object_uri, quoted_account: @account, state: [:pending, :accepted])
return if @quote.nil?
ActivityPub::Forwarder.new(@account, @json, @quote.status).forward! if @quote.status.present?

2
app/lib/activitypub/activity/quote_request.rb

@ -47,7 +47,7 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity
# NOTE: Replacing the object's context by that of the parent activity is
# not sound, but it's consistent with the rest of the codebase
instrument = @json['instrument'].merge({ '@context' => @json['@context'] })
return if non_matching_uri_hosts?(instrument['id'], @account.uri)
return if non_matching_uri_hosts?(@account.uri, instrument['id'])
ActivityPub::FetchRemoteStatusService.new.call(instrument['id'], prefetched_body: instrument, on_behalf_of: quoted_status.account, request_id: @options[:request_id])
end

3
app/lib/activitypub/activity/update.rb

@ -30,7 +30,8 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
@status = Status.find_by(uri: object_uri, account_id: @account.id)
# Ignore updates for old unknown objects, since those are updates we are not interested in
return if @status.nil? && object_too_old?
# Also ignore unknown objects from suspended users for the same reasons
return if @status.nil? && (@account.suspended? || object_too_old?)
# We may be getting `Create` and `Update` out of order
@status ||= ActivityPub::Activity::Create.new(@json, @account, **@options).perform

6
app/lib/activitypub/parser/poll_parser.rb

@ -3,6 +3,10 @@
class ActivityPub::Parser::PollParser
include JsonLdHelper
# Limit the number of items for performance purposes.
# We truncate rather than error out to avoid missing the post entirely.
MAX_ITEMS = 500
def initialize(json)
@json = json
end
@ -48,6 +52,6 @@ class ActivityPub::Parser::PollParser
private
def items
@json['anyOf'] || @json['oneOf']
(@json['anyOf'] || @json['oneOf'])&.take(MAX_ITEMS)
end
end

2
app/lib/activitypub/tag_manager.rb

@ -50,7 +50,7 @@ class ActivityPub::TagManager
context_url(target) unless target.parent_account_id.nil? || target.parent_status_id.nil?
when :note, :comment, :activity
if target.account.numeric_ap_id?
return activity_ap_account_status_url(target.account, target) if target.reblog?
return activity_ap_account_status_url(target.account.id, target) if target.reblog?
ap_account_status_url(target.account.id, target)
else

13
app/lib/connection_pool/shared_connection_pool.rb

@ -41,12 +41,17 @@ class ConnectionPool::SharedConnectionPool < ConnectionPool
# ConnectionPool 2.4+ calls `checkin(force: true)` after fork.
# When this happens, we should remove all connections from Thread.current
::Thread.current.keys.each do |name| # rubocop:disable Style/HashEachMethods
next unless name.to_s.start_with?("#{@key}-")
connection_keys = ::Thread.current.keys.select { |key| key.to_s.start_with?("#{@key}-") && !key.to_s.start_with?("#{@key_count}-") }
count_keys = ::Thread.current.keys.select { |key| key.to_s.start_with?("#{@key_count}-") }
@available.push(::Thread.current[name])
::Thread.current[name] = nil
connection_keys.each do |key|
@available.push(::Thread.current[key])
::Thread.current[key] = nil
end
count_keys.each do |key|
::Thread.current[key] = nil
end
elsif ::Thread.current[key(preferred_tag)]
if ::Thread.current[key_count(preferred_tag)] == 1
@available.push(::Thread.current[key(preferred_tag)])

1
app/lib/feed_manager.rb

@ -450,6 +450,7 @@ class FeedManager
return :filter if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
return :skip_home if timeline_type != :list && crutches[:exclusive_list_users][status.account_id].present?
return :filter if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)
return :filter if status.reblog? && status.reblog.blank?
check_for_blocks = crutches[:active_mentions][status.id] || []
check_for_blocks.push(status.account_id)

6
app/lib/signature_parser.rb

@ -25,9 +25,13 @@ class SignatureParser
# Use `skip` instead of `scan` as we only care about the subgroups
while scanner.skip(PARAM_RE)
key = scanner[:key]
# Detect a duplicate key
raise Mastodon::SignatureVerificationError, 'Error parsing signature with duplicate keys' if params.key?(key)
# This is not actually correct with regards to quoted pairs, but it's consistent
# with our previous implementation, and good enough in practice.
params[scanner[:key]] = scanner[:value] || scanner[:quoted_value][1...-1]
params[key] = scanner[:value] || scanner[:quoted_value][1...-1]
scanner.skip(/\s*/)
return params if scanner.eos?

9
app/models/account.rb

@ -80,6 +80,13 @@ class Account < ApplicationRecord
DISPLAY_NAME_LENGTH_LIMIT = 30
NOTE_LENGTH_LIMIT = 500
# Hard limits for federated content
USERNAME_LENGTH_HARD_LIMIT = 2048
DISPLAY_NAME_LENGTH_HARD_LIMIT = 2048
NOTE_LENGTH_HARD_LIMIT = 20.kilobytes
ATTRIBUTION_DOMAINS_HARD_LIMIT = 256
ALSO_KNOWN_AS_HARD_LIMIT = 256
AUTOMATED_ACTOR_TYPES = %w(Application Service).freeze
include Attachmentable # Load prior to Avatar & Header concerns
@ -112,7 +119,7 @@ class Account < ApplicationRecord
validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? }
# Remote user validations, also applies to internal actors
validates :username, format: { with: USERNAME_ONLY_RE }, if: -> { (remote? || actor_type_application?) && will_save_change_to_username? }
validates :username, format: { with: USERNAME_ONLY_RE }, length: { maximum: USERNAME_LENGTH_HARD_LIMIT }, if: -> { (remote? || actor_type_application?) && will_save_change_to_username? }
# Remote user validations
validates :uri, presence: true, unless: :local?, on: :create

5
app/models/custom_emoji.rb

@ -26,6 +26,8 @@ class CustomEmoji < ApplicationRecord
LIMIT = 256.kilobytes
MINIMUM_SHORTCODE_SIZE = 2
MAX_SHORTCODE_SIZE = 128
MAX_FEDERATED_SHORTCODE_SIZE = 2048
SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}'
@ -45,7 +47,8 @@ class CustomEmoji < ApplicationRecord
normalizes :domain, with: ->(domain) { domain.downcase.strip }
validates_attachment :image, content_type: { content_type: IMAGE_MIME_TYPES }, presence: true, size: { less_than: LIMIT }
validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: MINIMUM_SHORTCODE_SIZE }
validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: MINIMUM_SHORTCODE_SIZE, maximum: MAX_FEDERATED_SHORTCODE_SIZE }
validates :shortcode, length: { maximum: MAX_SHORTCODE_SIZE }, if: :local?
scope :local, -> { where(domain: nil) }
scope :remote, -> { where.not(domain: nil) }

3
app/models/custom_filter.rb

@ -30,6 +30,8 @@ class CustomFilter < ApplicationRecord
EXPIRATION_DURATIONS = [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].freeze
TITLE_LENGTH_LIMIT = 256
include Expireable
include Redisable
@ -41,6 +43,7 @@ class CustomFilter < ApplicationRecord
accepts_nested_attributes_for :keywords, reject_if: :all_blank, allow_destroy: true
validates :title, :context, presence: true
validates :title, length: { maximum: TITLE_LENGTH_LIMIT }
validate :context_must_be_valid
normalizes :context, with: ->(context) { context.map(&:strip).filter_map(&:presence) }

4
app/models/custom_filter_keyword.rb

@ -17,7 +17,9 @@ class CustomFilterKeyword < ApplicationRecord
belongs_to :custom_filter
validates :keyword, presence: true
KEYWORD_LENGTH_LIMIT = 512
validates :keyword, presence: true, length: { maximum: KEYWORD_LENGTH_LIMIT }
alias_attribute :phrase, :keyword

3
app/models/list.rb

@ -17,6 +17,7 @@ class List < ApplicationRecord
include Paginable
PER_ACCOUNT_LIMIT = 50
TITLE_LENGTH_LIMIT = 256
enum :replies_policy, { list: 0, followed: 1, none: 2 }, prefix: :show, validate: true
@ -26,7 +27,7 @@ class List < ApplicationRecord
has_many :accounts, through: :list_accounts
has_many :active_accounts, -> { merge(ListAccount.active) }, through: :list_accounts, source: :account
validates :title, presence: true
validates :title, presence: true, length: { maximum: TITLE_LENGTH_LIMIT }
validate :validate_account_lists_limit, on: :create

4
app/models/quote.rb

@ -51,9 +51,9 @@ class Quote < ApplicationRecord
def reject!
if accepted?
update!(state: :revoked)
update!(state: :revoked, approval_uri: nil)
elsif !revoked?
update!(state: :rejected)
update!(state: :rejected, approval_uri: nil)
end
end

1
app/services/activitypub/fetch_remote_status_service.rb

@ -92,7 +92,6 @@ class ActivityPub::FetchRemoteStatusService < BaseService
existing_status = Status.remote.find_by(uri: uri)
if existing_status&.distributable?
Rails.logger.debug { "FetchRemoteStatusService - Got 404 for orphaned status with URI #{uri}, deleting" }
Tombstone.find_or_create_by(uri: uri, account: existing_status.account)
RemoveStatusService.new.call(existing_status, redraft: false)
end
end

14
app/services/activitypub/process_account_service.rb

@ -6,6 +6,7 @@ class ActivityPub::ProcessAccountService < BaseService
include Redisable
include Lockable
MAX_PROFILE_FIELDS = 50
SUBDOMAINS_RATELIMIT = 10
DISCOVERIES_PER_REQUEST = 400
@ -123,15 +124,15 @@ class ActivityPub::ProcessAccountService < BaseService
def set_immediate_attributes!
@account.featured_collection_url = valid_collection_uri(@json['featured'])
@account.display_name = @json['name'] || ''
@account.note = @json['summary'] || ''
@account.display_name = (@json['name'] || '')[0...(Account::DISPLAY_NAME_LENGTH_HARD_LIMIT)]
@account.note = (@json['summary'] || '')[0...(Account::NOTE_LENGTH_HARD_LIMIT)]
@account.locked = @json['manuallyApprovesFollowers'] || false
@account.fields = property_values || {}
@account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) }
@account.also_known_as = as_array(@json['alsoKnownAs'] || []).take(Account::ALSO_KNOWN_AS_HARD_LIMIT).map { |item| value_or_id(item) }
@account.discoverable = @json['discoverable'] || false
@account.indexable = @json['indexable'] || false
@account.memorial = @json['memorial'] || false
@account.attribution_domains = as_array(@json['attributionDomains'] || []).map { |item| value_or_id(item) }
@account.attribution_domains = as_array(@json['attributionDomains'] || []).take(Account::ATTRIBUTION_DOMAINS_HARD_LIMIT).map { |item| value_or_id(item) }
end
def set_fetchable_key!
@ -252,7 +253,10 @@ class ActivityPub::ProcessAccountService < BaseService
def property_values
return unless @json['attachment'].is_a?(Array)
as_array(@json['attachment']).select { |attachment| attachment['type'] == 'PropertyValue' }.map { |attachment| attachment.slice('name', 'value') }
as_array(@json['attachment'])
.select { |attachment| attachment['type'] == 'PropertyValue' }
.take(MAX_PROFILE_FIELDS)
.map { |attachment| attachment.slice('name', 'value') }
end
def mismatching_origin?(url)

2
app/services/batched_remove_status_service.rb

@ -31,7 +31,7 @@ class BatchedRemoveStatusService < BaseService
# transaction lock the database, but we use the delete method instead
# of destroy to avoid all callbacks. We rely on foreign keys to
# cascade the delete faster without loading the associations.
statuses_and_reblogs.each_slice(50) { |slice| Status.where(id: slice.map(&:id)).delete_all }
statuses_and_reblogs.each_slice(50) { |slice| Status.unscoped.where(id: slice.pluck(:id)).delete_all }
# Since we skipped all callbacks, we also need to manually
# deindex the statuses

2
app/services/fan_out_on_write_service.rb

@ -14,6 +14,8 @@ class FanOutOnWriteService < BaseService
@account = status.account
@options = options
return if @status.proper.account.suspended?
check_race_condition!
warm_payload_cache!

6
docker-compose.yml

@ -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.4
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.4
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.4
image: ghcr.io/mastodon/mastodon:v4.5.5
restart: always
env_file: .env.production
command: bundle exec sidekiq

2
lib/mastodon/version.rb

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

22
spec/lib/activitypub/tag_manager_spec.rb

@ -128,6 +128,28 @@ RSpec.describe ActivityPub::TagManager do
.to eq("#{host_prefix}/ap/users/#{status.account.id}/statuses/#{status.id}")
end
end
context 'with a reblog' do
let(:status) { Fabricate(:status, account:, reblog: Fabricate(:status)) }
context 'when using a numeric ID based scheme' do
let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) }
it 'returns a string starting with web domain and with the expected path' do
expect(subject.uri_for(status))
.to eq("#{host_prefix}/ap/users/#{status.account.id}/statuses/#{status.id}/activity")
end
end
context 'when using the legacy username based scheme' do
let(:account) { Fabricate(:account, id_scheme: :username_ap_id) }
it 'returns a string starting with web domain and with the expected path' do
expect(subject.uri_for(status))
.to eq("#{host_prefix}/users/#{status.account.username}/statuses/#{status.id}/activity")
end
end
end
end
context 'with a remote status' do

9
spec/requests/api/v1/statuses_spec.rb

@ -508,6 +508,15 @@ RSpec.describe '/api/v1/statuses' do
.to start_with('application/json')
end
end
context 'when status has non-default quote policy and param is omitted' do
let(:status) { Fabricate(:status, account: user.account, quote_approval_policy: 'nobody') }
it 'preserves existing quote approval policy' do
expect { subject }
.to_not(change { status.reload.quote_approval_policy })
end
end
end
end

5
spec/requests/api/web/push_subscriptions_spec.rb

@ -163,9 +163,10 @@ RSpec.describe 'API Web Push Subscriptions' do
end
describe 'PUT /api/web/push_subscriptions/:id' do
before { sign_in Fabricate :user }
before { sign_in user }
let(:subscription) { Fabricate :web_push_subscription }
let(:user) { Fabricate(:user) }
let(:subscription) { Fabricate(:web_push_subscription, user: user) }
it 'gracefully handles invalid nested params' do
put api_web_push_subscription_path(subscription), params: { data: 'invalid' }

Loading…
Cancel
Save