libervia-backend/src/tmp/wokkel/rsm.py

376 lines
11 KiB
Python

# -*- 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):
pubsub.PubSubRequest.__init__(self, 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:
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)
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