Issue FeatureAuthorizations (#38004)

This commit is contained in:
David Roetzel
2026-02-27 12:00:21 +01:00
committed by GitHub
parent 3b7c33e763
commit 6b88dd4923
10 changed files with 172 additions and 8 deletions

View File

@@ -0,0 +1,29 @@
# frozen_string_literal: true
class ActivityPub::FeatureAuthorizationsController < ActivityPub::BaseController
include Authorization
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_collection_item
def show
expires_in 30.seconds, public: true if public_fetch_mode?
render json: @collection_item, serializer: ActivityPub::FeatureAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
private
def pundit_user
signed_request_account
end
def set_collection_item
@collection_item = @account.collection_items.accepted.find(params[:id])
authorize @collection_item.collection, :show?
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
end

View File

@@ -46,6 +46,7 @@ class Collection < ApplicationRecord
scope :with_items, -> { includes(:collection_items).merge(CollectionItem.with_accounts) }
scope :with_tag, -> { includes(:tag) }
scope :discoverable, -> { where(discoverable: true) }
scope :local, -> { where(local: true) }
def remote?
!local?

View File

@@ -40,6 +40,7 @@ class CollectionItem < ApplicationRecord
scope :ordered, -> { order(position: :asc) }
scope :with_accounts, -> { includes(account: [:account_stat, :user]) }
scope :not_blocked_by, ->(account) { where.not(accounts: { id: account.blocking }) }
scope :local, -> { joins(:collection).merge(Collection.local) }
def local_item_with_remote_account?
local? && account&.remote?

View File

@@ -0,0 +1,23 @@
# frozen_string_literal: true
class ActivityPub::FeatureAuthorizationSerializer < ActivityPub::Serializer
include RoutingHelper
attributes :id, :type, :interacting_object, :interaction_target
def id
ap_account_feature_authorization_url(object.account_id, object)
end
def type
'FeatureAuthorization'
end
def interaction_target
ActivityPub::TagManager.instance.uri_for(object.account)
end
def interacting_object
ActivityPub::TagManager.instance.uri_for(object.collection)
end
end

View File

@@ -1,7 +1,10 @@
# frozen_string_literal: true
class ActivityPub::FeaturedItemSerializer < ActivityPub::Serializer
attributes :id, :type, :featured_object, :featured_object_type
include RoutingHelper
attributes :id, :type, :featured_object, :featured_object_type,
:feature_authorization
def id
ActivityPub::TagManager.instance.uri_for(object)
@@ -18,4 +21,12 @@ class ActivityPub::FeaturedItemSerializer < ActivityPub::Serializer
def featured_object_type
object.account.actor_type || 'Person'
end
def feature_authorization
if object.account.local?
ap_account_feature_authorization_url(object.account_id, object)
else
object.approval_uri
end
end
end

View File

@@ -125,6 +125,7 @@ Rails.application.routes.draw do
scope path: 'ap', as: 'ap' do
resources :accounts, path: 'users', only: [:show], param: :id, concerns: :account_resources do
resources :collection_items, only: [:show]
resources :feature_authorizations, only: [:show], module: :activitypub
resources :featured_collections, only: [:index], module: :activitypub
resources :statuses, only: [:show] do

View File

@@ -0,0 +1,49 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'ActivityPub FeatureAuthorization endpoint' do
describe 'GET /ap/accounts/:account_id/feature_authorizations/:collection_item_id' do
let(:account) { Fabricate(:account) }
let(:collection) { Fabricate(:collection) }
let(:collection_item) { Fabricate(:collection_item, collection:, account:, state:) }
context 'with an accepted collection item' do
let(:state) { :accepted }
it 'returns http success and activity json' do
get ap_account_feature_authorization_path(account.id, collection_item)
expect(response)
.to have_http_status(200)
expect(response.media_type)
.to eq 'application/activity+json'
expect(response.parsed_body)
.to include(type: 'FeatureAuthorization')
end
end
shared_examples 'not found' do
it 'returns http not found' do
get ap_account_feature_authorization_path(collection.account_id, collection_item)
expect(response)
.to have_http_status(404)
end
end
context 'with a revoked collection item' do
let(:state) { :revoked }
it_behaves_like 'not found'
end
context 'with a collection item featuring a remote account' do
let(:account) { Fabricate(:remote_account) }
let(:state) { :accepted }
it_behaves_like 'not found'
end
end
end

View File

@@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ActivityPub::FeatureAuthorizationSerializer do
include RoutingHelper
subject { serialized_record_json(collection_item, described_class, adapter: ActivityPub::Adapter) }
describe 'serializing an object' do
let(:collection_item) { Fabricate(:collection_item) }
let(:tag_manager) { ActivityPub::TagManager.instance }
it 'returns the expected json structure' do
expect(subject)
.to include(
'type' => 'FeatureAuthorization',
'id' => ap_account_feature_authorization_url(collection_item.account_id, collection_item),
'interactionTarget' => tag_manager.uri_for(collection_item.account),
'interactingObject' => tag_manager.uri_for(collection_item.collection)
)
end
end
end

View File

@@ -3,6 +3,8 @@
require 'rails_helper'
RSpec.describe ActivityPub::FeaturedCollectionSerializer do
include RoutingHelper
subject { serialized_record_json(collection, described_class, adapter: ActivityPub::Adapter) }
let(:collection) do
@@ -35,12 +37,14 @@ RSpec.describe ActivityPub::FeaturedCollectionSerializer do
'type' => 'FeaturedItem',
'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_items.first.account),
'featuredObjectType' => 'Person',
'featureAuthorization' => ap_account_feature_authorization_url(collection_items.first.account_id, collection_items.first),
},
{
'id' => ActivityPub::TagManager.instance.uri_for(collection_items.last),
'type' => 'FeaturedItem',
'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_items.last.account),
'featuredObjectType' => 'Person',
'featureAuthorization' => ap_account_feature_authorization_url(collection_items.last.account_id, collection_items.last),
},
],
'published' => match_api_datetime_format,

View File

@@ -3,16 +3,37 @@
require 'rails_helper'
RSpec.describe ActivityPub::FeaturedItemSerializer do
include RoutingHelper
subject { serialized_record_json(collection_item, described_class, adapter: ActivityPub::Adapter) }
let(:collection_item) { Fabricate(:collection_item) }
it 'serializes to the expected structure' do
expect(subject).to include({
'type' => 'FeaturedItem',
'id' => ActivityPub::TagManager.instance.uri_for(collection_item),
'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_item.account),
'featuredObjectType' => 'Person',
})
context 'when a local account is featured' do
it 'serializes to the expected structure' do
expect(subject).to include({
'type' => 'FeaturedItem',
'id' => ActivityPub::TagManager.instance.uri_for(collection_item),
'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_item.account),
'featuredObjectType' => 'Person',
'featureAuthorization' => ap_account_feature_authorization_url(collection_item.account_id, collection_item),
})
end
end
context 'when a remote account is featured' do
let(:collection) { Fabricate(:collection) }
let(:account) { Fabricate(:remote_account) }
let(:collection_item) { Fabricate(:collection_item, collection:, account:, approval_uri: 'https://example.com/auth/1') }
it 'serializes to the expected structure' do
expect(subject).to include({
'type' => 'FeaturedItem',
'id' => ActivityPub::TagManager.instance.uri_for(collection_item),
'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_item.account),
'featuredObjectType' => 'Person',
'featureAuthorization' => 'https://example.com/auth/1',
})
end
end
end