See #170: federation for channels
This commit is contained in:
parent
c3cb8bc390
commit
5a37d9771e
|
@ -18,6 +18,7 @@ class ChannelFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
||||||
library = factory.SubFactory(
|
library = factory.SubFactory(
|
||||||
federation_factories.MusicLibraryFactory,
|
federation_factories.MusicLibraryFactory,
|
||||||
actor=factory.SelfAttribute("..attributed_to"),
|
actor=factory.SelfAttribute("..attributed_to"),
|
||||||
|
privacy_level="everyone",
|
||||||
)
|
)
|
||||||
actor = factory.LazyAttribute(set_actor)
|
actor = factory.LazyAttribute(set_actor)
|
||||||
artist = factory.SubFactory(music_factories.ArtistFactory)
|
artist = factory.SubFactory(music_factories.ArtistFactory)
|
||||||
|
@ -27,6 +28,8 @@ class ChannelFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
||||||
|
|
||||||
class Params:
|
class Params:
|
||||||
local = factory.Trait(
|
local = factory.Trait(
|
||||||
attributed_to__fid=factory.Faker("federation_url", local=True),
|
attributed_to=factory.SubFactory(
|
||||||
|
federation_factories.ActorFactory, local=True
|
||||||
|
),
|
||||||
artist__local=True,
|
artist__local=True,
|
||||||
)
|
)
|
||||||
|
|
|
@ -37,7 +37,7 @@ class ChannelCreateSerializer(serializers.Serializer):
|
||||||
|
|
||||||
channel.library = music_models.Library.objects.create(
|
channel.library = music_models.Library.objects.create(
|
||||||
name=channel.actor.preferred_username,
|
name=channel.actor.preferred_username,
|
||||||
privacy_level="public",
|
privacy_level="everyone",
|
||||||
actor=validated_data["attributed_to"],
|
actor=validated_data["attributed_to"],
|
||||||
)
|
)
|
||||||
channel.save()
|
channel.save()
|
||||||
|
|
|
@ -118,7 +118,7 @@ def should_reject(fid, actor_id=None, payload={}):
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def receive(activity, on_behalf_of):
|
def receive(activity, on_behalf_of, inbox_actor=None):
|
||||||
from . import models
|
from . import models
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from . import tasks
|
from . import tasks
|
||||||
|
@ -131,7 +131,12 @@ def receive(activity, on_behalf_of):
|
||||||
# we ensure the activity has the bare minimum structure before storing
|
# we ensure the activity has the bare minimum structure before storing
|
||||||
# it in our database
|
# it in our database
|
||||||
serializer = serializers.BaseActivitySerializer(
|
serializer = serializers.BaseActivitySerializer(
|
||||||
data=activity, context={"actor": on_behalf_of, "local_recipients": True}
|
data=activity,
|
||||||
|
context={
|
||||||
|
"actor": on_behalf_of,
|
||||||
|
"local_recipients": True,
|
||||||
|
"recipients": [inbox_actor] if inbox_actor else [],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
@ -161,14 +166,19 @@ def receive(activity, on_behalf_of):
|
||||||
|
|
||||||
local_to_recipients = get_actors_from_audience(activity.get("to", []))
|
local_to_recipients = get_actors_from_audience(activity.get("to", []))
|
||||||
local_to_recipients = local_to_recipients.exclude(user=None)
|
local_to_recipients = local_to_recipients.exclude(user=None)
|
||||||
|
local_to_recipients = local_to_recipients.values_list("pk", flat=True)
|
||||||
|
local_to_recipients = list(local_to_recipients)
|
||||||
|
if inbox_actor:
|
||||||
|
local_to_recipients.append(inbox_actor.pk)
|
||||||
|
|
||||||
local_cc_recipients = get_actors_from_audience(activity.get("cc", []))
|
local_cc_recipients = get_actors_from_audience(activity.get("cc", []))
|
||||||
local_cc_recipients = local_cc_recipients.exclude(user=None)
|
local_cc_recipients = local_cc_recipients.exclude(user=None)
|
||||||
|
local_cc_recipients = local_cc_recipients.values_list("pk", flat=True)
|
||||||
|
|
||||||
inbox_items = []
|
inbox_items = []
|
||||||
for recipients, type in [(local_to_recipients, "to"), (local_cc_recipients, "cc")]:
|
for recipients, type in [(local_to_recipients, "to"), (local_cc_recipients, "cc")]:
|
||||||
|
|
||||||
for r in recipients.values_list("pk", flat=True):
|
for r in recipients:
|
||||||
inbox_items.append(models.InboxItem(actor_id=r, type=type, activity=copy))
|
inbox_items.append(models.InboxItem(actor_id=r, type=type, activity=copy))
|
||||||
|
|
||||||
models.InboxItem.objects.bulk_create(inbox_items)
|
models.InboxItem.objects.bulk_create(inbox_items)
|
||||||
|
|
|
@ -86,7 +86,12 @@ class LibraryFollowSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
def serialize_generic_relation(activity, obj):
|
def serialize_generic_relation(activity, obj):
|
||||||
data = {"uuid": obj.uuid, "type": obj._meta.label}
|
data = {"type": obj._meta.label}
|
||||||
|
if data["type"] == "federation.Actor":
|
||||||
|
data["full_username"] = obj.full_username
|
||||||
|
else:
|
||||||
|
data["uuid"] = obj.uuid
|
||||||
|
|
||||||
if data["type"] == "music.Library":
|
if data["type"] == "music.Library":
|
||||||
data["name"] = obj.name
|
data["name"] = obj.name
|
||||||
if data["type"] == "federation.LibraryFollow":
|
if data["type"] == "federation.LibraryFollow":
|
||||||
|
|
|
@ -52,9 +52,13 @@ class SignatureAuthentication(authentication.BaseAuthentication):
|
||||||
actor = actors.get_actor(actor_url)
|
actor = actors.get_actor(actor_url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Discarding HTTP request from blocked actor/domain %s", actor_url
|
"Discarding HTTP request from blocked actor/domain %s, %s",
|
||||||
|
actor_url,
|
||||||
|
str(e),
|
||||||
|
)
|
||||||
|
raise rest_exceptions.AuthenticationFailed(
|
||||||
|
"Cannot fetch remote actor to authenticate signature"
|
||||||
)
|
)
|
||||||
raise rest_exceptions.AuthenticationFailed(str(e))
|
|
||||||
|
|
||||||
if not actor.public_key:
|
if not actor.public_key:
|
||||||
raise rest_exceptions.AuthenticationFailed("No public key found")
|
raise rest_exceptions.AuthenticationFailed("No public key found")
|
||||||
|
|
|
@ -214,14 +214,18 @@ def get_ids(v):
|
||||||
|
|
||||||
|
|
||||||
def get_default_context():
|
def get_default_context():
|
||||||
return ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {}]
|
return [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
{"manuallyApprovesFollowers": "as:manuallyApprovesFollowers"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_default_context_fw():
|
def get_default_context_fw():
|
||||||
return [
|
return [
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
"https://w3id.org/security/v1",
|
"https://w3id.org/security/v1",
|
||||||
{},
|
{"manuallyApprovesFollowers": "as:manuallyApprovesFollowers"},
|
||||||
"https://funkwhale.audio/ns",
|
"https://funkwhale.audio/ns",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 2.2.7 on 2019-12-04 15:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('federation', '0021_auto_20191029_1257'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='actor',
|
||||||
|
name='inbox_url',
|
||||||
|
field=models.URLField(blank=True, max_length=500, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='actor',
|
||||||
|
name='outbox_url',
|
||||||
|
field=models.URLField(blank=True, max_length=500, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -180,8 +180,8 @@ class Actor(models.Model):
|
||||||
|
|
||||||
fid = models.URLField(unique=True, max_length=500, db_index=True)
|
fid = models.URLField(unique=True, max_length=500, db_index=True)
|
||||||
url = models.URLField(max_length=500, null=True, blank=True)
|
url = models.URLField(max_length=500, null=True, blank=True)
|
||||||
outbox_url = models.URLField(max_length=500)
|
outbox_url = models.URLField(max_length=500, null=True, blank=True)
|
||||||
inbox_url = models.URLField(max_length=500)
|
inbox_url = models.URLField(max_length=500, null=True, blank=True)
|
||||||
following_url = models.URLField(max_length=500, null=True, blank=True)
|
following_url = models.URLField(max_length=500, null=True, blank=True)
|
||||||
followers_url = models.URLField(max_length=500, null=True, blank=True)
|
followers_url = models.URLField(max_length=500, null=True, blank=True)
|
||||||
shared_inbox_url = models.URLField(max_length=500, null=True, blank=True)
|
shared_inbox_url = models.URLField(max_length=500, null=True, blank=True)
|
||||||
|
|
|
@ -6,6 +6,7 @@ def get_ap_renderers():
|
||||||
("APActivity", "application/activity+json"),
|
("APActivity", "application/activity+json"),
|
||||||
("APLD", "application/ld+json"),
|
("APLD", "application/ld+json"),
|
||||||
("APJSON", "application/json"),
|
("APJSON", "application/json"),
|
||||||
|
("HTML", "text/html"),
|
||||||
]
|
]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -131,21 +131,28 @@ def outbox_follow(context):
|
||||||
@outbox.register({"type": "Create", "object.type": "Audio"})
|
@outbox.register({"type": "Create", "object.type": "Audio"})
|
||||||
def outbox_create_audio(context):
|
def outbox_create_audio(context):
|
||||||
upload = context["upload"]
|
upload = context["upload"]
|
||||||
|
channel = upload.library.get_channel()
|
||||||
|
upload_serializer = (
|
||||||
|
serializers.ChannelUploadSerializer if channel else serializers.UploadSerializer
|
||||||
|
)
|
||||||
|
followers_target = channel.actor if channel else upload.library
|
||||||
|
actor = channel.actor if channel else upload.library.actor
|
||||||
|
|
||||||
serializer = serializers.ActivitySerializer(
|
serializer = serializers.ActivitySerializer(
|
||||||
{
|
{
|
||||||
"type": "Create",
|
"type": "Create",
|
||||||
"actor": upload.library.actor.fid,
|
"actor": actor.fid,
|
||||||
"object": serializers.UploadSerializer(upload).data,
|
"object": upload_serializer(upload).data,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
yield {
|
yield {
|
||||||
"type": "Create",
|
"type": "Create",
|
||||||
"actor": upload.library.actor,
|
"actor": actor,
|
||||||
"payload": with_recipients(
|
"payload": with_recipients(
|
||||||
serializer.data, to=[{"type": "followers", "target": upload.library}]
|
serializer.data, to=[{"type": "followers", "target": followers_target}]
|
||||||
),
|
),
|
||||||
"object": upload,
|
"object": upload,
|
||||||
"target": upload.library,
|
"target": None if channel else upload.library,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -258,6 +265,9 @@ def inbox_delete_audio(payload, context):
|
||||||
def outbox_delete_audio(context):
|
def outbox_delete_audio(context):
|
||||||
uploads = context["uploads"]
|
uploads = context["uploads"]
|
||||||
library = uploads[0].library
|
library = uploads[0].library
|
||||||
|
channel = library.get_channel()
|
||||||
|
followers_target = channel.actor if channel else library
|
||||||
|
actor = channel.actor if channel else library.actor
|
||||||
serializer = serializers.ActivitySerializer(
|
serializer = serializers.ActivitySerializer(
|
||||||
{
|
{
|
||||||
"type": "Delete",
|
"type": "Delete",
|
||||||
|
@ -266,9 +276,9 @@ def outbox_delete_audio(context):
|
||||||
)
|
)
|
||||||
yield {
|
yield {
|
||||||
"type": "Delete",
|
"type": "Delete",
|
||||||
"actor": library.actor,
|
"actor": actor,
|
||||||
"payload": with_recipients(
|
"payload": with_recipients(
|
||||||
serializer.data, to=[{"type": "followers", "target": library}]
|
serializer.data, to=[{"type": "followers", "target": followers_target}]
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,8 +68,8 @@ class PublicKeySerializer(jsonld.JsonLdSerializer):
|
||||||
|
|
||||||
class ActorSerializer(jsonld.JsonLdSerializer):
|
class ActorSerializer(jsonld.JsonLdSerializer):
|
||||||
id = serializers.URLField(max_length=500)
|
id = serializers.URLField(max_length=500)
|
||||||
outbox = serializers.URLField(max_length=500)
|
outbox = serializers.URLField(max_length=500, required=False)
|
||||||
inbox = serializers.URLField(max_length=500)
|
inbox = serializers.URLField(max_length=500, required=False)
|
||||||
type = serializers.ChoiceField(
|
type = serializers.ChoiceField(
|
||||||
choices=[getattr(contexts.AS, c[0]) for c in models.TYPE_CHOICES]
|
choices=[getattr(contexts.AS, c[0]) for c in models.TYPE_CHOICES]
|
||||||
)
|
)
|
||||||
|
@ -77,7 +77,7 @@ class ActorSerializer(jsonld.JsonLdSerializer):
|
||||||
manuallyApprovesFollowers = serializers.NullBooleanField(required=False)
|
manuallyApprovesFollowers = serializers.NullBooleanField(required=False)
|
||||||
name = serializers.CharField(required=False, max_length=200)
|
name = serializers.CharField(required=False, max_length=200)
|
||||||
summary = serializers.CharField(max_length=None, required=False)
|
summary = serializers.CharField(max_length=None, required=False)
|
||||||
followers = serializers.URLField(max_length=500)
|
followers = serializers.URLField(max_length=500, required=False)
|
||||||
following = serializers.URLField(max_length=500, required=False, allow_null=True)
|
following = serializers.URLField(max_length=500, required=False, allow_null=True)
|
||||||
publicKey = PublicKeySerializer(required=False)
|
publicKey = PublicKeySerializer(required=False)
|
||||||
endpoints = EndpointsSerializer(required=False)
|
endpoints = EndpointsSerializer(required=False)
|
||||||
|
@ -142,8 +142,8 @@ class ActorSerializer(jsonld.JsonLdSerializer):
|
||||||
def prepare_missing_fields(self):
|
def prepare_missing_fields(self):
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"fid": self.validated_data["id"],
|
"fid": self.validated_data["id"],
|
||||||
"outbox_url": self.validated_data["outbox"],
|
"outbox_url": self.validated_data.get("outbox"),
|
||||||
"inbox_url": self.validated_data["inbox"],
|
"inbox_url": self.validated_data.get("inbox"),
|
||||||
"following_url": self.validated_data.get("following"),
|
"following_url": self.validated_data.get("following"),
|
||||||
"followers_url": self.validated_data.get("followers"),
|
"followers_url": self.validated_data.get("followers"),
|
||||||
"summary": self.validated_data.get("summary"),
|
"summary": self.validated_data.get("summary"),
|
||||||
|
@ -244,7 +244,7 @@ class BaseActivitySerializer(serializers.Serializer):
|
||||||
to = payload.get("to", [])
|
to = payload.get("to", [])
|
||||||
cc = payload.get("cc", [])
|
cc = payload.get("cc", [])
|
||||||
|
|
||||||
if not to and not cc:
|
if not to and not cc and not self.context.get("recipients"):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
"We cannot handle an activity with no recipient"
|
"We cannot handle an activity with no recipient"
|
||||||
)
|
)
|
||||||
|
@ -801,6 +801,10 @@ class TagSerializer(jsonld.JsonLdSerializer):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def repr_tag(tag_name):
|
||||||
|
return {"type": "Hashtag", "name": "#{}".format(tag_name)}
|
||||||
|
|
||||||
|
|
||||||
class MusicEntitySerializer(jsonld.JsonLdSerializer):
|
class MusicEntitySerializer(jsonld.JsonLdSerializer):
|
||||||
id = serializers.URLField(max_length=500)
|
id = serializers.URLField(max_length=500)
|
||||||
published = serializers.DateTimeField()
|
published = serializers.DateTimeField()
|
||||||
|
@ -831,7 +835,7 @@ class MusicEntitySerializer(jsonld.JsonLdSerializer):
|
||||||
|
|
||||||
def get_tags_repr(self, instance):
|
def get_tags_repr(self, instance):
|
||||||
return [
|
return [
|
||||||
{"type": "Hashtag", "name": "#{}".format(item.tag.name)}
|
repr_tag(item.tag.name)
|
||||||
for item in sorted(instance.tagged_items.all(), key=lambda i: i.tag.name)
|
for item in sorted(instance.tagged_items.all(), key=lambda i: i.tag.name)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1182,3 +1186,71 @@ class NodeInfoLinkSerializer(serializers.Serializer):
|
||||||
|
|
||||||
class NodeInfoSerializer(serializers.Serializer):
|
class NodeInfoSerializer(serializers.Serializer):
|
||||||
links = serializers.ListField(child=NodeInfoLinkSerializer(), min_length=1)
|
links = serializers.ListField(child=NodeInfoLinkSerializer(), min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelUploadSerializer(serializers.Serializer):
|
||||||
|
def to_representation(self, upload):
|
||||||
|
data = {
|
||||||
|
"id": upload.fid,
|
||||||
|
"type": "Audio",
|
||||||
|
"name": upload.track.full_name,
|
||||||
|
"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",
|
||||||
|
"mimeType": upload.mimetype,
|
||||||
|
"href": utils.full_url(upload.listen_url),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Link",
|
||||||
|
"mimeType": "text/html",
|
||||||
|
"href": utils.full_url(upload.track.get_absolute_url()),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
tags = [item.tag.name for item in upload.get_all_tagged_items()]
|
||||||
|
if tags:
|
||||||
|
data["tag"] = [repr_tag(name) for name in tags]
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelCreateUploadSerializer(serializers.Serializer):
|
||||||
|
def to_representation(self, upload):
|
||||||
|
return {
|
||||||
|
"@context": jsonld.get_default_context(),
|
||||||
|
"type": "Create",
|
||||||
|
"actor": upload.library.channel.actor.fid,
|
||||||
|
"object": ChannelUploadSerializer(
|
||||||
|
upload, context={"include_ap_context": False}
|
||||||
|
).data,
|
||||||
|
}
|
||||||
|
|
|
@ -55,18 +55,57 @@ class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericV
|
||||||
|
|
||||||
@action(methods=["get", "post"], detail=True)
|
@action(methods=["get", "post"], detail=True)
|
||||||
def inbox(self, request, *args, **kwargs):
|
def inbox(self, request, *args, **kwargs):
|
||||||
|
inbox_actor = self.get_object()
|
||||||
if request.method.lower() == "post" and request.actor is None:
|
if request.method.lower() == "post" and request.actor is None:
|
||||||
raise exceptions.AuthenticationFailed(
|
raise exceptions.AuthenticationFailed(
|
||||||
"You need a valid signature to send an activity"
|
"You need a valid signature to send an activity"
|
||||||
)
|
)
|
||||||
if request.method.lower() == "post":
|
if request.method.lower() == "post":
|
||||||
activity.receive(activity=request.data, on_behalf_of=request.actor)
|
activity.receive(
|
||||||
|
activity=request.data,
|
||||||
|
on_behalf_of=request.actor,
|
||||||
|
inbox_actor=inbox_actor,
|
||||||
|
)
|
||||||
return response.Response({}, status=200)
|
return response.Response({}, status=200)
|
||||||
|
|
||||||
@action(methods=["get", "post"], detail=True)
|
@action(methods=["get", "post"], detail=True)
|
||||||
def outbox(self, request, *args, **kwargs):
|
def outbox(self, request, *args, **kwargs):
|
||||||
|
actor = self.get_object()
|
||||||
|
channel = actor.channel
|
||||||
|
if channel:
|
||||||
|
return self.get_channel_outbox_response(request, channel)
|
||||||
return response.Response({}, status=200)
|
return response.Response({}, status=200)
|
||||||
|
|
||||||
|
def get_channel_outbox_response(self, request, channel):
|
||||||
|
conf = {
|
||||||
|
"id": channel.actor.outbox_url,
|
||||||
|
"actor": channel.actor,
|
||||||
|
"items": channel.library.uploads.for_federation()
|
||||||
|
.order_by("-creation_date")
|
||||||
|
.prefetch_related("library__channel__actor", "track__artist"),
|
||||||
|
"item_serializer": serializers.ChannelCreateUploadSerializer,
|
||||||
|
}
|
||||||
|
page = request.GET.get("page")
|
||||||
|
if page is None:
|
||||||
|
serializer = serializers.ChannelOutboxSerializer(channel)
|
||||||
|
data = serializer.data
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
page_number = int(page)
|
||||||
|
except Exception:
|
||||||
|
return response.Response({"page": ["Invalid page number"]}, status=400)
|
||||||
|
conf["page_size"] = preferences.get("federation__collection_page_size")
|
||||||
|
p = paginator.Paginator(conf["items"], conf["page_size"])
|
||||||
|
try:
|
||||||
|
page = p.page(page_number)
|
||||||
|
conf["page"] = page
|
||||||
|
serializer = serializers.CollectionPageSerializer(conf)
|
||||||
|
data = serializer.data
|
||||||
|
except paginator.EmptyPage:
|
||||||
|
return response.Response(status=404)
|
||||||
|
|
||||||
|
return response.Response(data)
|
||||||
|
|
||||||
@action(methods=["get"], detail=True)
|
@action(methods=["get"], detail=True)
|
||||||
def followers(self, request, *args, **kwargs):
|
def followers(self, request, *args, **kwargs):
|
||||||
self.get_object()
|
self.get_object()
|
||||||
|
@ -251,6 +290,11 @@ class MusicUploadViewSet(
|
||||||
actor = music_utils.get_actor_from_request(self.request)
|
actor = music_utils.get_actor_from_request(self.request)
|
||||||
return queryset.playable_by(actor)
|
return queryset.playable_by(actor)
|
||||||
|
|
||||||
|
def get_serializer(self, obj):
|
||||||
|
if obj.library.get_channel():
|
||||||
|
return serializers.ChannelUploadSerializer(obj)
|
||||||
|
return super().get_serializer(obj)
|
||||||
|
|
||||||
|
|
||||||
class MusicArtistViewSet(
|
class MusicArtistViewSet(
|
||||||
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||||
|
|
|
@ -634,7 +634,10 @@ class UploadQuerySet(common_models.NullsLastQuerySet):
|
||||||
return self.exclude(library__in=libraries, import_status="finished")
|
return self.exclude(library__in=libraries, import_status="finished")
|
||||||
|
|
||||||
def local(self, include=True):
|
def local(self, include=True):
|
||||||
return self.exclude(library__actor__user__isnull=include)
|
query = models.Q(library__actor__domain_id=settings.FEDERATION_HOSTNAME)
|
||||||
|
if not include:
|
||||||
|
query = ~query
|
||||||
|
return self.filter(query)
|
||||||
|
|
||||||
def for_federation(self):
|
def for_federation(self):
|
||||||
return self.filter(import_status="finished", mimetype__startswith="audio/")
|
return self.filter(import_status="finished", mimetype__startswith="audio/")
|
||||||
|
@ -904,6 +907,14 @@ class Upload(models.Model):
|
||||||
# external storage
|
# external storage
|
||||||
return self.audio_file.name
|
return self.audio_file.name
|
||||||
|
|
||||||
|
def get_all_tagged_items(self):
|
||||||
|
track_tags = self.track.tagged_items.all()
|
||||||
|
album_tags = self.track.album.tagged_items.all()
|
||||||
|
artist_tags = self.track.artist.tagged_items.all()
|
||||||
|
|
||||||
|
items = (track_tags | album_tags | artist_tags).order_by("tag__name")
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
MIMETYPE_CHOICES = [(mt, ext) for ext, mt in utils.AUDIO_EXTENSIONS_AND_MIMETYPE]
|
MIMETYPE_CHOICES = [(mt, ext) for ext, mt in utils.AUDIO_EXTENSIONS_AND_MIMETYPE]
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ from django.conf import settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from funkwhale_api.common import preferences
|
||||||
from funkwhale_api.common import utils
|
from funkwhale_api.common import utils
|
||||||
from funkwhale_api.playlists import models as playlists_models
|
from funkwhale_api.playlists import models as playlists_models
|
||||||
|
|
||||||
|
@ -65,8 +66,9 @@ def library_track(request, pk):
|
||||||
"content": obj.album.attachment_cover.download_url_medium_square_crop,
|
"content": obj.album.attachment_cover.download_url_medium_square_crop,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
playable_uploads = obj.uploads.playable_by(None).order_by("id")
|
||||||
if obj.uploads.playable_by(None).exists():
|
upload = playable_uploads.first()
|
||||||
|
if upload:
|
||||||
metas.append(
|
metas.append(
|
||||||
{
|
{
|
||||||
"tag": "meta",
|
"tag": "meta",
|
||||||
|
@ -74,7 +76,15 @@ def library_track(request, pk):
|
||||||
"content": utils.join_url(settings.FUNKWHALE_URL, obj.listen_url),
|
"content": utils.join_url(settings.FUNKWHALE_URL, obj.listen_url),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if preferences.get("federation__enabled"):
|
||||||
|
metas.append(
|
||||||
|
{
|
||||||
|
"tag": "link",
|
||||||
|
"rel": "alternate",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": upload.fid,
|
||||||
|
}
|
||||||
|
)
|
||||||
metas.append(
|
metas.append(
|
||||||
{
|
{
|
||||||
"tag": "link",
|
"tag": "link",
|
||||||
|
@ -133,6 +143,15 @@ def library_album(request, pk):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if preferences.get("federation__enabled"):
|
||||||
|
metas.append(
|
||||||
|
{
|
||||||
|
"tag": "link",
|
||||||
|
"rel": "alternate",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": obj.fid,
|
||||||
|
}
|
||||||
|
)
|
||||||
if models.Upload.objects.filter(track__album=obj).playable_by(None).exists():
|
if models.Upload.objects.filter(track__album=obj).playable_by(None).exists():
|
||||||
metas.append(
|
metas.append(
|
||||||
{
|
{
|
||||||
|
@ -179,6 +198,16 @@ def library_artist(request, pk):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if preferences.get("federation__enabled"):
|
||||||
|
metas.append(
|
||||||
|
{
|
||||||
|
"tag": "link",
|
||||||
|
"rel": "alternate",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": obj.fid,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
models.Upload.objects.filter(Q(track__artist=obj) | Q(track__album__artist=obj))
|
models.Upload.objects.filter(Q(track__artist=obj) | Q(track__album__artist=obj))
|
||||||
.playable_by(None)
|
.playable_by(None)
|
||||||
|
|
|
@ -9,6 +9,7 @@ django-debug-toolbar>=1.11,<1.12
|
||||||
|
|
||||||
# improved REPL
|
# improved REPL
|
||||||
ipdb==0.11
|
ipdb==0.11
|
||||||
|
prompt_toolkit<3
|
||||||
black
|
black
|
||||||
profiling
|
profiling
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ def test_channel_serializer_create(factories):
|
||||||
assert channel.actor.summary == data["summary"]
|
assert channel.actor.summary == data["summary"]
|
||||||
assert channel.actor.preferred_username == data["username"]
|
assert channel.actor.preferred_username == data["username"]
|
||||||
assert channel.actor.name == data["name"]
|
assert channel.actor.name == data["name"]
|
||||||
assert channel.library.privacy_level == "public"
|
assert channel.library.privacy_level == "everyone"
|
||||||
assert channel.library.actor == attributed_to
|
assert channel.library.actor == attributed_to
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ def test_channel_create(logged_in_api_client):
|
||||||
assert channel.attributed_to == actor
|
assert channel.attributed_to == actor
|
||||||
assert channel.actor.summary == data["summary"]
|
assert channel.actor.summary == data["summary"]
|
||||||
assert channel.actor.preferred_username == data["username"]
|
assert channel.actor.preferred_username == data["username"]
|
||||||
assert channel.library.privacy_level == "public"
|
assert channel.library.privacy_level == "everyone"
|
||||||
assert channel.library.actor == actor
|
assert channel.library.actor == actor
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,14 @@ from funkwhale_api.federation import (
|
||||||
def test_receive_validates_basic_attributes_and_stores_activity(
|
def test_receive_validates_basic_attributes_and_stores_activity(
|
||||||
mrf_inbox_registry, factories, now, mocker
|
mrf_inbox_registry, factories, now, mocker
|
||||||
):
|
):
|
||||||
|
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
activity.InboxRouter, "get_matching_handlers", return_value=True
|
activity.InboxRouter, "get_matching_handlers", return_value=True
|
||||||
)
|
)
|
||||||
mrf_inbox_registry_apply = mocker.spy(mrf_inbox_registry, "apply")
|
mrf_inbox_registry_apply = mocker.spy(mrf_inbox_registry, "apply")
|
||||||
|
serializer_init = mocker.spy(serializers.BaseActivitySerializer, "__init__")
|
||||||
mocked_dispatch = mocker.patch("funkwhale_api.common.utils.on_commit")
|
mocked_dispatch = mocker.patch("funkwhale_api.common.utils.on_commit")
|
||||||
|
inbox_actor = factories["federation.Actor"]()
|
||||||
local_to_actor = factories["users.User"]().create_actor()
|
local_to_actor = factories["users.User"]().create_actor()
|
||||||
local_cc_actor = factories["users.User"]().create_actor()
|
local_cc_actor = factories["users.User"]().create_actor()
|
||||||
remote_actor = factories["federation.Actor"]()
|
remote_actor = factories["federation.Actor"]()
|
||||||
|
@ -33,7 +36,9 @@ def test_receive_validates_basic_attributes_and_stores_activity(
|
||||||
"cc": [local_cc_actor.fid, activity.PUBLIC_ADDRESS],
|
"cc": [local_cc_actor.fid, activity.PUBLIC_ADDRESS],
|
||||||
}
|
}
|
||||||
|
|
||||||
copy = activity.receive(activity=a, on_behalf_of=remote_actor)
|
copy = activity.receive(
|
||||||
|
activity=a, on_behalf_of=remote_actor, inbox_actor=inbox_actor
|
||||||
|
)
|
||||||
mrf_inbox_registry_apply.assert_called_once_with(a, sender_id=a["actor"])
|
mrf_inbox_registry_apply.assert_called_once_with(a, sender_id=a["actor"])
|
||||||
|
|
||||||
assert copy.payload == a
|
assert copy.payload == a
|
||||||
|
@ -45,13 +50,24 @@ def test_receive_validates_basic_attributes_and_stores_activity(
|
||||||
tasks.dispatch_inbox.delay, activity_id=copy.pk
|
tasks.dispatch_inbox.delay, activity_id=copy.pk
|
||||||
)
|
)
|
||||||
|
|
||||||
assert models.InboxItem.objects.count() == 2
|
assert models.InboxItem.objects.count() == 3
|
||||||
for actor, t in [(local_to_actor, "to"), (local_cc_actor, "cc")]:
|
for actor, t in [
|
||||||
|
(local_to_actor, "to"),
|
||||||
|
(inbox_actor, "to"),
|
||||||
|
(local_cc_actor, "cc"),
|
||||||
|
]:
|
||||||
ii = models.InboxItem.objects.get(actor=actor)
|
ii = models.InboxItem.objects.get(actor=actor)
|
||||||
assert ii.type == t
|
assert ii.type == t
|
||||||
assert ii.activity == copy
|
assert ii.activity == copy
|
||||||
assert ii.is_read is False
|
assert ii.is_read is False
|
||||||
|
|
||||||
|
assert serializer_init.call_args[1]["context"] == {
|
||||||
|
"actor": remote_actor,
|
||||||
|
"local_recipients": True,
|
||||||
|
"recipients": [inbox_actor],
|
||||||
|
}
|
||||||
|
assert serializer_init.call_args[1]["data"] == a
|
||||||
|
|
||||||
|
|
||||||
def test_receive_uses_mrf_returned_payload(mrf_inbox_registry, factories, now, mocker):
|
def test_receive_uses_mrf_returned_payload(mrf_inbox_registry, factories, now, mocker):
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
|
|
|
@ -85,3 +85,29 @@ def test_manage_upload_action_read(factories):
|
||||||
s.handle_read(ii.__class__.objects.all())
|
s.handle_read(ii.__class__.objects.all())
|
||||||
|
|
||||||
assert ii.__class__.objects.filter(is_read=False).count() == 0
|
assert ii.__class__.objects.filter(is_read=False).count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"factory_name, factory_kwargs, expected",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"federation.Actor",
|
||||||
|
{"preferred_username": "hello", "domain__name": "world"},
|
||||||
|
{"full_username": "hello@world"},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"music.Library",
|
||||||
|
{"name": "hello", "uuid": "ad1ee1f7-589c-4abe-b303-e4fe7a889260"},
|
||||||
|
{"uuid": "ad1ee1f7-589c-4abe-b303-e4fe7a889260", "name": "hello"},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"federation.LibraryFollow",
|
||||||
|
{"approved": False, "uuid": "ad1ee1f7-589c-4abe-b303-e4fe7a889260"},
|
||||||
|
{"uuid": "ad1ee1f7-589c-4abe-b303-e4fe7a889260", "approved": False},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_serialize_generic_relation(factory_name, factory_kwargs, expected, factories):
|
||||||
|
obj = factories[factory_name](**factory_kwargs)
|
||||||
|
expected["type"] = factory_name
|
||||||
|
assert api_serializers.serialize_generic_relation({}, obj) == expected
|
||||||
|
|
|
@ -261,6 +261,26 @@ def test_outbox_create_audio(factories, mocker):
|
||||||
assert activity["object"] == upload
|
assert activity["object"] == upload
|
||||||
|
|
||||||
|
|
||||||
|
def test_outbox_create_audio_channel(factories, mocker):
|
||||||
|
channel = factories["audio.Channel"]()
|
||||||
|
upload = factories["music.Upload"](library=channel.library)
|
||||||
|
activity = list(routes.outbox_create_audio({"upload": upload}))[0]
|
||||||
|
serializer = serializers.ActivitySerializer(
|
||||||
|
{
|
||||||
|
"type": "Create",
|
||||||
|
"object": serializers.ChannelUploadSerializer(upload).data,
|
||||||
|
"actor": channel.actor.fid,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expected = serializer.data
|
||||||
|
expected["to"] = [{"type": "followers", "target": upload.library.channel.actor}]
|
||||||
|
|
||||||
|
assert dict(activity["payload"]) == dict(expected)
|
||||||
|
assert activity["actor"] == channel.actor
|
||||||
|
assert activity["target"] is None
|
||||||
|
assert activity["object"] == upload
|
||||||
|
|
||||||
|
|
||||||
def test_inbox_create_audio(factories, mocker):
|
def test_inbox_create_audio(factories, mocker):
|
||||||
activity = factories["federation.Activity"]()
|
activity = factories["federation.Activity"]()
|
||||||
upload = factories["music.Upload"](bitrate=42, duration=55)
|
upload = factories["music.Upload"](bitrate=42, duration=55)
|
||||||
|
@ -442,6 +462,20 @@ def test_outbox_delete_audio(factories):
|
||||||
assert activity["actor"] == upload.library.actor
|
assert activity["actor"] == upload.library.actor
|
||||||
|
|
||||||
|
|
||||||
|
def test_outbox_delete_audio_channel(factories):
|
||||||
|
channel = factories["audio.Channel"]()
|
||||||
|
upload = factories["music.Upload"](library=channel.library)
|
||||||
|
activity = list(routes.outbox_delete_audio({"uploads": [upload]}))[0]
|
||||||
|
expected = serializers.ActivitySerializer(
|
||||||
|
{"type": "Delete", "object": {"type": "Audio", "id": [upload.fid]}}
|
||||||
|
).data
|
||||||
|
|
||||||
|
expected["to"] = [{"type": "followers", "target": channel.actor}]
|
||||||
|
|
||||||
|
assert dict(activity["payload"]) == dict(expected)
|
||||||
|
assert activity["actor"] == channel.actor
|
||||||
|
|
||||||
|
|
||||||
def test_inbox_delete_follow_library(factories):
|
def test_inbox_delete_follow_library(factories):
|
||||||
local_actor = factories["users.User"]().create_actor()
|
local_actor = factories["users.User"]().create_actor()
|
||||||
remote_actor = factories["federation.Actor"]()
|
remote_actor = factories["federation.Actor"]()
|
||||||
|
|
|
@ -1022,6 +1022,12 @@ def test_activity_serializer_validate_recipients_empty(db):
|
||||||
s.validate_recipients({"cc": []})
|
s.validate_recipients({"cc": []})
|
||||||
|
|
||||||
|
|
||||||
|
def test_activity_serializer_validate_recipients_context(db):
|
||||||
|
s = serializers.BaseActivitySerializer(context={"recipients": ["dummy"]})
|
||||||
|
|
||||||
|
assert s.validate_recipients({}) is None
|
||||||
|
|
||||||
|
|
||||||
def test_track_serializer_update_license(factories):
|
def test_track_serializer_update_license(factories):
|
||||||
licenses.load(licenses.LICENSES)
|
licenses.load(licenses.LICENSES)
|
||||||
|
|
||||||
|
@ -1033,3 +1039,93 @@ def test_track_serializer_update_license(factories):
|
||||||
obj.refresh_from_db()
|
obj.refresh_from_db()
|
||||||
|
|
||||||
assert obj.license_id == "cc-by-2.0"
|
assert obj.license_id == "cc-by-2.0"
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_actor_outbox_serializer(factories):
|
||||||
|
channel = factories["audio.Channel"]()
|
||||||
|
uploads = factories["music.Upload"].create_batch(
|
||||||
|
5,
|
||||||
|
track__artist=channel.artist,
|
||||||
|
library=channel.library,
|
||||||
|
import_status="finished",
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"@context": jsonld.get_default_context(),
|
||||||
|
"type": "OrderedCollection",
|
||||||
|
"id": channel.actor.outbox_url,
|
||||||
|
"actor": channel.actor.fid,
|
||||||
|
"attributedTo": channel.actor.fid,
|
||||||
|
"totalItems": len(uploads),
|
||||||
|
"first": channel.actor.outbox_url + "?page=1",
|
||||||
|
"last": channel.actor.outbox_url + "?page=1",
|
||||||
|
"current": channel.actor.outbox_url + "?page=1",
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = serializers.ChannelOutboxSerializer(channel)
|
||||||
|
|
||||||
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_upload_serializer(factories):
|
||||||
|
channel = factories["audio.Channel"]()
|
||||||
|
upload = factories["music.Upload"](
|
||||||
|
playable=True,
|
||||||
|
library=channel.library,
|
||||||
|
import_status="finished",
|
||||||
|
track__set_tags=["Punk"],
|
||||||
|
track__album__set_tags=["Rock"],
|
||||||
|
track__artist__set_tags=["Indie"],
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"@context": jsonld.get_default_context(),
|
||||||
|
"type": "Audio",
|
||||||
|
"id": upload.fid,
|
||||||
|
"name": upload.track.full_name,
|
||||||
|
"summary": "#Indie #Punk #Rock",
|
||||||
|
"attributedTo": channel.actor.fid,
|
||||||
|
"published": upload.creation_date.isoformat(),
|
||||||
|
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||||
|
"url": [
|
||||||
|
{
|
||||||
|
"type": "Link",
|
||||||
|
"mimeType": upload.mimetype,
|
||||||
|
"href": utils.full_url(upload.listen_url),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Link",
|
||||||
|
"mimeType": "text/html",
|
||||||
|
"href": utils.full_url(upload.track.get_absolute_url()),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"tag": [
|
||||||
|
{"type": "Hashtag", "name": "#Indie"},
|
||||||
|
{"type": "Hashtag", "name": "#Punk"},
|
||||||
|
{"type": "Hashtag", "name": "#Rock"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = serializers.ChannelUploadSerializer(upload)
|
||||||
|
|
||||||
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_create_upload_serializer(factories):
|
||||||
|
channel = factories["audio.Channel"]()
|
||||||
|
upload = factories["music.Upload"](
|
||||||
|
playable=True, library=channel.library, import_status="finished"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"@context": jsonld.get_default_context(),
|
||||||
|
"type": "Create",
|
||||||
|
"actor": upload.library.channel.actor.fid,
|
||||||
|
"object": serializers.ChannelUploadSerializer(
|
||||||
|
upload, context={"include_ap_context": False}
|
||||||
|
).data,
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = serializers.ChannelCreateUploadSerializer(upload)
|
||||||
|
|
||||||
|
assert serializer.data == expected
|
||||||
|
|
|
@ -103,7 +103,9 @@ def test_local_actor_inbox_post(factories, api_client, mocker, authenticated_act
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
patched_receive.assert_called_once_with(
|
patched_receive.assert_called_once_with(
|
||||||
activity={"hello": "world"}, on_behalf_of=authenticated_actor
|
activity={"hello": "world"},
|
||||||
|
on_behalf_of=authenticated_actor,
|
||||||
|
inbox_actor=user.actor,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -196,6 +198,56 @@ def test_music_library_retrieve_page_public(factories, api_client):
|
||||||
assert response.data == expected
|
assert response.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_outbox_retrieve(factories, api_client):
|
||||||
|
channel = factories["audio.Channel"](actor__local=True)
|
||||||
|
expected = serializers.ChannelOutboxSerializer(channel).data
|
||||||
|
|
||||||
|
url = reverse(
|
||||||
|
"federation:actors-outbox",
|
||||||
|
kwargs={"preferred_username": channel.actor.preferred_username},
|
||||||
|
)
|
||||||
|
response = api_client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_outbox_retrieve_page(factories, api_client):
|
||||||
|
channel = factories["audio.Channel"](actor__local=True)
|
||||||
|
upload = factories["music.Upload"](library=channel.library, playable=True)
|
||||||
|
url = reverse(
|
||||||
|
"federation:actors-outbox",
|
||||||
|
kwargs={"preferred_username": channel.actor.preferred_username},
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = serializers.CollectionPageSerializer(
|
||||||
|
{
|
||||||
|
"id": channel.actor.outbox_url,
|
||||||
|
"item_serializer": serializers.ChannelCreateUploadSerializer,
|
||||||
|
"actor": channel.actor,
|
||||||
|
"page": Paginator([upload], 1).page(1),
|
||||||
|
}
|
||||||
|
).data
|
||||||
|
|
||||||
|
response = api_client.get(url, {"page": 1})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_upload_retrieve(factories, api_client):
|
||||||
|
channel = factories["audio.Channel"](local=True)
|
||||||
|
upload = factories["music.Upload"](library=channel.library, playable=True)
|
||||||
|
url = reverse("federation:music:uploads-detail", kwargs={"uuid": upload.uuid},)
|
||||||
|
|
||||||
|
expected = serializers.ChannelUploadSerializer(upload).data
|
||||||
|
|
||||||
|
response = api_client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("privacy_level", ["me", "instance"])
|
@pytest.mark.parametrize("privacy_level", ["me", "instance"])
|
||||||
def test_music_library_retrieve_page_private(factories, api_client, privacy_level):
|
def test_music_library_retrieve_page_private(factories, api_client, privacy_level):
|
||||||
library = factories["music.Library"](privacy_level=privacy_level)
|
library = factories["music.Library"](privacy_level=privacy_level)
|
||||||
|
|
|
@ -326,7 +326,7 @@ def test_library_detail(factories, superuser_api_client):
|
||||||
|
|
||||||
|
|
||||||
def test_library_update(factories, superuser_api_client):
|
def test_library_update(factories, superuser_api_client):
|
||||||
library = factories["music.Library"](privacy_level="public")
|
library = factories["music.Library"](privacy_level="everyone")
|
||||||
url = reverse(
|
url = reverse(
|
||||||
"api:v1:manage:library:libraries-detail", kwargs={"uuid": library.uuid}
|
"api:v1:manage:library:libraries-detail", kwargs={"uuid": library.uuid}
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,7 +7,8 @@ from funkwhale_api.music import serializers
|
||||||
|
|
||||||
|
|
||||||
def test_library_track(spa_html, no_api_auth, client, factories, settings):
|
def test_library_track(spa_html, no_api_auth, client, factories, settings):
|
||||||
track = factories["music.Upload"](playable=True, track__disc_number=1).track
|
upload = factories["music.Upload"](playable=True, track__disc_number=1)
|
||||||
|
track = upload.track
|
||||||
url = "/library/tracks/{}".format(track.pk)
|
url = "/library/tracks/{}".format(track.pk)
|
||||||
|
|
||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
|
@ -56,6 +57,12 @@ def test_library_track(spa_html, no_api_auth, client, factories, settings):
|
||||||
"property": "og:audio",
|
"property": "og:audio",
|
||||||
"content": utils.join_url(settings.FUNKWHALE_URL, track.listen_url),
|
"content": utils.join_url(settings.FUNKWHALE_URL, track.listen_url),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"tag": "link",
|
||||||
|
"rel": "alternate",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": upload.fid,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"tag": "link",
|
"tag": "link",
|
||||||
"rel": "alternate",
|
"rel": "alternate",
|
||||||
|
@ -116,6 +123,12 @@ def test_library_album(spa_html, no_api_auth, client, factories, settings):
|
||||||
"property": "og:image",
|
"property": "og:image",
|
||||||
"content": album.attachment_cover.download_url_medium_square_crop,
|
"content": album.attachment_cover.download_url_medium_square_crop,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"tag": "link",
|
||||||
|
"rel": "alternate",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": album.fid,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"tag": "link",
|
"tag": "link",
|
||||||
"rel": "alternate",
|
"rel": "alternate",
|
||||||
|
@ -164,6 +177,12 @@ def test_library_artist(spa_html, no_api_auth, client, factories, settings):
|
||||||
"property": "og:image",
|
"property": "og:image",
|
||||||
"content": album.attachment_cover.download_url_medium_square_crop,
|
"content": album.attachment_cover.download_url_medium_square_crop,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"tag": "link",
|
||||||
|
"rel": "alternate",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": artist.fid,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"tag": "link",
|
"tag": "link",
|
||||||
"rel": "alternate",
|
"rel": "alternate",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Added missing manuallyApprovesFollowers entry in JSON-LD contexts (#961)
|
2
dev.yml
2
dev.yml
|
@ -135,7 +135,7 @@ services:
|
||||||
|
|
||||||
labels:
|
labels:
|
||||||
traefik.backend: "${COMPOSE_PROJECT_NAME-node1}"
|
traefik.backend: "${COMPOSE_PROJECT_NAME-node1}"
|
||||||
traefik.frontend.rule: "Host:${COMPOSE_PROJECT_NAME-node1}.funkwhale.test,${NODE_IP-127.0.0.1}"
|
traefik.frontend.rule: "Host:${COMPOSE_PROJECT_NAME-node1}.funkwhale.test,${NODE_IP-127.0.0.1},${DJANGO_ALLOWED_HOSTS}"
|
||||||
traefik.enable: "true"
|
traefik.enable: "true"
|
||||||
traefik.federation.protocol: "http"
|
traefik.federation.protocol: "http"
|
||||||
traefik.federation.port: "80"
|
traefik.federation.port: "80"
|
||||||
|
|
Loading…
Reference in New Issue