diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index d70ce23e5fab8eb27af8bf568f2e331a639f9189..89c621edcae49d0552b5df74ed72f11c89c266aa 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -130,7 +130,7 @@ class SystemActor(object): 'No handler for activity %s', ac['type']) return - return handler(ac, actor) + return handler(data, actor) class LibraryActor(SystemActor): @@ -269,6 +269,40 @@ class TestActor(SystemActor): to=[ac['actor']], on_behalf_of=test_actor) + def handle_undo(self, ac, sender): + if ac['object']['type'] != 'Follow': + return + + if ac['object']['actor'] != sender.url: + # not the same actor, permission issue + return + + test_actor = self.get_actor_instance() + models.Follow.objects.filter( + actor=sender, + target=test_actor, + ).delete() + # we also unfollow the sender, if possible + try: + follow = models.Follow.objects.get( + target=sender, + actor=test_actor, + ) + except models.Follow.DoesNotExist: + return + undo = { + '@context': serializers.AP_CONTEXT, + 'type': 'Undo', + 'id': follow.get_federation_url() + '/undo', + 'actor': test_actor.url, + 'object': serializers.FollowSerializer(follow).data, + } + follow.delete() + activity.deliver( + undo, + to=[sender.url], + on_behalf_of=test_actor) + SYSTEM_ACTORS = { 'library': LibraryActor(), 'test': TestActor(), diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py index 217b472187eb86ecf55722ca2b69a85eebde63aa..16abf80dc921347a0041148c3f7b73c46e443d4b 100644 --- a/api/funkwhale_api/federation/factories.py +++ b/api/funkwhale_api/federation/factories.py @@ -3,6 +3,7 @@ import requests import requests_http_signature from django.utils import timezone +from django.conf import settings from funkwhale_api.factories import registry @@ -65,6 +66,12 @@ class ActorFactory(factory.DjangoModelFactory): class Meta: model = models.Actor + class Params: + local = factory.Trait( + domain=factory.LazyAttribute( + lambda o: settings.FEDERATION_HOSTNAME) + ) + @classmethod def _generate(cls, create, attrs): has_public = attrs.get('public_key') is not None @@ -84,6 +91,11 @@ class FollowFactory(factory.DjangoModelFactory): class Meta: model = models.Follow + class Params: + local = factory.Trait( + actor=factory.SubFactory(ActorFactory, local=True) + ) + @registry.register(name='federation.Note') class NoteFactory(factory.Factory): diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 875268bca3ff999bc7ced6c43c0b16bafb47dece..a228a38033832254f2c96d6154447376a9482e3d 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -14,6 +14,8 @@ TYPE_CHOICES = [ class Actor(models.Model): + ap_type = 'Actor' + url = models.URLField(unique=True, max_length=500, db_index=True) outbox_url = models.URLField(max_length=500) inbox_url = models.URLField(max_length=500) @@ -79,6 +81,8 @@ class Actor(models.Model): class Follow(models.Model): + ap_type = 'Follow' + uuid = models.UUIDField(default=uuid.uuid4, unique=True) actor = models.ForeignKey( Actor, @@ -96,3 +100,6 @@ class Follow(models.Model): class Meta: unique_together = ['actor', 'target'] + + def get_federation_url(self): + return '{}#follows/{}'.format(self.actor.url, self.uuid) diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index d50b52ee681473049d6c730a440e2156872fa45d..c1b9d8a235ce3ca6011d21a286df71208d0b58cb 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -169,11 +169,7 @@ def test_test_post_inbox_handles_create_note( }] ) expected_activity = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - {} - ], + '@context': serializers.AP_CONTEXT, 'actor': test_actor.url, 'id': 'https://{}/activities/note/{}/activity'.format( settings.FEDERATION_HOSTNAME, now.timestamp() @@ -288,11 +284,7 @@ def test_test_actor_handles_follow( }, } expected_follow = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - {} - ], + '@context': serializers.AP_CONTEXT, 'actor': test_actor.url, 'id': test_actor.url + '#follows/{}'.format(uid), 'object': actor.url, @@ -317,3 +309,38 @@ def test_test_actor_handles_follow( 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): + deliver = mocker.patch( + 'funkwhale_api.federation.activity.deliver') + test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance() + follow = factories['federation.Follow'](target=test_actor) + reverse_follow = factories['federation.Follow']( + actor=test_actor, target=follow.actor) + follow_serializer = serializers.FollowSerializer(follow) + reverse_follow_serializer = serializers.FollowSerializer( + reverse_follow) + undo = { + '@context': serializers.AP_CONTEXT, + 'type': 'Undo', + 'id': follow_serializer.data['id'] + '/undo', + 'actor': follow.actor.url, + 'object': follow_serializer.data, + } + expected_undo = { + '@context': serializers.AP_CONTEXT, + 'type': 'Undo', + 'id': reverse_follow_serializer.data['id'] + '/undo', + 'actor': reverse_follow.actor.url, + 'object': reverse_follow_serializer.data, + } + + actors.SYSTEM_ACTORS['test'].post_inbox(undo, actor=follow.actor) + deliver.assert_called_once_with( + expected_undo, + to=[follow.actor.url], + on_behalf_of=test_actor,) + + assert models.Follow.objects.count() == 0 diff --git a/api/tests/federation/test_authentication.py b/api/tests/federation/test_authentication.py index 1837b3950f471ba549d8f45077d5b773cc010da3..c6a97a07a75fc52065f4a7cc7a1a710015788a86 100644 --- a/api/tests/federation/test_authentication.py +++ b/api/tests/federation/test_authentication.py @@ -3,7 +3,7 @@ from funkwhale_api.federation import keys from funkwhale_api.federation import signing -def test_authenticate(nodb_factories, mocker, api_request): +def test_authenticate(factories, mocker, api_request): private, public = keys.get_key_pair() actor_url = 'https://test.federation/actor' mocker.patch( @@ -18,7 +18,7 @@ def test_authenticate(nodb_factories, mocker, api_request): 'id': actor_url + '#main-key', } }) - signed_request = nodb_factories['federation.SignedRequest']( + signed_request = factories['federation.SignedRequest']( auth__key=private, auth__key_id=actor_url + '#main-key', auth__headers=[ diff --git a/api/tests/federation/test_models.py b/api/tests/federation/test_models.py index 297fe2c58200c0c001430359daf5e0e908f332d5..18daf87887f94abb12a43a35a06ef3aabd7485d6 100644 --- a/api/tests/federation/test_models.py +++ b/api/tests/federation/test_models.py @@ -23,3 +23,10 @@ def test_cannot_duplicate_follow(factories): target=follow.target, actor=follow.actor, ) + +def test_follow_federation_url(factories): + follow = factories['federation.Follow'](local=True) + expected = '{}#follows/{}'.format( + follow.actor.url, follow.uuid) + + assert follow.get_federation_url() == expected