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::FaspConcern
|
||||||
include Account::FinderConcern
|
include Account::FinderConcern
|
||||||
include Account::Header
|
include Account::Header
|
||||||
|
include Account::InteractionPolicyConcern
|
||||||
include Account::Interactions
|
include Account::Interactions
|
||||||
include Account::Mappings
|
include Account::Mappings
|
||||||
include Account::Merging
|
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 :memorial, if: :memorial?
|
||||||
|
|
||||||
|
attribute :feature_approval, if: -> { Mastodon::Feature.collections_enabled? }
|
||||||
|
|
||||||
class AccountDecorator < SimpleDelegator
|
class AccountDecorator < SimpleDelegator
|
||||||
def self.model_name
|
def self.model_name
|
||||||
Account.model_name
|
Account.model_name
|
||||||
@ -157,4 +159,12 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||||||
def moved_and_not_nested?
|
def moved_and_not_nested?
|
||||||
object.moved?
|
object.moved?
|
||||||
end
|
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
|
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'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe REST::AccountSerializer do
|
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(:default_datetime) { DateTime.new(2024, 11, 28, 16, 20, 0) }
|
||||||
let(:role) { Fabricate(:user_role, name: 'Role', highlighted: true) }
|
let(:role) { Fabricate(:user_role, name: 'Role', highlighted: true) }
|
||||||
let(:user) { Fabricate(:user, role: role) }
|
let(:user) { Fabricate(:user, role: role) }
|
||||||
let(:account) { user.account }
|
let(:account) { user.account }
|
||||||
|
let(:current_user) { Fabricate(:user) }
|
||||||
|
|
||||||
context 'when the account is suspended' do
|
context 'when the account is suspended' do
|
||||||
before do
|
before do
|
||||||
@ -68,4 +74,51 @@ RSpec.describe REST::AccountSerializer do
|
|||||||
expect(subject['last_status_at']).to eq('2024-11-28')
|
expect(subject['last_status_at']).to eq('2024-11-28')
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user