Skip to content
Permalink
Browse files

REST API: add HTTP HEAD support

  • Loading branch information
snarfed committed Nov 8, 2019
1 parent dc25ab3 commit abf0a3ce31cbf8a9549bd554f83ca40bbc9603af
Showing with 60 additions and 31 deletions.
  1. +2 −0 README.md
  2. +29 −30 api.py
  3. +2 −0 app.py
  4. +8 −1 test_api.py
  5. +19 −0 test_app.py
@@ -313,6 +313,8 @@ Changelog
* Add `itunes:image`, `itunes:author`, and `itunes:category`.
* Atom:
* Bug fix: extract feed image from `hfeed` correctly.
* REST API:
* Add HTTP `HEAD` support.

### 2.2 - 2019-11-02
* Add Mastodon support!
59 api.py
@@ -62,20 +62,22 @@
PATH_DEFAULTS = ((source.ME,), (source.ALL, source.FRIENDS), (source.APP,), ())
MAX_PATH_LEN = len(PATH_DEFAULTS) + 1

FORMATS = (
'activitystreams',
'as1',
'as1-xml',
'as2',
'atom',
'html',
'json',
'json-mf2',
'jsonfeed',
'mf2-json',
'rss',
'xml',
)
# map granary format name to MIME type. list of official MIME types:
# https://www.iana.org/assignments/media-types/media-types.xhtml
FORMATS = {
'activitystreams': 'application/stream+json',
'as1': 'application/stream+json',
'as1-xml': 'application/xml',
'as2': 'application/activity+json',
'atom': 'application/atom+xml',
'html': 'text/html',
'json': 'application/json',
'json-mf2': 'application/mf2+json',
'jsonfeed': 'application/json',
'mf2-json': 'application/mf2+json',
'rss': 'application/rss+xml',
'xml': 'application/xml',
}

canonicalize_domain = handlers.redirect(
('granary-demo.appspot.com', 'www.granary.io'), 'granary.io')
@@ -177,6 +179,8 @@ def get(self):

self.write_response(response, actor=actor, url=src.BASE_URL)

head = get

def write_response(self, response, actor=None, url=None, title=None,
hfeed=None):
"""Converts ActivityStreams activities and writes them out.
@@ -195,17 +199,22 @@ def write_response(self, response, actor=None, url=None, title=None,
raise exc.HTTPBadRequest('Invalid format: %s, expected one of %r' %
(format, FORMATS))

activities = response['items']
if 'plaintext' in self.request.params:
# override content type
self.response.headers['Content-Type'] = 'text/plain'
else:
content_type = FORMATS.get(format)
if content_type:
self.response.headers['Content-Type'] = content_type

if self.request.method == 'HEAD':
return

activities = response['items']
try:
if format in ('as1', 'json', 'activitystreams'):
# list of official MIME types:
# https://www.iana.org/assignments/media-types/media-types.xhtml
self.response.headers['Content-Type'] = \
'application/json' if format == 'json' else 'application/stream+json'
self.response.out.write(json_dumps(response, indent=2))
elif format == 'as2':
self.response.headers['Content-Type'] = 'application/activity+json'
response.update({
'items': [as2.from_as1(a) for a in activities],
'totalItems': response.pop('totalResults', None),
@@ -215,7 +224,6 @@ def write_response(self, response, actor=None, url=None, title=None,
})
self.response.out.write(json_dumps(util.trim_nulls(response), indent=2))
elif format == 'atom':
self.response.headers['Content-Type'] = 'application/atom+xml'
hub = self.request.get('hub')
reader = self.request.get('reader', 'true').lower()
if reader not in ('true', 'false'):
@@ -236,25 +244,20 @@ def write_response(self, response, actor=None, url=None, title=None,
if hub:
self.response.headers.add('Link', str('<%s>; rel="hub"' % hub))
elif format == 'rss':
self.response.headers['Content-Type'] = 'application/rss+xml'
if not title:
title = 'Feed for %s' % url
self.response.out.write(rss.from_activities(
activities, actor, title=title,
feed_url=self.request.url, hfeed=hfeed,
home_page_url=util.base_url(url)))
elif format in ('as1-xml', 'xml'):
self.response.headers['Content-Type'] = 'application/xml'
self.response.out.write(XML_TEMPLATE % util.to_xml(response))
elif format == 'html':
self.response.headers['Content-Type'] = 'text/html'
self.response.out.write(microformats2.activities_to_html(activities))
elif format in ('mf2-json', 'json-mf2'):
self.response.headers['Content-Type'] = 'application/mf2+json'
items = [microformats2.activity_to_json(a) for a in activities]
self.response.out.write(json_dumps({'items': items}, indent=2))
elif format == 'jsonfeed':
self.response.headers['Content-Type'] = 'application/json'
try:
jf = jsonfeed.activities_to_jsonfeed(activities, actor=actor, title=title,
feed_url=self.request.url)
@@ -265,10 +268,6 @@ def write_response(self, response, actor=None, url=None, title=None,
logging.warning('converting to output format failed', exc_info=True)
self.abort(400, 'Could not convert to %s: %s' % (format, str(e)))

if 'plaintext' in self.request.params:
# override response content type
self.response.headers['Content-Type'] = 'text/plain'

def get_kwargs(self):
"""Extracts, normalizes and returns the kwargs for get_activities().
2 app.py
@@ -222,6 +222,8 @@ def fetch_mf2_func(url):
self.write_response(source.Source.make_activities_base_response(activities),
url=url, actor=actor, title=title, hfeed=hfeed)

head = get


class MastodonStart(mastodon.StartHandler):
"""Mastodon OAuth handler wrapper that handles URL discovery errors.
@@ -56,6 +56,7 @@ def reset(self):
def get_response(self, url, *args, **kwargs):
start_index = kwargs.setdefault('start_index', 0)
kwargs.setdefault('count', api.ITEMS_PER_PAGE_DEFAULT)
method = kwargs.pop('method', 'GET')

FakeSource.get_activities_response(*args, **kwargs).AndReturn({
'startIndex': start_index,
@@ -68,7 +69,7 @@ def get_response(self, url, *args, **kwargs):
})
self.mox.ReplayAll()

return api.application.get_response(url)
return api.application.get_response(url, method=method)

def check_request(self, url, *args, **kwargs):
resp = self.get_response('/fake' + url, *args, **kwargs)
@@ -307,3 +308,9 @@ def test_get_activities_connection_error(self):
self.mox.ReplayAll()
resp = api.application.get_response('/fake/@me')
self.assertEquals(504, resp.status_int)

def test_http_head(self):
resp = self.get_response('/fake?format=html', method='HEAD')
self.assertEquals(200, resp.status_int)
self.assertEquals('text/html', resp.headers['Content-Type'])
self.assertEquals('', resp.body)
@@ -670,3 +670,22 @@ def test_bad_mf2_json_input_400s(self):
resp = app.application.get_response('/url?url=http://some/jf2&input=mf2-json')
self.assert_equals(400, resp.status_int)

def test_url_head(self):
self.expect_requests_get('http://my/posts.json', AS1)
self.mox.ReplayAll()

resp = app.application.get_response(
'/url?url=http://my/posts.json&input=as1&output=mf2-json', method='HEAD')
self.assert_equals(200, resp.status_int)
self.assert_equals('application/mf2+json', resp.headers['Content-Type'])
self.assert_equals('', resp.body)

def test_url_head_bad_output(self):
self.expect_requests_get('http://my/posts.json', AS1)
self.mox.ReplayAll()

resp = app.application.get_response(
'/url?url=http://my/posts.json&input=as1&output=foo', method='HEAD')
self.assert_equals(400, resp.status_int)
self.assert_equals('', resp.body)

0 comments on commit abf0a3c

Please sign in to comment.
You can’t perform that action at this time.