Federation scanning

This commit is contained in:
Eliot Berriot 2018-09-24 18:44:22 +00:00
commit 125d0eed5e
21 changed files with 450 additions and 80 deletions

View file

@ -14,9 +14,23 @@ class NestedLibraryFollowSerializer(serializers.ModelSerializer):
fields = ["creation_date", "uuid", "fid", "approved", "modification_date"]
class LibraryScanSerializer(serializers.ModelSerializer):
class Meta:
model = music_models.LibraryScan
fields = [
"total_files",
"processed_files",
"errored_files",
"status",
"creation_date",
"modification_date",
]
class LibrarySerializer(serializers.ModelSerializer):
actor = federation_serializers.APIActorSerializer()
uploads_count = serializers.SerializerMethodField()
latest_scan = serializers.SerializerMethodField()
follow = serializers.SerializerMethodField()
class Meta:
@ -31,6 +45,7 @@ class LibrarySerializer(serializers.ModelSerializer):
"uploads_count",
"privacy_level",
"follow",
"latest_scan",
]
def get_uploads_count(self, o):
@ -42,6 +57,11 @@ class LibrarySerializer(serializers.ModelSerializer):
except (AttributeError, IndexError):
return None
def get_latest_scan(self, o):
scan = o.scans.order_by("-creation_date").first()
if scan:
return LibraryScanSerializer(scan).data
class LibraryFollowSerializer(serializers.ModelSerializer):
target = common_serializers.RelatedField("uuid", LibrarySerializer(), required=True)
@ -54,6 +74,9 @@ class LibraryFollowSerializer(serializers.ModelSerializer):
def validate_target(self, v):
actor = self.context["actor"]
if v.actor == actor:
raise serializers.ValidationError("You cannot follow your own library")
if v.received_follows.filter(actor=actor).exists():
raise serializers.ValidationError("You are already following this library")
return v

View file

@ -31,13 +31,14 @@ class LibraryFollowViewSet(
mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
lookup_field = "uuid"
queryset = (
models.LibraryFollow.objects.all()
.order_by("-creation_date")
.select_related("target__actor", "actor")
.select_related("actor", "target__actor")
)
serializer_class = api_serializers.LibraryFollowSerializer
permission_classes = [permissions.IsAuthenticated]
@ -52,6 +53,13 @@ class LibraryFollowViewSet(
follow = serializer.save(actor=self.request.user.actor)
routes.outbox.dispatch({"type": "Follow"}, context={"follow": follow})
@transaction.atomic
def perform_destroy(self, instance):
routes.outbox.dispatch(
{"type": "Undo", "object": {"type": "Follow"}}, context={"follow": instance}
)
instance.delete()
def get_serializer_context(self):
context = super().get_serializer_context()
context["actor"] = self.request.user.actor
@ -96,8 +104,25 @@ class LibraryViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
qs = super().get_queryset()
return qs.viewable_by(actor=self.request.user.actor)
@decorators.list_route(methods=["post"])
@decorators.detail_route(methods=["post"])
def scan(self, request, *args, **kwargs):
library = self.get_object()
if library.actor.is_local:
return response.Response({"status": "skipped"}, 200)
scan = library.schedule_scan(actor=request.user.actor)
if scan:
return response.Response(
{
"status": "scheduled",
"scan": api_serializers.LibraryScanSerializer(scan).data,
},
200,
)
return response.Response({"status": "skipped"}, 200)
@decorators.list_route(methods=["post"])
def fetch(self, request, *args, **kwargs):
try:
fid = request.data["fid"]
except KeyError:
@ -110,7 +135,7 @@ class LibraryViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
)
except requests.exceptions.RequestException as e:
return response.Response(
{"detail": "Error while scanning the library: {}".format(str(e))},
{"detail": "Error while fetching the library: {}".format(str(e))},
status=400,
)
except serializers.serializers.ValidationError as e:

View file

@ -90,7 +90,7 @@ def get_library_data(library_url, actor):
return {"errors": ["Permission denied while scanning library"]}
elif scode >= 400:
return {"errors": ["Error {} while fetching the library".format(scode)]}
serializer = serializers.PaginatedCollectionSerializer(data=response.json())
serializer = serializers.LibrarySerializer(data=response.json())
if not serializer.is_valid():
return {"errors": ["Invalid ActivityPub response from remote library"]}

View file

@ -77,6 +77,38 @@ def outbox_accept(context):
}
@inbox.register({"type": "Undo", "object.type": "Follow"})
def inbox_undo_follow(payload, context):
serializer = serializers.UndoFollowSerializer(data=payload, context=context)
if not serializer.is_valid(raise_exception=context.get("raise_exception", False)):
logger.debug(
"Discarding invalid follow undo from {}: %s",
context["actor"].fid,
serializer.errors,
)
return
serializer.save()
@outbox.register({"type": "Undo", "object.type": "Follow"})
def outbox_undo_follow(context):
follow = context["follow"]
actor = follow.actor
if follow._meta.label == "federation.LibraryFollow":
recipient = follow.target.actor
else:
recipient = follow.target
payload = serializers.UndoFollowSerializer(follow, context={"actor": actor}).data
yield {
"actor": actor,
"type": "Undo",
"payload": with_recipients(payload, to=[recipient]),
"object": follow,
"related_object": follow.target,
}
@outbox.register({"type": "Follow"})
def outbox_follow(context):
follow = context["follow"]

View file

@ -343,7 +343,7 @@ class AcceptFollowSerializer(serializers.Serializer):
follow.approved = True
follow.save()
if follow.target._meta.label == "music.Library":
follow.target.schedule_scan()
follow.target.schedule_scan(actor=follow.actor)
return follow
@ -354,7 +354,8 @@ class UndoFollowSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=["Undo"])
def validate_actor(self, v):
expected = self.context.get("follow_target")
expected = self.context.get("actor")
if expected and expected.fid != v:
raise serializers.ValidationError("Invalid actor")
try:
@ -366,11 +367,19 @@ class UndoFollowSerializer(serializers.Serializer):
# we ensure the accept actor actually match the follow actor
if validated_data["actor"] != validated_data["object"]["actor"]:
raise serializers.ValidationError("Actor mismatch")
target = validated_data["object"]["object"]
if target._meta.label == "music.Library":
follow_class = models.LibraryFollow
else:
follow_class = models.Follow
try:
validated_data["follow"] = models.Follow.objects.filter(
actor=validated_data["actor"], target=validated_data["object"]["object"]
validated_data["follow"] = follow_class.objects.filter(
actor=validated_data["actor"], target=target
).get()
except models.Follow.DoesNotExist:
except follow_class.DoesNotExist:
raise serializers.ValidationError("No follow to remove")
return validated_data
@ -545,7 +554,7 @@ class LibrarySerializer(PaginatedCollectionSerializer):
"summary": library.description,
"page_size": 100,
"actor": library.actor,
"items": library.uploads.filter(import_status="finished"),
"items": library.uploads.for_federation(),
"type": "Library",
}
r = super().to_representation(conf)
@ -599,9 +608,10 @@ class CollectionPageSerializer(serializers.Serializer):
raw_items = [item_serializer(data=i, context=self.context) for i in v]
valid_items = []
for i in raw_items:
if i.is_valid():
try:
i.is_valid(raise_exception=True)
valid_items.append(i)
else:
except serializers.ValidationError:
logger.debug("Invalid item %s: %s", i.data, i.errors)
return valid_items

View file

@ -191,7 +191,7 @@ class MusicLibraryViewSet(
authentication_classes = [authentication.SignatureAuthentication]
permission_classes = []
renderer_classes = [renderers.ActivityPubRenderer]
serializer_class = serializers.PaginatedCollectionSerializer
serializer_class = serializers.LibrarySerializer
queryset = music_models.Library.objects.all().select_related("actor")
lookup_field = "uuid"
@ -203,7 +203,7 @@ class MusicLibraryViewSet(
"actor": lb.actor,
"name": lb.name,
"summary": lb.description,
"items": lb.uploads.order_by("-creation_date"),
"items": lb.uploads.for_federation().order_by("-creation_date"),
"item_serializer": serializers.UploadSerializer,
}
page = request.GET.get("page")