feat(back):trigger public library follow on user follow

environments/review-docs-2422-qjbh7w/deployments/21461
Petitminion 2025-09-17 01:48:01 +02:00 zatwierdzone przez petitminion
rodzic b41c7fbaf1
commit 4db52025b9
6 zmienionych plików z 128 dodań i 2 usunięć

Wyświetl plik

@ -9,6 +9,7 @@ from rest_framework.exceptions import NotFound as RestNotFound
from funkwhale_api.common import preferences
from funkwhale_api.common import utils as common_utils
from funkwhale_api.common.permissions import ConditionalAuthentication
from funkwhale_api.federation import actors
from funkwhale_api.music import models as music_models
from funkwhale_api.music import serializers as music_serializers
from funkwhale_api.music import views as music_views
@ -359,6 +360,15 @@ class UserFollowViewSet(
def perform_create(self, serializer):
follow = serializer.save(actor=self.request.user.actor)
routes.outbox.dispatch({"type": "Follow"}, context={"follow": follow})
if not follow.target.is_local:
public_lib = utils.get_or_create_buildin_actor_library(
follow.target, privacy_level="everyone"
)
lib_follow = models.LibraryFollow.objects.create(
actor=actors.get_service_actor(),
target=public_lib,
)
routes.outbox.dispatch({"type": "Follow"}, context={"follow": lib_follow})
@transaction.atomic
def perform_destroy(self, instance):

Wyświetl plik

@ -290,7 +290,52 @@ def can_manage(obj_owner, actor):
return False
<<<<<<< HEAD
def update_actor_privacy(actor, privacy_level):
actor.track_favorites.update(privacy_level=privacy_level)
actor.listenings.update(privacy_level=privacy_level)
# to do : trigger federation privacy_level downgrade #2336
=======
class BuildInLibException(Exception):
pass
def get_or_create_builtin_actor_library(actor, privacy_level):
from funkwhale_api.music import models as music_models
from . import actors
service_actor = actors.get_service_actor()
auth = signing.get_auth(service_actor.private_key, service_actor.private_key_id)
response = session.get_session().get(
f"https://{actor.domain}/api/v2/federation/music/libraries",
auth=auth,
params={
"actor": actor.preferred_username,
"privacy_level": privacy_level,
"name": privacy_level,
},
headers={
"Accept": "application/activity+json",
"Content-Type": "application/activity+json",
},
)
response.raise_for_status()
data = response.json()
if len(data["results"]) == 0:
raise BuildInLibException(
f"Could not find built-in lib {privacy_level} for actor {actor}"
)
elif not len(data["results"]) == 1:
raise BuildInLibException(
f"Too many built-in lib {privacy_level} for actor {actor}"
)
else:
lib, created = music_models.Library.objects.get_or_create(
actor=actor,
playlist__isnull=True,
privacy_level="everyone",
name="everyone",
)
return lib
>>>>>>> afc77ed5e (feat(back):trigger public library follow on user follow)

Wyświetl plik

@ -14,6 +14,7 @@ from funkwhale_api.favorites import models as favorites_models
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.history import models as history_models
from funkwhale_api.moderation import models as moderation_models
from funkwhale_api.music import filters as music_filters
from funkwhale_api.music import models as music_models
from funkwhale_api.music import utils as music_utils
from funkwhale_api.playlists import models as playlists_models
@ -380,7 +381,10 @@ def has_playlist_access(request, playlist):
class MusicLibraryViewSet(
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
FederationMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet,
mixins.ListModelMixin,
):
authentication_classes = [authentication.SignatureAuthentication]
renderer_classes = renderers.get_ap_renderers()
@ -391,6 +395,8 @@ class MusicLibraryViewSet(
.select_related("actor")
.filter(channel=None)
)
filterset_class = music_filters.LibraryFilter
lookup_field = "uuid"
def retrieve(self, request, *args, **kwargs):

Wyświetl plik

@ -387,7 +387,18 @@ class LibraryFilter(filters.FilterSet):
distinct=True,
library_field="pk",
)
actor = filters.CharFilter(method="filter_actor")
class Meta:
model = models.Library
fields = ["privacy_level"]
def filter_actor(self, queryset, name, value):
# supports username or username@domain
if "@" in value:
username, domain = value.split("@", 1)
return queryset.filter(
actor__preferred_username=username,
actor__domain_id=domain,
)
return queryset.filter(actor__preferred_username=value)

Wyświetl plik

@ -1,9 +1,17 @@
import datetime
from unittest.mock import Mock
import pytest
from django.urls import reverse
from funkwhale_api.federation import api_serializers, serializers, tasks, views
from funkwhale_api.federation import (
actors,
api_serializers,
models,
serializers,
tasks,
views,
)
def test_user_can_list_their_library_follows(factories, logged_in_api_client):
@ -457,3 +465,33 @@ def test_user_can_accept_or_reject_own_received_follows(
mocked_dispatch.assert_called_once_with(
{"type": action.title()}, context={"follow": follow}
)
def test_following_using_trigger_service_actor_lib_follow(
factories, logged_in_api_client, mocker
):
target_actor = factories["federation.Actor"]()
lib = factories["music.Library"](actor=target_actor, privacy_level="everyone")
mocked_dispatch = mocker.patch(
"funkwhale_api.federation.activity.OutboxRouter.dispatch"
)
mock_session = Mock()
mock_response = Mock()
mock_response.raise_for_status.return_value = None
mock_response.json.side_effect = [
{"results": [serializers.LibrarySerializer(lib).data]}
]
mock_session.get.return_value = mock_response
mocker.patch(
"funkwhale_api.federation.utils.session.get_session",
return_value=mock_session,
)
lib.delete()
url = reverse("api:v1:federation:user-follows-list")
logged_in_api_client.user.create_actor()
logged_in_api_client.post(url, {"target": target_actor.fid})
service_follow = models.LibraryFollow.objects.get(actor=actors.get_service_actor())
mocked_dispatch.assert_called_with(
{"type": "Follow"}, context={"follow": service_follow}
)

Wyświetl plik

@ -204,6 +204,22 @@ sequenceDiagram
When a **requesting user** unfollows a **target user**, the UI must update to visually indicate that the action has succeeded. All activities relating to the **target user** must be visually hidden.
### Get user upload to remote pod
When you follow a user you expect to have access to its public and followers content.
#### Public content (#2422)
- When create a `UserFollow` we also send a `LibraryFollow` for the public user Library, using the service actor of the local pod. This will allow the remote pod to get access to the content.
- When deleting a `UserFollow` we keep the service actor follow, in case other users in the pod use the remote metadata.
#### Followers content (#2536)
As a user I want to share uploads with all my followers.
- Create a new built-in library with `followers` privacy_level
- Add the `followers` privacy_level in `ManageUploads` vue component
## Availability
- [x] App frontend