From 30f445aa3b2b27e158672aed547fdcc81b469904 Mon Sep 17 00:00:00 2001 From: Marnanel Thurman Date: Tue, 2 Feb 2021 20:08:32 +0000 Subject: [PATCH] trilby_api/views.py split out into several modules in trilby_api/views/. Tests all pass. Verify_Credentials renamed to VerifyCredentials for consistency. This is to make things easier when fixing issue #68. --- kepi/trilby_api/urls.py | 2 +- kepi/trilby_api/views/__init__.py | 73 ++++ kepi/trilby_api/views/oauth.py | 81 ++++ kepi/trilby_api/views/other.py | 90 +++++ kepi/trilby_api/views/persons.py | 373 +++++++++++++++++ .../{views.py => views/statuses.py} | 378 +----------------- kepi/trilby_api/views/timelines.py | 143 +++++++ 7 files changed, 764 insertions(+), 376 deletions(-) create mode 100644 kepi/trilby_api/views/__init__.py create mode 100644 kepi/trilby_api/views/oauth.py create mode 100644 kepi/trilby_api/views/other.py create mode 100644 kepi/trilby_api/views/persons.py rename kepi/trilby_api/{views.py => views/statuses.py} (57%) create mode 100644 kepi/trilby_api/views/timelines.py diff --git a/kepi/trilby_api/urls.py b/kepi/trilby_api/urls.py index 7b8a05e..6d739fb 100644 --- a/kepi/trilby_api/urls.py +++ b/kepi/trilby_api/urls.py @@ -13,7 +13,7 @@ urlpatterns = [ path('api/v1/instance/', Instance.as_view()), # keep tootstream happy path('api/v1/apps', Apps.as_view()), - path('api/v1/accounts/verify_credentials', Verify_Credentials.as_view()), + path('api/v1/accounts/verify_credentials', VerifyCredentials.as_view()), path('api/v1/accounts/update_credentials', UpdateCredentials.as_view()), diff --git a/kepi/trilby_api/views/__init__.py b/kepi/trilby_api/views/__init__.py new file mode 100644 index 0000000..cd8ef92 --- /dev/null +++ b/kepi/trilby_api/views/__init__.py @@ -0,0 +1,73 @@ +# trilby_api/views/__init__.py +# +# Part of kepi. +# Copyright (c) 2018-2021 Marnanel Thurman. +# Licensed under the GNU Public License v2. + +from .other import \ + Instance, \ + Emojis, \ + Filters, \ + Search, \ + AccountsSearch + +from .statuses import \ + Favourite, Unfavourite, \ + Reblog, Unreblog, \ + SpecificStatus, \ + Statuses, \ + StatusContext, \ + StatusFavouritedBy, \ + StatusRebloggedBy, \ + Notifications + +from .persons import \ + Follow, Unfollow, \ + UpdateCredentials, \ + VerifyCredentials,\ + User, \ + Followers, Following + +from .oauth import \ + Apps, \ + fix_oauth2_redirects + +from .timelines import \ + PublicTimeline, \ + HomeTimeline, \ + UserFeed + +__all__ = [ + # other + 'Instance', + 'Emojis', + 'Filters', + 'Search', + 'AccountsSearch', + + # statuses + 'Favourite', 'Unfavourite', + 'Reblog', 'Unreblog', + 'SpecificStatus', + 'Statuses', + 'StatusContext', + 'StatusFavouritedBy', + 'StatusRebloggedBy', + 'Notifications', + + # persons + 'Follow', 'Unfollow', + 'UpdateCredentials', + 'VerifyCredentials', + 'User', + 'Followers', 'Following', + + # oauth + 'Apps', + 'fix_oauth2_redirects', + + # timelines + 'PublicTimeline', + 'HomeTimeline', + 'UserFeed', + ] diff --git a/kepi/trilby_api/views/oauth.py b/kepi/trilby_api/views/oauth.py new file mode 100644 index 0000000..4f2bca6 --- /dev/null +++ b/kepi/trilby_api/views/oauth.py @@ -0,0 +1,81 @@ +# trilby_api/views/oauth.py +# +# Part of kepi. +# Copyright (c) 2018-2021 Marnanel Thurman. +# Licensed under the GNU Public License v2. + +import logging +logger = logging.getLogger(name='kepi') + +from django.db import IntegrityError, transaction +from django.shortcuts import render, get_object_or_404 +from django.views import View +from django.http import HttpResponse, JsonResponse, Http404 +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import AnonymousUser +from django.contrib.auth.mixins import LoginRequiredMixin +from django.utils.datastructures import MultiValueDictKeyError +from django.core.exceptions import SuspiciousOperation +from django.conf import settings +import kepi.trilby_api.models as trilby_models +import kepi.trilby_api.utils as trilby_utils +from kepi.trilby_api.serializers import * +from rest_framework import generics, response, mixins +from rest_framework.permissions import IsAuthenticated, \ + IsAuthenticatedOrReadOnly +from rest_framework.response import Response +from rest_framework.renderers import JSONRenderer +import kepi.trilby_api.receivers +from kepi.bowler_pub.utils import uri_to_url +import json +import re +import random + +def fix_oauth2_redirects(): + """ + Called from kepi.kepi.urls to fix a silly oversight + in oauth2_provider. This isn't elegant. + + oauth2_provider.http.OAuth2ResponseRedirect checks the + URL it's redirecting to, and raises DisallowedRedirect + if it's not a recognised protocol. But this breaks apps + like Tusky, which registers its own protocol with Android + and then redirects to that in order to bring itself + back once authentication's done. + + There's no way to fix this as a user of that package. + Hence, we have to monkey-patch that class. + """ + + def fake_validate_redirect(not_self, redirect_to): + return True + + from oauth2_provider.http import OAuth2ResponseRedirect as OA2RR + OA2RR.validate_redirect = fake_validate_redirect + logger.info("Monkey-patched %s.", OA2RR) + +########################### + +class Apps(View): + + def post(self, request, *args, **kwargs): + + new_app = Application( + name = request.POST['client_name'], + redirect_uris = request.POST['redirect_uris'], + client_type = 'confidential', + authorization_grant_type = 'authorization-code', + user = None, # don't need to be logged in + ) + + new_app.save() + + result = { + 'id': new_app.id, + 'client_id': new_app.client_id, + 'client_secret': new_app.client_secret, + } + + return JsonResponse(result) + + diff --git a/kepi/trilby_api/views/other.py b/kepi/trilby_api/views/other.py new file mode 100644 index 0000000..bef27df --- /dev/null +++ b/kepi/trilby_api/views/other.py @@ -0,0 +1,90 @@ +# trilby_api/views/other.py +# +# Part of kepi. +# Copyright (c) 2018-2021 Marnanel Thurman. +# Licensed under the GNU Public License v2. + +import logging +logger = logging.getLogger(name='kepi') + +from django.db import IntegrityError, transaction +from django.shortcuts import render, get_object_or_404 +from django.views import View +from django.http import HttpResponse, JsonResponse, Http404 +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import AnonymousUser +from django.contrib.auth.mixins import LoginRequiredMixin +from django.utils.datastructures import MultiValueDictKeyError +from django.core.exceptions import SuspiciousOperation +from django.conf import settings +import kepi.trilby_api.models as trilby_models +import kepi.trilby_api.utils as trilby_utils +from kepi.trilby_api.serializers import * +from rest_framework import generics, response, mixins +from rest_framework.permissions import IsAuthenticated, \ + IsAuthenticatedOrReadOnly +from rest_framework.response import Response +from rest_framework.renderers import JSONRenderer +import kepi.trilby_api.receivers +from kepi.bowler_pub.utils import uri_to_url +import json +import re +import random + +class Instance(View): + + def get(self, request, *args, **kwargs): + + result = { + 'uri': 'http://127.0.0.1', + 'title': settings.KEPI['INSTANCE_NAME'], + 'description': settings.KEPI['INSTANCE_DESCRIPTION'], + 'email': settings.KEPI['CONTACT_EMAIL'], + 'version': '1.0.0', # of the protocol + 'urls': {}, + 'languages': settings.KEPI['LANGUAGES'], + 'contact_account': settings.KEPI['CONTACT_ACCOUNT'], + } + + return JsonResponse(result) + +class Emojis(View): + # FIXME + def get(self, request, *args, **kwargs): + return JsonResponse([], + safe=False) + +class Filters(View): + # FIXME + def get(self, request, *args, **kwargs): + return JsonResponse([], + safe=False) + +class Search(View): + + # FIXME + + permission_classes = [ + IsAuthenticated, + ] + + def get(self, request, *args, **kwargs): + + result = { + 'accounts': [], + 'statuses': [], + 'hashtags': [], + } + + return JsonResponse(result) + +class AccountsSearch(generics.ListAPIView): + + # FIXME + + queryset = trilby_models.Person.objects.all() + serializer_class = UserSerializer + + permission_classes = [ + IsAuthenticated, + ] diff --git a/kepi/trilby_api/views/persons.py b/kepi/trilby_api/views/persons.py new file mode 100644 index 0000000..0a25511 --- /dev/null +++ b/kepi/trilby_api/views/persons.py @@ -0,0 +1,373 @@ +# trilby_api/views/persons.py +# +# Part of kepi. +# Copyright (c) 2018-2021 Marnanel Thurman. +# Licensed under the GNU Public License v2. + +import logging +logger = logging.getLogger(name='kepi') + +from django.db import IntegrityError, transaction +from django.shortcuts import render, get_object_or_404 +from django.views import View +from django.http import HttpResponse, JsonResponse, Http404 +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import AnonymousUser +from django.contrib.auth.mixins import LoginRequiredMixin +from django.utils.datastructures import MultiValueDictKeyError +from django.core.exceptions import SuspiciousOperation +from django.conf import settings +import kepi.trilby_api.models as trilby_models +import kepi.trilby_api.utils as trilby_utils +from kepi.trilby_api.serializers import * +from rest_framework import generics, response, mixins +from rest_framework.permissions import IsAuthenticated, \ + IsAuthenticatedOrReadOnly +from rest_framework.response import Response +from rest_framework.renderers import JSONRenderer +import kepi.trilby_api.receivers +from kepi.bowler_pub.utils import uri_to_url +import json +import re +import random + +class DoSomethingWithPerson(generics.GenericAPIView): + + serializer_class = UserSerializer + queryset = trilby_models.Person.objects.all() + + def _do_something_with(self, the_person, request): + raise NotImplementedError() + + def post(self, request, *args, **kwargs): + + if request.user is None: + logger.debug(' -- user not logged in') + return error_response(401, 'Not logged in') + + try: + the_person = get_object_or_404( + self.get_queryset(), + id = int(kwargs['user']), + ) + except ValueError: + return error_response(404, 'Non-decimal ID') + + result = self._do_something_with(the_person, request) + + if result is None: + result = the_person + + serializer = UserSerializer( + result, + context = { + 'request': request, + }, + ) + + return JsonResponse( + serializer.data, + status = 200, + reason = 'Done', + ) + +class Follow(DoSomethingWithPerson): + + def _do_something_with(self, the_person, request): + + try: + + if the_person.auto_follow: + offer = None + else: + number = random.randint(0, 0xffffffff) + offer = uri_to_url(settings.KEPI['FOLLOW_REQUEST_LINK'] % { + 'username': request.user.username, + 'number': number, + }) + + follow = trilby_models.Follow( + follower = request.user.localperson, + following = the_person, + offer = offer, + ) + + with transaction.atomic(): + follow.save( + send_signal = True, + ) + + logger.info(' -- follow: %s', follow) + logger.debug(' -- offer ID: %s', offer) + + if the_person.auto_follow: + follow_back = trilby_models.Follow( + follower = the_person, + following = request.user.localperson, + offer = None, + ) + + with transaction.atomic(): + follow_back.save( + send_signal = True, + ) + + logger.info(' -- follow back: %s', follow_back) + + return the_person + + except IntegrityError: + logger.info(' -- not creating a follow; it already exists') + +class Unfollow(DoSomethingWithPerson): + + def _do_something_with(self, the_person, request): + + try: + follow = trilby_models.Follow.objects.get( + follower = request.user.localperson, + following = the_person, + ) + + logger.info(' -- unfollowing: %s', follow) + + with transaction.atomic(): + follow.delete( + send_signal = True, + ) + + return the_person + + except trilby_models.Follow.DoesNotExist: + logger.info(' -- not unfollowing; they weren\'t following '+\ + 'in the first place') + +class UpdateCredentials(generics.GenericAPIView): + + def patch(self, request, *args, **kwargs): + + if request.user is None: + logger.debug(' -- user not logged in') + return error_response(401, 'Not logged in') + + who = request.user.localperson + + # The Mastodon spec doesn't say what to do + # if the user submits field names which don't + # exist! + + unknown_fields = [] + + # FIXME: the data in "v" needs cleaning. + + logger.info('-- updating user: %s', who) + + for f,v in request.data.items(): + + logger.info(' -- setting %s = %s', f, v) + + if f=='discoverable': + raise Http404("discoverable is not yet supported") + elif f=='bot': + who.bot = v + elif f=='display_name': + who.display_name = v + elif f=='note': + who.note = v + elif f=='avatar': + raise Http404("images are not yet supported") + elif f=='header': + raise Http404("images are not yet supported") + elif f=='locked': + who.locked = v + elif f=='source[privacy]': + who.default_visibility = v + elif f=='source[sensitive]': + who.default_sensitive = v + elif f=='source[language]': + who.language = v + elif f=='fields_attributes': + raise Http404("fields are not yet supported") + else: + logger.info(' -- field does not exist') + unknown_fields.append(f) + + if unknown_fields: + logger.info(' -- aborting because of unknown fields') + raise Http404(f"some fields do not exist: {unknown_fields}") + + who.save() + logger.info(' -- done.') + + serializer = UserSerializerWithSource( + who, + context = { + 'request': request, + }, + ) + + return JsonResponse( + serializer.data, + status = 200, + reason = 'Done', + ) + +########################### + +class VerifyCredentials(generics.GenericAPIView): + + queryset = TrilbyUser.objects.all() + + def get(self, request, *args, **kwargs): + serializer = UserSerializerWithSource(request.user.localperson) + return JsonResponse(serializer.data) + +########################### + +class User(generics.GenericAPIView): + + queryset = trilby_models.Person.objects.all() + + def get(self, request, *args, **kwargs): + try: + whoever = get_object_or_404( + self.get_queryset(), + id = int(kwargs['user']), + ) + except ValueError: + return error_response(404, 'Non-decimal ID') + + serializer = UserSerializer(whoever) + return JsonResponse(serializer.data) + +####################################### + +class Followers_or_Following(generics.GenericAPIView): + serializer_class = UserSerializer + queryset = trilby_models.Person.objects.all() + + def get(self, request, *args, **kwargs): + + params = request.data + + if request.user.localperson is None: + logger.debug(' -- user not logged in') + return error_response(401, 'Not logged in') + + try: + the_person = get_object_or_404( + self.get_queryset(), + id = int(kwargs['user']), + ) + except ValueError: + return error_response(404, 'Non-decimal ID') + + queryset = self._get_list_for(the_person) + + if 'max_id' in params: + queryset = queryset.filter( + id__le = params['max_id'], + ) + + if 'since_id' in params: + queryset = queryset.filter( + id__gt = params['since_id'], + ) + + if 'limit' in params: + queryset = queryset[:params['limit']] + + serializer = UserSerializer( + queryset, + many = True, + context = { + 'request': request, + }, + ) + + return JsonResponse( + serializer.data, + safe = False, # it's a list + status = 200, + reason = 'Done', + ) + +class Followers(Followers_or_Following): + def _get_list_for(self, the_person): + return the_person.followers + +class Following(Followers_or_Following): + def _get_list_for(self, the_person): + return the_person.following + +########################### + +class UpdateCredentials(generics.GenericAPIView): + + def patch(self, request, *args, **kwargs): + + if request.user is None: + logger.debug(' -- user not logged in') + return error_response(401, 'Not logged in') + + who = request.user.localperson + + # The Mastodon spec doesn't say what to do + # if the user submits field names which don't + # exist! + + unknown_fields = [] + + # FIXME: the data in "v" needs cleaning. + + logger.info('-- updating user: %s', who) + + for f,v in request.data.items(): + + logger.info(' -- setting %s = %s', f, v) + + if f=='discoverable': + raise Http404("discoverable is not yet supported") + elif f=='bot': + who.bot = v + elif f=='display_name': + who.display_name = v + elif f=='note': + who.note = v + elif f=='avatar': + raise Http404("images are not yet supported") + elif f=='header': + raise Http404("images are not yet supported") + elif f=='locked': + who.locked = v + elif f=='source[privacy]': + who.default_visibility = v + elif f=='source[sensitive]': + who.default_sensitive = v + elif f=='source[language]': + who.language = v + elif f=='fields_attributes': + raise Http404("fields are not yet supported") + else: + logger.info(' -- field does not exist') + unknown_fields.append(f) + + if unknown_fields: + logger.info(' -- aborting because of unknown fields') + raise Http404(f"some fields do not exist: {unknown_fields}") + + who.save() + logger.info(' -- done.') + + serializer = UserSerializerWithSource( + who, + context = { + 'request': request, + }, + ) + + return JsonResponse( + serializer.data, + status = 200, + reason = 'Done', + ) diff --git a/kepi/trilby_api/views.py b/kepi/trilby_api/views/statuses.py similarity index 57% rename from kepi/trilby_api/views.py rename to kepi/trilby_api/views/statuses.py index bca8aad..a11ba13 100644 --- a/kepi/trilby_api/views.py +++ b/kepi/trilby_api/views/statuses.py @@ -1,7 +1,7 @@ -# views.py +# trilby_api/views/statuses.py # # Part of kepi. -# Copyright (c) 2018-2020 Marnanel Thurman. +# Copyright (c) 2018-2021 Marnanel Thurman. # Licensed under the GNU Public License v2. import logging @@ -20,7 +20,7 @@ from django.core.exceptions import SuspiciousOperation from django.conf import settings import kepi.trilby_api.models as trilby_models import kepi.trilby_api.utils as trilby_utils -from .serializers import * +from kepi.trilby_api.serializers import * from rest_framework import generics, response, mixins from rest_framework.permissions import IsAuthenticated, \ IsAuthenticatedOrReadOnly @@ -34,25 +34,6 @@ import random ########################### -class Instance(View): - - def get(self, request, *args, **kwargs): - - result = { - 'uri': 'http://127.0.0.1', - 'title': settings.KEPI['INSTANCE_NAME'], - 'description': settings.KEPI['INSTANCE_DESCRIPTION'], - 'email': settings.KEPI['CONTACT_EMAIL'], - 'version': '1.0.0', # of the protocol - 'urls': {}, - 'languages': settings.KEPI['LANGUAGES'], - 'contact_account': settings.KEPI['CONTACT_ACCOUNT'], - } - - return JsonResponse(result) - -########################### - def error_response(status, reason): return JsonResponse( { @@ -313,149 +294,6 @@ class Unfollow(DoSomethingWithPerson): logger.info(' -- not unfollowing; they weren\'t following '+\ 'in the first place') -class UpdateCredentials(generics.GenericAPIView): - - def patch(self, request, *args, **kwargs): - - if request.user is None: - logger.debug(' -- user not logged in') - return error_response(401, 'Not logged in') - - who = request.user.localperson - - # The Mastodon spec doesn't say what to do - # if the user submits field names which don't - # exist! - - unknown_fields = [] - - # FIXME: the data in "v" needs cleaning. - - logger.info('-- updating user: %s', who) - - for f,v in request.data.items(): - - logger.info(' -- setting %s = %s', f, v) - - if f=='discoverable': - raise Http404("discoverable is not yet supported") - elif f=='bot': - who.bot = v - elif f=='display_name': - who.display_name = v - elif f=='note': - who.note = v - elif f=='avatar': - raise Http404("images are not yet supported") - elif f=='header': - raise Http404("images are not yet supported") - elif f=='locked': - who.locked = v - elif f=='source[privacy]': - who.default_visibility = v - elif f=='source[sensitive]': - who.default_sensitive = v - elif f=='source[language]': - who.language = v - elif f=='fields_attributes': - raise Http404("fields are not yet supported") - else: - logger.info(' -- field does not exist') - unknown_fields.append(f) - - if unknown_fields: - logger.info(' -- aborting because of unknown fields') - raise Http404(f"some fields do not exist: {unknown_fields}") - - who.save() - logger.info(' -- done.') - - serializer = UserSerializerWithSource( - who, - context = { - 'request': request, - }, - ) - - return JsonResponse( - serializer.data, - status = 200, - reason = 'Done', - ) - -########################### - -def fix_oauth2_redirects(): - """ - Called from kepi.kepi.urls to fix a silly oversight - in oauth2_provider. This isn't elegant. - - oauth2_provider.http.OAuth2ResponseRedirect checks the - URL it's redirecting to, and raises DisallowedRedirect - if it's not a recognised protocol. But this breaks apps - like Tusky, which registers its own protocol with Android - and then redirects to that in order to bring itself - back once authentication's done. - - There's no way to fix this as a user of that package. - Hence, we have to monkey-patch that class. - """ - - def fake_validate_redirect(not_self, redirect_to): - return True - - from oauth2_provider.http import OAuth2ResponseRedirect as OA2RR - OA2RR.validate_redirect = fake_validate_redirect - logger.info("Monkey-patched %s.", OA2RR) - -########################### - -class Apps(View): - - def post(self, request, *args, **kwargs): - - new_app = Application( - name = request.POST['client_name'], - redirect_uris = request.POST['redirect_uris'], - client_type = 'confidential', - authorization_grant_type = 'authorization-code', - user = None, # don't need to be logged in - ) - - new_app.save() - - result = { - 'id': new_app.id, - 'client_id': new_app.client_id, - 'client_secret': new_app.client_secret, - } - - return JsonResponse(result) - -class Verify_Credentials(generics.GenericAPIView): - - queryset = TrilbyUser.objects.all() - - def get(self, request, *args, **kwargs): - serializer = UserSerializerWithSource(request.user.localperson) - return JsonResponse(serializer.data) - -class User(generics.GenericAPIView): - - queryset = trilby_models.Person.objects.all() - - def get(self, request, *args, **kwargs): - try: - whoever = get_object_or_404( - self.get_queryset(), - id = int(kwargs['user']), - ) - except ValueError: - return error_response(404, 'Non-decimal ID') - - serializer = UserSerializer(whoever) - return JsonResponse(serializer.data) - class SpecificStatus(generics.GenericAPIView): queryset = trilby_models.Status.objects.filter(remote_url=None) @@ -646,145 +484,9 @@ class StatusRebloggedBy(generics.ListCreateAPIView): safe=False, # it's a list ) -class AbstractTimeline(generics.ListAPIView): - - serializer_class = StatusSerializer - permission_classes = [ - IsAuthenticated, - ] - - def get_queryset(self, request): - raise NotImplementedError("cannot query abstract timeline") - - def get(self, request): - queryset = self.get_queryset(request) - serializer = self.serializer_class(queryset, - many = True, - context = { - 'request': request, - }) - return Response(serializer.data) - -PUBLIC_TIMELINE_SLICE_LENGTH = 20 - -class PublicTimeline(AbstractTimeline): - - permission_classes = () - - def get_queryset(self, request): - - result = trilby_models.Status.objects.filter( - visibility = trilby_utils.VISIBILITY_PUBLIC, - )[:PUBLIC_TIMELINE_SLICE_LENGTH] - - return result - -class HomeTimeline(AbstractTimeline): - - permission_classes = [ - IsAuthenticated, - ] - - def get_queryset(self, request): - - result = request.user.localperson.inbox - - logger.debug("Home timeline is %s", - result) - - return result - ######################################## # TODO stub -class AccountsSearch(generics.ListAPIView): - - queryset = trilby_models.Person.objects.all() - serializer_class = UserSerializer - - permission_classes = [ - IsAuthenticated, - ] - -######################################## - -# TODO stub -class Search(View): - - permission_classes = [ - IsAuthenticated, - ] - - def get(self, request, *args, **kwargs): - - result = { - 'accounts': [], - 'statuses': [], - 'hashtags': [], - } - - return JsonResponse(result) - -######################################## - -class UserFeed(View): - - permission_classes = () - - def get(self, request, username, *args, **kwargs): - - try: - the_person = get_object_or_404( - self.get_queryset(), - id = int(kwargs['user']), - ) - except ValueError: - return error_response(404, 'Non-decimal ID') - - context = { - 'self': request.build_absolute_uri(), - 'user': the_person, - 'statuses': the_person.outbox, - 'server_name': settings.KEPI['LOCAL_OBJECT_HOSTNAME'], - } - - result = render( - request=request, - template_name='account.atom.xml', - context=context, - content_type='application/atom+xml', - ) - - links = ', '.join( - [ '<{}>; rel="{}"; type="{}"'.format( - settings.KEPI[uri].format( - hostname = settings.KEPI['LOCAL_OBJECT_HOSTNAME'], - username = the_person.id[1:], - ), - rel, mimetype) - for uri, rel, mimetype in - [ - ('USER_WEBFINGER_URLS', - 'lrdd', - 'application/xrd+xml', - ), - - ('USER_FEED_URLS', - 'alternate', - 'application/atom+xml', - ), - - ('USER_FEED_URLS', - 'alternate', - 'application/activity+json', - ), - ] - ]) - - result['Link'] = links - - return result - ######################################## class Notifications(generics.ListAPIView): @@ -802,77 +504,3 @@ class Notifications(generics.ListAPIView): serializer = self.serializer_class(queryset, many=True) return Response(serializer.data) - -######################################## - -class Emojis(View): - # FIXME - def get(self, request, *args, **kwargs): - return JsonResponse([], - safe=False) - -class Filters(View): - # FIXME - def get(self, request, *args, **kwargs): - return JsonResponse([], - safe=False) - -######################################## - -class Followers_or_Following(generics.GenericAPIView): - serializer_class = UserSerializer - queryset = trilby_models.Person.objects.all() - - def get(self, request, *args, **kwargs): - - params = request.data - - if request.user.localperson is None: - logger.debug(' -- user not logged in') - return error_response(401, 'Not logged in') - - try: - the_person = get_object_or_404( - self.get_queryset(), - id = int(kwargs['user']), - ) - except ValueError: - return error_response(404, 'Non-decimal ID') - - queryset = self._get_list_for(the_person) - - if 'max_id' in params: - queryset = queryset.filter( - id__le = params['max_id'], - ) - - if 'since_id' in params: - queryset = queryset.filter( - id__gt = params['since_id'], - ) - - if 'limit' in params: - queryset = queryset[:params['limit']] - - serializer = UserSerializer( - queryset, - many = True, - context = { - 'request': request, - }, - ) - - return JsonResponse( - serializer.data, - safe = False, # it's a list - status = 200, - reason = 'Done', - ) - -class Followers(Followers_or_Following): - def _get_list_for(self, the_person): - return the_person.followers - -class Following(Followers_or_Following): - def _get_list_for(self, the_person): - return the_person.following diff --git a/kepi/trilby_api/views/timelines.py b/kepi/trilby_api/views/timelines.py new file mode 100644 index 0000000..971156a --- /dev/null +++ b/kepi/trilby_api/views/timelines.py @@ -0,0 +1,143 @@ +# trilby_api/views/timelines.py +# +# Part of kepi. +# Copyright (c) 2018-2021 Marnanel Thurman. +# Licensed under the GNU Public License v2. + +import logging +logger = logging.getLogger(name='kepi') + +from django.db import IntegrityError, transaction +from django.shortcuts import render, get_object_or_404 +from django.views import View +from django.http import HttpResponse, JsonResponse, Http404 +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import AnonymousUser +from django.contrib.auth.mixins import LoginRequiredMixin +from django.utils.datastructures import MultiValueDictKeyError +from django.core.exceptions import SuspiciousOperation +from django.conf import settings +import kepi.trilby_api.models as trilby_models +import kepi.trilby_api.utils as trilby_utils +from kepi.trilby_api.serializers import * +from rest_framework import generics, response, mixins +from rest_framework.permissions import IsAuthenticated, \ + IsAuthenticatedOrReadOnly +from rest_framework.response import Response +from rest_framework.renderers import JSONRenderer +import kepi.trilby_api.receivers +from kepi.bowler_pub.utils import uri_to_url +import json +import re +import random + + +class AbstractTimeline(generics.ListAPIView): + + serializer_class = StatusSerializer + permission_classes = [ + IsAuthenticated, + ] + + def get_queryset(self, request): + raise NotImplementedError("cannot query abstract timeline") + + def get(self, request): + queryset = self.get_queryset(request) + serializer = self.serializer_class(queryset, + many = True, + context = { + 'request': request, + }) + return Response(serializer.data) + +PUBLIC_TIMELINE_SLICE_LENGTH = 20 + +class PublicTimeline(AbstractTimeline): + + permission_classes = () + + def get_queryset(self, request): + + result = trilby_models.Status.objects.filter( + visibility = trilby_utils.VISIBILITY_PUBLIC, + )[:PUBLIC_TIMELINE_SLICE_LENGTH] + + return result + +class HomeTimeline(AbstractTimeline): + + permission_classes = [ + IsAuthenticated, + ] + + def get_queryset(self, request): + + result = request.user.localperson.inbox + + logger.debug("Home timeline is %s", + result) + + return result + +######################################## + +######################################## + +class UserFeed(View): + + permission_classes = () + + def get(self, request, username, *args, **kwargs): + + try: + the_person = get_object_or_404( + self.get_queryset(), + id = int(kwargs['user']), + ) + except ValueError: + return error_response(404, 'Non-decimal ID') + + context = { + 'self': request.build_absolute_uri(), + 'user': the_person, + 'statuses': the_person.outbox, + 'server_name': settings.KEPI['LOCAL_OBJECT_HOSTNAME'], + } + + result = render( + request=request, + template_name='account.atom.xml', + context=context, + content_type='application/atom+xml', + ) + + links = ', '.join( + [ '<{}>; rel="{}"; type="{}"'.format( + settings.KEPI[uri].format( + hostname = settings.KEPI['LOCAL_OBJECT_HOSTNAME'], + username = the_person.id[1:], + ), + rel, mimetype) + for uri, rel, mimetype in + [ + ('USER_WEBFINGER_URLS', + 'lrdd', + 'application/xrd+xml', + ), + + ('USER_FEED_URLS', + 'alternate', + 'application/atom+xml', + ), + + ('USER_FEED_URLS', + 'alternate', + 'application/activity+json', + ), + ] + ]) + + result['Link'] = links + + return result