Add collection endpoint (#37468)
This commit is contained in:
parent
2f91d9755d
commit
c218849204
@ -1,6 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ActivityPub::CollectionsController < ActivityPub::BaseController
|
class ActivityPub::CollectionsController < ActivityPub::BaseController
|
||||||
|
SUPPORTED_COLLECTIONS = %w(featured tags).freeze
|
||||||
|
|
||||||
vary_by -> { 'Signature' if authorized_fetch_mode? }
|
vary_by -> { 'Signature' if authorized_fetch_mode? }
|
||||||
|
|
||||||
before_action :require_account_signature!, if: :authorized_fetch_mode?
|
before_action :require_account_signature!, if: :authorized_fetch_mode?
|
||||||
|
|||||||
46
app/controllers/collections_controller.rb
Normal file
46
app/controllers/collections_controller.rb
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CollectionsController < ApplicationController
|
||||||
|
include WebAppControllerConcern
|
||||||
|
include SignatureAuthentication
|
||||||
|
include Authorization
|
||||||
|
include AccountOwnedConcern
|
||||||
|
|
||||||
|
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
||||||
|
|
||||||
|
before_action :check_feature_enabled
|
||||||
|
before_action :require_account_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
|
before_action :set_collection
|
||||||
|
|
||||||
|
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||||
|
skip_before_action :require_functional!, only: :show, unless: :limited_federation_mode?
|
||||||
|
|
||||||
|
def show
|
||||||
|
respond_to do |format|
|
||||||
|
# TODO: format.html
|
||||||
|
|
||||||
|
format.json do
|
||||||
|
expires_in expiration_duration, public: true if public_fetch_mode?
|
||||||
|
render_with_cache json: @collection, content_type: 'application/activity+json', serializer: ActivityPub::FeaturedCollectionSerializer, adapter: ActivityPub::Adapter
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_collection
|
||||||
|
@collection = @account.collections.find(params[:id])
|
||||||
|
authorize @collection, :show?
|
||||||
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
|
not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
def expiration_duration
|
||||||
|
recently_updated = @collection.updated_at > 15.minutes.ago
|
||||||
|
recently_updated ? 30.seconds : 5.minutes
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_feature_enabled
|
||||||
|
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -63,7 +63,7 @@ class ActivityPub::TagManager
|
|||||||
when :flag
|
when :flag
|
||||||
target.uri
|
target.uri
|
||||||
when :featured_collection
|
when :featured_collection
|
||||||
ap_account_featured_collection_url(target.account.id, target)
|
ap_account_collection_url(target.account.id, target)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -135,7 +135,7 @@ class ActivityPub::TagManager
|
|||||||
def collection_uri_for(target, ...)
|
def collection_uri_for(target, ...)
|
||||||
raise ArgumentError, 'target must be a local account' unless target.local?
|
raise ArgumentError, 'target must be a local account' unless target.local?
|
||||||
|
|
||||||
target.numeric_ap_id? ? ap_account_collection_url(target.id, ...) : account_collection_url(target, ...)
|
target.numeric_ap_id? ? ap_account_actor_collection_url(target.id, ...) : account_actor_collection_url(target, ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
def inbox_uri_for(target)
|
def inbox_uri_for(target)
|
||||||
|
|||||||
@ -6,7 +6,7 @@ class CollectionPolicy < ApplicationPolicy
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show?
|
def show?
|
||||||
true
|
current_account.nil? || (!owner_blocking? && !owner_blocking_domain?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create?
|
def create?
|
||||||
@ -24,6 +24,22 @@ class CollectionPolicy < ApplicationPolicy
|
|||||||
private
|
private
|
||||||
|
|
||||||
def owner?
|
def owner?
|
||||||
current_account == record.account
|
current_account == owner
|
||||||
|
end
|
||||||
|
|
||||||
|
def owner_blocking_domain?
|
||||||
|
return false if current_account.nil? || current_account.domain.nil?
|
||||||
|
|
||||||
|
owner.domain_blocking?(current_account.domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
def owner_blocking?
|
||||||
|
return false if current_account.nil?
|
||||||
|
|
||||||
|
current_account.blocked_by?(owner)
|
||||||
|
end
|
||||||
|
|
||||||
|
def owner
|
||||||
|
record.account
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -96,14 +96,14 @@ Rails.application.routes.draw do
|
|||||||
get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" }
|
get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" }
|
||||||
|
|
||||||
concern :account_resources do
|
concern :account_resources do
|
||||||
resources :featured_collections, only: [:show]
|
resources :collections, only: [:show], constraints: { id: /\d+/ }
|
||||||
resources :followers, only: [:index], controller: :follower_accounts
|
resources :followers, only: [:index], controller: :follower_accounts
|
||||||
resources :following, only: [:index], controller: :following_accounts
|
resources :following, only: [:index], controller: :following_accounts
|
||||||
|
|
||||||
scope module: :activitypub do
|
scope module: :activitypub do
|
||||||
resource :outbox, only: [:show]
|
resource :outbox, only: [:show]
|
||||||
resource :inbox, only: [:create]
|
resource :inbox, only: [:create]
|
||||||
resources :collections, only: [:show]
|
resources :collections, only: [:show], as: :actor_collections, constraints: { id: Regexp.union(ActivityPub::CollectionsController::SUPPORTED_COLLECTIONS) }
|
||||||
resource :followers_synchronization, only: [:show]
|
resource :followers_synchronization, only: [:show]
|
||||||
resources :quote_authorizations, only: [:show]
|
resources :quote_authorizations, only: [:show]
|
||||||
end
|
end
|
||||||
|
|||||||
@ -198,7 +198,7 @@ RSpec.describe ActivityPub::TagManager do
|
|||||||
|
|
||||||
it 'returns a string starting with web domain and with the expected path' do
|
it 'returns a string starting with web domain and with the expected path' do
|
||||||
expect(subject.uri_for(collection))
|
expect(subject.uri_for(collection))
|
||||||
.to eq("#{host_prefix}/ap/users/#{collection.account.id}/featured_collections/#{collection.id}")
|
.to eq("#{host_prefix}/ap/users/#{collection.account.id}/collections/#{collection.id}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -16,11 +16,23 @@ RSpec.describe CollectionPolicy do
|
|||||||
end
|
end
|
||||||
|
|
||||||
permissions :show? do
|
permissions :show? do
|
||||||
it 'permits everyone to show' do
|
it 'permits when no user is given' do
|
||||||
expect(policy).to permit(nil, collection)
|
expect(policy).to permit(nil, collection)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'permits unblocked users' do
|
||||||
expect(policy).to permit(owner, collection)
|
expect(policy).to permit(owner, collection)
|
||||||
expect(policy).to permit(other_user, collection)
|
expect(policy).to permit(other_user, collection)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'denies blocked users' do
|
||||||
|
domain_blocked_user = Fabricate(:remote_account)
|
||||||
|
owner.block_domain!(domain_blocked_user.domain)
|
||||||
|
owner.block!(other_user)
|
||||||
|
|
||||||
|
expect(policy).to_not permit(domain_blocked_user, collection)
|
||||||
|
expect(policy).to_not permit(other_user, collection)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
permissions :create? do
|
permissions :create? do
|
||||||
|
|||||||
@ -14,7 +14,7 @@ RSpec.describe 'ActivityPub Collections' do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET #show' do
|
describe 'GET #show' do
|
||||||
subject { get account_collection_path(id: id, account_username: account.username), headers: nil, sign_with: remote_account }
|
subject { get account_actor_collection_path(id: id, account_username: account.username), headers: nil, sign_with: remote_account }
|
||||||
|
|
||||||
context 'when id is "featured"' do
|
context 'when id is "featured"' do
|
||||||
let(:id) { 'featured' }
|
let(:id) { 'featured' }
|
||||||
@ -131,16 +131,5 @@ RSpec.describe 'ActivityPub Collections' do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when id is not "featured"' do
|
|
||||||
let(:id) { 'hoge' }
|
|
||||||
|
|
||||||
it 'returns http not found' do
|
|
||||||
subject
|
|
||||||
|
|
||||||
expect(response)
|
|
||||||
.to have_http_status(404)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
138
spec/requests/collections_spec.rb
Normal file
138
spec/requests/collections_spec.rb
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Collections' do
|
||||||
|
describe 'GET /@:account_username/collections/:id', feature: :collections do
|
||||||
|
subject { get account_collection_path(account, collection, format: :json) }
|
||||||
|
|
||||||
|
let(:collection) { Fabricate(:collection) }
|
||||||
|
let(:account) { collection.account }
|
||||||
|
|
||||||
|
context 'when signed out' do
|
||||||
|
context 'when account is permanently suspended' do
|
||||||
|
before do
|
||||||
|
account.suspend!
|
||||||
|
account.deletion_request.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http gone' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(410)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when account is temporarily suspended' do
|
||||||
|
before { account.suspend! }
|
||||||
|
|
||||||
|
it 'returns http forbidden' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when account is accessible' do
|
||||||
|
context 'with JSON' do
|
||||||
|
subject { get ap_account_collection_path(account.id, collection, format: :json) }
|
||||||
|
|
||||||
|
it 'renders ActivityPub FeaturedCollection object successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(200)
|
||||||
|
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
|
||||||
|
|
||||||
|
expect(response.headers).to include(
|
||||||
|
'Content-Type' => include('application/activity+json')
|
||||||
|
)
|
||||||
|
expect(response.parsed_body)
|
||||||
|
.to include({
|
||||||
|
'type' => 'FeaturedCollection',
|
||||||
|
'name' => collection.name,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when signed in' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
post user_session_path, params: { user: { email: user.email, password: user.password } }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when account blocks user' do
|
||||||
|
before { account.block!(user.account) }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with "HTTP Signature" access signed by a remote account' do
|
||||||
|
subject do
|
||||||
|
get account_collection_path(account, collection, format: :json),
|
||||||
|
headers: nil,
|
||||||
|
sign_with: remote_account
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:remote_account) { Fabricate(:account, domain: 'host.example') }
|
||||||
|
|
||||||
|
context 'when account blocks the remote account' do
|
||||||
|
before { account.block!(remote_account) }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when account domain blocks the domain of the remote account' do
|
||||||
|
before { account.block_domain!(remote_account.domain) }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with JSON' do
|
||||||
|
subject do
|
||||||
|
get ap_account_collection_path(account.id, collection, format: :json),
|
||||||
|
headers: nil,
|
||||||
|
sign_with: remote_account
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders ActivityPub FeaturedCollection object successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(200)
|
||||||
|
.and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie')
|
||||||
|
|
||||||
|
expect(response.headers).to include(
|
||||||
|
'Content-Type' => include('application/activity+json')
|
||||||
|
)
|
||||||
|
expect(response.parsed_body)
|
||||||
|
.to include({
|
||||||
|
'type' => 'FeaturedCollection',
|
||||||
|
'name' => collection.name,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
x
Reference in New Issue
Block a user