kopia lustrzana https://gitlab.com/marnanel/chapeau
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.merge-requests/2/merge
rodzic
60b70f9cee
commit
30f445aa3b
|
@ -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()),
|
||||
|
||||
|
|
|
@ -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',
|
||||
]
|
|
@ -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)
|
||||
|
||||
|
|
@ -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,
|
||||
]
|
|
@ -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',
|
||||
)
|
|
@ -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
|
|
@ -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
|
Ładowanie…
Reference in New Issue