view src/tmp/wokkel/rsm.py @ 1596:b7ee113183fc

jp: better profile commands: - new "profile/default" command - info doesn't show password anymore by default, need to be explicitly requested - info and modify don't need to connect anymore - modify can now set default profile. As use_profile is set, at least a profile session need to be started when it would not be mandatory technicaly (if just setting the profile as default is needed). But this option should not be used often, and it's not a big side effect, so I don't feel the need to create a new dedicated command, or to do complicated checks to avoid the session start.
author Goffi <goffi@goffi.org>
date Sat, 14 Nov 2015 19:18:10 +0100
parents 30ba7cac1dd2
children 7debf3a4bf14
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