diff --git a/CHANGELOG.md b/CHANGELOG.md index 99175533af..bf49817dfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +## [4.3.20] - 2026-02-24 + +### Added + +- Add `--suspended-only` option to `tootctl emoji purge` (#37828 and #37861 by @ClearlyClaire and @mjankowski) + +### Fixed + +- Fix processing of object updates with duplicate hashtags (#37756 by @ClearlyClaire) + ## [4.3.19] - 2026-02-03 ### Security diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index d5e7e26baa..e9f5aa9e3e 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -194,7 +194,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService Tag.find_or_create_by_names([tag]).filter(&:valid?) rescue ActiveRecord::RecordInvalid [] - end + end.uniq return unless @status.distributable? diff --git a/docker-compose.yml b/docker-compose.yml index da091f21a3..38b487f76d 100644 --- a/docker-compose.yml +++ b/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/glitch-soc/mastodon:v4.3.19 + image: ghcr.io/glitch-soc/mastodon:v4.3.20 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/glitch-soc/mastodon-streaming:v4.3.19 + image: ghcr.io/glitch-soc/mastodon-streaming:v4.3.20 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/glitch-soc/mastodon:v4.3.19 + image: ghcr.io/glitch-soc/mastodon:v4.3.20 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/cli/emoji.rb b/lib/mastodon/cli/emoji.rb index 4a8949de0e..1f83f87a40 100644 --- a/lib/mastodon/cli/emoji.rb +++ b/lib/mastodon/cli/emoji.rb @@ -107,15 +107,27 @@ module Mastodon::CLI end option :remote_only, type: :boolean + option :suspended_only, type: :boolean desc 'purge', 'Remove all custom emoji' long_desc <<-LONG_DESC Removes all custom emoji. With the --remote-only option, only remote emoji will be deleted. + + With the --suspended-only option, only emoji from suspended servers will be deleted. LONG_DESC def purge - scope = options[:remote_only] ? CustomEmoji.remote : CustomEmoji - scope.in_batches.destroy_all + if options[:suspended_only] + DomainBlock.where(severity: :suspend).find_each do |domain_block| + CustomEmoji.by_domain_and_subdomains(domain_block.domain).find_in_batches do |custom_emojis| + AttachmentBatch.new(CustomEmoji, custom_emojis).delete + end + end + else + scope = options[:remote_only] ? CustomEmoji.remote : CustomEmoji + scope.in_batches.destroy_all + end + say('OK', :green) end diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 4ded61ded7..0b337f2595 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 19 + 20 end def default_prerelease diff --git a/spec/lib/mastodon/cli/emoji_spec.rb b/spec/lib/mastodon/cli/emoji_spec.rb index 4336db17d3..c257bdf792 100644 --- a/spec/lib/mastodon/cli/emoji_spec.rb +++ b/spec/lib/mastodon/cli/emoji_spec.rb @@ -23,6 +23,36 @@ RSpec.describe Mastodon::CLI::Emoji do .to output_results('OK') end end + + context 'with --suspended-only and existing custom emoji on blocked servers' do + let(:blocked_domain) { 'evil.com' } + let(:blocked_subdomain) { 'subdomain.evil.org' } + let(:blocked_domain_without_emoji) { 'blocked.com' } + let(:silenced_domain) { 'silenced.com' } + + let(:options) { { suspended_only: true } } + + before do + Fabricate(:custom_emoji) + Fabricate(:custom_emoji, domain: blocked_domain) + Fabricate(:custom_emoji, domain: blocked_subdomain) + Fabricate(:custom_emoji, domain: silenced_domain) + + Fabricate(:domain_block, severity: :suspend, domain: blocked_domain) + Fabricate(:domain_block, severity: :suspend, domain: 'evil.org') + Fabricate(:domain_block, severity: :suspend, domain: blocked_domain_without_emoji) + Fabricate(:domain_block, severity: :silence, domain: silenced_domain) + end + + it 'reports a successful purge' do + expect { subject } + .to output_results('OK') + .and change { CustomEmoji.by_domain_and_subdomains(blocked_domain).count }.to(0) + .and change { CustomEmoji.by_domain_and_subdomains('evil.org').count }.to(0) + .and not_change { CustomEmoji.by_domain_and_subdomains(silenced_domain).count } + .and(not_change { CustomEmoji.local.count }) + end + end end describe '#import' do diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index c01b48e93b..562b135eb3 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -259,6 +259,8 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do { type: 'Hashtag', name: 'foo' }, { type: 'Hashtag', name: 'bar' }, { type: 'Hashtag', name: '#2024' }, + { type: 'Hashtag', name: 'Foo Bar' }, + { type: 'Hashtag', name: 'FooBar' }, ], } end @@ -270,7 +272,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do it 'updates tags and featured tags' do expect { subject.call(status, json, json) } - .to change { status.tags.reload.pluck(:name) }.from(%w(test foo)).to(%w(foo bar)) + .to change { status.tags.reload.pluck(:name) }.from(contain_exactly('test', 'foo')).to(contain_exactly('foo', 'bar', 'foobar')) .and change { status.account.featured_tags.find_by(name: 'test').statuses_count }.by(-1) .and change { status.account.featured_tags.find_by(name: 'bar').statuses_count }.by(1) .and change { status.account.featured_tags.find_by(name: 'bar').last_status_at }.from(nil).to(be_present)