Added actions and tasks to purge domains and actors

merge-requests/552/head
Eliot Berriot 2019-01-09 14:18:32 +01:00
rodzic 833daa242c
commit 233ac870be
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: DD6965E2476E5C27
10 zmienionych plików z 251 dodań i 14 usunięć

Wyświetl plik

@ -0,0 +1,14 @@
from rest_framework import response
from rest_framework.decorators import list_route
def action_route(serializer_class):
@list_route(methods=["post"])
def action(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = serializer_class(request.data, queryset=queryset)
serializer.is_valid(raise_exception=True)
result = serializer.save()
return response.Response(result, status=200)
return action

Wyświetl plik

@ -123,7 +123,7 @@ class ActionSerializer(serializers.Serializer):
if type(value) in [list, tuple]:
return self.queryset.filter(
**{"{}__in".format(self.pk_field): value}
).order_by("id")
).order_by(self.pk_field)
raise serializers.ValidationError(
"{} is not a valid value for objects. You must provide either a "

Wyświetl plik

@ -186,3 +186,39 @@ def update_domain_nodeinfo(domain):
domain.nodeinfo_fetch_date = now
domain.nodeinfo = nodeinfo
domain.save(update_fields=["nodeinfo", "nodeinfo_fetch_date"])
def delete_qs(qs):
label = qs.model._meta.label
result = qs.delete()
related = sum(result[1].values())
logger.info(
"Purged %s %s objects (and %s related entities)", result[0], label, related
)
def handle_purge_actors(ids):
# 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))
# 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))
# purge remaining activities / deliveries
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=[]):
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)

Wyświetl plik

@ -3,8 +3,10 @@ from django.db import transaction
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.federation import models as federation_models
from funkwhale_api.federation import fields as federation_fields
from funkwhale_api.federation import tasks as federation_tasks
from funkwhale_api.moderation import models as moderation_models
from funkwhale_api.music import models as music_models
from funkwhale_api.users import models as users_models
@ -203,6 +205,17 @@ class ManageDomainSerializer(serializers.ModelSerializer):
return getattr(o, "outbox_activities_count", 0)
class ManageDomainActionSerializer(common_serializers.ActionSerializer):
actions = [common_serializers.Action("purge", allow_all=False)]
filterset_class = filters.ManageDomainFilterSet
pk_field = "name"
@transaction.atomic
def handle_purge(self, objects):
ids = objects.values_list("pk", flat=True)
common_utils.on_commit(federation_tasks.purge_actors.delay, domains=list(ids))
class ManageActorSerializer(serializers.ModelSerializer):
uploads_count = serializers.SerializerMethodField()
user = ManageUserSerializer()
@ -235,6 +248,16 @@ class ManageActorSerializer(serializers.ModelSerializer):
return getattr(o, "uploads_count", 0)
class ManageActorActionSerializer(common_serializers.ActionSerializer):
actions = [common_serializers.Action("purge", allow_all=False)]
filterset_class = filters.ManageActorFilterSet
@transaction.atomic
def handle_purge(self, objects):
ids = objects.values_list("id", flat=True)
common_utils.on_commit(federation_tasks.purge_actors.delay, ids=list(ids))
class TargetSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=["domain", "actor"])
id = serializers.CharField()
@ -279,10 +302,39 @@ class ManageInstancePolicySerializer(serializers.ModelSerializer):
read_only_fields = ["uuid", "id", "creation_date", "actor", "target"]
def validate(self, data):
target = data.pop("target")
try:
target = data.pop("target")
except KeyError:
# partial update
return data
if target["type"] == "domain":
data["target_domain"] = target["obj"]
if target["type"] == "actor":
data["target_actor"] = target["obj"]
return data
@transaction.atomic
def save(self, *args, **kwargs):
block_all = self.validated_data.get("block_all", False)
need_purge = (
# we purge when we create with block all
(not self.instance and block_all)
or
# or when block all value switch from False to True
(self.instance and block_all and not self.instance.block_all)
)
instance = super().save(*args, **kwargs)
if need_purge:
target = instance.target
if target["type"] == "domain":
common_utils.on_commit(
federation_tasks.purge_actors.delay, domains=[target["obj"].pk]
)
if target["type"] == "actor":
common_utils.on_commit(
federation_tasks.purge_actors.delay, ids=[target["obj"].pk]
)
return instance

Wyświetl plik

@ -2,7 +2,7 @@ from rest_framework import mixins, response, viewsets
from rest_framework.decorators import detail_route, list_route
from django.shortcuts import get_object_or_404
from funkwhale_api.common import preferences
from funkwhale_api.common import preferences, decorators
from funkwhale_api.federation import models as federation_models
from funkwhale_api.federation import tasks as federation_tasks
from funkwhale_api.music import models as music_models
@ -135,6 +135,8 @@ class ManageDomainViewSet(
domain = self.get_object()
return response.Response(domain.get_stats(), status=200)
action = decorators.action_route(serializers.ManageDomainActionSerializer)
class ManageActorViewSet(
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
@ -175,6 +177,8 @@ class ManageActorViewSet(
domain = self.get_object()
return response.Response(domain.get_stats(), status=200)
action = decorators.action_route(serializers.ManageActorActionSerializer)
class ManageInstancePolicyViewSet(
mixins.ListModelMixin,

Wyświetl plik

@ -190,3 +190,44 @@ def test_update_domain_nodeinfo_error(factories, r_mock, now):
"status": "error",
"error": "500 Server Error: None for url: {}".format(wellknown_url),
}
def test_handle_purge_actors(factories, mocker):
to_purge = factories["federation.Actor"]()
keeped = [
factories["music.Upload"](),
factories["federation.Activity"](),
factories["federation.InboxItem"](),
factories["federation.Follow"](),
factories["federation.LibraryFollow"](),
]
library = factories["music.Library"](actor=to_purge)
deleted = [
library,
factories["music.Upload"](library=library),
factories["federation.Activity"](actor=to_purge),
factories["federation.InboxItem"](actor=to_purge),
factories["federation.Follow"](actor=to_purge),
factories["federation.LibraryFollow"](actor=to_purge),
]
tasks.handle_purge_actors([to_purge.pk])
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])
handle_purge_actors.assert_called_once_with(ids=[to_delete.pk, to_delete_domain.pk])

Wyświetl plik

@ -1,6 +1,7 @@
import pytest
from funkwhale_api.manage import serializers
from funkwhale_api.federation import tasks as federation_tasks
def test_manage_upload_action_delete(factories):
@ -138,3 +139,89 @@ def test_instance_policy_serializer_save_domain(factories):
policy = serializer.save()
assert policy.target_domain == domain
def test_manage_actor_action_purge(factories, mocker):
actors = factories["federation.Actor"].create_batch(size=3)
s = serializers.ManageActorActionSerializer(queryset=None)
on_commit = mocker.patch("funkwhale_api.common.utils.on_commit")
s.handle_purge(actors[0].__class__.objects.all())
on_commit.assert_called_once_with(
federation_tasks.purge_actors.delay, ids=[a.pk for a in actors]
)
def test_manage_domain_action_purge(factories, mocker):
domains = factories["federation.Domain"].create_batch(size=3)
s = serializers.ManageDomainActionSerializer(queryset=None)
on_commit = mocker.patch("funkwhale_api.common.utils.on_commit")
s.handle_purge(domains[0].__class__.objects.all())
on_commit.assert_called_once_with(
federation_tasks.purge_actors.delay, domains=[d.pk for d in domains]
)
def test_instance_policy_serializer_purges_target_domain(factories, mocker):
policy = factories["moderation.InstancePolicy"](for_domain=True, block_all=False)
on_commit = mocker.patch("funkwhale_api.common.utils.on_commit")
serializer = serializers.ManageInstancePolicySerializer(
policy, data={"block_all": True}, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
policy.refresh_from_db()
assert policy.block_all is True
on_commit.assert_called_once_with(
federation_tasks.purge_actors.delay, domains=[policy.target_domain_id]
)
on_commit.reset_mock()
# setting to false should have no effect
serializer = serializers.ManageInstancePolicySerializer(
policy, data={"block_all": False}, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
policy.refresh_from_db()
assert policy.block_all 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)
on_commit = mocker.patch("funkwhale_api.common.utils.on_commit")
serializer = serializers.ManageInstancePolicySerializer(
policy, data={"block_all": True}, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
policy.refresh_from_db()
assert policy.block_all is True
on_commit.assert_called_once_with(
federation_tasks.purge_actors.delay, ids=[policy.target_actor_id]
)
on_commit.reset_mock()
# setting to false should have no effect
serializer = serializers.ManageInstancePolicySerializer(
policy, data={"block_all": False}, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
policy.refresh_from_db()
assert policy.block_all is False
assert on_commit.call_count == 0

Wyświetl plik

@ -78,6 +78,7 @@
:current="page"
:paginate-by="paginateBy"
:total="result.count"
action-url="manage/accounts/action/"
></pagination>
<span v-if="result && result.results.length > 0">
@ -178,11 +179,11 @@ export default {
},
actions () {
return [
// {
// name: 'delete',
// label: this.$gettext('Delete'),
// isDangerous: true
// }
{
name: 'purge',
label: this.$gettext('Purge'),
isDangerous: true
}
]
}
},

Wyświetl plik

@ -32,6 +32,8 @@
@action-launched="fetchData"
:objects-data="result"
:actions="actions"
action-url="manage/federation/domains/action/"
idField="name"
:filters="actionFilters">
<template slot="header-cells">
<th><translate>Name</translate></th>
@ -157,11 +159,11 @@ export default {
},
actions () {
return [
// {
// name: 'delete',
// label: this.$gettext('Delete'),
// isDangerous: true
// }
{
name: 'purge',
label: this.$gettext('Purge'),
isDangerous: true
}
]
}
},

Wyświetl plik

@ -107,7 +107,7 @@ export default {
return {
summaryHelp: this.$gettext("Explain why you're applying this policy. Depending on your instance configuration, this will help you remember why you acted on this account or domain, and may be displayed publicly to help users understand what moderation rules are in place."),
isActiveHelp: this.$gettext("Use this setting to temporarily enable/disable the policy without completely removing it."),
blockAllHelp: this.$gettext("Block everything from this account or domain. This will prevent any interaction with the entity."),
blockAllHelp: this.$gettext("Block everything from this account or domain. This will prevent any interaction with the entity, and purge related content (uploads, libraries, follows, etc.)"),
silenceActivity: {
help: this.$gettext("Hide account or domain content, except from followers."),
label: this.$gettext("Silence activity"),