Mercurial > sat_tmp
diff sat_tmp/wokkel/rsm.py @ 45:c8cb4e867897
made proper package + installation
author | Arnaud Joset <info@agayon.be> |
---|---|
date | Thu, 02 Nov 2017 22:50:59 +0100 |
parents | wokkel/rsm.py@213122b92b08 |
children | f4d569dc8e6b |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat_tmp/wokkel/rsm.py Thu Nov 02 22:50:59 2017 +0100 @@ -0,0 +1,425 @@ +# -*- coding: utf-8 -*- +# -*- test-case-name: wokkel.test.test_rsm -*- +# +# SàT Wokkel extension for Result Set Management (XEP-0059) +# Copyright (C) 2015 Adien 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 <http://www.gnu.org/licenses/>. + +""" +XMPP Result Set Management protocol. + +This protocol is specified in +U{XEP-0059<http://xmpp.org/extensions/xep-0059.html>}. +""" + +from twisted.words.xish import domish +from twisted.words.protocols.jabber import error + +import pubsub +import copy + + +NS_RSM = 'http://jabber.org/protocol/rsm' + + +class RSMError(error.StanzaError): + """ + RSM error. + """ + def __init__(self, text=None): + error.StanzaError.__init__(self, 'bad-request', text=text) + + +class RSMNotFoundError(Exception): + """ + An expected RSM element has not been found. + """ + + +class RSMRequest(object): + """ + 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} or C{None} + + @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} + """ + + def __init__(self, max_=10, after=None, before=None, index=None): + self.max = int(max_) + + if index is not None: + assert after is None and before is None + index = int(index) + self.index = index + + if after is not None: + assert before is None + assert isinstance(after, basestring) + self.after = after + + if before is not None: + assert isinstance(before, basestring) + self.before = before + + def __str__(self): + return "RSM Request: max={0.max} after={0.after} before={0.before} index={0.index}".format(self) + + @classmethod + def fromElement(cls, element): + """Parse the given request element. + + @param element: request containing a set element, or set element itself. + @type element: L{domish.Element} + + @return: RSMRequest instance. + @rtype: L{RSMRequest} + """ + + if element.name == 'set' and element.uri == NS_RSM: + set_elt = element + else: + try: + set_elt = element.elements(NS_RSM, 'set').next() + except StopIteration: + raise RSMNotFoundError() + + try: + before_elt = set_elt.elements(NS_RSM, 'before').next() + except StopIteration: + before = None + else: + before = unicode(before_elt) + + try: + after_elt = set_elt.elements(NS_RSM, 'after').next() + except StopIteration: + after = None + else: + after = unicode(after_elt) + if not after: + raise RSMError("<after/> element can't be empty in RSM request") + + try: + max_elt = set_elt.elements(NS_RSM, 'max').next() + except StopIteration: + # FIXME: even if it doesn't make a lot of sense without it + # <max/> element is not mandatory in XEP-0059 + raise RSMError("RSM request is missing its 'max' element") + else: + try: + max_ = int(unicode(max_elt)) + except ValueError: + raise RSMError("bad value for 'max' element") + + try: + index_elt = set_elt.elements(NS_RSM, 'index').next() + except StopIteration: + index = None + else: + try: + index = int(unicode(index_elt)) + except ValueError: + raise RSMError("bad value for 'index' element") + + return RSMRequest(max_, after, before, index) + + def toElement(self): + """ + Return the DOM representation of this RSM request. + + @rtype: L{domish.Element} + """ + set_elt = domish.Element((NS_RSM, 'set')) + set_elt.addElement('max', content=unicode(self.max)) + + if self.index is not None: + set_elt.addElement('index', content=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', content=self.before) + + if self.after is not None: + set_elt.addElement('after', content=self.after) + + return set_elt + + def render(self, element): + """Embed the DOM representation of this RSM request in the given element. + + @param element: Element to contain the RSM request. + @type element: L{domish.Element} + + @return: RSM request element. + @rtype: L{domish.Element} + """ + set_elt = self.toElement() + element.addChild(set_elt) + + return set_elt + + +class RSMResponse(object): + """ + A Result Set Management response. + + @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} + + @ivar index: starting index of the returned page. + @itype index: C{int} + + @ivar count: total number of items. + @itype count: C{int} + + """ + + def __init__(self, first=None, last=None, index=None, count=None): + if first is None: + assert last is None and index is None + if last is None: + assert first is None + self.first = first + self.last = last + if count is not None: + self.count = int(count) + else: + self.count = None + if index is not None: + self.index = int(index) + else: + self.index = None + + def __str__(self): + return "RSM Request: first={0.first} last={0.last} index={0.index} count={0.count}".format(self) + + @classmethod + def fromElement(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 = element.elements(NS_RSM, 'set').next() + except StopIteration: + raise RSMNotFoundError() + + try: + first_elt = set_elt.elements(NS_RSM, 'first').next() + except StopIteration: + first = None + index = None + else: + first = unicode(first_elt) + try: + index = int(first_elt['index']) + except KeyError: + index = None + except ValueError: + raise RSMError("bad index in RSM response") + + try: + last_elt = set_elt.elements(NS_RSM, 'last').next() + except StopIteration: + if first is not None: + raise RSMError("RSM response is missing its 'last' element") + else: + last = None + else: + if first is None: + raise RSMError("RSM response is missing its 'first' element") + last = unicode(last_elt) + + try: + count_elt = set_elt.elements(NS_RSM, 'count').next() + except StopIteration: + count = None + else: + try: + count = int(unicode(count_elt)) + except ValueError: + raise RSMError("invalid count in RSM response") + + return RSMResponse(first, last, index, count) + + def toElement(self): + """ + Return the DOM representation of this RSM request. + + @rtype: L{domish.Element} + """ + set_elt = domish.Element((NS_RSM, 'set')) + if self.first is not None: + first_elt = set_elt.addElement('first', content=self.first) + if self.index is not None: + first_elt['index'] = unicode(self.index) + + set_elt.addElement('last', content=self.last) + + if self.count is not None: + set_elt.addElement('count', content=unicode(self.count)) + + return set_elt + + def render(self, element): + """Embed the DOM representation of this RSM response in the given element. + + @param element: Element to contain the RSM response. + @type element: L{domish.Element} + + @return: RSM request element. + @rtype: L{domish.Element} + """ + set_elt = self.toElement() + element.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 ('first', 'last', 'index', 'count'): + 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 + _parameters = copy.deepcopy(pubsub.PubSubRequest._parameters) + _parameters['items'].append('rsm') + + def _parse_rsm(self, verbElement): + try: + self.rsm = RSMRequest.fromElement(verbElement.parent) + except RSMNotFoundError: + self.rsm = None + + def _render_rsm(self, verbElement): + if self.rsm: + self.rsm.render(verbElement.parent) + + +class PubSubClient(pubsub.PubSubClient): + """PubSubClient extension to handle RSM.""" + + _request_class = PubSubRequest + + def items(self, service, nodeIdentifier, maxItems=None, itemIdentifiers=None, + subscriptionIdentifier=None, sender=None, rsm_request=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 C{tuple} of L{domish.Element}, L{RSMResponse}. + @rtype: L{defer.Deferred} + """ + # XXX: we have to copy initial method instead of calling it, + # as original cb remove all non item elements + request = self._request_class('items') + request.recipient = service + request.nodeIdentifier = nodeIdentifier + if maxItems: + request.maxItems = str(int(maxItems)) + request.subscriptionIdentifier = subscriptionIdentifier + request.sender = sender + request.itemIdentifiers = itemIdentifiers + request.rsm = rsm_request + + def cb(iq): + items = [] + pubsub_elt = iq.pubsub + if pubsub_elt.items: + for element in pubsub_elt.items.elements(pubsub.NS_PUBSUB, 'item'): + items.append(element) + + try: + rsm_response = RSMResponse.fromElement(pubsub_elt) + except RSMNotFoundError: + rsm_response = None + return (items, rsm_response) + + d = request.send(self.xmlstream) + d.addCallback(cb) + return d + + +class PubSubService(pubsub.PubSubService): + """PubSubService extension to handle RSM.""" + + _request_class = PubSubRequest + + def _toResponse_items(self, elts, resource, request): + # default method only manage <item/> elements + # but we need to add RSM set element + rsm_elt = None + for idx, elt in enumerate(reversed(elts)): + if elt.name == "set" and elt.uri == NS_RSM: + rsm_elt = elts.pop(-1-idx) + break + + response = pubsub.PubSubService._toResponse_items(self, elts, + resource, request) + if rsm_elt is not None: + response.addChild(rsm_elt) + + return response