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:
Eliot Berriot 2018-11-09 19:02:41 +00:00
commit 6458a748b8
11 changed files with 190 additions and 5 deletions

View File

@ -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

View File

@ -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"]

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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"},
)

View File

@ -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"]()

View File

@ -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",

View File

@ -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}
)

View File

@ -0,0 +1 @@
Advertise public libraries properly in ActivityPub representations (#553)

View File

@ -0,0 +1 @@
Broadcast library updates (name, description, visibility) over federation