diff --git a/api/funkwhale_api/federation/api_serializers.py b/api/funkwhale_api/federation/api_serializers.py index 93dae4e55..2a1143ec7 100644 --- a/api/funkwhale_api/federation/api_serializers.py +++ b/api/funkwhale_api/federation/api_serializers.py @@ -27,6 +27,10 @@ class LibraryScanSerializer(serializers.ModelSerializer): ] +class DomainSerializer(serializers.Serializer): + name = serializers.CharField() + + class LibrarySerializer(serializers.ModelSerializer): actor = federation_serializers.APIActorSerializer() uploads_count = serializers.SerializerMethodField() diff --git a/api/funkwhale_api/federation/api_urls.py b/api/funkwhale_api/federation/api_urls.py index 896fa2430..4f0471b17 100644 --- a/api/funkwhale_api/federation/api_urls.py +++ b/api/funkwhale_api/federation/api_urls.py @@ -7,5 +7,6 @@ router.register(r"fetches", api_views.FetchViewSet, "fetches") router.register(r"follows/library", api_views.LibraryFollowViewSet, "library-follows") router.register(r"inbox", api_views.InboxItemViewSet, "inbox") router.register(r"libraries", api_views.LibraryViewSet, "libraries") +router.register(r"domains", api_views.DomainViewSet, "domains") urlpatterns = router.urls diff --git a/api/funkwhale_api/federation/api_views.py b/api/funkwhale_api/federation/api_views.py index 5f6f50d8f..395290be9 100644 --- a/api/funkwhale_api/federation/api_views.py +++ b/api/funkwhale_api/federation/api_views.py @@ -9,6 +9,8 @@ from rest_framework import permissions from rest_framework import response from rest_framework import viewsets +from funkwhale_api.common import preferences +from funkwhale_api.common.permissions import ConditionalAuthentication from funkwhale_api.music import models as music_models from funkwhale_api.users.oauth import permissions as oauth_permissions @@ -197,3 +199,22 @@ class FetchViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): queryset = models.Fetch.objects.select_related("actor") serializer_class = api_serializers.FetchSerializer permission_classes = [permissions.IsAuthenticated] + + +class DomainViewSet( + mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet +): + queryset = models.Domain.objects.order_by("name").external() + permission_classes = [ConditionalAuthentication] + serializer_class = api_serializers.DomainSerializer + ordering_fields = ("creation_date", "name") + max_page_size = 100 + + def get_queryset(self): + qs = super().get_queryset() + qs = qs.exclude( + instance_policy__is_active=True, instance_policy__block_all=True + ) + if preferences.get("moderation__allow_list_enabled"): + qs = qs.filter(allowed=True) + return qs diff --git a/api/funkwhale_api/instance/nodeinfo.py b/api/funkwhale_api/instance/nodeinfo.py index 712578c3c..9da128de8 100644 --- a/api/funkwhale_api/instance/nodeinfo.py +++ b/api/funkwhale_api/instance/nodeinfo.py @@ -1,5 +1,7 @@ import memoize.djangocache +from django.urls import reverse + import funkwhale_api from funkwhale_api.common import preferences from funkwhale_api.federation import actors, models as federation_models @@ -18,6 +20,7 @@ def get(): share_stats = all_preferences.get("instance__nodeinfo_stats_enabled") allow_list_enabled = all_preferences.get("moderation__allow_list_enabled") allow_list_public = all_preferences.get("moderation__allow_list_public") + auth_required = all_preferences.get("common__api_authentication_required") banner = all_preferences.get("instance__banner") unauthenticated_report_types = all_preferences.get( "moderation__unauthenticated_report_types" @@ -67,6 +70,7 @@ def get(): "instance__funkwhale_support_message_enabled" ), "instanceSupportMessage": all_preferences.get("instance__support_message"), + "knownNodesListUrl": None, }, } @@ -87,4 +91,8 @@ def get(): "favorites": {"tracks": {"total": statistics["track_favorites"]}}, "listenings": {"total": statistics["listenings"]}, } + if not auth_required: + data["metadata"]["knownNodesListUrl"] = federation_utils.full_url( + reverse("api:v1:federation:domains-list") + ) return data diff --git a/api/tests/federation/test_api_views.py b/api/tests/federation/test_api_views.py index c34c5e99a..1795a65e4 100644 --- a/api/tests/federation/test_api_views.py +++ b/api/tests/federation/test_api_views.py @@ -179,3 +179,21 @@ def test_can_detail_fetch(logged_in_api_client, factories): assert response.status_code == 200 assert response.data == expected + + +def test_user_can_list_domains(factories, api_client, preferences): + preferences["common__api_authentication_required"] = False + allowed = factories["federation.Domain"]() + factories["moderation.InstancePolicy"]( + actor=None, for_domain=True, block_all=True + ).target_domain + url = reverse("api:v1:federation:domains-list") + response = api_client.get(url) + + expected = { + "count": 1, + "next": None, + "previous": None, + "results": [api_serializers.DomainSerializer(allowed).data], + } + assert response.data == expected diff --git a/api/tests/instance/test_nodeinfo.py b/api/tests/instance/test_nodeinfo.py index 2e9075d26..bdca8f453 100644 --- a/api/tests/instance/test_nodeinfo.py +++ b/api/tests/instance/test_nodeinfo.py @@ -1,5 +1,7 @@ import pytest +from django.urls import reverse + import funkwhale_api from funkwhale_api.instance import nodeinfo from funkwhale_api.federation import actors @@ -10,6 +12,7 @@ from funkwhale_api.music import utils as music_utils def test_nodeinfo_dump(preferences, mocker, avatar): preferences["instance__banner"] = avatar preferences["instance__nodeinfo_stats_enabled"] = True + preferences["common__api_authentication_required"] = False preferences["moderation__unauthenticated_report_types"] = [ "takedown_request", "other", @@ -91,6 +94,9 @@ def test_nodeinfo_dump(preferences, mocker, avatar): "instance__funkwhale_support_message_enabled" ], "instanceSupportMessage": preferences["instance__support_message"], + "knownNodesListUrl": federation_utils.full_url( + reverse("api:v1:federation:domains-list") + ), }, } assert nodeinfo.get() == expected @@ -159,6 +165,7 @@ def test_nodeinfo_dump_stats_disabled(preferences, mocker): "instance__funkwhale_support_message_enabled" ], "instanceSupportMessage": preferences["instance__support_message"], + "knownNodesListUrl": None, }, } assert nodeinfo.get() == expected diff --git a/changes/changelog.d/known-nodes.enhancement b/changes/changelog.d/known-nodes.enhancement new file mode 100644 index 000000000..00497de4a --- /dev/null +++ b/changes/changelog.d/known-nodes.enhancement @@ -0,0 +1 @@ +Advertise list of known nodes on /api/v1/federation/domains and in nodeinfo if stats sharing is enabled