Federation scanning
This commit is contained in:
parent
a3875e3918
commit
125d0eed5e
21 changed files with 450 additions and 80 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"]}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue