From d90d68bddf50de32338bb9613d4654f626b2616f Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 3 Mar 2025 11:50:57 -0500 Subject: [PATCH] Remove deprecated `mb_chars` method (#34039) --- app/lib/hashtag_normalizer.rb | 2 +- app/models/tag.rb | 4 +-- app/services/batched_remove_status_service.rb | 2 +- app/services/fan_out_on_write_service.rb | 4 +-- app/services/remove_status_service.rb | 4 +-- app/validators/note_length_validator.rb | 2 +- app/validators/poll_options_validator.rb | 2 +- app/validators/status_length_validator.rb | 2 +- spec/validators/note_length_validator_spec.rb | 16 ++++++++++++ .../validators/poll_options_validator_spec.rb | 26 +++++++++++++++++++ .../status_length_validator_spec.rb | 16 ++++++++++++ 11 files changed, 69 insertions(+), 11 deletions(-) diff --git a/app/lib/hashtag_normalizer.rb b/app/lib/hashtag_normalizer.rb index 49fa6101de..5347271194 100644 --- a/app/lib/hashtag_normalizer.rb +++ b/app/lib/hashtag_normalizer.rb @@ -16,7 +16,7 @@ class HashtagNormalizer end def lowercase(str) - str.mb_chars.downcase.to_s + str.downcase.to_s end def cjk_width(str) diff --git a/app/models/tag.rb b/app/models/tag.rb index d29cd220f0..a3ccdd8ac6 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -160,11 +160,11 @@ class Tag < ApplicationRecord private def validate_name_change - errors.add(:name, I18n.t('tags.does_not_match_previous_name')) unless name_was.mb_chars.casecmp(name.mb_chars).zero? + errors.add(:name, I18n.t('tags.does_not_match_previous_name')) unless name_was.casecmp(name).zero? end def validate_display_name_change - unless HashtagNormalizer.new.normalize(display_name).casecmp(name.mb_chars).zero? + unless HashtagNormalizer.new.normalize(display_name).casecmp(name).zero? errors.add(:display_name, I18n.t('tags.does_not_match_previous_name')) end diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index de4ee16e91..5d6ea2550e 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -92,7 +92,7 @@ class BatchedRemoveStatusService < BaseService pipeline.publish(status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', payload) end - status.tags.map { |tag| tag.name.mb_chars.downcase }.each do |hashtag| + status.tags.map { |tag| tag.name.downcase }.each do |hashtag| pipeline.publish("timeline:hashtag:#{hashtag}", payload) pipeline.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local? end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 3c084bc857..f3aa479c15 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -128,8 +128,8 @@ class FanOutOnWriteService < BaseService def broadcast_to_hashtag_streams! @status.tags.map(&:name).each do |hashtag| - redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", anonymous_payload) - redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", anonymous_payload) if @status.local? + redis.publish("timeline:hashtag:#{hashtag.downcase}", anonymous_payload) + redis.publish("timeline:hashtag:#{hashtag.downcase}:local", anonymous_payload) if @status.local? end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index dc9fb6cab6..522437aeac 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -123,8 +123,8 @@ class RemoveStatusService < BaseService return if skip_streaming? @status.tags.map(&:name).each do |hashtag| - redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload) - redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if @status.local? + redis.publish("timeline:hashtag:#{hashtag.downcase}", @payload) + redis.publish("timeline:hashtag:#{hashtag.downcase}:local", @payload) if @status.local? end end diff --git a/app/validators/note_length_validator.rb b/app/validators/note_length_validator.rb index 554ad49ce2..1a16bbf2b3 100644 --- a/app/validators/note_length_validator.rb +++ b/app/validators/note_length_validator.rb @@ -8,7 +8,7 @@ class NoteLengthValidator < ActiveModel::EachValidator private def too_long?(value) - countable_text(value).mb_chars.grapheme_length > options[:maximum] + countable_text(value).each_grapheme_cluster.size > options[:maximum] end def countable_text(value) diff --git a/app/validators/poll_options_validator.rb b/app/validators/poll_options_validator.rb index 0ac84f93f4..fd29fc1b44 100644 --- a/app/validators/poll_options_validator.rb +++ b/app/validators/poll_options_validator.rb @@ -7,7 +7,7 @@ class PollOptionsValidator < ActiveModel::Validator def validate(poll) poll.errors.add(:options, I18n.t('polls.errors.too_few_options')) unless poll.options.size > 1 poll.errors.add(:options, I18n.t('polls.errors.too_many_options', max: MAX_OPTIONS)) if poll.options.size > MAX_OPTIONS - poll.errors.add(:options, I18n.t('polls.errors.over_character_limit', max: MAX_OPTION_CHARS)) if poll.options.any? { |option| option.mb_chars.grapheme_length > MAX_OPTION_CHARS } + poll.errors.add(:options, I18n.t('polls.errors.over_character_limit', max: MAX_OPTION_CHARS)) if poll.options.any? { |option| option.each_grapheme_cluster.size > MAX_OPTION_CHARS } poll.errors.add(:options, I18n.t('polls.errors.duplicate_options')) unless poll.options.uniq.size == poll.options.size end end diff --git a/app/validators/status_length_validator.rb b/app/validators/status_length_validator.rb index dc841ded3e..575aaf1869 100644 --- a/app/validators/status_length_validator.rb +++ b/app/validators/status_length_validator.rb @@ -18,7 +18,7 @@ class StatusLengthValidator < ActiveModel::Validator end def countable_length(str) - str.mb_chars.grapheme_length + str.each_grapheme_cluster.size end def combined_text(status) diff --git a/spec/validators/note_length_validator_spec.rb b/spec/validators/note_length_validator_spec.rb index 3fdb4ae8b9..c761c95280 100644 --- a/spec/validators/note_length_validator_spec.rb +++ b/spec/validators/note_length_validator_spec.rb @@ -30,6 +30,22 @@ RSpec.describe NoteLengthValidator do expect(account.errors).to have_received(:add) end + it 'counts multi byte emoji as single character' do + text = '✨' * 500 + account = instance_double(Account, note: text, errors: activemodel_errors) + + subject.validate_each(account, 'note', text) + expect(account.errors).to_not have_received(:add) + end + + it 'counts ZWJ sequence emoji as single character' do + text = '🏳️‍⚧️' * 500 + account = instance_double(Account, note: text, errors: activemodel_errors) + + subject.validate_each(account, 'note', text) + expect(account.errors).to_not have_received(:add) + end + private def starting_string diff --git a/spec/validators/poll_options_validator_spec.rb b/spec/validators/poll_options_validator_spec.rb index 9e4ec744db..cc03e9d673 100644 --- a/spec/validators/poll_options_validator_spec.rb +++ b/spec/validators/poll_options_validator_spec.rb @@ -41,5 +41,31 @@ RSpec.describe PollOptionsValidator do expect(errors).to have_received(:add) end end + + describe 'character length of poll options' do + context 'when poll has acceptable length options' do + let(:options) { %w(test this) } + + it 'has no errors' do + expect(errors).to_not have_received(:add) + end + end + + context 'when poll has multibyte and ZWJ emoji options' do + let(:options) { ['✨' * described_class::MAX_OPTION_CHARS, '🏳️‍⚧️' * described_class::MAX_OPTION_CHARS] } + + it 'has no errors' do + expect(errors).to_not have_received(:add) + end + end + + context 'when poll has options that are too long' do + let(:options) { ['ok', 'a' * (described_class::MAX_OPTION_CHARS**2)] } + + it 'has errors' do + expect(errors).to have_received(:add) + end + end + end end end diff --git a/spec/validators/status_length_validator_spec.rb b/spec/validators/status_length_validator_spec.rb index ecbfd4ba37..050b7500bb 100644 --- a/spec/validators/status_length_validator_spec.rb +++ b/spec/validators/status_length_validator_spec.rb @@ -80,6 +80,22 @@ RSpec.describe StatusLengthValidator do subject.validate(status) expect(status.errors).to have_received(:add) end + + it 'counts multi byte emoji as single character' do + text = '✨' * 500 + status = status_double(text: text) + + subject.validate(status) + expect(status.errors).to_not have_received(:add) + end + + it 'counts ZWJ sequence emoji as single character' do + text = '🏳️‍⚧️' * 500 + status = status_double(text: text) + + subject.validate(status) + expect(status.errors).to_not have_received(:add) + end end private