Skip to content

Commit

Permalink
extract atom rendering out into its own module
Browse files Browse the repository at this point in the history
  • Loading branch information
snarfed committed Oct 5, 2014
1 parent c4df093 commit c123883
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 49 deletions.
56 changes: 13 additions & 43 deletions activitystreams.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@
import json
import logging
import re
import os
import urllib
from webob import exc

import appengine_config
import atom
import facebook
import instagram
import microformats2
Expand All @@ -43,7 +43,6 @@
import source
import twitter

from google.appengine.ext.webapp import template
import webapp2

# maps app id to source class
Expand All @@ -57,7 +56,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<response>%s</response>
"""
ATOM_TEMPLATE_FILE = os.path.join(os.path.dirname(__file__), 'templates', 'user_feed.atom')
ITEMS_PER_PAGE = 100

# default values for each part of the API request path, e.g. /@me/@self/@all/...
Expand Down Expand Up @@ -120,37 +118,25 @@ def get(self):
response = source.get_activities_response(*args, **paging_params)
activities = response['items']

if format == 'atom':
# strip the access token from the request URL before returning
params = dict(self.request.GET.items())
for key in 'access_token', 'access_token_key', 'access_token_secret':
if key in params:
del params[key]
request_url = '%s?%s' % (self.request.path_url, urllib.urlencode(params))
actor = source.get_actor(user_id)
response.update({
'host_url': self.request.host_url + "/",
'request_url': request_url,
'title': 'User feed for ' + actor.get('displayName', 'unknown'),
'updated': activities[0]['object'].get('published') if activities else '',
'actor': actor,
})
# TODO: render activity contents as html. i'd need to allow markup
# through, though, which iirc has broken before when posts have included
# HTML entities.
# for activity in response.get('items', []):
# obj = activity['object']
# obj['content'] = microformats2.render_content(obj)

# encode and write response
self.response.headers['Access-Control-Allow-Origin'] = '*'
if format == 'json':
self.response.headers['Content-Type'] = 'application/json'
self.response.out.write(json.dumps(response, indent=2))
elif format == 'atom':
# TODO: render activity contents as html. i'd need to allow markup
# through, though, which iirc has broken before when posts have included
# HTML entities.
# for activity in response.get('items', []):
# obj = activity['object']
# obj['content'] = microformats2.render_content(obj)
actor = source.get_actor(user_id)
request_url = '%s?%s' % (self.request.path_url,
urllib.urlencode(dict(self.request.GET.items())))
self.response.headers['Content-Type'] = 'text/xml'
self.preprocess_for_atom(response)
self.response.out.write(template.render(ATOM_TEMPLATE_FILE, response))
self.response.out.write(atom.activities_to_atom(
activities, actor, host_url=self.request.host_url + '/',
request_url=request_url))
elif format == 'xml':
self.response.headers['Content-Type'] = 'text/xml'
self.response.out.write(XML_TEMPLATE % util.to_xml(response))
Expand Down Expand Up @@ -201,22 +187,6 @@ def get_positive_int(self, param):
raise exc.HTTPBadRequest('Invalid %s: %s (should be positive int)' %
(param, val))

def preprocess_for_atom(self, response):
"""Prepares data to be rendered as Atom.
Right now, just populates each activity's title field, since Atom <entry>
requires the title element.
Args:
response: ActivityStreams response dict
"""
for activity in response.get('items', []):
if not activity.get('title'):
obj = activity.get('object', {})
activity['title'] = util.ellipsize(
activity.get('displayName') or activity.get('content') or
obj.get('title') or obj.get('displayName') or obj.get('content'))


application = webapp2.WSGIApplication([('.*', Handler)],
debug=appengine_config.DEBUG)
7 changes: 4 additions & 3 deletions activitystreams_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,10 @@ def test_atom_format(self):
# include access_token param to check that it gets stripped
resp = self.get_response('?format=atom&access_token=foo&a=b')
self.assertEquals(200, resp.status_int)
request_url = 'http://localhost?a=b&format=atom'
self.assert_multiline_equals(test_module.ATOM % {'request_url': request_url},
resp.body)
self.assert_multiline_equals(
test_module.ATOM % {'request_url': 'http://localhost',
'host_url': 'http://localhost/'},
resp.body)

def test_unknown_format(self):
resp = activitystreams.application.get_response('?format=bad')
Expand Down
56 changes: 56 additions & 0 deletions atom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Convert ActivityStreams to Atom.
Atom spec: http://atomenabled.org/developers/syndication/
"""

import logging
import os
import string
import urlparse

from google.appengine.ext.webapp import template
from oauth_dropins.webutil import util

ATOM_TEMPLATE_FILE = os.path.join(os.path.dirname(__file__), 'templates', 'user_feed.atom')


def activities_to_atom(activities, actor, request_url=None, host_url=None):
"""Converts ActivityStreams activites to an Atom feed.
Args:
activities: list of ActivityStreams activity dicts
actor: ActivityStreams actor dict, the author of the feed
request_url: the URL of this Atom feed, if any. Used in a link rel="self".
host_url: the home URL for this Atom feed, if any. Used in the top-level
feed <id> element.
Returns: unicode string with Atom XML
"""
# Strip query params from URLs so that we don't include access tokens, etc
host_url = (_remove_query_params(host_url) if host_url
else 'https://github.com/snarfed/activitystreams-unofficial')
request_url = _remove_query_params(request_url) if request_url else host_url

# Make sure every activity has the title field, since Atom <entry> requires
# the title element.
for a in activities:
if not a.get('title'):
obj = a.get('object', {})
a['title'] = util.ellipsize(
a.get('displayName') or a.get('content') or
obj.get('title') or obj.get('displayName') or obj.get('content'))

return template.render(ATOM_TEMPLATE_FILE, {
'items': activities,
'host_url': host_url,
'request_url': request_url,
'title': 'User feed for ' + actor.get('displayName', 'unknown'),
'updated': activities[0]['object'].get('published') if activities else '',
'actor': actor,
})


def _remove_query_params(url):
parsed = list(urlparse.urlparse(url))
parsed[4] = ''
return urlparse.urlunparse(parsed)
26 changes: 26 additions & 0 deletions atom_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Unit tests for atom.py."""

import copy

import atom
import facebook_test
import instagram_test
from oauth_dropins.webutil import testutil
import twitter_test


class AtomTest(testutil.HandlerTest):

def test_activities_to_atom(self):
for test_module in facebook_test, instagram_test, twitter_test:
self.assert_multiline_equals(
test_module.ATOM % {'request_url': 'http://request/url',
'host_url': 'http://host/url',
},
atom.activities_to_atom(
[copy.deepcopy(test_module.ACTIVITY)],
test_module.ACTOR,
request_url='http://request/url?access_token=foo',
host_url='http://host/url',
))

2 changes: 1 addition & 1 deletion facebook_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ def tag_uri(name):
>
<generator uri="https://github.com/snarfed/activitystreams-unofficial" version="0.1">
activitystreams-unofficial</generator>
<id>http://localhost/</id>
<id>%(host_url)s</id>
<title>User feed for Ryan Barrett</title>
<subtitle>something about me</subtitle>
Expand Down
2 changes: 1 addition & 1 deletion instagram_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ def tag_uri(name):
>
<generator uri="https://github.com/snarfed/activitystreams-unofficial" version="0.1">
activitystreams-unofficial</generator>
<id>http://localhost/</id>
<id>%(host_url)s</id>
<title>User feed for Ryan B</title>
<subtitle>foo</subtitle>
Expand Down
2 changes: 1 addition & 1 deletion twitter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ def tag_uri(name):
>
<generator uri="https://github.com/snarfed/activitystreams-unofficial" version="0.1">
activitystreams-unofficial</generator>
<id>http://localhost/</id>
<id>%(host_url)s</id>
<title>User feed for Ryan Barrett</title>
<subtitle>my description</subtitle>
Expand Down

0 comments on commit c123883

Please sign in to comment.