view wokkel/rsm.py @ 18:ba8ce62bfab0

tmp (wokkel): removed delay as it is nearly identical to original one, and the additional exception should be catched elsewhere
author Goffi <goffi@goffi.org>
date Sun, 03 Jan 2016 18:36:39 +0100
parents 4629645a368d
children 80f9a1a3d002
line wrap: on
line source

# -*- 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

import pubsub
import copy


# RSM namespace
NS_RSM = 'http://jabber.org/protocol/rsm'


class RSMError(Exception):
    """
    RSM error.
    """


class RSMNotFoundError(Exception):
    """
    An expected RSM element has not been found.
    """


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} 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, index=None, after=None, before=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 = element.elements(NS_RSM, 'set').next()
        except StopIteration:
            raise RSMNotFoundError()

        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:
            raise RSMError("RSM request is missing its 'max' element")

        return request

    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').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)

        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}
        """
        if element.name == 'pubsub' and hasattr(element, 'items'):
            element.items.attributes['max_items'] = unicode(self.max)

        set_elt = self.toElement()
        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}
    """

    def __init__(self, count=0, index=None, first=None, last=None):
        assert isinstance(count, int) and count >= 0
        self.count = count
        if index is not None:
            assert isinstance(index, int) and index >= 0
            assert isinstance(first, unicode)
            assert isinstance(last, unicode)
        else:
            assert first is None and last is None
        self.index = index
        self.first = first
        self.last = last

    @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 = element.elements(NS_RSM, 'set').next()
        except StopIteration:
            raise RSMNotFoundError()

        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:
            raise RSMError("RSM response is missing its 'count' element")

        return response

    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('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)

        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 ('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):
        super(PubSubRequest, self).__init__(verb)
        self._parameters = copy.deepcopy(pubsub.PubSubRequest._parameters)
        self._parameters['items'].append('rsm')

    def _parse_rsm(self, verbElement):
        try:
            self.rsm = RSMRequest.parse(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."""

    _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 is not None:
            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(pubsub.NS_PUBSUB, 'item'):
                    items.append(element)

            if request.rsm:
                try:
                    response = RSMResponse.parse(iq.pubsub)
                    if response is not None:
                        self._rsm_responses[ext_data['id']] = response
                except RSMNotFoundError:  # target pubsub server doesn't support RSM
                    pass
            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)
        for item in result:
            if isinstance(item, tuple):
                # XXX: see sat_pubsub.pgsql_storage.LeafNode.getItemsById return value
                item = item[0]
            if item.name == "set":
                response.addChild(item)
                break

        return response