diff wokkel/rsm.py @ 0:09e7c32a6a00

use sat.tmp.wokkel as a buffer module until the changes are integrated to wokkel
author souliane <souliane@mailoo.org>
date Mon, 15 Dec 2014 12:46:58 +0100
parents
children 9d35f88168a1
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wokkel/rsm.py	Mon Dec 15 12:46:58 2014 +0100
@@ -0,0 +1,349 @@
+# -*- test-case-name: wokkel.test.test_rsm -*-
+#
+# Copyright (c) Adrien Cossa.
+# See LICENSE for details.
+
+"""
+XMPP Result Set Management protocol.
+
+This protocol is specified in
+U{XEP-0059<http://xmpp.org/extensions/xep-0059.html>}.
+"""
+
+from twisted.python import log
+from twisted.words.xish import domish
+
+import pubsub
+import copy
+
+
+# RSM namespace
+NS_RSM = 'http://jabber.org/protocol/rsm'
+
+
+class RSMRequest():
+    """
+    A Result Set Management request.
+
+    @ivar max: limit on the number of retrieved items.
+    @itype max: C{int} or C{unicode}
+
+    @ivar index: starting index of the requested page.
+    @itype index: C{int} or C{unicode}
+
+    @ivar after: ID of the element immediately preceding the page.
+    @itype after: C{unicode}
+
+    @ivar before: ID of the element immediately following the page.
+    @itype before: C{unicode}
+    """
+
+    max = 10
+    index = None
+    after = None
+    before = None
+
+    def __init__(self, max=None, index=None, after=None, before=None):
+        if max is not None:
+            max = int(max)
+            assert(max >= 0)
+            self.max = max
+
+        if index is not None:
+            assert(after is None and before is None)
+            index = int(index)
+            assert(index >= 0)
+            self.index = index
+
+        if after is not None:
+            assert(before is None)
+            assert(isinstance(after, unicode))
+            self.after = after
+
+        if before is not None:
+            assert(isinstance(before, unicode))
+            self.before = before
+
+    @classmethod
+    def parse(cls, element):
+        """Parse the given request element.
+
+        @param element: request containing a set element.
+        @type element: L{domish.Element}
+
+        @return: RSMRequest instance.
+        @rtype: L{RSMRequest}
+        """
+        try:
+            set_elt = domish.generateElementsQNamed(element.elements(),
+                                                    name="set",
+                                                    uri=NS_RSM).next()
+        except StopIteration:
+            return None
+
+        request = RSMRequest()
+        for elt in list(set_elt.elements()):
+            if elt.name in ('before', 'after'):
+                setattr(request, elt.name, ''.join(elt.children))
+            elif elt.name in ('max', 'index'):
+                setattr(request, elt.name, int(''.join(elt.children)))
+
+        if request.max is None:
+            log.err("RSM request is missing its 'max' element!")
+
+        return request
+
+    def render(self, element=None):
+        """Render a RSM page request, eventually embed it in the given element.
+
+        @param element: request element.
+        @type element: L{domish.Element}
+
+        @return: RSM request element.
+        @rtype: L{domish.Element}
+        """
+        if element and element.name == 'pubsub' and hasattr(element, 'items'):
+            element.items.attributes['max_items'] = unicode(self.max)
+
+        set_elt = domish.Element((NS_RSM, 'set'))
+        set_elt.addElement('max').addContent(unicode(self.max))
+
+        if self.index is not None:
+            set_elt.addElement('index').addContent(unicode(self.index))
+
+        if self.before is not None:
+            if self.before == '':  # request the last page
+                set_elt.addElement('before')
+            else:
+                set_elt.addElement('before').addContent(self.before)
+
+        if self.after is not None:
+            set_elt.addElement('after').addContent(self.after)
+
+        if element:
+            element.addChild(set_elt)
+
+        return set_elt
+
+
+class RSMResponse():
+    """
+    A Result Set Management response.
+
+    @ivar count: total number of items.
+    @itype count: C{int}
+
+    @ivar index: starting index of the returned page.
+    @itype index: C{int}
+
+    @ivar first: ID of the first element of the returned page.
+    @itype first: C{unicode}
+
+    @ivar last: ID of the last element of the returned page.
+    @itype last: C{unicode}
+    """
+
+    count = 0
+    index = None
+    first = None
+    last = None
+
+    def __init__(self, count=None, index=None, first=None, last=None):
+        if count is not None:
+            assert(isinstance(count, int) and count >= 0)
+            self.count = count
+
+        if index is not None:
+            assert(isinstance(index, int) and index >= 0)
+            self.index = index
+            assert(isinstance(first, unicode))
+            self.first = first
+            assert(isinstance(last, unicode))
+            self.last = last
+        else:
+            assert(first is None and last is None)
+
+    @classmethod
+    def parse(cls, element):
+        """Parse the given response element.
+
+        @param element: response element.
+        @type element: L{domish.Element}
+
+        @return: RSMResponse instance.
+        @rtype: L{RSMResponse}
+        """
+        try:
+            set_elt = domish.generateElementsQNamed(element.elements(),
+                                                    name="set",
+                                                    uri=NS_RSM).next()
+        except StopIteration:
+            return None
+
+        response = RSMResponse()
+        for elt in list(set_elt.elements()):
+            if elt.name in ('first', 'last'):
+                setattr(response, elt.name, ''.join(elt.children))
+                if elt.name == 'first':
+                    response.index = int(elt.getAttribute("index"))
+            elif elt.name == 'count':
+                response.count = int(''.join(elt.children))
+
+        if response.count is None:
+            log.err("RSM response is missing its 'count' element!")
+
+        return response
+
+    def render(self, parent=None):
+        """Render a RSM page response, eventually embed it in the given element.
+
+        @param element: response element.
+        @type element:  L{domish.Element}
+
+        @return: RSM request element.
+        @rtype: L{domish.Element}
+        """
+        set_elt = domish.Element((NS_RSM, 'set'))
+        set_elt.addElement('count').addContent(unicode(self.count))
+
+        if self.index is not None:
+            first_elt = set_elt.addElement('first')
+            first_elt.addContent(self.first)
+            first_elt['index'] = unicode(self.index)
+
+            set_elt.addElement('last').addContent(self.last)
+
+        if parent:
+            parent.addChild(set_elt)
+
+        return set_elt
+
+    def toDict(self):
+        """Return a dict representation of the object.
+
+        @return: a dict of strings.
+        @rtype: C{dict} binding C{unicode} to C{unicode}
+        """
+        result = {}
+        for attr in ('count', 'index', 'first', 'last'):
+            value = getattr(self, attr)
+            if value is not None:
+                result[attr] = unicode(value)
+        return result
+
+
+class PubSubRequest(pubsub.PubSubRequest):
+    """PubSubRequest extension to handle RSM.
+
+    @ivar rsm: RSM request instance.
+    @type rsm: L{RSMRequest}
+    """
+
+    rsm = None
+
+    def __init__(self, verb=None):
+        pubsub.PubSubRequest.__init__(self, verb)
+        self._parameters = copy.deepcopy(pubsub.PubSubRequest._parameters)
+        self._parameters['items'].append('rsm')
+
+    def _parse_rsm(self, verbElement):
+        self.rsm = RSMRequest.parse(verbElement.parent)
+
+    def _render_rsm(self, verbElement):
+        if self.rsm:
+            self.rsm.render(verbElement.parent)
+
+
+class PubSubClient(pubsub.PubSubClient):
+    """PubSubClient extension to handle RSM."""
+
+    _rsm_responses = {}
+
+    def items(self, service, nodeIdentifier, maxItems=None, itemIdentifiers=None,
+              subscriptionIdentifier=None, sender=None, ext_data=None):
+        """
+        Retrieve previously published items from a publish subscribe node.
+
+        @param service: The publish subscribe service that keeps the node.
+        @type service: L{JID<twisted.words.protocols.jabber.jid.JID>}
+
+        @param nodeIdentifier: The identifier of the node.
+        @type nodeIdentifier: C{unicode}
+
+        @param maxItems: Optional limit on the number of retrieved items.
+        @type maxItems: C{int}
+
+        @param itemIdentifiers: Identifiers of the items to be retrieved.
+        @type itemIdentifiers: C{set}
+
+        @param subscriptionIdentifier: Optional subscription identifier. In
+            case the node has been subscribed to multiple times, this narrows
+            the results to the specific subscription.
+        @type subscriptionIdentifier: C{unicode}
+
+        @param ext_data: extension data.
+        @type ext_data: L{dict}
+
+        @return: a Deferred that fires a C{list} of L{domish.Element}.
+        @rtype: L{defer.Deferred}
+        """
+        request = PubSubRequest('items')  # that's a rsm.PubSubRequest instance
+        request.recipient = service
+        request.nodeIdentifier = nodeIdentifier
+        if maxItems:
+            request.maxItems = str(int(maxItems))
+        request.subscriptionIdentifier = subscriptionIdentifier
+        request.sender = sender
+        request.itemIdentifiers = itemIdentifiers
+        if ext_data and 'rsm' in ext_data:
+            request.rsm = ext_data['rsm']
+
+        def cb(iq):
+            items = []
+            if iq.pubsub.items:
+                for element in iq.pubsub.items.elements():
+                    if element.uri == pubsub.NS_PUBSUB and element.name == 'item':
+                        items.append(element)
+
+            if request.rsm:
+                response = RSMResponse.parse(iq.pubsub)
+                if response is not None:
+                    self._rsm_responses[ext_data['id']] = response
+            return items
+
+        d = request.send(self.xmlstream)
+        d.addCallback(cb)
+        return d
+
+    def getRSMResponse(self, id):
+        """
+        Post-retrieve the RSM response data after items retrieval is done.
+
+        @param id: extension data ID
+        @type id: C{unicode}
+
+        @return: dict representation of the RSM response.
+        @rtype: C{dict} of C{unicode}
+        """
+        # This method exists to not modify the return value of self.items.
+        if id not in self._rsm_responses:
+            return {}
+        result = self._rsm_responses[id].toDict()
+        del self._rsm_responses[id]
+        return result
+
+
+class PubSubService(pubsub.PubSubService):
+    """PubSubService extension to handle RSM."""
+
+    _request_class = PubSubRequest
+
+    def _toResponse_items(self, result, resource, request):
+        response = pubsub.PubSubService._toResponse_items(self, result,
+                                                          resource, request)
+        set_elts = [elt for elt in result if elt.name == 'set']
+        if set_elts:
+            assert(len(set_elts) == 1)
+            response.addChild(set_elts[0])
+
+        return response