From 6aa6f1d8f869e3821a40dd7a2b061bfa098eb008 Mon Sep 17 00:00:00 2001 From: Eliot Berriot <contact@eliotberriot.com> Date: Tue, 3 Apr 2018 19:48:50 +0200 Subject: [PATCH] Test actor can now follow back --- api/funkwhale_api/federation/activity.py | 34 +++++++++ api/funkwhale_api/federation/actors.py | 81 ++++++++++++++++----- api/funkwhale_api/federation/factories.py | 17 +++++ api/funkwhale_api/federation/serializers.py | 4 +- api/tests/federation/test_actors.py | 77 +++++++++++++++++++- 5 files changed, 192 insertions(+), 21 deletions(-) diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index 4eeb193b..3b7648f1 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -83,3 +83,37 @@ def deliver(activity, on_behalf_of, to=[]): ) response.raise_for_status() logger.debug('Remote answered with %s', response.status_code) + + +def get_follow(follow_id, follower, followed): + return { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + {} + ], + 'actor': follower.url, + 'id': follower.url + '#follows/{}'.format(follow_id), + 'object': followed.url, + 'type': 'Follow' + } + + +def get_accept_follow(accept_id, accept_actor, follow, follow_actor): + return { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "id": accept_actor.url + '#accepts/follows/{}'.format( + accept_id), + "type": "Accept", + "actor": accept_actor.url, + "object": { + "id": follow['id'], + "type": "Follow", + "actor": follow_actor.url, + "object": accept_actor.url + }, + } diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index e29125f5..031526f8 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -1,5 +1,6 @@ import logging import requests +import uuid import xml from django.conf import settings @@ -98,7 +99,7 @@ class SystemActor(object): raise NotImplementedError def post_inbox(self, data, actor=None): - raise NotImplementedError + return self.handle(data, actor=actor) def get_outbox(self, data, actor=None): raise NotImplementedError @@ -106,6 +107,31 @@ class SystemActor(object): def post_outbox(self, data, actor=None): raise NotImplementedError + def handle(self, data, actor=None): + """ + Main entrypoint for handling activities posted to the + actor's inbox + """ + logger.info('Received activity on %s inbox', self.id) + + if actor is None: + raise PermissionDenied('Actor not authenticated') + + serializer = serializers.ActivitySerializer( + data=data, context={'actor': actor}) + serializer.is_valid(raise_exception=True) + + ac = serializer.data + try: + handler = getattr( + self, 'handle_{}'.format(ac['type'].lower())) + except (KeyError, AttributeError): + logger.debug( + 'No handler for activity %s', ac['type']) + return + + return handler(ac, actor) + class LibraryActor(SystemActor): id = 'library' @@ -147,23 +173,6 @@ class TestActor(SystemActor): "orderedItems": [] } - def post_inbox(self, data, actor=None): - if actor is None: - raise PermissionDenied('Actor not authenticated') - - serializer = serializers.ActivitySerializer( - data=data, context={'actor': actor}) - serializer.is_valid(raise_exception=True) - - ac = serializer.validated_data - logger.info('Received activity on %s inbox', self.id) - if ac['type'] == 'Create' and ac['object']['type'] == 'Note': - # we received a toot \o/ - command = self.parse_command(ac['object']['content']) - logger.debug('Parsed command: %s', command) - if command == 'ping': - self.handle_ping(ac, actor) - def parse_command(self, message): """ Remove any links or fancy markup to extract /command from @@ -175,7 +184,16 @@ class TestActor(SystemActor): except IndexError: return - def handle_ping(self, ac, sender): + def handle_create(self, ac, sender): + if ac['object']['type'] != 'Note': + return + + # we received a toot \o/ + command = self.parse_command(ac['object']['content']) + logger.debug('Parsed command: %s', command) + if command != 'ping': + return + now = timezone.now() test_actor = self.get_actor_instance() reply_url = 'https://{}/activities/note/{}'.format( @@ -221,6 +239,31 @@ class TestActor(SystemActor): to=[ac['actor']], on_behalf_of=test_actor) + def handle_follow(self, ac, sender): + # on a follow we: + # 1. send the accept answer + # 2. 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) + follow_uuid = uuid.uuid4() + follow = activity.get_follow( + follow_id=follow_uuid, + follower=test_actor, + followed=sender) + activity.deliver( + follow, + to=[ac['actor']], + 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 88c86f79..6e621c7f 100644 --- a/api/funkwhale_api/federation/factories.py +++ b/api/funkwhale_api/federation/factories.py @@ -89,3 +89,20 @@ class NoteFactory(factory.Factory): class Meta: model = dict + + +@registry.register(name='federation.Activity') +class ActivityFactory(factory.Factory): + type = 'Create' + id = factory.Faker('url') + published = factory.LazyFunction( + lambda: timezone.now().isoformat() + ) + actor = factory.Faker('url') + object = factory.SubFactory( + NoteFactory, + actor=factory.SelfAttribute('..actor'), + published=factory.SelfAttribute('..published')) + + class Meta: + model = dict diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index 2137e8d9..7c35aead 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -120,7 +120,9 @@ class ActivitySerializer(serializers.Serializer): type = value['type'] except KeyError: raise serializers.ValidationError('Missing object type') - + except TypeError: + # probably a URL + return value try: object_serializer = OBJECT_SERIALIZERS[type] except KeyError: diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index e72232fc..f0d8f784 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -1,4 +1,5 @@ import pytest +import uuid from django.urls import reverse from django.utils import timezone @@ -127,7 +128,7 @@ def test_test_post_outbox_validates_actor(nodb_factories): assert msg in exc_info.value -def test_test_post_outbox_handles_create_note( +def test_test_post_inbox_handles_create_note( settings, mocker, factories): deliver = mocker.patch( 'funkwhale_api.federation.activity.deliver') @@ -238,3 +239,77 @@ def test_library_actor_manually_approves_based_on_setting( settings.FEDERATION_MUSIC_NEEDS_APPROVAL = value library_conf = actors.SYSTEM_ACTORS['library'] assert library_conf.manually_approves_followers is value + + +def test_system_actor_handle(mocker, nodb_factories): + handler = mocker.patch( + 'funkwhale_api.federation.actors.TestActor.handle_create') + actor = nodb_factories['federation.Actor']() + activity = nodb_factories['federation.Activity']( + type='Create', actor=actor.url) + serializer = serializers.ActivitySerializer( + data=activity + ) + assert serializer.is_valid() + actors.SYSTEM_ACTORS['test'].handle(activity, actor) + handler.assert_called_once_with(serializer.data, actor) + + +def test_test_actor_handles_follow( + settings, mocker, factories): + deliver = mocker.patch( + 'funkwhale_api.federation.activity.deliver') + actor = factories['federation.Actor']() + now = timezone.now() + mocker.patch('django.utils.timezone.now', return_value=now) + test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance() + data = { + 'actor': actor.url, + 'type': 'Follow', + 'id': 'http://test.federation/user#follows/267', + 'object': test_actor.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": 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': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + {} + ], + 'actor': test_actor.url, + 'id': test_actor.url + '#follows/{}'.format(uid), + 'object': actor.url, + 'type': 'Follow' + } + + actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor) + expected_calls = [ + mocker.call( + expected_accept, + to=[actor.url], + on_behalf_of=test_actor, + ), + mocker.call( + expected_follow, + to=[actor.url], + on_behalf_of=test_actor, + ) + ] + deliver.assert_has_calls(expected_calls) -- GitLab