Skip to content
Snippets Groups Projects
views.py 10.5 KiB
Newer Older
from django import forms
from django.conf import settings
from django.core import paginator
from django.http import HttpResponse
from django.urls import reverse
Eliot Berriot's avatar
Eliot Berriot committed
from rest_framework import mixins
from rest_framework import permissions as rest_permissions
from rest_framework import response
from rest_framework import views
from rest_framework import viewsets
from rest_framework.decorators import list_route, detail_route
from rest_framework.serializers import ValidationError
from funkwhale_api.common import preferences
from funkwhale_api.common import utils as funkwhale_utils
from funkwhale_api.music.models import TrackFile
from funkwhale_api.users.permissions import HasUserPermission
from . import actors
from . import authentication
from . import library
from . import models
from . import permissions
from . import renderers
from . import serializers
from . import utils
from . import webfinger


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


class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet):
    lookup_field = 'actor'
    lookup_value_regex = '[a-z]*'
    authentication_classes = [
        authentication.SignatureAuthentication]
    permission_classes = []
    renderer_classes = [renderers.ActivityPubRenderer]
    def get_object(self):
        try:
            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)
    @detail_route(methods=['get', 'post'])
    def inbox(self, request, *args, **kwargs):
        system_actor = self.get_object()
        handler = getattr(system_actor, '{}_inbox'.format(
            request.method.lower()
        ))
        try:
            data = handler(request.data, actor=request.actor)
        except NotImplementedError:
            return response.Response(status=405)
        return response.Response({}, status=200)

    @detail_route(methods=['get', 'post'])
    def outbox(self, request, *args, **kwargs):
        system_actor = self.get_object()
        handler = getattr(system_actor, '{}_outbox'.format(
            request.method.lower()
        ))
        try:
            data = 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]
    @list_route(methods=['get'])
    def nodeinfo(self, request, *args, **kwargs):
        if not preferences.get('instance__nodeinfo_enabled'):
            return HttpResponse(status=404)
        data = {
            'links': [
                {
                    'rel': 'http://nodeinfo.diaspora.software/ns/schema/2.0',
                    'href': utils.full_url(
                        reverse('api:v1:instance:nodeinfo-2.0')
                    )
                }
            ]
        }
        return response.Response(data)

    @list_route(methods=['get'])
    def webfinger(self, request, *args, **kwargs):
        if not preferences.get('federation__enabled'):
            return HttpResponse(status=405)
        try:
            resource_type, resource = webfinger.clean_resource(
                request.GET['resource'])
            cleaner = getattr(webfinger, 'clean_{}'.format(resource_type))
            result = cleaner(resource)
        except forms.ValidationError as e:
            return response.Response({
                'errors': {
                    'resource': e.message
                }
            }, status=400)
        except KeyError:
            return response.Response({
                'errors': {
                    'resource': 'This field is required',
                }
            }, status=400)

        handler = getattr(self, 'handler_{}'.format(resource_type))
        data = handler(result)

        return response.Response(data)

    def handler_acct(self, clean_result):
        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').select_related(
            'track__artist',
            'track__album__artist'
        ).filter(library_track__isnull=True)
        if page is None:
            conf = {
                'id': utils.full_url(reverse('federation:music:files-list')),
                'page_size': preferences.get(
                    'federation__collection_page_size'),
                'item_serializer': serializers.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, preferences.get('federation__collection_page_size'))
            try:
                page = p.page(page_number)
                conf = {
                    '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(
        mixins.RetrieveModelMixin,
        mixins.UpdateModelMixin,
Eliot Berriot's avatar
Eliot Berriot committed
        mixins.ListModelMixin,
        viewsets.GenericViewSet):
    permission_classes = (HasUserPermission,)
    required_permissions = ['federation']
Eliot Berriot's avatar
Eliot Berriot committed
    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 = (
        'id',
        'creation_date',
        'fetched_date',
        'actor__domain',
        'tracks_count',

    @list_route(methods=['get'])
    def fetch(self, request, *args, **kwargs):
        account = request.GET.get('account')
        if not account:
            return response.Response(
                {'account': 'This field is mandatory'}, status=400)

        data = library.scan_from_account_name(account)
        return response.Response(data)
    @detail_route(methods=['post'])
    def scan(self, request, *args, **kwargs):
        library = self.get_object()
        serializer = serializers.APILibraryScanSerializer(
            data=request.data
        )
        serializer.is_valid(raise_exception=True)
        result = tasks.scan_library.delay(
            library_id=library.pk,
            until=serializer.validated_data.get('until')
        return response.Response({'task': result.id})
    @list_route(methods=['get'])
    def following(self, request, *args, **kwargs):
        library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
        queryset = models.Follow.objects.filter(
            actor=library_actor
        ).select_related(
            'target',
        ).order_by('-creation_date')
        filterset = filters.FollowFilter(request.GET, queryset=queryset)
        final_qs = filterset.qs
        serializer = serializers.APIFollowSerializer(final_qs, many=True)
        data = {
            'results': serializer.data,
            'count': len(final_qs),
    @list_route(methods=['get', 'patch'])
    def followers(self, request, *args, **kwargs):
        if request.method.lower() == 'patch':
            serializer = serializers.APILibraryFollowUpdateSerializer(
                data=request.data)
            serializer.is_valid(raise_exception=True)
            follow = serializer.save()
            return response.Response(
                serializers.APIFollowSerializer(follow).data
            )

        library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
        queryset = models.Follow.objects.filter(
            target=library_actor
        ).select_related(
            'target',
        ).order_by('-creation_date')
        filterset = filters.FollowFilter(request.GET, queryset=queryset)
        final_qs = filterset.qs
        serializer = serializers.APIFollowSerializer(final_qs, many=True)
        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)
        library = serializer.save()
        return response.Response(serializer.data, status=201)


class LibraryTrackViewSet(
        mixins.ListModelMixin,
        viewsets.GenericViewSet):
    permission_classes = (HasUserPermission,)
    required_permissions = ['federation']
    queryset = models.LibraryTrack.objects.all().select_related(
        'library__actor',
        'library__follow',
        'local_track_file',
    )
    filter_class = filters.LibraryTrackFilter
    serializer_class = serializers.APILibraryTrackSerializer
    ordering_fields = (
        'id',
        'artist_name',
        'title',
        'album_title',
        'creation_date',
        'modification_date',
        'fetched_date',
        'published_date',
    )