Skip to content
Snippets Groups Projects
views.py 9.23 KiB
Newer Older
from django import forms
from django.core import paginator
Eliot Berriot's avatar
Eliot Berriot committed
from django.http import HttpResponse
from django.urls import reverse
from rest_framework import exceptions, mixins, response, viewsets
from rest_framework.decorators import action
from funkwhale_api.common import preferences
from funkwhale_api.music import models as music_models
from funkwhale_api.music import utils as music_utils
Eliot Berriot's avatar
Eliot Berriot committed
from . import activity, authentication, models, renderers, serializers, utils, webfinger


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]

    @action(methods=["post"], detail=False)
Eliot Berriot's avatar
Eliot Berriot committed
    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

    @action(methods=["get", "post"], detail=True)
    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)

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

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

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

class EditViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    lookup_field = "uuid"
    authentication_classes = [authentication.SignatureAuthentication]
    permission_classes = []
    renderer_classes = [renderers.ActivityPubRenderer]
    # queryset = common_models.Mutation.objects.local().select_related()
    # serializer_class = serializers.ActorSerializer


class WellKnownViewSet(viewsets.GenericViewSet):
    authentication_classes = []
    permission_classes = []
    renderer_classes = [renderers.JSONRenderer, renderers.WebfingerRenderer]
    @action(methods=["get"], detail=False)
    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)

    @action(methods=["get"], detail=False)
    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
Eliot Berriot's avatar
Eliot Berriot committed
        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]
Eliot Berriot's avatar
Eliot Berriot committed
    serializer_class = serializers.LibrarySerializer
    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.for_federation().order_by("-creation_date"),
Eliot Berriot's avatar
Eliot Berriot committed
            "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

    @action(methods=["get"], detail=True)
Eliot Berriot's avatar
Eliot Berriot committed
    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.local().select_related(
        "library__actor", "track__artist", "track__album__artist"
    )
    serializer_class = serializers.UploadSerializer
Eliot Berriot's avatar
Eliot Berriot committed
    lookup_field = "uuid"

    def get_queryset(self):
        queryset = super().get_queryset()
        actor = music_utils.get_actor_from_request(self.request)
        return queryset.playable_by(actor)

Eliot Berriot's avatar
Eliot Berriot committed

class MusicArtistViewSet(
    FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    authentication_classes = [authentication.SignatureAuthentication]
    permission_classes = []
    renderer_classes = [renderers.ActivityPubRenderer]
    queryset = music_models.Artist.objects.local()
    serializer_class = serializers.ArtistSerializer
Eliot Berriot's avatar
Eliot Berriot committed
    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.local().select_related("artist")
    serializer_class = serializers.AlbumSerializer
Eliot Berriot's avatar
Eliot Berriot committed
    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.local().select_related(
        "album__artist", "artist"
    )
    serializer_class = serializers.TrackSerializer
Eliot Berriot's avatar
Eliot Berriot committed
    lookup_field = "uuid"