Skip to content
Snippets Groups Projects
views.py 9.87 KiB
Newer Older
from django import forms
from django.core import paginator
from django.http import HttpResponse, Http404
from django.urls import reverse
from rest_framework import exceptions, mixins, response, viewsets
Eliot Berriot's avatar
Eliot Berriot committed
from rest_framework.decorators import detail_route, list_route
from funkwhale_api.common import preferences
from funkwhale_api.music import models as music_models
Eliot Berriot's avatar
Eliot Berriot committed
from . import (
Eliot Berriot's avatar
Eliot Berriot committed
    actors,
    authentication,
    models,
    renderers,
    serializers,
    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)


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

    @list_route(methods=["post"])
    def inbox(self, request, *args, **kwargs):
        if request.method.lower() == "post" and request.actor is None:
            raise exceptions.AuthenticationFailed(
                "You need a valid signature to send an activity"
            )
        if request.method.lower() == "post":
            activity.receive(activity=request.data, on_behalf_of=request.actor)
        return response.Response({}, status=200)


class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    lookup_field = "preferred_username"
    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):
        if request.method.lower() == "post" and request.actor is None:
            raise exceptions.AuthenticationFailed(
                "You need a valid signature to send an activity"
            )
        if request.method.lower() == "post":
            activity.receive(activity=request.data, on_behalf_of=request.actor)
        return response.Response({}, status=200)

    @detail_route(methods=["get", "post"])
    def outbox(self, request, *args, **kwargs):
        return response.Response({}, status=200)

Eliot Berriot's avatar
Eliot Berriot committed
    @detail_route(methods=["get"])
    def followers(self, request, *args, **kwargs):
        self.get_object()
        # XXX to implement
        return response.Response({})

    @detail_route(methods=["get"])
    def following(self, request, *args, **kwargs):
        self.get_object()
        # XXX to implement
        return response.Response({})

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
def has_library_access(request, library):
    if library.privacy_level == "everyone":
        return True
    if request.user.is_authenticated and request.user.is_superuser:
        return True

    try:
        actor = request.actor
    except AttributeError:
        return False

    return library.received_follows.filter(actor=actor, approved=True).exists()


class MusicLibraryViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
Eliot Berriot's avatar
Eliot Berriot committed
    authentication_classes = [authentication.SignatureAuthentication]
    permission_classes = []
    renderer_classes = [renderers.ActivityPubRenderer]
    serializer_class = serializers.PaginatedCollectionSerializer
    queryset = music_models.Library.objects.all().select_related("actor")
    lookup_field = "uuid"
    def retrieve(self, request, *args, **kwargs):
        lb = self.get_object()

        conf = {
            "id": lb.get_federation_id(),
            "actor": lb.actor,
            "name": lb.name,
            "summary": lb.description,
Eliot Berriot's avatar
Eliot Berriot committed
            "items": lb.uploads.order_by("-creation_date"),
            "item_serializer": serializers.UploadSerializer,
Eliot Berriot's avatar
Eliot Berriot committed
        page = request.GET.get("page")
            serializer = serializers.LibrarySerializer(lb)
            data = serializer.data
        else:
            # if actor is requesting a specific page, we ensure library is public
            # or readable by the actor
            if not has_library_access(request, lb):
                raise exceptions.AuthenticationFailed(
                    "You do not have access to this library"
                )
            try:
                page_number = int(page)
Eliot Berriot's avatar
Eliot Berriot committed
                return response.Response({"page": ["Invalid page number"]}, status=400)
            conf["page_size"] = preferences.get("federation__collection_page_size")
            p = paginator.Paginator(conf["items"], conf["page_size"])
            try:
                page = p.page(page_number)
                conf["page"] = page
                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

    @detail_route(methods=["get"])
    def followers(self, request, *args, **kwargs):
        self.get_object()
        # XXX Implement this
        return response.Response({})


class MusicUploadViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    permission_classes = []
    renderer_classes = [renderers.ActivityPubRenderer]
    queryset = music_models.Upload.objects.none()
    lookup_field = "uuid"


class MusicArtistViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    permission_classes = []
    renderer_classes = [renderers.ActivityPubRenderer]
    queryset = music_models.Artist.objects.none()
    lookup_field = "uuid"


class MusicAlbumViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    permission_classes = []
    renderer_classes = [renderers.ActivityPubRenderer]
    queryset = music_models.Album.objects.none()
    lookup_field = "uuid"


class MusicTrackViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    permission_classes = []
    renderer_classes = [renderers.ActivityPubRenderer]
    queryset = music_models.Track.objects.none()
    lookup_field = "uuid"