Ensures federation urls can answer to application/ld+json, application/activity+json and application/json requests

This commit is contained in:
Eliot Berriot 2019-03-05 15:15:37 +01:00
parent c9e7eea65b
commit 12c9db3960
19 changed files with 1294 additions and 1001 deletions

View file

@ -9,22 +9,24 @@ from rest_framework import serializers
from funkwhale_api.common import utils as funkwhale_utils
from funkwhale_api.music import models as music_models
from . import activity, models, utils
from . import activity, contexts, jsonld, models, utils
AP_CONTEXT = [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{},
]
AP_CONTEXT = jsonld.get_default_context()
logger = logging.getLogger(__name__)
class LinkSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=["Link"])
class LinkSerializer(jsonld.JsonLdSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.Link])
href = serializers.URLField(max_length=500)
mediaType = serializers.CharField()
class Meta:
jsonld_mapping = {
"href": jsonld.first_id(contexts.AS.href),
"mediaType": jsonld.first_val(contexts.AS.mediaType),
}
def __init__(self, *args, **kwargs):
self.allowed_mimetypes = kwargs.pop("allowed_mimetypes", [])
super().__init__(*args, **kwargs)
@ -45,18 +47,52 @@ class LinkSerializer(serializers.Serializer):
)
class ActorSerializer(serializers.Serializer):
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)}
class ActorSerializer(jsonld.JsonLdSerializer):
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)
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)
summary = serializers.CharField(max_length=None, required=False)
followers = serializers.URLField(max_length=500)
following = serializers.URLField(max_length=500, required=False, allow_null=True)
publicKey = serializers.JSONField(required=False)
publicKey = PublicKeySerializer(required=False)
endpoints = EndpointsSerializer(required=False)
class Meta:
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),
}
def to_representation(self, instance):
ret = {
@ -115,16 +151,19 @@ class ActorSerializer(serializers.Serializer):
kwargs["manually_approves_followers"] = maf
domain = urllib.parse.urlparse(kwargs["fid"]).netloc
kwargs["domain"] = models.Domain.objects.get_or_create(pk=domain)[0]
for endpoint, url in self.initial_data.get("endpoints", {}).items():
for endpoint, url in self.validated_data.get("endpoints", {}).items():
if endpoint == "sharedInbox":
kwargs["shared_inbox_url"] = url
break
try:
kwargs["public_key"] = self.initial_data["publicKey"]["publicKeyPem"]
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)
@ -507,14 +546,26 @@ def get_additional_fields(data):
return additional_fields
class PaginatedCollectionSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=["Collection"])
PAGINATED_COLLECTION_JSONLD_MAPPING = {
"totalItems": jsonld.first_val(contexts.AS.totalItems),
"actor": jsonld.first_id(contexts.AS.actor),
"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])
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)
class Meta:
jsonld_mapping = PAGINATED_COLLECTION_JSONLD_MAPPING
def to_representation(self, conf):
paginator = Paginator(conf["items"], conf.get("page_size", 20))
first = funkwhale_utils.set_query_parameter(conf["id"], page=1)
@ -536,17 +587,30 @@ class PaginatedCollectionSerializer(serializers.Serializer):
class LibrarySerializer(PaginatedCollectionSerializer):
type = serializers.ChoiceField(choices=["Library"])
type = serializers.ChoiceField(
choices=[contexts.AS.Collection, contexts.FW.Library]
)
name = serializers.CharField()
summary = serializers.CharField(allow_blank=True, allow_null=True, required=False)
followers = serializers.URLField(max_length=500)
audience = serializers.ChoiceField(
choices=["", None, "https://www.w3.org/ns/activitystreams#Public"],
choices=["", "./", None, "https://www.w3.org/ns/activitystreams#Public"],
required=False,
allow_null=True,
allow_blank=True,
)
class Meta:
jsonld_mapping = funkwhale_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),
},
)
def to_representation(self, library):
conf = {
"id": library.fid,
@ -559,9 +623,7 @@ class LibrarySerializer(PaginatedCollectionSerializer):
}
r = super().to_representation(conf)
r["audience"] = (
"https://www.w3.org/ns/activitystreams#Public"
if library.privacy_level == "everyone"
else ""
contexts.AS.Public if library.privacy_level == "everyone" else ""
)
r["followers"] = library.followers_url
return r
@ -572,6 +634,7 @@ class LibrarySerializer(PaginatedCollectionSerializer):
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,
@ -580,17 +643,14 @@ class LibrarySerializer(PaginatedCollectionSerializer):
"name": validated_data["name"],
"description": validated_data["summary"],
"followers_url": validated_data["followers"],
"privacy_level": "everyone"
if validated_data["audience"]
== "https://www.w3.org/ns/activitystreams#Public"
else "me",
"privacy_level": privacy[validated_data["audience"]],
},
)
return library
class CollectionPageSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=["CollectionPage"])
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)
@ -601,6 +661,18 @@ class CollectionPageSerializer(serializers.Serializer):
prev = serializers.URLField(max_length=500, required=False)
partOf = serializers.URLField(max_length=500)
class Meta:
jsonld_mapping = {
"totalItems": jsonld.first_val(contexts.AS.totalItems),
"items": jsonld.raw(contexts.AS.items),
"actor": jsonld.first_id(contexts.AS.actor),
"first": jsonld.first_id(contexts.AS.first),
"last": jsonld.first_id(contexts.AS.last),
"next": jsonld.first_id(contexts.AS.next),
"prev": jsonld.first_id(contexts.AS.next),
"partOf": jsonld.first_id(contexts.AS.partOf),
}
def validate_items(self, v):
item_serializer = self.context.get("item_serializer")
if not item_serializer:
@ -654,7 +726,14 @@ class CollectionPageSerializer(serializers.Serializer):
return d
class MusicEntitySerializer(serializers.Serializer):
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),
}
class MusicEntitySerializer(jsonld.JsonLdSerializer):
id = serializers.URLField(max_length=500)
published = serializers.DateTimeField()
musicbrainzId = serializers.UUIDField(allow_null=True, required=False)
@ -662,6 +741,9 @@ class MusicEntitySerializer(serializers.Serializer):
class ArtistSerializer(MusicEntitySerializer):
class Meta:
jsonld_mapping = MUSIC_ENTITY_JSONLD_MAPPING
def to_representation(self, instance):
d = {
"type": "Artist",
@ -683,6 +765,16 @@ class AlbumSerializer(MusicEntitySerializer):
allowed_mimetypes=["image/*"], allow_null=True, required=False
)
class Meta:
jsonld_mapping = funkwhale_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),
},
)
def to_representation(self, instance):
d = {
"type": "Album",
@ -710,22 +802,6 @@ class AlbumSerializer(MusicEntitySerializer):
d["@context"] = AP_CONTEXT
return d
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"),
}
class TrackSerializer(MusicEntitySerializer):
position = serializers.IntegerField(min_value=0, allow_null=True, required=False)
@ -735,6 +811,19 @@ class TrackSerializer(MusicEntitySerializer):
license = serializers.URLField(allow_null=True, required=False)
copyright = serializers.CharField(allow_null=True, required=False)
class Meta:
jsonld_mapping = funkwhale_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),
},
)
def to_representation(self, instance):
d = {
"type": "Track",
@ -773,8 +862,8 @@ class TrackSerializer(MusicEntitySerializer):
return track
class UploadSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=["Audio"])
class UploadSerializer(jsonld.JsonLdSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.Audio])
id = serializers.URLField(max_length=500)
library = serializers.URLField(max_length=500)
url = LinkSerializer(allowed_mimetypes=["audio/*"])
@ -786,6 +875,18 @@ class UploadSerializer(serializers.Serializer):
track = TrackSerializer(required=True)
class Meta:
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:
v["href"]
@ -870,26 +971,6 @@ class UploadSerializer(serializers.Serializer):
return d
class CollectionSerializer(serializers.Serializer):
def to_representation(self, conf):
d = {
"id": conf["id"],
"actor": conf["actor"].fid,
"totalItems": len(conf["items"]),
"type": "Collection",
"items": [
conf["item_serializer"](
i, context={"actor": conf["actor"], "include_ap_context": False}
).data
for i in conf["items"]
],
}
if self.context.get("include_ap_context", True):
d["@context"] = AP_CONTEXT
return d
class NodeInfoLinkSerializer(serializers.Serializer):
href = serializers.URLField()
rel = serializers.URLField()