mirror of
https://code.eliotberriot.com/funkwhale/funkwhale.git
synced 2025-10-03 22:19:16 +02:00
Ensures federation urls can answer to application/ld+json, application/activity+json and application/json requests
This commit is contained in:
parent
c9e7eea65b
commit
12c9db3960
19 changed files with 1294 additions and 1001 deletions
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue