kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
feat(back):trigger public library follow on user follow
rodzic
b41c7fbaf1
commit
4db52025b9
|
@ -9,6 +9,7 @@ from rest_framework.exceptions import NotFound as RestNotFound
|
||||||
from funkwhale_api.common import preferences
|
from funkwhale_api.common import preferences
|
||||||
from funkwhale_api.common import utils as common_utils
|
from funkwhale_api.common import utils as common_utils
|
||||||
from funkwhale_api.common.permissions import ConditionalAuthentication
|
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 models as music_models
|
||||||
from funkwhale_api.music import serializers as music_serializers
|
from funkwhale_api.music import serializers as music_serializers
|
||||||
from funkwhale_api.music import views as music_views
|
from funkwhale_api.music import views as music_views
|
||||||
|
@ -359,6 +360,15 @@ class UserFollowViewSet(
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
follow = serializer.save(actor=self.request.user.actor)
|
follow = serializer.save(actor=self.request.user.actor)
|
||||||
routes.outbox.dispatch({"type": "Follow"}, context={"follow": follow})
|
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
|
@transaction.atomic
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
|
|
|
@ -290,7 +290,52 @@ def can_manage(obj_owner, actor):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
def update_actor_privacy(actor, privacy_level):
|
def update_actor_privacy(actor, privacy_level):
|
||||||
actor.track_favorites.update(privacy_level=privacy_level)
|
actor.track_favorites.update(privacy_level=privacy_level)
|
||||||
actor.listenings.update(privacy_level=privacy_level)
|
actor.listenings.update(privacy_level=privacy_level)
|
||||||
# to do : trigger federation privacy_level downgrade #2336
|
# 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)
|
||||||
|
|
|
@ -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.federation import utils as federation_utils
|
||||||
from funkwhale_api.history import models as history_models
|
from funkwhale_api.history import models as history_models
|
||||||
from funkwhale_api.moderation import models as moderation_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 models as music_models
|
||||||
from funkwhale_api.music import utils as music_utils
|
from funkwhale_api.music import utils as music_utils
|
||||||
from funkwhale_api.playlists import models as playlists_models
|
from funkwhale_api.playlists import models as playlists_models
|
||||||
|
@ -380,7 +381,10 @@ def has_playlist_access(request, playlist):
|
||||||
|
|
||||||
|
|
||||||
class MusicLibraryViewSet(
|
class MusicLibraryViewSet(
|
||||||
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
FederationMixin,
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
viewsets.GenericViewSet,
|
||||||
|
mixins.ListModelMixin,
|
||||||
):
|
):
|
||||||
authentication_classes = [authentication.SignatureAuthentication]
|
authentication_classes = [authentication.SignatureAuthentication]
|
||||||
renderer_classes = renderers.get_ap_renderers()
|
renderer_classes = renderers.get_ap_renderers()
|
||||||
|
@ -391,6 +395,8 @@ class MusicLibraryViewSet(
|
||||||
.select_related("actor")
|
.select_related("actor")
|
||||||
.filter(channel=None)
|
.filter(channel=None)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
filterset_class = music_filters.LibraryFilter
|
||||||
lookup_field = "uuid"
|
lookup_field = "uuid"
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
|
|
|
@ -387,7 +387,18 @@ class LibraryFilter(filters.FilterSet):
|
||||||
distinct=True,
|
distinct=True,
|
||||||
library_field="pk",
|
library_field="pk",
|
||||||
)
|
)
|
||||||
|
actor = filters.CharFilter(method="filter_actor")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Library
|
model = models.Library
|
||||||
fields = ["privacy_level"]
|
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)
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.urls import reverse
|
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):
|
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(
|
mocked_dispatch.assert_called_once_with(
|
||||||
{"type": action.title()}, context={"follow": follow}
|
{"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}
|
||||||
|
)
|
||||||
|
|
|
@ -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.
|
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
|
## Availability
|
||||||
|
|
||||||
- [x] App frontend
|
- [x] App frontend
|
||||||
|
|
Ładowanie…
Reference in New Issue