Add support for ingesting quote policies (#34479)
This commit is contained in:
		@@ -83,7 +83,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def process_status_params
 | 
			
		||||
    @status_parser = ActivityPub::Parser::StatusParser.new(@json, followers_collection: @account.followers_url, object: @object)
 | 
			
		||||
    @status_parser = ActivityPub::Parser::StatusParser.new(
 | 
			
		||||
      @json,
 | 
			
		||||
      followers_collection: @account.followers_url,
 | 
			
		||||
      actor_uri: ActivityPub::TagManager.instance.uri_for(@account),
 | 
			
		||||
      object: @object
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    attachment_ids = process_attachments.take(Status::MEDIA_ATTACHMENTS_LIMIT).map(&:id)
 | 
			
		||||
 | 
			
		||||
@@ -105,6 +110,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
 | 
			
		||||
      media_attachment_ids: attachment_ids,
 | 
			
		||||
      ordered_media_attachment_ids: attachment_ids,
 | 
			
		||||
      poll: process_poll,
 | 
			
		||||
      quote_approval_policy: @status_parser.quote_policy,
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ class ActivityPub::Parser::StatusParser
 | 
			
		||||
  # @param [Hash] json
 | 
			
		||||
  # @param [Hash] options
 | 
			
		||||
  # @option options [String] :followers_collection
 | 
			
		||||
  # @option options [String] :actor_uri
 | 
			
		||||
  # @option options [Hash]   :object
 | 
			
		||||
  def initialize(json, **options)
 | 
			
		||||
    @json    = json
 | 
			
		||||
@@ -101,6 +102,18 @@ class ActivityPub::Parser::StatusParser
 | 
			
		||||
    @object.dig(:shares, :totalItems)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def quote_policy
 | 
			
		||||
    flags = 0
 | 
			
		||||
    policy = @object.dig('interactionPolicy', 'canQuote')
 | 
			
		||||
    return flags if policy.blank?
 | 
			
		||||
 | 
			
		||||
    flags |= quote_subpolicy(policy['automaticApproval'])
 | 
			
		||||
    flags <<= 16
 | 
			
		||||
    flags |= quote_subpolicy(policy['manualApproval'])
 | 
			
		||||
 | 
			
		||||
    flags
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def quote_uri
 | 
			
		||||
    %w(quote _misskey_quote quoteUrl quoteUri).filter_map do |key|
 | 
			
		||||
      value_or_id(as_array(@object[key]).first)
 | 
			
		||||
@@ -113,6 +126,29 @@ class ActivityPub::Parser::StatusParser
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def quote_subpolicy(subpolicy)
 | 
			
		||||
    flags = 0
 | 
			
		||||
 | 
			
		||||
    allowed_actors = as_array(subpolicy)
 | 
			
		||||
    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])
 | 
			
		||||
    # TODO: we don't actually store that collection URI
 | 
			
		||||
    # flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:followed]
 | 
			
		||||
 | 
			
		||||
    # Remove the special-meaning actor URI
 | 
			
		||||
    allowed_actors.delete(@options[:actor_uri])
 | 
			
		||||
 | 
			
		||||
    # Tagged users are always allowed, so remove them
 | 
			
		||||
    allowed_actors -= as_array(@object['tag']).filter_map { |tag| tag['href'] if equals_or_includes?(tag['type'], 'Mention') }
 | 
			
		||||
 | 
			
		||||
    # Any unrecognized actor is marked as unknown
 | 
			
		||||
    flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:unknown] unless allowed_actors.empty?
 | 
			
		||||
 | 
			
		||||
    flags
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def raw_language_code
 | 
			
		||||
    if content_language_map?
 | 
			
		||||
      @object['contentMap'].keys.first
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@
 | 
			
		||||
#  trendable                    :boolean
 | 
			
		||||
#  ordered_media_attachment_ids :bigint(8)        is an Array
 | 
			
		||||
#  fetched_replies_at           :datetime
 | 
			
		||||
#  quote_approval_policy        :integer          default(0), not null
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
class Status < ApplicationRecord
 | 
			
		||||
@@ -44,6 +45,13 @@ class Status < ApplicationRecord
 | 
			
		||||
 | 
			
		||||
  MEDIA_ATTACHMENTS_LIMIT = 4
 | 
			
		||||
 | 
			
		||||
  QUOTE_APPROVAL_POLICY_FLAGS = {
 | 
			
		||||
    unknown: (1 << 0),
 | 
			
		||||
    public: (1 << 1),
 | 
			
		||||
    followers: (1 << 2),
 | 
			
		||||
    followed: (1 << 3),
 | 
			
		||||
  }.freeze
 | 
			
		||||
 | 
			
		||||
  rate_limit by: :account, family: :statuses
 | 
			
		||||
 | 
			
		||||
  self.discard_column = :deleted_at
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
 | 
			
		||||
 | 
			
		||||
    @activity_json             = activity_json
 | 
			
		||||
    @json                      = object_json
 | 
			
		||||
    @status_parser             = ActivityPub::Parser::StatusParser.new(@json)
 | 
			
		||||
    @status_parser             = ActivityPub::Parser::StatusParser.new(@json, followers_collection: status.account.followers_url, actor_uri: ActivityPub::TagManager.instance.uri_for(status.account))
 | 
			
		||||
    @uri                       = @status_parser.uri
 | 
			
		||||
    @status                    = status
 | 
			
		||||
    @account                   = status.account
 | 
			
		||||
@@ -41,6 +41,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
 | 
			
		||||
      Status.transaction do
 | 
			
		||||
        record_previous_edit!
 | 
			
		||||
        update_media_attachments!
 | 
			
		||||
        update_interaction_policies!
 | 
			
		||||
        update_poll!
 | 
			
		||||
        update_immediate_attributes!
 | 
			
		||||
        update_metadata!
 | 
			
		||||
@@ -62,12 +63,17 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
 | 
			
		||||
 | 
			
		||||
  def handle_implicit_update!
 | 
			
		||||
    with_redis_lock("create:#{@uri}") do
 | 
			
		||||
      update_interaction_policies!
 | 
			
		||||
      update_poll!(allow_significant_changes: false)
 | 
			
		||||
      queue_poll_notifications!
 | 
			
		||||
      update_counts!
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def update_interaction_policies!
 | 
			
		||||
    @status.quote_approval_policy = @status_parser.quote_policy
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def update_media_attachments!
 | 
			
		||||
    previous_media_attachments     = @status.media_attachments.to_a
 | 
			
		||||
    previous_media_attachments_ids = @status.ordered_media_attachment_ids || previous_media_attachments.map(&:id)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class AddQuoteApprovalPolicyToStatuses < ActiveRecord::Migration[8.0]
 | 
			
		||||
  def change
 | 
			
		||||
    add_column :statuses, :quote_approval_policy, :integer, null: false, default: 0
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
#
 | 
			
		||||
# It's strongly recommended that you check this file into your version control system.
 | 
			
		||||
 | 
			
		||||
ActiveRecord::Schema[8.0].define(version: 2025_04_25_134654) do
 | 
			
		||||
ActiveRecord::Schema[8.0].define(version: 2025_04_28_095029) do
 | 
			
		||||
  # These are extensions that must be enabled in order to support this database
 | 
			
		||||
  enable_extension "pg_catalog.plpgsql"
 | 
			
		||||
 | 
			
		||||
@@ -1086,6 +1086,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_25_134654) do
 | 
			
		||||
    t.boolean "trendable"
 | 
			
		||||
    t.bigint "ordered_media_attachment_ids", array: true
 | 
			
		||||
    t.datetime "fetched_replies_at"
 | 
			
		||||
    t.integer "quote_approval_policy", default: 0, null: false
 | 
			
		||||
    t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
 | 
			
		||||
    t.index ["account_id"], name: "index_statuses_on_account_id"
 | 
			
		||||
    t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
 | 
			
		||||
 
 | 
			
		||||
@@ -7,10 +7,11 @@ RSpec.describe ActivityPub::Parser::StatusParser do
 | 
			
		||||
 | 
			
		||||
  let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor') }
 | 
			
		||||
  let(:follower) { Fabricate(:account, username: 'bob') }
 | 
			
		||||
  let(:context) { 'https://www.w3.org/ns/activitystreams' }
 | 
			
		||||
 | 
			
		||||
  let(:json) do
 | 
			
		||||
    {
 | 
			
		||||
      '@context': 'https://www.w3.org/ns/activitystreams',
 | 
			
		||||
      '@context': context,
 | 
			
		||||
      id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join,
 | 
			
		||||
      type: 'Create',
 | 
			
		||||
      actor: ActivityPub::TagManager.instance.uri_for(sender),
 | 
			
		||||
@@ -47,4 +48,116 @@ RSpec.describe ActivityPub::Parser::StatusParser do
 | 
			
		||||
      language: :en
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#quote_policy' do
 | 
			
		||||
    subject do
 | 
			
		||||
      described_class
 | 
			
		||||
        .new(
 | 
			
		||||
          json,
 | 
			
		||||
          actor_uri: ActivityPub::TagManager.instance.uri_for(sender),
 | 
			
		||||
          followers_collection: sender.followers_url
 | 
			
		||||
        ).quote_policy
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    let(:context) do
 | 
			
		||||
      [
 | 
			
		||||
        'https://www.w3.org/ns/activitystreams',
 | 
			
		||||
        {
 | 
			
		||||
          gts: 'https://gotosocial.org/ns#',
 | 
			
		||||
          interactionPolicy: {
 | 
			
		||||
            '@id': 'gts:interactionPolicy',
 | 
			
		||||
            '@type': '@id',
 | 
			
		||||
          },
 | 
			
		||||
          canQuote: {
 | 
			
		||||
            '@id': 'gts:canQuote',
 | 
			
		||||
            '@type': '@id',
 | 
			
		||||
          },
 | 
			
		||||
          automaticApproval: {
 | 
			
		||||
            '@id': 'gts:automaticApproval',
 | 
			
		||||
            '@type': '@id',
 | 
			
		||||
          },
 | 
			
		||||
          manualApproval: {
 | 
			
		||||
            '@id': 'gts:manualApproval',
 | 
			
		||||
            '@type': '@id',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when nobody is allowed to quote' do
 | 
			
		||||
      let(:object_json) do
 | 
			
		||||
        {
 | 
			
		||||
          id: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
 | 
			
		||||
          type: 'Note',
 | 
			
		||||
          to: [
 | 
			
		||||
            'https://www.w3.org/ns/activitystreams#Public',
 | 
			
		||||
            ActivityPub::TagManager.instance.uri_for(follower),
 | 
			
		||||
          ],
 | 
			
		||||
          interactionPolicy: {
 | 
			
		||||
            canQuote: {
 | 
			
		||||
              automaticApproval: ActivityPub::TagManager.instance.uri_for(sender),
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          content: 'bleh',
 | 
			
		||||
          published: 1.hour.ago.utc.iso8601,
 | 
			
		||||
          updated: 1.hour.ago.utc.iso8601,
 | 
			
		||||
        }
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns a policy not allowing anyone to quote' do
 | 
			
		||||
        expect(subject).to eq 0
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when everybody is allowed to quote' do
 | 
			
		||||
      let(:object_json) do
 | 
			
		||||
        {
 | 
			
		||||
          id: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
 | 
			
		||||
          type: 'Note',
 | 
			
		||||
          to: [
 | 
			
		||||
            'https://www.w3.org/ns/activitystreams#Public',
 | 
			
		||||
            ActivityPub::TagManager.instance.uri_for(follower),
 | 
			
		||||
          ],
 | 
			
		||||
          interactionPolicy: {
 | 
			
		||||
            canQuote: {
 | 
			
		||||
              automaticApproval: 'https://www.w3.org/ns/activitystreams#Public',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          content: 'bleh',
 | 
			
		||||
          published: 1.hour.ago.utc.iso8601,
 | 
			
		||||
          updated: 1.hour.ago.utc.iso8601,
 | 
			
		||||
        }
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns a policy not allowing anyone to quote' do
 | 
			
		||||
        expect(subject).to eq(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when everybody is allowed to quote but only followers are automatically approved' do
 | 
			
		||||
      let(:object_json) do
 | 
			
		||||
        {
 | 
			
		||||
          id: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
 | 
			
		||||
          type: 'Note',
 | 
			
		||||
          to: [
 | 
			
		||||
            'https://www.w3.org/ns/activitystreams#Public',
 | 
			
		||||
            ActivityPub::TagManager.instance.uri_for(follower),
 | 
			
		||||
          ],
 | 
			
		||||
          interactionPolicy: {
 | 
			
		||||
            canQuote: {
 | 
			
		||||
              automaticApproval: sender.followers_url,
 | 
			
		||||
              manualApproval: 'https://www.w3.org/ns/activitystreams#Public',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          content: 'bleh',
 | 
			
		||||
          published: 1.hour.ago.utc.iso8601,
 | 
			
		||||
          updated: 1.hour.ago.utc.iso8601,
 | 
			
		||||
        }
 | 
			
		||||
      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)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user