mirror of
https://github.com/mastodon/mastodon.git
synced 2026-04-25 16:05:44 -05:00
Expose feature policy in API (#37322)
This commit is contained in:
parent
8d9192835d
commit
0231b6d350
|
|
@ -91,6 +91,7 @@ class Account < ApplicationRecord
|
|||
include Account::FaspConcern
|
||||
include Account::FinderConcern
|
||||
include Account::Header
|
||||
include Account::InteractionPolicyConcern
|
||||
include Account::Interactions
|
||||
include Account::Mappings
|
||||
include Account::Merging
|
||||
|
|
|
|||
69
app/models/concerns/account/interaction_policy_concern.rb
Normal file
69
app/models/concerns/account/interaction_policy_concern.rb
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Account::InteractionPolicyConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
composed_of :feature_interaction_policy, class_name: 'InteractionPolicy', mapping: { feature_approval_policy: :bitmap }
|
||||
end
|
||||
|
||||
def feature_policy_as_keys(kind)
|
||||
raise ArgumentError unless kind.in?(%i(automatic manual))
|
||||
return local_feature_policy(kind) if local?
|
||||
|
||||
sub_policy = feature_interaction_policy.send(kind)
|
||||
sub_policy.as_keys
|
||||
end
|
||||
|
||||
# Returns `:automatic`, `:manual`, `:unknown`, ':missing` or `:denied`
|
||||
def feature_policy_for_account(other_account)
|
||||
return :denied if other_account.nil? || (local? && !discoverable?)
|
||||
return :automatic if local?
|
||||
# Post author is always allowed to feature themselves
|
||||
return :automatic if self == other_account
|
||||
return :missing if feature_approval_policy.zero?
|
||||
|
||||
automatic_policy = feature_interaction_policy.automatic
|
||||
following_self = nil
|
||||
followed_by_self = nil
|
||||
|
||||
return :automatic if automatic_policy.public?
|
||||
|
||||
if automatic_policy.followers?
|
||||
following_self = followed_by?(other_account)
|
||||
return :automatic if following_self
|
||||
end
|
||||
|
||||
if automatic_policy.following?
|
||||
followed_by_self = following?(other_account)
|
||||
return :automatic if followed_by_self
|
||||
end
|
||||
|
||||
# We don't know we are allowed by the automatic policy, considering the manual one
|
||||
manual_policy = feature_interaction_policy.manual
|
||||
|
||||
return :manual if manual_policy.public?
|
||||
|
||||
if manual_policy.followers?
|
||||
following_self = followed_by?(other_account) if following_self.nil?
|
||||
return :manual if following_self
|
||||
end
|
||||
|
||||
if manual_policy.following?
|
||||
followed_by_self = following?(other_account) if followed_by_self.nil?
|
||||
return :manual if followed_by_self
|
||||
end
|
||||
|
||||
return :unknown if [automatic_policy, manual_policy].any?(&:unsupported_policy?)
|
||||
|
||||
:denied
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def local_feature_policy(kind)
|
||||
return [] if kind == :manual || !discoverable?
|
||||
|
||||
[:public]
|
||||
end
|
||||
end
|
||||
|
|
@ -20,6 +20,8 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||
|
||||
attribute :memorial, if: :memorial?
|
||||
|
||||
attribute :feature_approval, if: -> { Mastodon::Feature.collections_enabled? }
|
||||
|
||||
class AccountDecorator < SimpleDelegator
|
||||
def self.model_name
|
||||
Account.model_name
|
||||
|
|
@ -157,4 +159,12 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||
def moved_and_not_nested?
|
||||
object.moved?
|
||||
end
|
||||
|
||||
def feature_approval
|
||||
{
|
||||
automatic: object.feature_policy_as_keys(:automatic),
|
||||
manual: object.feature_policy_as_keys(:manual),
|
||||
current_user: object.feature_policy_for_account(current_user&.account),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Account::InteractionPolicyConcern do
|
||||
describe '#feature_policy_as_keys' do
|
||||
context 'when account is local' do
|
||||
context 'when account is discoverable' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
it 'returns public for automtatic and nothing for manual' do
|
||||
expect(account.feature_policy_as_keys(:automatic)).to eq [:public]
|
||||
expect(account.feature_policy_as_keys(:manual)).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is not discoverable' do
|
||||
let(:account) { Fabricate(:account, discoverable: false) }
|
||||
|
||||
it 'returns empty arrays for both inputs' do
|
||||
expect(account.feature_policy_as_keys(:automatic)).to eq []
|
||||
expect(account.feature_policy_as_keys(:manual)).to eq []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is remote' do
|
||||
let(:account) { Fabricate(:account, domain: 'example.com', feature_approval_policy: (0b0101 << 16) | 0b0010) }
|
||||
|
||||
it 'returns the expected values' do
|
||||
expect(account.feature_policy_as_keys(:automatic)).to eq ['unsupported_policy', 'followers']
|
||||
expect(account.feature_policy_as_keys(:manual)).to eq ['public']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#feature_policy_for_account' do
|
||||
context 'when account is remote' do
|
||||
let(:account) { Fabricate(:account, domain: 'example.com', feature_approval_policy:) }
|
||||
let(:feature_approval_policy) { (0b0101 << 16) | 0b0010 }
|
||||
let(:other_account) { Fabricate(:account) }
|
||||
|
||||
context 'when no policy is available' do
|
||||
let(:feature_approval_policy) { 0 }
|
||||
|
||||
context 'when both accounts are the same' do
|
||||
it 'returns :automatic' do
|
||||
expect(account.feature_policy_for_account(account)).to eq :automatic
|
||||
end
|
||||
end
|
||||
|
||||
context 'with two different accounts' do
|
||||
it 'returns :missing' do
|
||||
expect(account.feature_policy_for_account(other_account)).to eq :missing
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the other account is not following the account' do
|
||||
it 'returns :manual because of the public entry in the manual policy' do
|
||||
expect(account.feature_policy_for_account(other_account)).to eq :manual
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the other account is following the account' do
|
||||
before do
|
||||
other_account.follow!(account)
|
||||
end
|
||||
|
||||
it 'returns :automatic because of the followers entry in the automatic policy' do
|
||||
expect(account.feature_policy_for_account(other_account)).to eq :automatic
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the account falls into the unknown bucket' do
|
||||
let(:feature_approval_policy) { (0b0001 << 16) | 0b0100 }
|
||||
|
||||
it 'returns :automatic because of the followers entry in the automatic policy' do
|
||||
expect(account.feature_policy_for_account(other_account)).to eq :unknown
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,12 +3,18 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe REST::AccountSerializer do
|
||||
subject { serialized_record_json(account, described_class) }
|
||||
subject do
|
||||
serialized_record_json(account, described_class, options: {
|
||||
scope: current_user,
|
||||
scope_name: :current_user,
|
||||
})
|
||||
end
|
||||
|
||||
let(:default_datetime) { DateTime.new(2024, 11, 28, 16, 20, 0) }
|
||||
let(:role) { Fabricate(:user_role, name: 'Role', highlighted: true) }
|
||||
let(:user) { Fabricate(:user, role: role) }
|
||||
let(:account) { user.account }
|
||||
let(:current_user) { Fabricate(:user) }
|
||||
|
||||
context 'when the account is suspended' do
|
||||
before do
|
||||
|
|
@ -68,4 +74,51 @@ RSpec.describe REST::AccountSerializer do
|
|||
expect(subject['last_status_at']).to eq('2024-11-28')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#feature_approval' do
|
||||
# TODO: Remove when feature flag is removed
|
||||
context 'when collections feature is disabled' do
|
||||
it 'does not include the approval policy' do
|
||||
expect(subject).to_not have_key('feature_approval')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when collections feature is enabled', feature: :collections do
|
||||
context 'when account is local' do
|
||||
context 'when account is discoverable' do
|
||||
it 'includes a policy that allows featuring' do
|
||||
expect(subject['feature_approval']).to include({
|
||||
'automatic' => ['public'],
|
||||
'manual' => [],
|
||||
'current_user' => 'automatic',
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is not discoverable' do
|
||||
let(:account) { Fabricate(:account, discoverable: false) }
|
||||
|
||||
it 'includes a policy that disallows featuring' do
|
||||
expect(subject['feature_approval']).to include({
|
||||
'automatic' => [],
|
||||
'manual' => [],
|
||||
'current_user' => 'denied',
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is remote' do
|
||||
let(:account) { Fabricate(:account, domain: 'example.com', feature_approval_policy: 0b11000000000000000010) }
|
||||
|
||||
it 'includes the matching policy' do
|
||||
expect(subject['feature_approval']).to include({
|
||||
'automatic' => ['followers', 'following'],
|
||||
'manual' => ['public'],
|
||||
'current_user' => 'manual',
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user