Skip to content
Snippets Groups Projects
routes.py 16.1 KiB
Newer Older
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"}],
        ),
    }