kopia lustrzana https://gitlab.com/marnanel/chapeau
bowler's views rejigged, and obsolete code removed. Views tests now pass.
rodzic
e9d780d2a3
commit
39154b1450
|
@ -4,15 +4,6 @@
|
|||
# Copyright (c) 2018-2019 Marnanel Thurman.
|
||||
# Licensed under the GNU Public License v2.
|
||||
|
||||
"""
|
||||
Various views. See views/README.md if you're wondering
|
||||
about the ACTIVITY_* methods.
|
||||
|
||||
This module is too large and should be split up.
|
||||
Everything ends up here if it doesn't have a particular
|
||||
place to go.
|
||||
"""
|
||||
|
||||
from kepi.bowler_pub import ATSIGN_CONTEXT
|
||||
import kepi.bowler_pub.validation
|
||||
from kepi.bowler_pub.utils import configured_url, short_id_to_url, uri_to_url, is_local
|
||||
|
@ -38,7 +29,6 @@ logger = logging.getLogger(name='kepi')
|
|||
PAGE_LENGTH = 50
|
||||
PAGE_FIELD = 'page'
|
||||
|
||||
# TODO: KepiView is obsolescent
|
||||
class KepiView(django.views.View):
|
||||
|
||||
def __init__(self):
|
||||
|
@ -46,93 +36,26 @@ class KepiView(django.views.View):
|
|||
|
||||
self.http_method_names.extend([
|
||||
'activity_get',
|
||||
'activity_store',
|
||||
])
|
||||
|
||||
def activity_store(self, request, *args, **kwargs):
|
||||
"""
|
||||
An internal request to store request.activity
|
||||
in whatever we happen to represent. No return
|
||||
value is expected: instead, throw an exception if
|
||||
there's a problem.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"I don't know how to store %s in %s." % (
|
||||
request.activity,
|
||||
request.path,
|
||||
))
|
||||
|
||||
def activity_get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Returns this view in a form suitable for ActivityPub.
|
||||
|
||||
It may return:
|
||||
- a dict, suitable for turning into JSON
|
||||
- an iterable, such as a list or QuerySet;
|
||||
this will be turned into an OrderedCollection.
|
||||
Every member will be passed through
|
||||
_modify_list_item before rendering.
|
||||
- anything with a property called "activity_form";
|
||||
this value should be either an iterable or a dict,
|
||||
as above.
|
||||
|
||||
This method is usually called by a KepiView's get() handler.
|
||||
By default, its args and kwargs will be passed in unchanged.
|
||||
|
||||
It's also used to retrieve the ActivityPub form within
|
||||
the rest of the program, rather than as a response to
|
||||
a particular HTTP request. In that case, "request" will
|
||||
not be a real HttpRequest. (XXX so, what *will* it be?)
|
||||
It's used to retrieve the ActivityPub form within
|
||||
the rest of the program, rather than as a response to
|
||||
a particular HTTP request. In that case, "request" will
|
||||
not be a real HttpRequest from across the network;
|
||||
it'll be mocked up.
|
||||
|
||||
Override this method in your subclass. In KepiView
|
||||
it's abstract.
|
||||
"""
|
||||
raise NotImplementedError("implement activity_get() in a subclass: %s" % (
|
||||
raise NotImplementedError(
|
||||
"implement activity_get() in a subclass: %s" % (
|
||||
self.__class__,
|
||||
))
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
if request.headers['Content-Type'] not in [
|
||||
'application/activity+json',
|
||||
'application/json',
|
||||
]:
|
||||
return HttpResponse(
|
||||
status = 415, # unsupported media type
|
||||
reason = 'Try application/activity+json',
|
||||
)
|
||||
try:
|
||||
fields = json.loads(
|
||||
str(request.body, encoding='UTF-8'))
|
||||
except json.decoder.JSONDecodeError:
|
||||
return HttpResponse(
|
||||
status = 415, # unsupported media type
|
||||
reason = 'Invalid JSON',
|
||||
)
|
||||
except UnicodeDecodeError:
|
||||
return HttpResponse(
|
||||
status = 400, # bad request
|
||||
reason = 'Invalid UTF-8',
|
||||
)
|
||||
|
||||
validate(
|
||||
path = request.path,
|
||||
headers = request.headers,
|
||||
body = request.body,
|
||||
# is_local_user is used by create() to
|
||||
# determine whether to strip or require the
|
||||
# "id" field.
|
||||
# FIXME it probably shouldn't always be False here.
|
||||
is_local_user = False,
|
||||
)
|
||||
|
||||
return HttpResponse(
|
||||
status = 200,
|
||||
reason = 'Thank you',
|
||||
content = '',
|
||||
content_type = 'text/plain',
|
||||
)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Returns a rendered HttpResult for a GET request.
|
||||
|
@ -143,16 +66,24 @@ class KepiView(django.views.View):
|
|||
result = self.activity_get(request, *args, **kwargs)
|
||||
|
||||
if result is None:
|
||||
logger.debug(' -- activity_get returned None; 404')
|
||||
raise Http404()
|
||||
|
||||
logger.debug(' -- activity_get returned %s (%s)',
|
||||
result, type(result))
|
||||
|
||||
if isinstance(result, HttpResponse):
|
||||
logger.info('self.activity_get() returned HttpResponse %s',
|
||||
result)
|
||||
return result
|
||||
|
||||
return self._render_object(request, result)
|
||||
data = self._render_object(result)
|
||||
logger.debug(' -- rendered to %s', data)
|
||||
httpresponse = self._to_httpresponse(data)
|
||||
logger.debug(' -- as HttpResponse, %s', httpresponse)
|
||||
return httpresponse
|
||||
|
||||
def _to_json(self, data):
|
||||
def _to_httpresponse(self, data):
|
||||
|
||||
if '@context' not in data:
|
||||
data['@context'] = ATSIGN_CONTEXT
|
||||
|
@ -176,122 +107,8 @@ class KepiView(django.views.View):
|
|||
|
||||
return result
|
||||
|
||||
def _render_object(self, request, something):
|
||||
logger.debug('About to render object: %s of type %s',
|
||||
something, type(something))
|
||||
|
||||
while True:
|
||||
try:
|
||||
something = something.activity_form
|
||||
logger.debug(' -- it has an activity_form, %s; iterating',
|
||||
something)
|
||||
continue
|
||||
except AttributeError:
|
||||
break
|
||||
|
||||
if isinstance(something, dict):
|
||||
logger.debug(" -- it's a dict; our work here is done")
|
||||
return self._to_json(something)
|
||||
|
||||
elif isinstance(something, Iterable):
|
||||
logger.debug(" -- it's an iterable; treating as a collection ")
|
||||
return self._render_collection(request, something)
|
||||
|
||||
logger.warn("I don't know how to render objects like %s.", something)
|
||||
raise ValueError("I don't know how to render objects like %s." % (something,))
|
||||
|
||||
def _render_collection(self, request, items):
|
||||
|
||||
# XXX assert that items.ordered
|
||||
|
||||
our_url = request.build_absolute_uri()
|
||||
index_url = self._make_query_page_url(request, None)
|
||||
|
||||
if PAGE_FIELD in request.GET:
|
||||
|
||||
page_number = int(request.GET[PAGE_FIELD])
|
||||
logger.debug(" -- it's a request for page %d",
|
||||
page_number)
|
||||
|
||||
start = (page_number-1) * PAGE_LENGTH
|
||||
|
||||
listed_items = items[start: start+PAGE_LENGTH]
|
||||
|
||||
result = {
|
||||
"@context": ATSIGN_CONTEXT,
|
||||
"type" : "OrderedCollectionPage",
|
||||
"id" : our_url,
|
||||
"totalItems" : items.count(),
|
||||
"orderedItems" : [self._modify_list_item(x)
|
||||
for x in listed_items],
|
||||
"partOf": index_url,
|
||||
}
|
||||
|
||||
if page_number > 1:
|
||||
result["prev"] = self._make_query_page_url(request,
|
||||
page_number-1)
|
||||
|
||||
if start+PAGE_LENGTH < items.count():
|
||||
result["next"] = self._make_query_page_url(request,
|
||||
page_number+1)
|
||||
|
||||
else:
|
||||
|
||||
# Index page.
|
||||
logger.debug(" -- it's a request for the index")
|
||||
|
||||
count = items.count()
|
||||
|
||||
result = {
|
||||
"@context": ATSIGN_CONTEXT,
|
||||
"type": "OrderedCollection",
|
||||
"id": index_url,
|
||||
"totalItems" : count,
|
||||
}
|
||||
|
||||
if count>0:
|
||||
result["first"] = self._make_query_page_url(
|
||||
request = request,
|
||||
page_number = 1,
|
||||
)
|
||||
|
||||
result["last"] = self._make_query_page_url(
|
||||
request = request,
|
||||
page_number = int(
|
||||
1 + ((count+1)/PAGE_LENGTH),
|
||||
),
|
||||
)
|
||||
|
||||
return self._to_json(result)
|
||||
|
||||
def _make_query_page_url(
|
||||
self,
|
||||
request,
|
||||
page_number,
|
||||
):
|
||||
fields = dict(request.GET)
|
||||
|
||||
if page_number is None:
|
||||
if PAGE_FIELD in fields:
|
||||
del fields[PAGE_FIELD]
|
||||
else:
|
||||
fields[PAGE_FIELD] = page_number
|
||||
|
||||
encoded = urllib.parse.urlencode(fields)
|
||||
|
||||
if encoded!='':
|
||||
encoded = '?'+encoded
|
||||
|
||||
return '{}://{}{}{}'.format(
|
||||
request.scheme,
|
||||
request.get_host(),
|
||||
request.path,
|
||||
encoded,
|
||||
)
|
||||
|
||||
def _modify_list_item(self, obj):
|
||||
logger.debug(' -- default _modify_list_item for %s', obj)
|
||||
return self._render_object(obj)
|
||||
def _render_object(self, something):
|
||||
return something
|
||||
|
||||
class PersonView(KepiView):
|
||||
|
||||
|
@ -304,6 +121,7 @@ class PersonView(KepiView):
|
|||
local_user__username = self._username,
|
||||
)
|
||||
logger.debug(' -- found user: %s', person)
|
||||
return person
|
||||
|
||||
except trilby_models.LocalPerson.DoesNotExist:
|
||||
logger.info(' -- unknown user: %s', kwargs)
|
||||
|
@ -312,32 +130,7 @@ class PersonView(KepiView):
|
|||
logger.info(' -- invalid: %s', kwargs)
|
||||
return None
|
||||
|
||||
serializer = bowler_serializers.PersonSerializer(
|
||||
person,
|
||||
)
|
||||
return serializer.data
|
||||
|
||||
# FIXME rewrite using new API
|
||||
def activity_store(self, request, *args, **kwargs):
|
||||
|
||||
from kepi.bowler_pub.models.collection import Collection
|
||||
|
||||
user = AcActor.objects.get(
|
||||
id='@'+kwargs['username'],
|
||||
)
|
||||
|
||||
inbox = Collection.get(
|
||||
user = user,
|
||||
collection = 'inbox',
|
||||
create_if_missing = True,
|
||||
)
|
||||
|
||||
logger.debug('%s: inbox: storing %s',
|
||||
user.id, request.activity)
|
||||
|
||||
inbox.append(request.activity)
|
||||
|
||||
def _to_json(self, data):
|
||||
def _to_httpresponse(self, data):
|
||||
"""
|
||||
Adds the Link header to an Actor's record.
|
||||
|
||||
|
@ -345,7 +138,7 @@ class PersonView(KepiView):
|
|||
and <https://www.w3.org/wiki/LinkHeader>.
|
||||
"""
|
||||
|
||||
result = super()._to_json(data)
|
||||
result = super()._to_httpresponse(data)
|
||||
|
||||
user_url = configured_url('USER_LINK',
|
||||
username = self._username,
|
||||
|
@ -395,6 +188,12 @@ class PersonView(KepiView):
|
|||
|
||||
return result
|
||||
|
||||
def _render_object(self, something):
|
||||
serializer = bowler_serializers.PersonSerializer(
|
||||
something
|
||||
)
|
||||
return super()._render_object(serializer.data)
|
||||
|
||||
class FollowingView(KepiView):
|
||||
|
||||
def activity_get(self, request, *args, **kwargs):
|
||||
|
@ -489,55 +288,6 @@ class UserCollectionView(KepiView):
|
|||
|
||||
return result
|
||||
|
||||
def activity_store(self, request,
|
||||
username,
|
||||
listname,
|
||||
*args, **kwargs):
|
||||
|
||||
from kepi.bowler_pub.models.collection import Collection, CollectionMember
|
||||
|
||||
logger.debug('Finding user %s\'s %s collection',
|
||||
username, listname)
|
||||
try:
|
||||
the_collection = Collection.objects.get(
|
||||
owner__id = '@'+username,
|
||||
name = listname)
|
||||
|
||||
logger.debug(' -- found collection: %s. Appending %s.',
|
||||
the_collection, request.activity)
|
||||
|
||||
the_collection.append(request.activity)
|
||||
|
||||
except Collection.DoesNotExist:
|
||||
|
||||
if self._default_to_existing:
|
||||
logger.debug(' -- does not exist; creating it')
|
||||
|
||||
try:
|
||||
owner = AcActor.objects.get(
|
||||
id = '@'+username,
|
||||
)
|
||||
except AcActor.DoesNotExist:
|
||||
logger.debug(' -- but user %s doesn\'t exist; bailing',
|
||||
username)
|
||||
return
|
||||
|
||||
the_collection = Collection(
|
||||
owner = owner,
|
||||
name = listname)
|
||||
|
||||
the_collection.save()
|
||||
|
||||
the_collection.append(request.activity)
|
||||
|
||||
logger.debug(' -- done: collection is %s',
|
||||
the_collection)
|
||||
|
||||
return
|
||||
else:
|
||||
logger.debug(' -- does not exist; 404')
|
||||
raise Http404()
|
||||
|
||||
def _modify_list_item(self, obj):
|
||||
return obj
|
||||
|
||||
|
@ -563,52 +313,6 @@ class InboxView(UserCollectionView):
|
|||
listname = 'inbox',
|
||||
)
|
||||
|
||||
def activity_store(self, request,
|
||||
username=None, *args, **kwargs):
|
||||
|
||||
from kepi.bowler_pub.delivery import deliver
|
||||
|
||||
if username is None:
|
||||
logger.info(' -- storing into the shared inbox')
|
||||
|
||||
# This is a bit of a hack, but I don't want people
|
||||
# submitting requests to the shared inbox which
|
||||
# ask to be submitted back to the shared inbox.
|
||||
if hasattr(request, 'no_multiplexing'):
|
||||
logger.info(" -- but we've been down this road before")
|
||||
return
|
||||
else:
|
||||
request.no_multiplexing = True
|
||||
|
||||
recipients = sorted(set([url for urls in [
|
||||
urls for name,urls in request.activity.audiences.items()
|
||||
] for url in urls]))
|
||||
|
||||
for recipient in recipients:
|
||||
|
||||
if not is_local(recipient):
|
||||
logger.info(' -- recipient %s is remote; ignoring',
|
||||
recipient)
|
||||
continue
|
||||
|
||||
logger.info(' -- recipient %s gets a copy',
|
||||
recipient)
|
||||
|
||||
deliver(
|
||||
request.activity,
|
||||
incoming = True,
|
||||
)
|
||||
|
||||
logger.info(' -- storing to shared inbox done')
|
||||
|
||||
else:
|
||||
|
||||
super().activity_store(
|
||||
request,
|
||||
username = username,
|
||||
listname = 'inbox',
|
||||
)
|
||||
|
||||
class CollectionView(generics.GenericAPIView):
|
||||
|
||||
permission_classes = ()
|
||||
|
@ -685,7 +389,7 @@ class CollectionView(generics.GenericAPIView):
|
|||
),
|
||||
)
|
||||
|
||||
return self._to_json(result)
|
||||
return self._to_httpresponse(result)
|
||||
|
||||
def _get_items(self,
|
||||
username,
|
||||
|
@ -731,7 +435,7 @@ class CollectionView(generics.GenericAPIView):
|
|||
|
||||
return result
|
||||
|
||||
def _to_json(self, data):
|
||||
def _to_httpresponse(self, data):
|
||||
|
||||
if '@context' not in data:
|
||||
data['@context'] = ATSIGN_CONTEXT
|
||||
|
|
Ładowanie…
Reference in New Issue