From fdca24ba56276b3d0e6bf0b31152de02c6162f75 Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 10 Feb 2025 12:44:42 +0900 Subject: [PATCH 01/47] Bump version to 17.0 --- docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 61deb51ab6..47eb36698f 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: kmyblue:17.0-dev + image: kmyblue:17.0 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: kmyblue-streaming:17.0-dev + image: kmyblue-streaming:17.0 restart: always env_file: .env.production command: node ./streaming/index.js @@ -101,7 +101,7 @@ services: sidekiq: build: . - image: kmyblue:17.0-dev + image: kmyblue:17.0 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index f09f32ab30..f45b4a4305 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -18,8 +18,8 @@ module Mastodon def kmyblue_flag # 'LTS' - 'dev' - # nil + # 'dev' + nil end def major From e97f8d1b59bb20bb5b28713a7b470b9c5dd4c1e0 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 3 Feb 2025 14:43:45 +0100 Subject: [PATCH 02/47] Fix emoji rewrite adding unnecessary curft to the DOM for most emoji (#33818) --- .../features/emoji/__tests__/emoji-test.js | 36 +++++++++---------- .../mastodon/features/emoji/emoji.js | 32 ++++++++--------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js index 022c9baaf7..35804de82a 100644 --- a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js +++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js @@ -22,23 +22,23 @@ describe('emoji', () => { it('does unicode', () => { expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual( - '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ'); + '๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ'); expect(emojify('๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง')).toEqual( - '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง'); - expect(emojify('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ')).toEqual('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ'); + '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง'); + expect(emojify('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ')).toEqual('๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ'); expect(emojify('\u2757')).toEqual( - 'โ—'); + 'โ—'); }); it('does multiple unicode', () => { expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual( - 'โ— #๏ธโƒฃ'); + 'โ— #๏ธโƒฃ'); expect(emojify('\u2757#\uFE0F\u20E3')).toEqual( - 'โ—#๏ธโƒฃ'); + 'โ—#๏ธโƒฃ'); expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual( - 'โ— #๏ธโƒฃ โ—'); + 'โ— #๏ธโƒฃ โ—'); expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual( - 'foo โ— #๏ธโƒฃ bar'); + 'foo โ— #๏ธโƒฃ bar'); }); it('ignores unicode inside of tags', () => { @@ -46,16 +46,16 @@ describe('emoji', () => { }); it('does multiple emoji properly (issue 5188)', () => { - expect(emojify('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•')).toEqual('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•'); - expect(emojify('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•')).toEqual('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•'); + expect(emojify('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•')).toEqual('๐Ÿ‘Œ๐ŸŒˆ๐Ÿ’•'); + expect(emojify('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•')).toEqual('๐Ÿ‘Œ ๐ŸŒˆ ๐Ÿ’•'); }); it('does an emoji that has no shortcode', () => { - expect(emojify('๐Ÿ‘โ€๐Ÿ—จ')).toEqual('๐Ÿ‘โ€๐Ÿ—จ'); + expect(emojify('๐Ÿ‘โ€๐Ÿ—จ')).toEqual('๐Ÿ‘โ€๐Ÿ—จ'); }); it('does an emoji whose filename is irregular', () => { - expect(emojify('โ†™๏ธ')).toEqual('โ†™๏ธ'); + expect(emojify('โ†™๏ธ')).toEqual('โ†™๏ธ'); }); it('avoid emojifying on invisible text', () => { @@ -67,11 +67,11 @@ describe('emoji', () => { it('avoid emojifying on invisible text with nested tags', () => { expect(emojify('๐Ÿ˜‡')) - .toEqual('๐Ÿ˜‡'); + .toEqual('๐Ÿ˜‡'); expect(emojify('๐Ÿ˜‡')) - .toEqual('๐Ÿ˜‡'); + .toEqual('๐Ÿ˜‡'); expect(emojify('๐Ÿ˜‡')) - .toEqual('๐Ÿ˜‡'); + .toEqual('๐Ÿ˜‡'); }); it('does not emojify emojis with textual presentation VS15 character', () => { @@ -81,17 +81,17 @@ describe('emoji', () => { it('does a simple emoji properly', () => { expect(emojify('โ™€โ™‚')) - .toEqual('โ™€โ™‚'); + .toEqual('โ™€โ™‚'); }); it('does an emoji containing ZWJ properly', () => { expect(emojify('๐Ÿ’‚โ€โ™€๏ธ๐Ÿ’‚โ€โ™‚๏ธ')) - .toEqual('๐Ÿ’‚\u200Dโ™€๏ธ๐Ÿ’‚\u200Dโ™‚๏ธ'); + .toEqual('๐Ÿ’‚\u200Dโ™€๏ธ๐Ÿ’‚\u200Dโ™‚๏ธ'); }); it('keeps ordering as expected (issue fixed by PR 20677)', () => { expect(emojify('

๐Ÿ’• #foo test: foo.

')) - .toEqual('

๐Ÿ’• #foo test: foo.

'); + .toEqual('

๐Ÿ’• #foo test: foo.

'); }); }); }); diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index 1f469aced7..66dcd89488 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -97,30 +97,30 @@ const emojifyTextNode = (node, customEmojis) => { const { filename, shortCode } = unicodeMapping[unicode_emoji]; const title = shortCode ? `:${shortCode}:` : ''; - replacement = document.createElement('picture'); - const isSystemTheme = !!document.body?.classList.contains('theme-system'); - if(isSystemTheme) { - let source = document.createElement('source'); - source.setAttribute('media', '(prefers-color-scheme: dark)'); - source.setAttribute('srcset', `${assetHost}/emoji/${emojiFilename(filename, "dark")}.svg`); - replacement.appendChild(source); - } + const theme = (isSystemTheme || document.body?.classList.contains('theme-mastodon-light')) ? 'light' : 'dark'; - let img = document.createElement('img'); + const imageFilename = emojiFilename(filename, theme); + + const img = document.createElement('img'); img.setAttribute('draggable', 'false'); img.setAttribute('class', 'emojione'); img.setAttribute('alt', unicode_emoji); img.setAttribute('title', title); + img.setAttribute('src', `${assetHost}/emoji/${imageFilename}.svg`); - let theme = "light"; + if (isSystemTheme && imageFilename !== emojiFilename(filename, 'dark')) { + replacement = document.createElement('picture'); - if(!isSystemTheme && !document.body?.classList.contains('theme-mastodon-light')) - theme = "dark"; - - img.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename, theme)}.svg`); - replacement.appendChild(img); + const source = document.createElement('source'); + source.setAttribute('media', '(prefers-color-scheme: dark)'); + source.setAttribute('srcset', `${assetHost}/emoji/${emojiFilename(filename, 'dark')}.svg`); + replacement.appendChild(source); + replacement.appendChild(img); + } else { + replacement = img; + } } // Add the processed-up-to-now string and the emoji replacement @@ -135,7 +135,7 @@ const emojifyTextNode = (node, customEmojis) => { }; const emojifyNode = (node, customEmojis) => { - for (const child of node.childNodes) { + for (const child of Array.from(node.childNodes)) { switch(child.nodeType) { case Node.TEXT_NODE: emojifyTextNode(child, customEmojis); From 5fb4ae8edfe3ea4ee7ea3b3cb231fe286c2fe71f Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 5 Feb 2025 15:02:39 +0100 Subject: [PATCH 03/47] Optimize timeline generation (#33839) --- app/lib/feed_manager.rb | 83 +++++++++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index df4fc17908..895d45ce69 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -42,7 +42,7 @@ class FeedManager when :home filter_from_home(status, receiver.id, build_crutches(receiver.id, [status]), :home) when :list - (filter_from_list?(status, receiver) ? :filter : nil) || filter_from_home(status, receiver.account_id, build_crutches(receiver.account_id, [status]), :list, stl_home: stl_home) + (filter_from_list?(status, receiver) ? :filter : nil) || filter_from_home(status, receiver.account_id, build_crutches(receiver.account_id, [status], list: list), :list, stl_home: stl_home) when :mentions filter_from_mentions?(status, receiver.id) ? :filter : nil when :tags @@ -136,7 +136,7 @@ class FeedManager timeline_key = key(:home, into_account.id) aggregate = into_account.user&.aggregates_reblogs? - query = from_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) + query = from_account.statuses.list_eligible_visibility.includes(reblog: :account).limit(FeedManager::MAX_ITEMS / 4) if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4 oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i @@ -164,7 +164,7 @@ class FeedManager timeline_key = key(:list, list.id) aggregate = list.account.user&.aggregates_reblogs? - query = from_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) + query = from_account.statuses.list_eligible_visibility.includes(reblog: :account).limit(FeedManager::MAX_ITEMS / 4) if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4 oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i @@ -172,10 +172,10 @@ class FeedManager end statuses = query.to_a - crutches = build_crutches(list.account_id, statuses) + crutches = build_crutches(list.account_id, statuses, list: list) statuses.each do |status| - next if filter_from_home(status, list.account_id, crutches) || filter_from_list?(status, list) + next if filter_from_home(status, list.account_id, crutches, :list) add_to_feed(:list, list.id, status, aggregate_reblogs: aggregate) end @@ -309,23 +309,32 @@ class FeedManager limit = FeedManager::MAX_ITEMS / 2 aggregate = account.user&.aggregates_reblogs? timeline_key = key(:home, account.id) + over_limit = false account.statuses.limit(limit).each do |status| add_to_feed(:home, account.id, status, aggregate_reblogs: aggregate) end account.following.includes(:account_stat).reorder(nil).find_each do |target_account| - if redis.zcard(timeline_key) >= limit + query = target_account.statuses.list_eligible_visibility.includes(reblog: :account).limit(limit) + + over_limit ||= redis.zcard(timeline_key) >= limit + if over_limit oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i - last_status_score = Mastodon::Snowflake.id_at(target_account.last_status_at) + last_status_score = Mastodon::Snowflake.id_at(target_account.last_status_at, with_random: false) # If the feed is full and this account has not posted more recently # than the last item on the feed, then we can skip the whole account # because none of its statuses would stay on the feed anyway next if last_status_score < oldest_home_score + + # No need to get older statuses + query = query.where(id: oldest_home_score...) end - statuses = target_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, :account, reblog: :account).limit(limit) + statuses = query.to_a + next if statuses.empty? + crutches = build_crutches(account.id, statuses) statuses.each do |status| @@ -345,23 +354,32 @@ class FeedManager limit = FeedManager::MAX_ITEMS / 2 aggregate = list.account.user&.aggregates_reblogs? timeline_key = key(:list, list.id) + over_limit = false list.active_accounts.includes(:account_stat).reorder(nil).find_each do |target_account| - if redis.zcard(timeline_key) >= limit + query = target_account.statuses.list_eligible_visibility.includes(reblog: :account).limit(limit) + + over_limit ||= redis.zcard(timeline_key) >= limit + if over_limit oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i - last_status_score = Mastodon::Snowflake.id_at(target_account.last_status_at) + last_status_score = Mastodon::Snowflake.id_at(target_account.last_status_at, with_random: false) # If the feed is full and this account has not posted more recently # than the last item on the feed, then we can skip the whole account # because none of its statuses would stay on the feed anyway next if last_status_score < oldest_home_score + + # No need to get older statuses + query = query.where(id: oldest_home_score...) end - statuses = target_account.statuses.list_eligible_visibility.includes(:preloadable_poll, :media_attachments, :account, reblog: :account).limit(limit) - crutches = build_crutches(list.account_id, statuses) + statuses = query.to_a + next if statuses.empty? + + crutches = build_crutches(list.account_id, statuses, list: list) statuses.each do |status| - next if filter_from_home(status, list.account_id, crutches) || filter_from_list?(status, list) + next if filter_from_home(status, list.account_id, crutches, :list) add_to_feed(:list, list.id, status, aggregate_reblogs: aggregate) end @@ -632,8 +650,9 @@ class FeedManager # are going to be checked by the filtering methods # @param [Integer] receiver_id # @param [Array] statuses + # @param [List] list # @return [Hash] - def build_crutches(receiver_id, statuses) # rubocop:disable Metrics/AbcSize + def build_crutches(receiver_id, statuses, list: nil) crutches = {} crutches[:active_mentions] = crutches_active_mentions(statuses) @@ -650,25 +669,43 @@ class FeedManager arr end - lists = List.where(account_id: receiver_id, exclusive: true) - antennas = Antenna.where(list: lists, insert_feeds: true) - - replied_accounts = statuses.filter_map(&:in_reply_to_account_id) - replied_accounts += statuses.filter { |status| status.limited_visibility? && status.thread.present? }.map { |status| status.thread.account_id } - - crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: replied_accounts).pluck(:target_account_id).index_with(true) + crutches[:following] = crutches_following(receiver_id, statuses, list) crutches[:languages] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true) crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true) crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true) crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true) - crutches[:exclusive_list_users] = ListAccount.where(list: lists, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true) - crutches[:exclusive_antenna_users] = AntennaAccount.where(antenna: antennas, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true) + crutches[:exclusive_list_users] = crutches_exclusive_list_users(receiver_id, statuses) if list.blank? + crutches[:exclusive_antenna_users] = crutches_exclusive_antenna_users(receiver_id, statuses) crutches end + def crutches_exclusive_list_users(recipient_id, statuses) + lists = List.where(account_id: recipient_id, exclusive: true) + ListAccount.where(list: lists, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true) + end + + def crutches_exclusive_antenna_users(recipient_id, statuses) + lists = List.where(account_id: recipient_id, exclusive: true) + antennas = Antenna.where(list: lists, insert_feeds: true) + AntennaAccount.where(antenna: antennas, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true) + end + + def crutches_following(recipient_id, statuses, list) + if list.blank? || list.show_followed? + replied_accounts = statuses.filter_map(&:in_reply_to_account_id) + replied_accounts += statuses.filter { |status| status.limited_visibility? && status.thread.present? }.map { |status| status.thread.account_id } + + Follow.where(account_id: recipient_id, target_account_id: replied_accounts).pluck(:target_account_id).index_with(true) + elsif list.show_list? + ListAccount.where(list_id: list.id, account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:account_id).index_with(true) + else + {} + end + end + def crutches_active_mentions(statuses) Mention .active From d4f0b0120723d5037a9cf579543b0aa8cf4b3939 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 5 Feb 2025 16:08:12 +0100 Subject: [PATCH 04/47] Fix filtering for lists (#33842) --- app/lib/feed_manager.rb | 2 +- spec/lib/feed_manager_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 895d45ce69..1a558cacbe 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -42,7 +42,7 @@ class FeedManager when :home filter_from_home(status, receiver.id, build_crutches(receiver.id, [status]), :home) when :list - (filter_from_list?(status, receiver) ? :filter : nil) || filter_from_home(status, receiver.account_id, build_crutches(receiver.account_id, [status], list: list), :list, stl_home: stl_home) + (filter_from_list?(status, receiver) ? :filter : nil) || filter_from_home(status, receiver.account_id, build_crutches(receiver.account_id, [status], list: receiver), :list, stl_home: stl_home) when :mentions filter_from_mentions?(status, receiver.id) ? :filter : nil when :tags diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index ad7913c758..48defa1f83 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -233,6 +233,28 @@ RSpec.describe FeedManager do end end + context 'with list feed' do + let(:list) { Fabricate(:list, account: bob) } + + before do + bob.follow!(alice) + list.list_accounts.create!(account: alice) + end + + it "returns false for followee's status" do + status = Fabricate(:status, text: 'Hello world', account: alice) + + expect(subject.filter?(:list, status, list)).to be false + end + + it 'returns false for reblog by followee' do + status = Fabricate(:status, text: 'Hello world', account: jeff) + reblog = Fabricate(:status, reblog: status, account: alice) + + expect(subject.filter?(:list, reblog, list)).to be false + end + end + context 'with mentions feed' do it 'returns true for status that mentions blocked account' do bob.block!(jeff) From 4fa550d8816aa2989c2efff100aaf74a72f6db02 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 12 Feb 2025 16:34:56 +0100 Subject: [PATCH 05/47] Fix handling of duplicate mentions in incoming status `Update` (#33911) --- .../activitypub/process_status_update_service.rb | 2 +- spec/lib/activitypub/activity/create_spec.rb | 14 ++++++++++---- .../process_status_update_service_spec.rb | 1 + 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index 1be39aaedf..10224f4d7e 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -296,7 +296,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService nil end - @status.mentions.upsert_all(currently_mentioned_account_ids.map { |id| { account_id: id, silent: false } }, unique_by: %w(status_id account_id)) + @status.mentions.upsert_all(currently_mentioned_account_ids.uniq.map { |id| { account_id: id, silent: false } }, unique_by: %w(status_id account_id)) # If previous mentions are no longer contained in the text, convert them # to silent mentions, since withdrawing access from someone who already diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index f5528f13a7..108111c06b 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -48,10 +48,16 @@ RSpec.describe ActivityPub::Activity::Create do content: '@bob lorem ipsum', published: 1.hour.ago.utc.iso8601, updated: 1.hour.ago.utc.iso8601, - tag: { - type: 'Mention', - href: ActivityPub::TagManager.instance.uri_for(follower), - }, + tag: [ + { + type: 'Mention', + href: ActivityPub::TagManager.instance.uri_for(follower), + }, + { + type: 'Mention', + href: ActivityPub::TagManager.instance.uri_for(follower), + }, + ], } end diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 49d496e295..6d008840be 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -12,6 +12,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do [ { type: 'Hashtag', name: 'hoge' }, { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) }, + { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) }, { type: 'Mention', href: bogus_mention }, ] end From 689d431dc6b8825eec65195770a120c837fefd34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:48:43 +0100 Subject: [PATCH 06/47] chore(deps): update dependency ruby-vips to v2.2.3 (#33853) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8547e4fba1..2529435115 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -748,7 +748,7 @@ GEM ruby-saml (1.17.0) nokogiri (>= 1.13.10) rexml - ruby-vips (2.2.2) + ruby-vips (2.2.3) ffi (~> 1.12) logger rubyzip (2.4.1) From 4da06664a68dca9fc62d30c2d0c5cc269b5e794b Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 12 Feb 2025 11:31:42 +0100 Subject: [PATCH 07/47] Update dependency net-imap to 0.5.6 (#33901) --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2529435115..cfca6f8ab1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -416,7 +416,7 @@ GEM mutex_m (0.3.0) net-http (0.6.0) uri - net-imap (0.5.5) + net-imap (0.5.6) date net-protocol net-ldap (0.19.0) @@ -424,7 +424,7 @@ GEM net-protocol net-protocol (0.2.2) timeout - net-smtp (0.5.0) + net-smtp (0.5.1) net-protocol nio4r (2.7.4) nokogiri (1.18.2) From 7afe02b36bf1a6acb0cf44e333e8b3a80cb37632 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Feb 2025 09:37:16 +0100 Subject: [PATCH 08/47] chore(deps): update dependency rack to v2.2.11 (#33900) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index cfca6f8ab1..e7f61119b4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -597,7 +597,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.10) + rack (2.2.11) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-cors (2.0.2) From 9ed1cb3c298eac010023fad7664d14511a2eae74 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 09:17:40 +0100 Subject: [PATCH 09/47] chore(deps): update dependency nokogiri to v1.18.3 [security] (#33961) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e7f61119b4..7c92629114 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -427,7 +427,7 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.2) + nokogiri (1.18.3) mini_portile2 (~> 2.8.2) racc (~> 1.4) oj (3.16.9) From 6515244a16368cb94d50b674b0fa63daf5e28644 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 25 Feb 2025 15:37:44 +0100 Subject: [PATCH 10/47] Fix `GET /api/v2/notifications/:id` and `POST /api/v2/notifications/:id/dismiss` for ungrouped notifications (#33990) --- .../api/v2/notifications_controller.rb | 4 +-- app/models/concerns/notification/groups.rb | 4 +++ spec/requests/api/v2/notifications_spec.rb | 25 +++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v2/notifications_controller.rb b/app/controllers/api/v2/notifications_controller.rb index cc38b95114..848c361cfc 100644 --- a/app/controllers/api/v2/notifications_controller.rb +++ b/app/controllers/api/v2/notifications_controller.rb @@ -46,7 +46,7 @@ class Api::V2::NotificationsController < Api::BaseController end def show - @notification = current_account.notifications.without_suspended.find_by!(group_key: params[:group_key]) + @notification = current_account.notifications.without_suspended.by_group_key(params[:group_key]).take! presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification])) render json: presenter, serializer: REST::DedupNotificationGroupSerializer end @@ -57,7 +57,7 @@ class Api::V2::NotificationsController < Api::BaseController end def dismiss - current_account.notifications.where(group_key: params[:group_key]).destroy_all + current_account.notifications.by_group_key(params[:group_key]).destroy_all render_empty end diff --git a/app/models/concerns/notification/groups.rb b/app/models/concerns/notification/groups.rb index e064df8502..c678ff9f50 100644 --- a/app/models/concerns/notification/groups.rb +++ b/app/models/concerns/notification/groups.rb @@ -7,6 +7,10 @@ module Notification::Groups GROUPABLE_NOTIFICATION_TYPES = %i(favourite reblog follow emoji_reaction).freeze MAXIMUM_GROUP_SPAN_HOURS = 12 + included do + scope :by_group_key, ->(group_key) { group_key&.start_with?('ungrouped-') ? where(id: group_key.delete_prefix('ungrouped-')) : where(group_key: group_key) } + end + def set_group_key! return if filtered? || GROUPABLE_NOTIFICATION_TYPES.exclude?(type) diff --git a/spec/requests/api/v2/notifications_spec.rb b/spec/requests/api/v2/notifications_spec.rb index b2f6d71b51..a7608e1419 100644 --- a/spec/requests/api/v2/notifications_spec.rb +++ b/spec/requests/api/v2/notifications_spec.rb @@ -365,6 +365,18 @@ RSpec.describe 'Notifications' do .to start_with('application/json') end + context 'with an ungrouped notification' do + let(:notification) { Fabricate(:notification, account: user.account, type: :favourite) } + + it 'returns http success' do + get "/api/v2/notifications/ungrouped-#{notification.id}", headers: headers + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + end + end + context 'when notification belongs to someone else' do let(:notification) { Fabricate(:notification, group_key: 'foobar') } @@ -396,6 +408,19 @@ RSpec.describe 'Notifications' do expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound) end + context 'with an ungrouped notification' do + let(:notification) { Fabricate(:notification, account: user.account, type: :favourite) } + + it 'destroys the notification' do + post "/api/v2/notifications/ungrouped-#{notification.id}/dismiss", headers: headers + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + context 'when notification belongs to someone else' do let(:notification) { Fabricate(:notification, group_key: 'foobar') } From e09adc6314e00ea4d0cda1c939df542f39b8660e Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 27 Feb 2025 15:44:35 +0100 Subject: [PATCH 11/47] Merge commit from fork * Add rate-limit on `/auth/setup` * Remove useless test --- config/initializers/rack_attack.rb | 10 +++++++++- spec/requests/auth/setup_spec.rb | 11 ----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index b4eaab1daa..f558ee5fe0 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -122,7 +122,7 @@ class Rack::Attack end throttle('throttle_email_confirmations/ip', limit: 25, period: 5.minutes) do |req| - req.throttleable_remote_ip if req.post? && (req.path_matches?('/auth/confirmation') || req.path == '/api/v1/emails/confirmations') + req.throttleable_remote_ip if (req.post? && (req.path_matches?('/auth/confirmation') || req.path == '/api/v1/emails/confirmations')) || ((req.put? || req.patch?) && req.path_matches?('/auth/setup')) end throttle('throttle_email_confirmations/email', limit: 5, period: 30.minutes) do |req| @@ -133,6 +133,14 @@ class Rack::Attack end end + throttle('throttle_auth_setup/email', limit: 5, period: 10.minutes) do |req| + req.params.dig('user', 'email').presence if (req.put? || req.patch?) && req.path_matches?('/auth/setup') + end + + throttle('throttle_auth_setup/account', limit: 5, period: 10.minutes) do |req| + req.warden_user_id if (req.put? || req.patch?) && req.path_matches?('/auth/setup') + end + throttle('throttle_login_attempts/ip', limit: 25, period: 5.minutes) do |req| req.throttleable_remote_ip if req.post? && req.path_matches?('/auth/sign_in') end diff --git a/spec/requests/auth/setup_spec.rb b/spec/requests/auth/setup_spec.rb index 72413e1740..fa3c196805 100644 --- a/spec/requests/auth/setup_spec.rb +++ b/spec/requests/auth/setup_spec.rb @@ -24,15 +24,4 @@ RSpec.describe 'Auth Setup' do end end end - - describe 'PUT /auth/setup' do - before { sign_in Fabricate(:user, confirmed_at: nil) } - - it 'gracefully handles invalid nested params' do - put '/auth/setup?user=invalid' - - expect(response) - .to have_http_status(400) - end - end end From 55e31110e381be9775555b098be9caefeb769e54 Mon Sep 17 00:00:00 2001 From: Jeremy Kescher Date: Thu, 27 Feb 2025 15:49:57 +0100 Subject: [PATCH 12/47] Merge commit from fork * Fix domain blocks/rationales being visible to unapproved/unconfirmed users * Fix domain blocks/rationales being visible to suspended users Co-authored-by: Claire * Allow moved users to view domain blocks * Add authorization specs for `/api/v1/instance/domain_blocks` spec * Fix tests * Fix incorrect test setup --------- Co-authored-by: Claire --- .../v1/instances/domain_blocks_controller.rb | 4 +- .../api/v1/instances/domain_blocks_spec.rb | 99 +++++++++++++++++-- 2 files changed, 94 insertions(+), 9 deletions(-) diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb index 7ec94312f4..bf96fbaaa8 100644 --- a/app/controllers/api/v1/instances/domain_blocks_controller.rb +++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb @@ -31,7 +31,7 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr end def show_domain_blocks_to_user? - Setting.show_domain_blocks == 'users' && user_signed_in? + Setting.show_domain_blocks == 'users' && user_signed_in? && current_user.functional_or_moved? end def set_domain_blocks @@ -47,6 +47,6 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr end def show_rationale_for_user? - Setting.show_domain_blocks_rationale == 'users' && user_signed_in? + Setting.show_domain_blocks_rationale == 'users' && user_signed_in? && current_user.functional_or_moved? end end diff --git a/spec/requests/api/v1/instances/domain_blocks_spec.rb b/spec/requests/api/v1/instances/domain_blocks_spec.rb index d0707d784c..b48298d6bc 100644 --- a/spec/requests/api/v1/instances/domain_blocks_spec.rb +++ b/spec/requests/api/v1/instances/domain_blocks_spec.rb @@ -9,9 +9,10 @@ RSpec.describe 'Domain Blocks' do let(:headers) { { Authorization: "Bearer #{token.token}" } } describe 'GET /api/v1/instance/domain_blocks' do - before do - Fabricate(:domain_block) - end + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id).token } + + before { Fabricate(:domain_block) } context 'with domain blocks set to all' do before { Setting.show_domain_blocks = 'all' } @@ -45,11 +46,95 @@ RSpec.describe 'Domain Blocks' do context 'with domain blocks set to users' do before { Setting.show_domain_blocks = 'users' } - it 'returns http not found' do - get api_v1_instance_domain_blocks_path + context 'without authentication token' do + it 'returns http not found' do + get api_v1_instance_domain_blocks_path - expect(response) - .to have_http_status(404) + expect(response) + .to have_http_status(404) + end + end + + context 'with authentication token' do + context 'with unapproved user' do + before { user.update(approved: false) } + + it 'returns http not found' do + get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" } + + expect(response) + .to have_http_status(404) + end + end + + context 'with unconfirmed user' do + before { user.update(confirmed_at: nil) } + + it 'returns http not found' do + get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" } + + expect(response) + .to have_http_status(404) + end + end + + context 'with disabled user' do + before { user.update(disabled: true) } + + it 'returns http not found' do + get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" } + + expect(response) + .to have_http_status(404) + end + end + + context 'with suspended user' do + before { user.account.update(suspended_at: Time.zone.now) } + + it 'returns http not found' do + get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" } + + expect(response) + .to have_http_status(403) + end + end + + context 'with moved user' do + before { user.account.update(moved_to_account_id: Fabricate(:account).id) } + + it 'returns http success' do + get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" } + + expect(response) + .to have_http_status(200) + + expect(response.content_type) + .to start_with('application/json') + + expect(response.parsed_body) + .to be_present + .and(be_an(Array)) + .and(have_attributes(size: 1)) + end + end + + context 'with normal user' do + it 'returns http success' do + get api_v1_instance_domain_blocks_path, headers: { 'Authorization' => "Bearer #{token}" } + + expect(response) + .to have_http_status(200) + + expect(response.content_type) + .to start_with('application/json') + + expect(response.parsed_body) + .to be_present + .and(be_an(Array)) + .and(have_attributes(size: 1)) + end + end end end From 8f25192072040fe7ed8b86493133f74a51e2f0ae Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 27 Feb 2025 11:12:58 +0100 Subject: [PATCH 13/47] Change HTML sanitization to remove unusable and unused `embed` tag (#34021) --- lib/sanitize_ext/sanitize_config.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb index 7815d9ed52..410a58b07e 100644 --- a/lib/sanitize_ext/sanitize_config.rb +++ b/lib/sanitize_ext/sanitize_config.rb @@ -155,18 +155,16 @@ class Sanitize ) MASTODON_OEMBED = freeze_config( - elements: %w(audio embed iframe source video), + elements: %w(audio iframe source video), attributes: { 'audio' => %w(controls), - 'embed' => %w(height src type width), 'iframe' => %w(allowfullscreen frameborder height scrolling src width), 'source' => %w(src type), 'video' => %w(controls height loop width), }, protocols: { - 'embed' => { 'src' => HTTP_PROTOCOLS }, 'iframe' => { 'src' => HTTP_PROTOCOLS }, 'source' => { 'src' => HTTP_PROTOCOLS }, }, From d1208a2cf5651e5b8ad4e8c856a73e4202660fe2 Mon Sep 17 00:00:00 2001 From: KMY Date: Fri, 28 Feb 2025 09:54:45 +0900 Subject: [PATCH 14/47] Bump version to 17.1 --- docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 47eb36698f..dbbb430acd 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: kmyblue:17.0 + image: kmyblue:17.1 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: kmyblue-streaming:17.0 + image: kmyblue-streaming:17.1 restart: always env_file: .env.production command: node ./streaming/index.js @@ -101,7 +101,7 @@ services: sidekiq: build: . - image: kmyblue:17.0 + image: kmyblue:17.1 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index f45b4a4305..688ad8b158 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def kmyblue_minor - 0 + 1 end def kmyblue_flag From d5ba371a5efa189896b009bc515f9666af8af71c Mon Sep 17 00:00:00 2001 From: KMY Date: Fri, 28 Feb 2025 13:08:04 +0900 Subject: [PATCH 15/47] Fix test --- spec/requests/api/v1/instances/domain_blocks_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/requests/api/v1/instances/domain_blocks_spec.rb b/spec/requests/api/v1/instances/domain_blocks_spec.rb index b48298d6bc..40b79c9691 100644 --- a/spec/requests/api/v1/instances/domain_blocks_spec.rb +++ b/spec/requests/api/v1/instances/domain_blocks_spec.rb @@ -4,9 +4,9 @@ require 'rails_helper' RSpec.describe 'Domain Blocks' do let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes).token } let(:scopes) { 'read' } - let(:headers) { { Authorization: "Bearer #{token.token}" } } + let(:headers) { { Authorization: "Bearer #{token}" } } describe 'GET /api/v1/instance/domain_blocks' do let(:user) { Fabricate(:user) } From a4c43dcf1879f79828a7f6303bef3428d40b3e31 Mon Sep 17 00:00:00 2001 From: KMY Date: Fri, 28 Feb 2025 15:15:39 +0900 Subject: [PATCH 16/47] =?UTF-8?q?Fix:=20NG=E3=83=AF=E3=83=BC=E3=83=89?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E7=94=BB=E9=9D=A2=E3=81=AE=E3=82=A8=E3=83=A9?= =?UTF-8?q?=E3=83=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/ng_words/keywords_controller.rb | 4 ++++ app/controllers/admin/ng_words_controller.rb | 12 +++++++++++- app/controllers/system_css_controller.rb | 8 -------- app/views/custom_css/show_system.css.erb | 6 ------ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/controllers/admin/ng_words/keywords_controller.rb b/app/controllers/admin/ng_words/keywords_controller.rb index 9af38fab7b..10969204e8 100644 --- a/app/controllers/admin/ng_words/keywords_controller.rb +++ b/app/controllers/admin/ng_words/keywords_controller.rb @@ -21,6 +21,10 @@ module Admin false end + def avoid_save? + true + end + private def after_update_redirect_path diff --git a/app/controllers/admin/ng_words_controller.rb b/app/controllers/admin/ng_words_controller.rb index f052843475..f0d7142500 100644 --- a/app/controllers/admin/ng_words_controller.rb +++ b/app/controllers/admin/ng_words_controller.rb @@ -13,6 +13,12 @@ module Admin return unless validate + if avoid_save? + flash[:notice] = I18n.t('generic.changes_saved_msg') + redirect_to after_update_redirect_path + return + end + @admin_settings = Form::AdminSettings.new(settings_params) if @admin_settings.save @@ -33,6 +39,10 @@ module Admin admin_ng_words_path end + def avoid_save? + false + end + private def settings_params @@ -40,7 +50,7 @@ module Admin end def settings_params_test - params.require(:form_admin_settings)[:ng_words_test] + params.expect(form_admin_settings: [ng_words_test: [keywords: [], strangers: [], temporary_ids: []]])['ng_words_test'] end end end diff --git a/app/controllers/system_css_controller.rb b/app/controllers/system_css_controller.rb index a19728bbfd..dd90491894 100644 --- a/app/controllers/system_css_controller.rb +++ b/app/controllers/system_css_controller.rb @@ -1,16 +1,8 @@ # frozen_string_literal: true class SystemCssController < ActionController::Base # rubocop:disable Rails/ApplicationController - before_action :set_user_roles - def show expires_in 3.minutes, public: true render content_type: 'text/css' end - - private - - def set_user_roles - @user_roles = UserRole.providing_styles - end end diff --git a/app/views/custom_css/show_system.css.erb b/app/views/custom_css/show_system.css.erb index 72f0281aa1..e69de29bb2 100644 --- a/app/views/custom_css/show_system.css.erb +++ b/app/views/custom_css/show_system.css.erb @@ -1,6 +0,0 @@ -<%- @user_roles.each do |role| %> -.user-role-<%= role.id %> { - --user-role-accent: <%= role.color %>; -} - -<%- end %> From ac26f5f48ad5256a626d7719672dffb21350f111 Mon Sep 17 00:00:00 2001 From: KMY Date: Fri, 28 Feb 2025 15:18:11 +0900 Subject: [PATCH 17/47] Fix system css --- app/views/system_css/show.css.erb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/views/system_css/show.css.erb b/app/views/system_css/show.css.erb index 72f0281aa1..e69de29bb2 100644 --- a/app/views/system_css/show.css.erb +++ b/app/views/system_css/show.css.erb @@ -1,6 +0,0 @@ -<%- @user_roles.each do |role| %> -.user-role-<%= role.id %> { - --user-role-accent: <%= role.color %>; -} - -<%- end %> From 3cba3a6af36b99fe5e17eb2fcf4b3d02a554b3e4 Mon Sep 17 00:00:00 2001 From: KMY Date: Fri, 28 Feb 2025 16:51:15 +0900 Subject: [PATCH 18/47] Fix regexp --- app/controllers/admin/ng_words_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/ng_words_controller.rb b/app/controllers/admin/ng_words_controller.rb index f0d7142500..9e437f8c8b 100644 --- a/app/controllers/admin/ng_words_controller.rb +++ b/app/controllers/admin/ng_words_controller.rb @@ -50,7 +50,7 @@ module Admin end def settings_params_test - params.expect(form_admin_settings: [ng_words_test: [keywords: [], strangers: [], temporary_ids: []]])['ng_words_test'] + params.expect(form_admin_settings: [ng_words_test: [keywords: [], regexps: [], strangers: [], temporary_ids: []]])['ng_words_test'] end end end From 9d8f5fd45dea1eba40f03116f32ec2ee9dc5b6f1 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 5 Mar 2025 09:43:31 +0100 Subject: [PATCH 19/47] Fix preview cards under Content Warnings not being shown in detailed statuses (#34068) --- .../mastodon/features/status/components/detailed_status.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/status/components/detailed_status.tsx b/app/javascript/mastodon/features/status/components/detailed_status.tsx index 1540f50065..c8f082156e 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.tsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.tsx @@ -237,7 +237,7 @@ export const DetailedStatus: React.FC<{ ); } From 6e81109b6607185e91ed89f68997431374b4e9d4 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 21 Feb 2025 08:48:31 +0100 Subject: [PATCH 20/47] Fix streaming server not filtering unknown-language posts from public timelines (#33774) --- streaming/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/streaming/index.js b/streaming/index.js index 4b6817d8fd..ff3ef1c7ba 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -689,7 +689,7 @@ const startServer = async () => { // filtering of statuses: // Filter based on language: - if (Array.isArray(req.chosenLanguages) && payload.language !== null && req.chosenLanguages.indexOf(payload.language) === -1) { + if (Array.isArray(req.chosenLanguages) && req.chosenLanguages.indexOf(payload.language) === -1) { log.debug(`Message ${payload.id} filtered by language (${payload.language})`); return; } From 5ef848aa9c0453ae96b46a636af6861aad24f96f Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Thu, 6 Mar 2025 11:44:27 +0100 Subject: [PATCH 21/47] Fix processing errors for some HEIF images from iOS 18 (#34086) --- Dockerfile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4e1bb24ff8..588b100107 100644 --- a/Dockerfile +++ b/Dockerfile @@ -96,6 +96,9 @@ RUN \ # Set /opt/mastodon as working directory WORKDIR /opt/mastodon +# Add backport repository for some specific packages where we need the latest version +RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list + # hadolint ignore=DL3008,DL3005 RUN \ # Mount Apt cache and lib directories from Docker buildx caches @@ -165,7 +168,7 @@ RUN \ libexif-dev \ libexpat1-dev \ libgirepository1.0-dev \ - libheif-dev \ + libheif-dev/bookworm-backports \ libimagequant-dev \ libjpeg62-turbo-dev \ liblcms2-dev \ @@ -348,7 +351,7 @@ RUN \ # libvips components libcgif0 \ libexif12 \ - libheif1 \ + libheif1/bookworm-backports \ libimagequant0 \ libjpeg62-turbo \ liblcms2-2 \ From 090f9eaf1543470c633c145c475a1bb63f5c84a1 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 6 Mar 2025 11:00:33 +0100 Subject: [PATCH 22/47] Change hashtag suggestion to prefer personal history capitalization (#34070) --- app/javascript/mastodon/reducers/compose.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 1f0011448f..9b69663c45 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -342,12 +342,26 @@ const expiresInFromExpiresAt = expires_at => { const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => { prefix = prefix.toLowerCase(); + if (suggestions.length < 4) { const localTags = tagHistory.filter(tag => tag.toLowerCase().startsWith(prefix) && !suggestions.some(suggestion => suggestion.type === 'hashtag' && suggestion.name.toLowerCase() === tag.toLowerCase())); - return suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag }))); - } else { - return suggestions; + suggestions = suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag }))); } + + // Prefer capitalization from personal history, unless personal history is all lower-case + const fixSuggestionCapitalization = (suggestion) => { + if (suggestion.type !== 'hashtag') + return suggestion; + + const tagFromHistory = tagHistory.find((tag) => tag.localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) === 0); + + if (!tagFromHistory || tagFromHistory.toLowerCase() === tagFromHistory) + return suggestion; + + return { ...suggestion, name: tagFromHistory }; + }; + + return suggestions.map(fixSuggestionCapitalization); }; const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => { From 0beef81affd74774ab6421bbe3e75e3eefb963ac Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 10 Mar 2025 19:26:43 +0900 Subject: [PATCH 23/47] Bump version to 17.2 --- docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index dbbb430acd..7281ef595c 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: kmyblue:17.1 + image: kmyblue:17.2 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: kmyblue-streaming:17.1 + image: kmyblue-streaming:17.2 restart: always env_file: .env.production command: node ./streaming/index.js @@ -101,7 +101,7 @@ services: sidekiq: build: . - image: kmyblue:17.1 + image: kmyblue:17.2 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 688ad8b158..dd6571b5c9 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def kmyblue_minor - 1 + 2 end def kmyblue_flag From f0b525ccd8e9093aef2eee320267340f3f23213b Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 10 Mar 2025 15:27:43 +0100 Subject: [PATCH 24/47] Fix Stoplight errors when using `REDIS_NAMESPACE` (#34126) --- lib/redis/namespace_extensions.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/redis/namespace_extensions.rb b/lib/redis/namespace_extensions.rb index 9af59c296e..2be738b04d 100644 --- a/lib/redis/namespace_extensions.rb +++ b/lib/redis/namespace_extensions.rb @@ -5,6 +5,10 @@ class Redis def exists?(...) call_with_namespace('exists?', ...) end + + def with + yield self + end end end From d2962a525636ae20a425b7813f240319cf494538 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 09:41:25 +0100 Subject: [PATCH 25/47] chore(deps): update dependency rack to v2.2.13 [security] (#34135) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7c92629114..a51775b48a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -597,7 +597,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.11) + rack (2.2.13) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-cors (2.0.2) From f045fba749e5c05a8a37f29c6c4e9e71129c4fd4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 09:38:44 +0100 Subject: [PATCH 26/47] Update dependency omniauth-saml to v2.2.3 [SECURITY] (#34156) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a51775b48a..dbb677cf8a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -444,9 +444,9 @@ GEM omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) - omniauth-saml (2.2.1) + omniauth-saml (2.2.3) omniauth (~> 2.1) - ruby-saml (~> 1.17) + ruby-saml (~> 1.18) omniauth_openid_connect (0.6.1) omniauth (>= 1.9, < 3) openid_connect (~> 1.1) @@ -745,7 +745,7 @@ GEM rubocop-rspec (~> 3, >= 3.0.1) ruby-prof (1.7.1) ruby-progressbar (1.13.0) - ruby-saml (1.17.0) + ruby-saml (1.18.0) nokogiri (>= 1.13.10) rexml ruby-vips (2.2.3) From 8445fa183d931da9e4e7940d74128d81629af577 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 13 Mar 2025 15:27:20 +0100 Subject: [PATCH 27/47] Bump version to v4.3.6 (#34167) --- CHANGELOG.md | 59 +++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 6 ++--- lib/mastodon/version.rb | 4 +-- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef6a87ebb9..c7b0f64146 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,65 @@ All notable changes to this project will be documented in this file. +## [4.3.6] - 2025-03-13 + +### Security + +- Update dependency `omniauth-saml` +- Update dependency `rack` + +### Fixed + +- Fix Stoplight errors when using `REDIS_NAMESPACE` (#34126 by @ClearlyClaire) + +## [4.3.5] - 2025-03-10 + +### Changed + +- Change hashtag suggestion to prefer personal history capitalization (#34070 by @ClearlyClaire) + +### Fixed + +- Fix processing errors for some HEIF images from iOS 18 (#34086 by @renchap) +- Fix streaming server not filtering unknown-language posts from public timelines (#33774 by @ClearlyClaire) +- Fix preview cards under Content Warnings not being shown in detailed statuses (#34068 by @ClearlyClaire) +- Fix username and display name being hidden on narrow screens in moderation interface (#33064 by @ClearlyClaire) + +## [4.3.4] - 2025-02-27 + +### Security + +- Update dependencies +- Change HTML sanitization to remove unusable and unused `embed` tag (#34021 by @ClearlyClaire, [GHSA-mq2m-hr29-8gqf](https://github.com/mastodon/mastodon/security/advisories/GHSA-mq2m-hr29-8gqf)) +- Fix rate-limit on sign-up email verification ([GHSA-v39f-c9jj-8w7h](https://github.com/mastodon/mastodon/security/advisories/GHSA-v39f-c9jj-8w7h)) +- Fix improper disclosure of domain blocks to unverified users ([GHSA-94h4-fj37-c825](https://github.com/mastodon/mastodon/security/advisories/GHSA-94h4-fj37-c825)) + +### Changed + +- Change preview cards to be shown when Content Warnings are expanded (#33827 by @ClearlyClaire) +- Change warnings against changing encryption secrets to be even more noticeable (#33631 by @ClearlyClaire) +- Change `mastodon:setup` to prevent overwriting already-configured servers (#33603, #33616, and #33684 by @ClearlyClaire and @mjankowski) +- Change notifications from moderators to not be filtered (#32974 and #33654 by @ClearlyClaire and @mjankowski) + +### Fixed + +- Fix `GET /api/v2/notifications/:id` and `POST /api/v2/notifications/:id/dismiss` for ungrouped notifications (#33990 by @ClearlyClaire) +- Fix issue with some versions of libvips on some systems (#33853 by @kleisauke) +- Fix handling of duplicate mentions in incoming status `Update` (#33911 by @ClearlyClaire) +- Fix inefficiencies in timeline generation (#33839 and #33842 by @ClearlyClaire) +- Fix emoji rewrite adding unnecessary curft to the DOM for most emoji (#33818 by @ClearlyClaire) +- Fix `tootctl feeds build` not building list timelines (#33783 by @ClearlyClaire) +- Fix flaky test in `/api/v2/notifications` tests (#33773 by @ClearlyClaire) +- Fix incorrect signature after HTTP redirect (#33757 and #33769 by @ClearlyClaire) +- Fix polls not being validated on edition (#33755 by @ClearlyClaire) +- Fix media preview height in compose form when 3 or more images are attached (#33571 by @ClearlyClaire) +- Fix preview card sizing in โ€œAuthor attributionโ€ in profile settings (#33482 by @ClearlyClaire) +- Fix processing of incoming notifications for unfilterable types (#33429 by @ClearlyClaire) +- Fix featured tags for remote accounts not being kept up to date (#33372, #33406, and #33425 by @ClearlyClaire and @mjankowski) +- Fix notification polling showing a loading bar in web UI (#32960 by @Gargron) +- Fix accounts table long display name (#29316 by @WebCoder49) +- Fix exclusive lists interfering with notifications (#28162 by @ShadowJonathan) + ## [4.3.3] - 2025-01-16 ### Security diff --git a/docker-compose.yml b/docker-compose.yml index 7281ef595c..56c734318e 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: kmyblue:17.2 + image: kmyblue:17.3 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: kmyblue-streaming:17.2 + image: kmyblue-streaming:17.3 restart: always env_file: .env.production command: node ./streaming/index.js @@ -101,7 +101,7 @@ services: sidekiq: build: . - image: kmyblue:17.2 + image: kmyblue:17.3 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index dd6571b5c9..d523a1ef84 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def kmyblue_minor - 2 + 3 end def kmyblue_flag @@ -35,7 +35,7 @@ module Mastodon end def default_prerelease - 'alpha.2' + 'alpha.4' end def prerelease From 8bd585a0facbafdfe554ff03009133fd57254a6b Mon Sep 17 00:00:00 2001 From: KMY Date: Fri, 14 Mar 2025 08:28:19 +0900 Subject: [PATCH 28/47] Fix bundler-audit --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index dbb677cf8a..c11a970bc2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -845,7 +845,7 @@ GEM unf_ext unf_ext (0.0.9.1) unicode-display_width (2.6.0) - uri (1.0.2) + uri (1.0.3) useragent (0.16.11) validate_email (0.1.6) activemodel (>= 3.0) From 4dd07dfa1651c61abee2b27785c8cbaa882a1fda Mon Sep 17 00:00:00 2001 From: KMY Date: Fri, 14 Mar 2025 09:15:46 +0900 Subject: [PATCH 29/47] Fix test --- spec/system/report_interface_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/system/report_interface_spec.rb b/spec/system/report_interface_spec.rb index 6a90aa5bc6..1c61774594 100644 --- a/spec/system/report_interface_spec.rb +++ b/spec/system/report_interface_spec.rb @@ -18,6 +18,7 @@ RSpec.describe 'report interface', :attachment_processing, :js, :streaming do before do as_a_logged_in_admin visit admin_report_path(report) + page.driver.browser.manage.window.resize_to(1600, 1050) end it 'displays the report interface, including the javascript bits' do From 4c49ac2a074467e657e417e35e45e1cfee8942e5 Mon Sep 17 00:00:00 2001 From: KMY Date: Fri, 14 Mar 2025 09:32:35 +0900 Subject: [PATCH 30/47] Test --- lib/redis/namespace_extensions.rb | 4 ---- spec/system/report_interface_spec.rb | 1 - 2 files changed, 5 deletions(-) diff --git a/lib/redis/namespace_extensions.rb b/lib/redis/namespace_extensions.rb index 2be738b04d..9af59c296e 100644 --- a/lib/redis/namespace_extensions.rb +++ b/lib/redis/namespace_extensions.rb @@ -5,10 +5,6 @@ class Redis def exists?(...) call_with_namespace('exists?', ...) end - - def with - yield self - end end end diff --git a/spec/system/report_interface_spec.rb b/spec/system/report_interface_spec.rb index 1c61774594..6a90aa5bc6 100644 --- a/spec/system/report_interface_spec.rb +++ b/spec/system/report_interface_spec.rb @@ -18,7 +18,6 @@ RSpec.describe 'report interface', :attachment_processing, :js, :streaming do before do as_a_logged_in_admin visit admin_report_path(report) - page.driver.browser.manage.window.resize_to(1600, 1050) end it 'displays the report interface, including the javascript bits' do From 4a5bf16e73f43fd67d3b8e297b909389e426da17 Mon Sep 17 00:00:00 2001 From: KMY Date: Fri, 14 Mar 2025 09:37:59 +0900 Subject: [PATCH 31/47] Test --- lib/redis/namespace_extensions.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/redis/namespace_extensions.rb b/lib/redis/namespace_extensions.rb index 9af59c296e..2be738b04d 100644 --- a/lib/redis/namespace_extensions.rb +++ b/lib/redis/namespace_extensions.rb @@ -5,6 +5,10 @@ class Redis def exists?(...) call_with_namespace('exists?', ...) end + + def with + yield self + end end end From 6a672ec2276e24a12f995ade875eaf4f8df430bb Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 24 Mar 2025 15:07:27 +0100 Subject: [PATCH 32/47] Fix streaming server refusing unix socket path in `DATABASE_URL` (#34091) --- streaming/database.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/streaming/database.js b/streaming/database.js index 60a3b34ef0..553c9149cc 100644 --- a/streaming/database.js +++ b/streaming/database.js @@ -49,7 +49,7 @@ export function configFromEnv(env, environment) { if (typeof parsedUrl.password === 'string') baseConfig.password = parsedUrl.password; if (typeof parsedUrl.host === 'string') baseConfig.host = parsedUrl.host; if (typeof parsedUrl.user === 'string') baseConfig.user = parsedUrl.user; - if (typeof parsedUrl.port === 'string') { + if (typeof parsedUrl.port === 'string' && parsedUrl.port) { const parsedPort = parseInt(parsedUrl.port, 10); if (isNaN(parsedPort)) { throw new Error('Invalid port specified in DATABASE_URL environment variable'); From f9b715ca62edccd8c1c5ed1d079e80083a7085f7 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 17 Mar 2025 17:40:28 +0100 Subject: [PATCH 33/47] Fix incorrect URL being used when cache busting (#34189) --- app/models/media_attachment.rb | 2 +- app/services/suspend_account_service.rb | 2 +- app/services/unsuspend_account_service.rb | 2 +- spec/models/media_attachment_spec.rb | 8 ++--- spec/services/suspend_account_service_spec.rb | 30 ++++++++----------- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 49ff740884..74af6a7ec2 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -426,7 +426,7 @@ class MediaAttachment < ApplicationRecord @paths_to_cache_bust = MediaAttachment.attachment_definitions.keys.flat_map do |attachment_name| attachment = public_send(attachment_name) styles = DEFAULT_STYLES | attachment.styles.keys - styles.map { |style| attachment.path(style) } + styles.map { |style| attachment.url(style) } end.compact rescue => e # We really don't want any error here preventing media deletion diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 44210799f9..3934a738f7 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -95,7 +95,7 @@ class SuspendAccountService < BaseService end end - CacheBusterWorker.perform_async(attachment.path(style)) if Rails.configuration.x.cache_buster_enabled + CacheBusterWorker.perform_async(attachment.url(style)) if Rails.configuration.x.cache_buster_enabled end end end diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb index 652dd6a845..7d3bb806a6 100644 --- a/app/services/unsuspend_account_service.rb +++ b/app/services/unsuspend_account_service.rb @@ -91,7 +91,7 @@ class UnsuspendAccountService < BaseService end end - CacheBusterWorker.perform_async(attachment.path(style)) if Rails.configuration.x.cache_buster_enabled + CacheBusterWorker.perform_async(attachment.url(style)) if Rails.configuration.x.cache_buster_enabled end end end diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index 5f91ae0967..bf818c1e1e 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -295,12 +295,12 @@ RSpec.describe MediaAttachment, :attachment_processing do end it 'queues CacheBusterWorker jobs' do - original_path = media.file.path(:original) - small_path = media.file.path(:small) + original_url = media.file.url(:original) + small_url = media.file.url(:small) expect { media.destroy } - .to enqueue_sidekiq_job(CacheBusterWorker).with(original_path) - .and enqueue_sidekiq_job(CacheBusterWorker).with(small_path) + .to enqueue_sidekiq_job(CacheBusterWorker).with(original_url) + .and enqueue_sidekiq_job(CacheBusterWorker).with(small_url) end end diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb index 4a2f494e0c..c15c23ca30 100644 --- a/spec/services/suspend_account_service_spec.rb +++ b/spec/services/suspend_account_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe SuspendAccountService, :inline_jobs do +RSpec.describe SuspendAccountService do shared_examples 'common behavior' do subject { described_class.new.call(account) } @@ -11,6 +11,7 @@ RSpec.describe SuspendAccountService, :inline_jobs do before do allow(FeedManager.instance).to receive_messages(unmerge_from_home: nil, unmerge_from_list: nil) + allow(Rails.configuration.x).to receive(:cache_buster_enabled).and_return(true) local_follower.follow!(account) list.accounts << account @@ -23,6 +24,7 @@ RSpec.describe SuspendAccountService, :inline_jobs do it 'unmerges from feeds of local followers and changes file mode and preserves suspended flag' do expect { subject } .to change_file_mode + .and enqueue_sidekiq_job(CacheBusterWorker).with(account.media_attachments.first.file.url(:original)) .and not_change_suspended_flag expect(FeedManager.instance).to have_received(:unmerge_from_home).with(account, local_follower) expect(FeedManager.instance).to have_received(:unmerge_from_list).with(account, list) @@ -38,17 +40,12 @@ RSpec.describe SuspendAccountService, :inline_jobs do end describe 'suspending a local account' do - def match_update_actor_request(req, account) - json = JSON.parse(req.body) + def match_update_actor_request(json, account) + json = JSON.parse(json) actor_id = ActivityPub::TagManager.instance.uri_for(account) json['type'] == 'Update' && json['actor'] == actor_id && json['object']['id'] == actor_id && json['object']['suspended'] end - before do - stub_request(:post, 'https://alice.com/inbox').to_return(status: 201) - stub_request(:post, 'https://bob.com/inbox').to_return(status: 201) - end - include_examples 'common behavior' do let!(:account) { Fabricate(:account) } let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub, domain: 'alice.com') } @@ -61,22 +58,20 @@ RSpec.describe SuspendAccountService, :inline_jobs do it 'sends an Update actor activity to followers and reporters' do subject - expect(a_request(:post, remote_follower.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once - expect(a_request(:post, remote_reporter.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(satisfying { |json| match_update_actor_request(json, account) }, account.id, remote_follower.inbox_url).once + .and have_enqueued_sidekiq_job(satisfying { |json| match_update_actor_request(json, account) }, account.id, remote_reporter.inbox_url).once end end end describe 'suspending a remote account' do - def match_reject_follow_request(req, account, followee) - json = JSON.parse(req.body) + def match_reject_follow_request(json, account, followee) + json = JSON.parse(json) json['type'] == 'Reject' && json['actor'] == ActivityPub::TagManager.instance.uri_for(followee) && json['object']['actor'] == account.uri end - before do - stub_request(:post, 'https://bob.com/inbox').to_return(status: 201) - end - include_examples 'common behavior' do let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) } let!(:local_followee) { Fabricate(:account) } @@ -88,7 +83,8 @@ RSpec.describe SuspendAccountService, :inline_jobs do it 'sends a Reject Follow activity', :aggregate_failures do subject - expect(a_request(:post, account.inbox_url).with { |req| match_reject_follow_request(req, account, local_followee) }).to have_been_made.once + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(satisfying { |json| match_reject_follow_request(json, account, local_followee) }, local_followee.id, account.inbox_url).once end end end From b84d2fc860f1ca70224d4bf62c64b504af76fef2 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 24 Mar 2025 09:58:15 +0100 Subject: [PATCH 34/47] Fix `CacheBuster` being queued for missing media attachments (#34253) --- app/models/media_attachment.rb | 2 ++ spec/models/media_attachment_spec.rb | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 74af6a7ec2..3eaa978c27 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -425,6 +425,8 @@ class MediaAttachment < ApplicationRecord @paths_to_cache_bust = MediaAttachment.attachment_definitions.keys.flat_map do |attachment_name| attachment = public_send(attachment_name) + next if attachment.blank? + styles = DEFAULT_STYLES | attachment.styles.keys styles.map { |style| attachment.url(style) } end.compact diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index bf818c1e1e..43e9ed087b 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -302,6 +302,15 @@ RSpec.describe MediaAttachment, :attachment_processing do .to enqueue_sidekiq_job(CacheBusterWorker).with(original_url) .and enqueue_sidekiq_job(CacheBusterWorker).with(small_url) end + + context 'with a missing remote attachment' do + let(:media) { Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png', file: nil) } + + it 'does not queue CacheBusterWorker jobs' do + expect { media.destroy } + .to_not enqueue_sidekiq_job(CacheBusterWorker) + end + end end private From 21812a0a7889e97f2b069cdaba876dfe9e6fba22 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 18 Mar 2025 15:50:41 +0100 Subject: [PATCH 35/47] Fix handling of malformed/unusual HTML (#34201) --- app/helpers/admin/trends/statuses_helper.rb | 17 ++++++++++++----- app/lib/emoji_formatter.rb | 10 +++++++++- app/lib/plain_text_formatter.rb | 10 +++++++++- app/models/account/field.rb | 9 ++++++++- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb index c7a59660cf..33da1f7216 100644 --- a/app/helpers/admin/trends/statuses_helper.rb +++ b/app/helpers/admin/trends/statuses_helper.rb @@ -2,11 +2,18 @@ module Admin::Trends::StatusesHelper def one_line_preview(status) - text = if status.local? - status.text.split("\n").first - else - Nokogiri::HTML5(status.text).css('html > body > *').first&.text - end + text = begin + if status.local? + status.text.split("\n").first + else + Nokogiri::HTML5(status.text).css('html > body > *').first&.text + end + rescue ArgumentError + # This can happen if one of the Nokogumbo limits is encountered + # Unfortunately, it does not use a more precise error class + # nor allows more graceful handling + '' + end return '' if text.blank? diff --git a/app/lib/emoji_formatter.rb b/app/lib/emoji_formatter.rb index c0302767ef..1574d4588d 100644 --- a/app/lib/emoji_formatter.rb +++ b/app/lib/emoji_formatter.rb @@ -24,7 +24,15 @@ class EmojiFormatter def to_s return html if custom_emojis.empty? || html.blank? - tree = Nokogiri::HTML5.fragment(html) + begin + tree = Nokogiri::HTML5.fragment(html) + rescue ArgumentError + # This can happen if one of the Nokogumbo limits is encountered + # Unfortunately, it does not use a more precise error class + # nor allows more graceful handling + return '' + end + tree.xpath('./text()|.//text()[not(ancestor[@class="invisible"])]').to_a.each do |node| i = -1 inside_shortname = false diff --git a/app/lib/plain_text_formatter.rb b/app/lib/plain_text_formatter.rb index f960ba7acc..e8ff79806f 100644 --- a/app/lib/plain_text_formatter.rb +++ b/app/lib/plain_text_formatter.rb @@ -16,7 +16,15 @@ class PlainTextFormatter if local? text else - node = Nokogiri::HTML5.fragment(insert_newlines) + begin + node = Nokogiri::HTML5.fragment(insert_newlines) + rescue ArgumentError + # This can happen if one of the Nokogumbo limits is encountered + # Unfortunately, it does not use a more precise error class + # nor allows more graceful handling + return '' + end + # Elements that are entirely removed with our Sanitize config node.xpath('.//iframe|.//math|.//noembed|.//noframes|.//noscript|.//plaintext|.//script|.//style|.//svg|.//xmp').remove node.text.chomp diff --git a/app/models/account/field.rb b/app/models/account/field.rb index bcd89015de..4b3ccea9c4 100644 --- a/app/models/account/field.rb +++ b/app/models/account/field.rb @@ -73,7 +73,14 @@ class Account::Field < ActiveModelSerializers::Model end def extract_url_from_html - doc = Nokogiri::HTML5.fragment(value) + begin + doc = Nokogiri::HTML5.fragment(value) + rescue ArgumentError + # This can happen if one of the Nokogumbo limits is encountered + # Unfortunately, it does not use a more precise error class + # nor allows more graceful handling + return + end return if doc.nil? return if doc.children.size != 1 From ff2ba559e8689f3d8b8a197f63ad1e9ea90a7cab Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 24 Mar 2025 10:59:05 +0100 Subject: [PATCH 36/47] Change user archive signed URL TTL from 10 seconds to 1 hour (#34254) --- app/controllers/backups_controller.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb index 5df1af5f2f..076d19874b 100644 --- a/app/controllers/backups_controller.rb +++ b/app/controllers/backups_controller.rb @@ -9,13 +9,15 @@ class BackupsController < ApplicationController before_action :authenticate_user! before_action :set_backup + BACKUP_LINK_TIMEOUT = 1.hour.freeze + def download case Paperclip::Attachment.default_options[:storage] when :s3, :azure - redirect_to @backup.dump.expiring_url(10), allow_other_host: true + redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.to_i), allow_other_host: true when :fog if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? - redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true + redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.from_now), allow_other_host: true else redirect_to full_asset_url(@backup.dump.url), allow_other_host: true end From 853b84810903821902fb502b4819e65179dbd295 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 25 Mar 2025 14:11:49 +0100 Subject: [PATCH 37/47] Fix filters not applying in detailed view (#34259) --- .../status/components/detailed_status.tsx | 37 ++++++++++++++----- .../mastodon/features/status/index.jsx | 2 +- app/javascript/mastodon/selectors/index.js | 5 ++- app/javascript/mastodon/utils/filters.ts | 2 + 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/app/javascript/mastodon/features/status/components/detailed_status.tsx b/app/javascript/mastodon/features/status/components/detailed_status.tsx index c8f082156e..47d1692804 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.tsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.tsx @@ -15,6 +15,7 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re import { AnimatedNumber } from 'mastodon/components/animated_number'; import { ContentWarning } from 'mastodon/components/content_warning'; import EditedTimestamp from 'mastodon/components/edited_timestamp'; +import { FilterWarning } from 'mastodon/components/filter_warning'; import type { StatusLike } from 'mastodon/components/hashtag_bar'; import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar'; import { Icon } from 'mastodon/components/icon'; @@ -80,6 +81,7 @@ export const DetailedStatus: React.FC<{ }) => { const properStatus = status?.get('reblog') ?? status; const [height, setHeight] = useState(0); + const [showDespiteFilter, setShowDespiteFilter] = useState(false); const nodeRef = useRef(); const handleOpenVideo = useCallback( @@ -92,6 +94,10 @@ export const DetailedStatus: React.FC<{ [onOpenVideo, status], ); + const handleFilterToggle = useCallback(() => { + setShowDespiteFilter(!showDespiteFilter); + }, [showDespiteFilter, setShowDespiteFilter]); + const handleExpandedToggle = useCallback(() => { if (onToggleHidden) onToggleHidden(status); }, [onToggleHidden, status]); @@ -369,8 +375,12 @@ export const DetailedStatus: React.FC<{ const { statusContentProps, hashtagBar } = getHashtagBarForStatus( status as StatusLike, ); + + const matchedFilters = status.get('matched_filters'); + const expanded = - !status.get('hidden') || status.get('spoiler_text').length === 0; + (!matchedFilters || showDespiteFilter) && + (!status.get('hidden') || status.get('spoiler_text').length === 0); const quote = !muted && status.get('quote_id') && ( <> @@ -418,17 +428,26 @@ export const DetailedStatus: React.FC<{ )} - {status.get('spoiler_text').length > 0 && ( - )} + {status.get('spoiler_text').length > 0 && + (!matchedFilters || showDespiteFilter) && ( + + )} + {expanded && ( <> { }); const mapStateToProps = (state, props) => { - const status = getStatus(state, { id: props.params.statusId }); + const status = getStatus(state, { id: props.params.statusId, contextType: 'detailed' }); let ancestorsIds = ImmutableList(); let descendantsIds = ImmutableList(); diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 85773152f2..40bcf2ef41 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -17,9 +17,10 @@ export const makeGetStatus = () => { (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), getFilters, + (_, { contextType }) => contextType === 'detailed', ], - (statusBase, statusReblog, statusQuote, statusReblogQuote, accountBase, accountReblog, filters) => { + (statusBase, statusReblog, statusQuote, statusReblogQuote, accountBase, accountReblog, filters, warnInsteadOfHide) => { if (!statusBase || statusBase.get('isLoading')) { return null; } @@ -46,7 +47,7 @@ export const makeGetStatus = () => { } } - if (filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) { + if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) { return null; } filterResults = filterResults.filter(result => filters.has(result.get('filter'))); diff --git a/app/javascript/mastodon/utils/filters.ts b/app/javascript/mastodon/utils/filters.ts index 5d334fe509..543e03e516 100644 --- a/app/javascript/mastodon/utils/filters.ts +++ b/app/javascript/mastodon/utils/filters.ts @@ -7,6 +7,8 @@ export const toServerSideType = (columnType: string) => { case 'account': case 'explore': return columnType; + case 'detailed': + return 'thread'; default: if (columnType.includes('list:') || columnType.includes('antenna:')) { return 'home'; From fb7d01c6bd917d7ed814783d0c63855968d349f2 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 25 Mar 2025 16:20:36 +0100 Subject: [PATCH 38/47] Fix bookmarks and favourites not being filtered (#34260) --- app/javascript/mastodon/features/bookmarked_statuses/index.jsx | 1 + app/javascript/mastodon/features/favourited_statuses/index.jsx | 1 + app/javascript/mastodon/selectors/index.js | 2 +- app/javascript/mastodon/utils/filters.ts | 3 +++ 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx b/app/javascript/mastodon/features/bookmarked_statuses/index.jsx index 93be1c6b2e..284342d4ee 100644 --- a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx +++ b/app/javascript/mastodon/features/bookmarked_statuses/index.jsx @@ -101,6 +101,7 @@ class Bookmarks extends ImmutablePureComponent { onLoadMore={this.handleLoadMore} emptyMessage={emptyMessage} bindToDocument={!multiColumn} + timelineId='bookmarks' /> diff --git a/app/javascript/mastodon/features/favourited_statuses/index.jsx b/app/javascript/mastodon/features/favourited_statuses/index.jsx index f7d6d14178..9049a20f05 100644 --- a/app/javascript/mastodon/features/favourited_statuses/index.jsx +++ b/app/javascript/mastodon/features/favourited_statuses/index.jsx @@ -101,6 +101,7 @@ class Favourites extends ImmutablePureComponent { onLoadMore={this.handleLoadMore} emptyMessage={emptyMessage} bindToDocument={!multiColumn} + timelineId='favourites' /> diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 40bcf2ef41..c96309bb6a 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -17,7 +17,7 @@ export const makeGetStatus = () => { (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), getFilters, - (_, { contextType }) => contextType === 'detailed', + (_, { contextType }) => ['detailed', 'bookmarks', 'favourites'].includes(contextType), ], (statusBase, statusReblog, statusQuote, statusReblogQuote, accountBase, accountReblog, filters, warnInsteadOfHide) => { diff --git a/app/javascript/mastodon/utils/filters.ts b/app/javascript/mastodon/utils/filters.ts index 543e03e516..479e1f44ab 100644 --- a/app/javascript/mastodon/utils/filters.ts +++ b/app/javascript/mastodon/utils/filters.ts @@ -9,6 +9,9 @@ export const toServerSideType = (columnType: string) => { return columnType; case 'detailed': return 'thread'; + case 'bookmarks': + case 'favourites': + return 'home'; default: if (columnType.includes('list:') || columnType.includes('antenna:')) { return 'home'; From 85e8a1cb4fd5612aefcefa484e8560bcd7ed78d5 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 25 Mar 2025 16:50:05 +0100 Subject: [PATCH 39/47] Fix follower synchronization mechanism erroneously removing followers from multi-page collections (#34272) --- .../synchronize_followers_service.rb | 3 ++ .../synchronize_followers_service_spec.rb | 41 +++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/app/services/activitypub/synchronize_followers_service.rb b/app/services/activitypub/synchronize_followers_service.rb index f51d671a00..9d721dfaa1 100644 --- a/app/services/activitypub/synchronize_followers_service.rb +++ b/app/services/activitypub/synchronize_followers_service.rb @@ -57,6 +57,9 @@ class ActivityPub::SynchronizeFollowersService < BaseService collection = fetch_collection(collection['first']) if collection['first'].present? return unless collection.is_a?(Hash) + # Abort if we'd have to paginate through more than one page of followers + return if collection['next'].present? + case collection['type'] when 'Collection', 'CollectionPage' as_array(collection['items']) diff --git a/spec/services/activitypub/synchronize_followers_service_spec.rb b/spec/services/activitypub/synchronize_followers_service_spec.rb index 974368b7d7..3399e4fdf0 100644 --- a/spec/services/activitypub/synchronize_followers_service_spec.rb +++ b/spec/services/activitypub/synchronize_followers_service_spec.rb @@ -27,14 +27,14 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do }.with_indifferent_access end + before do + alice.follow!(actor) + bob.follow!(actor) + mallory.request_follow!(actor) + end + shared_examples 'synchronizes followers' do before do - alice.follow!(actor) - bob.follow!(actor) - mallory.request_follow!(actor) - - allow(ActivityPub::DeliveryWorker).to receive(:perform_async) - subject.call(actor, collection_uri) end @@ -46,7 +46,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do expect(mallory) .to be_following(actor) # Convert follow request to follow when accepted expect(ActivityPub::DeliveryWorker) - .to have_received(:perform_async).with(anything, eve.id, actor.inbox_url) # Send Undo Follow to actor + .to have_enqueued_sidekiq_job(anything, eve.id, actor.inbox_url) # Send Undo Follow to actor end end @@ -76,7 +76,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do it_behaves_like 'synchronizes followers' end - context 'when the endpoint is a paginated Collection of actor URIs' do + context 'when the endpoint is a single-page paginated Collection of actor URIs' do let(:payload) do { '@context': 'https://www.w3.org/ns/activitystreams', @@ -96,5 +96,30 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do it_behaves_like 'synchronizes followers' end + + context 'when the endpoint is a paginated Collection of actor URIs with a next page' do + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: collection_uri, + first: { + type: 'CollectionPage', + partOf: collection_uri, + items: items, + next: "#{collection_uri}/page2", + }, + }.with_indifferent_access + end + + before do + stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + end + + it 'does not change followers' do + expect { subject.call(actor, collection_uri) } + .to_not(change { actor.followers.reload.reorder(id: :asc).pluck(:id) }) + end + end end end From aa74e01890d0cb2c6506fe979a84ea5e17915bb6 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 26 Mar 2025 12:33:59 +0100 Subject: [PATCH 40/47] Add support for paginating partial collections in `SynchronizeFollowersService` (#34277) --- app/models/account.rb | 2 +- .../synchronize_followers_service.rb | 66 +++++++++----- .../synchronize_followers_service_spec.rb | 86 +++++++++++++++++-- 3 files changed, 128 insertions(+), 26 deletions(-) diff --git a/app/models/account.rb b/app/models/account.rb index 0612e63fd5..f3f591d006 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -160,7 +160,7 @@ class Account < ApplicationRecord scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) } scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) } scope :dormant, -> { joins(:account_stat).merge(AccountStat.without_recent_activity) } - scope :with_username, ->(value) { where arel_table[:username].lower.eq(value.to_s.downcase) } + scope :with_username, ->(value) { value.is_a?(Array) ? where(arel_table[:username].lower.in(value.map { |x| x.to_s.downcase })) : where(arel_table[:username].lower.eq(value.to_s.downcase)) } scope :with_domain, ->(value) { where arel_table[:domain].lower.eq(value&.to_s&.downcase) } scope :without_memorial, -> { where(memorial: false) } scope :duplicate_uris, -> { select(:uri, Arel.star.count).group(:uri).having(Arel.star.count.gt(1)) } diff --git a/app/services/activitypub/synchronize_followers_service.rb b/app/services/activitypub/synchronize_followers_service.rb index 9d721dfaa1..82d84a2f21 100644 --- a/app/services/activitypub/synchronize_followers_service.rb +++ b/app/services/activitypub/synchronize_followers_service.rb @@ -4,32 +4,46 @@ class ActivityPub::SynchronizeFollowersService < BaseService include JsonLdHelper include Payloadable + MAX_COLLECTION_PAGES = 10 + def call(account, partial_collection_url) @account = account + @expected_followers_ids = [] - items = collection_items(partial_collection_url) - return if items.nil? - - # There could be unresolved accounts (hence the call to .compact) but this - # should never happen in practice, since in almost all cases we keep an - # Account record, and should we not do that, we should have sent a Delete. - # In any case there is not much we can do if that occurs. - @expected_followers = items.filter_map { |uri| ActivityPub::TagManager.instance.uri_to_resource(uri, Account) } + return unless process_collection!(partial_collection_url) remove_unexpected_local_followers! - handle_unexpected_outgoing_follows! end private + def process_page!(items) + page_expected_followers = extract_local_followers(items) + @expected_followers_ids.concat(page_expected_followers.pluck(:id)) + + handle_unexpected_outgoing_follows!(page_expected_followers) + end + + def extract_local_followers(items) + # There could be unresolved accounts (hence the call to .filter_map) but this + # should never happen in practice, since in almost all cases we keep an + # Account record, and should we not do that, we should have sent a Delete. + # In any case there is not much we can do if that occurs. + + # TODO: this will need changes when switching to numeric IDs + + usernames = items.filter_map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username)&.downcase } + Account.local.with_username(usernames) + end + def remove_unexpected_local_followers! - @account.followers.local.where.not(id: @expected_followers.map(&:id)).reorder(nil).find_each do |unexpected_follower| + @account.followers.local.where.not(id: @expected_followers_ids).reorder(nil).find_each do |unexpected_follower| UnfollowService.new.call(unexpected_follower, @account) end end - def handle_unexpected_outgoing_follows! - @expected_followers.each do |expected_follower| + def handle_unexpected_outgoing_follows!(expected_followers) + expected_followers.each do |expected_follower| next if expected_follower.following?(@account) if expected_follower.requested?(@account) @@ -50,21 +64,33 @@ class ActivityPub::SynchronizeFollowersService < BaseService Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)) end - def collection_items(collection_or_uri) - collection = fetch_collection(collection_or_uri) - return unless collection.is_a?(Hash) + # Only returns true if the whole collection has been processed + def process_collection!(collection_uri, max_pages: MAX_COLLECTION_PAGES) + collection = fetch_collection(collection_uri) + return false unless collection.is_a?(Hash) collection = fetch_collection(collection['first']) if collection['first'].present? - return unless collection.is_a?(Hash) - # Abort if we'd have to paginate through more than one page of followers - return if collection['next'].present? + while collection.is_a?(Hash) + process_page!(as_array(collection_page_items(collection))) + max_pages -= 1 + + return true if collection['next'].blank? # We reached the end of the collection + return false if max_pages <= 0 # We reached our pages limit + + collection = fetch_collection(collection['next']) + end + + false + end + + def collection_page_items(collection) case collection['type'] when 'Collection', 'CollectionPage' - as_array(collection['items']) + collection['items'] when 'OrderedCollection', 'OrderedCollectionPage' - as_array(collection['orderedItems']) + collection['orderedItems'] end end diff --git a/spec/services/activitypub/synchronize_followers_service_spec.rb b/spec/services/activitypub/synchronize_followers_service_spec.rb index 3399e4fdf0..70f27627e1 100644 --- a/spec/services/activitypub/synchronize_followers_service_spec.rb +++ b/spec/services/activitypub/synchronize_followers_service_spec.rb @@ -10,7 +10,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do let(:bob) { Fabricate(:account, username: 'bob') } let(:eve) { Fabricate(:account, username: 'eve') } let(:mallory) { Fabricate(:account, username: 'mallory') } - let(:collection_uri) { 'http://example.com/partial-followers' } + let(:collection_uri) { 'https://example.com/partial-followers' } let(:items) do [alice, eve, mallory].map do |account| @@ -97,7 +97,76 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do it_behaves_like 'synchronizes followers' end - context 'when the endpoint is a paginated Collection of actor URIs with a next page' do + context 'when the endpoint is a paginated Collection of actor URIs split across multiple pages' do + before do + stub_request(:get, 'https://example.com/partial-followers') + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: 'https://example.com/partial-followers', + first: 'https://example.com/partial-followers/1', + })) + + stub_request(:get, 'https://example.com/partial-followers/1') + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'CollectionPage', + id: 'https://example.com/partial-followers/1', + partOf: 'https://example.com/partial-followers', + next: 'https://example.com/partial-followers/2', + items: [alice, eve].map { |account| ActivityPub::TagManager.instance.uri_for(account) }, + })) + + stub_request(:get, 'https://example.com/partial-followers/2') + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'CollectionPage', + id: 'https://example.com/partial-followers/2', + partOf: 'https://example.com/partial-followers', + items: ActivityPub::TagManager.instance.uri_for(mallory), + })) + end + + it_behaves_like 'synchronizes followers' + end + + context 'when the endpoint is a paginated Collection of actor URIs split across, but one page errors out' do + before do + stub_request(:get, 'https://example.com/partial-followers') + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: 'https://example.com/partial-followers', + first: 'https://example.com/partial-followers/1', + })) + + stub_request(:get, 'https://example.com/partial-followers/1') + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'CollectionPage', + id: 'https://example.com/partial-followers/1', + partOf: 'https://example.com/partial-followers', + next: 'https://example.com/partial-followers/2', + items: [mallory].map { |account| ActivityPub::TagManager.instance.uri_for(account) }, + })) + + stub_request(:get, 'https://example.com/partial-followers/2') + .to_return(status: 404) + end + + it 'confirms pending follow request but does not remove extra followers' do + previous_follower_ids = actor.followers.pluck(:id) + + subject.call(actor, collection_uri) + + expect(previous_follower_ids - actor.followers.reload.pluck(:id)) + .to be_empty + expect(mallory) + .to be_following(actor) + end + end + + context 'when the endpoint is a paginated Collection of actor URIs with more pages than we allow' do let(:payload) do { '@context': 'https://www.w3.org/ns/activitystreams', @@ -113,12 +182,19 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do end before do + stub_const('ActivityPub::SynchronizeFollowersService::MAX_COLLECTION_PAGES', 1) stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) end - it 'does not change followers' do - expect { subject.call(actor, collection_uri) } - .to_not(change { actor.followers.reload.reorder(id: :asc).pluck(:id) }) + it 'confirms pending follow request but does not remove extra followers' do + previous_follower_ids = actor.followers.pluck(:id) + + subject.call(actor, collection_uri) + + expect(previous_follower_ids - actor.followers.reload.pluck(:id)) + .to be_empty + expect(mallory) + .to be_following(actor) end end end From a40a0221c63eadb6b1df44b3f9504581e4b5465c Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 26 Mar 2025 14:26:24 +0100 Subject: [PATCH 41/47] Use fixed order in flaky spec (#34279) --- spec/requests/api/v2/notifications_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/api/v2/notifications_spec.rb b/spec/requests/api/v2/notifications_spec.rb index a7608e1419..72df063d49 100644 --- a/spec/requests/api/v2/notifications_spec.rb +++ b/spec/requests/api/v2/notifications_spec.rb @@ -170,7 +170,7 @@ RSpec.describe 'Notifications' do end context 'with min_id param' do - let(:params) { { min_id: user.account.notifications.reload.first.id - 1 } } + let(:params) { { min_id: user.account.notifications.order(id: :asc).first.id - 1 } } it 'returns a notification group covering all notifications' do subject From f62634b31311af3c32680bef20c156a20fe6557c Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 27 Mar 2025 14:41:13 +0100 Subject: [PATCH 42/47] Change `AccountReachFinder` to consider statuses based on suspension date (#34291) --- app/lib/account_reach_finder.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/lib/account_reach_finder.rb b/app/lib/account_reach_finder.rb index 19464024a6..f5e229c6a3 100644 --- a/app/lib/account_reach_finder.rb +++ b/app/lib/account_reach_finder.rb @@ -37,7 +37,11 @@ class AccountReachFinder def oldest_status_id Mastodon::Snowflake - .id_at(STATUS_SINCE.ago, with_random: false) + .id_at(oldest_status_date, with_random: false) + end + + def oldest_status_date + @account.suspended? && @account.suspension_origin_local? ? @account.suspended_at - STATUS_SINCE : STATUS_SINCE.ago end def recent_statuses From 577361ca9af19dab72c4add1a1a786256ea43c6c Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 28 Mar 2025 10:20:32 +0100 Subject: [PATCH 43/47] Change account suspensions to be federated to recently-followed accounts as well (#34294) --- app/lib/account_reach_finder.rb | 21 +++++++++++++++++--- spec/lib/account_reach_finder_spec.rb | 28 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/app/lib/account_reach_finder.rb b/app/lib/account_reach_finder.rb index f5e229c6a3..4bf5c229a5 100644 --- a/app/lib/account_reach_finder.rb +++ b/app/lib/account_reach_finder.rb @@ -10,7 +10,7 @@ class AccountReachFinder end def inboxes - (followers_inboxes + reporters_inboxes + recently_mentioned_inboxes + relay_inboxes).uniq + (followers_inboxes + reporters_inboxes + recently_mentioned_inboxes + recently_followed_inboxes + recently_requested_inboxes + relay_inboxes).uniq end private @@ -31,16 +31,31 @@ class AccountReachFinder .take(RECENT_LIMIT) end + def recently_followed_inboxes + @account + .following + .where(follows: { created_at: recent_date_cutoff... }) + .inboxes + .take(RECENT_LIMIT) + end + + def recently_requested_inboxes + Account + .where(id: @account.follow_requests.where({ created_at: recent_date_cutoff... }).select(:target_account_id)) + .inboxes + .take(RECENT_LIMIT) + end + def relay_inboxes Relay.enabled.pluck(:inbox_url) end def oldest_status_id Mastodon::Snowflake - .id_at(oldest_status_date, with_random: false) + .id_at(recent_date_cutoff, with_random: false) end - def oldest_status_date + def recent_date_cutoff @account.suspended? && @account.suspension_origin_local? ? @account.suspended_at - STATUS_SINCE : STATUS_SINCE.ago end diff --git a/spec/lib/account_reach_finder_spec.rb b/spec/lib/account_reach_finder_spec.rb index 0c1d92b2da..ed16c07c22 100644 --- a/spec/lib/account_reach_finder_spec.rb +++ b/spec/lib/account_reach_finder_spec.rb @@ -13,13 +13,28 @@ RSpec.describe AccountReachFinder do let(:ap_mentioned_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-3', domain: 'example.com') } let(:ap_mentioned_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.org/inbox-4', domain: 'example.org') } + let(:ap_followed_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-5', domain: 'example.com') } + let(:ap_followed_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-6', domain: 'example.org') } + + let(:ap_requested_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-7', domain: 'example.com') } + let(:ap_requested_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-8', domain: 'example.org') } + let(:unrelated_account) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/unrelated-inbox', domain: 'example.com') } + let(:old_followed_account) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/old-followed-inbox', domain: 'example.com') } before do + travel_to(2.months.ago) { account.follow!(old_followed_account) } + ap_follower_example_com.follow!(account) ap_follower_example_org.follow!(account) ap_follower_with_shared.follow!(account) + account.follow!(ap_followed_example_com) + account.follow!(ap_followed_example_org) + + account.request_follow!(ap_requested_example_com) + account.request_follow!(ap_requested_example_org) + Fabricate(:status, account: account).tap do |status| status.mentions << Mention.new(account: ap_follower_example_com) status.mentions << Mention.new(account: ap_mentioned_with_shared) @@ -44,7 +59,10 @@ RSpec.describe AccountReachFinder do expect(subject) .to include(*follower_inbox_urls) .and include(*mentioned_account_inbox_urls) + .and include(*recently_followed_inbox_urls) + .and include(*recently_requested_inbox_urls) .and not_include(unrelated_account.preferred_inbox_url) + .and not_include(old_followed_account.preferred_inbox_url) end def follower_inbox_urls @@ -56,5 +74,15 @@ RSpec.describe AccountReachFinder do [ap_mentioned_with_shared, ap_mentioned_example_com, ap_mentioned_example_org] .map(&:preferred_inbox_url) end + + def recently_followed_inbox_urls + [ap_followed_example_com, ap_followed_example_org] + .map(&:preferred_inbox_url) + end + + def recently_requested_inbox_urls + [ap_requested_example_com, ap_requested_example_org] + .map(&:preferred_inbox_url) + end end end From bccc4131a3e21130d207564aff8e29137606b125 Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 3 Apr 2025 06:57:54 +0900 Subject: [PATCH 44/47] Add delay to profile updates to debounce them (#34137) --- .../api/v1/accounts/credentials_controller.rb | 2 +- app/controllers/api/v1/profile/avatars_controller.rb | 2 +- app/controllers/api/v1/profile/headers_controller.rb | 2 +- app/controllers/settings/pictures_controller.rb | 2 +- app/controllers/settings/privacy_controller.rb | 2 +- app/controllers/settings/profiles_controller.rb | 2 +- app/controllers/settings/verifications_controller.rb | 2 +- app/workers/activitypub/update_distribution_worker.rb | 2 ++ spec/requests/api/v1/accounts/credentials_spec.rb | 4 +--- spec/requests/api/v1/profiles_spec.rb | 10 ++++------ spec/system/settings/profiles_spec.rb | 3 +-- 11 files changed, 15 insertions(+), 18 deletions(-) diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 032e42e9d2..bdd7732b87 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -14,7 +14,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController @account = current_account UpdateAccountService.new.call(@account, account_params, raise_error: true) current_user.update(user_params) if user_params - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) render json: @account, serializer: REST::CredentialAccountSerializer rescue ActiveRecord::RecordInvalid => e render json: ValidationErrorFormatter.new(e).as_json, status: 422 diff --git a/app/controllers/api/v1/profile/avatars_controller.rb b/app/controllers/api/v1/profile/avatars_controller.rb index bc4d01a597..e6c954ed63 100644 --- a/app/controllers/api/v1/profile/avatars_controller.rb +++ b/app/controllers/api/v1/profile/avatars_controller.rb @@ -7,7 +7,7 @@ class Api::V1::Profile::AvatarsController < Api::BaseController def destroy @account = current_account UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true) - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) render json: @account, serializer: REST::CredentialAccountSerializer end end diff --git a/app/controllers/api/v1/profile/headers_controller.rb b/app/controllers/api/v1/profile/headers_controller.rb index 9f4daa2f77..4472a01b05 100644 --- a/app/controllers/api/v1/profile/headers_controller.rb +++ b/app/controllers/api/v1/profile/headers_controller.rb @@ -7,7 +7,7 @@ class Api::V1::Profile::HeadersController < Api::BaseController def destroy @account = current_account UpdateAccountService.new.call(@account, { header: nil }, raise_error: true) - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) render json: @account, serializer: REST::CredentialAccountSerializer end end diff --git a/app/controllers/settings/pictures_controller.rb b/app/controllers/settings/pictures_controller.rb index 58a4325307..7e61e6d580 100644 --- a/app/controllers/settings/pictures_controller.rb +++ b/app/controllers/settings/pictures_controller.rb @@ -8,7 +8,7 @@ module Settings def destroy if valid_picture? if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' }) - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303 else redirect_to settings_profile_path diff --git a/app/controllers/settings/privacy_controller.rb b/app/controllers/settings/privacy_controller.rb index a5bb3b884f..96efa03ccf 100644 --- a/app/controllers/settings/privacy_controller.rb +++ b/app/controllers/settings/privacy_controller.rb @@ -8,7 +8,7 @@ class Settings::PrivacyController < Settings::BaseController def update if UpdateAccountService.new.call(@account, account_params.except(:settings)) current_user.update!(settings_attributes: account_params[:settings]) - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) redirect_to settings_privacy_path, notice: I18n.t('generic.changes_saved_msg') else render :show diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 99a647336a..04a10fbfb9 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -9,7 +9,7 @@ class Settings::ProfilesController < Settings::BaseController def update if UpdateAccountService.new.call(@account, account_params) - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg') else @account.build_fields diff --git a/app/controllers/settings/verifications_controller.rb b/app/controllers/settings/verifications_controller.rb index bed29dbeec..4b949ca72d 100644 --- a/app/controllers/settings/verifications_controller.rb +++ b/app/controllers/settings/verifications_controller.rb @@ -8,7 +8,7 @@ class Settings::VerificationsController < Settings::BaseController def update if UpdateAccountService.new.call(@account, account_params) - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg') else render :show diff --git a/app/workers/activitypub/update_distribution_worker.rb b/app/workers/activitypub/update_distribution_worker.rb index a04ac621f3..9a418f0f3d 100644 --- a/app/workers/activitypub/update_distribution_worker.rb +++ b/app/workers/activitypub/update_distribution_worker.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ActivityPub::UpdateDistributionWorker < ActivityPub::RawDistributionWorker + DEBOUNCE_DELAY = 5.seconds + sidekiq_options queue: 'push', lock: :until_executed, lock_ttl: 1.day.to_i # Distribute an profile update to servers that might have a copy diff --git a/spec/requests/api/v1/accounts/credentials_spec.rb b/spec/requests/api/v1/accounts/credentials_spec.rb index c92f4c7973..68ea259481 100644 --- a/spec/requests/api/v1/accounts/credentials_spec.rb +++ b/spec/requests/api/v1/accounts/credentials_spec.rb @@ -53,8 +53,6 @@ RSpec.describe 'credentials API' do patch '/api/v1/accounts/update_credentials', headers: headers, params: params end - before { allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) } - let(:params) do { avatar: fixture_file_upload('avatar.gif', 'image/gif'), @@ -113,7 +111,7 @@ RSpec.describe 'credentials API' do }) expect(ActivityPub::UpdateDistributionWorker) - .to have_received(:perform_async).with(user.account_id) + .to have_enqueued_sidekiq_job(user.account_id) end def expect_account_updates diff --git a/spec/requests/api/v1/profiles_spec.rb b/spec/requests/api/v1/profiles_spec.rb index fd3ab4bf58..de7a20b133 100644 --- a/spec/requests/api/v1/profiles_spec.rb +++ b/spec/requests/api/v1/profiles_spec.rb @@ -15,10 +15,6 @@ RSpec.describe 'Deleting profile images' do let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } describe 'DELETE /api/v1/profile' do - before do - allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) - end - context 'when deleting an avatar' do context 'with wrong scope' do before do @@ -38,7 +34,8 @@ RSpec.describe 'Deleting profile images' do account.reload expect(account.avatar).to_not exist expect(account.header).to exist - expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id) + expect(ActivityPub::UpdateDistributionWorker) + .to have_enqueued_sidekiq_job(account.id) end end @@ -61,7 +58,8 @@ RSpec.describe 'Deleting profile images' do account.reload expect(account.avatar).to exist expect(account.header).to_not exist - expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(account.id) + expect(ActivityPub::UpdateDistributionWorker) + .to have_enqueued_sidekiq_job(account.id) end end end diff --git a/spec/system/settings/profiles_spec.rb b/spec/system/settings/profiles_spec.rb index 73a5751141..f0fdbdb203 100644 --- a/spec/system/settings/profiles_spec.rb +++ b/spec/system/settings/profiles_spec.rb @@ -7,7 +7,6 @@ RSpec.describe 'Settings profile page' do let(:account) { user.account } before do - allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) sign_in user end @@ -24,7 +23,7 @@ RSpec.describe 'Settings profile page' do .to change { account.reload.display_name }.to('New name') .and(change { account.reload.avatar.instance.avatar_file_name }.from(nil).to(be_present)) expect(ActivityPub::UpdateDistributionWorker) - .to have_received(:perform_async).with(account.id) + .to have_enqueued_sidekiq_job(account.id) end def display_name_field From db59fcb13e7c4c7271239cd4a932bf6a9fb50cc8 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 1 Apr 2025 10:53:49 +0200 Subject: [PATCH 45/47] Fix static version of animated PNG emojis not being properly extracted (#34337) --- lib/paperclip/vips_lazy_thumbnail.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/paperclip/vips_lazy_thumbnail.rb b/lib/paperclip/vips_lazy_thumbnail.rb index fea4b86064..528d5604dc 100644 --- a/lib/paperclip/vips_lazy_thumbnail.rb +++ b/lib/paperclip/vips_lazy_thumbnail.rb @@ -123,7 +123,14 @@ module Paperclip end def needs_convert? - needs_different_geometry? || needs_different_format? || needs_metadata_stripping? + strip_animations? || needs_different_geometry? || needs_different_format? || needs_metadata_stripping? + end + + def strip_animations? + # Detecting whether the source image is animated across all our supported + # input file formats is not trivial, and converting unconditionally is just + # as simple for now + options[:style] == :static end def needs_different_geometry? From 28a08ce4786583a713877f65c1d39ae72faeaa4a Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 2 Apr 2025 09:14:21 +0200 Subject: [PATCH 46/47] Bump version to v4.3.7 (#34328) --- CHANGELOG.md | 23 +++++++++++++++++++++++ docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7b0f64146..4dd4783597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ All notable changes to this project will be documented in this file. +## [4.3.7] - 2025-04-02 + +### Add + +- Add delay to profile updates to debounce them (#34137 by @ClearlyClaire) +- Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire) + +### Changed + +- Change account suspensions to be federated to recently-followed accounts as well (#34294 by @ClearlyClaire) +- Change `AccountReachFinder` to consider statuses based on suspension date (#32805 and #34291 by @ClearlyClaire and @mjankowski) +- Change user archive signed URL TTL from 10 seconds to 1 hour (#34254 by @ClearlyClaire) + +### Fixed + +- Fix static version of animated PNG emojis not being properly extracted (#34337 by @ClearlyClaire) +- Fix filters not applying in detailed view, favourites and bookmarks (#34259 and #34260 by @ClearlyClaire) +- Fix handling of malformed/unusual HTML (#34201 by @ClearlyClaire) +- Fix `CacheBuster` being queued for missing media attachments (#34253 by @ClearlyClaire) +- Fix incorrect URL being used when cache busting (#34189 by @ClearlyClaire) +- Fix streaming server refusing unix socket path in `DATABASE_URL` (#34091 by @ClearlyClaire) +- Fix โ€œxโ€ hotkey not working on boosted filtered posts (#33758 by @ClearlyClaire) + ## [4.3.6] - 2025-03-13 ### Security diff --git a/docker-compose.yml b/docker-compose.yml index 56c734318e..143929d902 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: kmyblue:17.3 + image: kmyblue:17.4 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: kmyblue-streaming:17.3 + image: kmyblue-streaming:17.4 restart: always env_file: .env.production command: node ./streaming/index.js @@ -101,7 +101,7 @@ services: sidekiq: build: . - image: kmyblue:17.3 + image: kmyblue:17.4 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index d523a1ef84..d0366d5d14 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def kmyblue_minor - 3 + 4 end def kmyblue_flag From f24f83d9b01f9cd02fad84c77f77b2da54c89c10 Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 3 Apr 2025 07:17:26 +0900 Subject: [PATCH 47/47] Fix test --- app/controllers/settings/privacy_extra_controller.rb | 2 +- spec/controllers/settings/privacy_controller_spec.rb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/settings/privacy_extra_controller.rb b/app/controllers/settings/privacy_extra_controller.rb index f1292e644c..cb99c390dd 100644 --- a/app/controllers/settings/privacy_extra_controller.rb +++ b/app/controllers/settings/privacy_extra_controller.rb @@ -8,7 +8,7 @@ class Settings::PrivacyExtraController < Settings::BaseController def update if UpdateAccountService.new.call(@account, account_params.except(:settings)) current_user.update!(settings_attributes: account_params[:settings]) - ActivityPub::UpdateDistributionWorker.perform_async(@account.id) + ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) redirect_to settings_privacy_extra_path, notice: I18n.t('generic.changes_saved_msg') else render :show diff --git a/spec/controllers/settings/privacy_controller_spec.rb b/spec/controllers/settings/privacy_controller_spec.rb index 59fd342199..b09f700081 100644 --- a/spec/controllers/settings/privacy_controller_spec.rb +++ b/spec/controllers/settings/privacy_controller_spec.rb @@ -31,7 +31,7 @@ RSpec.describe Settings::PrivacyController do describe 'PUT #update' do context 'when update succeeds' do before do - allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) + allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in) end it 'updates the user profile' do @@ -44,14 +44,14 @@ RSpec.describe Settings::PrivacyController do .to redirect_to(settings_privacy_path) expect(ActivityPub::UpdateDistributionWorker) - .to have_received(:perform_async).with(account.id) + .to have_received(:perform_in).with(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, account.id) end end context 'when update fails' do before do allow(UpdateAccountService).to receive(:new).and_return(failing_update_service) - allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) + allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_in) end it 'updates the user profile' do @@ -61,7 +61,7 @@ RSpec.describe Settings::PrivacyController do .to render_template(:show) expect(ActivityPub::UpdateDistributionWorker) - .to_not have_received(:perform_async) + .to_not have_received(:perform_in) end private