kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
Model, view and serializer for instance-level policies
rodzic
ddffbeadfa
commit
0bc9bb65b0
|
@ -156,6 +156,7 @@ LOCAL_APPS = (
|
|||
"funkwhale_api.requests",
|
||||
"funkwhale_api.favorites",
|
||||
"funkwhale_api.federation",
|
||||
"funkwhale_api.moderation",
|
||||
"funkwhale_api.radios",
|
||||
"funkwhale_api.history",
|
||||
"funkwhale_api.playlists",
|
||||
|
|
|
@ -67,7 +67,7 @@ def create_user(actor):
|
|||
|
||||
|
||||
@registry.register
|
||||
class Domain(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
||||
class DomainFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
||||
name = factory.Faker("domain_name")
|
||||
|
||||
class Meta:
|
||||
|
@ -81,7 +81,7 @@ class ActorFactory(NoUpdateOnCreate, factory.DjangoModelFactory):
|
|||
private_key = None
|
||||
preferred_username = factory.Faker("user_name")
|
||||
summary = factory.Faker("paragraph")
|
||||
domain = factory.SubFactory(Domain)
|
||||
domain = factory.SubFactory(DomainFactory)
|
||||
fid = factory.LazyAttribute(
|
||||
lambda o: "https://{}/users/{}".format(o.domain.name, o.preferred_username)
|
||||
)
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class ActorRelatedField(serializers.EmailField):
|
||||
def to_representation(self, value):
|
||||
return value.full_username
|
||||
|
||||
def to_interal_value(self, value):
|
||||
value = super().to_interal_value(value)
|
||||
username, domain = value.split("@")
|
||||
try:
|
||||
return models.Actor.objects.get(
|
||||
preferred_username=username, domain_id=domain
|
||||
)
|
||||
except models.Actor.DoesNotExist:
|
||||
raise serializers.ValidationError("Invalid actor name")
|
|
@ -4,6 +4,7 @@ from funkwhale_api.common import fields
|
|||
from funkwhale_api.common import search
|
||||
|
||||
from funkwhale_api.federation import models as federation_models
|
||||
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
|
||||
|
||||
|
@ -87,3 +88,24 @@ class ManageInvitationFilterSet(filters.FilterSet):
|
|||
if value is None:
|
||||
return queryset
|
||||
return queryset.open(value)
|
||||
|
||||
|
||||
class ManageInstancePolicyFilterSet(filters.FilterSet):
|
||||
q = fields.SearchFilter(
|
||||
search_fields=[
|
||||
"summary",
|
||||
"target_domain__name",
|
||||
"target_actor__username",
|
||||
"target_actor__domain__name",
|
||||
]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = moderation_models.InstancePolicy
|
||||
fields = [
|
||||
"q",
|
||||
"block_all",
|
||||
"silence_activity",
|
||||
"silence_notifications",
|
||||
"reject_media",
|
||||
]
|
||||
|
|
|
@ -4,6 +4,8 @@ from rest_framework import serializers
|
|||
|
||||
from funkwhale_api.common import serializers as common_serializers
|
||||
from funkwhale_api.federation import models as federation_models
|
||||
from funkwhale_api.federation import fields as federation_fields
|
||||
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
|
||||
|
||||
|
@ -185,6 +187,13 @@ class ManageDomainSerializer(serializers.ModelSerializer):
|
|||
"outbox_activities_count",
|
||||
"nodeinfo",
|
||||
"nodeinfo_fetch_date",
|
||||
"instance_policy",
|
||||
]
|
||||
read_only_fields = [
|
||||
"creation_date",
|
||||
"instance_policy",
|
||||
"nodeinfo",
|
||||
"nodeinfo_fetch_date",
|
||||
]
|
||||
|
||||
def get_actors_count(self, o):
|
||||
|
@ -218,7 +227,62 @@ class ManageActorSerializer(serializers.ModelSerializer):
|
|||
"manually_approves_followers",
|
||||
"uploads_count",
|
||||
"user",
|
||||
"instance_policy",
|
||||
]
|
||||
read_only_fields = ["creation_date", "instance_policy"]
|
||||
|
||||
def get_uploads_count(self, o):
|
||||
return getattr(o, "uploads_count", 0)
|
||||
|
||||
|
||||
class TargetSerializer(serializers.Serializer):
|
||||
type = serializers.ChoiceField(choices=["domain", "actor"])
|
||||
id = serializers.CharField()
|
||||
|
||||
def to_representation(self, value):
|
||||
if value["type"] == "domain":
|
||||
return {"type": "domain", "id": value["obj"].name}
|
||||
if value["type"] == "actor":
|
||||
return {"type": "actor", "id": value["obj"].full_username}
|
||||
|
||||
def to_internal_value(self, value):
|
||||
if value["type"] == "domain":
|
||||
field = serializers.PrimaryKeyRelatedField(
|
||||
queryset=federation_models.Domain.objects.external()
|
||||
)
|
||||
if value["type"] == "actor":
|
||||
field = federation_fields.ActorRelatedField()
|
||||
value["obj"] = field.to_internal_value(value["id"])
|
||||
return value
|
||||
|
||||
|
||||
class ManageInstancePolicySerializer(serializers.ModelSerializer):
|
||||
target = TargetSerializer()
|
||||
actor = federation_fields.ActorRelatedField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = moderation_models.InstancePolicy
|
||||
fields = [
|
||||
"id",
|
||||
"uuid",
|
||||
"target",
|
||||
"creation_date",
|
||||
"actor",
|
||||
"summary",
|
||||
"is_active",
|
||||
"block_all",
|
||||
"silence_activity",
|
||||
"silence_notifications",
|
||||
"reject_media",
|
||||
]
|
||||
|
||||
read_only_fields = ["uuid", "id", "creation_date", "actor", "target"]
|
||||
|
||||
def validate(self, data):
|
||||
target = data.pop("target")
|
||||
if target["type"] == "domain":
|
||||
data["target_domain"] = target["obj"]
|
||||
if target["type"] == "actor":
|
||||
data["target_actor"] = target["obj"]
|
||||
|
||||
return data
|
||||
|
|
|
@ -5,8 +5,15 @@ from . import views
|
|||
|
||||
federation_router = routers.SimpleRouter()
|
||||
federation_router.register(r"domains", views.ManageDomainViewSet, "domains")
|
||||
|
||||
library_router = routers.SimpleRouter()
|
||||
library_router.register(r"uploads", views.ManageUploadViewSet, "uploads")
|
||||
|
||||
moderation_router = routers.SimpleRouter()
|
||||
moderation_router.register(
|
||||
r"instance-policies", views.ManageInstancePolicyViewSet, "instance-policies"
|
||||
)
|
||||
|
||||
users_router = routers.SimpleRouter()
|
||||
users_router.register(r"users", views.ManageUserViewSet, "users")
|
||||
users_router.register(r"invitations", views.ManageInvitationViewSet, "invitations")
|
||||
|
@ -20,5 +27,9 @@ urlpatterns = [
|
|||
include((federation_router.urls, "federation"), namespace="federation"),
|
||||
),
|
||||
url(r"^library/", include((library_router.urls, "instance"), namespace="library")),
|
||||
url(
|
||||
r"^moderation/",
|
||||
include((moderation_router.urls, "moderation"), namespace="moderation"),
|
||||
),
|
||||
url(r"^users/", include((users_router.urls, "instance"), namespace="users")),
|
||||
] + other_router.urls
|
||||
|
|
|
@ -6,6 +6,7 @@ from funkwhale_api.common import preferences
|
|||
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
|
||||
from funkwhale_api.moderation import models as moderation_models
|
||||
from funkwhale_api.users import models as users_models
|
||||
from funkwhale_api.users.permissions import HasUserPermission
|
||||
|
||||
|
@ -173,3 +174,26 @@ class ManageActorViewSet(
|
|||
def stats(self, request, *args, **kwargs):
|
||||
domain = self.get_object()
|
||||
return response.Response(domain.get_stats(), status=200)
|
||||
|
||||
|
||||
class ManageInstancePolicyViewSet(
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
mixins.CreateModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
queryset = (
|
||||
moderation_models.InstancePolicy.objects.all()
|
||||
.order_by("-creation_date")
|
||||
.select_related()
|
||||
)
|
||||
serializer_class = serializers.ManageInstancePolicySerializer
|
||||
filter_class = filters.ManageInstancePolicyFilterSet
|
||||
permission_classes = (HasUserPermission,)
|
||||
required_permissions = ["moderation"]
|
||||
ordering_fields = ["id", "creation_date"]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(actor=self.request.user.actor)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import factory
|
||||
|
||||
from funkwhale_api.factories import registry, NoUpdateOnCreate
|
||||
from funkwhale_api.federation import factories as federation_factories
|
||||
|
||||
|
||||
@registry.register
|
||||
class InstancePolicyFactory(NoUpdateOnCreate, factory.DjangoModelFactory):
|
||||
summary = factory.Faker("paragraph")
|
||||
actor = factory.SubFactory(federation_factories.ActorFactory)
|
||||
block_all = True
|
||||
is_active = True
|
||||
|
||||
class Meta:
|
||||
model = "moderation.InstancePolicy"
|
||||
|
||||
class Params:
|
||||
for_domain = factory.Trait(
|
||||
target_domain=factory.SubFactory(federation_factories.DomainFactory)
|
||||
)
|
||||
for_actor = factory.Trait(
|
||||
target_actor=factory.SubFactory(federation_factories.ActorFactory)
|
||||
)
|
|
@ -0,0 +1,35 @@
|
|||
# Generated by Django 2.0.9 on 2019-01-07 06:06
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('federation', '0016_auto_20181227_1605'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='InstancePolicy',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True)),
|
||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('summary', models.TextField(blank=True, max_length=10000, null=True)),
|
||||
('block_all', models.BooleanField(default=False)),
|
||||
('silence_activity', models.BooleanField(default=False)),
|
||||
('silence_notifications', models.BooleanField(default=False)),
|
||||
('reject_media', models.BooleanField(default=False)),
|
||||
('actor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_instance_policies', to='federation.Actor')),
|
||||
('target_actor', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='instance_policy', to='federation.Actor')),
|
||||
('target_domain', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='instance_policy', to='federation.Domain')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,63 @@
|
|||
import urllib.parse
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class InstancePolicyQuerySet(models.QuerySet):
|
||||
def active(self):
|
||||
return self.filter(is_active=True)
|
||||
|
||||
def matching_url(self, url):
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
return self.filter(
|
||||
models.Q(target_domain_id=parsed.hostname) | models.Q(target_actor__fid=url)
|
||||
)
|
||||
|
||||
|
||||
class InstancePolicy(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||
actor = models.ForeignKey(
|
||||
"federation.Actor",
|
||||
related_name="created_instance_policies",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
target_domain = models.OneToOneField(
|
||||
"federation.Domain",
|
||||
related_name="instance_policy",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
target_actor = models.OneToOneField(
|
||||
"federation.Actor",
|
||||
related_name="instance_policy",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
creation_date = models.DateTimeField(default=timezone.now)
|
||||
|
||||
is_active = models.BooleanField(default=True)
|
||||
# a summary explaining why the policy is in place
|
||||
summary = models.TextField(max_length=10000, null=True, blank=True)
|
||||
# either block everything (simpler, but less granularity)
|
||||
block_all = models.BooleanField(default=False)
|
||||
# or pick individual restrictions below
|
||||
# do not show in timelines/notifications, except for actual followers
|
||||
silence_activity = models.BooleanField(default=False)
|
||||
silence_notifications = models.BooleanField(default=False)
|
||||
# do not download any media from the target
|
||||
reject_media = models.BooleanField(default=False)
|
||||
|
||||
objects = InstancePolicyQuerySet.as_manager()
|
||||
|
||||
@property
|
||||
def target(self):
|
||||
if self.target_actor:
|
||||
return {"type": "actor", "obj": self.target_actor}
|
||||
if self.target_domain_id:
|
||||
return {"type": "domain", "obj": self.target_domain}
|
|
@ -49,6 +49,7 @@ def test_manage_domain_serializer(factories, now):
|
|||
"outbox_activities_count": 23,
|
||||
"nodeinfo": {},
|
||||
"nodeinfo_fetch_date": None,
|
||||
"instance_policy": None,
|
||||
}
|
||||
s = serializers.ManageDomainSerializer(domain)
|
||||
|
||||
|
@ -83,7 +84,57 @@ def test_manage_actor_serializer(factories, now):
|
|||
"manually_approves_followers": actor.manually_approves_followers,
|
||||
"full_username": actor.full_username,
|
||||
"user": None,
|
||||
"instance_policy": None,
|
||||
}
|
||||
s = serializers.ManageActorSerializer(actor)
|
||||
|
||||
assert s.data == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"factory_kwargs,expected",
|
||||
[
|
||||
(
|
||||
{"for_domain": True, "target_domain__name": "test.federation"},
|
||||
{"target": {"type": "domain", "id": "test.federation"}},
|
||||
),
|
||||
(
|
||||
{
|
||||
"for_actor": True,
|
||||
"target_actor__domain__name": "test.federation",
|
||||
"target_actor__preferred_username": "hello",
|
||||
},
|
||||
{"target": {"type": "actor", "id": "hello@test.federation"}},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_instance_policy_serializer_repr(factories, factory_kwargs, expected):
|
||||
policy = factories["moderation.InstancePolicy"](block_all=True, **factory_kwargs)
|
||||
|
||||
e = {
|
||||
"id": policy.id,
|
||||
"uuid": str(policy.uuid),
|
||||
"creation_date": policy.creation_date.isoformat().split("+")[0] + "Z",
|
||||
"actor": policy.actor.full_username,
|
||||
"block_all": True,
|
||||
"silence_activity": False,
|
||||
"silence_notifications": False,
|
||||
"reject_media": False,
|
||||
"is_active": policy.is_active,
|
||||
"summary": policy.summary,
|
||||
}
|
||||
e.update(expected)
|
||||
|
||||
assert serializers.ManageInstancePolicySerializer(policy).data == e
|
||||
|
||||
|
||||
def test_instance_policy_serializer_save_domain(factories):
|
||||
domain = factories["federation.Domain"]()
|
||||
|
||||
data = {"target": {"id": domain.name, "type": "domain"}, "block_all": True}
|
||||
|
||||
serializer = serializers.ManageInstancePolicySerializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
policy = serializer.save()
|
||||
|
||||
assert policy.target_domain == domain
|
||||
|
|
|
@ -14,6 +14,7 @@ from funkwhale_api.manage import serializers, views
|
|||
(views.ManageInvitationViewSet, ["settings"], "and"),
|
||||
(views.ManageDomainViewSet, ["moderation"], "and"),
|
||||
(views.ManageActorViewSet, ["moderation"], "and"),
|
||||
(views.ManageInstancePolicyViewSet, ["moderation"], "and"),
|
||||
],
|
||||
)
|
||||
def test_permissions(assert_user_permission, view, permissions, operator):
|
||||
|
@ -142,3 +143,19 @@ def test_actor_detail(factories, superuser_api_client):
|
|||
|
||||
assert response.status_code == 200
|
||||
assert response.data["id"] == actor.id
|
||||
|
||||
|
||||
def test_instance_policy_create(superuser_api_client, factories):
|
||||
domain = factories["federation.Domain"]()
|
||||
actor = superuser_api_client.user.create_actor()
|
||||
url = reverse("api:v1:manage:moderation:instance-policies-list")
|
||||
response = superuser_api_client.post(
|
||||
url,
|
||||
{"target": {"type": "domain", "id": domain.name}, "block_all": True},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
policy = domain.instance_policy
|
||||
assert policy.actor == actor
|
||||
|
|
Ładowanie…
Reference in New Issue