Fix #858: Broadcast/handle rejected follows

environments/review-docs-devel-1399dq/deployments/6607
Agate 2020-08-18 20:40:02 +02:00
rodzic 191f28f79c
commit cd109ddeb6
7 zmienionych plików z 120 dodań i 13 usunięć

Wyświetl plik

@ -34,6 +34,8 @@ def update_follow(follow, approved):
follow.save(update_fields=["approved"]) follow.save(update_fields=["approved"])
if approved: if approved:
routes.outbox.dispatch({"type": "Accept"}, context={"follow": follow}) routes.outbox.dispatch({"type": "Accept"}, context={"follow": follow})
else:
routes.outbox.dispatch({"type": "Reject"}, context={"follow": follow})
class LibraryFollowViewSet( class LibraryFollowViewSet(
@ -57,7 +59,7 @@ class LibraryFollowViewSet(
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
return qs.filter(actor=self.request.user.actor) return qs.filter(actor=self.request.user.actor).exclude(approved=False)
def perform_create(self, serializer): def perform_create(self, serializer):
follow = serializer.save(actor=self.request.user.actor) follow = serializer.save(actor=self.request.user.actor)

Wyświetl plik

@ -82,6 +82,37 @@ def outbox_accept(context):
} }
@outbox.register({"type": "Reject"})
def outbox_reject_follow(context):
follow = context["follow"]
if follow._meta.label == "federation.LibraryFollow":
actor = follow.target.actor
else:
actor = follow.target
payload = serializers.RejectFollowSerializer(follow, context={"actor": actor}).data
yield {
"actor": actor,
"type": "Reject",
"payload": with_recipients(payload, to=[follow.actor]),
"object": follow,
"related_object": follow.target,
}
@inbox.register({"type": "Reject"})
def inbox_reject_follow(payload, context):
serializer = serializers.RejectFollowSerializer(data=payload, context=context)
if not serializer.is_valid(raise_exception=context.get("raise_exception", False)):
logger.debug(
"Discarding invalid follow reject from %s: %s",
context["actor"].fid,
serializer.errors,
)
return
serializer.save()
@inbox.register({"type": "Undo", "object.type": "Follow"}) @inbox.register({"type": "Undo", "object.type": "Follow"})
def inbox_undo_follow(payload, context): def inbox_undo_follow(payload, context):
serializer = serializers.UndoFollowSerializer(data=payload, context=context) serializer = serializers.UndoFollowSerializer(data=payload, context=context)

Wyświetl plik

@ -688,11 +688,10 @@ class APIFollowSerializer(serializers.ModelSerializer):
] ]
class AcceptFollowSerializer(serializers.Serializer): class FollowActionSerializer(serializers.Serializer):
id = serializers.URLField(max_length=500, required=False) id = serializers.URLField(max_length=500, required=False)
actor = serializers.URLField(max_length=500) actor = serializers.URLField(max_length=500)
object = FollowSerializer() object = FollowSerializer()
type = serializers.ChoiceField(choices=["Accept"])
def validate_actor(self, v): def validate_actor(self, v):
expected = self.context.get("actor") expected = self.context.get("actor")
@ -720,12 +719,13 @@ class AcceptFollowSerializer(serializers.Serializer):
follow_class.objects.filter( follow_class.objects.filter(
target=target, actor=validated_data["object"]["actor"] target=target, actor=validated_data["object"]["actor"]
) )
.exclude(approved=True)
.select_related() .select_related()
.get() .get()
) )
except follow_class.DoesNotExist: except follow_class.DoesNotExist:
raise serializers.ValidationError("No follow to accept") raise serializers.ValidationError(
"No follow to {}".format(self.action_type)
)
return validated_data return validated_data
def to_representation(self, instance): def to_representation(self, instance):
@ -736,12 +736,18 @@ class AcceptFollowSerializer(serializers.Serializer):
return { return {
"@context": jsonld.get_default_context(), "@context": jsonld.get_default_context(),
"id": instance.get_federation_id() + "/accept", "id": instance.get_federation_id() + "/{}".format(self.action_type),
"type": "Accept", "type": self.action_type.title(),
"actor": actor.fid, "actor": actor.fid,
"object": FollowSerializer(instance).data, "object": FollowSerializer(instance).data,
} }
class AcceptFollowSerializer(FollowActionSerializer):
type = serializers.ChoiceField(choices=["Accept"])
action_type = "accept"
def save(self): def save(self):
follow = self.validated_data["follow"] follow = self.validated_data["follow"]
follow.approved = True follow.approved = True
@ -751,6 +757,18 @@ class AcceptFollowSerializer(serializers.Serializer):
return follow return follow
class RejectFollowSerializer(FollowActionSerializer):
type = serializers.ChoiceField(choices=["Reject"])
action_type = "reject"
def save(self):
follow = self.validated_data["follow"]
follow.approved = False
follow.save()
return follow
class UndoFollowSerializer(serializers.Serializer): class UndoFollowSerializer(serializers.Serializer):
id = serializers.URLField(max_length=500) id = serializers.URLField(max_length=500)
actor = serializers.URLField(max_length=500) actor = serializers.URLField(max_length=500)

Wyświetl plik

@ -126,12 +126,9 @@ def test_user_can_accept_or_reject_own_follows(
assert follow.approved is expected assert follow.approved is expected
if action == "accept": mocked_dispatch.assert_called_once_with(
mocked_dispatch.assert_called_once_with( {"type": action.title()}, context={"follow": follow}
{"type": "Accept"}, context={"follow": follow} )
)
if action == "reject":
mocked_dispatch.assert_not_called()
def test_user_can_list_inbox_items(factories, logged_in_api_client): def test_user_can_list_inbox_items(factories, logged_in_api_client):

Wyświetl plik

@ -16,6 +16,7 @@ from funkwhale_api.moderation import serializers as moderation_serializers
[ [
({"type": "Follow"}, routes.inbox_follow), ({"type": "Follow"}, routes.inbox_follow),
({"type": "Accept"}, routes.inbox_accept), ({"type": "Accept"}, routes.inbox_accept),
({"type": "Reject"}, routes.inbox_reject_follow),
({"type": "Create", "object": {"type": "Audio"}}, routes.inbox_create_audio), ({"type": "Create", "object": {"type": "Audio"}}, routes.inbox_create_audio),
( (
{"type": "Update", "object": {"type": "Library"}}, {"type": "Update", "object": {"type": "Library"}},
@ -51,6 +52,7 @@ def test_inbox_routes(route, handler):
({"type": "Accept"}, routes.outbox_accept), ({"type": "Accept"}, routes.outbox_accept),
({"type": "Flag"}, routes.outbox_flag), ({"type": "Flag"}, routes.outbox_flag),
({"type": "Follow"}, routes.outbox_follow), ({"type": "Follow"}, routes.outbox_follow),
({"type": "Reject"}, routes.outbox_reject_follow),
({"type": "Create", "object": {"type": "Audio"}}, routes.outbox_create_audio), ({"type": "Create", "object": {"type": "Audio"}}, routes.outbox_create_audio),
( (
{"type": "Update", "object": {"type": "Library"}}, {"type": "Update", "object": {"type": "Library"}},
@ -669,6 +671,46 @@ def test_outbox_delete_follow_library(factories):
assert activity["related_object"] == follow.target assert activity["related_object"] == follow.target
def test_inbox_reject_follow_library(factories):
local_actor = factories["users.User"]().create_actor()
remote_actor = factories["federation.Actor"]()
follow = factories["federation.LibraryFollow"](
actor=local_actor, target__actor=remote_actor, approved=True
)
assert follow.approved is True
serializer = serializers.RejectFollowSerializer(
follow, context={"actor": remote_actor}
)
ii = factories["federation.InboxItem"](actor=local_actor)
routes.inbox_reject_follow(
serializer.data,
context={"actor": remote_actor, "inbox_items": [ii], "raise_exception": True},
)
follow.refresh_from_db()
assert follow.approved is False
def test_outbox_reject_follow_library(factories):
remote_actor = factories["federation.Actor"]()
local_actor = factories["federation.Actor"](local=True)
follow = factories["federation.LibraryFollow"](
actor=remote_actor, target__actor=local_actor
)
activity = list(routes.outbox_reject_follow({"follow": follow}))[0]
serializer = serializers.RejectFollowSerializer(
follow, context={"actor": local_actor}
)
expected = serializer.data
expected["to"] = [remote_actor]
assert activity["payload"] == expected
assert activity["actor"] == local_actor
assert activity["object"] == follow
assert activity["related_object"] == follow.target
def test_handle_library_entry_update_can_manage(factories, mocker): def test_handle_library_entry_update_can_manage(factories, mocker):
update_library_entity = mocker.patch( update_library_entity = mocker.patch(
"funkwhale_api.music.tasks.update_library_entity" "funkwhale_api.music.tasks.update_library_entity"

Wyświetl plik

@ -318,6 +318,22 @@ def test_accept_follow_serializer_validates_on_context(factories):
assert "object" in serializer.errors["object"] assert "object" in serializer.errors["object"]
def test_reject_follow_serializer_representation(factories):
follow = factories["federation.Follow"](approved=None)
expected = {
"@context": jsonld.get_default_context(),
"id": follow.get_federation_id() + "/reject",
"type": "Reject",
"actor": follow.target.fid,
"object": serializers.FollowSerializer(follow).data,
}
serializer = serializers.RejectFollowSerializer(follow)
assert serializer.data == expected
def test_undo_follow_serializer_representation(factories): def test_undo_follow_serializer_representation(factories):
follow = factories["federation.Follow"](approved=True) follow = factories["federation.Follow"](approved=True)

Wyświetl plik

@ -0,0 +1 @@
Broadcast/handle rejected follows (#858)