# HG changeset patch # User souliane # Date 1411417544 -7200 # Node ID 16484ebb695bb1364753ef7818dbd0707e3deb98 # Parent 3b1c5f723c4b469ca73beda4ad58dae09fe3ca82 plugin XEP-0059: first draft, pubsub and jabber search do not exploit it yet diff -r 3b1c5f723c4b -r 16484ebb695b src/plugins/plugin_xep_0055.py --- a/src/plugins/plugin_xep_0055.py Mon Sep 22 20:50:20 2014 +0200 +++ b/src/plugins/plugin_xep_0055.py Mon Sep 22 22:25:44 2014 +0200 @@ -35,6 +35,8 @@ "import_name": "XEP-0055", "type": "XEP", "protocols": ["XEP-0055"], + "dependencies": [], + "recommendations": ["XEP-0059"], "main": "XEP_0055", "handler": "no", "description": _("""Implementation of Jabber Search""") @@ -156,6 +158,7 @@ x_form = data_form.Form('submit', formNamespace = NS_SEARCH) x_form.makeFields(search_dict) query_elt.addChild(x_form.toElement()) + # TODO: XEP-0059 could be used here (with the needed new method attributes) d = search_request.send(to_jid.full()) d.addCallbacks(self._searchOk, self._searchErr, callbackArgs=[client.profile], errbackArgs=[client.profile]) return d diff -r 3b1c5f723c4b -r 16484ebb695b src/plugins/plugin_xep_0059.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/plugins/plugin_xep_0059.py Mon Sep 22 22:25:44 2014 +0200 @@ -0,0 +1,149 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# SAT plugin for Result Set Management (XEP-0059) +# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org) +# Copyright (C) 2013, 2014 Adrien Cossa (souliane@mailoo.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from sat.core.i18n import _ +from sat.core.log import getLogger +log = getLogger(__name__) + +from twisted.words.xish import domish +from wokkel import disco, iwokkel +try: + from twisted.words.protocols.xmlstream import XMPPHandler +except ImportError: + from wokkel.subprotocols import XMPPHandler +from zope.interface import implements + + +NS_RSM = 'http://jabber.org/protocol/rsm' + +PLUGIN_INFO = { + "name": "Result Set Management", + "import_name": "XEP-0059", + "type": "XEP", + "protocols": ["XEP-0059"], + "main": "XEP_0059", + "handler": "no", + "description": _("""Implementation of Result Set Management""") +} + + +class XEP_0059(object): + + def __init__(self, host): + log.info(_("Result Set Management plugin initialization")) + self.host = host + + def requestPage(self, stanza, limit=10, index=None, after=None, before=None): + """Embed a RSM page request in the given stanza. + + @param stanza (domish.Element): any stanza to which RSM applies + @param limit (int): the maximum number of items in the page + @param index (int): the starting index of the requested page + @param after (str, int): the element immediately preceding the page + @param before (str, int): the element immediately following the page + """ + main_elt = None + try: + main_elt = domish.generateElementsNamed(stanza.elements(), name="query").next() + except StopIteration: + try: + main_elt = domish.generateElementsNamed(stanza.elements(), name="pubsub").next() + except StopIteration: + log.warning("Injection of a RSM element only applies to query or pubsub stanzas") + return + limit = str(int(limit)) + + # in case the service doesn't support RSM, do this at least + main_elt.items.attributes['max_items'] = limit + + set_elt = main_elt.addElement('set', NS_RSM) + set_elt.addElement('max').addContent(limit) + if index: + assert(after is None and before is None) + set_elt.addElement('index').addContent(str(int(index))) + if after: + assert(before is None) # could not specify both at the same time + set_elt.addElement('after').addContent(str(after)) + if before is not None: + if before == '': # request the last page, according to http://xmpp.org/extensions/xep-0059.html#last + set_elt.addElement('before') + else: + set_elt.addElement('before').addContent(str(before)) + + def countItems(self, stanza): + """Count the items without retrieving any of them. + + @param stanza (domish.Element): any stanza to which RSM applies + """ + self.requestPage(stanza, limit=0) + + def extractMetadata(self, stanza): + """Extract the RSM metadata from the given stanza. + + @param stanza (domish.Element, wokkel.pubsub.PubSubRequest): + any stanza to which RSM applies. When used by XEP-0060, + wokkel's PubSubRequest instance is also accepted. + @return: dict containing the page metadata + """ + try: + main_elt = domish.generateElementsNamed(stanza.elements(), name="query").next() + except StopIteration: + try: + main_elt = domish.generateElementsNamed(stanza.elements(), name="pubsub").next() + except StopIteration: + log.warning("Extracting data from a RSM element only applies to query or pubsub stanzas") + return {} + try: + set_elt = domish.generateElementsQNamed(main_elt.elements(), name="set", uri=NS_RSM).next() + except StopIteration: + log.debug("There's no RSM element in the stanza") + return {} + + data = {} + elts = set_elt.elements() + try: + elt = elts.next() + if elt.name == "first": + data["first"] = "".join(elt.children) + data["first_index"] = int(elt.getAttribute("index")) + elif elt.name == "last": + data["last"] = "".join(elt.children) + elif elt.name == "count": + data["count"] = int("".join(elt.children)) + except StopIteration: + pass + if "count" not in data: + log.warning("There's no 'count' element in the RSM element!") + return data + + +class XEP_0059_handler(XMPPHandler): + implements(iwokkel.IDisco) + + def __init__(self, plugin_parent, profile): + self.plugin_parent = plugin_parent + self.host = plugin_parent.host + self.profile = profile + + def getDiscoInfo(self, requestor, target, nodeIdentifier=''): + return [disco.DiscoFeature(NS_RSM)] + + def getDiscoItems(self, requestor, target, nodeIdentifier=''): + return [] diff -r 3b1c5f723c4b -r 16484ebb695b src/plugins/plugin_xep_0060.py --- a/src/plugins/plugin_xep_0060.py Mon Sep 22 20:50:20 2014 +0200 +++ b/src/plugins/plugin_xep_0060.py Mon Sep 22 22:25:44 2014 +0200 @@ -23,6 +23,7 @@ log = getLogger(__name__) from sat.memory.memory import Sessions +from wokkel.compat import IQ from wokkel import disco, pubsub from wokkel.pubsub import PubSubRequest, NS_PUBSUB from zope.interface import implements @@ -35,6 +36,7 @@ "type": "XEP", "protocols": ["XEP-0060"], "dependencies": [], + "recommendations": ["XEP-0059"], "main": "XEP_0060", "handler": "yes", "description": _("""Implementation of PubSub Protocol""") @@ -232,7 +234,7 @@ # FIXME: we have to temporary override this method here just # to set the attributes itemIdentifiers which is not used - # in pubsub.PubSubClient.items + # in pubsub.PubSubClient.items + use the XEP-0059 def items(self, service, nodeIdentifier, maxItems=None, itemIdentifiers=None, subscriptionIdentifier=None, sender=None): """ @@ -255,7 +257,8 @@ the results to the specific subscription. @type subscriptionIdentifier: C{unicode} """ - request = PubSubRequest('items') + # TODO: add method attributes for RSM: before, after, index + request = PubSubRequest('items', self.host, {'limit': maxItems} if maxItems else {}) request.recipient = service request.nodeIdentifier = nodeIdentifier if maxItems: @@ -269,6 +272,7 @@ for element in iq.pubsub.items.elements(): if element.uri == NS_PUBSUB and element.name == 'item': items.append(element) + # TODO: return (items, self.host.plugins['XEP-0059'].extractMetadata(iq)) ?? return items d = request.send(self.xmlstream) @@ -331,3 +335,64 @@ def getDiscoItems(self, requestor, service, nodeIdentifier=''): return self.host.getDiscoItems(service, nodeIdentifier, self.parent.profile) + + +class PubSubRequest(pubsub.PubSubRequest): + + def __init__(self, verb=None, host=None, page_attrs=None): + """ + @param verb (str): the type of pubsub request + @param host (SAT): the SAT instance + @param page_attrs (dict): options for RSM paging: + - limit (int): the maximum number of items in the page + - index (int): the starting index of the requested page + - after (str, int): the element immediately preceding the page + - before (str, int): the element immediately following the page + """ + self.verb = verb + self.host = host + self.page_attrs = page_attrs + + # FIXME: the redefinition of this wokkel method is the easiest way I found + # to handle RSM. We should find a proper solution, maybe just add in wokkel an + # empty method postProcessMessage, call it before sending and overwrite it here + # instead of overwriting the whole send method. + def send(self, xs): + """ + Send this request to its recipient. + + This renders all of the relevant parameters for this specific + requests into an L{IQ}, and invoke its C{send} method. + This returns a deferred that fires upon reception of a response. See + L{IQ} for details. + + @param xs: The XML stream to send the request on. + @type xs: L{twisted.words.protocols.jabber.xmlstream.XmlStream} + @rtype: L{defer.Deferred}. + """ + + try: + (self.stanzaType, + childURI, + childName) = self._verbRequestMap[self.verb] + except KeyError: + raise NotImplementedError() + + iq = IQ(xs, self.stanzaType) + iq.addElement((childURI, 'pubsub')) + verbElement = iq.pubsub.addElement(childName) + + if self.sender: + iq['from'] = self.sender.full() + if self.recipient: + iq['to'] = self.recipient.full() + + for parameter in self._parameters[self.verb]: + getattr(self, '_render_%s' % parameter)(verbElement) + + # This lines have been added for RSM + if self.host and 'XEP-0059' in self.host.plugins and self.page_attrs: + self.page_attrs['stanza'] = iq + self.host.plugins['XEP-0059'].requestPage(**self.page_attrs) + + return iq.send()