From a3505d2099ae4a7816b79b826c445b6f0eca5cae Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Fri, 14 Feb 2020 11:50:30 +0100
Subject: [PATCH] See #170: limit the amount of channels allowed per user

---
 .../audio/dynamic_preferences_registry.py     |  9 ++++++
 api/funkwhale_api/audio/serializers.py        |  6 ++++
 api/funkwhale_api/audio/views.py              |  2 ++
 api/tests/audio/test_serializers.py           | 28 +++++++++++++++++--
 front/src/views/admin/Settings.vue            |  9 ++++++
 5 files changed, 52 insertions(+), 2 deletions(-)

diff --git a/api/funkwhale_api/audio/dynamic_preferences_registry.py b/api/funkwhale_api/audio/dynamic_preferences_registry.py
index 8f9b096b0..3dfcc8558 100644
--- a/api/funkwhale_api/audio/dynamic_preferences_registry.py
+++ b/api/funkwhale_api/audio/dynamic_preferences_registry.py
@@ -14,3 +14,12 @@ class ChannelsEnabled(types.BooleanPreference):
         "If disabled, the channels feature will be completely switched off, "
         "and users won't be able to create channels or subscribe to them."
     )
+
+
+@global_preferences_registry.register
+class MaxChannels(types.IntegerPreference):
+    show_in_api = True
+    section = audio
+    default = 20
+    name = "max_channels"
+    verbose_name = "Max channels allowed per user"
diff --git a/api/funkwhale_api/audio/serializers.py b/api/funkwhale_api/audio/serializers.py
index 3f953436c..70c51587e 100644
--- a/api/funkwhale_api/audio/serializers.py
+++ b/api/funkwhale_api/audio/serializers.py
@@ -5,6 +5,7 @@ from rest_framework import serializers
 from funkwhale_api.common import serializers as common_serializers
 from funkwhale_api.common import utils as common_utils
 from funkwhale_api.common import locales
+from funkwhale_api.common import preferences
 from funkwhale_api.federation import serializers as federation_serializers
 from funkwhale_api.federation import utils as federation_utils
 from funkwhale_api.music import models as music_models
@@ -59,6 +60,11 @@ class ChannelCreateSerializer(serializers.Serializer):
     metadata = serializers.DictField(required=False)
 
     def validate(self, validated_data):
+        existing_channels = self.context["actor"].owned_channels.count()
+        if existing_channels >= preferences.get("audio__max_channels"):
+            raise serializers.ValidationError(
+                "You have reached the maximum amount of allowed channels"
+            )
         validated_data = super().validate(validated_data)
         metadata = validated_data.pop("metadata", {})
         if validated_data["content_category"] == "podcast":
diff --git a/api/funkwhale_api/audio/views.py b/api/funkwhale_api/audio/views.py
index 09ae6d0cf..91331d2f2 100644
--- a/api/funkwhale_api/audio/views.py
+++ b/api/funkwhale_api/audio/views.py
@@ -138,6 +138,8 @@ class ChannelViewSet(
             "update",
             "partial_update",
         ]
+        if self.request.user.is_authenticated:
+            context["actor"] = self.request.user.actor
         return context
 
 
diff --git a/api/tests/audio/test_serializers.py b/api/tests/audio/test_serializers.py
index 430673d63..2ada89653 100644
--- a/api/tests/audio/test_serializers.py
+++ b/api/tests/audio/test_serializers.py
@@ -23,7 +23,9 @@ def test_channel_serializer_create(factories):
         "content_category": "other",
     }
 
-    serializer = serializers.ChannelCreateSerializer(data=data)
+    serializer = serializers.ChannelCreateSerializer(
+        data=data, context={"actor": attributed_to}
+    )
     assert serializer.is_valid(raise_exception=True) is True
 
     channel = serializer.save(attributed_to=attributed_to)
@@ -49,6 +51,26 @@ def test_channel_serializer_create(factories):
     assert channel.library.actor == attributed_to
 
 
+def test_channel_serializer_create_honor_max_channels_setting(factories, preferences):
+    preferences["audio__max_channels"] = 1
+    attributed_to = factories["federation.Actor"](local=True)
+    factories["audio.Channel"](attributed_to=attributed_to)
+    data = {
+        # TODO: cover
+        "name": "My channel",
+        "username": "mychannel",
+        "description": {"text": "This is my channel", "content_type": "text/markdown"},
+        "tags": ["hello", "world"],
+        "content_category": "other",
+    }
+
+    serializer = serializers.ChannelCreateSerializer(
+        data=data, context={"actor": attributed_to}
+    )
+    with pytest.raises(serializers.serializers.ValidationError, match=r".*max.*"):
+        assert serializer.is_valid(raise_exception=True)
+
+
 def test_channel_serializer_create_podcast(factories):
     attributed_to = factories["federation.Actor"](local=True)
 
@@ -62,7 +84,9 @@ def test_channel_serializer_create_podcast(factories):
         "metadata": {"itunes_category": "Sports", "language": "en"},
     }
 
-    serializer = serializers.ChannelCreateSerializer(data=data)
+    serializer = serializers.ChannelCreateSerializer(
+        data=data, context={"actor": attributed_to}
+    )
     assert serializer.is_valid(raise_exception=True) is True
 
     channel = serializer.save(attributed_to=attributed_to)
diff --git a/front/src/views/admin/Settings.vue b/front/src/views/admin/Settings.vue
index fdab614ea..7f40a00e3 100644
--- a/front/src/views/admin/Settings.vue
+++ b/front/src/views/admin/Settings.vue
@@ -80,6 +80,7 @@ export default {
       let instanceLabel = this.$pgettext('Content/Admin/Menu','Instance information')
       let usersLabel = this.$pgettext('*/*/*/Noun', 'Users')
       let musicLabel = this.$pgettext('*/*/*/Noun', 'Music')
+      let channelsLabel = this.$pgettext('*/*/*', 'Channels')
       let playlistsLabel = this.$pgettext('*/*/*', 'Playlists')
       let federationLabel = this.$pgettext('*/*/*', 'Federation')
       let moderationLabel = this.$pgettext('*/Moderation/*', 'Moderation')
@@ -120,6 +121,14 @@ export default {
             {name: "music__transcoding_cache_duration"},
           ]
         },
+        {
+          label: channelsLabel,
+          id: "channels",
+          settings: [
+            {name: "audio__channels_enabled"},
+            {name: "audio__max_channels"},
+          ]
+        },
         {
           label: playlistsLabel,
           id: "playlists",
-- 
GitLab