funkwhale/api/funkwhale_api/federation/serializers.py

2033 wiersze
72 KiB
Python
Czysty Zwykły widok Historia

import logging
import os
import urllib.parse
import uuid
2020-03-11 10:39:55 +00:00
from django.core.exceptions import ObjectDoesNotExist
2018-04-06 15:58:43 +00:00
from django.core.paginator import Paginator
from django.db import transaction
2020-03-25 14:32:10 +00:00
from django.urls import reverse
from django.utils import timezone
from rest_framework import serializers
2018-03-30 16:02:50 +00:00
from funkwhale_api.common import utils as common_utils
from funkwhale_api.common import models as common_models
2020-03-11 10:39:55 +00:00
from funkwhale_api.moderation import models as moderation_models
from funkwhale_api.moderation import serializers as moderation_serializers
from funkwhale_api.moderation import signals as moderation_signals
2019-04-11 08:17:10 +00:00
from funkwhale_api.music import licenses
from funkwhale_api.music import models as music_models
2019-04-11 08:17:10 +00:00
from funkwhale_api.music import tasks as music_tasks
from funkwhale_api.tags import models as tags_models
from . import activity, actors, contexts, jsonld, models, utils
logger = logging.getLogger(__name__)
2020-03-25 14:32:10 +00:00
def include_if_not_none(data, value, field):
if value is not None:
data[field] = value
class MultipleSerializer(serializers.Serializer):
"""
A serializer that will try multiple serializers in turn
"""
def __init__(self, *args, **kwargs):
self.allowed = kwargs.pop("allowed")
super().__init__(*args, **kwargs)
def to_internal_value(self, v):
last_exception = None
for serializer_class in self.allowed:
s = serializer_class(data=v)
try:
s.is_valid(raise_exception=True)
except serializers.ValidationError as e:
last_exception = e
else:
return s.validated_data
raise last_exception
2020-01-23 10:09:52 +00:00
class TruncatedCharField(serializers.CharField):
def __init__(self, *args, **kwargs):
self.truncate_length = kwargs.pop("truncate_length")
super().__init__(*args, **kwargs)
def to_internal_value(self, v):
v = super().to_internal_value(v)
if v:
v = v[: self.truncate_length]
return v
2020-03-25 14:32:10 +00:00
class TagSerializer(jsonld.JsonLdSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.Hashtag])
name = serializers.CharField(max_length=100)
class Meta:
jsonld_mapping = {"name": jsonld.first_val(contexts.AS.name)}
def validate_name(self, value):
if value.startswith("#"):
# remove trailing #
value = value[1:]
return value
def tag_list(tagged_items):
return [
repr_tag(item.tag.name)
for item in sorted(set(tagged_items.all()), key=lambda i: i.tag.name)
]
def is_mimetype(mt, allowed_mimetypes):
for allowed in allowed_mimetypes:
if allowed.endswith("/*"):
if mt.startswith(allowed.replace("*", "")):
return True
else:
if mt == allowed:
return True
return False
class MediaSerializer(jsonld.JsonLdSerializer):
2018-09-23 12:38:42 +00:00
mediaType = serializers.CharField()
def __init__(self, *args, **kwargs):
self.allowed_mimetypes = kwargs.pop("allowed_mimetypes", [])
2020-03-11 10:39:55 +00:00
self.allow_empty_mimetype = kwargs.pop("allow_empty_mimetype", False)
2018-09-23 12:38:42 +00:00
super().__init__(*args, **kwargs)
2020-03-11 10:39:55 +00:00
self.fields["mediaType"].required = not self.allow_empty_mimetype
self.fields["mediaType"].allow_null = self.allow_empty_mimetype
2018-09-23 12:38:42 +00:00
def validate_mediaType(self, v):
if not self.allowed_mimetypes:
# no restrictions
return v
2020-03-11 10:39:55 +00:00
if self.allow_empty_mimetype and not v:
return None
2020-03-25 14:32:10 +00:00
if not is_mimetype(v, self.allowed_mimetypes):
raise serializers.ValidationError(
"Invalid mimetype {}. Allowed: {}".format(v, self.allowed_mimetypes)
)
return v
2018-09-23 12:38:42 +00:00
class LinkSerializer(MediaSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.Link])
href = serializers.URLField(max_length=500)
2020-03-25 14:32:10 +00:00
bitrate = serializers.IntegerField(min_value=0, required=False)
size = serializers.IntegerField(min_value=0, required=False)
class Meta:
jsonld_mapping = {
"href": jsonld.first_id(contexts.AS.href),
"mediaType": jsonld.first_val(contexts.AS.mediaType),
2020-03-25 14:32:10 +00:00
"bitrate": jsonld.first_val(contexts.FW.bitrate),
"size": jsonld.first_val(contexts.FW.size),
}
2020-03-25 14:32:10 +00:00
class LinkListSerializer(serializers.ListField):
def __init__(self, *args, **kwargs):
kwargs.setdefault("child", LinkSerializer(jsonld_expand=False))
self.keep_mediatype = kwargs.pop("keep_mediatype", [])
super().__init__(*args, **kwargs)
def to_internal_value(self, v):
links = super().to_internal_value(v)
if not self.keep_mediatype:
# no further filtering required
return links
links = [
link
for link in links
if link.get("mediaType")
and is_mimetype(link["mediaType"], self.keep_mediatype)
]
if not self.allow_empty and len(links) == 0:
self.fail("empty")
return links
class ImageSerializer(MediaSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.Image, contexts.AS.Link])
href = serializers.URLField(max_length=500, required=False)
url = serializers.URLField(max_length=500, required=False)
class Meta:
jsonld_mapping = {
"url": jsonld.first_id(contexts.AS.url),
"href": jsonld.first_id(contexts.AS.href),
"mediaType": jsonld.first_val(contexts.AS.mediaType),
}
def validate(self, data):
validated_data = super().validate(data)
if "url" not in validated_data:
try:
validated_data["url"] = validated_data["href"]
except KeyError:
if self.required:
raise serializers.ValidationError(
"You need to provide a url or href"
)
return validated_data
class URLSerializer(jsonld.JsonLdSerializer):
href = serializers.URLField(max_length=500)
mediaType = serializers.CharField(required=False)
class Meta:
jsonld_mapping = {
"href": jsonld.first_id(contexts.AS.href, aliases=[jsonld.raw("@id")]),
"mediaType": jsonld.first_val(contexts.AS.mediaType),
}
class EndpointsSerializer(jsonld.JsonLdSerializer):
sharedInbox = serializers.URLField(max_length=500, required=False)
class Meta:
jsonld_mapping = {"sharedInbox": jsonld.first_id(contexts.AS.sharedInbox)}
class PublicKeySerializer(jsonld.JsonLdSerializer):
publicKeyPem = serializers.CharField(trim_whitespace=False)
class Meta:
jsonld_mapping = {"publicKeyPem": jsonld.first_val(contexts.SEC.publicKeyPem)}
def get_by_media_type(urls, media_type):
for url in urls:
if url.get("mediaType", "text/html") == media_type:
return url
2020-03-25 14:32:10 +00:00
class BasicActorSerializer(jsonld.JsonLdSerializer):
id = serializers.URLField(max_length=500)
type = serializers.ChoiceField(
choices=[getattr(contexts.AS, c[0]) for c in models.TYPE_CHOICES]
)
class Meta:
jsonld_mapping = {}
class ActorSerializer(jsonld.JsonLdSerializer):
id = serializers.URLField(max_length=500)
2019-12-09 12:59:54 +00:00
outbox = serializers.URLField(max_length=500, required=False)
inbox = serializers.URLField(max_length=500, required=False)
url = serializers.ListField(
child=URLSerializer(jsonld_expand=False), required=False, min_length=0
)
type = serializers.ChoiceField(
choices=[getattr(contexts.AS, c[0]) for c in models.TYPE_CHOICES]
)
preferredUsername = serializers.CharField()
manuallyApprovesFollowers = serializers.NullBooleanField(required=False)
name = serializers.CharField(
required=False, max_length=200, allow_blank=True, allow_null=True
)
2020-01-23 10:09:52 +00:00
summary = TruncatedCharField(
truncate_length=common_models.CONTENT_TEXT_MAX_LENGTH,
required=False,
allow_null=True,
)
2019-12-09 12:59:54 +00:00
followers = serializers.URLField(max_length=500, required=False)
following = serializers.URLField(max_length=500, required=False, allow_null=True)
publicKey = PublicKeySerializer(required=False)
endpoints = EndpointsSerializer(required=False)
icon = ImageSerializer(
2020-03-11 10:39:55 +00:00
allowed_mimetypes=["image/*"],
allow_null=True,
required=False,
allow_empty_mimetype=True,
2020-01-23 15:38:04 +00:00
)
2020-03-25 14:32:10 +00:00
attributedTo = serializers.URLField(max_length=500, required=False)
tags = serializers.ListField(
child=TagSerializer(), min_length=0, required=False, allow_null=True
)
category = serializers.CharField(required=False)
# languages = serializers.Char(
# music_models.ARTIST_CONTENT_CATEGORY_CHOICES, required=False, default="music",
# )
class Meta:
2020-03-02 16:23:03 +00:00
# not strictly necessary because it's not a model serializer
# but used by tasks.py/fetch
model = models.Actor
jsonld_mapping = {
"outbox": jsonld.first_id(contexts.AS.outbox),
"inbox": jsonld.first_id(contexts.LDP.inbox),
"following": jsonld.first_id(contexts.AS.following),
"followers": jsonld.first_id(contexts.AS.followers),
"preferredUsername": jsonld.first_val(contexts.AS.preferredUsername),
"summary": jsonld.first_val(contexts.AS.summary),
"name": jsonld.first_val(contexts.AS.name),
"publicKey": jsonld.first_obj(contexts.SEC.publicKey),
"manuallyApprovesFollowers": jsonld.first_val(
contexts.AS.manuallyApprovesFollowers
),
"mediaType": jsonld.first_val(contexts.AS.mediaType),
"endpoints": jsonld.first_obj(contexts.AS.endpoints),
2020-01-23 15:38:04 +00:00
"icon": jsonld.first_obj(contexts.AS.icon),
"url": jsonld.raw(contexts.AS.url),
2020-03-25 14:32:10 +00:00
"attributedTo": jsonld.first_id(contexts.AS.attributedTo),
"tags": jsonld.raw(contexts.AS.tag),
"category": jsonld.first_val(contexts.SC.category),
# "language": jsonld.first_val(contexts.SC.inLanguage),
}
2018-03-30 16:02:50 +00:00
2020-03-25 14:32:10 +00:00
def validate_category(self, v):
return (
v
if v in [t for t, _ in music_models.ARTIST_CONTENT_CATEGORY_CHOICES]
else None
)
def to_representation(self, instance):
ret = {
"id": instance.fid,
2018-06-09 13:36:16 +00:00
"outbox": instance.outbox_url,
"inbox": instance.inbox_url,
"preferredUsername": instance.preferred_username,
"type": instance.type,
}
if instance.name:
2018-06-09 13:36:16 +00:00
ret["name"] = instance.name
if instance.followers_url:
2018-06-09 13:36:16 +00:00
ret["followers"] = instance.followers_url
if instance.following_url:
2018-06-09 13:36:16 +00:00
ret["following"] = instance.following_url
if instance.manually_approves_followers is not None:
2018-06-09 13:36:16 +00:00
ret["manuallyApprovesFollowers"] = instance.manually_approves_followers
2020-01-23 10:09:52 +00:00
if instance.summary_obj_id:
ret["summary"] = instance.summary_obj.rendered
urls = []
if instance.url:
urls.append(
{"type": "Link", "href": instance.url, "mediaType": "text/html"}
)
channel = instance.get_channel()
if channel:
ret["url"] = [
{
"type": "Link",
"href": instance.channel.get_absolute_url()
if instance.channel.artist.is_local
else instance.get_absolute_url(),
"mediaType": "text/html",
},
{
"type": "Link",
"href": instance.channel.get_rss_url(),
"mediaType": "application/rss+xml",
},
]
include_image(ret, channel.artist.attachment_cover, "icon")
if channel.artist.description_id:
ret["summary"] = channel.artist.description.rendered
2020-03-25 14:32:10 +00:00
ret["attributedTo"] = channel.attributed_to.fid
ret["category"] = channel.artist.content_category
ret["tag"] = tag_list(channel.artist.tagged_items.all())
else:
ret["url"] = [
{
"type": "Link",
"href": instance.get_absolute_url(),
"mediaType": "text/html",
}
]
include_image(ret, instance.attachment_icon, "icon")
2020-01-23 10:09:52 +00:00
2019-07-01 12:00:32 +00:00
ret["@context"] = jsonld.get_default_context()
if instance.public_key:
2018-06-09 13:36:16 +00:00
ret["publicKey"] = {
"owner": instance.fid,
2018-06-09 13:36:16 +00:00
"publicKeyPem": instance.public_key,
"id": "{}#main-key".format(instance.fid),
}
2018-06-09 13:36:16 +00:00
ret["endpoints"] = {}
2020-01-23 15:38:04 +00:00
if instance.shared_inbox_url:
2018-06-09 13:36:16 +00:00
ret["endpoints"]["sharedInbox"] = instance.shared_inbox_url
return ret
def prepare_missing_fields(self):
kwargs = {
"fid": self.validated_data["id"],
2019-12-09 12:59:54 +00:00
"outbox_url": self.validated_data.get("outbox"),
"inbox_url": self.validated_data.get("inbox"),
2018-06-09 13:36:16 +00:00
"following_url": self.validated_data.get("following"),
"followers_url": self.validated_data.get("followers"),
"type": self.validated_data["type"],
"name": self.validated_data.get("name"),
"preferred_username": self.validated_data["preferredUsername"],
}
url = get_by_media_type(self.validated_data.get("url", []), "text/html")
if url:
kwargs["url"] = url["href"]
2018-06-09 13:36:16 +00:00
maf = self.validated_data.get("manuallyApprovesFollowers")
if maf is not None:
2018-06-09 13:36:16 +00:00
kwargs["manually_approves_followers"] = maf
domain = urllib.parse.urlparse(kwargs["fid"]).netloc
domain, domain_created = models.Domain.objects.get_or_create(pk=domain)
if domain_created and not domain.is_local:
from . import tasks
# first time we see the domain, we trigger nodeinfo fetching
tasks.update_domain_nodeinfo(domain_name=domain.name)
kwargs["domain"] = domain
for endpoint, url in self.validated_data.get("endpoints", {}).items():
2018-06-09 13:36:16 +00:00
if endpoint == "sharedInbox":
kwargs["shared_inbox_url"] = url
break
try:
kwargs["public_key"] = self.validated_data["publicKey"]["publicKeyPem"]
except KeyError:
pass
return kwargs
def validate_type(self, v):
return v.split("#")[-1]
def build(self):
d = self.prepare_missing_fields()
return models.Actor(**d)
def save(self, **kwargs):
d = self.prepare_missing_fields()
d.update(kwargs)
2020-01-23 10:09:52 +00:00
actor = models.Actor.objects.update_or_create(fid=d["fid"], defaults=d)[0]
common_utils.attach_content(
actor, "summary_obj", self.validated_data["summary"]
)
2020-01-23 15:38:04 +00:00
if "icon" in self.validated_data:
new_value = self.validated_data["icon"]
common_utils.attach_file(
actor,
"attachment_icon",
2020-03-11 10:39:55 +00:00
{"url": new_value["url"], "mimetype": new_value.get("mediaType")}
2020-01-23 15:38:04 +00:00
if new_value
else None,
)
2020-03-25 14:32:10 +00:00
rss_url = get_by_media_type(
self.validated_data.get("url", []), "application/rss+xml"
)
if rss_url:
rss_url = rss_url["href"]
2020-04-08 11:28:46 +00:00
attributed_to = self.validated_data.get("attributedTo", actor.fid)
if rss_url:
2020-03-25 14:32:10 +00:00
# if the actor is attributed to another actor, and there is a RSS url,
# then we consider it's a channel
create_or_update_channel(
actor,
rss_url=rss_url,
attributed_to_fid=attributed_to,
**self.validated_data
)
2020-01-23 10:09:52 +00:00
return actor
2020-01-23 10:09:52 +00:00
def validate(self, data):
validated_data = super().validate(data)
if "summary" in data:
validated_data["summary"] = {
"content_type": "text/html",
"text": data["summary"],
}
else:
validated_data["summary"] = None
return validated_data
2018-03-31 16:41:15 +00:00
2020-03-25 14:32:10 +00:00
def create_or_update_channel(actor, rss_url, attributed_to_fid, **validated_data):
from funkwhale_api.audio import models as audio_models
attributed_to = actors.get_actor(attributed_to_fid)
artist_defaults = {
"name": validated_data.get("name", validated_data["preferredUsername"]),
"fid": validated_data["id"],
"content_category": validated_data.get("category", "music") or "music",
"attributed_to": attributed_to,
}
artist, created = music_models.Artist.objects.update_or_create(
channel__attributed_to=attributed_to,
channel__actor=actor,
defaults=artist_defaults,
)
common_utils.attach_content(artist, "description", validated_data.get("summary"))
if "icon" in validated_data:
new_value = validated_data["icon"]
common_utils.attach_file(
artist,
"attachment_cover",
{"url": new_value["url"], "mimetype": new_value.get("mediaType")}
if new_value
else None,
)
tags = [t["name"] for t in validated_data.get("tags", []) or []]
tags_models.set_tags(artist, *tags)
if created:
uid = uuid.uuid4()
fid = utils.full_url(
reverse("federation:music:libraries-detail", kwargs={"uuid": uid})
)
library = attributed_to.libraries.create(
privacy_level="everyone", name=artist_defaults["name"], fid=fid, uuid=uid,
)
else:
library = artist.channel.library
channel_defaults = {
"actor": actor,
"attributed_to": attributed_to,
"rss_url": rss_url,
"artist": artist,
"library": library,
}
channel, created = audio_models.Channel.objects.update_or_create(
actor=actor, attributed_to=attributed_to, defaults=channel_defaults,
)
return channel
class APIActorSerializer(serializers.ModelSerializer):
class Meta:
model = models.Actor
fields = [
"fid",
2018-06-09 13:36:16 +00:00
"url",
"creation_date",
"summary",
"preferred_username",
"name",
"last_fetch_date",
"domain",
"type",
"manually_approves_followers",
"full_username",
"is_local",
]
class BaseActivitySerializer(serializers.Serializer):
id = serializers.URLField(max_length=500, required=False)
type = serializers.CharField(max_length=100)
actor = serializers.URLField(max_length=500)
2020-04-08 11:28:46 +00:00
object = serializers.JSONField(required=False, allow_null=True)
2018-06-09 13:36:16 +00:00
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
2018-04-14 16:50:37 +00:00
try:
return models.Actor.objects.get(fid=v)
except models.Actor.DoesNotExist:
raise serializers.ValidationError("Actor not found")
2018-04-14 16:50:37 +00:00
def create(self, validated_data):
return models.Activity.objects.create(
fid=validated_data.get("id"),
actor=validated_data["actor"],
payload=self.initial_data,
2018-09-13 15:18:23 +00:00
type=validated_data["type"],
)
def validate(self, data):
2020-04-08 11:28:46 +00:00
self.validate_recipients(data, self.initial_data)
return super().validate(data)
2020-04-08 11:28:46 +00:00
def validate_recipients(self, data, payload):
"""
Ensure we have at least a to/cc field with valid actors
"""
2020-04-08 11:28:46 +00:00
data["to"] = payload.get("to", [])
data["cc"] = payload.get("cc", [])
if (
not data["to"]
and data.get("type") in ["Follow", "Accept"]
and data.get("object")
):
# there isn't always a to field for Accept/Follow
# in their follow activity, so we consider the recipient
# to be the follow object
if data["type"] == "Follow":
data["to"].append(str(data.get("object")))
else:
data["to"].append(data.get("object", {}).get("actor"))
2020-04-08 11:28:46 +00:00
if not data["to"] and not data["cc"] and not self.context.get("recipients"):
raise serializers.ValidationError(
"We cannot handle an activity with no recipient"
)
2018-04-12 18:38:06 +00:00
class FollowSerializer(serializers.Serializer):
id = serializers.URLField(max_length=500)
object = serializers.URLField(max_length=500)
actor = serializers.URLField(max_length=500)
2018-06-09 13:36:16 +00:00
type = serializers.ChoiceField(choices=["Follow"])
2018-04-03 21:24:51 +00:00
def validate_object(self, v):
2018-06-09 13:36:16 +00:00
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:
2018-06-09 13:36:16 +00:00
raise serializers.ValidationError("Invalid target")
try:
obj = models.Actor.objects.get(fid=v)
if recipient and recipient.fid != obj.fid:
raise serializers.ValidationError("Invalid target")
return obj
except models.Actor.DoesNotExist:
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")
def validate_actor(self, v):
2018-06-09 13:36:16 +00:00
expected = self.context.get("follow_actor")
if expected and expected.fid != v:
2018-06-09 13:36:16 +00:00
raise serializers.ValidationError("Invalid actor")
try:
return models.Actor.objects.get(fid=v)
except models.Actor.DoesNotExist:
2018-06-09 13:36:16 +00:00
raise serializers.ValidationError("Actor not found")
def save(self, **kwargs):
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"]
approved = kwargs.pop("approved", None)
follow, created = follow_class.objects.update_or_create(
2018-06-09 13:36:16 +00:00
actor=self.validated_data["actor"],
target=self.validated_data["object"],
defaults=defaults,
)
if not created:
# We likely received a new follow when we had an existing one in database
# this can happen when two instances are out of sync, e.g because some
# messages are not delivered properly. In this case, we don't change
# the follow approved status and return the follow as is.
# We set a new UUID to ensure the follow urls are updated properly
# cf #830
follow.uuid = uuid.uuid4()
follow.save(update_fields=["uuid"])
return follow
# it's a brand new follow, we use the approved value stored earlier
if approved != follow.approved:
follow.approved = approved
follow.save(update_fields=["approved"])
return follow
2018-04-03 21:24:51 +00:00
def to_representation(self, instance):
return {
2019-07-01 12:00:32 +00:00
"@context": jsonld.get_default_context(),
"actor": instance.actor.fid,
"id": instance.get_federation_id(),
"object": instance.target.fid,
2018-06-09 13:36:16 +00:00
"type": "Follow",
}
2018-04-03 21:24:51 +00:00
class APIFollowSerializer(serializers.ModelSerializer):
actor = APIActorSerializer()
target = APIActorSerializer()
class Meta:
model = models.Follow
fields = [
2018-06-09 13:36:16 +00:00
"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)
object = FollowSerializer()
2018-06-09 13:36:16 +00:00
type = serializers.ChoiceField(choices=["Accept"])
def validate_actor(self, v):
expected = self.context.get("actor")
if expected and expected.fid != v:
2018-06-09 13:36:16 +00:00
raise serializers.ValidationError("Invalid actor")
try:
return models.Actor.objects.get(fid=v)
except models.Actor.DoesNotExist:
2018-06-09 13:36:16 +00:00
raise serializers.ValidationError("Actor not found")
def validate(self, validated_data):
# 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:
2018-06-09 13:36:16 +00:00
raise serializers.ValidationError("Actor mismatch")
try:
2018-06-09 13:36:16 +00:00
validated_data["follow"] = (
follow_class.objects.filter(
target=target, actor=validated_data["object"]["actor"]
2018-06-09 13:36:16 +00:00
)
.exclude(approved=True)
.select_related()
2018-06-09 13:36:16 +00:00
.get()
)
except follow_class.DoesNotExist:
2018-06-09 13:36:16 +00:00
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
return {
2019-07-01 12:00:32 +00:00
"@context": jsonld.get_default_context(),
"id": instance.get_federation_id() + "/accept",
"type": "Accept",
"actor": actor.fid,
2018-06-09 13:36:16 +00:00
"object": FollowSerializer(instance).data,
}
def save(self):
follow = self.validated_data["follow"]
follow.approved = True
follow.save()
if follow.target._meta.label == "music.Library":
2018-09-24 18:44:22 +00:00
follow.target.schedule_scan(actor=follow.actor)
return follow
class UndoFollowSerializer(serializers.Serializer):
id = serializers.URLField(max_length=500)
actor = serializers.URLField(max_length=500)
object = FollowSerializer()
2018-06-09 13:36:16 +00:00
type = serializers.ChoiceField(choices=["Undo"])
def validate_actor(self, v):
2018-09-24 18:44:22 +00:00
expected = self.context.get("actor")
if expected and expected.fid != v:
2018-06-09 13:36:16 +00:00
raise serializers.ValidationError("Invalid actor")
try:
return models.Actor.objects.get(fid=v)
except models.Actor.DoesNotExist:
2018-06-09 13:36:16 +00:00
raise serializers.ValidationError("Actor not found")
def validate(self, validated_data):
# we ensure the accept actor actually match the follow actor
2018-06-09 13:36:16 +00:00
if validated_data["actor"] != validated_data["object"]["actor"]:
raise serializers.ValidationError("Actor mismatch")
2018-09-24 18:44:22 +00:00
target = validated_data["object"]["object"]
if target._meta.label == "music.Library":
follow_class = models.LibraryFollow
else:
follow_class = models.Follow
try:
2018-09-24 18:44:22 +00:00
validated_data["follow"] = follow_class.objects.filter(
actor=validated_data["actor"], target=target
).get()
2018-09-24 18:44:22 +00:00
except follow_class.DoesNotExist:
2018-06-09 13:36:16 +00:00
raise serializers.ValidationError("No follow to remove")
return validated_data
def to_representation(self, instance):
return {
2019-07-01 12:00:32 +00:00
"@context": jsonld.get_default_context(),
"id": instance.get_federation_id() + "/undo",
"type": "Undo",
"actor": instance.actor.fid,
2018-06-09 13:36:16 +00:00
"object": FollowSerializer(instance).data,
}
def save(self):
2018-06-09 13:36:16 +00:00
return self.validated_data["follow"].delete()
2018-04-08 08:42:10 +00:00
class ActorWebfingerSerializer(serializers.Serializer):
subject = serializers.CharField()
aliases = serializers.ListField(child=serializers.URLField(max_length=500))
2018-04-08 08:42:10 +00:00
links = serializers.ListField()
actor_url = serializers.URLField(max_length=500, required=False)
2018-04-08 08:42:10 +00:00
def validate(self, validated_data):
2018-06-09 13:36:16 +00:00
validated_data["actor_url"] = None
for l in validated_data["links"]:
2018-04-08 08:42:10 +00:00
try:
2018-06-09 13:36:16 +00:00
if not l["rel"] == "self":
2018-04-08 08:42:10 +00:00
continue
2018-06-09 13:36:16 +00:00
if not l["type"] == "application/activity+json":
2018-04-08 08:42:10 +00:00
continue
2018-06-09 13:36:16 +00:00
validated_data["actor_url"] = l["href"]
2018-04-08 08:42:10 +00:00
break
except KeyError:
pass
2018-06-09 13:36:16 +00:00
if validated_data["actor_url"] is None:
raise serializers.ValidationError("No valid actor url found")
2018-04-08 08:42:10 +00:00
return validated_data
def to_representation(self, instance):
data = {}
2018-06-09 13:36:16 +00:00
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)
2018-06-09 13:36:16 +00:00
type = serializers.ChoiceField(choices=[(c, c) for c in activity.ACTIVITY_TYPES])
2018-09-22 12:29:30 +00:00
object = serializers.JSONField(required=False)
target = serializers.JSONField(required=False)
def validate_object(self, value):
try:
2018-06-09 13:36:16 +00:00
type = value["type"]
except KeyError:
2018-06-09 13:36:16 +00:00
raise serializers.ValidationError("Missing object type")
2018-04-03 17:48:50 +00:00
except TypeError:
# probably a URL
return value
try:
object_serializer = OBJECT_SERIALIZERS[type]
except KeyError:
2018-06-09 13:36:16 +00:00
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):
2018-06-09 13:36:16 +00:00
request_actor = self.context.get("actor")
if request_actor and request_actor.fid != value:
raise serializers.ValidationError(
2018-06-09 13:36:16 +00:00
"The actor making the request do not match" " the activity actor"
)
return value
def to_representation(self, conf):
d = {}
d.update(conf)
2018-06-09 13:36:16 +00:00
if self.context.get("include_ap_context", True):
2019-07-01 12:00:32 +00:00
d["@context"] = jsonld.get_default_context()
return d
class ObjectSerializer(serializers.Serializer):
id = serializers.URLField(max_length=500)
url = serializers.URLField(max_length=500, required=False, allow_null=True)
2018-06-09 13:36:16 +00:00
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(
2018-06-09 13:36:16 +00:00
child=serializers.URLField(max_length=500), required=False, allow_null=True
)
cc = serializers.ListField(
2018-06-09 13:36:16 +00:00
child=serializers.URLField(max_length=500), required=False, allow_null=True
)
bto = serializers.ListField(
2018-06-09 13:36:16 +00:00
child=serializers.URLField(max_length=500), required=False, allow_null=True
)
bcc = serializers.ListField(
2018-06-09 13:36:16 +00:00
child=serializers.URLField(max_length=500), required=False, allow_null=True
)
2018-06-09 13:36:16 +00:00
OBJECT_SERIALIZERS = {t: ObjectSerializer for t in activity.OBJECT_TYPES}
2018-04-06 15:58:43 +00:00
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
PAGINATED_COLLECTION_JSONLD_MAPPING = {
"totalItems": jsonld.first_val(contexts.AS.totalItems),
"first": jsonld.first_id(contexts.AS.first),
"last": jsonld.first_id(contexts.AS.last),
"partOf": jsonld.first_id(contexts.AS.partOf),
}
class PaginatedCollectionSerializer(jsonld.JsonLdSerializer):
type = serializers.ChoiceField(
choices=[contexts.AS.Collection, contexts.AS.OrderedCollection]
)
totalItems = serializers.IntegerField(min_value=0)
id = serializers.URLField(max_length=500)
first = serializers.URLField(max_length=500)
last = serializers.URLField(max_length=500)
2018-04-06 15:58:43 +00:00
class Meta:
jsonld_mapping = PAGINATED_COLLECTION_JSONLD_MAPPING
2018-04-06 15:58:43 +00:00
def to_representation(self, conf):
2018-06-09 13:36:16 +00:00
paginator = Paginator(conf["items"], conf.get("page_size", 20))
first = common_utils.set_query_parameter(conf["id"], page=1)
2018-04-06 15:58:43 +00:00
current = first
last = common_utils.set_query_parameter(conf["id"], page=paginator.num_pages)
2018-04-06 15:58:43 +00:00
d = {
2018-06-09 13:36:16 +00:00
"id": conf["id"],
# XXX Stable release: remove the obsolete actor field
"actor": conf["actor"].fid,
"attributedTo": conf["actor"].fid,
2018-06-09 13:36:16 +00:00
"totalItems": paginator.count,
"type": conf.get("type", "Collection"),
2018-06-09 13:36:16 +00:00
"current": current,
"first": first,
"last": last,
2018-04-06 15:58:43 +00:00
}
d.update(get_additional_fields(conf))
2018-06-09 13:36:16 +00:00
if self.context.get("include_ap_context", True):
2019-07-01 12:00:32 +00:00
d["@context"] = jsonld.get_default_context()
2018-04-06 15:58:43 +00:00
return d
class LibrarySerializer(PaginatedCollectionSerializer):
type = serializers.ChoiceField(
choices=[contexts.AS.Collection, contexts.FW.Library]
)
actor = serializers.URLField(max_length=500, required=False)
attributedTo = serializers.URLField(max_length=500, required=False)
name = serializers.CharField()
summary = serializers.CharField(allow_blank=True, allow_null=True, required=False)
2018-09-22 12:29:30 +00:00
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,
)
class Meta:
2020-03-02 16:23:03 +00:00
# not strictly necessary because it's not a model serializer
# but used by tasks.py/fetch
model = music_models.Library
jsonld_mapping = common_utils.concat_dicts(
PAGINATED_COLLECTION_JSONLD_MAPPING,
{
"name": jsonld.first_val(contexts.AS.name),
"summary": jsonld.first_val(contexts.AS.summary),
"audience": jsonld.first_id(contexts.AS.audience),
"followers": jsonld.first_id(contexts.AS.followers),
"actor": jsonld.first_id(contexts.AS.actor),
"attributedTo": jsonld.first_id(contexts.AS.attributedTo),
},
)
def validate(self, validated_data):
d = super().validate(validated_data)
actor = d.get("actor")
attributed_to = d.get("attributedTo")
if not actor and not attributed_to:
raise serializers.ValidationError(
"You need to provide at least actor or attributedTo"
)
d["attributedTo"] = attributed_to or actor
return d
def to_representation(self, library):
conf = {
"id": library.fid,
"name": library.name,
"summary": library.description,
"page_size": 100,
# XXX Stable release: remove the obsolete actor field
"actor": library.actor,
"attributedTo": library.actor,
2018-09-24 18:44:22 +00:00
"items": library.uploads.for_federation(),
"type": "Library",
}
r = super().to_representation(conf)
r["audience"] = (
contexts.AS.Public if library.privacy_level == "everyone" else ""
)
2018-09-22 12:29:30 +00:00
r["followers"] = library.followers_url
return r
def create(self, validated_data):
2020-03-02 16:23:03 +00:00
if self.instance:
actor = self.instance.actor
else:
actor = utils.retrieve_ap_object(
validated_data["attributedTo"],
actor=self.context.get("fetch_actor"),
queryset=models.Actor,
serializer_class=ActorSerializer,
)
privacy = {"": "me", "./": "me", None: "me", contexts.AS.Public: "everyone"}
library, created = music_models.Library.objects.update_or_create(
fid=validated_data["id"],
actor=actor,
defaults={
2018-09-22 12:29:30 +00:00
"uploads_count": validated_data["totalItems"],
"name": validated_data["name"],
"description": validated_data.get("summary"),
2018-09-22 12:29:30 +00:00
"followers_url": validated_data["followers"],
"privacy_level": privacy[validated_data["audience"]],
},
)
return library
2020-03-02 16:23:03 +00:00
def update(self, instance, validated_data):
return self.create(validated_data)
class CollectionPageSerializer(jsonld.JsonLdSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.CollectionPage])
totalItems = serializers.IntegerField(min_value=0)
items = serializers.ListField()
actor = serializers.URLField(max_length=500, required=False)
attributedTo = serializers.URLField(max_length=500, required=False)
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)
2018-04-06 15:58:43 +00:00
class Meta:
jsonld_mapping = {
"totalItems": jsonld.first_val(contexts.AS.totalItems),
"items": jsonld.raw(contexts.AS.items),
"actor": jsonld.first_id(contexts.AS.actor),
"attributedTo": jsonld.first_id(contexts.AS.attributedTo),
"first": jsonld.first_id(contexts.AS.first),
"last": jsonld.first_id(contexts.AS.last),
"next": jsonld.first_id(contexts.AS.next),
2019-04-11 08:17:10 +00:00
"prev": jsonld.first_id(contexts.AS.prev),
"partOf": jsonld.first_id(contexts.AS.partOf),
}
2018-04-11 21:13:33 +00:00
def validate_items(self, v):
2018-06-09 13:36:16 +00:00
item_serializer = self.context.get("item_serializer")
2018-04-11 21:13:33 +00:00
if not item_serializer:
return v
raw_items = [item_serializer(data=i, context=self.context) for i in v]
valid_items = []
2018-04-11 21:13:33 +00:00
for i in raw_items:
2018-09-24 18:44:22 +00:00
try:
i.is_valid(raise_exception=True)
valid_items.append(i)
2018-09-24 18:44:22 +00:00
except serializers.ValidationError:
2018-06-09 13:36:16 +00:00
logger.debug("Invalid item %s: %s", i.data, i.errors)
2018-04-11 21:13:33 +00:00
return valid_items
2018-04-11 21:13:33 +00:00
2018-04-06 15:58:43 +00:00
def to_representation(self, conf):
2018-06-09 13:36:16 +00:00
page = conf["page"]
first = common_utils.set_query_parameter(conf["id"], page=1)
last = common_utils.set_query_parameter(
2018-06-09 13:36:16 +00:00
conf["id"], page=page.paginator.num_pages
)
id = common_utils.set_query_parameter(conf["id"], page=page.number)
2018-04-06 15:58:43 +00:00
d = {
2018-06-09 13:36:16 +00:00
"id": id,
"partOf": conf["id"],
# XXX Stable release: remove the obsolete actor field
"actor": conf["actor"].fid,
"attributedTo": conf["actor"].fid,
2018-06-09 13:36:16 +00:00
"totalItems": page.paginator.count,
"type": "CollectionPage",
"first": first,
"last": last,
"items": [
conf["item_serializer"](
i, context={"actor": conf["actor"], "include_ap_context": False}
2018-04-06 15:58:43 +00:00
).data
for i in page.object_list
2018-06-09 13:36:16 +00:00
],
2018-04-06 15:58:43 +00:00
}
if page.has_previous():
d["prev"] = common_utils.set_query_parameter(
2018-06-09 13:36:16 +00:00
conf["id"], page=page.previous_page_number()
)
2018-04-06 15:58:43 +00:00
2018-04-07 16:37:40 +00:00
if page.has_next():
d["next"] = common_utils.set_query_parameter(
2018-06-09 13:36:16 +00:00
conf["id"], page=page.next_page_number()
)
d.update(get_additional_fields(conf))
2018-06-09 13:36:16 +00:00
if self.context.get("include_ap_context", True):
2019-07-01 12:00:32 +00:00
d["@context"] = jsonld.get_default_context()
2018-04-06 15:58:43 +00:00
return d
MUSIC_ENTITY_JSONLD_MAPPING = {
"name": jsonld.first_val(contexts.AS.name),
"published": jsonld.first_val(contexts.AS.published),
"musicbrainzId": jsonld.first_val(contexts.FW.musicbrainzId),
2019-04-11 08:17:10 +00:00
"attributedTo": jsonld.first_id(contexts.AS.attributedTo),
"tags": jsonld.raw(contexts.AS.tag),
"mediaType": jsonld.first_val(contexts.AS.mediaType),
"content": jsonld.first_val(contexts.AS.content),
}
2019-12-09 12:59:54 +00:00
def repr_tag(tag_name):
return {"type": "Hashtag", "name": "#{}".format(tag_name)}
def include_content(repr, content_obj):
if not content_obj:
return
repr["content"] = common_utils.render_html(
content_obj.text, content_obj.content_type
)
repr["mediaType"] = "text/html"
2020-01-23 15:38:04 +00:00
def include_image(repr, attachment, field="image"):
2020-01-17 15:27:11 +00:00
if attachment:
2020-01-23 15:38:04 +00:00
repr[field] = {
2020-01-17 15:27:11 +00:00
"type": "Image",
"url": attachment.download_url_original,
2020-01-17 15:27:11 +00:00
"mediaType": attachment.mimetype or "image/jpeg",
}
else:
2020-01-23 15:38:04 +00:00
repr[field] = None
2020-01-17 15:27:11 +00:00
class MusicEntitySerializer(jsonld.JsonLdSerializer):
2018-09-22 12:29:30 +00:00
id = serializers.URLField(max_length=500)
published = serializers.DateTimeField()
musicbrainzId = serializers.UUIDField(allow_null=True, required=False)
name = serializers.CharField(max_length=1000)
2019-04-11 08:17:10 +00:00
attributedTo = serializers.URLField(max_length=500, allow_null=True, required=False)
updateable_fields = []
tags = serializers.ListField(
child=TagSerializer(), min_length=0, required=False, allow_null=True
)
mediaType = serializers.ChoiceField(
choices=common_models.CONTENT_TEXT_SUPPORTED_TYPES,
default="text/html",
required=False,
)
content = TruncatedCharField(
truncate_length=common_models.CONTENT_TEXT_MAX_LENGTH,
required=False,
allow_null=True,
)
2019-04-11 08:17:10 +00:00
def update(self, instance, validated_data):
2020-03-02 16:23:03 +00:00
return self.update_or_create(validated_data)
@transaction.atomic
def update_or_create(self, validated_data):
instance = self.instance or self.Meta.model(fid=validated_data["id"])
creating = instance.pk is None
2019-04-11 08:17:10 +00:00
attributed_to_fid = validated_data.get("attributedTo")
if attributed_to_fid:
validated_data["attributedTo"] = actors.get_actor(attributed_to_fid)
updated_fields = common_utils.get_updated_fields(
2019-04-11 08:17:10 +00:00
self.updateable_fields, validated_data, instance
)
updated_fields = self.validate_updated_data(instance, updated_fields)
2020-03-02 16:23:03 +00:00
if creating:
instance, created = self.Meta.model.objects.get_or_create(
fid=validated_data["id"], defaults=updated_fields
)
else:
music_tasks.update_library_entity(instance, updated_fields)
2019-04-11 08:17:10 +00:00
tags = [t["name"] for t in validated_data.get("tags", []) or []]
tags_models.set_tags(instance, *tags)
common_utils.attach_content(
instance, "description", validated_data.get("description")
)
2019-04-11 08:17:10 +00:00
return instance
2018-09-22 12:29:30 +00:00
def get_tags_repr(self, instance):
2020-03-25 14:32:10 +00:00
return tag_list(instance.tagged_items.all())
def validate_updated_data(self, instance, validated_data):
2020-01-17 15:27:11 +00:00
try:
attachment_cover = validated_data.pop("attachment_cover")
except KeyError:
return validated_data
if (
instance.attachment_cover
and instance.attachment_cover.url == attachment_cover["url"]
2020-01-17 15:27:11 +00:00
):
# we already have the proper attachment
return validated_data
# create the attachment by hand so it can be attached as the cover
validated_data["attachment_cover"] = common_models.Attachment.objects.create(
2020-03-11 10:39:55 +00:00
mimetype=attachment_cover.get("mediaType"),
url=attachment_cover["url"],
2020-01-17 15:27:11 +00:00
actor=instance.attributed_to,
)
return validated_data
def validate(self, data):
validated_data = super().validate(data)
if data.get("content"):
validated_data["description"] = {
"content_type": data["mediaType"],
"text": data["content"],
}
return validated_data
2018-09-22 12:29:30 +00:00
class ArtistSerializer(MusicEntitySerializer):
image = ImageSerializer(
2020-03-11 10:39:55 +00:00
allowed_mimetypes=["image/*"],
allow_null=True,
required=False,
allow_empty_mimetype=True,
2020-01-17 15:27:11 +00:00
)
2019-04-11 08:17:10 +00:00
updateable_fields = [
("name", "name"),
("musicbrainzId", "mbid"),
("attributedTo", "attributed_to"),
2020-01-17 15:27:11 +00:00
("image", "attachment_cover"),
2019-04-11 08:17:10 +00:00
]
class Meta:
2019-04-18 12:37:17 +00:00
model = music_models.Artist
2020-01-17 15:27:11 +00:00
jsonld_mapping = common_utils.concat_dicts(
MUSIC_ENTITY_JSONLD_MAPPING,
{
"released": jsonld.first_val(contexts.FW.released),
"artists": jsonld.first_attr(contexts.FW.artists, "@list"),
"image": jsonld.first_obj(contexts.AS.image),
},
)
2018-09-22 12:29:30 +00:00
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,
2019-04-11 08:17:10 +00:00
"attributedTo": instance.attributed_to.fid
if instance.attributed_to
else None,
"tag": self.get_tags_repr(instance),
2018-09-22 12:29:30 +00:00
}
include_content(d, instance.description)
2020-01-17 15:27:11 +00:00
include_image(d, instance.attachment_cover)
2018-09-22 12:29:30 +00:00
if self.context.get("include_ap_context", self.parent is None):
2019-07-01 12:00:32 +00:00
d["@context"] = jsonld.get_default_context()
2018-09-22 12:29:30 +00:00
return d
2020-03-02 16:23:03 +00:00
create = MusicEntitySerializer.update_or_create
2018-09-22 12:29:30 +00:00
class AlbumSerializer(MusicEntitySerializer):
released = serializers.DateField(allow_null=True, required=False)
2020-03-25 14:32:10 +00:00
artists = serializers.ListField(
child=MultipleSerializer(allowed=[BasicActorSerializer, ArtistSerializer]),
min_length=1,
)
2020-01-17 15:27:11 +00:00
# XXX: 1.0 rename to image
cover = ImageSerializer(
2020-03-11 10:39:55 +00:00
allowed_mimetypes=["image/*"],
allow_null=True,
required=False,
allow_empty_mimetype=True,
2018-09-23 12:38:42 +00:00
)
2019-04-11 08:17:10 +00:00
updateable_fields = [
("name", "title"),
2020-03-02 16:23:03 +00:00
("cover", "attachment_cover"),
2019-04-11 08:17:10 +00:00
("musicbrainzId", "mbid"),
("attributedTo", "attributed_to"),
("released", "release_date"),
2020-03-02 16:23:03 +00:00
("_artist", "artist"),
2019-04-11 08:17:10 +00:00
]
2018-09-22 12:29:30 +00:00
class Meta:
2019-04-18 12:37:17 +00:00
model = music_models.Album
jsonld_mapping = common_utils.concat_dicts(
MUSIC_ENTITY_JSONLD_MAPPING,
{
"released": jsonld.first_val(contexts.FW.released),
"artists": jsonld.first_attr(contexts.FW.artists, "@list"),
"cover": jsonld.first_obj(contexts.FW.cover),
},
)
2018-09-22 12:29:30 +00:00
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,
2019-04-11 08:17:10 +00:00
"attributedTo": instance.attributed_to.fid
if instance.attributed_to
else None,
"tag": self.get_tags_repr(instance),
2018-09-22 12:29:30 +00:00
}
2020-03-25 14:32:10 +00:00
if instance.artist.get_channel():
d["artists"] = [
{
"type": instance.artist.channel.actor.type,
"id": instance.artist.channel.actor.fid,
}
]
else:
d["artists"] = [
ArtistSerializer(
instance.artist, context={"include_ap_context": False}
).data
]
include_content(d, instance.description)
2019-11-25 08:49:06 +00:00
if instance.attachment_cover:
2018-09-23 12:38:42 +00:00
d["cover"] = {
"type": "Link",
2019-11-25 08:49:06 +00:00
"href": instance.attachment_cover.download_url_original,
"mediaType": instance.attachment_cover.mimetype or "image/jpeg",
2018-09-23 12:38:42 +00:00
}
2020-01-17 15:27:11 +00:00
include_image(d, instance.attachment_cover)
2018-09-22 12:29:30 +00:00
if self.context.get("include_ap_context", self.parent is None):
2019-07-01 12:00:32 +00:00
d["@context"] = jsonld.get_default_context()
2018-09-22 12:29:30 +00:00
return d
2020-03-02 16:23:03 +00:00
def validate(self, data):
validated_data = super().validate(data)
if not self.parent:
2020-03-25 14:32:10 +00:00
artist_data = validated_data["artists"][0]
if artist_data.get("type", "Artist") == "Artist":
validated_data["_artist"] = utils.retrieve_ap_object(
artist_data["id"],
actor=self.context.get("fetch_actor"),
queryset=music_models.Artist,
serializer_class=ArtistSerializer,
)
else:
# we have an actor as an artist, so it's a channel
actor = actors.get_actor(artist_data["id"])
validated_data["_artist"] = actor.channel.artist
2020-03-02 16:23:03 +00:00
return validated_data
create = MusicEntitySerializer.update_or_create
2018-09-22 12:29:30 +00:00
class TrackSerializer(MusicEntitySerializer):
position = serializers.IntegerField(min_value=0, allow_null=True, required=False)
disc = serializers.IntegerField(min_value=1, allow_null=True, required=False)
2018-09-22 12:29:30 +00:00
artists = serializers.ListField(child=ArtistSerializer(), min_length=1)
album = AlbumSerializer()
2018-12-04 14:13:37 +00:00
license = serializers.URLField(allow_null=True, required=False)
copyright = serializers.CharField(allow_null=True, required=False)
image = ImageSerializer(
2020-03-11 10:39:55 +00:00
allowed_mimetypes=["image/*"],
allow_null=True,
required=False,
allow_empty_mimetype=True,
2020-01-17 15:27:11 +00:00
)
2018-09-22 12:29:30 +00:00
2019-04-11 08:17:10 +00:00
updateable_fields = [
("name", "title"),
("musicbrainzId", "mbid"),
("attributedTo", "attributed_to"),
("disc", "disc_number"),
("position", "position"),
("copyright", "copyright"),
("license", "license"),
2020-01-17 15:27:11 +00:00
("image", "attachment_cover"),
2019-04-11 08:17:10 +00:00
]
class Meta:
2019-04-18 12:37:17 +00:00
model = music_models.Track
jsonld_mapping = common_utils.concat_dicts(
MUSIC_ENTITY_JSONLD_MAPPING,
{
"album": jsonld.first_obj(contexts.FW.album),
"artists": jsonld.first_attr(contexts.FW.artists, "@list"),
"copyright": jsonld.first_val(contexts.FW.copyright),
"disc": jsonld.first_val(contexts.FW.disc),
"license": jsonld.first_id(contexts.FW.license),
"position": jsonld.first_val(contexts.FW.position),
2020-01-17 15:27:11 +00:00
"image": jsonld.first_obj(contexts.AS.image),
},
)
2018-09-22 12:29:30 +00:00
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,
"disc": instance.disc_number,
2018-12-04 14:13:37 +00:00
"license": instance.local_license["identifiers"][0]
if instance.local_license
else None,
"copyright": instance.copyright if instance.copyright else None,
2018-09-22 12:29:30 +00:00
"artists": [
ArtistSerializer(
instance.artist, context={"include_ap_context": False}
).data
],
"album": AlbumSerializer(
instance.album, context={"include_ap_context": False}
).data,
2019-04-11 08:17:10 +00:00
"attributedTo": instance.attributed_to.fid
if instance.attributed_to
else None,
"tag": self.get_tags_repr(instance),
2018-09-22 12:29:30 +00:00
}
include_content(d, instance.description)
2020-01-17 15:27:11 +00:00
include_image(d, instance.attachment_cover)
2018-09-22 12:29:30 +00:00
if self.context.get("include_ap_context", self.parent is None):
2019-07-01 12:00:32 +00:00
d["@context"] = jsonld.get_default_context()
2018-09-22 12:29:30 +00:00
return d
2018-09-23 12:38:42 +00:00
def create(self, validated_data):
from funkwhale_api.music import tasks as music_tasks
2019-04-11 08:17:10 +00:00
references = {}
actors_to_fetch = set()
actors_to_fetch.add(
common_utils.recursive_getattr(
2019-04-11 08:17:10 +00:00
validated_data, "attributedTo", permissive=True
)
)
actors_to_fetch.add(
common_utils.recursive_getattr(
2019-04-11 08:17:10 +00:00
validated_data, "album.attributedTo", permissive=True
)
)
artists = (
common_utils.recursive_getattr(validated_data, "artists", permissive=True)
2019-04-11 08:17:10 +00:00
or []
)
album_artists = (
common_utils.recursive_getattr(
2019-04-11 08:17:10 +00:00
validated_data, "album.artists", permissive=True
)
or []
)
for artist in artists + album_artists:
actors_to_fetch.add(artist.get("attributedTo"))
for url in actors_to_fetch:
if not url:
continue
references[url] = actors.get_actor(url)
metadata = music_tasks.federation_audio_track_to_metadata(
validated_data, references
)
2018-09-23 12:38:42 +00:00
from_activity = self.context.get("activity")
if from_activity:
metadata["from_activity_id"] = from_activity.pk
track = music_tasks.get_track_from_import_metadata(metadata, update_cover=True)
2019-07-13 09:15:31 +00:00
2018-09-23 12:38:42 +00:00
return track
2019-04-11 08:17:10 +00:00
def update(self, obj, validated_data):
if validated_data.get("license"):
validated_data["license"] = licenses.match(validated_data["license"])
return super().update(obj, validated_data)
class UploadSerializer(jsonld.JsonLdSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.Audio])
id = serializers.URLField(max_length=500)
library = serializers.URLField(max_length=500)
2018-09-23 12:38:42 +00:00
url = LinkSerializer(allowed_mimetypes=["audio/*"])
published = serializers.DateTimeField()
2018-09-22 12:29:30 +00:00
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)
2018-09-22 12:29:30 +00:00
track = TrackSerializer(required=True)
class Meta:
2019-04-18 12:37:17 +00:00
model = music_models.Upload
jsonld_mapping = {
"track": jsonld.first_obj(contexts.FW.track),
"library": jsonld.first_id(contexts.FW.library),
"url": jsonld.first_obj(contexts.AS.url),
"published": jsonld.first_val(contexts.AS.published),
"updated": jsonld.first_val(contexts.AS.updated),
"duration": jsonld.first_val(contexts.AS.duration),
"bitrate": jsonld.first_val(contexts.FW.bitrate),
"size": jsonld.first_val(contexts.FW.size),
}
def validate_url(self, v):
try:
2018-06-09 15:41:59 +00:00
v["href"]
except (KeyError, TypeError):
2018-06-09 13:36:16 +00:00
raise serializers.ValidationError("Missing href")
try:
2018-06-09 13:36:16 +00:00
media_type = v["mediaType"]
except (KeyError, TypeError):
2018-06-09 13:36:16 +00:00
raise serializers.ValidationError("Missing mediaType")
2018-06-09 13:36:16 +00:00
if not media_type or not media_type.startswith("audio/"):
raise serializers.ValidationError("Invalid mediaType")
return v
def validate_library(self, v):
lb = self.context.get("library")
if lb:
if lb.fid != v:
raise serializers.ValidationError("Invalid library")
return lb
2018-09-22 12:29:30 +00:00
actor = self.context.get("actor")
2020-03-02 16:23:03 +00:00
try:
2020-03-02 16:23:03 +00:00
library = utils.retrieve_ap_object(
v,
actor=self.context.get("fetch_actor"),
queryset=music_models.Library,
serializer_class=LibrarySerializer,
)
except Exception:
raise serializers.ValidationError("Invalid library")
if actor and library.actor != actor:
raise serializers.ValidationError("Invalid library")
2020-03-02 16:23:03 +00:00
return library
def update(self, instance, validated_data):
return self.create(validated_data)
2020-03-02 16:23:03 +00:00
@transaction.atomic
def create(self, validated_data):
2020-03-02 16:23:03 +00:00
instance = self.instance or None
if not self.instance:
try:
instance = music_models.Upload.objects.get(fid=validated_data["id"])
except music_models.Upload.DoesNotExist:
pass
2018-09-22 12:29:30 +00:00
2020-03-02 16:23:03 +00:00
if instance:
data = {
"mimetype": validated_data["url"]["mediaType"],
"source": validated_data["url"]["href"],
"creation_date": validated_data["published"],
"modification_date": validated_data.get("updated"),
"duration": validated_data["duration"],
"size": validated_data["size"],
"bitrate": validated_data["bitrate"],
"import_status": "finished",
}
return music_models.Upload.objects.update_or_create(
fid=validated_data["id"], defaults=data
)[0]
else:
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"],
"modification_date": validated_data.get("updated"),
"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",
}
return music_models.Upload.objects.create(**data)
def to_representation(self, instance):
track = instance.track
d = {
2018-06-09 13:36:16 +00:00
"type": "Audio",
"id": instance.get_federation_id(),
2018-09-22 12:29:30 +00:00
"library": instance.library.fid,
"name": track.full_name,
2018-06-09 13:36:16 +00:00
"published": instance.creation_date.isoformat(),
2018-09-22 12:29:30 +00:00
"bitrate": instance.bitrate,
"size": instance.size,
"duration": instance.duration,
"url": [
{
"href": utils.full_url(instance.listen_url_no_download),
"type": "Link",
"mediaType": instance.mimetype,
},
{
"type": "Link",
"mediaType": "text/html",
"href": utils.full_url(instance.track.get_absolute_url()),
},
],
2018-09-22 12:29:30 +00:00
"track": TrackSerializer(track, context={"include_ap_context": False}).data,
"to": contexts.AS.Public
if instance.library.privacy_level == "everyone"
else "",
"attributedTo": instance.library.actor.fid,
}
if instance.modification_date:
d["updated"] = instance.modification_date.isoformat()
2018-09-22 12:29:30 +00:00
if self.context.get("include_ap_context", self.parent is None):
2019-07-01 12:00:32 +00:00
d["@context"] = jsonld.get_default_context()
return d
class ActorDeleteSerializer(jsonld.JsonLdSerializer):
fid = serializers.URLField(max_length=500)
class Meta:
jsonld_mapping = {"fid": jsonld.first_id(contexts.AS.object)}
2020-03-11 10:39:55 +00:00
class FlagSerializer(jsonld.JsonLdSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.Flag])
id = serializers.URLField(max_length=500)
object = serializers.URLField(max_length=500)
content = serializers.CharField(required=False, allow_null=True, allow_blank=True)
actor = serializers.URLField(max_length=500)
type = serializers.ListField(
child=TagSerializer(), min_length=0, required=False, allow_null=True
)
class Meta:
jsonld_mapping = {
"object": jsonld.first_id(contexts.AS.object),
"content": jsonld.first_val(contexts.AS.content),
"actor": jsonld.first_id(contexts.AS.actor),
"type": jsonld.raw(contexts.AS.tag),
}
def validate_object(self, v):
try:
return utils.get_object_by_fid(v, local=True)
except ObjectDoesNotExist:
raise serializers.ValidationError(
"Unknown id {} for reported object".format(v)
)
def validate_type(self, tags):
if tags:
for tag in tags:
if tag["name"] in dict(moderation_models.REPORT_TYPES):
return tag["name"]
return "other"
def validate_actor(self, v):
try:
return models.Actor.objects.get(fid=v, domain=self.context["actor"].domain)
except models.Actor.DoesNotExist:
raise serializers.ValidationError("Invalid actor")
def validate(self, data):
validated_data = super().validate(data)
return validated_data
def create(self, validated_data):
kwargs = {
"target": validated_data["object"],
"target_owner": moderation_serializers.get_target_owner(
validated_data["object"]
),
"target_state": moderation_serializers.get_target_state(
validated_data["object"]
),
"type": validated_data.get("type", "other"),
"summary": validated_data.get("content"),
"submitter": validated_data["actor"],
}
report, created = moderation_models.Report.objects.update_or_create(
fid=validated_data["id"], defaults=kwargs,
)
moderation_signals.report_created.send(sender=None, report=report)
return report
def to_representation(self, instance):
d = {
"type": "Flag",
"id": instance.get_federation_id(),
"actor": actors.get_service_actor().fid,
"object": [instance.target.fid],
"content": instance.summary,
"tag": [repr_tag(instance.type)],
}
if self.context.get("include_ap_context", self.parent is None):
d["@context"] = jsonld.get_default_context()
return d
2018-12-27 16:42:43 +00:00
class NodeInfoLinkSerializer(serializers.Serializer):
href = serializers.URLField()
rel = serializers.URLField()
class NodeInfoSerializer(serializers.Serializer):
2018-12-27 19:39:03 +00:00
links = serializers.ListField(child=NodeInfoLinkSerializer(), min_length=1)
2019-12-09 12:59:54 +00:00
class ChannelOutboxSerializer(PaginatedCollectionSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.OrderedCollection])
class Meta:
jsonld_mapping = PAGINATED_COLLECTION_JSONLD_MAPPING
def to_representation(self, channel):
conf = {
"id": channel.actor.outbox_url,
"page_size": 100,
"attributedTo": channel.actor,
"actor": channel.actor,
"items": channel.library.uploads.for_federation()
.order_by("-creation_date")
.filter(track__artist=channel.artist),
"type": "OrderedCollection",
}
r = super().to_representation(conf)
return r
2020-03-25 14:32:10 +00:00
class ChannelUploadSerializer(jsonld.JsonLdSerializer):
id = serializers.URLField(max_length=500)
type = serializers.ChoiceField(choices=[contexts.AS.Audio])
url = LinkListSerializer(keep_mediatype=["audio/*"], min_length=1)
name = TruncatedCharField(truncate_length=music_models.MAX_LENGTHS["TRACK_TITLE"])
published = serializers.DateTimeField(required=False)
duration = serializers.IntegerField(min_value=0, required=False)
position = serializers.IntegerField(min_value=0, allow_null=True, required=False)
disc = serializers.IntegerField(min_value=1, allow_null=True, required=False)
album = serializers.URLField(max_length=500, required=False)
license = serializers.URLField(allow_null=True, required=False)
attributedTo = serializers.URLField(max_length=500, required=False)
2020-03-25 14:32:10 +00:00
copyright = TruncatedCharField(
truncate_length=music_models.MAX_LENGTHS["COPYRIGHT"],
allow_null=True,
required=False,
)
image = ImageSerializer(
allowed_mimetypes=["image/*"],
allow_null=True,
required=False,
allow_empty_mimetype=True,
)
mediaType = serializers.ChoiceField(
choices=common_models.CONTENT_TEXT_SUPPORTED_TYPES,
default="text/html",
required=False,
)
content = TruncatedCharField(
truncate_length=common_models.CONTENT_TEXT_MAX_LENGTH,
required=False,
2020-04-08 11:28:46 +00:00
allow_blank=True,
2020-03-25 14:32:10 +00:00
allow_null=True,
)
tags = serializers.ListField(
child=TagSerializer(), min_length=0, required=False, allow_null=True
)
class Meta:
jsonld_mapping = {
"name": jsonld.first_val(contexts.AS.name),
"url": jsonld.raw(contexts.AS.url),
"published": jsonld.first_val(contexts.AS.published),
"mediaType": jsonld.first_val(contexts.AS.mediaType),
"content": jsonld.first_val(contexts.AS.content),
"duration": jsonld.first_val(contexts.AS.duration),
"album": jsonld.first_id(contexts.FW.album),
"copyright": jsonld.first_val(contexts.FW.copyright),
"disc": jsonld.first_val(contexts.FW.disc),
"license": jsonld.first_id(contexts.FW.license),
"position": jsonld.first_val(contexts.FW.position),
"image": jsonld.first_obj(contexts.AS.image),
"tags": jsonld.raw(contexts.AS.tag),
"attributedTo": jsonld.first_id(contexts.AS.attributedTo),
2020-03-25 14:32:10 +00:00
}
def _validate_album(self, v):
2020-03-25 14:32:10 +00:00
return utils.retrieve_ap_object(
v,
actor=actors.get_service_actor(),
serializer_class=AlbumSerializer,
queryset=music_models.Album.objects.filter(
artist__channel=self.context["channel"]
),
)
def validate(self, data):
if not self.context.get("channel"):
if not data.get("attributedTo"):
raise serializers.ValidationError(
"Missing channel context and no attributedTo available"
)
actor = actors.get_actor(data["attributedTo"])
if not actor.get_channel():
raise serializers.ValidationError("Not a channel")
self.context["channel"] = actor.get_channel()
if data.get("album"):
data["album"] = self._validate_album(data["album"])
2020-03-25 14:32:10 +00:00
validated_data = super().validate(data)
if data.get("content"):
validated_data["description"] = {
"content_type": data["mediaType"],
"text": data["content"],
}
return validated_data
2019-12-09 12:59:54 +00:00
def to_representation(self, upload):
data = {
"id": upload.fid,
"type": "Audio",
2020-03-25 14:32:10 +00:00
"name": upload.track.title,
2019-12-09 12:59:54 +00:00
"attributedTo": upload.library.channel.actor.fid,
"published": upload.creation_date.isoformat(),
"to": contexts.AS.Public
if upload.library.privacy_level == "everyone"
else "",
"url": [
{
"type": "Link",
2020-03-25 14:32:10 +00:00
"mediaType": "text/html",
"href": utils.full_url(upload.track.get_absolute_url()),
2019-12-09 12:59:54 +00:00
},
{
"type": "Link",
2020-03-25 14:32:10 +00:00
"mediaType": upload.mimetype,
"href": utils.full_url(upload.listen_url_no_download),
2019-12-09 12:59:54 +00:00
},
],
}
2020-03-25 14:32:10 +00:00
if upload.track.album:
data["album"] = upload.track.album.fid
if upload.track.local_license:
data["license"] = upload.track.local_license["identifiers"][0]
include_if_not_none(data, upload.duration, "duration")
include_if_not_none(data, upload.track.position, "position")
include_if_not_none(data, upload.track.disc_number, "disc")
include_if_not_none(data, upload.track.copyright, "copyright")
include_if_not_none(data["url"][1], upload.bitrate, "bitrate")
include_if_not_none(data["url"][1], upload.size, "size")
include_content(data, upload.track.description)
2020-03-25 14:32:10 +00:00
include_image(data, upload.track.attachment_cover)
2019-12-09 12:59:54 +00:00
tags = [item.tag.name for item in upload.get_all_tagged_items()]
if tags:
data["tag"] = [repr_tag(name) for name in sorted(set(tags))]
2019-12-09 12:59:54 +00:00
data["summary"] = " ".join(["#{}".format(name) for name in tags])
if self.context.get("include_ap_context", True):
data["@context"] = jsonld.get_default_context()
return data
2020-03-25 14:32:10 +00:00
def update(self, instance, validated_data):
return self.update_or_create(validated_data)
@transaction.atomic
def update_or_create(self, validated_data):
channel = self.context["channel"]
now = timezone.now()
track_defaults = {
"fid": validated_data["id"],
"artist": channel.artist,
"position": validated_data.get("position", 1),
"disc_number": validated_data.get("disc", 1),
"title": validated_data["name"],
"copyright": validated_data.get("copyright"),
"attributed_to": channel.attributed_to,
"album": validated_data.get("album"),
"creation_date": validated_data.get("published", now),
}
if validated_data.get("license"):
track_defaults["license"] = licenses.match(validated_data["license"])
track, created = music_models.Track.objects.update_or_create(
artist__channel=channel, fid=validated_data["id"], defaults=track_defaults
)
if "image" in validated_data:
new_value = self.validated_data["image"]
common_utils.attach_file(
track,
"attachment_cover",
{"url": new_value["url"], "mimetype": new_value.get("mediaType")}
if new_value
else None,
)
common_utils.attach_content(
track, "description", validated_data.get("description")
)
tags = [t["name"] for t in validated_data.get("tags", []) or []]
tags_models.set_tags(track, *tags)
upload_defaults = {
"fid": validated_data["id"],
"track": track,
"library": channel.library,
"creation_date": validated_data.get("published", now),
"duration": validated_data.get("duration"),
"bitrate": validated_data["url"][0].get("bitrate"),
"size": validated_data["url"][0].get("size"),
"mimetype": validated_data["url"][0]["mediaType"],
"source": validated_data["url"][0]["href"],
"import_status": "finished",
}
upload, created = music_models.Upload.objects.update_or_create(
fid=validated_data["id"], defaults=upload_defaults
)
return upload
def create(self, validated_data):
return self.update_or_create(validated_data)
2019-12-09 12:59:54 +00:00
class ChannelCreateUploadSerializer(jsonld.JsonLdSerializer):
object = serializers.DictField()
class Meta:
jsonld_mapping = {
"object": jsonld.first_obj(contexts.AS.object),
}
2019-12-09 12:59:54 +00:00
def to_representation(self, upload):
payload = {
2019-12-09 12:59:54 +00:00
"@context": jsonld.get_default_context(),
"type": self.context.get("type", "Create"),
2020-04-08 11:28:46 +00:00
"id": utils.full_url(
reverse(
"federation:music:uploads-activity", kwargs={"uuid": upload.uuid}
)
),
2019-12-09 12:59:54 +00:00
"actor": upload.library.channel.actor.fid,
"object": ChannelUploadSerializer(
upload, context={"include_ap_context": False}
).data,
}
if self.context.get("activity_id_suffix"):
payload["id"] = os.path.join(
payload["id"], self.context["activity_id_suffix"]
)
return payload
def validate(self, validated_data):
serializer = ChannelUploadSerializer(
data=validated_data["object"], context=self.context, jsonld_expand=False
)
serializer.is_valid(raise_exception=True)
return {"audio_serializer": serializer}
def save(self, **kwargs):
return self.validated_data["audio_serializer"].save(**kwargs)
class DeleteSerializer(jsonld.JsonLdSerializer):
object = serializers.URLField(max_length=500)
type = serializers.ChoiceField(choices=[contexts.AS.Delete])
class Meta:
jsonld_mapping = {"object": jsonld.first_id(contexts.AS.object)}
def validate_object(self, url):
try:
obj = utils.get_object_by_fid(url)
except utils.ObjectDoesNotExist:
raise serializers.ValidationError("No object matching {}".format(url))
if isinstance(obj, music_models.Upload):
obj = obj.track
return obj
def validate(self, validated_data):
if not utils.can_manage(
validated_data["object"].attributed_to, self.context["actor"]
):
raise serializers.ValidationError("You cannot delete this object")
return validated_data