From d8f86c4fce52b6b610ac72e22bf658ecc1cbba3c Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Wed, 4 Apr 2018 19:38:55 +0200
Subject: [PATCH] Factorized follow logic between system actors, Library can
 now accept follows

---
 api/funkwhale_api/federation/activity.py | 19 +++++
 api/funkwhale_api/federation/actors.py   | 61 ++++++++--------
 api/tests/federation/test_activity.py    | 42 +++++++++++
 api/tests/federation/test_actors.py      | 89 ++++++++++++++----------
 4 files changed, 145 insertions(+), 66 deletions(-)

diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py
index 5a097401..1b03d19f 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 89c621ed..8871b101 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 a6e1d28a..09c5e3bf 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 c1b9d8a2..5ade9cdc 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)
-- 
GitLab