Skip to content
Snippets Groups Projects
routes.py 16.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • from django.db.models import Q
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    from funkwhale_api.music import models as music_models
    
    
    from . import activity
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    from . import actors
    
    from . import serializers
    
    logger = logging.getLogger(__name__)
    inbox = activity.InboxRouter()
    outbox = activity.OutboxRouter()
    
    
    def with_recipients(payload, to=[], cc=[]):
        if to:
            payload["to"] = to
        if cc:
            payload["cc"] = cc
        return payload
    
    
    @inbox.register({"type": "Follow"})
    def inbox_follow(payload, context):
        context["recipient"] = [
            ii.actor for ii in context["inbox_items"] if ii.type == "to"
        ][0]
        serializer = serializers.FollowSerializer(data=payload, context=context)
        if not serializer.is_valid(raise_exception=context.get("raise_exception", False)):
            logger.debug(
                "Discarding invalid follow from {}: %s",
                context["actor"].fid,
                serializer.errors,
            )
            return
    
        autoapprove = serializer.validated_data["object"].should_autoapprove_follow(
            context["actor"]
        )
    
        follow = serializer.save(approved=True if autoapprove else None)
        if follow.approved:
            outbox.dispatch({"type": "Accept"}, context={"follow": follow})
        return {"object": follow.target, "related_object": follow}
    
    
    
    @inbox.register({"type": "Accept"})
    def inbox_accept(payload, context):
        context["recipient"] = [
            ii.actor for ii in context["inbox_items"] if ii.type == "to"
        ][0]
        serializer = serializers.AcceptFollowSerializer(data=payload, context=context)
        if not serializer.is_valid(raise_exception=context.get("raise_exception", False)):
            logger.debug(
                "Discarding invalid accept from {}: %s",
                context["actor"].fid,
                serializer.errors,
            )
            return
    
        serializer.save()
    
        obj = serializer.validated_data["follow"]
        return {"object": obj, "related_object": obj.target}
    
    
    
    @outbox.register({"type": "Accept"})
    def outbox_accept(context):
        follow = context["follow"]
        if follow._meta.label == "federation.LibraryFollow":
            actor = follow.target.actor
        else:
            actor = follow.target
        payload = serializers.AcceptFollowSerializer(follow, context={"actor": actor}).data
    
        yield {
            "actor": actor,
            "type": "Accept",
            "payload": with_recipients(payload, to=[follow.actor]),
            "object": follow,
            "related_object": follow.target,
        }
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    @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: %s",
    
    Eliot Berriot's avatar
    Eliot Berriot committed
                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"]
        if follow._meta.label == "federation.LibraryFollow":
            target = follow.target.actor
        else:
            target = follow.target
        payload = serializers.FollowSerializer(follow, context={"actor": follow.actor}).data
    
        yield {
            "type": "Follow",
            "actor": follow.actor,
            "payload": with_recipients(payload, to=[target]),
            "object": follow.target,
            "related_object": follow,
        }
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
    
    @outbox.register({"type": "Create", "object.type": "Audio"})
    def outbox_create_audio(context):
        upload = context["upload"]
    
        channel = upload.library.get_channel()
        upload_serializer = (
            serializers.ChannelUploadSerializer if channel else serializers.UploadSerializer
        )
        followers_target = channel.actor if channel else upload.library
        actor = channel.actor if channel else upload.library.actor
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        serializer = serializers.ActivitySerializer(
            {
                "type": "Create",
    
                "actor": actor.fid,
                "object": upload_serializer(upload).data,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            }
        )
        yield {
            "type": "Create",
    
            "actor": actor,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "payload": with_recipients(
    
                serializer.data, to=[{"type": "followers", "target": followers_target}]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            ),
            "object": upload,
    
            "target": None if channel else upload.library,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        }
    
    
    @inbox.register({"type": "Create", "object.type": "Audio"})
    def inbox_create_audio(payload, context):
    
        is_channel = "library" not in payload["object"]
        if is_channel:
            channel = context["actor"].get_channel()
            serializer = serializers.ChannelUploadSerializer(
                data=payload["object"], context={"channel": channel},
            )
        else:
            serializer = serializers.UploadSerializer(
                data=payload["object"],
                context={"activity": context.get("activity"), "actor": context["actor"]},
            )
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        if not serializer.is_valid(raise_exception=context.get("raise_exception", False)):
            logger.warn("Discarding invalid audio create")
            return
    
        upload = serializer.save()
    
        if is_channel:
            return {"object": upload, "target": channel}
        else:
            return {"object": upload, "target": upload.library}
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
    
    @inbox.register({"type": "Delete", "object.type": "Library"})
    def inbox_delete_library(payload, context):
        actor = context["actor"]
        library_id = payload["object"].get("id")
        if not library_id:
            logger.debug("Discarding deletion of empty library")
            return
    
        try:
            library = actor.libraries.get(fid=library_id)
        except music_models.Library.DoesNotExist:
            logger.debug("Discarding deletion of unkwnown library %s", library_id)
            return
    
        library.delete()
    
    
    @outbox.register({"type": "Delete", "object.type": "Library"})
    def outbox_delete_library(context):
        library = context["library"]
        serializer = serializers.ActivitySerializer(
            {"type": "Delete", "object": {"type": "Library", "id": library.fid}}
        )
        yield {
            "type": "Delete",
            "actor": library.actor,
            "payload": with_recipients(
                serializer.data, to=[{"type": "followers", "target": library}]
            ),
        }
    
    
    
    @outbox.register({"type": "Update", "object.type": "Library"})
    def outbox_update_library(context):
        library = context["library"]
        serializer = serializers.ActivitySerializer(
            {"type": "Update", "object": serializers.LibrarySerializer(library).data}
        )
    
        yield {
            "type": "Update",
            "actor": library.actor,
            "payload": with_recipients(
                serializer.data, to=[{"type": "followers", "target": library}]
            ),
        }
    
    
    @inbox.register({"type": "Update", "object.type": "Library"})
    def inbox_update_library(payload, context):
        actor = context["actor"]
        library_id = payload["object"].get("id")
        if not library_id:
            logger.debug("Discarding deletion of empty library")
            return
    
        if not actor.libraries.filter(fid=library_id).exists():
            logger.debug("Discarding deletion of unkwnown library %s", library_id)
            return
    
        serializer = serializers.LibrarySerializer(data=payload["object"])
        if serializer.is_valid():
            serializer.save()
        else:
            logger.debug(
                "Discarding update of library %s because of payload errors: %s",
                library_id,
                serializer.errors,
            )
    
    
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    @inbox.register({"type": "Delete", "object.type": "Audio"})
    def inbox_delete_audio(payload, context):
        actor = context["actor"]
        try:
            upload_fids = [i for i in payload["object"]["id"]]
        except TypeError:
            # we did not receive a list of Ids, so we can probably use the value directly
            upload_fids = [payload["object"]["id"]]
    
    
        query = Q(fid__in=upload_fids) & (
            Q(library__actor=actor) | Q(track__artist__channel__actor=actor)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        )
    
        candidates = music_models.Upload.objects.filter(query)
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
        total = candidates.count()
        logger.info("Deleting %s uploads with ids %s", total, upload_fids)
        candidates.delete()
    
    
    @outbox.register({"type": "Delete", "object.type": "Audio"})
    def outbox_delete_audio(context):
        uploads = context["uploads"]
        library = uploads[0].library
    
        channel = library.get_channel()
        followers_target = channel.actor if channel else library
        actor = channel.actor if channel else library.actor
    
    Eliot Berriot's avatar
    Eliot Berriot committed
        serializer = serializers.ActivitySerializer(
            {
                "type": "Delete",
                "object": {"type": "Audio", "id": [u.get_federation_id() for u in uploads]},
            }
        )
        yield {
            "type": "Delete",
    
            "actor": actor,
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            "payload": with_recipients(
    
                serializer.data, to=[{"type": "followers", "target": followers_target}]
    
    Eliot Berriot's avatar
    Eliot Berriot committed
            ),
        }
    
    Eliot Berriot's avatar
    Eliot Berriot committed
    
    
    def handle_library_entry_update(payload, context, queryset, serializer_class):
        actor = context["actor"]
        obj_id = payload["object"].get("id")
        if not obj_id:
            logger.debug("Discarding update of empty obj")
            return
    
        try:
            obj = queryset.select_related("attributed_to").get(fid=obj_id)
        except queryset.model.DoesNotExist:
            logger.debug("Discarding update of unkwnown obj %s", obj_id)
            return
        if not actor.can_manage(obj):
            logger.debug(
                "Discarding unauthorize update of obj %s from %s", obj_id, actor.fid
            )
            return
    
        serializer = serializer_class(obj, data=payload["object"])
        if serializer.is_valid():
            serializer.save()
        else:
            logger.debug(
                "Discarding update of obj %s because of payload errors: %s",
                obj_id,
                serializer.errors,
            )
    
    
    @inbox.register({"type": "Update", "object.type": "Track"})
    def inbox_update_track(payload, context):
        return handle_library_entry_update(
            payload,
            context,
            queryset=music_models.Track.objects.all(),
            serializer_class=serializers.TrackSerializer,
        )
    
    
    @inbox.register({"type": "Update", "object.type": "Artist"})
    def inbox_update_artist(payload, context):
        return handle_library_entry_update(
            payload,
            context,
            queryset=music_models.Artist.objects.all(),
            serializer_class=serializers.ArtistSerializer,
        )
    
    
    @inbox.register({"type": "Update", "object.type": "Album"})
    def inbox_update_album(payload, context):
        return handle_library_entry_update(
            payload,
            context,
            queryset=music_models.Album.objects.all(),
            serializer_class=serializers.AlbumSerializer,
        )
    
    
    @outbox.register({"type": "Update", "object.type": "Track"})
    def outbox_update_track(context):
        track = context["track"]
        serializer = serializers.ActivitySerializer(
            {"type": "Update", "object": serializers.TrackSerializer(track).data}
        )
    
        yield {
            "type": "Update",
            "actor": actors.get_service_actor(),
            "payload": with_recipients(
                serializer.data,
                to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
            ),
        }
    
    
    
    @outbox.register({"type": "Update", "object.type": "Album"})
    def outbox_update_album(context):
        album = context["album"]
        serializer = serializers.ActivitySerializer(
            {"type": "Update", "object": serializers.AlbumSerializer(album).data}
        )
    
        yield {
            "type": "Update",
            "actor": actors.get_service_actor(),
            "payload": with_recipients(
                serializer.data,
                to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
            ),
        }
    
    
    @outbox.register({"type": "Update", "object.type": "Artist"})
    def outbox_update_artist(context):
        artist = context["artist"]
        serializer = serializers.ActivitySerializer(
            {"type": "Update", "object": serializers.ArtistSerializer(artist).data}
        )
    
        yield {
            "type": "Update",
            "actor": actors.get_service_actor(),
            "payload": with_recipients(
                serializer.data,
                to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
            ),
        }
    
    
    
    @outbox.register(
        {
            "type": "Delete",
            "object.type": [
                "Tombstone",
                "Actor",
                "Person",
                "Application",
                "Organization",
                "Service",
                "Group",
            ],
        }
    )
    def outbox_delete_actor(context):
        actor = context["actor"]
        serializer = serializers.ActivitySerializer(
            {"type": "Delete", "object": {"type": actor.type, "id": actor.fid}}
        )
        yield {
            "type": "Delete",
            "actor": actor,
            "payload": with_recipients(
                serializer.data,
                to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
            ),
        }
    
    
    @inbox.register(
        {
            "type": "Delete",
            "object.type": [
                "Tombstone",
                "Actor",
                "Person",
                "Application",
                "Organization",
                "Service",
                "Group",
            ],
        }
    )
    def inbox_delete_actor(payload, context):
        actor = context["actor"]
        serializer = serializers.ActorDeleteSerializer(data=payload)
        if not serializer.is_valid():
            logger.info("Skipped actor %s deletion, invalid payload", actor.fid)
            return
    
        deleted_fid = serializer.validated_data["fid"]
        try:
            # ensure the actor only can delete itself, and is a remote one
            actor = models.Actor.objects.local(False).get(fid=deleted_fid, pk=actor.pk)
        except models.Actor.DoesNotExist:
            logger.warn("Cannot delete actor %s, no matching object found", actor.fid)
            return
        actor.delete()
    
    
    
    @inbox.register({"type": "Flag"})
    def inbox_flag(payload, context):
        serializer = serializers.FlagSerializer(data=payload, context=context)
        if not serializer.is_valid(raise_exception=context.get("raise_exception", False)):
            logger.debug(
                "Discarding invalid report from {}: %s",
                context["actor"].fid,
                serializer.errors,
            )
            return
    
        report = serializer.save()
        return {"object": report.target, "related_object": report}
    
    
    @outbox.register({"type": "Flag"})
    def outbox_flag(context):
        report = context["report"]
        actor = actors.get_service_actor()
        serializer = serializers.FlagSerializer(report)
        yield {
            "type": "Flag",
            "actor": actor,
            "payload": with_recipients(
                serializer.data,
                # Mastodon requires the report to be sent to the reported actor inbox
                # (and not the shared inbox)
                to=[{"type": "actor_inbox", "actor": report.target_owner}],
            ),
        }
    
    
    
    @inbox.register({"type": "Delete", "object.type": "Album"})
    def inbox_delete_album(payload, context):
        actor = context["actor"]
        album_id = payload["object"].get("id")
        if not album_id:
            logger.debug("Discarding deletion of empty library")
            return
    
        query = Q(fid=album_id) & (Q(attributed_to=actor) | Q(artist__channel__actor=actor))
        try:
            album = music_models.Album.objects.get(query)
        except music_models.Album.DoesNotExist:
            logger.debug("Discarding deletion of unkwnown album %s", album_id)
            return
    
        album.delete()
    
    
    @outbox.register({"type": "Delete", "object.type": "Album"})
    def outbox_delete_album(context):
        album = context["album"]
        actor = (
            album.artist.channel.actor
            if album.artist.get_channel()
            else album.attributed_to
        )
        actor = actor or actors.get_service_actor()
        serializer = serializers.ActivitySerializer(
            {"type": "Delete", "object": {"type": "Album", "id": album.fid}}
        )
    
        yield {
            "type": "Delete",
            "actor": actor,
            "payload": with_recipients(
                serializer.data,
                to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
            ),
        }