Merge branch '170-channel-track-update' into 'develop'
See #170: dispatch / handle delete and update on Audio See merge request funkwhale/funkwhale!1085
This commit is contained in:
commit
7f90b4d91d
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
import uuid
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
|
@ -333,6 +334,37 @@ def inbox_update_track(payload, context):
|
|||
)
|
||||
|
||||
|
||||
@inbox.register({"type": "Update", "object.type": "Audio"})
|
||||
def inbox_update_audio(payload, context):
|
||||
serializer = serializers.ChannelCreateUploadSerializer(
|
||||
data=payload, context=context
|
||||
)
|
||||
|
||||
if not serializer.is_valid(raise_exception=context.get("raise_exception", False)):
|
||||
logger.info("Skipped update, invalid payload")
|
||||
return
|
||||
serializer.save()
|
||||
|
||||
|
||||
@outbox.register({"type": "Update", "object.type": "Audio"})
|
||||
def outbox_update_audio(context):
|
||||
upload = context["upload"]
|
||||
channel = upload.library.get_channel()
|
||||
actor = channel.actor
|
||||
serializer = serializers.ChannelCreateUploadSerializer(
|
||||
upload, context={"type": "Update", "activity_id_suffix": str(uuid.uuid4())[:8]}
|
||||
)
|
||||
|
||||
yield {
|
||||
"type": "Update",
|
||||
"actor": actor,
|
||||
"payload": with_recipients(
|
||||
serializer.data,
|
||||
to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@inbox.register({"type": "Update", "object.type": "Artist"})
|
||||
def inbox_update_artist(payload, context):
|
||||
return handle_library_entry_update(
|
||||
|
@ -437,7 +469,6 @@ def outbox_delete_actor(context):
|
|||
{
|
||||
"type": "Delete",
|
||||
"object.type": [
|
||||
"Tombstone",
|
||||
"Actor",
|
||||
"Person",
|
||||
"Application",
|
||||
|
@ -464,6 +495,17 @@ def inbox_delete_actor(payload, context):
|
|||
actor.delete()
|
||||
|
||||
|
||||
@inbox.register({"type": "Delete", "object.type": "Tombstone"})
|
||||
def inbox_delete(payload, context):
|
||||
serializer = serializers.DeleteSerializer(data=payload, context=context)
|
||||
if not serializer.is_valid(raise_exception=context.get("raise_exception", False)):
|
||||
logger.info("Skipped deletion, invalid payload")
|
||||
return
|
||||
|
||||
to_delete = serializer.validated_data["object"]
|
||||
to_delete.delete()
|
||||
|
||||
|
||||
@inbox.register({"type": "Flag"})
|
||||
def inbox_flag(payload, context):
|
||||
serializer = serializers.FlagSerializer(data=payload, context=context)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
import os
|
||||
import urllib.parse
|
||||
import uuid
|
||||
|
||||
|
@ -1967,7 +1968,6 @@ class ChannelUploadSerializer(jsonld.JsonLdSerializer):
|
|||
|
||||
|
||||
class ChannelCreateUploadSerializer(jsonld.JsonLdSerializer):
|
||||
type = serializers.ChoiceField(choices=[contexts.AS.Create])
|
||||
object = serializers.DictField()
|
||||
|
||||
class Meta:
|
||||
|
@ -1976,9 +1976,9 @@ class ChannelCreateUploadSerializer(jsonld.JsonLdSerializer):
|
|||
}
|
||||
|
||||
def to_representation(self, upload):
|
||||
return {
|
||||
payload = {
|
||||
"@context": jsonld.get_default_context(),
|
||||
"type": "Create",
|
||||
"type": self.context.get("type", "Create"),
|
||||
"id": utils.full_url(
|
||||
reverse(
|
||||
"federation:music:uploads-activity", kwargs={"uuid": upload.uuid}
|
||||
|
@ -1989,6 +1989,12 @@ class ChannelCreateUploadSerializer(jsonld.JsonLdSerializer):
|
|||
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(
|
||||
|
@ -1999,3 +2005,28 @@ class ChannelCreateUploadSerializer(jsonld.JsonLdSerializer):
|
|||
|
||||
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
|
||||
|
|
|
@ -277,3 +277,18 @@ def get_object_by_fid(fid, local=None):
|
|||
return channel
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
def can_manage(obj_owner, actor):
|
||||
if not obj_owner:
|
||||
return False
|
||||
|
||||
if not actor:
|
||||
return False
|
||||
|
||||
if obj_owner == actor:
|
||||
return True
|
||||
if obj_owner.domain.service_actor == actor:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
|
@ -127,9 +127,18 @@ class TrackMutationSerializer(CoverMutation, TagMutation, DescriptionMutation):
|
|||
return serialized_relations
|
||||
|
||||
def post_apply(self, obj, validated_data):
|
||||
routes.outbox.dispatch(
|
||||
{"type": "Update", "object": {"type": "Track"}}, context={"track": obj}
|
||||
)
|
||||
channel = obj.artist.get_channel()
|
||||
if channel:
|
||||
upload = channel.library.uploads.filter(track=obj).first()
|
||||
if upload:
|
||||
routes.outbox.dispatch(
|
||||
{"type": "Update", "object": {"type": "Audio"}},
|
||||
context={"upload": upload},
|
||||
)
|
||||
else:
|
||||
routes.outbox.dispatch(
|
||||
{"type": "Update", "object": {"type": "Track"}}, context={"track": obj}
|
||||
)
|
||||
|
||||
|
||||
@mutations.registry.connect(
|
||||
|
|
|
@ -31,7 +31,9 @@ from funkwhale_api.moderation import serializers as moderation_serializers
|
|||
({"type": "Update", "object": {"type": "Artist"}}, routes.inbox_update_artist),
|
||||
({"type": "Update", "object": {"type": "Album"}}, routes.inbox_update_album),
|
||||
({"type": "Update", "object": {"type": "Track"}}, routes.inbox_update_track),
|
||||
({"type": "Update", "object": {"type": "Audio"}}, routes.inbox_update_audio),
|
||||
({"type": "Delete", "object": {"type": "Person"}}, routes.inbox_delete_actor),
|
||||
({"type": "Delete", "object": {"type": "Tombstone"}}, routes.inbox_delete),
|
||||
({"type": "Flag"}, routes.inbox_flag),
|
||||
],
|
||||
)
|
||||
|
@ -62,6 +64,7 @@ def test_inbox_routes(route, handler):
|
|||
({"type": "Delete", "object": {"type": "Album"}}, routes.outbox_delete_album),
|
||||
({"type": "Undo", "object": {"type": "Follow"}}, routes.outbox_undo_follow),
|
||||
({"type": "Update", "object": {"type": "Track"}}, routes.outbox_update_track),
|
||||
({"type": "Update", "object": {"type": "Audio"}}, routes.outbox_update_audio),
|
||||
(
|
||||
{"type": "Delete", "object": {"type": "Tombstone"}},
|
||||
routes.outbox_delete_actor,
|
||||
|
@ -354,7 +357,7 @@ def test_inbox_create_audio_channel(factories, mocker):
|
|||
"@context": jsonld.get_default_context(),
|
||||
"type": "Create",
|
||||
"actor": channel.actor.fid,
|
||||
"object": serializers.ChannelCreateUploadSerializer(upload).data,
|
||||
"object": serializers.ChannelUploadSerializer(upload).data,
|
||||
}
|
||||
upload.delete()
|
||||
init = mocker.spy(serializers.ChannelCreateUploadSerializer, "__init__")
|
||||
|
@ -368,7 +371,7 @@ def test_inbox_create_audio_channel(factories, mocker):
|
|||
|
||||
assert init.call_count == 1
|
||||
args = init.call_args
|
||||
assert args[1]["data"] == payload["object"]
|
||||
assert args[1]["data"] == payload
|
||||
assert args[1]["context"] == {"channel": channel}
|
||||
assert save.call_count == 1
|
||||
|
||||
|
@ -765,6 +768,46 @@ def test_inbox_update_track(factories, mocker):
|
|||
update_library_entity.assert_called_once_with(obj, {"title": "New title"})
|
||||
|
||||
|
||||
def test_inbox_update_audio(factories, mocker, r_mock):
|
||||
channel = factories["audio.Channel"]()
|
||||
upload = factories["music.Upload"](
|
||||
library=channel.library,
|
||||
track__artist=channel.artist,
|
||||
track__attributed_to=channel.actor,
|
||||
)
|
||||
upload.track.fid = upload.fid
|
||||
upload.track.save()
|
||||
r_mock.get(
|
||||
upload.track.album.fid,
|
||||
json=serializers.AlbumSerializer(upload.track.album).data,
|
||||
)
|
||||
data = serializers.ChannelCreateUploadSerializer(upload).data
|
||||
data["object"]["name"] = "New title"
|
||||
|
||||
routes.inbox_update_audio(
|
||||
data, context={"actor": channel.actor, "raise_exception": True}
|
||||
)
|
||||
|
||||
upload.track.refresh_from_db()
|
||||
|
||||
assert upload.track.title == "New title"
|
||||
|
||||
|
||||
def test_outbox_update_audio(factories, faker, mocker):
|
||||
fake_uuid = faker.uuid4()
|
||||
mocker.patch("uuid.uuid4", return_value=fake_uuid)
|
||||
upload = factories["music.Upload"](channel=True)
|
||||
activity = list(routes.outbox_update_audio({"upload": upload}))[0]
|
||||
expected = serializers.ChannelCreateUploadSerializer(upload).data
|
||||
|
||||
expected["type"] = "Update"
|
||||
expected["id"] += "/" + fake_uuid[:8]
|
||||
expected["to"] = [contexts.AS.Public, {"type": "instances_with_followers"}]
|
||||
|
||||
assert dict(activity["payload"]) == dict(expected)
|
||||
assert activity["actor"] == upload.library.channel.actor
|
||||
|
||||
|
||||
def test_outbox_update_track(factories):
|
||||
track = factories["music.Track"]()
|
||||
activity = list(routes.outbox_update_track({"track": track}))[0]
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import pytest
|
||||
|
||||
from funkwhale_api.federation import routes
|
||||
from funkwhale_api.federation import serializers
|
||||
|
||||
|
||||
|
@ -141,3 +144,93 @@ def test_reel2bits_channel_from_actor_ap(db, mocker):
|
|||
assert channel.rss_url == payload["url"][1]["href"]
|
||||
assert channel.artist.name == actor.name
|
||||
assert channel.artist.attributed_to == actor
|
||||
|
||||
|
||||
def test_reel2bits_upload_create(factories):
|
||||
channel = factories["audio.Channel"]()
|
||||
payload = {
|
||||
"id": "https://r2b.example/outbox/cb89c969224d7c9d",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type": "Create",
|
||||
"actor": "https://r2b.example/user/anna",
|
||||
"object": {
|
||||
"cc": ["https://r2b.example/user/anna/followers"],
|
||||
"id": "https://r2b.example/outbox/cb89c969224d7c9d/activity",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"url": {
|
||||
"href": "https://r2b.example/uploads/sounds/anna/test.mp3",
|
||||
"type": "Link",
|
||||
"mediaType": "audio/mpeg",
|
||||
},
|
||||
"name": "nya",
|
||||
"tag": [
|
||||
{"name": "#nya", "type": "Hashtag"},
|
||||
{"name": "#cat", "type": "Hashtag"},
|
||||
{"name": "#paws", "type": "Hashtag"},
|
||||
],
|
||||
"type": "Audio",
|
||||
"genre": "cat",
|
||||
"image": {
|
||||
"url": "https://r2b.example/uploads/artwork_sounds/anna/test.jpg",
|
||||
"type": "Image",
|
||||
"mediaType": "image/jpeg",
|
||||
},
|
||||
"content": "nya nya",
|
||||
"licence": {"id": "0", "icon": "", "link": "", "name": "Not Specified"},
|
||||
"mediaType": "text/plain",
|
||||
"published": "2020-04-08T12:47:29Z",
|
||||
"attributedTo": "https://r2b.example/user/anna",
|
||||
},
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"Hashtag": "as:Hashtag",
|
||||
"featured": "toot:featured",
|
||||
"sensitive": "as:sensitive",
|
||||
},
|
||||
],
|
||||
"published": "2020-04-08T12:47:29Z",
|
||||
}
|
||||
serializer = serializers.ChannelCreateUploadSerializer(
|
||||
data=payload, context={"channel": channel}
|
||||
)
|
||||
assert serializer.is_valid(raise_exception=True) is True
|
||||
|
||||
serializer.save()
|
||||
|
||||
|
||||
def test_reel2bits_upload_delete(factories):
|
||||
actor = factories["federation.Actor"]()
|
||||
channel = factories["audio.Channel"](actor=actor, attributed_to=actor)
|
||||
upload = factories["music.Upload"](channel=channel, track__attributed_to=actor)
|
||||
payload = {
|
||||
"id": "https://r2b.example/outbox/4987acc5b25f0aac",
|
||||
"to": [
|
||||
"https://channels.tests.funkwhale.audio/federation/actors/demo",
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
],
|
||||
"type": "Delete",
|
||||
"actor": actor.fid,
|
||||
"object": {"id": upload.fid, "type": "Tombstone"},
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"Hashtag": "as:Hashtag",
|
||||
"featured": "toot:featured",
|
||||
"sensitive": "as:sensitive",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
routes.inbox_delete(
|
||||
payload, context={"actor": actor, "raise_exception": True, "activity": payload},
|
||||
)
|
||||
|
||||
with pytest.raises(upload.track.DoesNotExist):
|
||||
upload.track.refresh_from_db()
|
||||
with pytest.raises(upload.DoesNotExist):
|
||||
upload.refresh_from_db()
|
||||
|
|
|
@ -123,6 +123,19 @@ def test_track_mutation_apply_outbox(factories, mocker):
|
|||
)
|
||||
|
||||
|
||||
def test_channel_track_mutation_apply_outbox(factories, mocker):
|
||||
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
|
||||
upload = factories["music.Upload"](channel=True, track__position=4)
|
||||
mutation = factories["common.Mutation"](
|
||||
type="update", target=upload.track, payload={"position": 12}
|
||||
)
|
||||
mutation.apply()
|
||||
|
||||
dispatch.assert_called_once_with(
|
||||
{"type": "Update", "object": {"type": "Audio"}}, context={"upload": upload}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("factory_name", ["music.Artist", "music.Album", "music.Track"])
|
||||
def test_mutation_set_tags(factory_name, factories, now, mocker):
|
||||
tags = ["tag1", "tag2"]
|
||||
|
|
Loading…
Reference in New Issue