Skip to content
Snippets Groups Projects
views.py 11.1 KiB
Newer Older
from django import forms
from django.core import paginator
from django.http import HttpResponse, Http404
from django.urls import reverse
Eliot Berriot's avatar
Eliot Berriot committed
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
Eliot Berriot's avatar
Eliot Berriot committed
from . import (
    actors,
    authentication,
    filters,
    library,
    models,
    permissions,
    renderers,
    serializers,
    tasks,
    utils,


class FederationMixin(object):
    def dispatch(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
        if not preferences.get("federation__enabled"):
            return HttpResponse(status=405)
        return super().dispatch(request, *args, **kwargs)


class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    lookup_field = "preferred_username"
    lookup_value_regex = ".*"
    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):
Eliot Berriot's avatar
Eliot Berriot committed
    lookup_field = "actor"
    lookup_value_regex = "[a-z]*"
    authentication_classes = [authentication.SignatureAuthentication]
    permission_classes = []
    renderer_classes = [renderers.ActivityPubRenderer]
    def get_object(self):
        try:
Eliot Berriot's avatar
Eliot Berriot committed
            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)
Eliot Berriot's avatar
Eliot Berriot committed
    @detail_route(methods=["get", "post"])
    def inbox(self, request, *args, **kwargs):
        system_actor = self.get_object()
Eliot Berriot's avatar
Eliot Berriot committed
        handler = getattr(system_actor, "{}_inbox".format(request.method.lower()))
            handler(request.data, actor=request.actor)
        except NotImplementedError:
            return response.Response(status=405)
        return response.Response({}, status=200)
Eliot Berriot's avatar
Eliot Berriot committed
    @detail_route(methods=["get", "post"])
    def outbox(self, request, *args, **kwargs):
        system_actor = self.get_object()
Eliot Berriot's avatar
Eliot Berriot committed
        handler = getattr(system_actor, "{}_outbox".format(request.method.lower()))
            handler(request.data, actor=request.actor)
        except NotImplementedError:
            return response.Response(status=405)
        return response.Response({}, status=200)
class WellKnownViewSet(viewsets.GenericViewSet):
    authentication_classes = []
    permission_classes = []
    renderer_classes = [renderers.JSONRenderer, renderers.WebfingerRenderer]
Eliot Berriot's avatar
Eliot Berriot committed
    @list_route(methods=["get"])
    def nodeinfo(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
        if not preferences.get("instance__nodeinfo_enabled"):
            return HttpResponse(status=404)
        data = {
Eliot Berriot's avatar
Eliot Berriot committed
            "links": [
Eliot Berriot's avatar
Eliot Berriot committed
                    "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
                    "href": utils.full_url(reverse("api:v1:instance:nodeinfo-2.0")),
                }
            ]
        }
        return response.Response(data)

Eliot Berriot's avatar
Eliot Berriot committed
    @list_route(methods=["get"])
    def webfinger(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
        if not preferences.get("federation__enabled"):
            return HttpResponse(status=405)
Eliot Berriot's avatar
Eliot Berriot committed
            resource_type, resource = webfinger.clean_resource(request.GET["resource"])
            cleaner = getattr(webfinger, "clean_{}".format(resource_type))
            result = cleaner(resource)
            handler = getattr(self, "handler_{}".format(resource_type))
            data = handler(result)
        except forms.ValidationError as e:
Eliot Berriot's avatar
Eliot Berriot committed
            return response.Response({"errors": {"resource": e.message}}, status=400)
Eliot Berriot's avatar
Eliot Berriot committed
            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

        if username in actors.SYSTEM_ACTORS:
            actor = actors.SYSTEM_ACTORS[username].get_actor_instance()
        else:
            try:
                actor = models.Actor.objects.local().get(preferred_username=username)
            except models.Actor.DoesNotExist:
                raise forms.ValidationError("Invalid username")

        return serializers.ActorWebfingerSerializer(actor).data


class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet):
Eliot Berriot's avatar
Eliot Berriot committed
    authentication_classes = [authentication.SignatureAuthentication]
    permission_classes = [permissions.LibraryFollower]
    renderer_classes = [renderers.ActivityPubRenderer]

    def list(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
        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 = {
Eliot Berriot's avatar
Eliot Berriot committed
                "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)
Eliot Berriot's avatar
Eliot Berriot committed
                return response.Response({"page": ["Invalid page number"]}, status=400)
            p = paginator.Paginator(
Eliot Berriot's avatar
Eliot Berriot committed
                qs, preferences.get("federation__collection_page_size")
            )
            try:
                page = p.page(page_number)
Eliot Berriot's avatar
Eliot Berriot committed
                    "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)
Eliot Berriot's avatar
Eliot Berriot committed
class LibraryViewSet(
Eliot Berriot's avatar
Eliot Berriot committed
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    mixins.ListModelMixin,
    viewsets.GenericViewSet,
):
    permission_classes = (HasUserPermission,)
Eliot Berriot's avatar
Eliot Berriot committed
    required_permissions = ["federation"]
    queryset = models.Library.objects.all().select_related("actor", "follow")
    lookup_field = "uuid"
Eliot Berriot's avatar
Eliot Berriot committed
    filter_class = filters.LibraryFilter
    serializer_class = serializers.APILibrarySerializer
    ordering_fields = (
Eliot Berriot's avatar
Eliot Berriot committed
        "id",
        "creation_date",
        "fetched_date",
        "actor__domain",
        "tracks_count",
Eliot Berriot's avatar
Eliot Berriot committed
    @list_route(methods=["get"])
    def fetch(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
        account = request.GET.get("account")
Eliot Berriot's avatar
Eliot Berriot committed
            return response.Response({"account": "This field is mandatory"}, status=400)

        data = library.scan_from_account_name(account)
        return response.Response(data)
Eliot Berriot's avatar
Eliot Berriot committed
    @detail_route(methods=["post"])
    def scan(self, request, *args, **kwargs):
        library = self.get_object()
Eliot Berriot's avatar
Eliot Berriot committed
        serializer = serializers.APILibraryScanSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        result = tasks.scan_library.delay(
Eliot Berriot's avatar
Eliot Berriot committed
            library_id=library.pk, until=serializer.validated_data.get("until")
Eliot Berriot's avatar
Eliot Berriot committed
        return response.Response({"task": result.id})
Eliot Berriot's avatar
Eliot Berriot committed
    @list_route(methods=["get"])
    def following(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
        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)
        final_qs = filterset.qs
        serializer = serializers.APIFollowSerializer(final_qs, many=True)
Eliot Berriot's avatar
Eliot Berriot committed
        data = {"results": serializer.data, "count": len(final_qs)}
Eliot Berriot's avatar
Eliot Berriot committed
    @list_route(methods=["get", "patch"])
    def followers(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
        if request.method.lower() == "patch":
            serializer = serializers.APILibraryFollowUpdateSerializer(data=request.data)
            serializer.is_valid(raise_exception=True)
            follow = serializer.save()
Eliot Berriot's avatar
Eliot Berriot committed
            return response.Response(serializers.APIFollowSerializer(follow).data)
Eliot Berriot's avatar
Eliot Berriot committed
        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)
        final_qs = filterset.qs
        serializer = serializers.APIFollowSerializer(final_qs, many=True)
Eliot Berriot's avatar
Eliot Berriot committed
        data = {"results": serializer.data, "count": len(final_qs)}
    @transaction.atomic
    def create(self, request, *args, **kwargs):
        serializer = serializers.APILibraryCreateSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return response.Response(serializer.data, status=201)
Eliot Berriot's avatar
Eliot Berriot committed
class LibraryTrackViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
    permission_classes = (HasUserPermission,)
Eliot Berriot's avatar
Eliot Berriot committed
    required_permissions = ["federation"]
    queryset = (
        models.LibraryTrack.objects.all()
        .select_related("library__actor", "library__follow", "local_track_file")
        .prefetch_related("import_jobs")
    )
    filter_class = filters.LibraryTrackFilter
    serializer_class = serializers.APILibraryTrackSerializer
    ordering_fields = (
Eliot Berriot's avatar
Eliot Berriot committed
        "id",
        "artist_name",
        "title",
        "album_title",
        "creation_date",
        "modification_date",
        "fetched_date",
        "published_date",
Eliot Berriot's avatar
Eliot Berriot committed
    @list_route(methods=["post"])
    def action(self, request, *args, **kwargs):
Eliot Berriot's avatar
Eliot Berriot committed
        queryset = models.LibraryTrack.objects.filter(local_track_file__isnull=True)
        serializer = serializers.LibraryTrackActionSerializer(
Eliot Berriot's avatar
Eliot Berriot committed
            request.data, queryset=queryset, context={"submitted_by": request.user}
        )
        serializer.is_valid(raise_exception=True)
        result = serializer.save()
        return response.Response(result, status=200)