mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-24 19:37:26 +00:00
Extract interaction policy class (#37277)
This commit is contained in:
@@ -6,9 +6,9 @@ module Api::InteractionPoliciesConcern
|
||||
def quote_approval_policy
|
||||
case status_params[:quote_approval_policy].presence || current_user.setting_default_quote_policy
|
||||
when 'public'
|
||||
Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16
|
||||
InteractionPolicy::POLICY_FLAGS[:public] << 16
|
||||
when 'followers'
|
||||
Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16
|
||||
InteractionPolicy::POLICY_FLAGS[:followers] << 16
|
||||
when 'nobody'
|
||||
0
|
||||
else
|
||||
|
||||
@@ -174,15 +174,15 @@ class ActivityPub::Parser::StatusParser
|
||||
allowed_actors = as_array(subpolicy).dup
|
||||
allowed_actors.uniq!
|
||||
|
||||
flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] if allowed_actors.delete('as:Public') || allowed_actors.delete('Public') || allowed_actors.delete('https://www.w3.org/ns/activitystreams#Public')
|
||||
flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] if allowed_actors.delete(@options[:followers_collection])
|
||||
flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:following] if allowed_actors.delete(@options[:following_collection])
|
||||
flags |= InteractionPolicy::POLICY_FLAGS[:public] if allowed_actors.delete('as:Public') || allowed_actors.delete('Public') || allowed_actors.delete('https://www.w3.org/ns/activitystreams#Public')
|
||||
flags |= InteractionPolicy::POLICY_FLAGS[:followers] if allowed_actors.delete(@options[:followers_collection])
|
||||
flags |= InteractionPolicy::POLICY_FLAGS[:following] if allowed_actors.delete(@options[:following_collection])
|
||||
|
||||
# Remove the special-meaning actor URI
|
||||
allowed_actors.delete(@options[:actor_uri])
|
||||
|
||||
# Any unrecognized actor is marked as unsupported
|
||||
flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:unsupported_policy] unless allowed_actors.empty?
|
||||
flags |= InteractionPolicy::POLICY_FLAGS[:unsupported_policy] unless allowed_actors.empty?
|
||||
|
||||
flags
|
||||
end
|
||||
|
||||
39
app/lib/interaction_policy.rb
Normal file
39
app/lib/interaction_policy.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class InteractionPolicy
|
||||
POLICY_FLAGS = {
|
||||
unsupported_policy: (1 << 0), # Not supported by Mastodon
|
||||
public: (1 << 1), # Everyone is allowed to interact
|
||||
followers: (1 << 2), # Only followers may interact
|
||||
following: (1 << 3), # Only accounts followed by the target may interact
|
||||
disabled: (1 << 4), # All interaction explicitly disabled
|
||||
}.freeze
|
||||
|
||||
class SubPolicy
|
||||
def initialize(bitmap)
|
||||
@bitmap = bitmap
|
||||
end
|
||||
|
||||
def as_keys
|
||||
POLICY_FLAGS.keys.select { |key| @bitmap.anybits?(POLICY_FLAGS[key]) }.map(&:to_s)
|
||||
end
|
||||
|
||||
POLICY_FLAGS.each_key do |key|
|
||||
define_method :"#{key}?" do
|
||||
@bitmap.anybits?(POLICY_FLAGS[key])
|
||||
end
|
||||
end
|
||||
|
||||
def missing?
|
||||
@bitmap.zero?
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :automatic, :manual
|
||||
|
||||
def initialize(bitmap)
|
||||
@bitmap = bitmap
|
||||
@automatic = SubPolicy.new(@bitmap >> 16)
|
||||
@manual = SubPolicy.new(@bitmap & 0xFFFF)
|
||||
end
|
||||
end
|
||||
@@ -3,26 +3,17 @@
|
||||
module Status::InteractionPolicyConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
QUOTE_APPROVAL_POLICY_FLAGS = {
|
||||
unsupported_policy: (1 << 0),
|
||||
public: (1 << 1),
|
||||
followers: (1 << 2),
|
||||
following: (1 << 3),
|
||||
}.freeze
|
||||
|
||||
included do
|
||||
composed_of :quote_interaction_policy, class_name: 'InteractionPolicy', mapping: { quote_approval_policy: :bitmap }
|
||||
|
||||
before_validation :downgrade_quote_policy, if: -> { local? && !distributable? }
|
||||
end
|
||||
|
||||
def quote_policy_as_keys(kind)
|
||||
case kind
|
||||
when :automatic
|
||||
policy = quote_approval_policy >> 16
|
||||
when :manual
|
||||
policy = quote_approval_policy & 0xFFFF
|
||||
end
|
||||
raise ArgumentError unless kind.in?(%i(automatic manual))
|
||||
|
||||
QUOTE_APPROVAL_POLICY_FLAGS.keys.select { |key| policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[key]) }.map(&:to_s)
|
||||
sub_policy = quote_interaction_policy.send(kind)
|
||||
sub_policy.as_keys
|
||||
end
|
||||
|
||||
# Returns `:automatic`, `:manual`, `:unknown` or `:denied`
|
||||
@@ -35,35 +26,36 @@ module Status::InteractionPolicyConcern
|
||||
# Post author is always allowed to quote themselves
|
||||
return :automatic if account_id == other_account.id
|
||||
|
||||
automatic_policy = quote_approval_policy >> 16
|
||||
manual_policy = quote_approval_policy & 0xFFFF
|
||||
automatic_policy = quote_interaction_policy.automatic
|
||||
|
||||
return :automatic if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:public])
|
||||
return :automatic if automatic_policy.public?
|
||||
|
||||
if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:followers])
|
||||
if automatic_policy.followers?
|
||||
following_author = other_account.following?(account) if following_author.nil?
|
||||
return :automatic if following_author
|
||||
end
|
||||
|
||||
if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:following])
|
||||
if automatic_policy.following?
|
||||
followed_by_author = account.following?(other_account) if followed_by_author.nil?
|
||||
return :automatic if followed_by_author
|
||||
end
|
||||
|
||||
# We don't know we are allowed by the automatic policy, considering the manual one
|
||||
return :manual if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:public])
|
||||
manual_policy = quote_interaction_policy.manual
|
||||
|
||||
if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:followers])
|
||||
return :manual if manual_policy.public?
|
||||
|
||||
if manual_policy.followers?
|
||||
following_author = other_account.following?(account) if following_author.nil?
|
||||
return :manual if following_author
|
||||
end
|
||||
|
||||
if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:following])
|
||||
if manual_policy.following?
|
||||
followed_by_author = account.following?(other_account) if followed_by_author.nil?
|
||||
return :manual if followed_by_author
|
||||
end
|
||||
|
||||
return :unknown if (automatic_policy | manual_policy).anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:unsupported_policy])
|
||||
return :unknown if [automatic_policy, manual_policy].any?(&:unsupported_policy?)
|
||||
|
||||
:denied
|
||||
end
|
||||
|
||||
@@ -235,10 +235,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
approved_uris = []
|
||||
|
||||
# On outgoing posts, only automatic approval is supported
|
||||
policy = object.quote_approval_policy >> 16
|
||||
approved_uris << ActivityPub::TagManager::COLLECTIONS[:public] if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public])
|
||||
approved_uris << ActivityPub::TagManager.instance.followers_uri_for(object.account) if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers])
|
||||
approved_uris << ActivityPub::TagManager.instance.following_uri_for(object.account) if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:following])
|
||||
policy = object.quote_interaction_policy.automatic
|
||||
approved_uris << ActivityPub::TagManager::COLLECTIONS[:public] if policy.public?
|
||||
approved_uris << ActivityPub::TagManager.instance.followers_uri_for(object.account) if policy.followers?
|
||||
approved_uris << ActivityPub::TagManager.instance.following_uri_for(object.account) if policy.following?
|
||||
approved_uris << ActivityPub::TagManager.instance.uri_for(object.account) if approved_uris.empty?
|
||||
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ class REST::ScheduledStatusSerializer < ActiveModel::Serializer
|
||||
def params
|
||||
object.params.merge(
|
||||
quoted_status_id: object.params['quoted_status_id']&.to_s,
|
||||
quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS.keys.find { |key| object.params['quote_approval_policy']&.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[key] << 16) }&.to_s || 'nobody'
|
||||
quote_approval_policy: InteractionPolicy::POLICY_FLAGS.keys.find { |key| object.params['quote_approval_policy']&.anybits?(InteractionPolicy::POLICY_FLAGS[key] << 16) }&.to_s || 'nobody'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -87,7 +87,7 @@ RSpec.describe ActivityPub::Activity::QuoteRequest do
|
||||
context 'when trying to quote a quotable local status' do
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/unknown-status').to_return(status: 200, body: Oj.dump(status_json), headers: { 'Content-Type': 'application/activity+json' })
|
||||
quoted_post.update(quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16)
|
||||
quoted_post.update(quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16)
|
||||
end
|
||||
|
||||
it 'accepts the quote and sends an Accept activity' do
|
||||
@@ -105,7 +105,7 @@ RSpec.describe ActivityPub::Activity::QuoteRequest do
|
||||
let(:instrument) { status_json.without('@context') }
|
||||
|
||||
before do
|
||||
quoted_post.update(quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16)
|
||||
quoted_post.update(quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16)
|
||||
end
|
||||
|
||||
it 'accepts the quote and sends an Accept activity' do
|
||||
|
||||
@@ -148,7 +148,7 @@ RSpec.describe ActivityPub::Parser::StatusParser do
|
||||
end
|
||||
|
||||
it 'returns a policy not allowing anyone to quote' do
|
||||
expect(subject).to eq(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16)
|
||||
expect(subject).to eq(InteractionPolicy::POLICY_FLAGS[:public] << 16)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -174,7 +174,7 @@ RSpec.describe ActivityPub::Parser::StatusParser do
|
||||
end
|
||||
|
||||
it 'returns a policy allowing everyone including followers' do
|
||||
expect(subject).to eq Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] | (Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16)
|
||||
expect(subject).to eq InteractionPolicy::POLICY_FLAGS[:public] | (InteractionPolicy::POLICY_FLAGS[:followers] << 16)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
41
spec/lib/interaction_policy_spec.rb
Normal file
41
spec/lib/interaction_policy_spec.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe InteractionPolicy do
|
||||
subject { described_class.new(bitmap) }
|
||||
|
||||
let(:bitmap) { (0b0101 << 16) | 0b0010 }
|
||||
|
||||
describe described_class::SubPolicy do
|
||||
subject { InteractionPolicy.new(bitmap) }
|
||||
|
||||
describe '#as_keys' do
|
||||
it 'returns the expected values' do
|
||||
expect(subject.automatic.as_keys).to eq ['unsupported_policy', 'followers']
|
||||
expect(subject.manual.as_keys).to eq ['public']
|
||||
end
|
||||
end
|
||||
|
||||
describe '#public?' do
|
||||
it 'returns the expected values' do
|
||||
expect(subject.automatic.public?).to be false
|
||||
expect(subject.manual.public?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#unsupported_policy?' do
|
||||
it 'returns the expected values' do
|
||||
expect(subject.automatic.unsupported_policy?).to be true
|
||||
expect(subject.manual.unsupported_policy?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#followers?' do
|
||||
it 'returns the expected values' do
|
||||
expect(subject.automatic.followers?).to be true
|
||||
expect(subject.manual.followers?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,7 +29,7 @@ RSpec.describe StatusCacheHydrator do
|
||||
end
|
||||
|
||||
context 'when handling a status with a quote policy' do
|
||||
let(:status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) }
|
||||
let(:status) { Fabricate(:status, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:followers] << 16) }
|
||||
|
||||
before do
|
||||
account.follow!(status.account)
|
||||
|
||||
@@ -152,19 +152,19 @@ RSpec.describe StatusPolicy, type: :model do
|
||||
end
|
||||
|
||||
it 'grants access when public and policy allows everyone' do
|
||||
status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:public]
|
||||
status.quote_approval_policy = InteractionPolicy::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]
|
||||
status.quote_approval_policy = InteractionPolicy::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]
|
||||
status.quote_approval_policy = InteractionPolicy::POLICY_FLAGS[:followers]
|
||||
viewer = Fabricate(:account)
|
||||
viewer.follow!(status.account)
|
||||
expect(subject).to permit(viewer, status)
|
||||
|
||||
@@ -46,7 +46,7 @@ RSpec.describe 'Interaction policies' do
|
||||
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)
|
||||
.to change { status.reload.quote_approval_policy }.to(InteractionPolicy::POLICY_FLAGS[:followers] << 16)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.content_type)
|
||||
|
||||
@@ -264,7 +264,7 @@ RSpec.describe '/api/v1/statuses' do
|
||||
end
|
||||
|
||||
context 'with a quote to a non-mentioned user in a Private Mention' do
|
||||
let!(:quoted_status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) }
|
||||
let!(:quoted_status) { Fabricate(:status, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16) }
|
||||
let(:params) do
|
||||
{
|
||||
status: 'Hello, this is a quote',
|
||||
@@ -283,7 +283,7 @@ RSpec.describe '/api/v1/statuses' do
|
||||
end
|
||||
|
||||
context 'with a quote to a mentioned user in a Private Mention' do
|
||||
let!(:quoted_status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) }
|
||||
let!(:quoted_status) { Fabricate(:status, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16) }
|
||||
let(:params) do
|
||||
{
|
||||
status: "Hello @#{quoted_status.account.acct}, this is a quote",
|
||||
@@ -305,7 +305,7 @@ RSpec.describe '/api/v1/statuses' do
|
||||
end
|
||||
|
||||
context 'with a quote of a reblog' do
|
||||
let(:quoted_status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) }
|
||||
let(:quoted_status) { Fabricate(:status, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16) }
|
||||
let(:reblog) { Fabricate(:status, reblog: quoted_status) }
|
||||
let(:params) do
|
||||
{
|
||||
@@ -501,7 +501,7 @@ RSpec.describe '/api/v1/statuses' do
|
||||
|
||||
it 'updates the status', :aggregate_failures do
|
||||
expect { subject }
|
||||
.to change { status.reload.quote_approval_policy }.to(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16)
|
||||
.to change { status.reload.quote_approval_policy }.to(InteractionPolicy::POLICY_FLAGS[:public] << 16)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.content_type)
|
||||
|
||||
@@ -74,7 +74,7 @@ RSpec.describe ActivityPub::NoteSerializer do
|
||||
end
|
||||
|
||||
context 'with a quote policy' do
|
||||
let(:parent) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) }
|
||||
let(:parent) { Fabricate(:status, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:followers] << 16) }
|
||||
|
||||
it 'has the expected shape' do
|
||||
expect(subject).to include({
|
||||
|
||||
@@ -10,7 +10,7 @@ RSpec.describe REST::ScheduledStatusSerializer do
|
||||
)
|
||||
end
|
||||
|
||||
let(:scheduled_status) { Fabricate.build(:scheduled_status, scheduled_at: 4.minutes.from_now, params: { application_id: 123, quoted_status_id: 456, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16 }) }
|
||||
let(:scheduled_status) { Fabricate.build(:scheduled_status, scheduled_at: 4.minutes.from_now, params: { application_id: 123, quoted_status_id: 456, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16 }) }
|
||||
|
||||
describe 'serialization' do
|
||||
it 'returns expected values and removes application_id from params' do
|
||||
|
||||
@@ -330,7 +330,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do
|
||||
end
|
||||
|
||||
it 'updates status' do
|
||||
expect(existing_status.reload.quote_approval_policy).to eq(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16)
|
||||
expect(existing_status.reload.quote_approval_policy).to eq(InteractionPolicy::POLICY_FLAGS[:public] << 16)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -602,7 +602,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||
|
||||
context 'when an approved quote of a local post gets updated through an explicit update, removing text' do
|
||||
let(:quoted_account) { Fabricate(:account) }
|
||||
let(:quoted_status) { Fabricate(:status, account: quoted_account, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) }
|
||||
let(:quoted_status) { Fabricate(:status, account: quoted_account, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16) }
|
||||
let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, state: :accepted) }
|
||||
let(:approval_uri) { ActivityPub::TagManager.instance.approval_uri_for(quote) }
|
||||
|
||||
@@ -638,7 +638,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||
|
||||
context 'when an approved quote of a local post gets updated through an explicit update' do
|
||||
let(:quoted_account) { Fabricate(:account) }
|
||||
let(:quoted_status) { Fabricate(:status, account: quoted_account, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) }
|
||||
let(:quoted_status) { Fabricate(:status, account: quoted_account, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16) }
|
||||
let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, state: :accepted) }
|
||||
let(:approval_uri) { ActivityPub::TagManager.instance.approval_uri_for(quote) }
|
||||
|
||||
|
||||
@@ -161,9 +161,9 @@ RSpec.describe PostStatusService do
|
||||
end
|
||||
|
||||
it 'creates a status with the quote approval policy set' do
|
||||
status = create_status_with_options(quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16)
|
||||
status = create_status_with_options(quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:followers] << 16)
|
||||
|
||||
expect(status.quote_approval_policy).to eq(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16)
|
||||
expect(status.quote_approval_policy).to eq(InteractionPolicy::POLICY_FLAGS[:followers] << 16)
|
||||
end
|
||||
|
||||
it 'processes mentions' do
|
||||
|
||||
Reference in New Issue
Block a user