Expose public libraries and channels in standard API

This commit is contained in:
Agate 2020-07-28 14:21:15 +02:00
commit eb66d4e3d2
8 changed files with 311 additions and 55 deletions

View file

@ -53,3 +53,13 @@ class ActorFetchDelay(preferences.DefaultFromSettingMixin, types.IntPreference):
"request authentication."
)
field_kwargs = {"required": False}
@global_preferences_registry.register
class PublicIndex(types.BooleanPreference):
show_in_api = True
section = federation
name = "public_index"
default = True
verbose_name = "Enable public index"
help_text = "If this is enabled, public channels and libraries will be crawlable by other pods and bots"

View file

@ -1096,9 +1096,6 @@ class CollectionPageSerializer(jsonld.JsonLdSerializer):
d = {
"id": id,
"partOf": conf["id"],
# XXX Stable release: remove the obsolete actor field
"actor": conf["actor"].fid,
"attributedTo": conf["actor"].fid,
"totalItems": page.paginator.count,
"type": "CollectionPage",
"first": first,
@ -1110,6 +1107,10 @@ class CollectionPageSerializer(jsonld.JsonLdSerializer):
for i in page.object_list
],
}
if conf["actor"]:
# XXX Stable release: remove the obsolete actor field
d["actor"] = conf["actor"].fid
d["attributedTo"] = conf["actor"].fid
if page.has_previous():
d["prev"] = common_utils.set_query_parameter(
@ -2030,3 +2031,33 @@ class DeleteSerializer(jsonld.JsonLdSerializer):
):
raise serializers.ValidationError("You cannot delete this object")
return validated_data
class IndexSerializer(jsonld.JsonLdSerializer):
type = serializers.ChoiceField(
choices=[contexts.AS.Collection, contexts.AS.OrderedCollection]
)
totalItems = serializers.IntegerField(min_value=0)
id = serializers.URLField(max_length=500)
first = serializers.URLField(max_length=500)
last = serializers.URLField(max_length=500)
class Meta:
jsonld_mapping = PAGINATED_COLLECTION_JSONLD_MAPPING
def to_representation(self, conf):
paginator = Paginator(conf["items"], conf["page_size"])
first = common_utils.set_query_parameter(conf["id"], page=1)
current = first
last = common_utils.set_query_parameter(conf["id"], page=paginator.num_pages)
d = {
"id": conf["id"],
"totalItems": paginator.count,
"type": "OrderedCollection",
"current": current,
"first": first,
"last": last,
}
if self.context.get("include_ap_context", True):
d["@context"] = jsonld.get_default_context()
return d

View file

@ -5,6 +5,7 @@ from . import views
router = routers.SimpleRouter(trailing_slash=False)
music_router = routers.SimpleRouter(trailing_slash=False)
index_router = routers.SimpleRouter(trailing_slash=False)
router.register(r"federation/shared", views.SharedViewSet, "shared")
router.register(r"federation/actors", views.ActorViewSet, "actors")
@ -17,6 +18,11 @@ music_router.register(r"uploads", views.MusicUploadViewSet, "uploads")
music_router.register(r"artists", views.MusicArtistViewSet, "artists")
music_router.register(r"albums", views.MusicAlbumViewSet, "albums")
music_router.register(r"tracks", views.MusicTrackViewSet, "tracks")
index_router.register(r"index", views.IndexViewSet, "index")
urlpatterns = router.urls + [
url("federation/music/", include((music_router.urls, "music"), namespace="music"))
url("federation/music/", include((music_router.urls, "music"), namespace="music")),
url("federation/", include((index_router.urls, "index"), namespace="index")),
]

View file

@ -9,6 +9,7 @@ from rest_framework.decorators import action
from funkwhale_api.common import preferences
from funkwhale_api.common import utils as common_utils
from funkwhale_api.federation import utils as federation_utils
from funkwhale_api.moderation import models as moderation_models
from funkwhale_api.music import models as music_models
from funkwhale_api.music import utils as music_utils
@ -31,6 +32,34 @@ def redirect_to_html(public_url):
return response
def get_collection_response(
conf, querystring, collection_serializer, page_access_check=None
):
page = querystring.get("page")
if page is None:
data = collection_serializer.data
else:
if page_access_check and not page_access_check():
raise exceptions.AuthenticationFailed(
"You do not have access to this resource"
)
try:
page_number = int(page)
except Exception:
return response.Response({"page": ["Invalid page number"]}, status=400)
conf["page_size"] = preferences.get("federation__collection_page_size")
p = paginator.Paginator(conf["items"], conf["page_size"])
try:
page = p.page(page_number)
conf["page"] = page
serializer = serializers.CollectionPageSerializer(conf)
data = serializer.data
except paginator.EmptyPage:
return response.Response(status=404)
return response.Response(data)
class AuthenticatedIfAllowListEnabled(permissions.BasePermission):
def has_permission(self, request, view):
allow_list_enabled = preferences.get("moderation__allow_list_enabled")
@ -128,26 +157,11 @@ class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericV
.prefetch_related("library__channel__actor", "track__artist"),
"item_serializer": serializers.ChannelCreateUploadSerializer,
}
page = request.GET.get("page")
if page is None:
serializer = serializers.ChannelOutboxSerializer(channel)
data = serializer.data
else:
try:
page_number = int(page)
except Exception:
return response.Response({"page": ["Invalid page number"]}, status=400)
conf["page_size"] = preferences.get("federation__collection_page_size")
p = paginator.Paginator(conf["items"], conf["page_size"])
try:
page = p.page(page_number)
conf["page"] = page
serializer = serializers.CollectionPageSerializer(conf)
data = serializer.data
except paginator.EmptyPage:
return response.Response(status=404)
return response.Response(data)
return get_collection_response(
conf=conf,
querystring=request.GET,
collection_serializer=serializers.ChannelOutboxSerializer(channel),
)
@action(methods=["get"], detail=True)
def followers(self, request, *args, **kwargs):
@ -290,32 +304,13 @@ class MusicLibraryViewSet(
),
"item_serializer": serializers.UploadSerializer,
}
page = request.GET.get("page")
if page is None:
serializer = serializers.LibrarySerializer(lb)
data = serializer.data
else:
# if actor is requesting a specific page, we ensure library is public
# or readable by the actor
if not has_library_access(request, lb):
raise exceptions.AuthenticationFailed(
"You do not have access to this library"
)
try:
page_number = int(page)
except Exception:
return response.Response({"page": ["Invalid page number"]}, status=400)
conf["page_size"] = preferences.get("federation__collection_page_size")
p = paginator.Paginator(conf["items"], conf["page_size"])
try:
page = p.page(page_number)
conf["page"] = page
serializer = serializers.CollectionPageSerializer(conf)
data = serializer.data
except paginator.EmptyPage:
return response.Response(status=404)
return response.Response(data)
return get_collection_response(
conf=conf,
querystring=request.GET,
collection_serializer=serializers.LibrarySerializer(lb),
page_access_check=lambda: has_library_access(request, lb),
)
@action(methods=["get"], detail=True)
def followers(self, request, *args, **kwargs):
@ -436,3 +431,90 @@ class MusicTrackViewSet(
serializer = self.get_serializer(instance)
return response.Response(serializer.data)
class ChannelViewSet(
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
authentication_classes = [authentication.SignatureAuthentication]
renderer_classes = renderers.get_ap_renderers()
queryset = music_models.Artist.objects.local().select_related(
"description", "attachment_cover"
)
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 IndexViewSet(FederationMixin, viewsets.GenericViewSet):
authentication_classes = [authentication.SignatureAuthentication]
renderer_classes = renderers.get_ap_renderers()
def dispatch(self, request, *args, **kwargs):
if not preferences.get("federation__public_index"):
return HttpResponse(status=405)
return super().dispatch(request, *args, **kwargs)
@action(
methods=["get"], detail=False,
)
def libraries(self, request, *args, **kwargs):
libraries = (
music_models.Library.objects.local()
.filter(channel=None, privacy_level="everyone")
.prefetch_related("actor")
.order_by("creation_date")
)
conf = {
"id": federation_utils.full_url(
reverse("federation:index:index-libraries")
),
"items": libraries,
"item_serializer": serializers.LibrarySerializer,
"page_size": 100,
"actor": None,
}
return get_collection_response(
conf=conf,
querystring=request.GET,
collection_serializer=serializers.IndexSerializer(conf),
)
return response.Response({}, status=200)
@action(
methods=["get"], detail=False,
)
def channels(self, request, *args, **kwargs):
actors = (
models.Actor.objects.local()
.exclude(channel=None)
.order_by("channel__creation_date")
.prefetch_related(
"channel__attributed_to",
"channel__artist",
"channel__artist__description",
"channel__artist__attachment_cover",
)
)
conf = {
"id": federation_utils.full_url(reverse("federation:index:index-channels")),
"items": actors,
"item_serializer": serializers.ActorSerializer,
"page_size": 100,
"actor": None,
}
return get_collection_response(
conf=conf,
querystring=request.GET,
collection_serializer=serializers.IndexSerializer(conf),
)
return response.Response({}, status=200)