Skip to content
Snippets Groups Projects
serializers.py 13.6 KiB
Newer Older
  • Learn to ignore specific revisions
  • import urllib.parse
    
    
    from django.urls import reverse
    from django.conf import settings
    
    from django.core.paginator import Paginator
    
    from rest_framework import serializers
    
    from dynamic_preferences.registries import global_preferences_registry
    
    
    from funkwhale_api.common.utils import set_query_parameter
    
    
    from . import activity
    
    from . import models
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    AP_CONTEXT = [
        'https://www.w3.org/ns/activitystreams',
        'https://w3id.org/security/v1',
        {},
    ]
    
    
    class ActorSerializer(serializers.ModelSerializer):
        # left maps to activitypub fields, right to our internal models
        id = serializers.URLField(source='url')
        outbox = serializers.URLField(source='outbox_url')
        inbox = serializers.URLField(source='inbox_url')
        following = serializers.URLField(source='following_url', required=False)
        followers = serializers.URLField(source='followers_url', required=False)
        preferredUsername = serializers.CharField(
            source='preferred_username', required=False)
        publicKey = serializers.JSONField(source='public_key', required=False)
        manuallyApprovesFollowers = serializers.NullBooleanField(
            source='manually_approves_followers', required=False)
    
        summary = serializers.CharField(max_length=None, required=False)
    
    
        class Meta:
            model = models.Actor
            fields = [
                'id',
                'type',
                'name',
                'summary',
                'preferredUsername',
                'publicKey',
                'inbox',
                'outbox',
                'following',
                'followers',
                'manuallyApprovesFollowers',
            ]
    
        def to_representation(self, instance):
            ret = super().to_representation(instance)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            ret['@context'] = AP_CONTEXT
    
            if instance.public_key:
                ret['publicKey'] = {
                    'owner': instance.url,
                    'publicKeyPem': instance.public_key,
                    'id': '{}#main-key'.format(instance.url)
                }
            ret['endpoints'] = {}
            if instance.shared_inbox_url:
                ret['endpoints']['sharedInbox'] = instance.shared_inbox_url
            return ret
    
        def prepare_missing_fields(self):
            kwargs = {}
            domain = urllib.parse.urlparse(self.validated_data['url']).netloc
            kwargs['domain'] = domain
            for endpoint, url in self.initial_data.get('endpoints', {}).items():
                if endpoint == 'sharedInbox':
                    kwargs['shared_inbox_url'] = url
                    break
            try:
                kwargs['public_key'] = self.initial_data['publicKey']['publicKeyPem']
            except KeyError:
                pass
            return kwargs
    
        def build(self):
            d = self.validated_data.copy()
            d.update(self.prepare_missing_fields())
            return self.Meta.model(**d)
    
        def save(self, **kwargs):
            kwargs.update(self.prepare_missing_fields())
            return super().save(**kwargs)
    
    
        def validate_summary(self, value):
            if value:
                return value[:500]
    
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    class FollowSerializer(serializers.ModelSerializer):
        # left maps to activitypub fields, right to our internal models
        id = serializers.URLField(source='get_federation_url')
        object = serializers.URLField(source='target.url')
        actor = serializers.URLField(source='actor.url')
        type = serializers.CharField(source='ap_type')
    
        class Meta:
            model = models.Actor
            fields = [
                'id',
                'object',
                'actor',
                'type'
            ]
    
        def to_representation(self, instance):
            ret = super().to_representation(instance)
            ret['@context'] = AP_CONTEXT
            return ret
    
    
    
    class ActorWebfingerSerializer(serializers.ModelSerializer):
        class Meta:
            model = models.Actor
            fields = ['url']
    
        def to_representation(self, instance):
            data = {}
            data['subject'] = 'acct:{}'.format(instance.webfinger_subject)
            data['links'] = [
                {
                    'rel': 'self',
                    'href': instance.url,
                    'type': 'application/activity+json'
                }
            ]
            data['aliases'] = [
                instance.url
            ]
            return data
    
    
    
    class ActivitySerializer(serializers.Serializer):
        actor = serializers.URLField()
        id = serializers.URLField()
        type = serializers.ChoiceField(
            choices=[(c, c) for c in activity.ACTIVITY_TYPES])
        object = serializers.JSONField()
    
        def validate_object(self, value):
            try:
                type = value['type']
            except KeyError:
                raise serializers.ValidationError('Missing object type')
    
            except TypeError:
                # probably a URL
                return value
    
            try:
                object_serializer = OBJECT_SERIALIZERS[type]
            except KeyError:
                raise serializers.ValidationError(
                    'Unsupported type {}'.format(type))
    
            serializer = object_serializer(data=value)
            serializer.is_valid(raise_exception=True)
            return serializer.data
    
        def validate_actor(self, value):
            request_actor = self.context.get('actor')
            if request_actor and request_actor.url != value:
                raise serializers.ValidationError(
                    'The actor making the request do not match'
                    ' the activity actor'
                )
            return value
    
    
    class ObjectSerializer(serializers.Serializer):
        id = serializers.URLField()
        url = serializers.URLField(required=False, allow_null=True)
        type = serializers.ChoiceField(
            choices=[(c, c) for c in activity.OBJECT_TYPES])
        content = serializers.CharField(
            required=False, allow_null=True)
        summary = serializers.CharField(
            required=False, allow_null=True)
        name = serializers.CharField(
            required=False, allow_null=True)
        published = serializers.DateTimeField(
            required=False, allow_null=True)
        updated = serializers.DateTimeField(
            required=False, allow_null=True)
        to = serializers.ListField(
            child=serializers.URLField(),
            required=False, allow_null=True)
        cc = serializers.ListField(
            child=serializers.URLField(),
            required=False, allow_null=True)
        bto = serializers.ListField(
            child=serializers.URLField(),
            required=False, allow_null=True)
        bcc = serializers.ListField(
            child=serializers.URLField(),
            required=False, allow_null=True)
    
    OBJECT_SERIALIZERS = {
        t: ObjectSerializer
        for t in activity.OBJECT_TYPES
    }
    
    
    
    class PaginatedCollectionSerializer(serializers.Serializer):
    
        type = serializers.ChoiceField(choices=['Collection'])
        totalItems = serializers.IntegerField(min_value=0)
        items = serializers.ListField()
        actor = serializers.URLField()
        id = serializers.URLField()
    
    
        def to_representation(self, conf):
            paginator = Paginator(
                conf['items'],
                conf.get('page_size', 20)
            )
            first = set_query_parameter(conf['id'], page=1)
            current = first
            last = set_query_parameter(conf['id'], page=paginator.num_pages)
            d = {
                'id': conf['id'],
                'actor': conf['actor'].url,
                'totalItems': paginator.count,
                'type': 'Collection',
                'current': current,
                'first': first,
                'last': last,
            }
            if self.context.get('include_ap_context', True):
                d['@context'] = AP_CONTEXT
            return d
    
    
    class CollectionPageSerializer(serializers.Serializer):
    
        type = serializers.ChoiceField(choices=['CollectionPage'])
        totalItems = serializers.IntegerField(min_value=0)
        items = serializers.ListField()
        actor = serializers.URLField()
        id = serializers.URLField()
        prev = serializers.URLField(required=False)
        next = serializers.URLField(required=False)
        partOf = serializers.URLField()
    
    
        def to_representation(self, conf):
            page = conf['page']
            first = set_query_parameter(conf['id'], page=1)
            last = set_query_parameter(conf['id'], page=page.paginator.num_pages)
            id = set_query_parameter(conf['id'], page=page.number)
            d = {
                'id': id,
                'partOf': conf['id'],
                'actor': conf['actor'].url,
                'totalItems': page.paginator.count,
                'type': 'CollectionPage',
                'first': first,
                'last': last,
                'items': [
                    conf['item_serializer'](
                        i,
                        context={
                            'actor': conf['actor'],
                            'include_ap_context': False}
                    ).data
                    for i in page.object_list
                ]
            }
    
            if page.has_previous():
                d['prev'] = set_query_parameter(
                    conf['id'], page=page.previous_page_number())
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            if page.has_next():
    
                d['next'] = set_query_parameter(
                    conf['id'], page=page.next_page_number())
    
            if self.context.get('include_ap_context', True):
                d['@context'] = AP_CONTEXT
            return d
    
    
    
    class ArtistMetadataSerializer(serializers.Serializer):
        musicbrainz_id = serializers.UUIDField(required=False)
        name = serializers.CharField()
    
    
    class ReleaseMetadataSerializer(serializers.Serializer):
        musicbrainz_id = serializers.UUIDField(required=False)
        title = serializers.CharField()
    
    
    class RecordingMetadataSerializer(serializers.Serializer):
        musicbrainz_id = serializers.UUIDField(required=False)
        title = serializers.CharField()
    
    
    class AudioMetadataSerializer(serializers.Serializer):
        artist = ArtistMetadataSerializer()
        release = ReleaseMetadataSerializer()
        recording = RecordingMetadataSerializer()
    
    
    class AudioSerializer(serializers.Serializer):
        type = serializers.CharField()
        id = serializers.URLField()
        url = serializers.JSONField()
        published = serializers.DateTimeField()
        updated = serializers.DateTimeField(required=False)
        metadata = AudioMetadataSerializer()
    
        def validate_type(self, v):
            if v != 'Audio':
                raise serializers.ValidationError('Invalid type for audio')
            return v
    
        def validate_url(self, v):
            try:
                url = v['href']
            except (KeyError, TypeError):
                raise serializers.ValidationError('Missing href')
    
            try:
                media_type = v['mediaType']
            except (KeyError, TypeError):
                raise serializers.ValidationError('Missing mediaType')
    
            if not media_type.startswith('audio/'):
                raise serializers.ValidationError('Invalid mediaType')
    
            return url
    
        def validate_url(self, v):
            try:
                url = v['href']
            except (KeyError, TypeError):
                raise serializers.ValidationError('Missing href')
    
            try:
                media_type = v['mediaType']
            except (KeyError, TypeError):
                raise serializers.ValidationError('Missing mediaType')
    
            if not media_type.startswith('audio/'):
                raise serializers.ValidationError('Invalid mediaType')
    
            return v
    
        def create(self, validated_data):
            defaults = {
                'audio_mimetype': validated_data['url']['mediaType'],
                'audio_url': validated_data['url']['href'],
                'metadata': validated_data['metadata'],
                'artist_name': validated_data['metadata']['artist']['name'],
                'album_title': validated_data['metadata']['release']['title'],
                'title': validated_data['metadata']['recording']['title'],
                'published_date': validated_data['published'],
                'modification_date': validated_data.get('updated'),
            }
            return models.LibraryTrack.objects.get_or_create(
                library=self.context['library'],
                url=validated_data['id'],
                defaults=defaults
            )[0]
    
        def to_representation(self, instance):
            track = instance.track
            album = instance.track.album
            artist = instance.track.artist
    
            d = {
                'type': 'Audio',
                'id': instance.get_federation_url(),
                'name': instance.track.full_name,
                'published': instance.creation_date.isoformat(),
                'updated': instance.modification_date.isoformat(),
                'metadata': {
                    'artist': {
                        'musicbrainz_id': str(artist.mbid) if artist.mbid else None,
                        'name': artist.name,
                    },
                    'release': {
                        'musicbrainz_id': str(album.mbid) if album.mbid else None,
                        'title': album.title,
                    },
                    'recording': {
                        'musicbrainz_id': str(track.mbid) if track.mbid else None,
                        'title': track.title,
                    },
                },
                'url': {
                    'href': utils.full_url(instance.path),
                    'type': 'Link',
                    'mediaType': instance.mimetype
                },
                'attributedTo': [
                    self.context['actor'].url
                ]
            }
            if self.context.get('include_ap_context', True):
                d['@context'] = AP_CONTEXT
            return d
    
    
    class CollectionSerializer(serializers.Serializer):
    
        def to_representation(self, conf):
            d = {
                'id': conf['id'],
                'actor': conf['actor'].url,
                'totalItems': len(conf['items']),
                'type': 'Collection',
                'items': [
                    conf['item_serializer'](
                        i,
                        context={
                            'actor': conf['actor'],
                            'include_ap_context': False}
                    ).data
                    for i in conf['items']
                ]
            }
    
            if self.context.get('include_ap_context', True):
                d['@context'] = AP_CONTEXT
            return d