diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index 5a0974011dd1d9c21136a1b05563e2ff6722dc51..1b03d19f8e2be2a080ff905a3435686084412599 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -2,7 +2,9 @@ import logging import json import requests import requests_http_signature +import uuid +from . import models from . import signing logger = logging.getLogger(__name__) @@ -117,3 +119,20 @@ def get_accept_follow(accept_id, accept_actor, follow, follow_actor): "object": accept_actor.url }, } + + +def accept_follow(target, follow, actor): + accept_uuid = uuid.uuid4() + accept = get_accept_follow( + accept_id=accept_uuid, + accept_actor=target, + follow=follow, + follow_actor=actor) + deliver( + accept, + to=[actor.url], + on_behalf_of=target) + return models.Follow.objects.get_or_create( + actor=actor, + target=target, + ) diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index 89c621edcae49d0552b5df74ed72f11c89c266aa..8871b1013b4d492fb35aa90885c8df8a1fd8fc63 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -132,6 +132,20 @@ class SystemActor(object): return handler(data, actor) + def handle_follow(self, ac, sender): + system_actor = self.get_actor_instance() + if self.manually_approves_followers: + fr, created = models.FollowRequest.objects.get_or_create( + actor=sender, + target=system_actor, + approved=None, + ) + return fr + + return activity.accept_follow( + system_actor, ac, sender + ) + class LibraryActor(SystemActor): id = 'library' @@ -140,6 +154,7 @@ class LibraryActor(SystemActor): additional_attributes = { 'manually_approves_followers': True } + @property def manually_approves_followers(self): return settings.FEDERATION_MUSIC_NEEDS_APPROVAL @@ -159,18 +174,18 @@ class TestActor(SystemActor): def get_outbox(self, data, actor=None): return { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - {} - ], - "id": utils.full_url( + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "id": utils.full_url( reverse( 'federation:instance-actors-outbox', kwargs={'actor': self.id})), - "type": "OrderedCollection", - "totalItems": 0, - "orderedItems": [] + "type": "OrderedCollection", + "totalItems": 0, + "orderedItems": [] } def parse_command(self, message): @@ -204,10 +219,10 @@ class TestActor(SystemActor): ) reply_activity = { "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - {} - ], + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], 'type': 'Create', 'actor': test_actor.url, 'id': '{}/activity'.format(reply_url), @@ -240,25 +255,9 @@ class TestActor(SystemActor): on_behalf_of=test_actor) def handle_follow(self, ac, sender): - # on a follow we: - # 1. send the accept answer - # 2. follow back - # + super().handle_follow(ac, sender) + # also, we follow back test_actor = self.get_actor_instance() - accept_uuid = uuid.uuid4() - accept = activity.get_accept_follow( - accept_id=accept_uuid, - accept_actor=test_actor, - follow=ac, - follow_actor=sender) - activity.deliver( - accept, - to=[ac['actor']], - on_behalf_of=test_actor) - models.Follow.objects.get_or_create( - actor=sender, - target=test_actor, - ) follow_uuid = uuid.uuid4() follow = activity.get_follow( follow_id=follow_uuid, diff --git a/api/tests/federation/test_activity.py b/api/tests/federation/test_activity.py index a6e1d28aa23623251a1ab26661d2814c14704f00..09c5e3bf7226fc4f36d9e92d01cf6a43106ed3a7 100644 --- a/api/tests/federation/test_activity.py +++ b/api/tests/federation/test_activity.py @@ -1,5 +1,8 @@ +import uuid + from funkwhale_api.federation import activity + def test_deliver(nodb_factories, r_mock, mocker): to = nodb_factories['federation.Actor']() mocker.patch( @@ -30,3 +33,42 @@ def test_deliver(nodb_factories, r_mock, mocker): assert r_mock.call_count == 1 assert request.url == to.inbox_url assert request.headers['content-type'] == 'application/activity+json' + + +def test_accept_follow(mocker, factories): + deliver = mocker.patch( + 'funkwhale_api.federation.activity.deliver') + actor = factories['federation.Actor']() + target = factories['federation.Actor'](local=True) + follow = { + 'actor': actor.url, + 'type': 'Follow', + 'id': 'http://test.federation/user#follows/267', + 'object': target.url, + } + uid = uuid.uuid4() + mocker.patch('uuid.uuid4', return_value=uid) + expected_accept = { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "id": target.url + '#accepts/follows/{}'.format(uid), + "type": "Accept", + "actor": target.url, + "object": { + "id": follow['id'], + "type": "Follow", + "actor": actor.url, + "object": target.url + }, + } + activity.accept_follow( + target, follow, actor + ) + deliver.assert_called_once_with( + expected_accept, to=[actor.url], on_behalf_of=target + ) + follow_instance = actor.emitted_follows.first() + assert follow_instance.target == target diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index c1b9d8a235ce3ca6011d21a286df71208d0b58cb..5ade9cdc8c94c5fde73f977b3ecbf47322bb199d 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -93,18 +93,18 @@ def test_get_test(settings, preferences): def test_test_get_outbox(): expected = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - {} - ], - "id": utils.full_url( + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "id": utils.full_url( reverse( 'federation:instance-actors-outbox', kwargs={'actor': 'test'})), - "type": "OrderedCollection", - "totalItems": 0, - "orderedItems": [] + "type": "OrderedCollection", + "totalItems": 0, + "orderedItems": [] } data = actors.SYSTEM_ACTORS['test'].get_outbox({}, actor=None) @@ -248,7 +248,7 @@ def test_system_actor_handle(mocker, nodb_factories): ) assert serializer.is_valid() actors.SYSTEM_ACTORS['test'].handle(activity, actor) - handler.assert_called_once_with(serializer.data, actor) + handler.assert_called_once_with(activity, actor) def test_test_actor_handles_follow( @@ -258,6 +258,8 @@ def test_test_actor_handles_follow( actor = factories['federation.Actor']() now = timezone.now() mocker.patch('django.utils.timezone.now', return_value=now) + accept_follow = mocker.patch( + 'funkwhale_api.federation.activity.accept_follow') test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance() data = { 'actor': actor.url, @@ -267,22 +269,6 @@ def test_test_actor_handles_follow( } uid = uuid.uuid4() mocker.patch('uuid.uuid4', return_value=uid) - expected_accept = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - {} - ], - "id": test_actor.url + '#accepts/follows/{}'.format(uid), - "type": "Accept", - "actor": test_actor.url, - "object": { - "id": data['id'], - "type": "Follow", - "actor": actor.url, - "object": test_actor.url - }, - } expected_follow = { '@context': serializers.AP_CONTEXT, 'actor': test_actor.url, @@ -292,12 +278,10 @@ def test_test_actor_handles_follow( } actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor) + accept_follow.assert_called_once_with( + test_actor, data, actor + ) expected_calls = [ - mocker.call( - expected_accept, - to=[actor.url], - on_behalf_of=test_actor, - ), mocker.call( expected_follow, to=[actor.url], @@ -306,10 +290,6 @@ def test_test_actor_handles_follow( ] deliver.assert_has_calls(expected_calls) - follow = test_actor.received_follows.first() - assert follow.actor == actor - assert follow.target == test_actor - def test_test_actor_handles_undo_follow( settings, mocker, factories): @@ -344,3 +324,42 @@ def test_test_actor_handles_undo_follow( on_behalf_of=test_actor,) assert models.Follow.objects.count() == 0 + + +def test_library_actor_handles_follow_manual_approval( + settings, mocker, factories): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True + actor = factories['federation.Actor']() + now = timezone.now() + mocker.patch('django.utils.timezone.now', return_value=now) + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + data = { + 'actor': actor.url, + 'type': 'Follow', + 'id': 'http://test.federation/user#follows/267', + 'object': library_actor.url, + } + + library_actor.system_conf.post_inbox(data, actor=actor) + fr = library_actor.received_follow_requests.first() + + assert library_actor.received_follow_requests.count() == 1 + assert fr.target == library_actor + assert fr.actor == actor + assert fr.approved is None + + +def test_library_actor_handles_follow_auto_approval( + settings, mocker, factories): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True + actor = factories['federation.Actor']() + accept_follow = mocker.patch( + 'funkwhale_api.federation.activity.accept_follow') + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + data = { + 'actor': actor.url, + 'type': 'Follow', + 'id': 'http://test.federation/user#follows/267', + 'object': library_actor.url, + } + library_actor.system_conf.post_inbox(data, actor=actor)