diff --git a/api/config/urls/api_v2.py b/api/config/urls/api_v2.py index f513180d3..a1d0befc8 100644 --- a/api/config/urls/api_v2.py +++ b/api/config/urls/api_v2.py @@ -15,11 +15,6 @@ v2_patterns += [ r"^radios/", include(("funkwhale_api.radios.urls_v2", "radios"), namespace="radios"), ), - # to do : to delete - # re_path( - # r"^users/", - # include(("funkwhale_api.users.api_urls_v2", "users"), namespace="users"), - # ), ] urlpatterns = [re_path("", include((v2_patterns, "v2"), namespace="v2"))] diff --git a/api/funkwhale_api/activity/utils.py b/api/funkwhale_api/activity/utils.py index a7329507a..2088595f7 100644 --- a/api/funkwhale_api/activity/utils.py +++ b/api/funkwhale_api/activity/utils.py @@ -37,12 +37,8 @@ def combined_recent(limit, **kwargs): return records -# to do : this should look into actor privacy_level and not iuser privacy level if we want to handle federated -# privacy_level acces mmanagement def get_activity(user, limit=20): - query = fields.privacy_level_query( - user, "actor__user__privacy_level", "actor__user" - ) + query = fields.privacy_level_query(user, "actor__privacy_level", "actor__user") querysets = [ Listening.objects.filter(query).select_related( "track", "actor", "track__artist", "track__album__artist" diff --git a/api/funkwhale_api/common/fields.py b/api/funkwhale_api/common/fields.py index 6217f66b0..3907f76cd 100644 --- a/api/funkwhale_api/common/fields.py +++ b/api/funkwhale_api/common/fields.py @@ -24,8 +24,20 @@ def privacy_level_query(user, lookup_field="privacy_level", user_field="user"): if user.is_anonymous: return models.Q(**{lookup_field: "everyone"}) - return models.Q(**{f"{lookup_field}__in": ["instance", "everyone"]}) | models.Q( - **{lookup_field: "me", user_field: user} + actors_follows = user.actor.user_follows.filter(approved=True).values_list( + "target", flat=True + ) + + followers_query = models.Q( + **{ + f"{lookup_field}": "followers", + f"{user_field}__actor__pk__in": actors_follows, + } + ) + return ( + models.Q(**{f"{lookup_field}__in": ["instance", "everyone"]}) + | models.Q(**{lookup_field: "me", user_field: user}) + | followers_query ) diff --git a/api/funkwhale_api/common/permissions.py b/api/funkwhale_api/common/permissions.py index 60a3c6bcd..5723fbf7e 100644 --- a/api/funkwhale_api/common/permissions.py +++ b/api/funkwhale_api/common/permissions.py @@ -69,12 +69,12 @@ class PrivacyLevelPermission(BasePermission): # to do : it's a remote actor. We could trigger an update of the remote actor data # to avoid leaking data return True - if obj.user.privacy_level == "everyone": + if obj.user.actor.privacy_level == "everyone": return True # user is anonymous elif not hasattr(request.user, "actor"): return False - elif obj.user.privacy_level == "instance": + elif obj.user.actor.privacy_level == "instance": # user is local if hasattr(request.user, "actor"): return True @@ -83,11 +83,14 @@ class PrivacyLevelPermission(BasePermission): else: return False - elif obj.user.privacy_level == "me" and obj.user.actor == request.user.actor: + elif ( + obj.user.actor.privacy_level == "me" + and obj.user.actor == request.user.actor + ): return True elif ( - obj.user.privacy_level == "followers" + obj.user.actor.privacy_level == "followers" and request.user.actor in obj.user.actor.get_followers() ): return True diff --git a/api/funkwhale_api/contrib/listenbrainz/funkwhale_ready.py b/api/funkwhale_api/contrib/listenbrainz/funkwhale_ready.py index 175c87904..f8291c9fe 100644 --- a/api/funkwhale_api/contrib/listenbrainz/funkwhale_ready.py +++ b/api/funkwhale_api/contrib/listenbrainz/funkwhale_ready.py @@ -104,7 +104,7 @@ def sync_listenings_from_listenbrainz(user, conf): logger.info("Getting listenings from ListenBrainz") try: last_ts = ( - history_models.Listening.objects.filter(user=user) + history_models.Listening.objects.filter(actor=user.actor) .filter(source="Listenbrainz") .latest("creation_date") .values_list("creation_date", flat=True) @@ -124,7 +124,7 @@ def sync_favorites_from_listenbrainz(user, conf): return try: last_ts = ( - favorites_models.TrackFavorite.objects.filter(user=user) + favorites_models.TrackFavorite.objects.filter(actor=user.actor) .filter(source="Listenbrainz") .latest("creation_date") .creation_date.timestamp() diff --git a/api/funkwhale_api/contrib/listenbrainz/tasks.py b/api/funkwhale_api/contrib/listenbrainz/tasks.py index 85420c281..a37d60272 100644 --- a/api/funkwhale_api/contrib/listenbrainz/tasks.py +++ b/api/funkwhale_api/contrib/listenbrainz/tasks.py @@ -106,7 +106,7 @@ def add_lb_listenings_to_db(listens, user): listen.listened_at, timezone.utc ), track=track, - user=user, + actor=user.actor, source="Listenbrainz", ) fw_listens.append(fw_listen) @@ -147,7 +147,7 @@ def add_lb_feedback_to_db(feedbacks, user): if feedback["score"] == 1: favorites_models.TrackFavorite.objects.get_or_create( - user=user, + actor=user.actor, creation_date=datetime.datetime.fromtimestamp( feedback["created"], timezone.utc ), @@ -157,7 +157,7 @@ def add_lb_feedback_to_db(feedbacks, user): elif feedback["score"] == 0: try: favorites_models.TrackFavorite.objects.get( - user=user, track=track + actor=user.actor, track=track ).delete() except favorites_models.TrackFavorite.DoesNotExist: continue diff --git a/api/funkwhale_api/favorites/activities.py b/api/funkwhale_api/favorites/activities.py index cb387692e..f4833a32b 100644 --- a/api/funkwhale_api/favorites/activities.py +++ b/api/funkwhale_api/favorites/activities.py @@ -8,7 +8,7 @@ record.registry.register_serializer(serializers.TrackFavoriteActivitySerializer) @record.registry.register_consumer("favorites.TrackFavorite") def broadcast_track_favorite_to_instance_activity(data, obj): - if obj.actor.user.privacy_level not in ["instance", "everyone"]: + if obj.actor.privacy_level not in ["instance", "everyone"]: return channels.group_send( diff --git a/api/funkwhale_api/favorites/migrations/0002_trackfavorite_actor_trackfavorite_fid_and_more.py b/api/funkwhale_api/favorites/migrations/0003_trackfavorite_actor_trackfavorite_fid_and_more.py similarity index 97% rename from api/funkwhale_api/favorites/migrations/0002_trackfavorite_actor_trackfavorite_fid_and_more.py rename to api/funkwhale_api/favorites/migrations/0003_trackfavorite_actor_trackfavorite_fid_and_more.py index f9cac3342..0dd307b7e 100644 --- a/api/funkwhale_api/favorites/migrations/0002_trackfavorite_actor_trackfavorite_fid_and_more.py +++ b/api/funkwhale_api/favorites/migrations/0003_trackfavorite_actor_trackfavorite_fid_and_more.py @@ -19,7 +19,7 @@ def gen_uuid(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ ("federation", "0029_userfollow"), - ("favorites", "0001_initial"), + ("favorites", "0002_trackfavorite_source"), ] operations = [ diff --git a/api/funkwhale_api/favorites/migrations/0004_alter_trackfavorite_unique_together_and_more.py b/api/funkwhale_api/favorites/migrations/0004_alter_trackfavorite_unique_together_and_more.py index bc602b5b9..d6bb8e5ff 100644 --- a/api/funkwhale_api/favorites/migrations/0004_alter_trackfavorite_unique_together_and_more.py +++ b/api/funkwhale_api/favorites/migrations/0004_alter_trackfavorite_unique_together_and_more.py @@ -15,7 +15,7 @@ class Migration(migrations.Migration): dependencies = [ ("federation", "0029_userfollow"), ("music", "0057_auto_20221118_2108"), - ("favorites", "0002_trackfavorite_actor_trackfavorite_fid_and_more"), + ("favorites", "0003_trackfavorite_actor_trackfavorite_fid_and_more"), ] operations = [ diff --git a/api/funkwhale_api/favorites/models.py b/api/funkwhale_api/favorites/models.py index 25dd36716..b4422cfd7 100644 --- a/api/funkwhale_api/favorites/models.py +++ b/api/funkwhale_api/favorites/models.py @@ -18,24 +18,24 @@ FAVORITE_PRIVACY_LEVEL_CHOICES = [ class TrackFavoriteQuerySet(models.QuerySet, common_models.LocalFromFidQuerySet): def viewable_by(self, actor): if actor is None: - return self.filter(actor__user__privacy_level="everyone") + return self.filter(actor__privacy_level="everyone") if hasattr(actor, "user"): - me_query = models.Q(actor__user__privacy_level="me", actor=actor) - me_query = models.Q(actor__user__privacy_level="me", actor=actor) + me_query = models.Q(actor__privacy_level="me", actor=actor) + me_query = models.Q(actor__privacy_level="me", actor=actor) instance_query = models.Q( - actor__user__privacy_level="instance", actor__domain=actor.domain + actor__privacy_level="instance", actor__domain=actor.domain ) instance_actor_query = models.Q( - actor__user__privacy_level="instance", actor__domain=actor.domain + actor__privacy_level="instance", actor__domain=actor.domain ) return self.filter( me_query | instance_query | instance_actor_query - | models.Q(actor__user__privacy_level="everyone") + | models.Q(actor__privacy_level="everyone") ) diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py index fbd117f98..dcb86c986 100644 --- a/api/funkwhale_api/favorites/views.py +++ b/api/funkwhale_api/favorites/views.py @@ -54,7 +54,7 @@ class TrackFavoriteViewSet( ) record.send(instance) routes.outbox.dispatch( - {"type": "Create", "object": {"type": "Favorite"}}, + {"type": "Like", "object": {"type": "Track"}}, context={"favorite": instance}, ) return Response( @@ -65,7 +65,7 @@ class TrackFavoriteViewSet( queryset = super().get_queryset() queryset = queryset.filter( fields.privacy_level_query( - self.request.user, "actor__user__privacy_level", "actor__user" + self.request.user, "actor__privacy_level", "actor__user" ) ) tracks = Track.objects.with_playable_uploads( @@ -90,7 +90,7 @@ class TrackFavoriteViewSet( except (AttributeError, ValueError, models.TrackFavorite.DoesNotExist): return Response({}, status=400) routes.outbox.dispatch( - {"type": "Delete", "object": {"type": "Favorite"}}, + {"type": "Delete", "object": {"type": "Like"}}, context={"favorite": favorite}, ) favorite.delete() diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index c8196f431..dbcf8d23a 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -55,7 +55,6 @@ FUNKWHALE_OBJECT_TYPES = [ ("Album", "Album"), ("Track", "Track"), ("Library", "Library"), - ("Favorite", "Favorite"), ] OBJECT_TYPES = ( [ diff --git a/api/funkwhale_api/federation/contexts.py b/api/funkwhale_api/federation/contexts.py index 9c657ecaf..351b4f7e2 100644 --- a/api/funkwhale_api/federation/contexts.py +++ b/api/funkwhale_api/federation/contexts.py @@ -294,8 +294,6 @@ CONTEXTS = [ "Track": "fw:Track", "Artist": "fw:Artist", "Library": "fw:Library", - # might be possible to do "Favorite": "as:Like" ? - "Favorite": "fw:Favorite", "bitrate": {"@id": "fw:bitrate", "@type": "xsd:nonNegativeInteger"}, "size": {"@id": "fw:size", "@type": "xsd:nonNegativeInteger"}, "position": {"@id": "fw:position", "@type": "xsd:nonNegativeInteger"}, diff --git a/api/funkwhale_api/federation/migrations/0030_actor_privacy_level.py b/api/funkwhale_api/federation/migrations/0030_actor_privacy_level.py new file mode 100644 index 000000000..a9f16ac67 --- /dev/null +++ b/api/funkwhale_api/federation/migrations/0030_actor_privacy_level.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.9 on 2024-04-17 19:15 + +from django.db import migrations, models + + +def gen_privacy_level(apps, schema_editor): + user_model = apps.get_model("users", "User") + for user in user_model.objects.all(): + user.actor.privacy_level = user.actor.privacy_level + user.actor.save(update_fields=["privacy_level"]) + + +class Migration(migrations.Migration): + dependencies = [ + ("federation", "0029_userfollow"), + ] + + operations = [ + migrations.AddField( + model_name="actor", + name="privacy_level", + field=models.CharField( + choices=[ + ("me", "Only me"), + ("followers", "Me and my followers"), + ("instance", "Everyone on my instance, and my followers"), + ("everyone", "Everyone, including people on other instances"), + ], + default="instance", + max_length=30, + ), + ), + migrations.RunPython(gen_privacy_level, reverse_code=migrations.RunPython.noop), + ] diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index a014f0146..810e5a394 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -14,7 +14,7 @@ from django.dispatch import receiver from django.urls import reverse from django.utils import timezone -from funkwhale_api.common import session +from funkwhale_api.common import session, fields from funkwhale_api.common import utils as common_utils from funkwhale_api.common import validators as common_validators from funkwhale_api.music import utils as music_utils @@ -218,6 +218,7 @@ class Actor(models.Model): on_delete=models.SET_NULL, related_name="iconed_actor", ) + privacy_level = fields.get_privacy_field() objects = ActorQuerySet.as_manager() @@ -254,7 +255,7 @@ class Actor(models.Model): def should_autoapprove_follow(self, actor): if self.get_channel(): return True - if self.user.privacy_level == "public": + if self.user.actor.privacy_level == "public": return True return False @@ -402,8 +403,6 @@ class Fetch(models.Model): serializers.ChannelUploadSerializer, ], contexts.FW.Library: [serializers.LibrarySerializer], - # to do : don't need to fetch a favorite since we can fetch the track and actor already ? - # contexts.FW.Favorite: [serializers.TrackFavoriteSerializer], contexts.AS.Group: [serializers.ActorSerializer], contexts.AS.Person: [serializers.ActorSerializer], contexts.AS.Organization: [serializers.ActorSerializer], diff --git a/api/funkwhale_api/federation/routes.py b/api/funkwhale_api/federation/routes.py index 989ff9497..4518eec20 100644 --- a/api/funkwhale_api/federation/routes.py +++ b/api/funkwhale_api/federation/routes.py @@ -612,46 +612,31 @@ def outbox_delete_album(context): } -@outbox.register({"type": "Create", "object.type": "Favorite"}) +@outbox.register({"type": "Like", "object.type": "Track"}) def outbox_create_favorite(context): - from funkwhale_api.favorites import serializers as favorites_serializers - favorite = context["favorite"] actor = favorite.actor serializer = serializers.ActivitySerializer( - { - "type": "Create", - "object": serializers.TrackFavoriteSerializer(favorite).data, - "actor": actor.fid, - } + {"type": "Like", "object": {"type": "Like", "id": favorite.fid}} ) - yield { - "type": "Create", + "type": "Like", "actor": actor, "payload": with_recipients( serializer.data, to=[{"type": "followers", "target": actor}], ), - "object": favorite, - "target": actor, } -@outbox.register({"type": "Delete", "object.type": "Favorite"}) +@outbox.register({"type": "Delete", "object.type": "Like"}) def outbox_delete_favorite(context): favorite = context["favorite"] actor = favorite.actor - serializer = serializers.ActivitySerializer( - { - "type": "Delete", - "object": serializers.TrackFavoriteSerializer(favorite).data, - "actor": actor.fid, - } + {"type": "Delete", "object": {"type": "Like", "id": favorite.fid}} ) - yield { "type": "Delete", "actor": actor, @@ -659,24 +644,18 @@ def outbox_delete_favorite(context): serializer.data, to=[{"type": "followers", "target": actor}], ), - "object": favorite, - "target": actor, } -@inbox.register({"type": "Create", "object.type": "Favorite"}) +@inbox.register({"type": "Like", "object.type": "Track"}) def inbox_create_favorite(payload, context): - from funkwhale_api.favorites import serializers as favorites_serializers - - actor = context["actor"] - favorite = payload["object"] - serializer = serializers.TrackFavoriteSerializer(data=favorite) + serializer = serializers.TrackFavoriteSerializer(data=payload) serializer.is_valid(raise_exception=True) instance = serializer.save() return {"object": instance} -@inbox.register({"type": "Delete", "object.type": "Favorite"}) +@inbox.register({"type": "Delete", "object.type": "Like"}) def inbox_delete_favorite(payload, context): actor = context["actor"] favorite_id = payload["object"].get("id") diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index 86744133e..7f3d49793 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -2094,24 +2094,21 @@ class IndexSerializer(jsonld.JsonLdSerializer): class TrackFavoriteSerializer(jsonld.JsonLdSerializer): type = serializers.ChoiceField(choices=[contexts.AS.Like]) id = serializers.URLField(max_length=500) - # to do : should thi be target like followserializer ? - track = TrackSerializer(required=True) + object = serializers.URLField(max_length=500) actor = serializers.URLField(max_length=500) class Meta: jsonld_mapping = { - "track": jsonld.first_obj(contexts.FW.track), + "object": jsonld.first_id(contexts.AS.object), "actor": jsonld.first_id(contexts.AS.actor), } def to_representation(self, favorite): payload = { - "type": "Favorite", + "type": "Like", "id": favorite.fid, "actor": favorite.actor.fid, - "track": TrackSerializer( - favorite.track, context={"include_ap_context": False} - ).data, + "object": favorite.track.fid, } if self.context.get("include_ap_context", True): payload["@context"] = jsonld.get_default_context() @@ -2119,9 +2116,8 @@ class TrackFavoriteSerializer(jsonld.JsonLdSerializer): def create(self, validated_data): actor = actors.get_actor(validated_data["actor"]) - track = utils.retrieve_ap_object( - validated_data["track"]["id"], + validated_data["object"], actor=actors.get_service_actor(), serializer_class=TrackSerializer, ) @@ -2131,5 +2127,4 @@ class TrackFavoriteSerializer(jsonld.JsonLdSerializer): uuid=uuid.uuid4(), actor=actor, track=track, - user=None, ) diff --git a/api/funkwhale_api/history/activities.py b/api/funkwhale_api/history/activities.py index 614c930e6..8407f2f85 100644 --- a/api/funkwhale_api/history/activities.py +++ b/api/funkwhale_api/history/activities.py @@ -8,7 +8,7 @@ record.registry.register_serializer(serializers.ListeningActivitySerializer) @record.registry.register_consumer("history.Listening") def broadcast_listening_to_instance_activity(data, obj): - if obj.actor.user.privacy_level not in ["instance", "everyone"]: + if obj.actor.privacy_level not in ["instance", "everyone"]: return channels.group_send( diff --git a/api/funkwhale_api/history/migrations/0003_listening_actor_listening_fid_listening_url.py b/api/funkwhale_api/history/migrations/0004_listening_actor_listening_fid_listening_url.py similarity index 97% rename from api/funkwhale_api/history/migrations/0003_listening_actor_listening_fid_listening_url.py rename to api/funkwhale_api/history/migrations/0004_listening_actor_listening_fid_listening_url.py index 567769665..6fc0caf42 100644 --- a/api/funkwhale_api/history/migrations/0003_listening_actor_listening_fid_listening_url.py +++ b/api/funkwhale_api/history/migrations/0004_listening_actor_listening_fid_listening_url.py @@ -15,7 +15,7 @@ def gen_uuid(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ ("federation", "0029_userfollow"), - ("history", "0002_auto_20180325_1433"), + ("history", "0003_listening_source"), ] operations = [ diff --git a/api/funkwhale_api/history/migrations/0004_remove_listening_user.py b/api/funkwhale_api/history/migrations/0005_remove_listening_user.py similarity index 90% rename from api/funkwhale_api/history/migrations/0004_remove_listening_user.py rename to api/funkwhale_api/history/migrations/0005_remove_listening_user.py index a8eb46ffd..db5c3c796 100644 --- a/api/funkwhale_api/history/migrations/0004_remove_listening_user.py +++ b/api/funkwhale_api/history/migrations/0005_remove_listening_user.py @@ -13,7 +13,7 @@ def get_user_actor(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ("history", "0003_listening_actor_listening_fid_listening_url"), + ("history", "0004_listening_actor_listening_fid_listening_url"), ] operations = [ diff --git a/api/funkwhale_api/history/models.py b/api/funkwhale_api/history/models.py index 18b2e1da0..e01bb48e2 100644 --- a/api/funkwhale_api/history/models.py +++ b/api/funkwhale_api/history/models.py @@ -10,7 +10,7 @@ from funkwhale_api.federation import utils as federation_utils from funkwhale_api.music.models import Track -class TrackFavoriteQuerySet(models.QuerySet, common_models.LocalFromFidQuerySet): +class ListeningQuerySet(models.QuerySet, common_models.LocalFromFidQuerySet): pass @@ -33,7 +33,7 @@ class Listening(federation_models.FederationMixin): session_key = models.CharField(max_length=100, null=True, blank=True) source = models.CharField(max_length=100, null=True, blank=True) federation_namespace = "listenings" - objects = TrackFavoriteQuerySet.as_manager() + objects = ListeningQuerySet.as_manager() class Meta: ordering = ("-creation_date",) diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py index fc7f92da5..962885aab 100644 --- a/api/funkwhale_api/history/views.py +++ b/api/funkwhale_api/history/views.py @@ -48,7 +48,9 @@ class ListeningViewSet( def get_queryset(self): queryset = super().get_queryset() queryset = queryset.filter( - fields.privacy_level_query(self.request.user, "actor__user__privacy_level") + fields.privacy_level_query( + self.request.user, "actor__privacy_level", "actor__user" + ) ) tracks = Track.objects.with_playable_uploads( music_utils.get_actor_from_request(self.request) diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py index fb283de99..171ff93f3 100644 --- a/api/funkwhale_api/manage/filters.py +++ b/api/funkwhale_api/manage/filters.py @@ -295,7 +295,7 @@ class ManageActorFilterSet(filters.FilterSet): class Meta: model = federation_models.Actor - fields = ["domain", "type", "manually_approves_followers"] + fields = ["domain", "type", "manually_approves_followers", "privacy_level"] def filter_local(self, queryset, name, value): return queryset.local(value) @@ -316,7 +316,6 @@ class ManageUserFilterSet(filters.FilterSet): model = users_models.User fields = [ "is_active", - "privacy_level", "is_staff", "is_superuser", "permission_library", diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index e097fc88f..9c3427f5e 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -43,7 +43,6 @@ class ManageUserSimpleSerializer(serializers.ModelSerializer): "is_superuser", "date_joined", "last_activity", - "privacy_level", "upload_quota", ) @@ -67,7 +66,6 @@ class ManageUserSerializer(serializers.ModelSerializer): "date_joined", "last_activity", "permissions", - "privacy_level", "upload_quota", "full_username", ) @@ -224,6 +222,7 @@ class ManageBaseActorSerializer(serializers.ModelSerializer): "shared_inbox_url", "manually_approves_followers", "is_local", + "privacy_level", ] read_only_fields = ["creation_date", "instance_policy"] diff --git a/api/funkwhale_api/playlists/views.py b/api/funkwhale_api/playlists/views.py index 0b57a6ef9..d32dbc2a4 100644 --- a/api/funkwhale_api/playlists/views.py +++ b/api/funkwhale_api/playlists/views.py @@ -94,7 +94,7 @@ class PlaylistViewSet( return serializer.save( user=self.request.user, privacy_level=serializer.validated_data.get( - "privacy_level", self.request.user.privacy_level + "privacy_level", self.request.user.actor.privacy_level ), ) diff --git a/api/funkwhale_api/radios/radios.py b/api/funkwhale_api/radios/radios.py index 09b501b88..f1e02d604 100644 --- a/api/funkwhale_api/radios/radios.py +++ b/api/funkwhale_api/radios/radios.py @@ -292,17 +292,17 @@ class SimilarRadio(RelatedObjectRadio): FROM ( SELECT track_id, - creation_date, + h.creation_date, LEAD(track_id) OVER ( - PARTITION by user_id order by creation_date asc + PARTITION by actor_id ORDER BY h.creation_date ASC ) AS next - FROM history_listening - INNER JOIN users_user ON (users_user.id = user_id) - WHERE users_user.privacy_level = 'instance' OR users_user.privacy_level = 'everyone' OR user_id = %s - ORDER BY creation_date ASC - ) t WHERE track_id = %s AND next != %s GROUP BY next ORDER BY c DESC; + FROM history_listening h + INNER JOIN federation_actor fa ON (fa.id = h.actor_id) + WHERE fa.privacy_level = 'instance' OR fa.privacy_level = 'everyone' OR h.actor_id = %s + ORDER BY h.creation_date ASC + ) t WHERE track_id = %s AND next IS NOT NULL AND next != %s GROUP BY next ORDER BY c DESC; """ - cursor.execute(query, [self.session.user_id, seed, seed]) + cursor.execute(query, [self.session.user.actor, seed, seed]) next_candidates = list(cursor.fetchall()) if not next_candidates: diff --git a/api/funkwhale_api/radios/radios_v2.py b/api/funkwhale_api/radios/radios_v2.py index 31a184ab4..2219e1b7d 100644 --- a/api/funkwhale_api/radios/radios_v2.py +++ b/api/funkwhale_api/radios/radios_v2.py @@ -327,22 +327,21 @@ class SimilarRadio(RelatedObjectRadio): def find_next_id(self, queryset, seed): with connection.cursor() as cursor: - query = """ - SELECT next, count(next) AS c + query = """SELECT next, count(next) AS c FROM ( SELECT track_id, - creation_date, + h.creation_date, LEAD(track_id) OVER ( - PARTITION by user_id order by creation_date asc + PARTITION by actor_id ORDER BY h.creation_date ASC ) AS next - FROM history_listening - INNER JOIN users_user ON (users_user.id = user_id) - WHERE users_user.privacy_level = 'instance' OR users_user.privacy_level = 'everyone' OR user_id = %s - ORDER BY creation_date ASC - ) t WHERE track_id = %s AND next != %s GROUP BY next ORDER BY c DESC; + FROM history_listening h + INNER JOIN federation_actor fa ON (fa.id = h.actor_id) + WHERE fa.privacy_level = 'instance' OR fa.privacy_level = 'everyone' OR h.actor_id = %s + ORDER BY h.creation_date ASC + ) t WHERE track_id = %s AND next IS NOT NULL AND next != %s GROUP BY next ORDER BY c DESC; """ - cursor.execute(query, [self.session.user_id, seed, seed]) + cursor.execute(query, [self.session.user.actor, seed, seed]) next_candidates = list(cursor.fetchall()) if not next_candidates: diff --git a/api/funkwhale_api/users/admin.py b/api/funkwhale_api/users/admin.py index 26c58f448..6a714fe12 100644 --- a/api/funkwhale_api/users/admin.py +++ b/api/funkwhale_api/users/admin.py @@ -60,14 +60,13 @@ class UserAdmin(AuthUserAdmin): list_filter = [ "is_superuser", "is_staff", - "privacy_level", "permission_settings", "permission_library", "permission_moderation", ] actions = [disable, enable] fieldsets = ( - (None, {"fields": ("username", "password", "privacy_level")}), + (None, {"fields": ("username", "password")}), ( _("Personal info"), {"fields": ("first_name", "last_name", "email", "avatar")}, diff --git a/api/funkwhale_api/users/api_urls_v2.py b/api/funkwhale_api/users/api_urls_v2.py deleted file mode 100644 index 005a53601..000000000 --- a/api/funkwhale_api/users/api_urls_v2.py +++ /dev/null @@ -1,22 +0,0 @@ -# to do : to delete -# from django.urls import re_path, include - -# from funkwhale_api.common import routers - -# from funkwhale_api.federation import api_views as federation_views - -# from . import views_v2 - - -# router = routers.OptionalSlashRouter() -# router.register(r"", views_v2.UserViewSet, "users") - -# urlpatterns = [ -# re_path(r"^login/?$", views_v2.login, name="login"), -# re_path(r"^logout/?$", views_v2.logout, name="logout"), -# re_path( -# r"^(?P[0-9]+)/follow_requests/(?P[0-9]+)/?$", -# views_v2.follow_request_patch, -# name="follow_request_patch", -# ), -# ] + router.urls diff --git a/api/funkwhale_api/users/migrations/0026_remove_user_privacy_level.py b/api/funkwhale_api/users/migrations/0026_remove_user_privacy_level.py new file mode 100644 index 000000000..9e5c9a8a0 --- /dev/null +++ b/api/funkwhale_api/users/migrations/0026_remove_user_privacy_level.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.9 on 2024-04-18 17:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0023_merge_20221125_1902"), + ("federation", "0030_actor_privacy_level"), + ] + + operations = [ + migrations.RemoveField( + model_name="user", + name="privacy_level", + ), + ] diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py index 14efbca19..ac6553794 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -120,7 +120,6 @@ class User(AbstractUser): # updated on logout or password change, to invalidate JWT secret_key = models.UUIDField(default=uuid.uuid4, null=True) - privacy_level = fields.get_privacy_field() # Unfortunately, Subsonic API assumes a MD5/password authentication # scheme, which is weak in terms of security, and not achievable @@ -214,6 +213,10 @@ class User(AbstractUser): u.settings[key] = value u.save(update_fields=["settings"]) self.settings = u.settings + # to do : this is never called + if "privacy_level" in settings: + u.actor.privacy_level = settings["privacy_level"] + u.actor.save() def has_permissions(self, *perms, **kwargs): operator = kwargs.pop("operator", "and") diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py index 3b72c5b51..d0dce04c0 100644 --- a/api/funkwhale_api/users/serializers.py +++ b/api/funkwhale_api/users/serializers.py @@ -157,7 +157,6 @@ class UserWriteSerializer(serializers.ModelSerializer): model = models.User fields = [ "name", - "privacy_level", "avatar", "instance_support_message_display_date", "funkwhale_support_message_display_date", @@ -204,7 +203,6 @@ class UserReadSerializer(serializers.ModelSerializer): "is_superuser", "permissions", "date_joined", - "privacy_level", "avatar", ] diff --git a/api/funkwhale_api/users/views.py b/api/funkwhale_api/users/views.py index 307d84210..4648b1963 100644 --- a/api/funkwhale_api/users/views.py +++ b/api/funkwhale_api/users/views.py @@ -94,7 +94,6 @@ class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): """Return information about the current user or delete it""" new_settings = request.data request.user.set_settings(**new_settings) - # to do : privacy downgrade if "privacy_level" in new_settings: dispatch_privacy_downgrade(new_settings["privacy_level"], request.user) return Response(request.user.settings) @@ -140,9 +139,17 @@ class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): serializer.save(request) return Response(status=204) + # to do : this work but maybe front should send privacy level update on the actor endpoint an not hte user endpoint ? def update(self, request, *args, **kwargs): if not self.request.user.username == kwargs.get("username"): return Response(status=403) + if "privacy_level" in request.data: + user = self.get_object() + request.data._mutable = True + privacy_level = request.data.pop("privacy_level") + request.data._mutable = False + user.actor.privacy_level = privacy_level[0] + user.actor.save() return super().update(request, *args, **kwargs) def partial_update(self, request, *args, **kwargs): @@ -187,7 +194,13 @@ def logout(request): # to do : privacy downgrade def dispatch_privacy_downgrade(privacy_level, user): if privacy_level == "me" or privacy_level == "instance": - routes.outbox.dispatch({"type": "Delete"}, context={"actor": user.actor}) - + # this will automatically delete all related actor acitivities + routes.outbox.dispatch( + {"type": "Delete", "object": {"type": user.actor.type}}, + context={"actor": user.actor}, + ) if privacy_level == "followers": - routes.outbox.dispatch({"type": "Update"}, context={"actor": user.actor}) + routes.outbox.dispatch( + {"type": "Update", "object": {"type": user.actor.type}}, + context={"actor": user.actor}, + ) diff --git a/api/funkwhale_api/users/views_v2.py b/api/funkwhale_api/users/views_v2.py deleted file mode 100644 index 7db196a9e..000000000 --- a/api/funkwhale_api/users/views_v2.py +++ /dev/null @@ -1,270 +0,0 @@ -# to do : to delete import json - -# from allauth.account.adapter import get_adapter -# from allauth.account.utils import send_email_confirmation -# from dj_rest_auth import views as rest_auth_views -# from dj_rest_auth.registration import views as registration_views -# from django import http -# from django.contrib import auth -# from django.middleware import csrf -# from drf_spectacular.utils import extend_schema, extend_schema_view -# from rest_framework import mixins, viewsets, exceptions - -# from rest_framework.decorators import action, api_view - -# from rest_framework.response import Response - -# from funkwhale_api.common import preferences, throttling - -# from . import models, serializers, tasks -# from funkwhale_api.federation import models as federation_models -# from funkwhale_api.federation import api_serializers as api_federation_serializers -# from funkwhale_api.federation import serializers as federation_serializers -# from funkwhale_api.federation import routes - -# from . import models, serializers, tasks - -# from django.shortcuts import get_object_or_404 - - -# class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): -# queryset = models.User.objects.all().select_related("actor__attachment_icon") -# serializer_class = serializers.UserWriteSerializer -# lookup_field = "pk" -# lookup_value_regex = r"[a-zA-Z0-9-_.]+" -# required_scope = "profile" - -# @extend_schema(operation_id="follow_requests") -# @action(methods=["post"], detail=True) -# def follow_requests(self, *args, **kwargs): -# user_id = kwargs["pk"] -# actor = self.request.user.actor - -# serializer = api_federation_serializers.UserFollowSerializer( -# data={"actor": actor, "target": user_id}, -# context={"actor": self.request.user.actor}, -# ) -# serializer.is_valid(raise_exception=True) -# follow = serializer.save(actor=self.request.user.actor) - -# routes.outbox.dispatch({"type": "Follow"}, context={"follow": follow}) - -# return Response(status=204) - -# @extend_schema(operation_id="unfollow") -# @action(methods=["post"], detail=True) -# def unfollow(self, *args, **kwargs): -# follow = get_object_or_404( -# federation_models.UserFollow, -# actor=self.request.user.actor, -# target=self.get_object(), -# ) -# follow.delete() -# routes.outbox.dispatch({"type": "Delete"}, context={"follow": follow}) -# return Response(status=200) - -# @extend_schema(operation_id="followings") -# @action( -# methods=["get"], -# detail=True, -# ) -# def followings(self, *args, **kwargs): -# user = self.get_object() -# if ( -# self.request.user != self.get_object() -# and self.get_object().privacy_level == "private" -# ): -# raise exceptions.PermissionDenied - -# if ( -# self.request.user != self.get_object() -# and self.request.user.actor.is_local is False -# and self.get_object().privacy_level == "pod" -# ): -# raise exceptions.PermissionDenied - -# followings = federation_models.UserFollow.objects.filter( -# actor=user.actor -# ).order_by("creation_date") -# serializer = api_federation_serializers.UserFollowSerializer( -# followings, many=True -# ) -# page = self.paginate_queryset(followings) -# if page is not None: -# serializer = api_federation_serializers.UserFollowSerializer( -# page, many=True, required=False -# ) -# return self.get_paginated_response(serializer.data) - -# serializer = api_federation_serializers.UserFollowSerializer( -# followings, many=True, required=False -# ) -# return Response(serializer.data) - -# @extend_schema(operation_id="followers") -# @action( -# methods=["get"], -# detail=True, -# ) -# def followers(self, *args, **kwargs): -# user = self.get_object() -# if ( -# self.request.user != self.get_object() -# and self.get_object().privacy_level == "private" -# ): -# raise exceptions.PermissionDenied - -# if ( -# self.request.user != self.get_object() -# and self.request.actor.is_local is False -# and self.get_object().privacy_level == "pod" -# ): -# raise exceptions.PermissionDenied - -# followers = federation_models.UserFollow.objects.filter(target=user).order_by( -# "creation_date" -# ) -# serializer = api_federation_serializers.UserFollowSerializer( -# followers, many=True -# ) -# page = self.paginate_queryset(followers) -# if page is not None: -# serializer = api_federation_serializers.UserFollowSerializer( -# page, many=True, required=False -# ) -# return self.get_paginated_response(serializer.data) - -# serializer = api_federation_serializers.UserFollowSerializer( -# followers, many=True, required=False -# ) -# return Response(serializer.data) - -# @extend_schema(operation_id="get_authenticated_user", methods=["get"]) -# @extend_schema(operation_id="delete_authenticated_user", methods=["delete"]) -# @action(methods=["get", "delete"], detail=False) -# def me(self, request, *args, **kwargs): -# """Return information about the current user or delete it""" -# if request.method.lower() == "delete": -# serializer = serializers.UserDeleteSerializer( -# request.user, data=request.data -# ) -# serializer.is_valid(raise_exception=True) -# tasks.delete_account.delay(user_id=request.user.pk) -# # at this point, password is valid, we launch deletion -# return Response(status=204) -# serializer = serializers.MeSerializer(request.user) -# return Response(serializer.data) - -# @extend_schema(operation_id="update_settings") -# @action(methods=["post"], detail=False, url_name="settings", url_path="settings") -# def set_settings(self, request, *args, **kwargs): -# """Return information about the current user or delete it""" -# new_settings = request.data -# request.user.set_settings(**new_settings) -# return Response(request.user.settings) - -# @action( -# methods=["get", "post", "delete"], -# required_scope="security", -# url_path="subsonic-token", -# detail=True, -# ) -# def subsonic_token(self, request, *args, **kwargs): -# if not self.request.user.username == kwargs.get("username"): -# return Response(status=403) -# if not preferences.get("subsonic__enabled"): -# return Response(status=405) -# if request.method.lower() == "get": -# return Response( -# {"subsonic_api_token": self.request.user.subsonic_api_token} -# ) -# if request.method.lower() == "delete": -# self.request.user.subsonic_api_token = None -# self.request.user.save(update_fields=["subsonic_api_token"]) -# return Response(status=204) -# self.request.user.update_subsonic_api_token() -# self.request.user.save(update_fields=["subsonic_api_token"]) -# data = {"subsonic_api_token": self.request.user.subsonic_api_token} -# return Response(data) - -# @extend_schema(operation_id="change_email", responses={200: None, 403: None}) -# @action( -# methods=["post"], -# required_scope="security", -# url_path="change-email", -# detail=False, -# ) -# def change_email(self, request, *args, **kwargs): -# if not self.request.user.is_authenticated: -# return Response(status=403) -# serializer = serializers.UserChangeEmailSerializer( -# request.user, data=request.data, context={"user": request.user} -# ) -# serializer.is_valid(raise_exception=True) -# serializer.save(request) -# return Response(status=204) - -# def update(self, request, *args, **kwargs): -# if not self.request.user.username == kwargs.get("username"): -# return Response(status=403) -# return super().update(request, *args, **kwargs) - -# def partial_update(self, request, *args, **kwargs): -# if not self.request.user.username == kwargs.get("username"): -# return Response(status=403) -# return super().partial_update(request, *args, **kwargs) - - -# @extend_schema(operation_id="login") -# @action(methods=["post"], detail=False) -# def login(request): -# throttling.check_request(request, "login") -# if request.method != "POST": -# return http.HttpResponse(status=405) -# serializer = serializers.LoginSerializer( -# data=request.POST, context={"request": request} -# ) -# if not serializer.is_valid(): -# return http.HttpResponse( -# json.dumps(serializer.errors), status=400, content_type="application/json" -# ) -# serializer.save(request) -# csrf.rotate_token(request) -# token = csrf.get_token(request) -# response = http.HttpResponse(status=200) -# response.set_cookie("csrftoken", token, max_age=None) -# return response - - -# @extend_schema(operation_id="logout") -# @action(methods=["post"], detail=False) -# def logout(request): -# if request.method != "POST": -# return http.HttpResponse(status=405) -# auth.logout(request) -# token = csrf.get_token(request) -# response = http.HttpResponse(status=200) -# response.set_cookie("csrftoken", token, max_age=None) -# return response - - -# @action(methods=["patch"], detail=False) -# @extend_schema(operation_id="follow_request_patch") -# def follow_request_patch(request, user_pk, follow_pk): -# try: -# user = models.User.objects.get(pk=user_pk) -# follow = federation_models.UserFollow.objects.get(pk=follow_pk) -# except (models.User.DoesNotExist, federation_models.UserFollow.DoesNotExist): -# raise http.Http404 -# if request.user != user: -# raise exceptions.PermissionDenied - -# request_body = request.body.decode("utf-8") -# data = json.loads(request_body) -# if not isinstance(data["approved"], bool): -# raise BaseException("Approved typemust be boolean") -# follow.approved = data["approved"] -# follow.save() - -# routes.outbox.dispatch({"type": "Update"}, context={"follow": follow}) -# return http.HttpResponse(status=204) diff --git a/api/tests/activity/test_utils.py b/api/tests/activity/test_utils.py index 6610438b0..2ba187ca3 100644 --- a/api/tests/activity/test_utils.py +++ b/api/tests/activity/test_utils.py @@ -13,11 +13,16 @@ def test_get_activity(factories): def test_get_activity_honors_privacy_level(factories, anonymous_user): - user = factories["users.User"](privacy_level="me") - user2 = factories["users.User"](privacy_level="instance") - factories["history.Listening"](actor=user.actor) + user = factories["users.User"]() + user.create_actor(privacy_level="everyone") + user2 = factories["users.User"]() + user2.create_actor(privacy_level="instance") + + listening1 = factories["history.Listening"](actor=user.actor) favorite1 = factories["favorites.TrackFavorite"](actor=user.actor) + factories["favorites.TrackFavorite"](actor=user2.actor) objects = list(utils.get_activity(anonymous_user)) - assert objects == [favorite1] + assert objects == [favorite1, listening1] + # to do : test others diff --git a/api/tests/activity/test_views.py b/api/tests/activity/test_views.py index abf243ad9..409a13d52 100644 --- a/api/tests/activity/test_views.py +++ b/api/tests/activity/test_views.py @@ -5,7 +5,8 @@ from funkwhale_api.activity import serializers, utils def test_activity_view(factories, api_client, preferences, anonymous_user): preferences["common__api_authentication_required"] = False - user = factories["users.User"](privacy_level="everyone") + user = factories["users.User"]() + user.create_actor(privacy_level="everyone") factories["favorites.TrackFavorite"](actor=user.actor) factories["history.Listening"]() url = reverse("api:v1:activity-list") diff --git a/api/tests/common/test_fields.py b/api/tests/common/test_fields.py index 0fdf74cae..182826839 100644 --- a/api/tests/common/test_fields.py +++ b/api/tests/common/test_fields.py @@ -1,25 +1,91 @@ import pytest from django.contrib.auth.models import AnonymousUser -from django.db.models import Q +from django.db.models import Q, QuerySet from funkwhale_api.common import fields from funkwhale_api.users.factories import UserFactory +from funkwhale_api.history import models +from funkwhale_api.favorites import models as favorite_models +from funkwhale_api.federation import models as federation_models + + +def test_privacy_level_query(factories): + user = factories["users.User"](with_actor=True) + user_query = ( + Q(privacy_level__in=["instance", "everyone"]) + | Q(privacy_level="me", user=user) + | Q( + privacy_level="followers", + user__actor__pk__in=user.actor.user_follows.filter( + approved=True + ).values_list("target", flat=True), + ) + ) -@pytest.mark.parametrize( - "user,expected", - [ - (AnonymousUser(), Q(privacy_level="everyone")), - ( - UserFactory.build(pk=1), - Q(privacy_level__in=["instance", "everyone"]) - | Q(privacy_level="me", user=UserFactory.build(pk=1)), - ), - ], -) -def test_privacy_level_query(user, expected): query = fields.privacy_level_query(user) - assert query == expected + assert str(query) == str(user_query) + + user = AnonymousUser() + user_query = Q(privacy_level="everyone") + query = fields.privacy_level_query(user) + assert str(query) == str(user_query) + + +def test_privacy_level_query_followers(factories): + user = factories["users.User"](with_actor=True) + target = factories["users.User"]() + target.create_actor(privacy_level="followers") + + target.refresh_from_db() + + userfollow = factories["federation.UserFollow"]( + actor=user.actor, target=target.actor, approved=True + ) + listening = factories["history.Listening"](actor=userfollow.target) + favorite = factories["favorites.TrackFavorite"](actor=userfollow.target) + + factories["history.Listening"]() + factories["history.Listening"]() + factories["favorites.TrackFavorite"]() + factories["favorites.TrackFavorite"]() + + queryset = models.Listening.objects.all().filter( + fields.privacy_level_query(user, "actor__privacy_level", "actor__user") + ) + fav_qs = favorite_models.TrackFavorite.objects.all().filter( + fields.privacy_level_query(user, "actor__privacy_level", "actor__user") + ) + + assert listening in queryset + assert favorite in fav_qs + + +def test_privacy_level_query_not_followers(factories): + user = factories["users.User"](with_actor=True) + target = factories["users.User"]() + target.create_actor(privacy_level="followers") + + target.refresh_from_db() + + userfollow = factories["federation.UserFollow"](target=target.actor, approved=True) + listening = factories["history.Listening"](actor=userfollow.target) + favorite = factories["favorites.TrackFavorite"](actor=userfollow.target) + + factories["history.Listening"]() + factories["history.Listening"]() + factories["favorites.TrackFavorite"]() + factories["favorites.TrackFavorite"]() + + queryset = models.Listening.objects.all().filter( + fields.privacy_level_query(user, "actor__privacy_level", "actor__user") + ) + fav_qs = favorite_models.TrackFavorite.objects.all().filter( + fields.privacy_level_query(user, "actor__privacy_level", "actor__user") + ) + + assert listening not in queryset + assert favorite not in fav_qs def test_generic_relation_field(factories): diff --git a/api/tests/common/test_permissions.py b/api/tests/common/test_permissions.py index b378f7ea8..9ea76493c 100644 --- a/api/tests/common/test_permissions.py +++ b/api/tests/common/test_permissions.py @@ -48,7 +48,8 @@ def test_owner_permission_read_only(anonymous_user, nodb_factories, api_request) def test_privacylevel_permission_anonymous( factories, api_request, anonymous_user, privacy_level, expected ): - user = factories["users.User"](with_actor=True, privacy_level=privacy_level) + user = factories["users.User"]() + user.create_actor(privacy_level=privacy_level) view = APIView.as_view() permission = permissions.PrivacyLevelPermission() request = api_request.get("/") @@ -65,7 +66,8 @@ def test_privacylevel_permission_anonymous( def test_privacylevel_permission_instance( factories, api_request, anonymous_user, privacy_level, expected, mocker ): - user = factories["users.User"](with_actor=True, privacy_level=privacy_level) + user = factories["users.User"]() + user.create_actor(privacy_level=privacy_level) request_user = factories["users.User"](with_actor=True) view = APIView.as_view() permission = permissions.PrivacyLevelPermission() @@ -83,7 +85,8 @@ def test_privacylevel_permission_instance( def test_privacylevel_permission_me( factories, api_request, anonymous_user, privacy_level, expected, mocker ): - user = factories["users.User"](with_actor=True, privacy_level=privacy_level) + user = factories["users.User"]() + user.create_actor(privacy_level=privacy_level) view = APIView.as_view() permission = permissions.PrivacyLevelPermission() request = api_request.get("/") diff --git a/api/tests/contrib/listenbrainz/test_listenbrainz.py b/api/tests/contrib/listenbrainz/test_listenbrainz.py index 7cf504707..56c770990 100644 --- a/api/tests/contrib/listenbrainz/test_listenbrainz.py +++ b/api/tests/contrib/listenbrainz/test_listenbrainz.py @@ -13,6 +13,7 @@ from funkwhale_api.history import models as history_models def test_listenbrainz_submit_listen(logged_in_client, mocker, factories): + logged_in_client.user.create_actor() config = plugins.get_plugin_config( name="listenbrainz", description="A plugin that allows you to submit or sync your listens and favorites to ListenBrainz.", @@ -38,7 +39,7 @@ def test_listenbrainz_submit_listen(logged_in_client, mocker, factories): url = reverse("api:v1:history:listenings-list") logged_in_client.post(url, {"track": track.pk}) logged_in_client.get(url) - listening = history_models.Listening.objects.get(user=logged_in_client.user) + listening = history_models.Listening.objects.get(actor=logged_in_client.user.actor) handler.assert_called_once_with(listening=listening, conf=None) @@ -117,14 +118,14 @@ def test_sync_favorites_from_listenbrainz(factories, mocker, caplog): logger = logging.getLogger("plugins") caplog.set_level(logging.INFO) logger.addHandler(caplog.handler) - user = factories["users.User"]() + user = factories["users.User"](with_actor=True) # track lb fav factories["music.Track"](mbid="195565db-65f9-4d0d-b347-5f0c85509528") # random track factories["music.Track"]() # track lb neutral track = factories["music.Track"](mbid="c5af5351-dbbf-4481-b52e-a480b6c57986") - favorite = factories["favorites.TrackFavorite"](track=track, user=user) + favorite = factories["favorites.TrackFavorite"](track=track, actor=user.actor) # last_sync track_last_sync = factories["music.Track"]( mbid="c878ef2f-c08d-4a81-a047-f2a9f978cec7" @@ -189,12 +190,12 @@ def test_sync_favorites_from_listenbrainz_since(factories, mocker, caplog): logger = logging.getLogger("plugins") caplog.set_level(logging.INFO) logger.addHandler(caplog.handler) - user = factories["users.User"]() + user = factories["users.User"](with_actor=True) # track lb fav factories["music.Track"](mbid="195565db-65f9-4d0d-b347-5f0c85509528") # track lb neutral track = factories["music.Track"](mbid="c5af5351-dbbf-4481-b52e-a480b6c57986") - favorite = factories["favorites.TrackFavorite"](track=track, user=user) + favorite = factories["favorites.TrackFavorite"](track=track, actor=user.actor) # track should be not synced factories["music.Track"](mbid="1fd02cf2-7247-4715-8862-c378ec196000") # last_sync @@ -203,7 +204,7 @@ def test_sync_favorites_from_listenbrainz_since(factories, mocker, caplog): ) factories["favorites.TrackFavorite"]( track=track_last_sync, - user=user, + actor=user.actor, source="Listenbrainz", creation_date=datetime.datetime.fromtimestamp(1690775094), ) diff --git a/api/tests/favorites/test_activity.py b/api/tests/favorites/test_activity.py index a2923580c..a65cd0b4c 100644 --- a/api/tests/favorites/test_activity.py +++ b/api/tests/favorites/test_activity.py @@ -54,7 +54,9 @@ def test_broadcast_track_favorite_to_instance_activity(factories, mocker): def test_broadcast_track_favorite_to_instance_activity_private(factories, mocker): p = mocker.patch("funkwhale_api.common.channels.group_send") - user = factories["users.User"](privacy_level="me", with_actor=True) + user = factories["users.User"]() + user.create_actor(privacy_level="me") + favorite = factories["favorites.TrackFavorite"](actor=user.actor) data = serializers.TrackFavoriteActivitySerializer(favorite).data consumer = activities.broadcast_track_favorite_to_instance_activity diff --git a/api/tests/favorites/test_models.py b/api/tests/favorites/test_models.py index c052b701c..7acd576af 100644 --- a/api/tests/favorites/test_models.py +++ b/api/tests/favorites/test_models.py @@ -10,7 +10,8 @@ from funkwhale_api.favorites import models def test_playable_by_local_actor(privacy_level, expected, factories): actor = factories["federation.Actor"](local=True) # default user actor is local - user = factories["users.User"](with_actor=True, privacy_level=privacy_level) + user = factories["users.User"]() + user.create_actor(privacy_level=privacy_level) favorite = factories["favorites.TrackFavorite"](actor=user.actor) queryset = models.TrackFavorite.objects.all().viewable_by(actor) match = favorite in list(queryset) @@ -23,10 +24,9 @@ def test_playable_by_local_actor(privacy_level, expected, factories): def test_not_playable_by_remote_actor(privacy_level, expected, factories): actor = factories["federation.Actor"]() # default user actor is local - user = factories["users.User"]( - with_actor=True, - privacy_level=privacy_level, - ) + user = factories["users.User"]() + user.create_actor(privacy_level=privacy_level) + favorite = factories["favorites.TrackFavorite"](actor=user.actor) queryset = models.TrackFavorite.objects.all().viewable_by(actor) match = favorite in list(queryset) diff --git a/api/tests/favorites/test_views.py b/api/tests/favorites/test_views.py index d6a6c705d..96ad2433e 100644 --- a/api/tests/favorites/test_views.py +++ b/api/tests/favorites/test_views.py @@ -5,7 +5,8 @@ from django.urls import reverse @pytest.mark.parametrize("level", ["instance", "me", "followers"]) def test_privacy_filter(preferences, level, factories, api_client): preferences["common__api_authentication_required"] = False - user = factories["users.User"](with_actor=True, privacy_level=level) + user = factories["users.User"]() + user.create_actor(privacy_level=level) factories["favorites.TrackFavorite"](actor=user.actor) url = reverse("api:v1:favorites:tracks-list") response = api_client.get(url) diff --git a/api/tests/federation/test_migrations.py b/api/tests/federation/test_migrations.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/tests/federation/test_routes.py b/api/tests/federation/test_routes.py index d3b9f3e55..1ff163369 100644 --- a/api/tests/federation/test_routes.py +++ b/api/tests/federation/test_routes.py @@ -40,7 +40,7 @@ from funkwhale_api.favorites import serializers as favorites_serializers ({"type": "Delete", "object": {"type": "Tombstone"}}, routes.inbox_delete), ({"type": "Flag"}, routes.inbox_flag), ( - {"type": "Create", "object": {"type": "Favorite"}}, + {"type": "Like", "object": {"type": "Track"}}, routes.inbox_create_favorite, ), ], @@ -90,7 +90,7 @@ def test_inbox_routes(route, handler): routes.outbox_delete_actor, ), ( - {"type": "Create", "object": {"type": "Favorite"}}, + {"type": "Like", "object": {"type": "Track"}}, routes.outbox_create_favorite, ), ], @@ -144,7 +144,7 @@ def test_inbox_follow_user_autoapprove(factories, mocker): "funkwhale_api.federation.activity.OutboxRouter.dispatch" ) - local_actor = factories["users.User"](privacy_level="public").create_actor() + local_actor = factories["users.User"]().create_actor(privacy_level="public") remote_actor = factories["federation.Actor"]() ii = factories["federation.InboxItem"](actor=local_actor) @@ -1039,43 +1039,29 @@ def test_outbox_flag(factory_name, factory_kwargs, factories, mocker): def test_outbox_create_favorite(factories, mocker): user = factories["users.User"](with_actor=True) favorite = factories["favorites.TrackFavorite"](actor=user.actor) + userfollow = factories["federation.UserFollow"](target=favorite.actor) activity = list(routes.outbox_create_favorite({"favorite": favorite}))[0] serializer = serializers.ActivitySerializer( - { - "type": "Create", - "object": serializers.TrackFavoriteSerializer(favorite).data, - "actor": favorite.actor.fid, - } + {"type": "Like", "object": {"type": "Like", "id": favorite.fid}} ) expected = serializer.data expected["to"] = [{"type": "followers", "target": favorite.actor}] assert dict(activity["payload"]) == dict(expected) assert activity["actor"] == favorite.actor - assert activity["target"] == favorite.actor.received_user_follows.all() - assert activity["object"] == favorite def test_inbox_create_favorite(factories, mocker): actor = factories["federation.Actor"]() favorite = factories["favorites.TrackFavorite"](actor=actor) - follow = factories["federation.UserFollow"](target=actor) - - data = serializers.TrackFavoriteSerializer(favorite).data - serializer = serializers.ActivitySerializer( - { - "type": "Create", - "object": data, - "actor": actor.fid, - } - ) + serializer = serializers.TrackFavoriteSerializer(favorite) init = mocker.spy(serializers.TrackFavoriteSerializer, "__init__") save = mocker.spy(serializers.TrackFavoriteSerializer, "save") - track_data = serializers.TrackSerializer(favorite.track).data mocker.patch.object(utils, "retrieve_ap_object", return_value=favorite.track) favorite.delete() + result = routes.inbox_create_favorite( serializer.data, context={ diff --git a/api/tests/history/test_activity.py b/api/tests/history/test_activity.py index f8b77a064..163ee4e89 100644 --- a/api/tests/history/test_activity.py +++ b/api/tests/history/test_activity.py @@ -54,8 +54,9 @@ def test_broadcast_listening_to_instance_activity(factories, mocker): def test_broadcast_listening_to_instance_activity_private(factories, mocker): p = mocker.patch("funkwhale_api.common.channels.group_send") - user = factories["users.User"](privacy_level="me", with_actor=True) - listening = factories["history.Listening"](actor__user=user) + user = factories["users.User"]() + user.create_actor(privacy_level="me") + listening = factories["history.Listening"](actor=user.actor) data = serializers.ListeningActivitySerializer(listening).data consumer = activities.broadcast_listening_to_instance_activity consumer(data=data, obj=listening) diff --git a/api/tests/history/test_views.py b/api/tests/history/test_views.py index f1f12c425..ba4f2ec21 100644 --- a/api/tests/history/test_views.py +++ b/api/tests/history/test_views.py @@ -5,7 +5,8 @@ from django.urls import reverse @pytest.mark.parametrize("level", ["instance", "me", "followers"]) def test_privacy_filter(preferences, level, factories, api_client): preferences["common__api_authentication_required"] = False - user = factories["users.User"](privacy_level=level) + user = factories["users.User"]() + user.create_actor(privacy_level=level) factories["history.Listening"](actor__user=user) url = reverse("api:v1:history:listenings-list") response = api_client.get(url) diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py index 6ba29897e..e7d0c5f16 100644 --- a/api/tests/manage/test_serializers.py +++ b/api/tests/manage/test_serializers.py @@ -89,6 +89,7 @@ def test_manage_actor_serializer(factories, now, to_api_date): "user": None, "instance_policy": None, "is_local": False, + "privacy_level": "instance", } s = serializers.ManageActorSerializer(actor) diff --git a/api/tests/playlists/test_views.py b/api/tests/playlists/test_views.py index 5b9cf4202..73a56a119 100644 --- a/api/tests/playlists/test_views.py +++ b/api/tests/playlists/test_views.py @@ -5,6 +5,7 @@ from funkwhale_api.playlists import models def test_can_create_playlist_via_api(logged_in_api_client): + logged_in_api_client.user.create_actor() url = reverse("api:v1:playlists-list") data = {"name": "test", "privacy_level": "everyone"} @@ -16,6 +17,7 @@ def test_can_create_playlist_via_api(logged_in_api_client): def test_serializer_includes_tracks_count(factories, logged_in_api_client): + logged_in_api_client.user.create_actor() playlist = factories["playlists.Playlist"]() factories["playlists.PlaylistTrack"](playlist=playlist) @@ -26,6 +28,7 @@ def test_serializer_includes_tracks_count(factories, logged_in_api_client): def test_serializer_includes_tracks_count_986(factories, logged_in_api_client): + logged_in_api_client.user.create_actor() playlist = factories["playlists.Playlist"]() plt = factories["playlists.PlaylistTrack"](playlist=playlist) factories["music.Upload"].create_batch( @@ -38,6 +41,7 @@ def test_serializer_includes_tracks_count_986(factories, logged_in_api_client): def test_serializer_includes_is_playable(factories, logged_in_api_client): + logged_in_api_client.user.create_actor() playlist = factories["playlists.Playlist"]() factories["playlists.PlaylistTrack"](playlist=playlist) @@ -48,16 +52,17 @@ def test_serializer_includes_is_playable(factories, logged_in_api_client): def test_playlist_inherits_user_privacy(logged_in_api_client): + logged_in_api_client.user.create_actor() url = reverse("api:v1:playlists-list") user = logged_in_api_client.user - user.privacy_level = "me" + user.actor.privacy_level = "me" user.save() data = {"name": "test"} logged_in_api_client.post(url, data) playlist = user.playlists.latest("id") - assert playlist.privacy_level == user.privacy_level + assert playlist.privacy_level == user.actor.privacy_level @pytest.mark.parametrize( @@ -73,6 +78,7 @@ def test_url_requires_login(name, method, factories, api_client): def test_only_can_add_track_on_own_playlist_via_api(factories, logged_in_api_client): + logged_in_api_client.user.create_actor() track = factories["music.Track"]() playlist = factories["playlists.Playlist"]() url = reverse("api:v1:playlists-add", kwargs={"pk": playlist.pk}) @@ -84,6 +90,7 @@ def test_only_can_add_track_on_own_playlist_via_api(factories, logged_in_api_cli def test_deleting_plt_updates_indexes(mocker, factories, logged_in_api_client): + logged_in_api_client.user.create_actor() remove = mocker.spy(models.Playlist, "remove") factories["music.Track"]() playlist = factories["playlists.Playlist"](user=logged_in_api_client.user) @@ -115,6 +122,7 @@ def test_playlist_privacy_respected_in_list_anon( @pytest.mark.parametrize("method", ["PUT", "PATCH", "DELETE"]) def test_only_owner_can_edit_playlist(method, factories, logged_in_api_client): + logged_in_api_client.user.create_actor() playlist = factories["playlists.Playlist"]() url = reverse("api:v1:playlists-detail", kwargs={"pk": playlist.pk}) response = getattr(logged_in_api_client, method.lower())(url) @@ -125,6 +133,7 @@ def test_only_owner_can_edit_playlist(method, factories, logged_in_api_client): def test_can_add_multiple_tracks_at_once_via_api( factories, mocker, logged_in_api_client ): + logged_in_api_client.user.create_actor() playlist = factories["playlists.Playlist"](user=logged_in_api_client.user) tracks = factories["music.Track"].create_batch(size=5) track_ids = [t.id for t in tracks] @@ -141,6 +150,7 @@ def test_can_add_multiple_tracks_at_once_via_api( def test_honor_max_playlist_size(factories, mocker, logged_in_api_client, preferences): + logged_in_api_client.user.create_actor() preferences["playlists__max_tracks"] = 3 playlist = factories["playlists.Playlist"](user=logged_in_api_client.user) tracks = factories["music.Track"].create_batch( @@ -155,6 +165,7 @@ def test_honor_max_playlist_size(factories, mocker, logged_in_api_client, prefer def test_can_clear_playlist_from_api(factories, mocker, logged_in_api_client): + logged_in_api_client.user.create_actor() playlist = factories["playlists.Playlist"](user=logged_in_api_client.user) factories["playlists.PlaylistTrack"].create_batch(size=5, playlist=playlist) url = reverse("api:v1:playlists-clear", kwargs={"pk": playlist.pk}) @@ -165,6 +176,7 @@ def test_can_clear_playlist_from_api(factories, mocker, logged_in_api_client): def test_update_playlist_from_api(factories, mocker, logged_in_api_client): + logged_in_api_client.user.create_actor() playlist = factories["playlists.Playlist"](user=logged_in_api_client.user) factories["playlists.PlaylistTrack"].create_batch(size=5, playlist=playlist) url = reverse("api:v1:playlists-detail", kwargs={"pk": playlist.pk}) @@ -176,6 +188,7 @@ def test_update_playlist_from_api(factories, mocker, logged_in_api_client): def test_move_plt_updates_indexes(mocker, factories, logged_in_api_client): + logged_in_api_client.user.create_actor() playlist = factories["playlists.Playlist"](user=logged_in_api_client.user) plt0 = factories["playlists.PlaylistTrack"](index=0, playlist=playlist) plt1 = factories["playlists.PlaylistTrack"](index=1, playlist=playlist) diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py index d62c8b3c4..a456f4043 100644 --- a/api/tests/subsonic/test_views.py +++ b/api/tests/subsonic/test_views.py @@ -633,6 +633,7 @@ def test_search3(f, db, logged_in_api_client, factories): @pytest.mark.parametrize("f", ["json"]) def test_get_playlists(f, db, logged_in_api_client, factories): + logged_in_api_client.user.create_actor() url = reverse("api:subsonic:subsonic-get_playlists") assert url.endswith("getPlaylists") is True playlist1 = factories["playlists.PlaylistTrack"]( @@ -664,6 +665,7 @@ def test_get_playlists(f, db, logged_in_api_client, factories): @pytest.mark.parametrize("f", ["json"]) def test_get_playlist(f, db, logged_in_api_client, factories): + logged_in_api_client.user.create_actor() url = reverse("api:subsonic:subsonic-get_playlist") assert url.endswith("getPlaylist") is True playlist = factories["playlists.PlaylistTrack"]( diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py index 6950cf3ca..a5f50c848 100644 --- a/api/tests/users/test_views.py +++ b/api/tests/users/test_views.py @@ -190,16 +190,16 @@ def test_can_request_password_reset( def test_user_can_patch_his_own_settings(logged_in_api_client): + logged_in_api_client.user.create_actor() user = logged_in_api_client.user payload = {"privacy_level": "me"} url = reverse("api:v1:users:users-detail", kwargs={"username": user.username}) - response = logged_in_api_client.patch(url, payload) assert response.status_code == 200 user.refresh_from_db() - assert user.privacy_level == "me" + assert user.actor.privacy_level == "me" def test_user_can_patch_description(logged_in_api_client): @@ -554,4 +554,4 @@ def test_user_change_email(logged_in_api_client, mocker, mailoutbox): # assert response.status_code == 200 # user.refresh_from_db() -# assert user.privacy_level == "me" +# assert user.actor.privacy_level == "me" diff --git a/api/tests/users/test_views_v2.py b/api/tests/users/test_views_v2.py deleted file mode 100644 index 225eb1069..000000000 --- a/api/tests/users/test_views_v2.py +++ /dev/null @@ -1,170 +0,0 @@ -# to do : to delete -# import pytest -# from django.test import Client -# from django.urls import reverse - -# from funkwhale_api.common import serializers as common_serializers -# from funkwhale_api.common import utils as common_utils -# from funkwhale_api.moderation import tasks as moderation_tasks -# from funkwhale_api.users.models import User - - -# def test_can_follow_user(factories, logged_in_api_client, mocker): -# followed_user = factories["users.User"]() -# actor = factories["federation.Actor"]() -# logged_in_api_client.user.actor = actor -# url = reverse("api:v2:users:users-follow-requests", kwargs={"pk": followed_user.pk}) -# routes = mocker.patch("funkwhale_api.federation.api_views.routes.outbox.dispatch") -# response = logged_in_api_client.post(url) -# assert response.status_code == 204 -# assert routes.call_count == 1 - - -# def test_can_unfollow(factories, logged_in_api_client, mocker): -# logged_in_api_client.user.create_actor() -# followed_user = factories["users.User"](with_actor=True) -# user_follow = factories["federation.UserFollow"]( -# target=followed_user, actor=logged_in_api_client.user.actor -# ) -# url = reverse("api:v2:users:users-unfollow", kwargs={"pk": followed_user.pk}) -# routes = mocker.patch("funkwhale_api.federation.api_views.routes.outbox.dispatch") -# response = logged_in_api_client.post(url) -# assert response.status_code == 200 - - -# # /users/id/follow_requests/ -# # def test_can_patch_follow_user(factories, logged_in_api_client, mocker): -# # logged_in_api_client.user.create_actor() -# # following_user = factories["users.User"](with_actor=True) -# # url = reverse( -# # "api:v2:users:users-follow-requests", -# # kwargs={"pk": logged_in_api_client.user.pk}, -# # ) -# # routes = mocker.patch("funkwhale_api.federation.api_views.routes.outbox.dispatch") -# # data = { -# # "approved": True, -# # "actor": following_user.actor, -# # "target": logged_in_api_client.user.pk, -# # } -# # response = logged_in_api_client.patch(url, data=data) -# # assert response.status_code == 204 -# # assert routes.call_count == 1 - - -# def test_can_patch_follow_user_v2(factories, logged_in_api_client, mocker): -# logged_in_api_client.user.create_actor() -# following_user = factories["users.User"](with_actor=True) -# user_follow = factories["federation.UserFollow"]( -# target=following_user, actor=following_user.actor -# ) -# url = reverse( -# "api:v2:users:follow_request_patch", -# kwargs={ -# "user_pk": logged_in_api_client.user.pk, -# "follow_pk": user_follow.pk, -# }, -# ) -# routes = mocker.patch("funkwhale_api.federation.api_views.routes.outbox.dispatch") -# data = { -# "approved": True, -# } -# response = logged_in_api_client.patch(url, data=data, format="json") -# assert response.status_code == 204 -# assert routes.call_count == 1 - - -# # def test_only_target_user_can_patch_follow_user(factories, logged_in_api_client): -# # logged_in_api_client.user.create_actor() -# # followed_user = factories["users.User"]() -# # url = reverse("api:v2:users:users-follow-requests", kwargs={"pk": followed_user.pk}) -# # data = { -# # "approved": True, -# # "actor": logged_in_api_client.user.actor, -# # "target": followed_user.pk, -# # } -# # response = logged_in_api_client.patch(url, data=data) -# # assert response.status_code == 403 - - -# def test_can_get_my_userfollowings(factories, logged_in_api_client, mocker): -# logged_in_api_client.user.create_actor() -# followed_user = factories["users.User"](with_actor=True) -# user_follow = factories["federation.UserFollow"]( -# actor=logged_in_api_client.user.actor, target=followed_user -# ) -# url = reverse( -# "api:v2:users:users-followings", kwargs={"pk": logged_in_api_client.user.pk} -# ) -# response = logged_in_api_client.get(url) -# assert response.status_code == 200 -# assert str(user_follow.uuid) == response.data["results"][0]["uuid"] - - -# def test_can_get_user_public_profile_userfollowings( -# factories, logged_in_api_client, mocker -# ): -# logged_in_api_client.user.create_actor() -# following_user = factories["users.User"](with_actor=True, privacy_level="public") -# user_follow = factories["federation.UserFollow"](actor=following_user.actor) -# url = reverse("api:v2:users:users-followings", kwargs={"pk": following_user.pk}) -# response = logged_in_api_client.get(url) -# assert response.status_code == 200 -# assert str(user_follow.uuid) == response.data["results"][0]["uuid"] - - -# def test_cannot_get_user_private_profile_userfollowings( -# factories, logged_in_api_client, mocker -# ): -# logged_in_api_client.user.create_actor() -# following_user = factories["users.User"](with_actor=True, privacy_level="private") -# user_follow = factories["federation.UserFollow"](actor=following_user.actor) -# url = reverse("api:v2:users:users-followings", kwargs={"pk": following_user.pk}) -# response = logged_in_api_client.get(url) -# assert response.status_code == 403 - - -# def test_can_get_user_pod_profile_userfollowings( -# factories, logged_in_api_client, mocker -# ): -# logged_in_api_client.user.create_actor() -# following_user = factories["users.User"](with_actor=True, privacy_level="pod") -# user_follow = factories["federation.UserFollow"](actor=following_user.actor) -# url = reverse("api:v2:users:users-followings", kwargs={"pk": following_user.pk}) -# response = logged_in_api_client.get(url) -# assert response.status_code == 200 - - -# def test_cannot_get_user_pod_profile_userfollowings( -# factories, logged_in_api_client, mocker -# ): -# factories["federation.Domain"](name="notatalllocal") -# logged_in_api_client.user.create_actor(domain_id="notatalllocal") -# following_user = factories["users.User"](with_actor=True, privacy_level="pod") -# user_follow = factories["federation.UserFollow"](actor=following_user.actor) -# url = reverse("api:v2:users:users-followings", kwargs={"pk": following_user.pk}) -# response = logged_in_api_client.get(url) -# assert response.status_code == 403 - - -# def test_can_get_my_followers(factories, logged_in_api_client, mocker): -# following_user = factories["users.User"](with_actor=True) -# user_follow = factories["federation.UserFollow"]( -# target=logged_in_api_client.user, actor=following_user.actor -# ) -# url = reverse( -# "api:v2:users:users-followers", kwargs={"pk": logged_in_api_client.user.pk} -# ) -# response = logged_in_api_client.get(url) -# assert response.status_code == 200 -# assert str(user_follow.uuid) == response.data["results"][0]["uuid"] - - -# # to do : do this through should_autoapprove_follow autoapprove = serializer.validated_data["object"].should_autoapprove_follow( -# def test_follow_public_user_autoapprove(factories, logged_in_api_client, mocker): -# logged_in_api_client.user.create_actor() -# followed_user = factories["users.User"](with_actor=True, privacy_level="public") -# url = reverse("api:v2:users:users-follow-requests", kwargs={"pk": followed_user.pk}) -# routes = mocker.patch("funkwhale_api.federation.api_views.routes.outbox.dispatch") -# response = logged_in_api_client.post(url) -# assert response.status_code == 204 -# assert routes.call_count == 1