From b4ad7a4a716b0cfca0c3a19976794b2a4e97bbdd Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Mon, 7 May 2018 22:09:03 +0200
Subject: [PATCH] See #192: replaced old stats endpoint with nodeinfo

---
 .../instance/dynamic_preferences_registry.py  |  28 +++++
 api/funkwhale_api/instance/nodeinfo.py        |  74 ++++++++++++
 api/funkwhale_api/instance/urls.py            |   4 +-
 api/funkwhale_api/instance/views.py           |   9 +-
 api/tests/instance/test_nodeinfo.py           | 107 ++++++++++++++++++
 api/tests/instance/test_stats.py              |  10 --
 api/tests/instance/test_views.py              |  22 ++++
 7 files changed, 239 insertions(+), 15 deletions(-)
 create mode 100644 api/funkwhale_api/instance/nodeinfo.py
 create mode 100644 api/tests/instance/test_nodeinfo.py
 create mode 100644 api/tests/instance/test_views.py

diff --git a/api/funkwhale_api/instance/dynamic_preferences_registry.py b/api/funkwhale_api/instance/dynamic_preferences_registry.py
index 1d11a298..03555b0b 100644
--- a/api/funkwhale_api/instance/dynamic_preferences_registry.py
+++ b/api/funkwhale_api/instance/dynamic_preferences_registry.py
@@ -68,3 +68,31 @@ class RavenEnabled(types.BooleanPreference):
         'Wether error reporting to a Sentry instance using raven is enabled'
         ' for front-end errors'
     )
+
+
+@global_preferences_registry.register
+class InstanceNodeinfoEnabled(types.BooleanPreference):
+    show_in_api = False
+    section = instance
+    name = 'nodeinfo_enabled'
+    default = True
+    verbose_name = 'Enable nodeinfo endpoint'
+    help_text = (
+        'This endpoint is needed for your about page to work.'
+        'It\'s also helpful for the various monitoring '
+        'tools that map and analyzize the fediverse, '
+        'but you can disable it completely if needed.'
+    )
+
+
+@global_preferences_registry.register
+class InstanceNodeinfoStatsEnabled(types.BooleanPreference):
+    show_in_api = False
+    section = instance
+    name = 'nodeinfo_stats_enabled'
+    default = True
+    verbose_name = 'Enable usage and library stats in nodeinfo endpoint'
+    help_text = (
+        'Disable this f you don\'t want to share usage and library statistics'
+        'in the nodeinfo endpoint but don\'t want to disable it completely.'
+    )
diff --git a/api/funkwhale_api/instance/nodeinfo.py b/api/funkwhale_api/instance/nodeinfo.py
new file mode 100644
index 00000000..22d1a889
--- /dev/null
+++ b/api/funkwhale_api/instance/nodeinfo.py
@@ -0,0 +1,74 @@
+import memoize.djangocache
+
+import funkwhale_api
+from funkwhale_api.common import preferences
+
+from . import stats
+
+
+store = memoize.djangocache.Cache('default')
+memo = memoize.Memoizer(store, namespace='instance:stats')
+
+
+def get():
+    share_stats = preferences.get('instance__nodeinfo_stats_enabled')
+    data = {
+        'version': '2.0',
+        'software': {
+            'name': 'funkwhale',
+            'version': funkwhale_api.__version__
+        },
+        'protocols': ['activitypub'],
+        'services': {
+            'inbound': [],
+            'outbound': []
+        },
+        'openRegistrations': preferences.get('users__registration_enabled'),
+        'usage': {
+            'users': {
+                'total': 0,
+            },
+            'localPosts': 0,
+            'localComments': 0,
+        },
+        'metadata': {
+            'shortDescription': preferences.get('instance__short_description'),
+            'longDescription': preferences.get('instance__long_description'),
+            'name': preferences.get('instance__name'),
+            'library': {
+                'federationEnabled': preferences.get('federation__enabled'),
+                'federationNeedsApproval': preferences.get('federation__music_needs_approval'),
+            },
+        }
+    }
+    if share_stats:
+        getter = memo(
+            lambda: stats.get(),
+            max_age=600
+        )
+        statistics = getter()
+        data['usage']['users']['total'] = statistics['users']
+        data['metadata']['library']['tracks'] = {
+            'total': statistics['tracks'],
+        }
+        data['metadata']['library']['artists'] = {
+            'total': statistics['artists'],
+        }
+        data['metadata']['library']['albums'] = {
+            'total': statistics['albums'],
+        }
+        data['metadata']['library']['music'] = {
+            'hours': statistics['music_duration']
+        }
+
+        data['metadata']['usage'] = {
+            'favorites': {
+                'tracks': {
+                    'total': statistics['track_favorites'],
+                }
+            },
+            'listenings': {
+                'total': statistics['listenings']
+            }
+        }
+    return data
diff --git a/api/funkwhale_api/instance/urls.py b/api/funkwhale_api/instance/urls.py
index af23e7e0..e66fdf88 100644
--- a/api/funkwhale_api/instance/urls.py
+++ b/api/funkwhale_api/instance/urls.py
@@ -1,11 +1,9 @@
 from django.conf.urls import url
-from django.views.decorators.cache import cache_page
 
 from . import views
 
 
 urlpatterns = [
+    url(r'^nodeinfo/$', views.NodeInfo.as_view(), name='nodeinfo'),
     url(r'^settings/$', views.InstanceSettings.as_view(), name='settings'),
-    url(r'^stats/$',
-        cache_page(60 * 5)(views.InstanceStats.as_view()), name='stats'),
 ]
diff --git a/api/funkwhale_api/instance/views.py b/api/funkwhale_api/instance/views.py
index 7f8f393c..b40721c9 100644
--- a/api/funkwhale_api/instance/views.py
+++ b/api/funkwhale_api/instance/views.py
@@ -4,6 +4,9 @@ from rest_framework.response import Response
 from dynamic_preferences.api import serializers
 from dynamic_preferences.registries import global_preferences_registry
 
+from funkwhale_api.common import preferences
+
+from . import nodeinfo
 from . import stats
 
 
@@ -27,10 +30,12 @@ class InstanceSettings(views.APIView):
         return Response(data, status=200)
 
 
-class InstanceStats(views.APIView):
+class NodeInfo(views.APIView):
     permission_classes = []
     authentication_classes = []
 
     def get(self, request, *args, **kwargs):
-        data = stats.get()
+        if not preferences.get('instance__nodeinfo_enabled'):
+            return Response(status=404)
+        data = nodeinfo.get()
         return Response(data, status=200)
diff --git a/api/tests/instance/test_nodeinfo.py b/api/tests/instance/test_nodeinfo.py
new file mode 100644
index 00000000..5f5ca920
--- /dev/null
+++ b/api/tests/instance/test_nodeinfo.py
@@ -0,0 +1,107 @@
+from django.urls import reverse
+
+import funkwhale_api
+
+from funkwhale_api.instance import nodeinfo
+
+
+def test_nodeinfo_dump(preferences, mocker):
+    preferences['instance__nodeinfo_stats_enabled'] = True
+    stats = {
+        'users': 1,
+        'tracks': 2,
+        'albums': 3,
+        'artists': 4,
+        'track_favorites': 5,
+        'music_duration': 6,
+        'listenings': 7,
+    }
+    mocker.patch('funkwhale_api.instance.stats.get', return_value=stats)
+
+    expected = {
+        'version': '2.0',
+        'software': {
+            'name': 'funkwhale',
+            'version': funkwhale_api.__version__
+        },
+        'protocols': ['activitypub'],
+        'services': {
+            'inbound': [],
+            'outbound': []
+        },
+        'openRegistrations': preferences['users__registration_enabled'],
+        'usage': {
+            'users': {
+                'total': stats['users'],
+            },
+            'localPosts': 0,
+            'localComments': 0,
+        },
+        'metadata': {
+            'shortDescription': preferences['instance__short_description'],
+            'longDescription': preferences['instance__long_description'],
+            'name': preferences['instance__name'],
+            'library': {
+                'federationEnabled': preferences['federation__enabled'],
+                'federationNeedsApproval': preferences['federation__music_needs_approval'],
+                'tracks': {
+                    'total': stats['tracks'],
+                },
+                'artists': {
+                    'total': stats['artists'],
+                },
+                'albums': {
+                    'total': stats['albums'],
+                },
+                'music': {
+                    'hours': stats['music_duration']
+                },
+            },
+            'usage': {
+                'favorites': {
+                    'tracks': {
+                        'total': stats['track_favorites'],
+                    }
+                },
+                'listenings': {
+                    'total': stats['listenings']
+                }
+            }
+        }
+    }
+    assert nodeinfo.get() == expected
+
+
+def test_nodeinfo_dump_stats_disabled(preferences, mocker):
+    preferences['instance__nodeinfo_stats_enabled'] = False
+
+    expected = {
+        'version': '2.0',
+        'software': {
+            'name': 'funkwhale',
+            'version': funkwhale_api.__version__
+        },
+        'protocols': ['activitypub'],
+        'services': {
+            'inbound': [],
+            'outbound': []
+        },
+        'openRegistrations': preferences['users__registration_enabled'],
+        'usage': {
+            'users': {
+                'total': 0,
+            },
+            'localPosts': 0,
+            'localComments': 0,
+        },
+        'metadata': {
+            'shortDescription': preferences['instance__short_description'],
+            'longDescription': preferences['instance__long_description'],
+            'name': preferences['instance__name'],
+            'library': {
+                'federationEnabled': preferences['federation__enabled'],
+                'federationNeedsApproval': preferences['federation__music_needs_approval'],
+            },
+        }
+    }
+    assert nodeinfo.get() == expected
diff --git a/api/tests/instance/test_stats.py b/api/tests/instance/test_stats.py
index 6eaad76f..6063e930 100644
--- a/api/tests/instance/test_stats.py
+++ b/api/tests/instance/test_stats.py
@@ -3,16 +3,6 @@ from django.urls import reverse
 from funkwhale_api.instance import stats
 
 
-def test_can_get_stats_via_api(db, api_client, mocker):
-    stats = {
-        'foo': 'bar'
-    }
-    mocker.patch('funkwhale_api.instance.stats.get', return_value=stats)
-    url = reverse('api:v1:instance:stats')
-    response = api_client.get(url)
-    assert response.data == stats
-
-
 def test_get_users(mocker):
     mocker.patch(
         'funkwhale_api.users.models.User.objects.count', return_value=42)
diff --git a/api/tests/instance/test_views.py b/api/tests/instance/test_views.py
new file mode 100644
index 00000000..c67688d5
--- /dev/null
+++ b/api/tests/instance/test_views.py
@@ -0,0 +1,22 @@
+from django.urls import reverse
+
+
+def test_nodeinfo_endpoint(db, api_client, mocker):
+    payload = {
+        'test': 'test'
+    }
+    mocked_nodeinfo = mocker.patch(
+        'funkwhale_api.instance.nodeinfo.get', return_value=payload)
+    url = reverse('api:v1:instance:nodeinfo')
+    response = api_client.get(url)
+
+    assert response.status_code == 200
+    assert response.data == payload
+
+
+def test_nodeinfo_endpoint_disabled(db, api_client, preferences):
+    preferences['instance__nodeinfo_enabled'] = False
+    url = reverse('api:v1:instance:nodeinfo')
+    response = api_client.get(url)
+
+    assert response.status_code == 404
-- 
GitLab