funquail/api/funkwhale_api/federation/views.py

303 lines
11 KiB
Python
Raw Normal View History

from django import forms
from django.core import paginator
from django.db import transaction
from django.http import HttpResponse, Http404
from django.urls import reverse
2018-06-10 10:55:16 +02:00
from rest_framework import mixins, response, viewsets
from rest_framework.decorators import detail_route, list_route
from funkwhale_api.common import preferences
from funkwhale_api.music import models as music_models
from funkwhale_api.users.permissions import HasUserPermission
2018-06-10 10:55:16 +02:00
from . import (
actors,
authentication,
filters,
library,
models,
permissions,
renderers,
serializers,
tasks,
utils,
webfinger,
2018-06-10 10:55:16 +02:00
)
class FederationMixin(object):
def dispatch(self, request, *args, **kwargs):
2018-06-09 15:36:16 +02:00
if not preferences.get("federation__enabled"):
return HttpResponse(status=405)
return super().dispatch(request, *args, **kwargs)
2018-07-22 10:20:16 +00:00
class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
2018-08-22 18:10:39 +00:00
lookup_field = "preferred_username"
lookup_value_regex = ".*"
2018-07-22 10:20:16 +00:00
authentication_classes = [authentication.SignatureAuthentication]
permission_classes = []
renderer_classes = [renderers.ActivityPubRenderer]
queryset = models.Actor.objects.local().select_related("user")
serializer_class = serializers.ActorSerializer
@detail_route(methods=["get", "post"])
def inbox(self, request, *args, **kwargs):
return response.Response({}, status=200)
@detail_route(methods=["get", "post"])
def outbox(self, request, *args, **kwargs):
return response.Response({}, status=200)
class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet):
2018-06-09 15:36:16 +02:00
lookup_field = "actor"
lookup_value_regex = "[a-z]*"
authentication_classes = [authentication.SignatureAuthentication]
permission_classes = []
renderer_classes = [renderers.ActivityPubRenderer]
def get_object(self):
try:
2018-06-09 15:36:16 +02:00
return actors.SYSTEM_ACTORS[self.kwargs["actor"]]
except KeyError:
raise Http404
def retrieve(self, request, *args, **kwargs):
system_actor = self.get_object()
actor = system_actor.get_actor_instance()
data = actor.system_conf.serialize()
return response.Response(data, status=200)
2018-06-09 15:36:16 +02:00
@detail_route(methods=["get", "post"])
def inbox(self, request, *args, **kwargs):
system_actor = self.get_object()
2018-06-09 15:36:16 +02:00
handler = getattr(system_actor, "{}_inbox".format(request.method.lower()))
try:
handler(request.data, actor=request.actor)
except NotImplementedError:
return response.Response(status=405)
2018-04-12 19:57:53 +02:00
return response.Response({}, status=200)
2018-06-09 15:36:16 +02:00
@detail_route(methods=["get", "post"])
def outbox(self, request, *args, **kwargs):
system_actor = self.get_object()
2018-06-09 15:36:16 +02:00
handler = getattr(system_actor, "{}_outbox".format(request.method.lower()))
try:
handler(request.data, actor=request.actor)
except NotImplementedError:
return response.Response(status=405)
2018-04-12 19:57:53 +02:00
return response.Response({}, status=200)
class WellKnownViewSet(viewsets.GenericViewSet):
authentication_classes = []
permission_classes = []
renderer_classes = [renderers.JSONRenderer, renderers.WebfingerRenderer]
2018-06-09 15:36:16 +02:00
@list_route(methods=["get"])
def nodeinfo(self, request, *args, **kwargs):
2018-06-09 15:36:16 +02:00
if not preferences.get("instance__nodeinfo_enabled"):
return HttpResponse(status=404)
data = {
2018-06-09 15:36:16 +02:00
"links": [
{
2018-06-09 15:36:16 +02:00
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
"href": utils.full_url(reverse("api:v1:instance:nodeinfo-2.0")),
}
]
}
return response.Response(data)
2018-06-09 15:36:16 +02:00
@list_route(methods=["get"])
def webfinger(self, request, *args, **kwargs):
2018-06-09 15:36:16 +02:00
if not preferences.get("federation__enabled"):
return HttpResponse(status=405)
try:
2018-06-09 15:36:16 +02:00
resource_type, resource = webfinger.clean_resource(request.GET["resource"])
cleaner = getattr(webfinger, "clean_{}".format(resource_type))
result = cleaner(resource)
2018-07-22 10:20:16 +00:00
handler = getattr(self, "handler_{}".format(resource_type))
data = handler(result)
except forms.ValidationError as e:
2018-06-09 15:36:16 +02:00
return response.Response({"errors": {"resource": e.message}}, status=400)
except KeyError:
2018-06-09 15:36:16 +02:00
return response.Response(
{"errors": {"resource": "This field is required"}}, status=400
)
return response.Response(data)
def handler_acct(self, clean_result):
username, hostname = clean_result
2018-07-22 10:20:16 +00:00
if username in actors.SYSTEM_ACTORS:
actor = actors.SYSTEM_ACTORS[username].get_actor_instance()
else:
try:
2018-08-22 18:10:39 +00:00
actor = models.Actor.objects.local().get(preferred_username=username)
2018-07-22 10:20:16 +00:00
except models.Actor.DoesNotExist:
raise forms.ValidationError("Invalid username")
return serializers.ActorWebfingerSerializer(actor).data
class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet):
2018-06-09 15:36:16 +02:00
authentication_classes = [authentication.SignatureAuthentication]
permission_classes = [permissions.LibraryFollower]
renderer_classes = [renderers.ActivityPubRenderer]
def list(self, request, *args, **kwargs):
2018-06-09 15:36:16 +02:00
page = request.GET.get("page")
library = actors.SYSTEM_ACTORS["library"].get_actor_instance()
qs = (
music_models.TrackFile.objects.order_by("-creation_date")
.select_related("track__artist", "track__album__artist")
.filter(library_track__isnull=True)
)
if page is None:
conf = {
2018-06-09 15:36:16 +02:00
"id": utils.full_url(reverse("federation:music:files-list")),
"page_size": preferences.get("federation__collection_page_size"),
"items": qs,
"item_serializer": serializers.AudioSerializer,
"actor": library,
}
serializer = serializers.PaginatedCollectionSerializer(conf)
data = serializer.data
else:
try:
page_number = int(page)
except Exception:
2018-06-09 15:36:16 +02:00
return response.Response({"page": ["Invalid page number"]}, status=400)
p = paginator.Paginator(
2018-06-09 15:36:16 +02:00
qs, preferences.get("federation__collection_page_size")
)
try:
page = p.page(page_number)
conf = {
2018-06-09 15:36:16 +02:00
"id": utils.full_url(reverse("federation:music:files-list")),
"page": page,
"item_serializer": serializers.AudioSerializer,
"actor": library,
}
serializer = serializers.CollectionPageSerializer(conf)
data = serializer.data
except paginator.EmptyPage:
return response.Response(status=404)
return response.Response(data)
2018-04-10 23:34:57 +02:00
class LibraryViewSet(
2018-06-09 15:36:16 +02:00
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet,
):
permission_classes = (HasUserPermission,)
2018-06-09 15:36:16 +02:00
required_permissions = ["federation"]
queryset = models.Library.objects.all().select_related("actor", "follow")
lookup_field = "uuid"
2018-04-10 23:34:57 +02:00
filter_class = filters.LibraryFilter
serializer_class = serializers.APILibrarySerializer
ordering_fields = (
2018-06-09 15:36:16 +02:00
"id",
"creation_date",
"fetched_date",
"actor__domain",
"tracks_count",
2018-04-10 23:34:57 +02:00
)
2018-06-09 15:36:16 +02:00
@list_route(methods=["get"])
def fetch(self, request, *args, **kwargs):
2018-06-09 15:36:16 +02:00
account = request.GET.get("account")
if not account:
2018-06-09 15:36:16 +02:00
return response.Response({"account": "This field is mandatory"}, status=400)
data = library.scan_from_account_name(account)
return response.Response(data)
2018-06-09 15:36:16 +02:00
@detail_route(methods=["post"])
def scan(self, request, *args, **kwargs):
library = self.get_object()
2018-06-09 15:36:16 +02:00
serializer = serializers.APILibraryScanSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
2018-04-12 20:38:06 +02:00
result = tasks.scan_library.delay(
2018-06-09 15:36:16 +02:00
library_id=library.pk, until=serializer.validated_data.get("until")
)
2018-06-09 15:36:16 +02:00
return response.Response({"task": result.id})
2018-06-09 15:36:16 +02:00
@list_route(methods=["get"])
def following(self, request, *args, **kwargs):
2018-06-09 15:36:16 +02:00
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
queryset = (
models.Follow.objects.filter(actor=library_actor)
.select_related("actor", "target")
.order_by("-creation_date")
)
filterset = filters.FollowFilter(request.GET, queryset=queryset)
2018-04-14 18:50:37 +02:00
final_qs = filterset.qs
serializer = serializers.APIFollowSerializer(final_qs, many=True)
2018-06-09 15:36:16 +02:00
data = {"results": serializer.data, "count": len(final_qs)}
return response.Response(data)
2018-06-09 15:36:16 +02:00
@list_route(methods=["get", "patch"])
def followers(self, request, *args, **kwargs):
2018-06-09 15:36:16 +02:00
if request.method.lower() == "patch":
serializer = serializers.APILibraryFollowUpdateSerializer(data=request.data)
2018-04-14 18:50:37 +02:00
serializer.is_valid(raise_exception=True)
follow = serializer.save()
2018-06-09 15:36:16 +02:00
return response.Response(serializers.APIFollowSerializer(follow).data)
2018-04-14 18:50:37 +02:00
2018-06-09 15:36:16 +02:00
library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance()
queryset = (
models.Follow.objects.filter(target=library_actor)
.select_related("actor", "target")
.order_by("-creation_date")
)
filterset = filters.FollowFilter(request.GET, queryset=queryset)
2018-04-14 18:50:37 +02:00
final_qs = filterset.qs
serializer = serializers.APIFollowSerializer(final_qs, many=True)
2018-06-09 15:36:16 +02:00
data = {"results": serializer.data, "count": len(final_qs)}
return response.Response(data)
@transaction.atomic
def create(self, request, *args, **kwargs):
serializer = serializers.APILibraryCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
2018-06-09 17:41:59 +02:00
serializer.save()
return response.Response(serializer.data, status=201)
2018-04-12 20:38:06 +02:00
2018-06-09 15:36:16 +02:00
class LibraryTrackViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
permission_classes = (HasUserPermission,)
2018-06-09 15:36:16 +02:00
required_permissions = ["federation"]
queryset = (
models.LibraryTrack.objects.all()
.select_related("library__actor", "library__follow", "local_track_file")
.prefetch_related("import_jobs")
)
2018-04-12 20:38:06 +02:00
filter_class = filters.LibraryTrackFilter
serializer_class = serializers.APILibraryTrackSerializer
ordering_fields = (
2018-06-09 15:36:16 +02:00
"id",
"artist_name",
"title",
"album_title",
"creation_date",
"modification_date",
"fetched_date",
"published_date",
2018-04-12 20:38:06 +02:00
)
2018-06-09 15:36:16 +02:00
@list_route(methods=["post"])
def action(self, request, *args, **kwargs):
2018-06-09 15:36:16 +02:00
queryset = models.LibraryTrack.objects.filter(local_track_file__isnull=True)
serializer = serializers.LibraryTrackActionSerializer(
2018-06-09 15:36:16 +02:00
request.data, queryset=queryset, context={"submitted_by": request.user}
)
serializer.is_valid(raise_exception=True)
result = serializer.save()
return response.Response(result, status=200)