From 511b6ecb108d9a00340f1e58b849c3fcdb4536c9 Mon Sep 17 00:00:00 2001 From: Mikael Frykholm Date: Fri, 1 Apr 2016 05:52:41 +0200 Subject: [PATCH] Some xml fixes, added rd library --- friends/rd/__init__.py | 6 + .../rd/__pycache__/__init__.cpython-35.pyc | Bin 0 -> 326 bytes friends/rd/__pycache__/core.cpython-35.pyc | Bin 0 -> 12786 bytes friends/rd/__pycache__/jrd.cpython-35.pyc | Bin 0 -> 4036 bytes friends/rd/core.py | 390 ++++++++++++++++++ friends/rd/jrd.py | 146 +++++++ friends/rd/xrd.py | 163 ++++++++ friends/server.py | 3 + friends/static/mikael.jpg | Bin 0 -> 23571 bytes friends/templates/feed.xml | 3 +- 10 files changed, 710 insertions(+), 1 deletion(-) create mode 100644 friends/rd/__init__.py create mode 100644 friends/rd/__pycache__/__init__.cpython-35.pyc create mode 100644 friends/rd/__pycache__/core.cpython-35.pyc create mode 100644 friends/rd/__pycache__/jrd.cpython-35.pyc create mode 100644 friends/rd/core.py create mode 100644 friends/rd/jrd.py create mode 100755 friends/rd/xrd.py create mode 100644 friends/static/mikael.jpg diff --git a/friends/rd/__init__.py b/friends/rd/__init__.py new file mode 100644 index 0000000..0386a2d --- /dev/null +++ b/friends/rd/__init__.py @@ -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 index 0000000000000000000000000000000000000000..0b27d3fdbb7fe1cc1201c67aaaa02a2389126280 GIT binary patch literal 326 zcmZ8by-ve05I!gAkAku?M@XQCA#OV(R8b}dmLdi(r(m1J5XY61ASKVj3-pyDHYQ#H z7R+Ai5F=m# zh%s!E$(!4Sgbd`gKfaPy>MmYq_AYDlBAz|)U%JY5rplBXo%H@ZUGzt5v+b>;Ume+FF^)H4!5J)BBl~ip#2&h&!-> z`|umyDJ_haRIA5KmbA8<-diOrQSYl26j4>GK}@&ZFg?>ow52>JPNSYW2)!}+@=qz; BS^59~ literal 0 HcmV?d00001 diff --git a/friends/rd/__pycache__/core.cpython-35.pyc b/friends/rd/__pycache__/core.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e417a5516ee60c94b3ef5a4608d2edbd925cc229 GIT binary patch literal 12786 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 BqHF*F literal 0 HcmV?d00001 diff --git a/friends/rd/__pycache__/jrd.cpython-35.pyc b/friends/rd/__pycache__/jrd.cpython-35.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3421e215bde702663d2dea3e10eaaffc293b859a GIT binary patch literal 4036 zcmZ`+&2uA16@NY7k|o>g5AAwMcC!g4C@E}~0s_HF<--)If>Kg(3Rj6Nu361^cjVEB zyT`1ol1@vJgg<~2R}N6zsDcAGioc@amP0C6PO0U>@AZtvT4}L7t)4fp-@N|4_kR7< zsMo9i{{4S{{)<(j&uQ+@!u%~G@N15ml`rv5YBfD=eR!xVZ)cfI z?q6iF_J5qly||xg=VwX(Oa~vx!7!HDrFK6`vNY}pUWY$+-p2JVF7+J@qgN>-nhmkM_x(l(}lBw2&aC$H>3 z9Q5M-UUC-3>AsSk{ZpC5{YaJHGTQITXm5C_S57)<9QIG5q>}~gm4|`Ej;IO==&5wQ zjvE%R`|1e>AQD-O4!kW9oLW~1P66u-!Ox%%aI38UnV?UIJ`r>ZWguIl-@u4NQJtby zV-PV|@9m;H)1k`m(toP_+i>6hYuF%Azu1k|DO?-L`VT~sE$1NUJ=&?=W{GW7;=~44W ztHUtxAAfUHk8r65hT`oACw#Ez1pXIMv$db7K@?_jdvzE}6`$}tOM1wkcr;98teUo5 zfemf{DonLKxZgDa%?A#EUMmz&9)|rWjpaJOmfufed@CJ*2c0lgZ|*IqKHFus z3Z&UnQ$i-cg!a|N{chaJ3Wwx1sL2f;IK$>0Dimi9ZQ}9&b7(=ebjZOP#?fH7i^=E~ z`*e7(LC7Jv*3BboGp6F)pDzSIOp{Q>N_*jOh#YS^2Ei+bwQDq%{4Unb{N~Upc7#NI z8OjCa(sLL3d>PN*!esPAQ_o_)o_9@-*_`6M7yR3lVUDdK5s{s|sk!QQB^RQ0lPvBj zT^;6JlEg}0hg@=jHr=IN4ztT)-1dgZoQrsIw;NYRW*fc+*#It_MSh4 zmC;X^_x#%2o>9Ak&McC5xfF_smM9n?7E!QT+K=MX@FLByRfa>dzuL=~P75G>NS1(T zyIC6cAGE#fnLw=O24*1Kf(gauN!%7S;fe9{3qU-rzXXCw_kUph=qqggcx~U@35J z@%&GQF$445FQ0P`g6g0J{rP&Tw6rbbR6CqW+IcABQym}&hbd@E-o_@iYv7Py<9Sfz z$YnE-Tm#yE5NGlZX7WWIUgF{F{J>3dDCR+1iWi?`I59=qwB+-=;sqWI$r!?`o3*oe zu{p!CT%{GUU3_X{3(?yWpbv zAu?ylOo~95G{$+7AucAq4ID|cY@QL7-ICk@jDt}x?W@;!RnyCJx&o>8OroOD!&y5~ zCuE;ev@aM3~~=ci7Dgh;4)135hlP5M=+=9YdiskLjvSJ#?flHknu7Un72M? z1{za<3`9$AFh^5?CQ9I_X}6w;*T?Rz0nyd28m396tDSI|WZ>v=tb;i3#0m`ArKOyc z`;cmf_q&MO+C~4YKX}xiq4qX33)E^zHlVh6$}TKGZ|HQ@ppT^I(^&G^d_`dLW%j48 z{m!7DpklpM9xwmFjtlMHkY*}VqXV$TD_EmAci%aceH;Cc`jFuT~ zGdh$&;_^Ikxg8uG6(K*o_a31o~0^&lmg(3@3WJm_gY}!FRab{o< z+B=gp;(QOS7qSTH^gCnQFc>P7msIG0p9LiUY@UJfAg$8D=tXm1$(w3eg&5_`_6zR& zD;g@ie3`ZS}t8BdCy2!jkKOi*GMWOMGFkwRcr$u!pNeBcsS?bR2+}) z8ohOR_zmuSxz)W!!@mi1)98Qs7Td!zJ}dy*`5(k_w!spFKrlE~4o)sOw_);Sv*}Hk ze2)8M^uufewT%fvy2S_G!TDFYp*&XEHgy}U1FJ{P^;S+EhBufpavEU<8GAV)qA?)) zk~40OaqjT#(+7#<mglw=Pj|D4J@q&IlRow2T<;XWP6UYIS? zn6YepGB0|Gm+r~m)} literal 0 HcmV?d00001 diff --git a/friends/rd/core.py b/friends/rd/core.py new file mode 100644 index 0000000..b017434 --- /dev/null +++ b/friends/rd/core.py @@ -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 ''.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 index 0000000..4dda5b5 --- /dev/null +++ b/friends/rd/jrd.py @@ -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 index 0000000..afc1247 --- /dev/null +++ b/friends/rd/xrd.py @@ -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 diff --git a/friends/server.py b/friends/server.py index ca64779..750ddc8 100644 --- a/friends/server.py +++ b/friends/server.py @@ -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','') + print(self.request.body) 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) + #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']) diff --git a/friends/static/mikael.jpg b/friends/static/mikael.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ace1db236d78d03733bb0fe10a04e03264496d8a GIT binary patch literal 23571 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 - + {{feed_url}} {{user}} http://activitystrea.ms/schema/1.0/person + {{user}} {{user}}'s tidslinje {{feed_url}} -- 2.39.2