Compare commits

...

3 Commits

Author SHA1 Message Date
Claire
6fa52e3517 Add ability to quote statuses to REST API 2025-07-10 12:03:05 +02:00
Claire
91a8d29110 Use StatusPolicy#quote? in local quote-posting code 2025-07-10 11:01:51 +02:00
Claire
54659824f0 Add StatusPolicy#quote? for quote acceptance 2025-07-10 10:47:50 +02:00
4 changed files with 122 additions and 1 deletions

View File

@ -9,6 +9,7 @@ class Api::V1::StatusesController < Api::BaseController
before_action :set_statuses, only: [:index]
before_action :set_status, only: [:show, :context]
before_action :set_thread, only: [:create]
before_action :set_quoted_status, only: [:create]
before_action :check_statuses_limit, only: [:index]
override_rate_limit_headers :create, family: :statuses
@ -67,6 +68,7 @@ class Api::V1::StatusesController < Api::BaseController
current_user.account,
text: status_params[:status],
thread: @thread,
quoted_status: @quoted_status,
media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text],
@ -138,6 +140,15 @@ class Api::V1::StatusesController < Api::BaseController
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
end
def set_quoted_status
@quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present?
authorize(@quoted_status, :quote?) if @quoted_status.present?
# TODO: handle non-local quote posts when we implement sending `QuoteRequest`
@quoted_status = nil unless @quoted_status.local?
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
render json: { error: I18n.t('statuses.errors.quoted_status_not_found') }, status: 404
end
def check_statuses_limit
raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT
end
@ -154,6 +165,7 @@ class Api::V1::StatusesController < Api::BaseController
params.permit(
:status,
:in_reply_to_id,
:quoted_status_id,
:sensitive,
:spoiler_text,
:visibility,

View File

@ -19,6 +19,11 @@ class StatusPolicy < ApplicationPolicy
end
end
# This is about requesting a quote post, not validating it
def quote?
owned? || active_mention_exists? || quote_approved_by_policy?
end
def reblog?
!requires_mention? && (!private? || owned?) && show? && !blocking_author?
end
@ -39,6 +44,14 @@ class StatusPolicy < ApplicationPolicy
private
def quote_approved_by_policy?
flattened_policy = record.quote_approval_policy | (record.quote_approval_policy >> 16)
return true if flattened_policy & (Status::QUOTE_APPROVAL_POLICY_FLAGS[:unknown] | Status::QUOTE_APPROVAL_POLICY_FLAGS[:public]) != 0
# TODO: support `:followed`
(flattened_policy & Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] != 0) && following_author?
end
def requires_mention?
record.direct_visibility? || record.limited_visibility?
end
@ -61,6 +74,16 @@ class StatusPolicy < ApplicationPolicy
end
end
def active_mention_exists?
return false if current_account.nil?
if record.active_mentions.loaded?
record.active_mentions.any? { |mention| mention.account_id == current_account.id }
else
record.active_mentions.exists?(account: current_account)
end
end
def author_blocking_domain?
return false if current_account.nil? || current_account.domain.nil?

View File

@ -97,7 +97,7 @@ class PostStatusService < BaseService
# we only support incoming quotes so far
status.quote = Quote.new(quoted_status: @quoted_status)
status.quote.accept! if @status.account == @quoted_status.account || @quoted_status.active_mentions.exists?(mentions: { account_id: status.account_id })
status.quote.accept! if StatusPolicy.new(@status.account, @quoted_status).quote? && @quoted_status.local?
# TODO: the following has yet to be implemented:
# - handle approval of local users (requires the interactionPolicy PR)

View File

@ -86,6 +86,92 @@ RSpec.describe StatusPolicy, type: :model do
end
end
context 'with the permission of quote?' do
permissions :quote? do
it 'grants access when direct and account is viewer' do
status.visibility = :direct
expect(subject).to permit(status.account, status)
end
it 'grants access when direct and viewer is mentioned' do
status.visibility = :direct
status.mentions = [Fabricate(:mention, account: alice)]
expect(subject).to permit(alice, status)
end
it 'grants access when direct and non-owner viewer is mentioned and mentions are loaded' do
status.visibility = :direct
status.mentions = [Fabricate(:mention, account: bob)]
status.active_mentions.load
expect(subject).to permit(bob, status)
end
it 'denies access when direct and viewer is not mentioned' do
viewer = Fabricate(:account)
status.visibility = :direct
expect(subject).to_not permit(viewer, status)
end
it 'denies access when private and viewer is not mentioned' do
viewer = Fabricate(:account)
status.visibility = :private
expect(subject).to_not permit(viewer, status)
end
it 'grants access when private and viewer is mentioned' do
status.visibility = :private
status.mentions = [Fabricate(:mention, account: bob)]
expect(subject).to permit(bob, status)
end
it 'denies access when private and non-viewer is mentioned' do
viewer = Fabricate(:account)
status.visibility = :private
status.mentions = [Fabricate(:mention, account: bob)]
expect(subject).to_not permit(viewer, status)
end
it 'denies access when private and account is following viewer' do
follow = Fabricate(:follow)
status.visibility = :private
status.account = follow.target_account
expect(subject).to_not permit(follow.account, status)
end
it 'denies access when public but policy does not allow anyone' do
viewer = Fabricate(:account)
expect(subject).to_not permit(viewer, status)
end
it 'grants access when public and policy allows everyone' do
status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:public]
viewer = Fabricate(:account)
expect(subject).to permit(viewer, status)
end
it 'denies access when public and policy allows followers but viewer is not one' do
status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers]
viewer = Fabricate(:account)
expect(subject).to_not permit(viewer, status)
end
it 'grants access when public and policy allows followers and viewer is one' do
status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers]
viewer = Fabricate(:account)
viewer.follow!(status.account)
expect(subject).to permit(viewer, status)
end
end
end
context 'with the permission of reblog?' do
permissions :reblog? do
it 'denies access when private' do