Skip to content
Snippets Groups Projects
serializers.py 30.6 KiB
Newer Older
import mimetypes
import urllib.parse

from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
Eliot Berriot's avatar
Eliot Berriot committed
from django.db.models import F, Q
from rest_framework import serializers
Eliot Berriot's avatar
Eliot Berriot committed
from funkwhale_api.common import utils as funkwhale_utils
from funkwhale_api.music import models as music_models
from . import activity, models, utils
Eliot Berriot's avatar
Eliot Berriot committed
AP_CONTEXT = [
Eliot Berriot's avatar
Eliot Berriot committed
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
logger = logging.getLogger(__name__)

class ActorSerializer(serializers.Serializer):
    id = serializers.URLField(max_length=500)
    outbox = serializers.URLField(max_length=500)
    inbox = serializers.URLField(max_length=500)
    type = serializers.ChoiceField(choices=models.TYPE_CHOICES)
    preferredUsername = serializers.CharField()
    manuallyApprovesFollowers = serializers.NullBooleanField(required=False)
    name = serializers.CharField(required=False, max_length=200)
    summary = serializers.CharField(max_length=None, required=False)
Eliot Berriot's avatar
Eliot Berriot committed
    followers = serializers.URLField(max_length=500)
    following = serializers.URLField(max_length=500, required=False, allow_null=True)
    publicKey = serializers.JSONField(required=False)
    def to_representation(self, instance):
            "id": instance.fid,
Eliot Berriot's avatar
Eliot Berriot committed
            "outbox": instance.outbox_url,
            "inbox": instance.inbox_url,
            "preferredUsername": instance.preferred_username,
            "type": instance.type,
Eliot Berriot's avatar
Eliot Berriot committed
            ret["name"] = instance.name
        if instance.followers_url:
Eliot Berriot's avatar
Eliot Berriot committed
            ret["followers"] = instance.followers_url
        if instance.following_url:
Eliot Berriot's avatar
Eliot Berriot committed
            ret["following"] = instance.following_url
        if instance.summary:
Eliot Berriot's avatar
Eliot Berriot committed
            ret["summary"] = instance.summary
        if instance.manually_approves_followers is not None:
Eliot Berriot's avatar
Eliot Berriot committed
            ret["manuallyApprovesFollowers"] = instance.manually_approves_followers
Eliot Berriot's avatar
Eliot Berriot committed
        ret["@context"] = AP_CONTEXT
        if instance.public_key:
Eliot Berriot's avatar
Eliot Berriot committed
            ret["publicKey"] = {
                "owner": instance.fid,
Eliot Berriot's avatar
Eliot Berriot committed
                "publicKeyPem": instance.public_key,
                "id": "{}#main-key".format(instance.fid),
Eliot Berriot's avatar
Eliot Berriot committed
        ret["endpoints"] = {}
        if instance.shared_inbox_url:
Eliot Berriot's avatar
Eliot Berriot committed
            ret["endpoints"]["sharedInbox"] = instance.shared_inbox_url
        try:
            if instance.user.avatar:
                ret["icon"] = {
                    "type": "Image",
                    "mediaType": mimetypes.guess_type(instance.user.avatar.path)[0],
                    "url": utils.full_url(instance.user.avatar.crop["400x400"].url),
                }
        except ObjectDoesNotExist:
            pass
        return ret

    def prepare_missing_fields(self):
            "fid": self.validated_data["id"],
Eliot Berriot's avatar
Eliot Berriot committed
            "outbox_url": self.validated_data["outbox"],
            "inbox_url": self.validated_data["inbox"],
            "following_url": self.validated_data.get("following"),
            "followers_url": self.validated_data.get("followers"),
            "summary": self.validated_data.get("summary"),
            "type": self.validated_data["type"],
            "name": self.validated_data.get("name"),
            "preferred_username": self.validated_data["preferredUsername"],
Eliot Berriot's avatar
Eliot Berriot committed
        maf = self.validated_data.get("manuallyApprovesFollowers")
        if maf is not None:
Eliot Berriot's avatar
Eliot Berriot committed
            kwargs["manually_approves_followers"] = maf
        domain = urllib.parse.urlparse(kwargs["fid"]).netloc
Eliot Berriot's avatar
Eliot Berriot committed
        kwargs["domain"] = domain
        for endpoint, url in self.initial_data.get("endpoints", {}).items():
            if endpoint == "sharedInbox":
                kwargs["shared_inbox_url"] = url
Eliot Berriot's avatar
Eliot Berriot committed
            kwargs["public_key"] = self.initial_data["publicKey"]["publicKeyPem"]
        except KeyError:
            pass
        return kwargs

    def build(self):
        d = self.prepare_missing_fields()
        return models.Actor(**d)

    def save(self, **kwargs):
        d = self.prepare_missing_fields()
        d.update(kwargs)
        return models.Actor.objects.update_or_create(fid=d["fid"], defaults=d)[0]
    def validate_summary(self, value):
        if value:
            return value[:500]


class APIActorSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Actor
        fields = [
Eliot Berriot's avatar
Eliot Berriot committed
            "id",
Eliot Berriot's avatar
Eliot Berriot committed
            "url",
            "creation_date",
            "summary",
            "preferred_username",
            "name",
            "last_fetch_date",
            "domain",
            "type",
            "manually_approves_followers",
class BaseActivitySerializer(serializers.Serializer):
    id = serializers.URLField(max_length=500, required=False)
    type = serializers.CharField(max_length=100)
    actor = serializers.URLField(max_length=500)
Eliot Berriot's avatar
Eliot Berriot committed

    def validate_actor(self, v):
        expected = self.context.get("actor")
        if expected and expected.fid != v:
            raise serializers.ValidationError("Invalid actor")
        if expected:
            # avoid a DB lookup
            return expected
            return models.Actor.objects.get(fid=v)
        except models.Actor.DoesNotExist:
            raise serializers.ValidationError("Actor not found")
    def create(self, validated_data):
        return models.Activity.objects.create(
            fid=validated_data.get("id"),
            actor=validated_data["actor"],
            payload=self.initial_data,
            type=validated_data["type"],
    def validate(self, data):
        data["recipients"] = self.validate_recipients(self.initial_data)
        return super().validate(data)
    def validate_recipients(self, payload):
        """
        Ensure we have at least a to/cc field with valid actors
        """
        to = payload.get("to", [])
        cc = payload.get("cc", [])
        if not to and not cc:
            raise serializers.ValidationError(
                "We cannot handle an activity with no recipient"
class FollowSerializer(serializers.Serializer):
    id = serializers.URLField(max_length=500)
    object = serializers.URLField(max_length=500)
    actor = serializers.URLField(max_length=500)
Eliot Berriot's avatar
Eliot Berriot committed
    type = serializers.ChoiceField(choices=["Follow"])
Eliot Berriot's avatar
Eliot Berriot committed

Eliot Berriot's avatar
Eliot Berriot committed
        expected = self.context.get("follow_target")
        if self.parent:
            # it's probably an accept, so everything is inverted, the actor
            # the recipient does not matter
            recipient = None
        else:
            recipient = self.context.get("recipient")
        if expected and expected.fid != v:
Eliot Berriot's avatar
Eliot Berriot committed
            raise serializers.ValidationError("Invalid target")
            obj = models.Actor.objects.get(fid=v)
            if recipient and recipient.fid != obj.fid:
                raise serializers.ValidationError("Invalid target")
            return obj
            pass
        try:
            qs = music_models.Library.objects.filter(fid=v)
            if recipient:
                qs = qs.filter(actor=recipient)
            return qs.get()
        except music_models.Library.DoesNotExist:
            pass

        raise serializers.ValidationError("Target not found")
Eliot Berriot's avatar
Eliot Berriot committed
        expected = self.context.get("follow_actor")
        if expected and expected.fid != v:
Eliot Berriot's avatar
Eliot Berriot committed
            raise serializers.ValidationError("Invalid actor")
            return models.Actor.objects.get(fid=v)
Eliot Berriot's avatar
Eliot Berriot committed
            raise serializers.ValidationError("Actor not found")
        target = self.validated_data["object"]

        if target._meta.label == "music.Library":
            follow_class = models.LibraryFollow
        else:
            follow_class = models.Follow
        defaults = kwargs
        defaults["fid"] = self.validated_data["id"]
        return follow_class.objects.update_or_create(
Eliot Berriot's avatar
Eliot Berriot committed
            actor=self.validated_data["actor"],
            target=self.validated_data["object"],
Eliot Berriot's avatar
Eliot Berriot committed

    def to_representation(self, instance):
Eliot Berriot's avatar
Eliot Berriot committed
            "@context": AP_CONTEXT,
            "actor": instance.actor.fid,
            "id": instance.get_federation_id(),
            "object": instance.target.fid,
Eliot Berriot's avatar
Eliot Berriot committed
            "type": "Follow",
class APIFollowSerializer(serializers.ModelSerializer):
    actor = APIActorSerializer()
    target = APIActorSerializer()

    class Meta:
        model = models.Follow
        fields = [
Eliot Berriot's avatar
Eliot Berriot committed
            "uuid",
            "id",
            "approved",
            "creation_date",
            "modification_date",
            "actor",
            "target",
class AcceptFollowSerializer(serializers.Serializer):
    id = serializers.URLField(max_length=500, required=False)
    actor = serializers.URLField(max_length=500)
Eliot Berriot's avatar
Eliot Berriot committed
    type = serializers.ChoiceField(choices=["Accept"])
        expected = self.context.get("actor")
        if expected and expected.fid != v:
Eliot Berriot's avatar
Eliot Berriot committed
            raise serializers.ValidationError("Invalid actor")
            return models.Actor.objects.get(fid=v)
Eliot Berriot's avatar
Eliot Berriot committed
            raise serializers.ValidationError("Actor not found")
        # we ensure the accept actor actually match the follow target / library owner
        target = validated_data["object"]["object"]

        if target._meta.label == "music.Library":
            expected = target.actor
            follow_class = models.LibraryFollow
        else:
            expected = target
            follow_class = models.Follow
        if validated_data["actor"] != expected:
Eliot Berriot's avatar
Eliot Berriot committed
            raise serializers.ValidationError("Actor mismatch")
Eliot Berriot's avatar
Eliot Berriot committed
            validated_data["follow"] = (
                follow_class.objects.filter(
                    target=target, actor=validated_data["object"]["actor"]
Eliot Berriot's avatar
Eliot Berriot committed
                )
                .exclude(approved=True)
Eliot Berriot's avatar
Eliot Berriot committed
                .get()
            )
        except follow_class.DoesNotExist:
Eliot Berriot's avatar
Eliot Berriot committed
            raise serializers.ValidationError("No follow to accept")
        return validated_data

    def to_representation(self, instance):
        if instance.target._meta.label == "music.Library":
            actor = instance.target.actor
        else:
            actor = instance.target

            "id": instance.get_federation_id() + "/accept",
            "actor": actor.fid,
Eliot Berriot's avatar
Eliot Berriot committed
            "object": FollowSerializer(instance).data,
        follow = self.validated_data["follow"]
        follow.approved = True
        follow.save()
        if follow.target._meta.label == "music.Library":
            follow.target.schedule_scan()
        return follow


class UndoFollowSerializer(serializers.Serializer):
    id = serializers.URLField(max_length=500)
    actor = serializers.URLField(max_length=500)
Eliot Berriot's avatar
Eliot Berriot committed
    type = serializers.ChoiceField(choices=["Undo"])
Eliot Berriot's avatar
Eliot Berriot committed
        expected = self.context.get("follow_target")
        if expected and expected.fid != v:
Eliot Berriot's avatar
Eliot Berriot committed
            raise serializers.ValidationError("Invalid actor")
            return models.Actor.objects.get(fid=v)
Eliot Berriot's avatar
Eliot Berriot committed
            raise serializers.ValidationError("Actor not found")

    def validate(self, validated_data):
        # we ensure the accept actor actually match the follow actor
Eliot Berriot's avatar
Eliot Berriot committed
        if validated_data["actor"] != validated_data["object"]["actor"]:
            raise serializers.ValidationError("Actor mismatch")
Eliot Berriot's avatar
Eliot Berriot committed
            validated_data["follow"] = models.Follow.objects.filter(
                actor=validated_data["actor"], target=validated_data["object"]["object"]
Eliot Berriot's avatar
Eliot Berriot committed
            raise serializers.ValidationError("No follow to remove")
        return validated_data

    def to_representation(self, instance):
        return {
            "@context": AP_CONTEXT,
            "id": instance.get_federation_id() + "/undo",
            "actor": instance.actor.fid,
Eliot Berriot's avatar
Eliot Berriot committed
            "object": FollowSerializer(instance).data,
Eliot Berriot's avatar
Eliot Berriot committed
        return self.validated_data["follow"].delete()
class ActorWebfingerSerializer(serializers.Serializer):
    subject = serializers.CharField()
    aliases = serializers.ListField(child=serializers.URLField(max_length=500))
    links = serializers.ListField()
    actor_url = serializers.URLField(max_length=500, required=False)

    def validate(self, validated_data):
Eliot Berriot's avatar
Eliot Berriot committed
        validated_data["actor_url"] = None
        for l in validated_data["links"]:
Eliot Berriot's avatar
Eliot Berriot committed
                if not l["rel"] == "self":
                    continue
Eliot Berriot's avatar
Eliot Berriot committed
                if not l["type"] == "application/activity+json":
                    continue
Eliot Berriot's avatar
Eliot Berriot committed
                validated_data["actor_url"] = l["href"]
                break
            except KeyError:
                pass
Eliot Berriot's avatar
Eliot Berriot committed
        if validated_data["actor_url"] is None:
            raise serializers.ValidationError("No valid actor url found")
        return validated_data

    def to_representation(self, instance):
        data = {}
Eliot Berriot's avatar
Eliot Berriot committed
        data["subject"] = "acct:{}".format(instance.webfinger_subject)
        data["links"] = [
            {"rel": "self", "href": instance.fid, "type": "application/activity+json"}
        data["aliases"] = [instance.fid]
        return data


class ActivitySerializer(serializers.Serializer):
    actor = serializers.URLField(max_length=500)
    id = serializers.URLField(max_length=500, required=False)
Eliot Berriot's avatar
Eliot Berriot committed
    type = serializers.ChoiceField(choices=[(c, c) for c in activity.ACTIVITY_TYPES])
Eliot Berriot's avatar
Eliot Berriot committed
    object = serializers.JSONField(required=False)
    target = serializers.JSONField(required=False)

    def validate_object(self, value):
        try:
Eliot Berriot's avatar
Eliot Berriot committed
            type = value["type"]
Eliot Berriot's avatar
Eliot Berriot committed
            raise serializers.ValidationError("Missing object type")
        except TypeError:
            # probably a URL
            return value
        try:
            object_serializer = OBJECT_SERIALIZERS[type]
        except KeyError:
Eliot Berriot's avatar
Eliot Berriot committed
            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):
Eliot Berriot's avatar
Eliot Berriot committed
        request_actor = self.context.get("actor")
        if request_actor and request_actor.fid != value:
            raise serializers.ValidationError(
Eliot Berriot's avatar
Eliot Berriot committed
                "The actor making the request do not match" " the activity actor"
    def to_representation(self, conf):
        d = {}
        d.update(conf)

Eliot Berriot's avatar
Eliot Berriot committed
        if self.context.get("include_ap_context", True):
            d["@context"] = AP_CONTEXT

class ObjectSerializer(serializers.Serializer):
    id = serializers.URLField(max_length=500)
    url = serializers.URLField(max_length=500, required=False, allow_null=True)
Eliot Berriot's avatar
Eliot Berriot committed
    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(
Eliot Berriot's avatar
Eliot Berriot committed
        child=serializers.URLField(max_length=500), required=False, allow_null=True
    )
    cc = serializers.ListField(
Eliot Berriot's avatar
Eliot Berriot committed
        child=serializers.URLField(max_length=500), required=False, allow_null=True
    )
    bto = serializers.ListField(
Eliot Berriot's avatar
Eliot Berriot committed
        child=serializers.URLField(max_length=500), required=False, allow_null=True
    )
    bcc = serializers.ListField(
Eliot Berriot's avatar
Eliot Berriot committed
        child=serializers.URLField(max_length=500), required=False, allow_null=True
    )

Eliot Berriot's avatar
Eliot Berriot committed
OBJECT_SERIALIZERS = {t: ObjectSerializer for t in activity.OBJECT_TYPES}
def get_additional_fields(data):
    UNSET = object()
    additional_fields = {}
    for field in ["name", "summary"]:
        v = data.get(field, UNSET)
        if v == UNSET:
            continue
        additional_fields[field] = v

    return additional_fields


class PaginatedCollectionSerializer(serializers.Serializer):
Eliot Berriot's avatar
Eliot Berriot committed
    type = serializers.ChoiceField(choices=["Collection"])
    totalItems = serializers.IntegerField(min_value=0)
    actor = serializers.URLField(max_length=500)
    id = serializers.URLField(max_length=500)
    first = serializers.URLField(max_length=500)
    last = serializers.URLField(max_length=500)

    def to_representation(self, conf):
Eliot Berriot's avatar
Eliot Berriot committed
        paginator = Paginator(conf["items"], conf.get("page_size", 20))
        first = funkwhale_utils.set_query_parameter(conf["id"], page=1)
        current = first
Eliot Berriot's avatar
Eliot Berriot committed
        last = funkwhale_utils.set_query_parameter(conf["id"], page=paginator.num_pages)
Eliot Berriot's avatar
Eliot Berriot committed
            "id": conf["id"],
            "actor": conf["actor"].fid,
Eliot Berriot's avatar
Eliot Berriot committed
            "totalItems": paginator.count,
            "type": conf.get("type", "Collection"),
Eliot Berriot's avatar
Eliot Berriot committed
            "current": current,
            "first": first,
            "last": last,
        d.update(get_additional_fields(conf))
Eliot Berriot's avatar
Eliot Berriot committed
        if self.context.get("include_ap_context", True):
            d["@context"] = AP_CONTEXT
class LibrarySerializer(PaginatedCollectionSerializer):
    type = serializers.ChoiceField(choices=["Library"])
    name = serializers.CharField()
    summary = serializers.CharField(allow_blank=True, allow_null=True, required=False)
Eliot Berriot's avatar
Eliot Berriot committed
    followers = serializers.URLField(max_length=500)
    audience = serializers.ChoiceField(
        choices=["", None, "https://www.w3.org/ns/activitystreams#Public"],
        required=False,
        allow_null=True,
        allow_blank=True,
    )

    def to_representation(self, library):
        conf = {
            "id": library.fid,
            "name": library.name,
            "summary": library.description,
            "page_size": 100,
            "actor": library.actor,
Eliot Berriot's avatar
Eliot Berriot committed
            "items": library.uploads.filter(import_status="finished"),
            "type": "Library",
        }
        r = super().to_representation(conf)
        r["audience"] = (
            "https://www.w3.org/ns/activitystreams#Public"
            if library.privacy_level == "public"
            else ""
        )
Eliot Berriot's avatar
Eliot Berriot committed
        r["followers"] = library.followers_url
        return r

    def create(self, validated_data):
        actor = utils.retrieve(
            validated_data["actor"],
            queryset=models.Actor,
            serializer_class=ActorSerializer,
        )
        library, created = music_models.Library.objects.update_or_create(
            fid=validated_data["id"],
            actor=actor,
            defaults={
Eliot Berriot's avatar
Eliot Berriot committed
                "uploads_count": validated_data["totalItems"],
                "name": validated_data["name"],
                "description": validated_data["summary"],
Eliot Berriot's avatar
Eliot Berriot committed
                "followers_url": validated_data["followers"],
                "privacy_level": "everyone"
                if validated_data["audience"]
                == "https://www.w3.org/ns/activitystreams#Public"
                else "me",
            },
        )
        return library


class CollectionPageSerializer(serializers.Serializer):
Eliot Berriot's avatar
Eliot Berriot committed
    type = serializers.ChoiceField(choices=["CollectionPage"])
    totalItems = serializers.IntegerField(min_value=0)
    items = serializers.ListField()
    actor = serializers.URLField(max_length=500)
    id = serializers.URLField(max_length=500)
    first = serializers.URLField(max_length=500)
    last = serializers.URLField(max_length=500)
    next = serializers.URLField(max_length=500, required=False)
    prev = serializers.URLField(max_length=500, required=False)
    partOf = serializers.URLField(max_length=500)
    def validate_items(self, v):
Eliot Berriot's avatar
Eliot Berriot committed
        item_serializer = self.context.get("item_serializer")
        if not item_serializer:
            return v
        raw_items = [item_serializer(data=i, context=self.context) for i in v]
        for i in raw_items:
            if i.is_valid():
                valid_items.append(i)
Eliot Berriot's avatar
Eliot Berriot committed
                logger.debug("Invalid item %s: %s", i.data, i.errors)
    def to_representation(self, conf):
Eliot Berriot's avatar
Eliot Berriot committed
        page = conf["page"]
        first = funkwhale_utils.set_query_parameter(conf["id"], page=1)
        last = funkwhale_utils.set_query_parameter(
Eliot Berriot's avatar
Eliot Berriot committed
            conf["id"], page=page.paginator.num_pages
        )
        id = funkwhale_utils.set_query_parameter(conf["id"], page=page.number)
Eliot Berriot's avatar
Eliot Berriot committed
            "id": id,
            "partOf": conf["id"],
            "actor": conf["actor"].fid,
Eliot Berriot's avatar
Eliot Berriot committed
            "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
Eliot Berriot's avatar
Eliot Berriot committed
            ],
        }

        if page.has_previous():
Eliot Berriot's avatar
Eliot Berriot committed
            d["prev"] = funkwhale_utils.set_query_parameter(
                conf["id"], page=page.previous_page_number()
            )
Eliot Berriot's avatar
Eliot Berriot committed
        if page.has_next():
Eliot Berriot's avatar
Eliot Berriot committed
            d["next"] = funkwhale_utils.set_query_parameter(
                conf["id"], page=page.next_page_number()
            )
        d.update(get_additional_fields(conf))
Eliot Berriot's avatar
Eliot Berriot committed
        if self.context.get("include_ap_context", True):
            d["@context"] = AP_CONTEXT
Eliot Berriot's avatar
Eliot Berriot committed
class MusicEntitySerializer(serializers.Serializer):
    id = serializers.URLField(max_length=500)
    published = serializers.DateTimeField()
    musicbrainzId = serializers.UUIDField(allow_null=True, required=False)
    name = serializers.CharField(max_length=1000)

    def create(self, validated_data):
        mbid = validated_data.get("musicbrainzId")
        candidates = self.model.objects.filter(
            Q(mbid=mbid) | Q(fid=validated_data["id"])
        ).order_by(F("fid").desc(nulls_last=True))

        existing = candidates.first()
        if existing:
            return existing

        # nothing matching in our database, let's create a new object
        return self.model.objects.create(**self.get_create_data(validated_data))

    def get_create_data(self, validated_data):
        return {
            "mbid": validated_data.get("musicbrainzId"),
            "fid": validated_data["id"],
            "name": validated_data["name"],
            "creation_date": validated_data["published"],
            "from_activity": self.context.get("activity"),
        }


class ArtistSerializer(MusicEntitySerializer):
    model = music_models.Artist

    def to_representation(self, instance):
        d = {
            "type": "Artist",
            "id": instance.fid,
            "name": instance.name,
            "published": instance.creation_date.isoformat(),
            "musicbrainzId": str(instance.mbid) if instance.mbid else None,
        }

        if self.context.get("include_ap_context", self.parent is None):
            d["@context"] = AP_CONTEXT
        return d


class AlbumSerializer(MusicEntitySerializer):
    model = music_models.Album
    released = serializers.DateField(allow_null=True, required=False)
    artists = serializers.ListField(child=ArtistSerializer(), min_length=1)

    def to_representation(self, instance):
        d = {
            "type": "Album",
            "id": instance.fid,
            "name": instance.title,
            "published": instance.creation_date.isoformat(),
            "musicbrainzId": str(instance.mbid) if instance.mbid else None,
            "released": instance.release_date.isoformat()
            if instance.release_date
            else None,
            "artists": [
                ArtistSerializer(
                    instance.artist, context={"include_ap_context": False}
                ).data
            ],
        }
        if instance.cover:
            d["cover"] = {"type": "Image", "url": utils.full_url(instance.cover.url)}
        if self.context.get("include_ap_context", self.parent is None):
            d["@context"] = AP_CONTEXT
        return d
Eliot Berriot's avatar
Eliot Berriot committed
    def get_create_data(self, validated_data):
        artist_data = validated_data["artists"][0]
        artist = ArtistSerializer(
            context={"activity": self.context.get("activity")}
        ).create(artist_data)

        return {
            "mbid": validated_data.get("musicbrainzId"),
            "fid": validated_data["id"],
            "title": validated_data["name"],
            "creation_date": validated_data["published"],
            "artist": artist,
            "release_date": validated_data.get("released"),
            "from_activity": self.context.get("activity"),
        }
Eliot Berriot's avatar
Eliot Berriot committed
class TrackSerializer(MusicEntitySerializer):
    model = music_models.Track
    position = serializers.IntegerField(min_value=0, allow_null=True, required=False)
    artists = serializers.ListField(child=ArtistSerializer(), min_length=1)
    album = AlbumSerializer()

    def to_representation(self, instance):
        d = {
            "type": "Track",
            "id": instance.fid,
            "name": instance.title,
            "published": instance.creation_date.isoformat(),
            "musicbrainzId": str(instance.mbid) if instance.mbid else None,
            "position": instance.position,
            "artists": [
                ArtistSerializer(
                    instance.artist, context={"include_ap_context": False}
                ).data
            ],
            "album": AlbumSerializer(
                instance.album, context={"include_ap_context": False}
            ).data,
        }
Eliot Berriot's avatar
Eliot Berriot committed
        if self.context.get("include_ap_context", self.parent is None):
            d["@context"] = AP_CONTEXT
        return d
Eliot Berriot's avatar
Eliot Berriot committed
    def get_create_data(self, validated_data):
        artist_data = validated_data["artists"][0]
        artist = ArtistSerializer(
            context={"activity": self.context.get("activity")}
        ).create(artist_data)
        album = AlbumSerializer(
            context={"activity": self.context.get("activity")}
        ).create(validated_data["album"])
Eliot Berriot's avatar
Eliot Berriot committed
        return {
            "mbid": validated_data.get("musicbrainzId"),
            "fid": validated_data["id"],
            "title": validated_data["name"],
            "position": validated_data.get("position"),
            "creation_date": validated_data["published"],
            "artist": artist,
            "album": album,
            "from_activity": self.context.get("activity"),
        }
Eliot Berriot's avatar
Eliot Berriot committed
class UploadSerializer(serializers.Serializer):
    type = serializers.ChoiceField(choices=["Audio"])
    id = serializers.URLField(max_length=500)
    library = serializers.URLField(max_length=500)
    url = serializers.JSONField()
    published = serializers.DateTimeField()
Eliot Berriot's avatar
Eliot Berriot committed
    updated = serializers.DateTimeField(required=False, allow_null=True)
    bitrate = serializers.IntegerField(min_value=0)
    size = serializers.IntegerField(min_value=0)
    duration = serializers.IntegerField(min_value=0)
Eliot Berriot's avatar
Eliot Berriot committed
    track = TrackSerializer(required=True)
Eliot Berriot's avatar
Eliot Berriot committed
            raise serializers.ValidationError("Missing href")
Eliot Berriot's avatar
Eliot Berriot committed
            media_type = v["mediaType"]
Eliot Berriot's avatar
Eliot Berriot committed
            raise serializers.ValidationError("Missing mediaType")
Eliot Berriot's avatar
Eliot Berriot committed
        if not media_type or not media_type.startswith("audio/"):
            raise serializers.ValidationError("Invalid mediaType")
    def validate_library(self, v):
        lb = self.context.get("library")
        if lb:
            if lb.fid != v:
                raise serializers.ValidationError("Invalid library")
            return lb
Eliot Berriot's avatar
Eliot Berriot committed

        actor = self.context.get("actor")
        kwargs = {}
        if actor:
            kwargs["actor"] = actor
Eliot Berriot's avatar
Eliot Berriot committed
            return music_models.Library.objects.get(fid=v, **kwargs)
        except music_models.Library.DoesNotExist:
            raise serializers.ValidationError("Invalid library")

Eliot Berriot's avatar
Eliot Berriot committed
        try:
            return music_models.Upload.objects.get(fid=validated_data["id"])
        except music_models.Upload.DoesNotExist:
            pass

        track = TrackSerializer(
            context={"activity": self.context.get("activity")}
        ).create(validated_data["track"])

        data = {
            "fid": validated_data["id"],
            "mimetype": validated_data["url"]["mediaType"],
            "source": validated_data["url"]["href"],
            "creation_date": validated_data["published"],
Eliot Berriot's avatar
Eliot Berriot committed
            "modification_date": validated_data.get("updated"),
Eliot Berriot's avatar
Eliot Berriot committed
            "track": track,
            "duration": validated_data["duration"],
            "size": validated_data["size"],
            "bitrate": validated_data["bitrate"],
            "library": validated_data["library"],
            "from_activity": self.context.get("activity"),
            "import_status": "finished",
Eliot Berriot's avatar
Eliot Berriot committed
        return music_models.Upload.objects.create(**data)

    def to_representation(self, instance):
        track = instance.track
        d = {
Eliot Berriot's avatar
Eliot Berriot committed
            "type": "Audio",
            "id": instance.get_federation_id(),
Eliot Berriot's avatar
Eliot Berriot committed
            "library": instance.library.fid,
            "name": track.full_name,
Eliot Berriot's avatar
Eliot Berriot committed
            "published": instance.creation_date.isoformat(),
Eliot Berriot's avatar
Eliot Berriot committed
            "bitrate": instance.bitrate,
            "size": instance.size,
            "duration": instance.duration,
Eliot Berriot's avatar
Eliot Berriot committed
            "url": {
                "href": utils.full_url(instance.listen_url),
Eliot Berriot's avatar
Eliot Berriot committed
                "type": "Link",
                "mediaType": instance.mimetype,
Eliot Berriot's avatar
Eliot Berriot committed
            "track": TrackSerializer(track, context={"include_ap_context": False}).data,
        if instance.modification_date:
            d["updated"] = instance.modification_date.isoformat()

Eliot Berriot's avatar
Eliot Berriot committed
        if self.context.get("include_ap_context", self.parent is None):
Eliot Berriot's avatar
Eliot Berriot committed
            d["@context"] = AP_CONTEXT
        return d


class CollectionSerializer(serializers.Serializer):
    def to_representation(self, conf):
        d = {
Eliot Berriot's avatar
Eliot Berriot committed
            "id": conf["id"],
            "actor": conf["actor"].fid,
Eliot Berriot's avatar
Eliot Berriot committed
            "totalItems": len(conf["items"]),
            "type": "Collection",
            "items": [
                conf["item_serializer"](
                    i, context={"actor": conf["actor"], "include_ap_context": False}
Eliot Berriot's avatar
Eliot Berriot committed
                for i in conf["items"]
            ],
Eliot Berriot's avatar
Eliot Berriot committed
        if self.context.get("include_ap_context", True):
            d["@context"] = AP_CONTEXT