From b4ad7a4a716b0cfca0c3a19976794b2a4e97bbdd Mon Sep 17 00:00:00 2001 From: Eliot Berriot 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 1d11a2988..03555b0be 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 000000000..22d1a8892 --- /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 af23e7e08..e66fdf88d 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 7f8f393c9..b40721c9c 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 000000000..5f5ca920c --- /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 6eaad76f7..6063e9300 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 000000000..c67688d53 --- /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