diff --git a/app/controllers/concerns/api/interaction_policies_concern.rb b/app/controllers/concerns/api/interaction_policies_concern.rb index f1e1480c0c..0679c3c691 100644 --- a/app/controllers/concerns/api/interaction_policies_concern.rb +++ b/app/controllers/concerns/api/interaction_policies_concern.rb @@ -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 diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb index 83f03756ef..bc2abc0f1a 100644 --- a/app/lib/activitypub/parser/status_parser.rb +++ b/app/lib/activitypub/parser/status_parser.rb @@ -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 diff --git a/app/lib/interaction_policy.rb b/app/lib/interaction_policy.rb new file mode 100644 index 0000000000..8d0b8dd060 --- /dev/null +++ b/app/lib/interaction_policy.rb @@ -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 diff --git a/app/models/concerns/status/interaction_policy_concern.rb b/app/models/concerns/status/interaction_policy_concern.rb index ed1e7a237f..332feef86b 100644 --- a/app/models/concerns/status/interaction_policy_concern.rb +++ b/app/models/concerns/status/interaction_policy_concern.rb @@ -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 diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index f162f4ee24..455f8d72db 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -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? { diff --git a/app/serializers/rest/scheduled_status_serializer.rb b/app/serializers/rest/scheduled_status_serializer.rb index 71ddb7b3e1..cc66c7cc95 100644 --- a/app/serializers/rest/scheduled_status_serializer.rb +++ b/app/serializers/rest/scheduled_status_serializer.rb @@ -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 diff --git a/spec/lib/activitypub/activity/quote_request_spec.rb b/spec/lib/activitypub/activity/quote_request_spec.rb index aae4ce0338..db80448a80 100644 --- a/spec/lib/activitypub/activity/quote_request_spec.rb +++ b/spec/lib/activitypub/activity/quote_request_spec.rb @@ -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 diff --git a/spec/lib/activitypub/parser/status_parser_spec.rb b/spec/lib/activitypub/parser/status_parser_spec.rb index b251b63f43..3084f3ffd6 100644 --- a/spec/lib/activitypub/parser/status_parser_spec.rb +++ b/spec/lib/activitypub/parser/status_parser_spec.rb @@ -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 diff --git a/spec/lib/interaction_policy_spec.rb b/spec/lib/interaction_policy_spec.rb new file mode 100644 index 0000000000..2382c74349 --- /dev/null +++ b/spec/lib/interaction_policy_spec.rb @@ -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 diff --git a/spec/lib/status_cache_hydrator_spec.rb b/spec/lib/status_cache_hydrator_spec.rb index 8ad4e5614e..a6fea36397 100644 --- a/spec/lib/status_cache_hydrator_spec.rb +++ b/spec/lib/status_cache_hydrator_spec.rb @@ -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) diff --git a/spec/policies/status_policy_spec.rb b/spec/policies/status_policy_spec.rb index eb79aa6fcd..9690984317 100644 --- a/spec/policies/status_policy_spec.rb +++ b/spec/policies/status_policy_spec.rb @@ -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) diff --git a/spec/requests/api/v1/statuses/interaction_policies_spec.rb b/spec/requests/api/v1/statuses/interaction_policies_spec.rb index 321a68cd25..aa5819cdd7 100644 --- a/spec/requests/api/v1/statuses/interaction_policies_spec.rb +++ b/spec/requests/api/v1/statuses/interaction_policies_spec.rb @@ -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) diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index e63437cc66..5cfd4eaa48 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -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) diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index 0d11386d57..4970de709d 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -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({ diff --git a/spec/serializers/rest/scheduled_status_serializer_spec.rb b/spec/serializers/rest/scheduled_status_serializer_spec.rb index 6fc2f2eca9..9eb27035d3 100644 --- a/spec/serializers/rest/scheduled_status_serializer_spec.rb +++ b/spec/serializers/rest/scheduled_status_serializer_spec.rb @@ -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 diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 07d05d762f..6afee5f25e 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -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 diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 9d63c5f1fe..1a19cbb782 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -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) } diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index 96289cdeee..d226d77167 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -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