Merge branch '553-ap-public-libraries' into 'develop'
Resolve "Add `Audience` field in AP representations when library is public" Closes #553 See merge request funkwhale/funkwhale!463
This commit is contained in:
commit
6458a748b8
|
@ -159,3 +159,34 @@ class ActionSerializer(serializers.Serializer):
|
|||
"result": result,
|
||||
}
|
||||
return payload
|
||||
|
||||
|
||||
def track_fields_for_update(*fields):
|
||||
"""
|
||||
Apply this decorator to serializer to call function when specific values
|
||||
are updated on an object:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@track_fields_for_update('privacy_level')
|
||||
class LibrarySerializer(serializers.ModelSerializer):
|
||||
def on_updated_privacy_level(self, obj, old_value, new_value):
|
||||
print('Do someting')
|
||||
"""
|
||||
|
||||
def decorator(serializer_class):
|
||||
original_update = serializer_class.update
|
||||
|
||||
def new_update(self, obj, validated_data):
|
||||
tracked_fields_before = {f: getattr(obj, f) for f in fields}
|
||||
obj = original_update(self, obj, validated_data)
|
||||
tracked_fields_after = {f: getattr(obj, f) for f in fields}
|
||||
|
||||
if tracked_fields_before != tracked_fields_after:
|
||||
self.on_updated_fields(obj, tracked_fields_before, tracked_fields_after)
|
||||
return obj
|
||||
|
||||
serializer_class.update = new_update
|
||||
return serializer_class
|
||||
|
||||
return decorator
|
||||
|
|
|
@ -82,7 +82,7 @@ def inbox_undo_follow(payload, context):
|
|||
serializer = serializers.UndoFollowSerializer(data=payload, context=context)
|
||||
if not serializer.is_valid(raise_exception=context.get("raise_exception", False)):
|
||||
logger.debug(
|
||||
"Discarding invalid follow undo from {}: %s",
|
||||
"Discarding invalid follow undo from %s: %s",
|
||||
context["actor"].fid,
|
||||
serializer.errors,
|
||||
)
|
||||
|
@ -195,6 +195,45 @@ def outbox_delete_library(context):
|
|||
}
|
||||
|
||||
|
||||
@outbox.register({"type": "Update", "object.type": "Library"})
|
||||
def outbox_update_library(context):
|
||||
library = context["library"]
|
||||
serializer = serializers.ActivitySerializer(
|
||||
{"type": "Update", "object": serializers.LibrarySerializer(library).data}
|
||||
)
|
||||
|
||||
yield {
|
||||
"type": "Update",
|
||||
"actor": library.actor,
|
||||
"payload": with_recipients(
|
||||
serializer.data, to=[{"type": "followers", "target": library}]
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@inbox.register({"type": "Update", "object.type": "Library"})
|
||||
def inbox_update_library(payload, context):
|
||||
actor = context["actor"]
|
||||
library_id = payload["object"].get("id")
|
||||
if not library_id:
|
||||
logger.debug("Discarding deletion of empty library")
|
||||
return
|
||||
|
||||
if not actor.libraries.filter(fid=library_id).exists():
|
||||
logger.debug("Discarding deletion of unkwnown library %s", library_id)
|
||||
return
|
||||
|
||||
serializer = serializers.LibrarySerializer(data=payload["object"])
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
else:
|
||||
logger.debug(
|
||||
"Discarding update of library %s because of payload errors: %s",
|
||||
library_id,
|
||||
serializer.errors,
|
||||
)
|
||||
|
||||
|
||||
@inbox.register({"type": "Delete", "object.type": "Audio"})
|
||||
def inbox_delete_audio(payload, context):
|
||||
actor = context["actor"]
|
||||
|
|
|
@ -560,7 +560,7 @@ class LibrarySerializer(PaginatedCollectionSerializer):
|
|||
r = super().to_representation(conf)
|
||||
r["audience"] = (
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
if library.privacy_level == "public"
|
||||
if library.privacy_level == "everyone"
|
||||
else ""
|
||||
)
|
||||
r["followers"] = library.followers_url
|
||||
|
|
|
@ -85,7 +85,7 @@ def verify_django(django_request, public_key):
|
|||
def get_auth(private_key, private_key_id):
|
||||
return requests_http_signature.HTTPSignatureAuth(
|
||||
use_auth_header=False,
|
||||
headers=["(request-target)", "user-agent", "host", "date", "content-type"],
|
||||
headers=["(request-target)", "user-agent", "host", "date"],
|
||||
algorithm="rsa-sha256",
|
||||
key=private_key.encode("utf-8"),
|
||||
key_id=private_key_id,
|
||||
|
|
|
@ -192,6 +192,7 @@ class TrackSerializer(serializers.ModelSerializer):
|
|||
return TrackUploadSerializer(uploads, many=True).data
|
||||
|
||||
|
||||
@common_serializers.track_fields_for_update("name", "description", "privacy_level")
|
||||
class LibraryForOwnerSerializer(serializers.ModelSerializer):
|
||||
uploads_count = serializers.SerializerMethodField()
|
||||
size = serializers.SerializerMethodField()
|
||||
|
@ -216,6 +217,11 @@ class LibraryForOwnerSerializer(serializers.ModelSerializer):
|
|||
def get_size(self, o):
|
||||
return getattr(o, "_size", 0)
|
||||
|
||||
def on_updated_fields(self, obj, before, after):
|
||||
routes.outbox.dispatch(
|
||||
{"type": "Update", "object": {"type": "Library"}}, context={"library": obj}
|
||||
)
|
||||
|
||||
|
||||
class UploadSerializer(serializers.ModelSerializer):
|
||||
track = TrackSerializer(required=False, allow_null=True)
|
||||
|
|
|
@ -134,3 +134,32 @@ def test_action_serializers_can_require_filter(factories):
|
|||
|
||||
assert serializer.is_valid(raise_exception=True) is True
|
||||
assert list(serializer.validated_data["objects"]) == [user1]
|
||||
|
||||
|
||||
def test_track_fields_for_update(mocker):
|
||||
@serializers.track_fields_for_update("field1", "field2")
|
||||
class S(serializers.serializers.Serializer):
|
||||
field1 = serializers.serializers.CharField()
|
||||
field2 = serializers.serializers.CharField()
|
||||
|
||||
def update(self, obj, validated_data):
|
||||
for key, value in validated_data.items():
|
||||
setattr(obj, key, value)
|
||||
return obj
|
||||
|
||||
on_updated_fields = mocker.stub()
|
||||
|
||||
class Obj(object):
|
||||
field1 = "value1"
|
||||
field2 = "value2"
|
||||
|
||||
obj = Obj()
|
||||
serializer = S(obj, data={"field1": "newvalue1", "field2": "newvalue2"})
|
||||
assert serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
serializer.on_updated_fields.assert_called_once_with(
|
||||
obj,
|
||||
{"field1": "value1", "field2": "value2"},
|
||||
{"field1": "newvalue1", "field2": "newvalue2"},
|
||||
)
|
||||
|
|
|
@ -9,6 +9,7 @@ from funkwhale_api.federation import routes, serializers
|
|||
({"type": "Follow"}, routes.inbox_follow),
|
||||
({"type": "Accept"}, routes.inbox_accept),
|
||||
({"type": "Create", "object.type": "Audio"}, routes.inbox_create_audio),
|
||||
({"type": "Update", "object.type": "Library"}, routes.inbox_update_library),
|
||||
({"type": "Delete", "object.type": "Library"}, routes.inbox_delete_library),
|
||||
({"type": "Delete", "object.type": "Audio"}, routes.inbox_delete_audio),
|
||||
({"type": "Undo", "object.type": "Follow"}, routes.inbox_undo_follow),
|
||||
|
@ -29,6 +30,7 @@ def test_inbox_routes(route, handler):
|
|||
({"type": "Accept"}, routes.outbox_accept),
|
||||
({"type": "Follow"}, routes.outbox_follow),
|
||||
({"type": "Create", "object.type": "Audio"}, routes.outbox_create_audio),
|
||||
({"type": "Update", "object.type": "Library"}, routes.outbox_update_library),
|
||||
({"type": "Delete", "object.type": "Library"}, routes.outbox_delete_library),
|
||||
({"type": "Delete", "object.type": "Audio"}, routes.outbox_delete_audio),
|
||||
({"type": "Undo", "object.type": "Follow"}, routes.outbox_undo_follow),
|
||||
|
@ -262,6 +264,55 @@ def test_outbox_delete_library(factories):
|
|||
assert activity["actor"] == library.actor
|
||||
|
||||
|
||||
def test_outbox_update_library(factories):
|
||||
library = factories["music.Library"]()
|
||||
activity = list(routes.outbox_update_library({"library": library}))[0]
|
||||
expected = serializers.ActivitySerializer(
|
||||
{"type": "Update", "object": serializers.LibrarySerializer(library).data}
|
||||
).data
|
||||
|
||||
expected["to"] = [{"type": "followers", "target": library}]
|
||||
|
||||
assert dict(activity["payload"]) == dict(expected)
|
||||
assert activity["actor"] == library.actor
|
||||
|
||||
|
||||
def test_inbox_update_library(factories):
|
||||
activity = factories["federation.Activity"]()
|
||||
|
||||
library = factories["music.Library"]()
|
||||
data = serializers.LibrarySerializer(library).data
|
||||
data["name"] = "New name"
|
||||
payload = {"type": "Update", "actor": library.actor.fid, "object": data}
|
||||
|
||||
routes.inbox_update_library(
|
||||
payload,
|
||||
context={"actor": library.actor, "raise_exception": True, "activity": activity},
|
||||
)
|
||||
|
||||
library.refresh_from_db()
|
||||
assert library.name == "New name"
|
||||
|
||||
|
||||
# def test_inbox_update_library_impostor(factories):
|
||||
# activity = factories["federation.Activity"]()
|
||||
# impostor = factories["federation.Actor"]()
|
||||
# library = factories["music.Library"]()
|
||||
# payload = {
|
||||
# "type": "Delete",
|
||||
# "actor": library.actor.fid,
|
||||
# "object": {"type": "Library", "id": library.fid},
|
||||
# }
|
||||
|
||||
# routes.inbox_update_library(
|
||||
# payload,
|
||||
# context={"actor": impostor, "raise_exception": True, "activity": activity},
|
||||
# )
|
||||
|
||||
# # not deleted, should still be here
|
||||
# library.refresh_from_db()
|
||||
|
||||
|
||||
def test_inbox_delete_audio(factories):
|
||||
activity = factories["federation.Activity"]()
|
||||
|
||||
|
|
|
@ -476,7 +476,7 @@ def test_collection_page_serializer(factories):
|
|||
|
||||
|
||||
def test_music_library_serializer_to_ap(factories):
|
||||
library = factories["music.Library"]()
|
||||
library = factories["music.Library"](privacy_level="everyone")
|
||||
# pending, errored and skippednot included
|
||||
factories["music.Upload"](import_status="pending")
|
||||
factories["music.Upload"](import_status="errored")
|
||||
|
@ -488,11 +488,11 @@ def test_music_library_serializer_to_ap(factories):
|
|||
"https://w3id.org/security/v1",
|
||||
{},
|
||||
],
|
||||
"audience": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Library",
|
||||
"id": library.fid,
|
||||
"name": library.name,
|
||||
"summary": library.description,
|
||||
"audience": "",
|
||||
"actor": library.actor.fid,
|
||||
"totalItems": 0,
|
||||
"current": library.fid + "?page=1",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import pytest
|
||||
|
||||
from funkwhale_api.music import models
|
||||
from funkwhale_api.music import serializers
|
||||
from funkwhale_api.music import tasks
|
||||
|
@ -274,3 +276,28 @@ def test_track_upload_serializer(factories):
|
|||
|
||||
serializer = serializers.TrackUploadSerializer(upload)
|
||||
assert serializer.data == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field,before,after",
|
||||
[
|
||||
("privacy_level", "me", "everyone"),
|
||||
("name", "Before", "After"),
|
||||
("description", "Before", "After"),
|
||||
],
|
||||
)
|
||||
def test_update_library_privacy_level_broadcasts_to_followers(
|
||||
factories, field, before, after, mocker
|
||||
):
|
||||
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
|
||||
library = factories["music.Library"](**{field: before})
|
||||
|
||||
serializer = serializers.LibraryForOwnerSerializer(
|
||||
library, data={field: after}, partial=True
|
||||
)
|
||||
assert serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
dispatch.assert_called_once_with(
|
||||
{"type": "Update", "object": {"type": "Library"}}, context={"library": library}
|
||||
)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Advertise public libraries properly in ActivityPub representations (#553)
|
|
@ -0,0 +1 @@
|
|||
Broadcast library updates (name, description, visibility) over federation
|
Loading…
Reference in New Issue