diff --git a/api/funkwhale_api/federation/jsonld.py b/api/funkwhale_api/federation/jsonld.py index 05a438606..4a23ae418 100644 --- a/api/funkwhale_api/federation/jsonld.py +++ b/api/funkwhale_api/federation/jsonld.py @@ -220,16 +220,11 @@ def get_default_context(): return [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", - {"manuallyApprovesFollowers": "as:manuallyApprovesFollowers"}, - ] - - -def get_default_context_fw(): - return [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - {"manuallyApprovesFollowers": "as:manuallyApprovesFollowers"}, "https://funkwhale.audio/ns", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "Hashtag": "as:Hashtag", + }, ] diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index 08b51a630..207c4b76d 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -1583,12 +1583,12 @@ class ChannelUploadSerializer(serializers.Serializer): "url": [ { "type": "Link", - "mimeType": upload.mimetype, + "mediaType": upload.mimetype, "href": utils.full_url(upload.listen_url_no_download), }, { "type": "Link", - "mimeType": "text/html", + "mediaType": "text/html", "href": utils.full_url(upload.track.get_absolute_url()), }, ], diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py index 5efebf1ec..93977dcd2 100644 --- a/api/funkwhale_api/federation/views.py +++ b/api/funkwhale_api/federation/views.py @@ -61,6 +61,16 @@ class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericV queryset = models.Actor.objects.local().select_related("user") serializer_class = serializers.ActorSerializer + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + if utils.should_redirect_ap_to_html(request.headers.get("accept")): + if instance.get_channel(): + return redirect_to_html(instance.channel.get_absolute_url()) + return redirect_to_html(instance.get_absolute_url()) + + serializer = self.get_serializer(instance) + return response.Response(serializer.data) + @action(methods=["get", "post"], detail=True) def inbox(self, request, *args, **kwargs): inbox_actor = self.get_object() @@ -222,7 +232,6 @@ class MusicLibraryViewSet( def retrieve(self, request, *args, **kwargs): lb = self.get_object() if utils.should_redirect_ap_to_html(request.headers.get("accept")): - # XXX: implement this for actors, albums, tracks, artists return redirect_to_html(lb.get_absolute_url()) conf = { "id": lb.get_federation_id(), @@ -308,6 +317,14 @@ class MusicUploadViewSet( serializer_class = serializers.UploadSerializer lookup_field = "uuid" + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + if utils.should_redirect_ap_to_html(request.headers.get("accept")): + return redirect_to_html(instance.track.get_absolute_url()) + + serializer = self.get_serializer(instance) + return response.Response(serializer.data) + def get_queryset(self): queryset = super().get_queryset() actor = music_utils.get_actor_from_request(self.request) @@ -330,6 +347,14 @@ class MusicArtistViewSet( serializer_class = serializers.ArtistSerializer lookup_field = "uuid" + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + if utils.should_redirect_ap_to_html(request.headers.get("accept")): + return redirect_to_html(instance.get_absolute_url()) + + serializer = self.get_serializer(instance) + return response.Response(serializer.data) + class MusicAlbumViewSet( FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet @@ -342,6 +367,14 @@ class MusicAlbumViewSet( serializer_class = serializers.AlbumSerializer lookup_field = "uuid" + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + if utils.should_redirect_ap_to_html(request.headers.get("accept")): + return redirect_to_html(instance.get_absolute_url()) + + serializer = self.get_serializer(instance) + return response.Response(serializer.data) + class MusicTrackViewSet( FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet @@ -360,3 +393,11 @@ class MusicTrackViewSet( ) serializer_class = serializers.TrackSerializer lookup_field = "uuid" + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + if utils.should_redirect_ap_to_html(request.headers.get("accept")): + return redirect_to_html(instance.get_absolute_url()) + + serializer = self.get_serializer(instance) + return response.Response(serializer.data) diff --git a/api/tests/federation/test_serializers.py b/api/tests/federation/test_serializers.py index e203e0aff..ccf6a796d 100644 --- a/api/tests/federation/test_serializers.py +++ b/api/tests/federation/test_serializers.py @@ -21,7 +21,7 @@ def test_actor_serializer_from_ap(db): private, public = keys.get_key_pair() actor_url = "https://test.federation/actor" payload = { - "@context": jsonld.get_default_context_fw(), + "@context": jsonld.get_default_context(), "id": actor_url, "type": "Person", "outbox": "https://test.com/outbox", @@ -76,7 +76,7 @@ def test_actor_serializer_from_ap_no_icon_mediaType(db): private, public = keys.get_key_pair() actor_url = "https://test.federation/actor" payload = { - "@context": jsonld.get_default_context_fw(), + "@context": jsonld.get_default_context(), "id": actor_url, "type": "Person", "inbox": "https://test.com/inbox", @@ -281,7 +281,7 @@ def test_accept_follow_serializer_save(factories): follow = factories["federation.Follow"](approved=None) data = { - "@context": jsonld.get_default_context_fw(), + "@context": jsonld.get_default_context(), "id": follow.get_federation_id() + "/accept", "type": "Accept", "actor": follow.target.fid, @@ -301,7 +301,7 @@ def test_accept_follow_serializer_validates_on_context(factories): follow = factories["federation.Follow"](approved=None) impostor = factories["federation.Actor"]() data = { - "@context": jsonld.get_default_context_fw(), + "@context": jsonld.get_default_context(), "id": follow.get_federation_id() + "/accept", "type": "Accept", "actor": impostor.url, @@ -337,7 +337,7 @@ def test_undo_follow_serializer_save(factories): follow = factories["federation.Follow"](approved=True) data = { - "@context": jsonld.get_default_context_fw(), + "@context": jsonld.get_default_context(), "id": follow.get_federation_id() + "/undo", "type": "Undo", "actor": follow.actor.fid, @@ -356,7 +356,7 @@ def test_undo_follow_serializer_validates_on_context(factories): follow = factories["federation.Follow"](approved=True) impostor = factories["federation.Actor"]() data = { - "@context": jsonld.get_default_context_fw(), + "@context": jsonld.get_default_context(), "id": follow.get_federation_id() + "/undo", "type": "Undo", "actor": impostor.url, @@ -402,7 +402,7 @@ def test_paginated_collection_serializer(factories): def test_paginated_collection_serializer_validation(): data = { - "@context": jsonld.get_default_context_fw(), + "@context": jsonld.get_default_context(), "type": "Collection", "id": "https://test.federation/test", "totalItems": 5, @@ -579,7 +579,7 @@ def test_music_library_serializer_from_private(factories, mocker): "funkwhale_api.federation.utils.retrieve_ap_object", return_value=actor ) data = { - "@context": jsonld.get_default_context_fw(), + "@context": jsonld.get_default_context(), "audience": "", "name": "Hello", "summary": "World", @@ -1470,12 +1470,12 @@ def test_channel_upload_serializer(factories): "url": [ { "type": "Link", - "mimeType": upload.mimetype, + "mediaType": upload.mimetype, "href": utils.full_url(upload.listen_url_no_download), }, { "type": "Link", - "mimeType": "text/html", + "mediaType": "text/html", "href": utils.full_url(upload.track.get_absolute_url()), }, ], diff --git a/api/tests/federation/test_tasks.py b/api/tests/federation/test_tasks.py index de90f2886..f6aef6eba 100644 --- a/api/tests/federation/test_tasks.py +++ b/api/tests/federation/test_tasks.py @@ -389,7 +389,7 @@ def test_fetch_success(factories, r_mock, mocker): tasks.fetch(fetch_id=fetch.pk) fetch.refresh_from_db() - payload["@context"].append("https://funkwhale.audio/ns") + assert fetch.status == "finished" assert init.call_count == 1 assert init.call_args[0][1] == artist @@ -421,7 +421,7 @@ def test_fetch_webfinger(factories, r_mock, mocker): tasks.fetch(fetch_id=fetch.pk) fetch.refresh_from_db() - payload["@context"].append("https://funkwhale.audio/ns") + assert fetch.status == "finished" assert fetch.object == actor assert init.call_count == 1 @@ -451,7 +451,7 @@ def test_fetch_rel_alternate(factories, r_mock, mocker): tasks.fetch(fetch_id=fetch.pk) fetch.refresh_from_db() - ap_payload["@context"].append("https://funkwhale.audio/ns") + assert fetch.status == "finished" assert fetch.object == actor assert init.call_count == 1 @@ -482,7 +482,7 @@ def test_fetch_url(factory_name, serializer_class, factories, r_mock, mocker): tasks.fetch(fetch_id=fetch.pk) fetch.refresh_from_db() - payload["@context"].append("https://funkwhale.audio/ns") + assert fetch.status == "finished" assert fetch.object == obj assert init.call_count == 1 diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py index bd778778f..341f052d4 100644 --- a/api/tests/federation/test_views.py +++ b/api/tests/federation/test_views.py @@ -387,3 +387,103 @@ def test_music_library_retrieve_redirects_to_html_if_header_set( ) assert response.status_code == 302 assert response["Location"] == expected_url + + +def test_actor_retrieve_redirects_to_html_if_header_set( + factories, api_client, settings +): + actor = factories["federation.Actor"](local=True) + + url = reverse( + "federation:actors-detail", + kwargs={"preferred_username": actor.preferred_username}, + ) + response = api_client.get(url, HTTP_ACCEPT="text/html") + expected_url = utils.join_url( + settings.FUNKWHALE_URL, + utils.spa_reverse( + "actor_detail", kwargs={"username": actor.preferred_username} + ), + ) + assert response.status_code == 302 + assert response["Location"] == expected_url + + +def test_channel_actor_retrieve_redirects_to_html_if_header_set( + factories, api_client, settings +): + channel = factories["audio.Channel"](local=True) + + url = reverse( + "federation:actors-detail", + kwargs={"preferred_username": channel.actor.preferred_username}, + ) + response = api_client.get(url, HTTP_ACCEPT="text/html") + expected_url = utils.join_url( + settings.FUNKWHALE_URL, + utils.spa_reverse( + "channel_detail", kwargs={"username": channel.actor.preferred_username} + ), + ) + assert response.status_code == 302 + assert response["Location"] == expected_url + + +def test_upload_retrieve_redirects_to_html_if_header_set( + factories, api_client, settings +): + upload = factories["music.Upload"](library__local=True, playable=True) + + url = reverse("federation:music:uploads-detail", kwargs={"uuid": upload.uuid},) + response = api_client.get(url, HTTP_ACCEPT="text/html") + expected_url = utils.join_url( + settings.FUNKWHALE_URL, + utils.spa_reverse("library_track", kwargs={"pk": upload.track.pk}), + ) + assert response.status_code == 302 + assert response["Location"] == expected_url + + +def test_track_retrieve_redirects_to_html_if_header_set( + factories, api_client, settings +): + track = factories["music.Track"](local=True) + + url = reverse("federation:music:tracks-detail", kwargs={"uuid": track.uuid},) + response = api_client.get(url, HTTP_ACCEPT="text/html") + expected_url = utils.join_url( + settings.FUNKWHALE_URL, + utils.spa_reverse("library_track", kwargs={"pk": track.pk}), + ) + assert response.status_code == 302 + assert response["Location"] == expected_url + + +def test_album_retrieve_redirects_to_html_if_header_set( + factories, api_client, settings +): + album = factories["music.Album"](local=True) + + url = reverse("federation:music:albums-detail", kwargs={"uuid": album.uuid},) + response = api_client.get(url, HTTP_ACCEPT="text/html") + expected_url = utils.join_url( + settings.FUNKWHALE_URL, + utils.spa_reverse("library_album", kwargs={"pk": album.pk}), + ) + assert response.status_code == 302 + assert response["Location"] == expected_url + + +def test_artist_retrieve_redirects_to_html_if_header_set( + factories, api_client, settings +): + artist = factories["music.Artist"](local=True) + + url = reverse("federation:music:artists-detail", kwargs={"uuid": artist.uuid},) + response = api_client.get(url, HTTP_ACCEPT="text/html") + expected_url = utils.join_url( + settings.FUNKWHALE_URL, + utils.spa_reverse("library_artist", kwargs={"pk": artist.pk}), + ) + assert response.status_code == 302 + assert response["Location"] == expected_url diff --git a/front/src/views/channels/DetailBase.vue b/front/src/views/channels/DetailBase.vue index f7391dd57..a20efddc5 100644 --- a/front/src/views/channels/DetailBase.vue +++ b/front/src/views/channels/DetailBase.vue @@ -268,6 +268,15 @@ export default { this.isLoading = true let channelPromise = axios.get(`channels/${this.id}`).then(response => { self.object = response.data + if (self.id == response.data.uuid && response.data.actor) { + // replace with the pretty channel url if possible + let actor = response.data.actor + if (actor.is_local) { + self.$router.replace({name: 'channels.detail', params: {id: actor.preferred_username}}) + } else { + self.$router.replace({name: 'channels.detail', params: {id: actor.full_username}}) + } + } let tracksPromise = axios.get("tracks", {params: {channel: response.data.uuid, page_size: 1, playable: true, include_channels: true}}).then(response => { self.totalTracks = response.data.count self.isLoading = false