From 4ef0ce033e6ae088a99230ab7c5c4f0f3d7ca04a Mon Sep 17 00:00:00 2001 From: Claire Date: Sat, 23 Aug 2025 01:59:11 +0200 Subject: [PATCH 01/29] Fix export of large user archives on 4.4 by enabling Zip64 (#35850) --- app/services/backup_service.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index cc86af000..e0e27f9f4 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -55,6 +55,7 @@ class BackupService < BaseService def build_archive! tmp_file = Tempfile.new(%w(archive .zip)) + Zip.write_zip64_support = true Zip::File.open(tmp_file, create: true) do |zipfile| dump_outbox!(zipfile) dump_media_attachments!(zipfile) From cbb9a4dbe3fabf7caa6c75ec81515c41299d15bb Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Thu, 21 Aug 2025 16:51:11 +0200 Subject: [PATCH 02/29] Refactor to reuse the one status partial across moderation tools (#35644) --- app/helpers/application_helper.rb | 10 ++++ .../styles/mastodon/components.scss | 9 ++-- app/models/status_edit.rb | 4 ++ .../reports/_media_attachments.html.haml | 6 --- app/views/admin/reports/_status.html.haml | 53 ------------------- app/views/admin/reports/show.html.haml | 2 +- app/views/admin/shared/_status.html.haml | 40 ++++++++++++++ .../shared/_status_attachments.html.haml | 7 +++ .../admin/shared/_status_batch_row.html.haml | 5 ++ .../admin/shared/_status_content.html.haml | 10 ++++ .../admin/status_edits/_status_edit.html.haml | 12 +---- app/views/admin/statuses/index.html.haml | 2 +- app/views/admin/statuses/show.html.haml | 45 +--------------- config/locales/en.yml | 2 +- 14 files changed, 88 insertions(+), 119 deletions(-) delete mode 100644 app/views/admin/reports/_media_attachments.html.haml delete mode 100644 app/views/admin/reports/_status.html.haml create mode 100644 app/views/admin/shared/_status.html.haml create mode 100644 app/views/admin/shared/_status_attachments.html.haml create mode 100644 app/views/admin/shared/_status_batch_row.html.haml create mode 100644 app/views/admin/shared/_status_content.html.haml diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 33d4bf6d0..65f803b10 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -102,6 +102,16 @@ module ApplicationHelper policy(record).public_send(:"#{action}?") end + def conditional_link_to(condition, name, options = {}, html_options = {}, &block) + if condition && !current_page?(block_given? ? name : options) + link_to(name, options, html_options, &block) + elsif block_given? + content_tag(:span, options, html_options, &block) + else + content_tag(:span, name, html_options) + end + end + def material_symbol(icon, attributes = {}) safe_join( [ diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 40b073f68..70b0da95b 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2333,6 +2333,7 @@ a .account__avatar { .detailed-status__display-name, .detailed-status__datetime, .detailed-status__application, +.detailed-status__link, .account__display-name { text-decoration: none; } @@ -2365,7 +2366,8 @@ a.account__display-name { } .detailed-status__application, -.detailed-status__datetime { +.detailed-status__datetime, +.detailed-status__link { color: inherit; } @@ -2551,8 +2553,9 @@ a.account__display-name { } .status__relative-time, -.detailed-status__datetime { - &:hover { +.detailed-status__datetime, +.detailed-status__link { + &:is(a):hover { text-decoration: underline; } } diff --git a/app/models/status_edit.rb b/app/models/status_edit.rb index 25e222887..2c2306bb7 100644 --- a/app/models/status_edit.rb +++ b/app/models/status_edit.rb @@ -45,6 +45,10 @@ class StatusEdit < ApplicationRecord delegate :local?, :application, :edited?, :edited_at, :discarded?, :visibility, :language, to: :status + def with_media? + ordered_media_attachments.any? + end + def emojis return @emojis if defined?(@emojis) diff --git a/app/views/admin/reports/_media_attachments.html.haml b/app/views/admin/reports/_media_attachments.html.haml deleted file mode 100644 index aa82ec09a..000000000 --- a/app/views/admin/reports/_media_attachments.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -- if status.ordered_media_attachments.first.video? - = render_video_component(status, visible: false) -- elsif status.ordered_media_attachments.first.audio? - = render_audio_component(status) -- else - = render_media_gallery_component(status, visible: false) diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml deleted file mode 100644 index c1e47d5b3..000000000 --- a/app/views/admin/reports/_status.html.haml +++ /dev/null @@ -1,53 +0,0 @@ -.batch-table__row - %label.batch-table__row__select.batch-checkbox - = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id - .batch-table__row__content - .status__card - - if status.reblog? - .status__prepend - = material_symbol('repeat') - = t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account, path: admin_account_status_path(status.proper.account.id, status.proper.id))) - - elsif status.reply? && status.in_reply_to_id.present? - .status__prepend - = material_symbol('reply') - = t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(status.in_reply_to_account, path: status.thread.present? ? admin_account_status_path(status.thread.account_id, status.in_reply_to_id) : nil)) - .status__content>< - - if status.proper.spoiler_text.blank? - = prerender_custom_emojis(status_content_format(status.proper), status.proper.emojis) - - else - %details< - %summary>< - %strong> Content warning: #{prerender_custom_emojis(h(status.proper.spoiler_text), status.proper.emojis)} - = prerender_custom_emojis(status_content_format(status.proper), status.proper.emojis) - - - unless status.proper.ordered_media_attachments.empty? - = render partial: 'admin/reports/media_attachments', locals: { status: status.proper } - - .detailed-status__meta - - if status.application - = status.application.name - · - - = link_to admin_account_status_path(status.account.id, status), class: 'detailed-status__datetime' do - %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) - - if status.edited? - · - = link_to t('statuses.edited_at_html', date: content_tag(:time, l(status.edited_at), datetime: status.edited_at.iso8601, title: l(status.edited_at), class: 'formatted')), - admin_account_status_path(status.account_id, status), - class: 'detailed-status__datetime' - - if status.discarded? - · - %span.negative-hint= t('admin.statuses.deleted') - · - - = material_symbol visibility_icon(status) - = t("statuses.visibilities.#{status.visibility}") - · - - = link_to ActivityPub::TagManager.instance.url_for(status.proper), class: 'detailed-status__link', rel: 'noopener' do - = t('admin.statuses.view_publicly') - - - if status.proper.sensitive? - · - = material_symbol('visibility_off') - = t('stream_entries.sensitive_content') diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 69e9c0292..516d2f5d2 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -57,7 +57,7 @@ - if @statuses.empty? = nothing_here 'nothing-here--under-tabs' - else - = render partial: 'admin/reports/status', collection: @statuses, locals: { f: f } + = render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f } - if @report.unresolved? %hr.spacer/ diff --git a/app/views/admin/shared/_status.html.haml b/app/views/admin/shared/_status.html.haml new file mode 100644 index 000000000..c042fd7a2 --- /dev/null +++ b/app/views/admin/shared/_status.html.haml @@ -0,0 +1,40 @@ +-# locals: (status:) + +.status__card>< + - if status.reblog? + .status__prepend + = material_symbol('repeat') + = t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account, path: admin_account_status_path(status.proper.account.id, status.proper.id))) + - elsif status.reply? && status.in_reply_to_id.present? + .status__prepend + = material_symbol('reply') + = t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(status.in_reply_to_account, path: status.thread.present? ? admin_account_status_path(status.thread.account_id, status.in_reply_to_id) : nil)) + + = render partial: 'admin/shared/status_content', locals: { status: status.proper } + + .detailed-status__meta + - if status.application + = status.application.name + · + = conditional_link_to can?(:show, status), admin_account_status_path(status.account.id, status), class: 'detailed-status__datetime' do + %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }><= l(status.created_at) + - if status.edited? +  · + = conditional_link_to can?(:show, status), admin_account_status_path(status.account.id, status, { anchor: 'history' }), class: 'detailed-status__datetime' do + %span><= t('statuses.edited_at_html', date: content_tag(:time, l(status.edited_at), datetime: status.edited_at.iso8601, title: l(status.edited_at), class: 'relative-formatted')) + - if status.discarded? +  · + %span.negative-hint= t('admin.statuses.deleted') + - unless status.reblog? +  · + %span< + = material_symbol(visibility_icon(status)) + = t("statuses.visibilities.#{status.visibility}") + - if status.proper.sensitive? +  · + = material_symbol('visibility_off') + = t('stream_entries.sensitive_content') + - unless status.direct_visibility? +  · + = link_to ActivityPub::TagManager.instance.url_for(status.proper), class: 'detailed-status__link', target: 'blank', rel: 'noopener' do + = t('admin.statuses.view_publicly') diff --git a/app/views/admin/shared/_status_attachments.html.haml b/app/views/admin/shared/_status_attachments.html.haml new file mode 100644 index 000000000..92cc69645 --- /dev/null +++ b/app/views/admin/shared/_status_attachments.html.haml @@ -0,0 +1,7 @@ +- if status.with_media? + - if status.ordered_media_attachments.first.video? + = render_video_component(status, visible: false) + - elsif status.ordered_media_attachments.first.audio? + = render_audio_component(status) + - else + = render_media_gallery_component(status, visible: false) diff --git a/app/views/admin/shared/_status_batch_row.html.haml b/app/views/admin/shared/_status_batch_row.html.haml new file mode 100644 index 000000000..53d8a56e6 --- /dev/null +++ b/app/views/admin/shared/_status_batch_row.html.haml @@ -0,0 +1,5 @@ +.batch-table__row + %label.batch-table__row__select.batch-checkbox + = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id + .batch-table__row__content + = render partial: 'admin/shared/status', object: status diff --git a/app/views/admin/shared/_status_content.html.haml b/app/views/admin/shared/_status_content.html.haml new file mode 100644 index 000000000..aedd84bdd --- /dev/null +++ b/app/views/admin/shared/_status_content.html.haml @@ -0,0 +1,10 @@ +.status__content>< + - if status.spoiler_text.present? + %details< + %summary>< + %strong> Content warning: #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)} + = prerender_custom_emojis(status_content_format(status), status.emojis) + = render partial: 'admin/shared/status_attachments', locals: { status: status.proper } + - else + = prerender_custom_emojis(status_content_format(status), status.emojis) + = render partial: 'admin/shared/status_attachments', locals: { status: status.proper } diff --git a/app/views/admin/status_edits/_status_edit.html.haml b/app/views/admin/status_edits/_status_edit.html.haml index 0bec0159e..d4cc4f5ea 100644 --- a/app/views/admin/status_edits/_status_edit.html.haml +++ b/app/views/admin/status_edits/_status_edit.html.haml @@ -9,17 +9,7 @@ %time.formatted{ datetime: status_edit.created_at.iso8601, title: l(status_edit.created_at) }= l(status_edit.created_at) .status - .status__content>< - - if status_edit.spoiler_text.blank? - = prerender_custom_emojis(status_content_format(status_edit), status_edit.emojis) - - else - %details< - %summary>< - %strong> Content warning: #{prerender_custom_emojis(h(status_edit.spoiler_text), status_edit.emojis)} - = prerender_custom_emojis(status_content_format(status_edit), status_edit.emojis) - - - unless status_edit.ordered_media_attachments.empty? - = render partial: 'admin/reports/media_attachments', locals: { status: status_edit } + = render partial: 'admin/shared/status_content', locals: { status: status_edit } .detailed-status__meta %time.formatted{ datetime: status_edit.created_at.iso8601, title: l(status_edit.created_at) }= l(status_edit.created_at) diff --git a/app/views/admin/statuses/index.html.haml b/app/views/admin/statuses/index.html.haml index 57b9fe0e1..e914585db 100644 --- a/app/views/admin/statuses/index.html.haml +++ b/app/views/admin/statuses/index.html.haml @@ -47,6 +47,6 @@ - if @statuses.empty? = nothing_here 'nothing-here--under-tabs' - else - = render partial: 'admin/reports/status', collection: @statuses, locals: { f: f } + = render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f } = paginate @statuses diff --git a/app/views/admin/statuses/show.html.haml b/app/views/admin/statuses/show.html.haml index 7328eeb0a..ba5ba8198 100644 --- a/app/views/admin/statuses/show.html.haml +++ b/app/views/admin/statuses/show.html.haml @@ -53,52 +53,11 @@ %h3= t('admin.statuses.contents') -.status__card - - if @status.reblog? - .status__prepend - = material_symbol('repeat') - = t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(@status.proper.account, path: admin_account_status_path(@status.proper.account.id, @status.proper.id))) - - elsif @status.reply? && @status.in_reply_to_id.present? - .status__prepend - = material_symbol('reply') - = t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(@status.in_reply_to_account, path: @status.thread.present? ? admin_account_status_path(@status.thread.account_id, @status.in_reply_to_id) : nil)) - .status__content>< - - if @status.proper.spoiler_text.blank? - = prerender_custom_emojis(status_content_format(@status.proper), @status.proper.emojis) - - else - %details< - %summary>< - %strong> Content warning: #{prerender_custom_emojis(h(@status.proper.spoiler_text), @status.proper.emojis)} - = prerender_custom_emojis(status_content_format(@status.proper), @status.proper.emojis) - - - unless @status.proper.ordered_media_attachments.empty? - = render partial: 'admin/reports/media_attachments', locals: { status: @status.proper } - - .detailed-status__meta - - if @status.application - = @status.application.name - · - %span.detailed-status__datetime - %time.formatted{ datetime: @status.created_at.iso8601, title: l(@status.created_at) }= l(@status.created_at) - - if @status.edited? - · - %span.detailed-status__datetime - = t('statuses.edited_at_html', date: content_tag(:time, l(@status.edited_at), datetime: @status.edited_at.iso8601, title: l(@status.edited_at), class: 'formatted')) - - if @status.discarded? - · - %span.negative-hint= t('admin.statuses.deleted') - - unless @status.reblog? - · - = material_symbol(visibility_icon(@status)) - = t("statuses.visibilities.#{@status.visibility}") - - if @status.proper.sensitive? - · - = material_symbol('visibility_off') - = t('stream_entries.sensitive_content') += render partial: 'admin/shared/status', object: @status %hr.spacer/ -%h3= t('admin.statuses.history') +%h3#history= t('admin.statuses.history') - if @status.edits.empty? %p= t('admin.statuses.no_history') - else diff --git a/config/locales/en.yml b/config/locales/en.yml index e3a828060..e1a813169 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1881,7 +1881,7 @@ en: public: Everyone title: '%{name}: "%{quote}"' visibilities: - direct: Direct + direct: Private mention private: Followers-only private_long: Only show to followers public: Public From e61900cadc43e1c2cabeb8503df608827e923a41 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 7 Aug 2025 10:03:15 +0200 Subject: [PATCH 03/29] Fix quote revocation not being streamed (#35710) --- app/lib/activitypub/activity/delete.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 69b7bd035..ce36cfe76 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -61,6 +61,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity ActivityPub::Forwarder.new(@account, @json, @quote.status).forward! @quote.reject! + DistributionWorker.perform_async(@quote.status_id, { 'update' => true }) end def forwarder From 0741381670007041312a90c0960c254dbfcd5dd3 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 11 Aug 2025 10:55:23 +0200 Subject: [PATCH 04/29] Add test for `Delete` of inlined `QuoteAuthorization` (#35724) --- spec/lib/activitypub/activity/delete_spec.rb | 84 ++++++++------------ 1 file changed, 35 insertions(+), 49 deletions(-) diff --git a/spec/lib/activitypub/activity/delete_spec.rb b/spec/lib/activitypub/activity/delete_spec.rb index 849c7ada9..48d2946b9 100644 --- a/spec/lib/activitypub/activity/delete_spec.rb +++ b/spec/lib/activitypub/activity/delete_spec.rb @@ -12,27 +12,22 @@ RSpec.describe ActivityPub::Activity::Delete do id: 'foo', type: 'Delete', actor: ActivityPub::TagManager.instance.uri_for(sender), - object: ActivityPub::TagManager.instance.uri_for(status), + object: object_json, signature: 'foo', - }.with_indifferent_access + }.deep_stringify_keys end + let(:object_json) { ActivityPub::TagManager.instance.uri_for(status) } + describe '#perform' do subject { described_class.new(json, sender) } - before do - subject.perform - end - it 'deletes sender\'s status' do + subject.perform expect(Status.find_by(id: status.id)).to be_nil end - end - - context 'when the status has been reblogged' do - describe '#perform' do - subject { described_class.new(json, sender) } + context 'when the status has been reblogged' do let!(:reblogger) { Fabricate(:account) } let!(:follower) { Fabricate(:account, username: 'follower', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } let!(:reblog) { Fabricate(:status, account: reblogger, reblog: status) } @@ -55,12 +50,8 @@ RSpec.describe ActivityPub::Activity::Delete do expect { reblog.reload }.to raise_error(ActiveRecord::RecordNotFound) end end - end - - context 'when the status has been reported' do - describe '#perform' do - subject { described_class.new(json, sender) } + context 'when the status has been reported' do let!(:reporter) { Fabricate(:account) } before do @@ -76,23 +67,9 @@ RSpec.describe ActivityPub::Activity::Delete do expect(Status.with_discarded.find_by(id: status.id)).to_not be_nil end end - end - - context 'when the deleted object is an account' do - let(:json) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: 'Delete', - actor: ActivityPub::TagManager.instance.uri_for(sender), - object: ActivityPub::TagManager.instance.uri_for(sender), - signature: 'foo', - }.with_indifferent_access - end - - describe '#perform' do - subject { described_class.new(json, sender) } + context 'when the deleted object is an account' do + let(:object_json) { ActivityPub::TagManager.instance.uri_for(sender) } let(:service) { instance_double(DeleteAccountService, call: true) } before do @@ -106,27 +83,36 @@ RSpec.describe ActivityPub::Activity::Delete do .to have_received(:call).with(sender, { reserve_username: false, skip_activitypub: true }) end end - end - context 'when the deleted object is a quote authorization' do - let(:quoter) { Fabricate(:account, domain: 'b.example.com') } - let(:status) { Fabricate(:status, account: quoter) } - let(:quoted_status) { Fabricate(:status, account: sender, uri: 'https://example.com/statuses/1234') } - let!(:quote) { Fabricate(:quote, approval_uri: 'https://example.com/approvals/1234', state: :accepted, status: status, quoted_status: quoted_status) } + context 'when the deleted object is a quote authorization' do + let(:quoter) { Fabricate(:account, domain: 'b.example.com') } + let(:status) { Fabricate(:status, account: quoter) } + let(:quoted_status) { Fabricate(:status, account: sender, uri: 'https://example.com/statuses/1234') } + let!(:quote) { Fabricate(:quote, approval_uri: 'https://example.com/approvals/1234', state: :accepted, status: status, quoted_status: quoted_status) } - let(:json) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: 'foo', - type: 'Delete', - actor: ActivityPub::TagManager.instance.uri_for(sender), - object: quote.approval_uri, - signature: 'foo', - }.with_indifferent_access + let(:object_json) { quote.approval_uri } + + it 'revokes the authorization' do + expect { subject.perform } + .to change { quote.reload.state }.to('revoked') + end end - describe '#perform' do - subject { described_class.new(json, sender) } + context 'when the deleted object is an inlined quote authorization' do + let(:quoter) { Fabricate(:account, domain: 'b.example.com') } + let(:status) { Fabricate(:status, account: quoter) } + let(:quoted_status) { Fabricate(:status, account: sender, uri: 'https://example.com/statuses/1234') } + let!(:quote) { Fabricate(:quote, approval_uri: 'https://example.com/approvals/1234', state: :accepted, status: status, quoted_status: quoted_status) } + + let(:object_json) do + { + type: 'QuoteAuthorization', + id: quote.approval_uri, + attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), + interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), + interactingObject: ActivityPub::TagManager.instance.uri_for(status), + }.deep_stringify_keys + end it 'revokes the authorization' do expect { subject.perform } From 5f4116a3113de1e04ada08a23790ab033d937fb9 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 12 Aug 2025 14:19:29 +0200 Subject: [PATCH 05/29] Fix interaction policy changes in implicit updates not being saved (#35751) --- .../process_status_update_service.rb | 2 +- .../fetch_remote_status_service_spec.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index ed2aab2cc..6725fa552 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -74,7 +74,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService end def update_interaction_policies! - @status.quote_approval_policy = @status_parser.quote_policy + @status.update(quote_approval_policy: @status_parser.quote_policy) end def update_media_attachments! diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 2503a58ac..07d05d762 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -316,6 +316,23 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do expect(existing_status.edits).to_not be_empty end end + + context 'with an implicit update to quoting policy' do + let(:object) do + note.merge({ + 'content' => existing_status.text, + 'interactionPolicy' => { + 'canQuote' => { + 'automaticApproval' => ['https://www.w3.org/ns/activitystreams#Public'], + }, + }, + }) + end + + it 'updates status' do + expect(existing_status.reload.quote_approval_policy).to eq(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) + end + end end end From 1675eab56145152dfbc7858efb36f3c30d89dcf4 Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 13 Aug 2025 15:52:29 +0200 Subject: [PATCH 06/29] Redirect on success for standalone compose (#35763) --- app/javascript/mastodon/actions/compose.js | 5 ++++- .../mastodon/features/compose/components/compose_form.jsx | 3 ++- .../features/compose/containers/compose_form_container.js | 8 ++++++-- .../mastodon/features/standalone/compose/index.jsx | 2 +- spec/system/share_entrypoint_spec.rb | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index d70834cec..84ed2de9c 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -183,7 +183,7 @@ export function directCompose(account) { }; } -export function submitCompose() { +export function submitCompose(successCallback) { return function (dispatch, getState) { const status = getState().getIn(['compose', 'text'], ''); const media = getState().getIn(['compose', 'media_attachments']); @@ -239,6 +239,9 @@ export function submitCompose() { dispatch(insertIntoTagHistory(response.data.tags, status)); dispatch(submitComposeSuccess({ ...response.data })); + if (typeof successCallback === 'function') { + successCallback(response.data); + } // To make the app more responsive, immediately push the status // into the columns diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx index 6dd3dbd05..f3c9d9201 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.jsx +++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx @@ -73,6 +73,7 @@ class ComposeForm extends ImmutablePureComponent { singleColumn: PropTypes.bool, lang: PropTypes.string, maxChars: PropTypes.number, + redirectOnSuccess: PropTypes.bool, }; static defaultProps = { @@ -310,7 +311,7 @@ class ComposeForm extends ImmutablePureComponent { > {intl.formatMessage( this.props.isEditing ? - messages.saveChanges : + messages.saveChanges : (this.props.isInReply ? messages.reply : messages.publish) )} diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js index 15ccabf74..5f86426c4 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -34,7 +34,7 @@ const mapStateToProps = state => ({ maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500), }); -const mapDispatchToProps = (dispatch) => ({ +const mapDispatchToProps = (dispatch, props) => ({ onChange (text) { dispatch(changeCompose(text)); @@ -47,7 +47,11 @@ const mapDispatchToProps = (dispatch) => ({ modalProps: {}, })); } else { - dispatch(submitCompose()); + dispatch(submitCompose((status) => { + if (props.redirectOnSuccess) { + window.location.assign(status.url); + } + })); } }, diff --git a/app/javascript/mastodon/features/standalone/compose/index.jsx b/app/javascript/mastodon/features/standalone/compose/index.jsx index 3aff78ffe..5d336275d 100644 --- a/app/javascript/mastodon/features/standalone/compose/index.jsx +++ b/app/javascript/mastodon/features/standalone/compose/index.jsx @@ -5,7 +5,7 @@ import ModalContainer from 'mastodon/features/ui/containers/modal_container'; const Compose = () => ( <> - + diff --git a/spec/system/share_entrypoint_spec.rb b/spec/system/share_entrypoint_spec.rb index b55ea3165..0f07d96ef 100644 --- a/spec/system/share_entrypoint_spec.rb +++ b/spec/system/share_entrypoint_spec.rb @@ -23,7 +23,7 @@ RSpec.describe 'Share page', :js, :streaming do fill_in_form expect(page) - .to have_css('.notification-bar-message', text: frontend_translations('compose.published.body')) + .to have_current_path(%r{/@bob/[0-9]+}) end def fill_in_form From 7a862d33084f8df9da492abee88a78b3c4af28c1 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 14 Aug 2025 03:57:18 -0400 Subject: [PATCH 07/29] First pass coverage addition for antispam class (#35771) --- app/lib/antispam.rb | 12 ++++++++-- spec/lib/antispam_spec.rb | 47 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 spec/lib/antispam_spec.rb diff --git a/app/lib/antispam.rb b/app/lib/antispam.rb index 4ebf19248..9dfcda7df 100644 --- a/app/lib/antispam.rb +++ b/app/lib/antispam.rb @@ -57,8 +57,16 @@ class Antispam end def report_if_needed!(account) - return if Report.unresolved.exists?(account: Account.representative, target_account: account) + return if system_reports.unresolved.exists?(target_account: account) - Report.create!(account: Account.representative, target_account: account, category: :spam, comment: 'Account automatically reported for posting a banned URL') + system_reports.create!( + category: :spam, + comment: 'Account automatically reported for posting a banned URL', + target_account: account + ) + end + + def system_reports + Account.representative.reports end end diff --git a/spec/lib/antispam_spec.rb b/spec/lib/antispam_spec.rb new file mode 100644 index 000000000..869fd4c21 --- /dev/null +++ b/spec/lib/antispam_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Antispam do + describe '#local_preflight_check!' do + subject { described_class.new.local_preflight_check!(status) } + + let(:status) { Fabricate :status } + + context 'when there is no spammy text registered' do + it { is_expected.to be_nil } + end + + context 'with spammy text' do + before { redis.sadd 'antispam:spammy_texts', 'https://banned.example' } + + context 'when status matches' do + let(:status) { Fabricate :status, text: 'I use https://banned.example urls in my text' } + + it 'raises error and reports' do + expect { subject } + .to raise_error(described_class::SilentlyDrop) + .and change(spam_reports, :count).by(1) + end + + context 'when report already exists' do + before { Fabricate :report, account: Account.representative, target_account: status.account } + + it 'raises error and does not report' do + expect { subject } + .to raise_error(described_class::SilentlyDrop) + .and not_change(spam_reports, :count) + end + end + + def spam_reports + Account.representative.reports.where(target_account: status.account).spam + end + end + + context 'when status does not match' do + it { is_expected.to be_nil } + end + end + end +end From ea5d1f02972455ac6f991bc01b52f2ddb1456834 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 14 Aug 2025 15:35:19 +0200 Subject: [PATCH 08/29] Fix `tootctl admin create` not bypassing reserved username checks (#35779) --- app/models/account.rb | 2 +- app/models/user.rb | 8 +++----- spec/lib/mastodon/cli/accounts_spec.rb | 11 +++++++++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/models/account.rb b/app/models/account.rb index c23549c22..5fa1f0ceb 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -116,7 +116,7 @@ class Account < ApplicationRecord # Local user validations validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: USERNAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_username? && !actor_type_application? } - validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && !actor_type_application? } + validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && !actor_type_application? && !user&.bypass_registration_checks } validates :display_name, length: { maximum: DISPLAY_NAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_display_name? } validates :note, note_length: { maximum: NOTE_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_note? } validates :fields, length: { maximum: DEFAULT_FIELDS_SIZE }, if: -> { local? && will_save_change_to_fields? } diff --git a/app/models/user.rb b/app/models/user.rb index 966ffbe9c..81e3c50a9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -142,7 +142,9 @@ class User < ApplicationRecord delegate :can?, to: :role attr_reader :invite_code, :date_of_birth - attr_writer :external, :bypass_registration_checks, :current_account + attr_writer :external, :current_account + + attribute :bypass_registration_checks, :boolean, default: false def self.those_who_can(*any_of_privileges) matching_role_ids = UserRole.that_can(*any_of_privileges).map(&:id) @@ -505,10 +507,6 @@ class User < ApplicationRecord !!@external end - def bypass_registration_checks? - @bypass_registration_checks - end - def sanitize_role self.role = nil if role.present? && role.everyone? end diff --git a/spec/lib/mastodon/cli/accounts_spec.rb b/spec/lib/mastodon/cli/accounts_spec.rb index 238f72f83..7287b6a03 100644 --- a/spec/lib/mastodon/cli/accounts_spec.rb +++ b/spec/lib/mastodon/cli/accounts_spec.rb @@ -32,6 +32,7 @@ RSpec.describe Mastodon::CLI::Accounts do describe '#create' do let(:action) { :create } + let(:username) { 'tootctl_username' } shared_examples 'a new user with given email address and username' do it 'creates user and accounts from options and displays success message' do @@ -48,18 +49,24 @@ RSpec.describe Mastodon::CLI::Accounts do end def account_from_options - Account.find_local('tootctl_username') + Account.find_local(username) end end context 'when required USERNAME and --email are provided' do - let(:arguments) { ['tootctl_username'] } + let(:arguments) { [username] } context 'with USERNAME and --email only' do let(:options) { { email: 'tootctl@example.com' } } it_behaves_like 'a new user with given email address and username' + context 'with a reserved username' do + let(:username) { 'security' } + + it_behaves_like 'a new user with given email address and username' + end + context 'with invalid --email value' do let(:options) { { email: 'invalid' } } From 97f118013a739530c9c4ae56fc400695d686b94f Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 18 Aug 2025 03:35:56 -0400 Subject: [PATCH 09/29] Include `update` in the resources args for api/web/push_subscriptions route (#35801) --- config/routes/api.rb | 6 +----- spec/requests/api/web/push_subscriptions_spec.rb | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/config/routes/api.rb b/config/routes/api.rb index 4040a4350..93d7274ab 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -361,10 +361,6 @@ namespace :api, format: false do namespace :web do resource :settings, only: [:update] resources :embeds, only: [:show] - resources :push_subscriptions, only: [:create, :destroy] do - member do - put :update - end - end + resources :push_subscriptions, only: [:create, :destroy, :update] end end diff --git a/spec/requests/api/web/push_subscriptions_spec.rb b/spec/requests/api/web/push_subscriptions_spec.rb index 42545b3d6..2ed62b0df 100644 --- a/spec/requests/api/web/push_subscriptions_spec.rb +++ b/spec/requests/api/web/push_subscriptions_spec.rb @@ -64,7 +64,7 @@ RSpec.describe 'API Web Push Subscriptions' do end end - describe 'PUT /api/web/push_subscriptions' do + describe 'PUT /api/web/push_subscriptions/:id' do before { sign_in Fabricate :user } let(:subscription) { Fabricate :web_push_subscription } From 567f337db3ce7364fab489d887439c6f0841dc8d Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 19 Aug 2025 16:16:30 +0200 Subject: [PATCH 10/29] Fix self-destruct scheduler behavior on some Redis setups (#35823) --- app/workers/scheduler/self_destruct_scheduler.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/workers/scheduler/self_destruct_scheduler.rb b/app/workers/scheduler/self_destruct_scheduler.rb index 645d95732..d7aaef56e 100644 --- a/app/workers/scheduler/self_destruct_scheduler.rb +++ b/app/workers/scheduler/self_destruct_scheduler.rb @@ -21,8 +21,9 @@ class Scheduler::SelfDestructScheduler def sidekiq_overwhelmed? redis_mem_info = Sidekiq.default_configuration.redis_info + maxmemory = [redis_mem_info['maxmemory'].to_f, redis_mem_info['total_system_memory'].to_f].filter(&:positive?).min - Sidekiq::Stats.new.enqueued > MAX_ENQUEUED || redis_mem_info['used_memory'].to_f > redis_mem_info['total_system_memory'].to_f * MAX_REDIS_MEM_USAGE + Sidekiq::Stats.new.enqueued > MAX_ENQUEUED || redis_mem_info['used_memory'].to_f > maxmemory * MAX_REDIS_MEM_USAGE end def delete_accounts! From 36974aaa9997edbcb5b8bf00bff9f9fb17314e7c Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 20 Aug 2025 08:06:35 -0400 Subject: [PATCH 11/29] Use `debug?` query method on httplog initializer check (#35833) --- config/initializers/httplog.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/httplog.rb b/config/initializers/httplog.rb index 7a009e84d..ea4e32c7b 100644 --- a/config/initializers/httplog.rb +++ b/config/initializers/httplog.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -# Disable httplog in production unless log_level is `debug` -if !Rails.env.production? || Rails.configuration.log_level == :debug +# Disable in production unless log level is `debug` +if Rails.env.local? || Rails.logger.debug? require 'httplog' HttpLog.configure do |config| From b71216a08a2c567bc908bacc961a1d100ccb1ebe Mon Sep 17 00:00:00 2001 From: Shlee Date: Mon, 25 Aug 2025 06:54:47 +0000 Subject: [PATCH 12/29] Add crossorigin back to inert css (regression? of #30687) (#35876) --- app/views/layouts/application.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 15d08b3fc..f5318d6c2 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -30,7 +30,7 @@ = vite_react_refresh_tag = vite_polyfills_tag -# Needed for the wicg-inert polyfill. It needs to be on it's own