]> git.frykholm.com Git - friends.git/commitdiff
Some xml fixes, added rd library
authorMikael Frykholm <mikael@frykholm.com>
Fri, 1 Apr 2016 03:52:41 +0000 (05:52 +0200)
committerMikael Frykholm <mikael@frykholm.com>
Fri, 1 Apr 2016 03:52:41 +0000 (05:52 +0200)
friends/rd/__init__.py [new file with mode: 0644]
friends/rd/__pycache__/__init__.cpython-35.pyc [new file with mode: 0644]
friends/rd/__pycache__/core.cpython-35.pyc [new file with mode: 0644]
friends/rd/__pycache__/jrd.cpython-35.pyc [new file with mode: 0644]
friends/rd/core.py [new file with mode: 0644]
friends/rd/jrd.py [new file with mode: 0644]
friends/rd/xrd.py [new file with mode: 0755]
friends/server.py
friends/static/mikael.jpg [new file with mode: 0644]
friends/templates/feed.xml

diff --git a/friends/rd/__init__.py b/friends/rd/__init__.py
new file mode 100644 (file)
index 0000000..0386a2d
--- /dev/null
@@ -0,0 +1,6 @@
+from rd.core import *
+
+__author__ = "Jeremy Carbaugh (jcarbaugh@gmail.com)"
+__version__ = "0.1"
+__copyright__ = "Copyright (c) 2012 Jeremy Carbaugh"
+__license__ = "BSD"
diff --git a/friends/rd/__pycache__/__init__.cpython-35.pyc b/friends/rd/__pycache__/__init__.cpython-35.pyc
new file mode 100644 (file)
index 0000000..0b27d3f
Binary files /dev/null and b/friends/rd/__pycache__/__init__.cpython-35.pyc differ
diff --git a/friends/rd/__pycache__/core.cpython-35.pyc b/friends/rd/__pycache__/core.cpython-35.pyc
new file mode 100644 (file)
index 0000000..e417a55
Binary files /dev/null and b/friends/rd/__pycache__/core.cpython-35.pyc differ
diff --git a/friends/rd/__pycache__/jrd.cpython-35.pyc b/friends/rd/__pycache__/jrd.cpython-35.pyc
new file mode 100644 (file)
index 0000000..3421e21
Binary files /dev/null and b/friends/rd/__pycache__/jrd.cpython-35.pyc differ
diff --git a/friends/rd/core.py b/friends/rd/core.py
new file mode 100644 (file)
index 0000000..b017434
--- /dev/null
@@ -0,0 +1,390 @@
+import datetime
+import logging
+
+RFC6415_TYPE = 'application/xrd+xml'
+RFC7033_TYPE = 'application/jrd+json'
+
+JRD_TYPES = ('application/jrd+json', 'application/xrd+json', 'application/json', 'text/json')
+XRD_TYPES = ('application/xrd+xml', 'text/xml')
+
+KNOWN_RELS = {
+    'activity_streams': 'http://activitystrea.ms/spec/1.0',
+    'app': ('http://apinamespace.org/atom', 'application/atomsvc+xml'),
+    'avatar': 'http://webfinger.net/rel/avatar',
+    'foaf': ('describedby', 'application/rdf+xml'),
+    'hcard': 'http://microformats.org/profile/hcard',
+    'oauth_access_token':   'http://apinamespace.org/oauth/access_token',
+    'oauth_authorize':      'http://apinamespace.org/oauth/authorize',
+    'oauth_request_token':  'http://apinamespace.org/oauth/request_token',
+    'openid': 'http://specs.openid.net/auth/2.0/provider',
+    'opensocial': 'http://ns.opensocial.org/2008/opensocial/activitystreams',
+    'portable_contacts': 'http://portablecontacts.net/spec/1.0',
+    'profile': 'http://webfinger.net/rel/profile-page',
+    'updates_from': 'http://schemas.google.com/g/2010#updates-from',
+    'ostatus_sub': 'http://ostatus.org/schema/1.0/subscribe',
+    'salmon_endpoint': 'salmon',
+    'salmon_key': 'magic-public-key',
+    'webfist': 'http://webfist.org/spec/rel',
+    'xfn': 'http://gmpg.org/xfn/11',
+
+    'jrd':  ('lrdd', 'application/json'),
+    'webfinger': ('lrdd', 'application/jrd+json'),
+    'xrd': ('lrdd', 'application/xrd+xml'),
+}
+
+logger = logging.getLogger("rd")
+
+
+def _is_str(s):
+    try:
+        return isinstance(s, str)
+    except NameError:
+        return isinstance(s, str)
+
+
+def loads(content, content_type):
+
+    from rd import jrd, xrd
+
+    content_type = content_type.split(";")[0]
+
+    if content_type in JRD_TYPES:
+        logger.debug("loads() loading JRD")
+        return jrd.loads(content)
+
+    elif content_type in XRD_TYPES:
+        logger.debug("loads() loading XRD")
+        return xrd.loads(content)
+
+    raise TypeError('Unknown content type')
+
+#
+# helper functions for host parsing and discovery
+#
+
+def parse_uri_components(resource, default_scheme='https'):
+    hostname = None
+    scheme = default_scheme
+
+    from urllib.parse import urlparse
+
+    parts = urlparse(resource)
+    if parts.scheme and parts.netloc:
+        scheme = parts.scheme
+        ''' FIXME: if we have https://user@some.example/ we end up with parts.netloc='user@some.example' here. '''
+        hostname = parts.netloc
+        path = parts.path
+
+    elif parts.scheme == 'acct' or (not parts.scheme and '@' in parts.path):
+        ''' acct: means we expect WebFinger to work, and RFC7033 requires https, so host-meta should support it too. '''
+        scheme = 'https'
+
+        ''' We should just have user@site.example here, but if it instead
+            is user@site.example/whatever/else we have to split it later
+            on the first slash character, '/'.
+        '''
+        hostname = parts.path.split('@')[-1]
+        path = None
+
+        ''' In case we have hostname=='site.example/whatever/else' we do the split
+            on the first slash, giving us 'site.example' and 'whatever/else'.
+        '''
+        if '/' in hostname:
+            (hostname, path) = hostname.split('/', maxsplit=1)
+            ''' Normalize path so it always starts with /, which is the behaviour of urlparse too. '''
+            path = '/' + path
+
+    else:
+        if not parts.path:
+            raise ValueError('No hostname could be deduced from arguments.')
+
+        elif '/' in parts.path:
+            (hostname, path) = parts.path.split('/', maxsplit=1)
+            ''' Normalize path so it always starts with /, which is the behaviour of urlparse too. '''
+            path = '/' + path
+        else:
+            hostname = parts.path
+            path = None
+
+    return (scheme, hostname, path)
+
+#
+# special XRD types
+#
+
+class Attribute(object):
+
+    def __init__(self, name, value):
+        self.name = name
+        self.value = value
+
+    def __cmp__(self, other):
+        return cmp(str(self), str(other))
+
+    def __eq__(self, other):
+        return str(self) == other
+
+    def __str__(self):
+        return "%s=%s" % (self.name, self.value)
+
+
+class Element(object):
+
+    def __init__(self, name, value, attrs=None):
+        self.name = name
+        self.value = value
+        self.attrs = attrs or {}
+
+
+class Title(object):
+
+    def __init__(self, value, lang=None):
+        self.value = value
+        self.lang = lang
+
+    def __cmp__(self, other):
+        return cmp(str(self), str(other))
+
+    def __eq__(self, other):
+        return str(self) == str(other)
+
+    def __str__(self):
+        if self.lang:
+            return "%s:%s" % (self.lang, self.value)
+        return self.value
+
+
+class Property(object):
+
+    def __init__(self, type_, value=None):
+        self.type = type_
+        self.value = value
+
+    def __cmp__(self, other):
+        return cmp(str(self), str(other))
+
+    def __eq__(self, other):
+        return str(self) == other
+
+    def __str__(self):
+        if self.value:
+            return "%s:%s" % (self.type, self.value)
+        return self.type
+
+
+#
+# special list types
+#
+
+class ListLikeObject(list):
+
+    def __setitem__(self, key, value):
+        value = self.item(value)
+        super(ListLikeObject, self).__setitem__(key, value)
+
+    def append(self, value):
+        value = self.item(value)
+        super(ListLikeObject, self).append(value)
+
+    def extend(self, values):
+        values = (self.item(value) for value in values)
+        super(ListLikeObject, self).extend(values)
+
+
+class AttributeList(ListLikeObject):
+
+    def __call__(self, name):
+        for attr in self:
+            if attr.name == name:
+                yield attr
+
+    def item(self, value):
+        if isinstance(value, (list, tuple)):
+            return Attribute(*value)
+        elif not isinstance(value, Attribute):
+            raise ValueError('value must be an instance of Attribute')
+        return value
+
+
+class ElementList(ListLikeObject):
+
+    def item(self, value):
+        if not isinstance(value, Element):
+            raise ValueError('value must be an instance of Type')
+        return value
+
+
+class TitleList(ListLikeObject):
+
+    def item(self, value):
+        if _is_str(value):
+            return Title(value)
+        elif isinstance(value, (list, tuple)):
+            return Title(*value)
+        elif not isinstance(value, Title):
+            raise ValueError('value must be an instance of Title')
+        return value
+
+
+class LinkList(ListLikeObject):
+
+    def __call__(self, rel):
+        for link in self:
+            if link.rel == rel:
+                yield link
+
+    def item(self, value):
+        if not isinstance(value, Link):
+            raise ValueError('value must be an instance of Link')
+        return value
+
+
+class PropertyList(ListLikeObject):
+
+    def __call__(self, type_):
+        for prop in self:
+            if prop.type == type_:
+                yield prop
+
+    def item(self, value):
+        if _is_str(value):
+            return Property(value)
+        elif isinstance(value, (tuple, list)):
+            return Property(*value)
+        elif not isinstance(value, Property):
+            raise ValueError('value must be an instance of Property')
+        return value
+
+
+#
+# Link object
+#
+
+class Link(object):
+
+    def __init__(self, rel=None, type=None, href=None, template=None):
+        self.rel = rel
+        self.type = type
+        self.href = href
+        self.template = template
+        self._titles = TitleList()
+        self._properties = PropertyList()
+
+    def get_titles(self):
+        return self._titles
+    titles = property(get_titles)
+
+    def get_properties(self):
+        return self._properties
+    properties = property(get_properties)
+
+    def apply_template(self, uri):
+
+        from urllib.parse import quote
+
+        if not self.template:
+            raise TypeError('This is not a template Link')
+        return self.template.replace('{uri}', quote(uri, safe=''))
+
+    def __str__(self):
+
+        from cgi import escape
+
+        attrs = ''
+        for prop in ['rel', 'type', 'href', 'template']:
+            val = getattr(self, prop)
+            if val:
+                attrs += ' {!s}="{!s}"'.format(escape(prop), escape(val))
+
+        return '<Link{!s}/>'.format(attrs)
+
+
+#
+# main RD class
+#
+
+class RD(object):
+
+    def __init__(self, xml_id=None, subject=None):
+
+        self.xml_id = xml_id
+        self.subject = subject
+        self._expires = None
+        self._aliases = []
+        self._properties = PropertyList()
+        self._links = LinkList()
+        self._signatures = []
+
+        self._attributes = AttributeList()
+        self._elements = ElementList()
+
+    # ser/deser methods
+
+    def to_json(self):
+        from rd import jrd
+        return jrd.dumps(self)
+
+    def to_xml(self):
+        from rd import xrd
+        return xrd.dumps(self)
+
+    # helper methods
+
+    def find_link(self, rels, attr=None, mimetype=None):
+        if not isinstance(rels, (list, tuple)):
+            rels = (rels,)
+        for link in self.links:
+            if link.rel in rels:
+                if mimetype and link.type != mimetype:
+                    continue
+                if attr:
+                    return getattr(link, attr, None)
+                return link
+
+    def __getattr__(self, name, attr=None):
+        if name in KNOWN_RELS:
+            try:
+                ''' If we have a specific mimetype for this rel value '''
+                rel, mimetype = KNOWN_RELS[name]
+            except ValueError:
+                rel = KNOWN_RELS[name]
+                mimetype = None 
+            return self.find_link(rel, attr=attr, mimetype=mimetype)
+        raise AttributeError(name)
+
+    # custom elements and attributes
+
+    def get_elements(self):
+        return self._elements
+    elements = property(get_elements)
+
+    @property
+    def attributes(self):
+        return self._attributes
+
+    # defined elements and attributes
+
+    def get_expires(self):
+        return self._expires
+
+    def set_expires(self, expires):
+        if not isinstance(expires, datetime.datetime):
+            raise ValueError('expires must be a datetime object')
+        self._expires = expires
+    expires = property(get_expires, set_expires)
+
+    def get_aliases(self):
+        return self._aliases
+    aliases = property(get_aliases)
+
+    def get_properties(self):
+        return self._properties
+    properties = property(get_properties)
+
+    def get_links(self):
+        return self._links
+    links = property(get_links)
+
+    def get_signatures(self):
+        return self._signatures
+    signatures = property(get_links)
diff --git a/friends/rd/jrd.py b/friends/rd/jrd.py
new file mode 100644 (file)
index 0000000..4dda5b5
--- /dev/null
@@ -0,0 +1,146 @@
+
+import json
+import isodate
+
+from rd.core import RD, Attribute, Element, Link, Property, Title
+
+
+def _clean_dict(d):
+    for key in list(d.keys()):
+        if not d[key]:
+            del d[key]
+
+
+def loads(content):
+
+    def expires_handler(key, val, obj):
+        obj.expires = isodate.parse_datetime(val)
+
+    def subject_handler(key, val, obj):
+        obj.subject = val
+
+    def aliases_handler(key, val, obj):
+        for alias in val:
+            obj.aliases.append(alias)
+
+    def properties_handler(key, val, obj):
+        for ptype, pvalue in list(val.items()):
+            obj.properties.append(Property(ptype, pvalue))
+
+    def titles_handler(key, val, obj):
+        for tlang, tvalue in list(val.items()):
+            if tlang == 'default':
+                tlang = None
+            obj.titles.append(Title(tvalue, tlang))
+
+    def links_handler(key, val, obj):
+        for link in val:
+            l = Link()
+            l.rel = link.get('rel', None)
+            l.type = link.get('type', None)
+            l.href = link.get('href', None)
+            l.template = link.get('template', None)
+            if 'titles' in link:
+                titles_handler('title', link['titles'], l)
+            if 'properties' in link:
+                properties_handler('property', link['properties'], l)
+            obj.links.append(l)
+
+    def namespace_handler(key, val, obj):
+        for namespace in val:
+            ns = list(namespace.keys())[0]
+            ns_uri = list(namespace.values())[0]
+            obj.attributes.append(Attribute("xmlns:%s" % ns, ns_uri))
+
+    handlers = {
+        'expires': expires_handler,
+        'subject': subject_handler,
+        'aliases': aliases_handler,
+        'properties': properties_handler,
+        'links': links_handler,
+        'titles': titles_handler,
+        'namespace': namespace_handler,
+    }
+
+    def unknown_handler(key, val, obj):
+        if ':' in key:
+            (ns, name) = key.split(':')
+            key = "%s:%s" % (ns, name.capitalize())
+        obj.elements.append(Element(key, val))
+
+    doc = json.loads(content)
+
+    rd = RD()
+
+    for key, value in list(doc.items()):
+        handler = handlers.get(key, unknown_handler)
+        handler(key, value, rd)
+
+    return rd
+
+
+def dumps(xrd):
+
+    doc = {
+        "aliases": [],
+        "links": [],
+        "namespace": [],
+        "properties": {},
+        "titles": [],
+    }
+
+    #list_keys = doc.keys()
+
+    for attr in xrd.attributes:
+        if attr.name.startswith("xmlns:"):
+            ns = attr.name.split(":")[1]
+            doc['namespace'].append({ns: attr.value})
+
+    if xrd.expires:
+        doc['expires'] = xrd.expires.isoformat()
+
+    if xrd.subject:
+        doc['subject'] = xrd.subject
+
+    for alias in xrd.aliases:
+        doc['aliases'].append(alias)
+
+    for prop in xrd.properties:
+        doc['properties'][prop.type] = prop.value
+
+    for link in xrd.links:
+
+        link_doc = {
+            'titles': {},
+            'properties': {},
+        }
+
+        if link.rel:
+            link_doc['rel'] = link.rel
+
+        if link.type:
+            link_doc['type'] = link.type
+
+        if link.href:
+            link_doc['href'] = link.href
+
+        if link.template:
+            link_doc['template'] = link.template
+
+        for prop in link.properties:
+            link_doc['properties'][prop.type] = prop.value
+
+        for title in link.titles:
+            lang = title.lang or "default"
+            link_doc['titles'][lang] = title.value
+
+        _clean_dict(link_doc)
+
+        doc['links'].append(link_doc)
+
+    for elem in xrd.elements:
+        doc[elem.name.lower()] = elem.value
+
+    _clean_dict(doc)
+
+    return json.dumps(doc)
diff --git a/friends/rd/xrd.py b/friends/rd/xrd.py
new file mode 100755 (executable)
index 0000000..afc1247
--- /dev/null
@@ -0,0 +1,163 @@
+
+from xml.dom.minidom import getDOMImplementation, parseString, Node
+
+from rd.core import RD, Element, Link, Property, Title
+
+XRD_NAMESPACE = "http://docs.oasis-open.org/ns/xri/xrd-1.0"
+
+
+def _get_text(root):
+    text = ''
+    for node in root.childNodes:
+        if node.nodeType == Node.TEXT_NODE and node.nodeValue:
+            text += node.nodeValue
+        else:
+            text += _get_text(node)
+    return text.strip() or None
+
+
+def loads(content):
+
+    import isodate
+
+    def expires_handler(node, obj):
+        obj.expires = isodate.parse_datetime(_get_text(node))
+
+    def subject_handler(node, obj):
+        obj.subject = _get_text(node)
+
+    def alias_handler(node, obj):
+        obj.aliases.append(_get_text(node))
+
+    def property_handler(node, obj):
+        obj.properties.append(Property(node.getAttribute('type'), _get_text(node)))
+
+    def title_handler(node, obj):
+        obj.titles.append(Title(_get_text(node), node.getAttribute('xml:lang')))
+
+    def link_handler(node, obj):
+        l = Link()
+        l.rel = node.getAttribute('rel')
+        l.type = node.getAttribute('type')
+        l.href = node.getAttribute('href')
+        l.template = node.getAttribute('template')
+        obj.links.append(l)
+
+    handlers = {
+        'Expires': expires_handler,
+        'Subject': subject_handler,
+        'Alias': alias_handler,
+        'Property': property_handler,
+        'Link': link_handler,
+        'Title': title_handler,
+    }
+
+    def unknown_handler(node, obj):
+        obj.elements.append(Element(
+            name=node.tagName,
+            value=_get_text(node),
+        ))
+
+    def handle_node(node, obj):
+        handler = handlers.get(node.nodeName, unknown_handler)
+        if handler and node.nodeType == node.ELEMENT_NODE:
+            handler(node, obj)
+
+    doc = parseString(content)
+    root = doc.documentElement
+
+    rd = RD(root.getAttribute('xml:id'))
+
+    for name, value in list(root.attributes.items()):
+        if name != 'xml:id':
+            rd.attributes.append((name, value))
+
+    for node in root.childNodes:
+        handle_node(node, rd)
+        if node.nodeName == 'Link':
+            link = rd.links[-1]
+            for child in node.childNodes:
+                handle_node(child, link)
+
+    return rd
+
+
+def dumps(xrd):
+
+    doc = getDOMImplementation().createDocument(XRD_NAMESPACE, "XRD", None)
+    root = doc.documentElement
+    root.setAttribute('xmlns', XRD_NAMESPACE)
+
+    if xrd.xml_id:
+        root.setAttribute('xml:id', xrd.xml_id)
+
+    for attr in xrd.attributes:
+        root.setAttribute(attr.name, attr.value)
+
+    if xrd.expires:
+        node = doc.createElement('Expires')
+        node.appendChild(doc.createTextNode(xrd.expires.isoformat()))
+        root.appendChild(node)
+
+    if xrd.subject:
+        node = doc.createElement('Subject')
+        node.appendChild(doc.createTextNode(xrd.subject))
+        root.appendChild(node)
+
+    for alias in xrd.aliases:
+        node = doc.createElement('Alias')
+        node.appendChild(doc.createTextNode(alias))
+        root.appendChild(node)
+
+    for prop in xrd.properties:
+        node = doc.createElement('Property')
+        node.setAttribute('type', prop.type)
+        if prop.value:
+            node.appendChild(doc.createTextNode(str(prop.value)))
+        else:
+            node.setAttribute('xsi:nil', 'true')
+        root.appendChild(node)
+
+    for element in xrd.elements:
+        node = doc.createElement(element.name)
+        node.appendChild(doc.createTextNode(element.value))
+        root.appendChild(node)
+
+    for link in xrd.links:
+
+        if link.href and link.template:
+            raise ValueError('only one of href or template attributes may be specified')
+
+        link_node = doc.createElement('Link')
+
+        if link.rel:
+            link_node.setAttribute('rel', link.rel)
+
+        if link.type:
+            link_node.setAttribute('type', link.type)
+
+        if link.href:
+            link_node.setAttribute('href', link.href)
+
+        if link.template:
+            link_node.setAttribute('template', link.template)
+
+        for title in link.titles:
+            node = doc.createElement('Title')
+            node.appendChild(doc.createTextNode(str(title)))
+            if title.lang:
+                node.setAttribute('xml:lang', title.lang)
+            link_node.appendChild(node)
+
+        for prop in link.properties:
+            node = doc.createElement('Property')
+            node.setAttribute('type', prop.type)
+            if prop.value:
+                node.appendChild(doc.createTextNode(str(prop.value)))
+            else:
+                node.setAttribute('xsi:nil', 'true')
+            link_node.appendChild(node)
+
+        root.appendChild(link_node)
+
+    return doc
index ca64779b6cb91abcc1909f810b924685ac12676f..750ddc826a34d032fae38a1a3d6dee75fbdde0e1 100644 (file)
@@ -30,6 +30,7 @@ class PushHandler(tornado.web.RequestHandler):
         hub_lease_seconds = self.get_argument('hub.lease_seconds','')
         hub_secret = self.get_argument('hub.sercret','')
         hub_verify_token = self.get_argument('hub.verify_token','')
         hub_lease_seconds = self.get_argument('hub.lease_seconds','')
         hub_secret = self.get_argument('hub.sercret','')
         hub_verify_token = self.get_argument('hub.verify_token','')
+        print(self.request.body)
         if hub_mode == 'unsubscribe':
             pass #FIXME
         path = hub_topic.split(self.settings['domain'])[1]
         if hub_mode == 'unsubscribe':
             pass #FIXME
         path = hub_topic.split(self.settings['domain'])[1]
@@ -39,6 +40,8 @@ class PushHandler(tornado.web.RequestHandler):
             db.execute("INSERT into subscriptions (userid, expires, callback, verified) values (?,?,?,?)",(row['id'],datetime.datetime.now(),hub_callback,False))
             db.commit()
             self.set_status(202)
             db.execute("INSERT into subscriptions (userid, expires, callback, verified) values (?,?,?,?)",(row['id'],datetime.datetime.now(),hub_callback,False))
             db.commit()
             self.set_status(202)
+            #TODO add GET callback with the same data we got
+            #TODO store secret, add it to outgoing feeds with hmac
 class XrdHandler(tornado.web.RequestHandler):
     def get(self):
         self.render("templates/xrd.xml", hostname="ronin.frykholm.com", url=self.settings['domain'])
 class XrdHandler(tornado.web.RequestHandler):
     def get(self):
         self.render("templates/xrd.xml", hostname="ronin.frykholm.com", url=self.settings['domain'])
diff --git a/friends/static/mikael.jpg b/friends/static/mikael.jpg
new file mode 100644 (file)
index 0000000..ace1db2
Binary files /dev/null and b/friends/static/mikael.jpg differ
index 4308d1e0ca8c85cad50247921a5a3da9ab056ddf..320eb65d6ba20512c635b8986d3437ece800d9bd 100644 (file)
@@ -1,9 +1,10 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
-<feed xmlns:activity='http://activitystrea.ms/spec/1.0/' xmlns='http://www.w3.org/2005/Atom'><link rel='hub' href='{{hub_url}}'/><link rel='self' href='{{user}}'/>
+<feed xmlns:activity='http://activitystrea.ms/spec/1.0/' xmlns='http://www.w3.org/2005/Atom' xmlns:poco="http://portablecontacts.net/spec/1.0"><link rel='hub' href='{{hub_url}}'/><link rel='self' href='{{feed_url}}' type="application/atom+xml"/>
   <author>
     <uri>{{feed_url}}</uri>
     <name>{{user}}</name>
     <object-type xmlns='http://activitystrea.ms/spec/1.0/'>http://activitystrea.ms/schema/1.0/person</object-type>
   <author>
     <uri>{{feed_url}}</uri>
     <name>{{user}}</name>
     <object-type xmlns='http://activitystrea.ms/spec/1.0/'>http://activitystrea.ms/schema/1.0/person</object-type>
+    <poco:preferredUsername>{{user}}</poco:preferredUsername>
   </author>
   <title>{{user}}'s tidslinje</title>
   <id>{{feed_url}}</id>
   </author>
   <title>{{user}}'s tidslinje</title>
   <id>{{feed_url}}</id>