Merge tag 'v4.2.14'

This commit is contained in:
bgme 2024-12-20 17:25:02 +08:00
commit a84b56e403
20 changed files with 208 additions and 89 deletions

View file

@ -22,7 +22,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.2.') }}
latest=false
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}

View file

@ -2,6 +2,25 @@
All notable changes to this project will be documented in this file.
## [4.2.14] - 2024-02-03
### Added
- Add `tootctl feeds vacuum` (#33065 by @ClearlyClaire)
### Fixed
- Fix inactive users' timelines being backfilled on follow and unsuspend (#33094 by @ClearlyClaire)
- Fix direct inbox delivery pushing posts into inactive followers' timelines (#33067 by @ClearlyClaire)
- Fix `TagFollow` records not being correctly handled in account operations (#33063 by @ClearlyClaire)
- Fix pushing hashtag-followed posts to feeds of inactive users (#33018 by @Gargron)
- Fix and improve batch attachment deletion handling when using OpenStack Swift (#32637 by @hugogameiro)
- Fix tl language native name (#32606 by @seav)
### Security
- Update dependencies
## [4.2.13] - 2024-09-30
### Security

View file

@ -28,47 +28,47 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.8.4)
actionpack (= 7.0.8.4)
activesupport (= 7.0.8.4)
actioncable (7.0.8.6)
actionpack (= 7.0.8.6)
activesupport (= 7.0.8.6)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.8.4)
actionpack (= 7.0.8.4)
activejob (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
actionmailbox (7.0.8.6)
actionpack (= 7.0.8.6)
activejob (= 7.0.8.6)
activerecord (= 7.0.8.6)
activestorage (= 7.0.8.6)
activesupport (= 7.0.8.6)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.8.4)
actionpack (= 7.0.8.4)
actionview (= 7.0.8.4)
activejob (= 7.0.8.4)
activesupport (= 7.0.8.4)
actionmailer (7.0.8.6)
actionpack (= 7.0.8.6)
actionview (= 7.0.8.6)
activejob (= 7.0.8.6)
activesupport (= 7.0.8.6)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.8.4)
actionview (= 7.0.8.4)
activesupport (= 7.0.8.4)
actionpack (7.0.8.6)
actionview (= 7.0.8.6)
activesupport (= 7.0.8.6)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.8.4)
actionpack (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
actiontext (7.0.8.6)
actionpack (= 7.0.8.6)
activerecord (= 7.0.8.6)
activestorage (= 7.0.8.6)
activesupport (= 7.0.8.6)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.8.4)
activesupport (= 7.0.8.4)
actionview (7.0.8.6)
activesupport (= 7.0.8.6)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -78,22 +78,22 @@ GEM
activemodel (>= 4.1, < 7.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (7.0.8.4)
activesupport (= 7.0.8.4)
activejob (7.0.8.6)
activesupport (= 7.0.8.6)
globalid (>= 0.3.6)
activemodel (7.0.8.4)
activesupport (= 7.0.8.4)
activerecord (7.0.8.4)
activemodel (= 7.0.8.4)
activesupport (= 7.0.8.4)
activestorage (7.0.8.4)
actionpack (= 7.0.8.4)
activejob (= 7.0.8.4)
activerecord (= 7.0.8.4)
activesupport (= 7.0.8.4)
activemodel (7.0.8.6)
activesupport (= 7.0.8.6)
activerecord (7.0.8.6)
activemodel (= 7.0.8.6)
activesupport (= 7.0.8.6)
activestorage (7.0.8.6)
actionpack (= 7.0.8.6)
activejob (= 7.0.8.6)
activerecord (= 7.0.8.6)
activesupport (= 7.0.8.6)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.8.4)
activesupport (7.0.8.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -350,7 +350,7 @@ GEM
httplog (1.6.2)
rack (>= 2.0)
rainbow (>= 2.0.0)
i18n (1.14.5)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
i18n-tasks (1.0.12)
activesupport (>= 4.0.2)
@ -446,7 +446,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2023.0808)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
mini_portile2 (2.8.8)
minitest (5.19.0)
msgpack (1.7.1)
multi_json (1.15.0)
@ -468,7 +468,7 @@ GEM
net-smtp (0.3.4)
net-protocol
net-ssh (7.1.0)
nio4r (2.7.3)
nio4r (2.7.4)
nokogiri (1.16.7)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
@ -533,7 +533,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (2.2.9)
rack (2.2.10)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (2.0.2)
@ -550,20 +550,20 @@ GEM
rack
rack-test (2.1.0)
rack (>= 1.3)
rails (7.0.8.4)
actioncable (= 7.0.8.4)
actionmailbox (= 7.0.8.4)
actionmailer (= 7.0.8.4)
actionpack (= 7.0.8.4)
actiontext (= 7.0.8.4)
actionview (= 7.0.8.4)
activejob (= 7.0.8.4)
activemodel (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
rails (7.0.8.6)
actioncable (= 7.0.8.6)
actionmailbox (= 7.0.8.6)
actionmailer (= 7.0.8.6)
actionpack (= 7.0.8.6)
actiontext (= 7.0.8.6)
actionview (= 7.0.8.6)
activejob (= 7.0.8.6)
activemodel (= 7.0.8.6)
activerecord (= 7.0.8.6)
activestorage (= 7.0.8.6)
activesupport (= 7.0.8.6)
bundler (>= 1.15.0)
railties (= 7.0.8.4)
railties (= 7.0.8.6)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@ -578,9 +578,9 @@ GEM
rails-i18n (7.0.7)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
railties (7.0.8.4)
actionpack (= 7.0.8.4)
activesupport (= 7.0.8.4)
railties (7.0.8.6)
actionpack (= 7.0.8.6)
activesupport (= 7.0.8.6)
method_source
rake (>= 12.2)
thor (~> 1.0)
@ -604,7 +604,7 @@ GEM
responders (3.1.0)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.3.7)
rexml (3.3.9)
rotp (6.3.0)
rouge (4.1.2)
rpam2 (4.0.2)
@ -741,9 +741,9 @@ GEM
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
test-prof (1.2.3)
thor (1.3.1)
thor (1.3.2)
tilt (2.2.0)
timeout (0.4.1)
timeout (0.4.2)
tpm-key_attestation (0.12.0)
bindata (~> 2.4)
openssl (> 2.0)
@ -807,7 +807,7 @@ GEM
xorcist (1.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.16)
zeitwerk (2.6.18)
PLATFORMS
ruby

View file

@ -2,7 +2,7 @@
If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you can either:
- open a [Github security issue on the Mastodon project](https://github.com/mastodon/mastodon/security/advisories/new)
- open a [GitHub security issue on the Mastodon project](https://github.com/mastodon/mastodon/security/advisories/new)
- reach us at <security@joinmastodon.org>
You should _not_ report such issues on public GitHub issues or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk.
@ -13,8 +13,9 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
## Supported Versions
| Version | Supported |
| ------- | --------- |
| 4.2.x | Yes |
| 4.1.x | Yes |
| < 4.1 | No |
| Version | Supported |
| ------- | ---------------- |
| 4.3.x | Yes |
| 4.2.x | Yes |
| 4.1.x | Until 2025-04-08 |
| < 4.1 | No |

View file

@ -161,7 +161,7 @@ module LanguagesHelper
th: ['Thai', 'ไทย'].freeze,
ti: ['Tigrinya', 'ትግርኛ'].freeze,
tk: ['Turkmen', 'Türkmen'].freeze,
tl: ['Tagalog', 'Wikang Tagalog'].freeze,
tl: ['Tagalog', 'Tagalog'].freeze,
tn: ['Tswana', 'Setswana'].freeze,
to: ['Tonga', 'faka Tonga'].freeze,
tr: ['Turkish', 'Türkçe'].freeze,

View file

@ -76,10 +76,22 @@ class AttachmentBatch
when :fog
logger.debug { "Deleting #{attachment.path(style)}" }
retries = 0
begin
attachment.send(:directory).files.new(key: attachment.path(style)).destroy
rescue Fog::Storage::OpenStack::NotFound
# Ignore failure to delete a file that has already been deleted
rescue Fog::OpenStack::Storage::NotFound
logger.debug "Will ignore because file is not found #{attachment.path(style)}"
rescue => e
retries += 1
if retries < MAX_RETRY
logger.debug "Retry #{retries}/#{MAX_RETRY} after #{e.message}"
sleep 2**retries
retry
else
logger.error "Batch deletion from fog failed after #{e.message}"
raise e
end
end
when :azure
logger.debug { "Deleting #{attachment.path(style)}" }

View file

@ -18,7 +18,7 @@ class FeedManager
# @yield [Account]
# @return [void]
def with_active_accounts(&block)
Account.joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).find_each(&block)
Account.joins(:user).merge(User.signed_in_recently).find_each(&block)
end
# Redis key of a feed
@ -58,6 +58,7 @@ class FeedManager
# @param [Boolean] update
# @return [Boolean]
def push_to_home(account, status, update: false)
return false unless account.user&.signed_in_recently?
return false unless add_to_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?)
trim(:home, account.id)
@ -83,7 +84,9 @@ class FeedManager
# @param [Boolean] update
# @return [Boolean]
def push_to_list(list, status, update: false)
return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?)
return false if filter_from_list?(status, list)
return false unless list.account.user&.signed_in_recently?
return false unless add_to_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?)
trim(:list, list.id)
PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}", { 'update' => update }) if push_update_required?("timeline:list:#{list.id}")
@ -107,6 +110,8 @@ class FeedManager
# @param [Account] into_account
# @return [void]
def merge_into_home(from_account, into_account)
return unless into_account.user&.signed_in_recently?
timeline_key = key(:home, into_account.id)
aggregate = into_account.user&.aggregates_reblogs?
query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
@ -133,6 +138,8 @@ class FeedManager
# @param [List] list
# @return [void]
def merge_into_list(from_account, list)
return unless list.account.user&.signed_in_recently?
timeline_key = key(:list, list.id)
aggregate = list.account.user&.aggregates_reblogs?
query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)

View file

@ -21,7 +21,7 @@ class Vacuum::FeedsVacuum
end
def inactive_users
User.confirmed.inactive
User.confirmed.not_signed_in_recently
end
def inactive_users_lists

View file

@ -83,6 +83,9 @@ module AccountInteractions
has_many :following, -> { order('follows.id desc') }, through: :active_relationships, source: :target_account
has_many :followers, -> { order('follows.id desc') }, through: :passive_relationships, source: :account
# Hashtag follows
has_many :tag_follows, inverse_of: :account, dependent: :destroy
# Account notes
has_many :account_notes, dependent: :destroy
@ -261,13 +264,13 @@ module AccountInteractions
def followers_for_local_distribution
followers.local
.joins(:user)
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
.merge(User.signed_in_recently)
end
def lists_for_local_distribution
scope = lists.joins(account: :user)
scope.where.not(list_accounts: { follow_id: nil }).or(scope.where(account_id: id))
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
.merge(User.signed_in_recently)
end
def remote_followers_hash(url)

View file

@ -16,7 +16,7 @@ module AccountMerging
Follow, FollowRequest, Block, Mute,
AccountModerationNote, AccountPin, AccountStat, ListAccount,
PollVote, Mention, AccountDeletionRequest, AccountNote, FollowRecommendationSuppression,
Appeal
Appeal, TagFollow
]
owned_classes.each do |klass|

View file

@ -21,4 +21,6 @@ class TagFollow < ApplicationRecord
accepts_nested_attributes_for :tag
rate_limit by: :account, family: :follows
scope :for_local_distribution, -> { joins(account: :user).merge(User.signed_in_recently) }
end

View file

@ -110,14 +110,16 @@ class User < ApplicationRecord
validates :confirm_password, absence: true, on: :create
validate :validate_role_elevation
scope :account_not_suspended, -> { joins(:account).merge(Account.without_suspended) }
scope :recent, -> { order(id: :desc) }
scope :pending, -> { where(approved: false) }
scope :approved, -> { where(approved: true) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :enabled, -> { where(disabled: false) }
scope :disabled, -> { where(disabled: true) }
scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended_at: nil }) }
scope :active, -> { confirmed.signed_in_recently.account_not_suspended }
scope :signed_in_recently, -> { where(current_sign_in_at: ACTIVE_DURATION.ago..) }
scope :not_signed_in_recently, -> { where(current_sign_in_at: ...ACTIVE_DURATION.ago) }
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
scope :matches_ip, ->(value) { left_joins(:ips).where('user_ips.ip <<= ?', value).group('users.id') }
scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }
@ -160,6 +162,10 @@ class User < ApplicationRecord
end
end
def signed_in_recently?
current_sign_in_at.present? && current_sign_in_at >= ACTIVE_DURATION.ago
end
def confirmed?
confirmed_at.present?
end

View file

@ -52,6 +52,7 @@ class DeleteAccountService < BaseService
owned_lists
scheduled_statuses
status_pins
tag_follows
)
ASSOCIATIONS_ON_DESTROY = %w(

View file

@ -94,7 +94,7 @@ class FanOutOnWriteService < BaseService
end
def deliver_to_hashtag_followers!
TagFollow.where(tag_id: @status.tags.map(&:id)).select(:id, :account_id).reorder(nil).find_in_batches do |follows|
TagFollow.for_local_distribution.where(tag_id: @status.tags.map(&:id)).select(:id, :account_id).reorder(nil).find_in_batches do |follows|
FeedInsertWorker.push_bulk(follows) do |follow|
[@status.id, follow.account_id, 'tags', { 'update' => update? }]
end

View file

@ -56,7 +56,7 @@ services:
web:
build: .
image: ghcr.io/mastodon/mastodon:v4.2.13
image: ghcr.io/mastodon/mastodon:v4.2.14
restart: always
env_file: .env.production
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
@ -77,7 +77,7 @@ services:
streaming:
build: .
image: ghcr.io/mastodon/mastodon:v4.2.13
image: ghcr.io/mastodon/mastodon:v4.2.14
restart: always
env_file: .env.production
command: node ./streaming
@ -95,7 +95,7 @@ services:
sidekiq:
build: .
image: ghcr.io/mastodon/mastodon:v4.2.13
image: ghcr.io/mastodon/mastodon:v4.2.14
restart: always
env_file: .env.production
command: bundle exec sidekiq

View file

@ -5,6 +5,7 @@ require_relative 'base'
module Mastodon::CLI
class Feeds < Base
include Redisable
include DatabaseHelper
option :all, type: :boolean, default: false
option :concurrency, type: :numeric, default: 5, aliases: [:c]
@ -48,6 +49,38 @@ module Mastodon::CLI
say('OK', :green)
end
desc 'vacuum', 'Remove home feeds of inactive users from Redis'
long_desc <<-LONG_DESC
Running this task should not be needed in most cases, as Mastodon will
automatically clean up feeds from inactive accounts every day.
However, this task is more aggressive in order to clean up feeds that
may have been missed because of bugs or database mishaps.
LONG_DESC
def vacuum
with_read_replica do
say('Deleting orphaned home feeds…')
redis.scan_each(match: 'feed:home:*').each_slice(1000) do |keys|
ids = keys.map { |key| key.split(':')[2] }.compact_blank
known_ids = User.confirmed.signed_in_recently.where(account_id: ids).pluck(:account_id)
keys_to_delete = keys.filter { |key| known_ids.exclude?(key.split(':')[2]&.to_i) }
redis.del(keys_to_delete)
end
say('Deleting orphaned list feeds…')
redis.scan_each(match: 'feed:list:*').each_slice(1000) do |keys|
ids = keys.map { |key| key.split(':')[2] }.compact_blank
known_ids = List.where(account_id: User.confirmed.signed_in_recently.select(:account_id)).where(id: ids).pluck(:id)
keys_to_delete = keys.filter { |key| known_ids.exclude?(key.split(':')[2]&.to_i) }
redis.del(keys_to_delete)
end
end
end
private
def active_user_accounts

View file

@ -39,6 +39,7 @@ module Mastodon::CLI
class Webhook < ApplicationRecord; end
class BulkImport < ApplicationRecord; end
class SoftwareUpdate < ApplicationRecord; end
class TagFollow < ApplicationRecord; end
class PreviewCard < ApplicationRecord
self.inheritance_column = false
@ -89,6 +90,7 @@ module Mastodon::CLI
owned_classes << AccountIdentityProof if ActiveRecord::Base.connection.table_exists?(:account_identity_proofs)
owned_classes << Appeal if ActiveRecord::Base.connection.table_exists?(:appeals)
owned_classes << BulkImport if ActiveRecord::Base.connection.table_exists?(:bulk_imports)
owned_classes << TagFollow if ActiveRecord::Base.connection.table_exists?(:tag_follows)
owned_classes.each do |klass|
klass.where(account_id: other_account.id).find_each do |record|

View file

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

View file

@ -81,12 +81,36 @@ RSpec.describe User do
end
end
describe 'inactive' do
it 'returns a relation of inactive users' do
specified = Fabricate(:user, current_sign_in_at: 15.days.ago)
Fabricate(:user, current_sign_in_at: 6.days.ago)
describe 'signed_in_recently' do
it 'returns a relation of users who have signed in during the recent period' do
recent_sign_in_user = Fabricate(:user, current_sign_in_at: within_duration_window_days.ago)
Fabricate(:user, current_sign_in_at: exceed_duration_window_days.ago)
expect(described_class.inactive).to contain_exactly(specified)
expect(described_class.signed_in_recently)
.to contain_exactly(recent_sign_in_user)
end
end
describe 'not_signed_in_recently' do
it 'returns a relation of users who have not signed in during the recent period' do
no_recent_sign_in_user = Fabricate(:user, current_sign_in_at: exceed_duration_window_days.ago)
Fabricate(:user, current_sign_in_at: within_duration_window_days.ago)
expect(described_class.not_signed_in_recently)
.to contain_exactly(no_recent_sign_in_user)
end
end
describe 'account_not_suspended' do
it 'returns with linked accounts that are not suspended' do
suspended_account = Fabricate(:account, suspended_at: 10.days.ago)
non_suspended_account = Fabricate(:account, suspended_at: nil)
suspended_user = Fabricate(:user, account: suspended_account)
non_suspended_user = Fabricate(:user, account: non_suspended_account)
expect(described_class.account_not_suspended)
.to include(non_suspended_user)
.and not_include(suspended_user)
end
end
@ -111,6 +135,14 @@ RSpec.describe User do
expect(described_class.matches_ip('2160:2160::/32')).to contain_exactly(user1)
end
end
def exceed_duration_window_days
described_class::ACTIVE_DURATION + 2.days
end
def within_duration_window_days
described_class::ACTIVE_DURATION - 2.days
end
end
describe 'blacklist' do

View file

@ -196,6 +196,7 @@ RSpec::Sidekiq.configure do |config|
end
RSpec::Matchers.define_negated_matcher :not_change, :change
RSpec::Matchers.define_negated_matcher :not_include, :include
def request_fixture(name)
Rails.root.join('spec', 'fixtures', 'requests', name).read