Change public self-quotes of private posts to be disallowed (#35975)
Some checks failed
Check i18n / check-i18n (push) Waiting to run
Chromatic / Run Chromatic (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.2) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Crowdin / Upload translations / upload-translations (push) Has been cancelled

This commit is contained in:
Claire 2025-09-02 17:48:37 +02:00 committed by GitHub
parent 5d9a9c76fb
commit 75fca715e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 70 additions and 24 deletions

View File

@ -89,6 +89,24 @@ const selectStatusPolicy = createAppSelector(
}, },
); );
const selectDisablePublicVisibilities = createAppSelector(
[
(state) => state.statuses,
(_state, statusId?: string) => !!statusId,
(state) => state.compose.get('quoted_status_id') as string | null,
],
(statuses, isEditing, statusId) => {
if (isEditing || !statusId) return false;
const status = statuses.get(statusId);
if (!status) {
return false;
}
return status.get('visibility') === 'private';
},
);
export const VisibilityModal: FC<VisibilityModalProps> = forwardRef( export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
({ onClose, onChange, statusId }, _ref) => { ({ onClose, onChange, statusId }, _ref) => {
@ -110,24 +128,12 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
const disableVisibility = !!statusId; const disableVisibility = !!statusId;
const disableQuotePolicy = const disableQuotePolicy =
visibility === 'private' || visibility === 'direct'; visibility === 'private' || visibility === 'direct';
const disablePublicVisibilities: boolean = useAppSelector(
selectDisablePublicVisibilities,
);
const visibilityItems = useMemo<SelectItem<StatusVisibility>[]>( const visibilityItems = useMemo<SelectItem<StatusVisibility>[]>(() => {
() => [ const items: SelectItem<StatusVisibility>[] = [
{
value: 'public',
text: intl.formatMessage(privacyMessages.public_short),
meta: intl.formatMessage(privacyMessages.public_long),
icon: 'globe',
iconComponent: PublicIcon,
},
{
value: 'unlisted',
text: intl.formatMessage(privacyMessages.unlisted_short),
meta: intl.formatMessage(privacyMessages.unlisted_long),
extra: intl.formatMessage(privacyMessages.unlisted_extra),
icon: 'unlock',
iconComponent: QuietTimeIcon,
},
{ {
value: 'private', value: 'private',
text: intl.formatMessage(privacyMessages.private_short), text: intl.formatMessage(privacyMessages.private_short),
@ -142,9 +148,30 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
icon: 'at', icon: 'at',
iconComponent: AlternateEmailIcon, iconComponent: AlternateEmailIcon,
}, },
], ];
[intl],
); if (!disablePublicVisibilities) {
items.unshift(
{
value: 'public',
text: intl.formatMessage(privacyMessages.public_short),
meta: intl.formatMessage(privacyMessages.public_long),
icon: 'globe',
iconComponent: PublicIcon,
},
{
value: 'unlisted',
text: intl.formatMessage(privacyMessages.unlisted_short),
meta: intl.formatMessage(privacyMessages.unlisted_long),
extra: intl.formatMessage(privacyMessages.unlisted_extra),
icon: 'unlock',
iconComponent: QuietTimeIcon,
},
);
}
return items;
}, [intl, disablePublicVisibilities]);
const quoteItems = useMemo<SelectItem<ApiQuotePolicy>[]>( const quoteItems = useMemo<SelectItem<ApiQuotePolicy>[]>(
() => [ () => [
{ value: 'public', text: intl.formatMessage(messages.quotePublic) }, { value: 'public', text: intl.formatMessage(messages.quotePublic) },
@ -236,6 +263,14 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
/> />
</p> </p>
)} )}
{!statusId && disablePublicVisibilities && (
<p className='visibility-dropdown__helper'>
<FormattedMessage
id='visibility_modal.helper.privacy_private_self_quote'
defaultMessage='Self-quotes of private posts cannot be made public.'
/>
</p>
)}
</label> </label>
<label <label

View File

@ -989,6 +989,7 @@
"visibility_modal.header": "Visibility and interaction", "visibility_modal.header": "Visibility and interaction",
"visibility_modal.helper.direct_quoting": "Private mentions authored on Mastodon can't be quoted by others.", "visibility_modal.helper.direct_quoting": "Private mentions authored on Mastodon can't be quoted by others.",
"visibility_modal.helper.privacy_editing": "Published posts cannot change their visibility.", "visibility_modal.helper.privacy_editing": "Published posts cannot change their visibility.",
"visibility_modal.helper.privacy_private_self_quote": "Self-quotes of private posts cannot be made public.",
"visibility_modal.helper.private_quoting": "Follower-only posts authored on Mastodon can't be quoted by others.", "visibility_modal.helper.private_quoting": "Follower-only posts authored on Mastodon can't be quoted by others.",
"visibility_modal.helper.unlisted_quoting": "When people quote you, their post will also be hidden from trending timelines.", "visibility_modal.helper.unlisted_quoting": "When people quote you, their post will also be hidden from trending timelines.",
"visibility_modal.instructions": "Control who can interact with this post. You can also apply settings to all future posts by navigating to <link>Preferences > Posting defaults</link>.", "visibility_modal.instructions": "Control who can interact with this post. You can also apply settings to all future posts by navigating to <link>Preferences > Posting defaults</link>.",

View File

@ -334,7 +334,8 @@ export const composeReducer = (state = initialState, action) => {
return state return state
.set('quoted_status_id', status.get('id')) .set('quoted_status_id', status.get('id'))
.set('spoiler', status.get('sensitive')) .set('spoiler', status.get('sensitive'))
.set('spoiler_text', status.get('spoiler_text')); .set('spoiler_text', status.get('spoiler_text'))
.update('privacy', (visibility) => ['public', 'unlisted'].includes(visibility) && status.get('visibility') === 'private' ? 'private' : visibility);
} else if (quoteComposeCancel.match(action)) { } else if (quoteComposeCancel.match(action)) {
return state.set('quoted_status_id', null); return state.set('quoted_status_id', null);
} else if (setComposeQuotePolicy.match(action)) { } else if (setComposeQuotePolicy.match(action)) {

View File

@ -27,7 +27,7 @@ module Status::InteractionPolicyConcern
# Returns `:automatic`, `:manual`, `:unknown` or `:denied` # Returns `:automatic`, `:manual`, `:unknown` or `:denied`
def quote_policy_for_account(other_account, preloaded_relations: {}) def quote_policy_for_account(other_account, preloaded_relations: {})
return :denied if other_account.nil? return :denied if other_account.nil? || direct_visibility?
following_author = nil following_author = nil

View File

@ -69,6 +69,7 @@ class PostStatusService < BaseService
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present? @text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
@visibility = @options[:visibility] || @account.user&.setting_default_privacy @visibility = @options[:visibility] || @account.user&.setting_default_privacy
@visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced? @visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced?
@visibility = :private if @quoted_status&.private_visibility?
@scheduled_at = @options[:scheduled_at]&.to_datetime @scheduled_at = @options[:scheduled_at]&.to_datetime
@scheduled_at = nil if scheduled_in_the_past? @scheduled_at = nil if scheduled_in_the_past?
rescue ArgumentError rescue ArgumentError

View File

@ -88,10 +88,10 @@ RSpec.describe StatusPolicy, type: :model do
context 'with the permission of quote?' do context 'with the permission of quote?' do
permissions :quote? do permissions :quote? do
it 'grants access when direct and account is viewer' do it 'does not grant access when direct and account is viewer' do
status.visibility = :direct status.visibility = :direct
expect(subject).to permit(status.account, status) expect(subject).to_not permit(status.account, status)
end end
it 'does not grant access access when direct and viewer is mentioned but not explicitly allowed' do it 'does not grant access access when direct and viewer is mentioned but not explicitly allowed' do

View File

@ -305,6 +305,14 @@ RSpec.describe PostStatusService do
.to enqueue_sidekiq_job(ActivityPub::QuoteRequestWorker) .to enqueue_sidekiq_job(ActivityPub::QuoteRequestWorker)
end end
it 'correctly downgrades visibility for private self-quotes' do
account = Fabricate(:account)
quoted_status = Fabricate(:status, account: account, visibility: :private)
status = subject.call(account, text: 'test', quoted_status: quoted_status)
expect(status).to be_private_visibility
end
it 'returns existing status when used twice with idempotency key' do it 'returns existing status when used twice with idempotency key' do
account = Fabricate(:account) account = Fabricate(:account)
status1 = subject.call(account, text: 'test', idempotency: 'meepmeep') status1 = subject.call(account, text: 'test', idempotency: 'meepmeep')