From 377f237fdb2cad00126168f4c0c0bf081f882537 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Thu, 10 Jan 2019 11:02:09 +0100
Subject: [PATCH] Rejecting media files on an instance or account now purge
 existing media

---
 api/funkwhale_api/federation/tasks.py         | 29 ++++++-----
 api/funkwhale_api/manage/serializers.py       | 16 +++++--
 api/tests/federation/test_tasks.py            | 39 ++++++++++++++-
 api/tests/manage/test_serializers.py          | 48 +++++++++++++------
 .../manage/moderation/InstancePolicyForm.vue  |  2 +-
 5 files changed, 102 insertions(+), 32 deletions(-)

diff --git a/api/funkwhale_api/federation/tasks.py b/api/funkwhale_api/federation/tasks.py
index 67f8fabc..d7c48957 100644
--- a/api/funkwhale_api/federation/tasks.py
+++ b/api/funkwhale_api/federation/tasks.py
@@ -198,27 +198,34 @@ def delete_qs(qs):
     )
 
 
-def handle_purge_actors(ids):
+def handle_purge_actors(ids, only=[]):
+    """
+    Empty only means we purge everything
+    Otherwise, we purge only the requested bits: media
+    """
     # purge follows (received emitted)
-    delete_qs(models.LibraryFollow.objects.filter(target__actor_id__in=ids))
-    delete_qs(models.LibraryFollow.objects.filter(actor_id__in=ids))
-    delete_qs(models.Follow.objects.filter(target_id__in=ids))
-    delete_qs(models.Follow.objects.filter(actor_id__in=ids))
+    if not only:
+        delete_qs(models.LibraryFollow.objects.filter(target__actor_id__in=ids))
+        delete_qs(models.Follow.objects.filter(actor_id__in=ids))
 
     # purge audio content
-    delete_qs(music_models.Upload.objects.filter(library__actor_id__in=ids))
-    delete_qs(music_models.Library.objects.filter(actor_id__in=ids))
+    if not only or "media" in only:
+        delete_qs(models.LibraryFollow.objects.filter(actor_id__in=ids))
+        delete_qs(models.Follow.objects.filter(target_id__in=ids))
+        delete_qs(music_models.Upload.objects.filter(library__actor_id__in=ids))
+        delete_qs(music_models.Library.objects.filter(actor_id__in=ids))
 
     # purge remaining activities / deliveries
-    delete_qs(models.InboxItem.objects.filter(actor_id__in=ids))
-    delete_qs(models.Activity.objects.filter(actor_id__in=ids))
+    if not only:
+        delete_qs(models.InboxItem.objects.filter(actor_id__in=ids))
+        delete_qs(models.Activity.objects.filter(actor_id__in=ids))
 
 
 @celery.app.task(name="federation.purge_actors")
-def purge_actors(ids=[], domains=[]):
+def purge_actors(ids=[], domains=[], only=[]):
     actors = models.Actor.objects.filter(
         Q(id__in=ids) | Q(domain_id__in=domains)
     ).order_by("id")
     found_ids = list(actors.values_list("id", flat=True))
     logger.info("Starting purging %s accounts", len(found_ids))
-    handle_purge_actors(ids=found_ids)
+    handle_purge_actors(ids=found_ids, only=only)
diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py
index e8d1a294..ed50d867 100644
--- a/api/funkwhale_api/manage/serializers.py
+++ b/api/funkwhale_api/manage/serializers.py
@@ -317,17 +317,25 @@ class ManageInstancePolicySerializer(serializers.ModelSerializer):
     @transaction.atomic
     def save(self, *args, **kwargs):
         instance = super().save(*args, **kwargs)
-        need_purge = self.instance.is_active and self.instance.block_all
-
+        need_purge = self.instance.is_active and (
+            self.instance.block_all or self.instance.reject_media
+        )
         if need_purge:
+            only = []
+            if self.instance.reject_media:
+                only.append("media")
             target = instance.target
             if target["type"] == "domain":
                 common_utils.on_commit(
-                    federation_tasks.purge_actors.delay, domains=[target["obj"].pk]
+                    federation_tasks.purge_actors.delay,
+                    domains=[target["obj"].pk],
+                    only=only,
                 )
             if target["type"] == "actor":
                 common_utils.on_commit(
-                    federation_tasks.purge_actors.delay, ids=[target["obj"].pk]
+                    federation_tasks.purge_actors.delay,
+                    ids=[target["obj"].pk],
+                    only=only,
                 )
 
         return instance
diff --git a/api/tests/federation/test_tasks.py b/api/tests/federation/test_tasks.py
index e5398106..5e10dfa5 100644
--- a/api/tests/federation/test_tasks.py
+++ b/api/tests/federation/test_tasks.py
@@ -223,11 +223,46 @@ def test_handle_purge_actors(factories, mocker):
             d.refresh_from_db()
 
 
+def test_handle_purge_actors_restrict_media(factories, mocker):
+    to_purge = factories["federation.Actor"]()
+    keeped = [
+        factories["music.Upload"](),
+        factories["federation.Activity"](),
+        factories["federation.InboxItem"](),
+        factories["federation.Follow"](),
+        factories["federation.LibraryFollow"](),
+        factories["federation.Activity"](actor=to_purge),
+        factories["federation.InboxItem"](actor=to_purge),
+        factories["federation.Follow"](actor=to_purge),
+    ]
+
+    library = factories["music.Library"](actor=to_purge)
+    deleted = [
+        library,
+        factories["music.Upload"](library=library),
+        factories["federation.LibraryFollow"](actor=to_purge),
+    ]
+
+    tasks.handle_purge_actors([to_purge.pk], only=["media"])
+
+    for k in keeped:
+        # this should not be deleted
+        k.refresh_from_db()
+
+    for d in deleted:
+        with pytest.raises(d.__class__.DoesNotExist):
+            d.refresh_from_db()
+
+
 def test_purge_actors(factories, mocker):
     handle_purge_actors = mocker.spy(tasks, "handle_purge_actors")
     factories["federation.Actor"]()
     to_delete = factories["federation.Actor"]()
     to_delete_domain = factories["federation.Actor"]()
-    tasks.purge_actors(ids=[to_delete.pk], domains=[to_delete_domain.domain.name])
+    tasks.purge_actors(
+        ids=[to_delete.pk], domains=[to_delete_domain.domain.name], only=["hello"]
+    )
 
-    handle_purge_actors.assert_called_once_with(ids=[to_delete.pk, to_delete_domain.pk])
+    handle_purge_actors.assert_called_once_with(
+        ids=[to_delete.pk, to_delete_domain.pk], only=["hello"]
+    )
diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py
index 1cb3ff90..53bc2504 100644
--- a/api/tests/manage/test_serializers.py
+++ b/api/tests/manage/test_serializers.py
@@ -175,65 +175,85 @@ def test_manage_domain_action_purge(factories, mocker):
     )
 
 
-def test_instance_policy_serializer_purges_target_domain(factories, mocker):
-    policy = factories["moderation.InstancePolicy"](for_domain=True, block_all=False)
+@pytest.mark.parametrize(
+    "param,expected_only", [("block_all", []), ("reject_media", ["media"])]
+)
+def test_instance_policy_serializer_purges_target_domain(
+    factories, mocker, param, expected_only
+):
+    params = {param: False}
+    if param != "block_all":
+        params["block_all"] = False
+    policy = factories["moderation.InstancePolicy"](for_domain=True, **params)
     on_commit = mocker.patch("funkwhale_api.common.utils.on_commit")
 
     serializer = serializers.ManageInstancePolicySerializer(
-        policy, data={"block_all": True}, partial=True
+        policy, data={param: True}, partial=True
     )
     serializer.is_valid(raise_exception=True)
     serializer.save()
 
     policy.refresh_from_db()
 
-    assert policy.block_all is True
+    assert getattr(policy, param) is True
     on_commit.assert_called_once_with(
-        federation_tasks.purge_actors.delay, domains=[policy.target_domain_id]
+        federation_tasks.purge_actors.delay,
+        domains=[policy.target_domain_id],
+        only=expected_only,
     )
 
     on_commit.reset_mock()
 
     # setting to false should have no effect
     serializer = serializers.ManageInstancePolicySerializer(
-        policy, data={"block_all": False}, partial=True
+        policy, data={param: False}, partial=True
     )
     serializer.is_valid(raise_exception=True)
     serializer.save()
 
     policy.refresh_from_db()
 
-    assert policy.block_all is False
+    assert getattr(policy, param) is False
     assert on_commit.call_count == 0
 
 
-def test_instance_policy_serializer_purges_target_actor(factories, mocker):
-    policy = factories["moderation.InstancePolicy"](for_actor=True, block_all=False)
+@pytest.mark.parametrize(
+    "param,expected_only", [("block_all", []), ("reject_media", ["media"])]
+)
+def test_instance_policy_serializer_purges_target_actor(
+    factories, mocker, param, expected_only
+):
+    params = {param: False}
+    if param != "block_all":
+        params["block_all"] = False
+    policy = factories["moderation.InstancePolicy"](for_actor=True, **params)
     on_commit = mocker.patch("funkwhale_api.common.utils.on_commit")
 
     serializer = serializers.ManageInstancePolicySerializer(
-        policy, data={"block_all": True}, partial=True
+        policy, data={param: True}, partial=True
     )
     serializer.is_valid(raise_exception=True)
     serializer.save()
 
     policy.refresh_from_db()
 
-    assert policy.block_all is True
+    assert getattr(policy, param) is True
     on_commit.assert_called_once_with(
-        federation_tasks.purge_actors.delay, ids=[policy.target_actor_id]
+        federation_tasks.purge_actors.delay,
+        ids=[policy.target_actor_id],
+        only=expected_only,
     )
 
     on_commit.reset_mock()
 
     # setting to false should have no effect
     serializer = serializers.ManageInstancePolicySerializer(
-        policy, data={"block_all": False}, partial=True
+        policy, data={param: False}, partial=True
     )
     serializer.is_valid(raise_exception=True)
     serializer.save()
 
     policy.refresh_from_db()
 
-    assert policy.block_all is False
+    assert getattr(policy, param) is False
     assert on_commit.call_count == 0
diff --git a/front/src/components/manage/moderation/InstancePolicyForm.vue b/front/src/components/manage/moderation/InstancePolicyForm.vue
index 52084bd5..98fd1d64 100644
--- a/front/src/components/manage/moderation/InstancePolicyForm.vue
+++ b/front/src/components/manage/moderation/InstancePolicyForm.vue
@@ -119,7 +119,7 @@ export default {
           label: this.$gettext("Silence notifications"),
         },
         rejectMedia: {
-          help: this.$gettext("Do not download any media file (audio, album cover, account avatar…) from this account or domain."),
+          help: this.$gettext("Do not download any media file (audio, album cover, account avatar…) from this account or domain. This will purge existing content as well."),
           label: this.$gettext("Reject media"),
         }
       }
-- 
GitLab