Add PUT /api/v1/statuses/:status_id/interaction_policy (#35769)

This commit is contained in:
Claire 2025-08-13 17:51:16 +02:00 committed by GitHub
parent 49a6e4cbb5
commit 2648bbdc51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 152 additions and 17 deletions

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::BaseController
include Api::InteractionPoliciesConcern
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action -> { check_feature_enabled }
def update
authorize @status, :update?
@status.update!(quote_approval_policy: quote_approval_policy)
broadcast_updates! if @status.quote_approval_policy_previously_changed?
render json: @status, serializer: REST::StatusSerializer
end
private
def status_params
params.permit(:quote_approval_policy)
end
def check_feature_enabled
raise ActionController::RoutingError unless Mastodon::Feature.outgoing_quotes_enabled?
end
def broadcast_updates!
DistributionWorker.perform_async(@status.id, { 'update' => true })
ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id)
end
end

View File

@ -3,6 +3,7 @@
class Api::V1::StatusesController < Api::BaseController
include Authorization
include AsyncRefreshesConcern
include Api::InteractionPoliciesConcern
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
@ -205,23 +206,6 @@ class Api::V1::StatusesController < Api::BaseController
)
end
def quote_approval_policy
# TODO: handle `nil` separately
return nil unless Mastodon::Feature.outgoing_quotes_enabled? && status_params[:quote_approval_policy].present?
case status_params[:quote_approval_policy]
when 'public'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16
when 'followers'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16
when 'nobody'
0
else
# TODO: raise more useful message
raise ActiveRecord::RecordInvalid
end
end
def serializer_for_status
@status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Api::InteractionPoliciesConcern
extend ActiveSupport::Concern
def quote_approval_policy
# TODO: handle `nil` separately
return nil unless Mastodon::Feature.outgoing_quotes_enabled? && status_params[:quote_approval_policy].present?
case status_params[:quote_approval_policy]
when 'public'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16
when 'followers'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16
when 'nobody'
0
else
# TODO: raise more useful message
raise ActiveRecord::RecordInvalid
end
end
end

View File

@ -39,6 +39,8 @@ namespace :api, format: false do
resource :history, only: :show
resource :source, only: :show
resource :interaction_policy, only: :update
post :translate, to: 'translations#create'
end

View File

@ -0,0 +1,94 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Interaction policies', feature: :outgoing_quotes do
let(:user) { Fabricate(:user) }
let(:scopes) { 'write:statuses' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
let(:status) { Fabricate(:status, account: user.account) }
let(:params) { { quote_approval_policy: 'followers' } }
describe 'PUT /api/v1/statuses/:status_id/interaction_policy' do
subject do
put "/api/v1/statuses/#{status.id}/interaction_policy", headers: headers, params: params
end
it_behaves_like 'forbidden for wrong scope', 'read read:statuses'
context 'without an authorization header' do
let(:headers) { {} }
it 'returns http unauthorized' do
expect { subject }
.to_not(change { status.reload.quote_approval_policy })
expect(response).to have_http_status(401)
expect(response.content_type)
.to start_with('application/json')
end
end
context 'with a status from a different user' do
let(:status) { Fabricate(:status) }
it 'returns http unauthorized' do
expect { subject }
.to_not(change { status.reload.quote_approval_policy })
expect(response).to have_http_status(403)
expect(response.content_type)
.to start_with('application/json')
end
end
context 'when changing the interaction policy' do
it 'changes the interaction policy, returns the updated status, and schedules distribution jobs' do
expect { subject }
.to change { status.reload.quote_approval_policy }.to(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16)
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body).to include(
'quote_approval' => match(
'automatic' => ['followers'],
'manual' => [],
'current_user' => 'automatic'
)
)
expect(DistributionWorker)
.to have_enqueued_sidekiq_job(status.id, { 'update' => true })
expect(ActivityPub::StatusUpdateDistributionWorker)
.to have_enqueued_sidekiq_job(status.id)
end
end
context 'when not changing the interaction policy' do
let(:params) { { quote_approval_policy: 'nobody' } }
it 'keeps the interaction policy, returns the status, and does not schedule distribution jobs' do
expect { subject }
.to_not(change { status.reload.quote_approval_policy }.from(0))
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body).to include(
'quote_approval' => match(
'automatic' => [],
'manual' => [],
'current_user' => 'automatic'
)
)
expect(DistributionWorker)
.to_not have_enqueued_sidekiq_job
expect(ActivityPub::StatusUpdateDistributionWorker)
.to_not have_enqueued_sidekiq_job
end
end
end
end