From a03f0ffea54432b6c2c5c36c9df7c3098dee3b8c Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Fri, 6 Apr 2018 17:59:06 +0200 Subject: [PATCH] We now have a library browsable via activitypub --- api/funkwhale_api/federation/actors.py | 16 +++++ api/funkwhale_api/federation/urls.py | 13 +++- api/funkwhale_api/federation/views.py | 55 +++++++++++++++- api/tests/federation/test_views.py | 90 +++++++++++++++++++++++++- 4 files changed, 169 insertions(+), 5 deletions(-) diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index fa1b56282..6f782ced4 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -50,6 +50,11 @@ class SystemActor(object): additional_attributes = {} manually_approves_followers = False + def serialize(self): + actor = self.get_actor_instance() + serializer = serializers.ActorSerializer() + return serializer.data + def get_actor_instance(self): args = self.get_instance_argument( self.id, @@ -172,6 +177,17 @@ class LibraryActor(SystemActor): 'manually_approves_followers': True } + def serialize(self): + data = super().serialize() + urls = data.setdefault('url', []) + urls.append({ + 'type': 'Link', + 'mediaType': 'application/activity+json', + 'name': 'library', + 'href': utils.full_url(reverse('federation:music:files-list')) + }) + return data + @property def manually_approves_followers(self): return settings.FEDERATION_MUSIC_NEEDS_APPROVAL diff --git a/api/funkwhale_api/federation/urls.py b/api/funkwhale_api/federation/urls.py index f2c6f4c78..e899869a4 100644 --- a/api/funkwhale_api/federation/urls.py +++ b/api/funkwhale_api/federation/urls.py @@ -1,8 +1,10 @@ -from rest_framework import routers +from django.conf.urls import include, url +from rest_framework import routers from . import views router = routers.SimpleRouter(trailing_slash=False) +music_router = routers.SimpleRouter(trailing_slash=False) router.register( r'federation/instance/actors', views.InstanceActorViewSet, @@ -12,4 +14,11 @@ router.register( views.WellKnownViewSet, 'well-known') -urlpatterns = router.urls +music_router.register( + r'federation/files', + views.MusicFilesViewSet, + 'files', +) +urlpatterns = router.urls + [ + url('music/', include((music_router.urls, 'music'), namespace='music')) +] diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py index 2e3feb8d0..390a371bc 100644 --- a/api/funkwhale_api/federation/views.py +++ b/api/funkwhale_api/federation/views.py @@ -1,16 +1,23 @@ from django import forms from django.conf import settings +from django.core import paginator from django.http import HttpResponse +from django.urls import reverse from rest_framework import viewsets from rest_framework import views from rest_framework import response from rest_framework.decorators import list_route, detail_route +from funkwhale_api.music.models import TrackFile +from funkwhale_api.music.serializers import AudioSerializer + from . import actors from . import authentication +from . import permissions from . import renderers from . import serializers +from . import utils from . import webfinger @@ -38,8 +45,8 @@ class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet): def retrieve(self, request, *args, **kwargs): system_actor = self.get_object() actor = system_actor.get_actor_instance() - serializer = serializers.ActorSerializer(actor) - return response.Response(serializer.data, status=200) + data = actor.system_conf.serialize() + return response.Response(data, status=200) @detail_route(methods=['get', 'post']) def inbox(self, request, *args, **kwargs): @@ -101,3 +108,47 @@ class WellKnownViewSet(FederationMixin, viewsets.GenericViewSet): username, hostname = clean_result actor = actors.SYSTEM_ACTORS[username].get_actor_instance() return serializers.ActorWebfingerSerializer(actor).data + + +class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet): + authentication_classes = [ + authentication.SignatureAuthentication] + permission_classes = [permissions.LibraryFollower] + renderer_classes = [renderers.ActivityPubRenderer] + + def list(self, request, *args, **kwargs): + page = request.GET.get('page') + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + qs = TrackFile.objects.order_by('-creation_date') + if page is None: + conf = { + 'id': utils.full_url(reverse('federation:music:files-list')), + 'page_size': settings.FEDERATION_COLLECTION_PAGE_SIZE, + 'items': qs, + 'item_serializer': AudioSerializer, + 'actor': library, + } + serializer = serializers.PaginatedCollectionSerializer(conf) + data = serializer.data + else: + try: + page_number = int(page) + except: + return response.Response( + {'page': ['Invalid page number']}, status=400) + p = paginator.Paginator( + qs, settings.FEDERATION_COLLECTION_PAGE_SIZE) + try: + page = p.page(page_number) + except paginator.EmptyPage: + return response.Response(status=404) + conf = { + 'id': utils.full_url(reverse('federation:music:files-list')), + 'page': page, + 'item_serializer': AudioSerializer, + 'actor': library, + } + serializer = serializers.CollectionPageSerializer(conf) + data = serializer.data + + return response.Response(data) diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py index 0d2ac882f..6f05a16f9 100644 --- a/api/tests/federation/test_views.py +++ b/api/tests/federation/test_views.py @@ -1,11 +1,13 @@ from django.urls import reverse +from django.core.paginator import Paginator import pytest from funkwhale_api.federation import actors from funkwhale_api.federation import serializers +from funkwhale_api.federation import utils from funkwhale_api.federation import webfinger - +from funkwhale_api.music.serializers import AudioSerializer @pytest.mark.parametrize('system_actor', actors.SYSTEM_ACTORS.keys()) @@ -62,3 +64,89 @@ def test_wellknown_webfinger_system( assert response.status_code == 200 assert response['Content-Type'] == 'application/jrd+json' assert response.data == serializer.data + + +def test_audio_file_list_requires_authenticated_actor( + db, settings, api_client): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True + url = reverse('federation:music:files-list') + response = api_client.get(url) + + assert response.status_code == 403 + + +def test_audio_file_list_actor_no_page( + db, settings, api_client, factories): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False + settings.FEDERATION_COLLECTION_PAGE_SIZE = 2 + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + tfs = factories['music.TrackFile'].create_batch(size=5) + conf = { + 'id': utils.full_url(reverse('federation:music:files-list')), + 'page_size': 2, + 'items': list(reversed(tfs)), # we order by -creation_date + 'item_serializer': AudioSerializer, + 'actor': library + } + expected = serializers.PaginatedCollectionSerializer(conf).data + url = reverse('federation:music:files-list') + response = api_client.get(url) + + assert response.status_code == 200 + assert response.data == expected + + +def test_audio_file_list_actor_page( + db, settings, api_client, factories): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False + settings.FEDERATION_COLLECTION_PAGE_SIZE = 2 + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + tfs = factories['music.TrackFile'].create_batch(size=5) + conf = { + 'id': utils.full_url(reverse('federation:music:files-list')), + 'page': Paginator(list(reversed(tfs)), 2).page(2), + 'item_serializer': AudioSerializer, + 'actor': library + } + expected = serializers.CollectionPageSerializer(conf).data + url = reverse('federation:music:files-list') + response = api_client.get(url, data={'page': 2}) + + assert response.status_code == 200 + assert response.data == expected + + +def test_audio_file_list_actor_page_error( + db, settings, api_client, factories): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False + url = reverse('federation:music:files-list') + response = api_client.get(url, data={'page': 'nope'}) + + assert response.status_code == 400 + + +def test_audio_file_list_actor_page_error_too_far( + db, settings, api_client, factories): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False + url = reverse('federation:music:files-list') + response = api_client.get(url, data={'page': 5000}) + + assert response.status_code == 404 + + +def test_library_actor_includes_library_link(db, settings, api_client): + actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + url = reverse( + 'federation:instance-actors-detail', + kwargs={'actor': 'library'}) + response = api_client.get(url) + expected_links = [ + { + 'type': 'Link', + 'name': 'library', + 'mediaType': 'application/activity+json', + 'href': utils.full_url(reverse('federation:music:files-list')) + } + ] + assert response.status_code == 200 + assert response.data['url'] == expected_links