tmp (mam): various improvments:

- updated behaviour and namespace to new urn:xmpp:mam:1
    - fixed use of query_id in MAMQueryRequest/MAMClient
    - added a couple of assertions/checks
    - use try/except instead of hasAttribute
    - do not use generateElementsNamed (use elements method instead)
    - do not ignore missing elements
    - queryFields return a data form instead of a raw element
    - fixed bad use of namespace in several addElement
    - IMAMResource.onArchiveRequest doesn't have to return a Deferred anymore (but may do)
    - buildForm use data_form.Field class directly and doesn't use intermediate list anymore
    - /!\ XEP-0313 plugin is temporarly broken
This commit is contained in:
Goffi 2016-01-03 18:36:41 +01:00
commit b5d2fda488

View file

@ -2,6 +2,7 @@
# -*- test-case-name: wokkel.test.test_mam -*- # -*- test-case-name: wokkel.test.test_mam -*-
# #
# SàT Wokkel extension for Message Archive Management (XEP-0313) # SàT Wokkel extension for Message Archive Management (XEP-0313)
# Copyright (C) 2015 Jérôme Poisson (goffi@goffi.org)
# Copyright (C) 2015 Adien Cossa (souliane@mailoo.org) # Copyright (C) 2015 Adien Cossa (souliane@mailoo.org)
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
@ -16,9 +17,6 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (c) Adrien Cossa.
# See LICENSE for details.
""" """
XMPP Message Archive Management protocol. XMPP Message Archive Management protocol.
@ -34,6 +32,7 @@ from zope.interface import Interface, implements
from twisted.words.protocols.jabber.xmlstream import IQ, toResponse from twisted.words.protocols.jabber.xmlstream import IQ, toResponse
from twisted.words.xish import domish from twisted.words.xish import domish
from twisted.words.protocols.jabber import jid from twisted.words.protocols.jabber import jid
from twisted.internet import defer
from twisted.python import log from twisted.python import log
from wokkel.subprotocols import IQHandlerMixin, XMPPHandler from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
@ -41,7 +40,7 @@ from wokkel import disco, data_form, delay
import rsm import rsm
NS_MAM = 'urn:xmpp:mam:0' NS_MAM = 'urn:xmpp:mam:1'
NS_FORWARD = 'urn:xmpp:forward:0' NS_FORWARD = 'urn:xmpp:forward:0'
FIELDS_REQUEST = "/iq[@type='get']/query[@xmlns='%s']" % NS_MAM FIELDS_REQUEST = "/iq[@type='get']/query[@xmlns='%s']" % NS_MAM
@ -84,14 +83,19 @@ class MAMQueryRequest():
@ivar node: pubsub node id if querying a pubsub node, else None. @ivar node: pubsub node id if querying a pubsub node, else None.
@itype form: C{unicode} @itype form: C{unicode}
@ivar query_id: id to use to track the query
@itype form: C{unicode}
""" """
def __init__(self, form=None, rsm_=None, node=None): def __init__(self, form=None, rsm_=None, node=None, query_id=None):
if form is not None: if form is not None:
assert form.formType == 'submit' assert form.formType == 'submit'
assert form.formNamespace == NS_MAM
self.form = form self.form = form
self.rsm = rsm_ self.rsm = rsm_
self.node = node self.node = node
self.query_id = query_id
@classmethod @classmethod
def parse(cls, element): def parse(cls, element):
@ -110,8 +114,9 @@ class MAMQueryRequest():
rsm_request = rsm.RSMRequest.parse(element) rsm_request = rsm.RSMRequest.parse(element)
except rsm.RSMNotFoundError: except rsm.RSMNotFoundError:
rsm_request = None rsm_request = None
node = element['node'] if element.hasAttribute('node') else None node = element.getAttribute('node')
return MAMQueryRequest(form, rsm_request, node) query_id = element.getAttribute('queryid')
return MAMQueryRequest(form, rsm_request, node, query_id)
def toElement(self): def toElement(self):
""" """
@ -122,6 +127,8 @@ class MAMQueryRequest():
mam_elt = domish.Element((NS_MAM, 'query')) mam_elt = domish.Element((NS_MAM, 'query'))
if self.node is not None: if self.node is not None:
mam_elt['node'] = self.node mam_elt['node'] = self.node
if self.query_id is not None:
mam_elt['queryid'] = self.query_id
if self.form is not None: if self.form is not None:
mam_elt.addChild(self.form.toElement()) mam_elt.addChild(self.form.toElement())
if self.rsm is not None: if self.rsm is not None:
@ -158,9 +165,8 @@ class MAMPrefs():
@type never: C{list} @type never: C{list}
""" """
def __init__(self, default=None, always=None, never=None): def __init__(self, default, always=None, never=None):
if default is not None: assert default in ('always', 'never', 'roster')
assert default in ('always', 'never', 'roster')
self.default = default self.default = default
if always is not None: if always is not None:
assert isinstance(always, list) assert isinstance(always, list)
@ -174,27 +180,34 @@ class MAMPrefs():
self.never = never self.never = never
@classmethod @classmethod
def parse(cls, element): def parse(cls, prefs_elt):
"""Parse the DOM representation of a MAM <prefs/> request. """Parse the DOM representation of a MAM <prefs/> request.
@param element: MAM <prefs/> request element. @param prefs_elt: MAM <prefs/> request element.
@type element: L{Element<twisted.words.xish.domish.Element>} @type prefs_elt: L{Element<twisted.words.xish.domish.Element>}
@return: MAMPrefs instance. @return: MAMPrefs instance.
@rtype: L{MAMPrefs} @rtype: L{MAMPrefs}
""" """
if element.uri != NS_MAM or element.name != 'prefs': if prefs_elt.uri != NS_MAM or prefs_elt.name != 'prefs':
raise MAMError('Element provided is not a MAM <prefs/> request') raise MAMError('Element provided is not a MAM <prefs/> request')
default = element['default'] if element.hasAttribute('default') else None try:
default = prefs_elt['default']
except KeyError:
# FIXME: return proper error here
raise MAMError('Element provided is not a valid MAM <prefs/> request')
prefs = {} prefs = {}
for attr in ('always', 'never'): for attr in ('always', 'never'):
prefs[attr] = [] prefs[attr] = []
try: try:
pref = domish.generateElementsNamed(element.elements(), attr).next() pref = prefs_elt.elements(NS_MAM, attr).next()
for jid_s in domish.generateElementsNamed(pref.elements(), 'jid'):
prefs[attr].append(jid.JID(jid_s))
except StopIteration: except StopIteration:
pass # FIXME: return proper error here
raise MAMError('Element provided is not a valid MAM <prefs/> request')
else:
for jid_s in pref.elements(NS_MAM, 'jid'):
prefs[attr].append(jid.JID(jid_s))
return MAMPrefs(default, **prefs) return MAMPrefs(default, **prefs)
def toElement(self): def toElement(self):
@ -235,7 +248,7 @@ class MAMClient(XMPPHandler):
This handler implements the protocol for sending out MAM requests. This handler implements the protocol for sending out MAM requests.
""" """
def queryArchive(self, service=None, form=None, rsm=None, node=None, sender=None): def queryArchive(self, service=None, form=None, rsm=None, node=None, sender=None, query_id=None):
"""Query a user, MUC or pubsub archive. """Query a user, MUC or pubsub archive.
@param service: Entity offering the MAM service (None for user archives). @param service: Entity offering the MAM service (None for user archives).
@ -253,11 +266,14 @@ class MAMClient(XMPPHandler):
@param sender: Optional sender address. @param sender: Optional sender address.
@type sender: L{JID<twisted.words.protocols.jabber.jid.JID>} @type sender: L{JID<twisted.words.protocols.jabber.jid.JID>}
@param query_id: Optional query id
@type query_id: C{unicode}
@return: A deferred that fires upon receiving a response. @return: A deferred that fires upon receiving a response.
@rtype: L{Deferred<twisted.internet.defer.Deferred>} @rtype: L{Deferred<twisted.internet.defer.Deferred>}
""" """
iq = IQ(self.xmlstream, 'set') iq = IQ(self.xmlstream, 'set')
MAMQueryRequest(form, rsm, node).render(iq) MAMQueryRequest(form, rsm, node, query_id).render(iq)
if sender is not None: if sender is not None:
iq['from'] = unicode(sender) iq['from'] = unicode(sender)
return iq.send(to=service.full() if service else None) return iq.send(to=service.full() if service else None)
@ -271,7 +287,7 @@ class MAMClient(XMPPHandler):
@param sender: Optional sender address. @param sender: Optional sender address.
@type sender: L{JID<twisted.words.protocols.jabber.jid.JID>} @type sender: L{JID<twisted.words.protocols.jabber.jid.JID>}
@return: A deferred that fires upon receiving a response. @return: data Form with the fields, or None if not found
@rtype: L{Deferred<twisted.internet.defer.Deferred>} @rtype: L{Deferred<twisted.internet.defer.Deferred>}
""" """
# http://xmpp.org/extensions/xep-0313.html#query-form # http://xmpp.org/extensions/xep-0313.html#query-form
@ -279,7 +295,10 @@ class MAMClient(XMPPHandler):
MAMQueryRequest().render(iq) MAMQueryRequest().render(iq)
if sender is not None: if sender is not None:
iq['from'] = unicode(sender) iq['from'] = unicode(sender)
return iq.send(to=service.full() if service else None) d = iq.send(to=service.full() if service else None)
d.addCallback(lambda iq_result: iq_result.elements(NS_MAM, 'query').next())
d.addCallback(data_form.findForm, NS_MAM)
return d
def queryPrefs(self, service=None, sender=None): def queryPrefs(self, service=None, sender=None):
"""Retrieve the current user preferences. """Retrieve the current user preferences.
@ -407,7 +426,7 @@ class MAMService(XMPPHandler, IQHandlerMixin):
@type resource: L{object} @type resource: L{object}
""" """
self.resource = resource self.resource = resource
self.extra_filters = {} self.extra_fields = {}
def connectionInitialized(self): def connectionInitialized(self):
""" """
@ -424,11 +443,10 @@ class MAMService(XMPPHandler, IQHandlerMixin):
""" """
Add a new filter for querying MAM archive. Add a new filter for querying MAM archive.
@param field: dictionary specifying the attributes to build a @param field: data form field of the filter
wokkel.data_form.Field. @type field: L{Form<wokkel.data_form.Field>}
@type field: C{dict}
""" """
self.extra_filters[field.var] = field self.extra_fields[field.var] = field
def _onFieldsRequest(self, iq): def _onFieldsRequest(self, iq):
""" """
@ -438,7 +456,7 @@ class MAMService(XMPPHandler, IQHandlerMixin):
""" """
response = toResponse(iq, 'result') response = toResponse(iq, 'result')
query = response.addElement((NS_MAM, 'query')) query = response.addElement((NS_MAM, 'query'))
query.addChild(buildForm('form', extra=self.extra_filters).toElement()) query.addChild(buildForm('form', extra=self.extra_fields).toElement())
self.xmlstream.send(response) self.xmlstream.send(response)
iq.handled = True iq.handled = True
@ -446,12 +464,10 @@ class MAMService(XMPPHandler, IQHandlerMixin):
""" """
Called when a message archive request has been received. Called when a message archive request has been received.
This immediately replies with a result response, followed by the This replies with the list of archived message and the <iq> result
list of archived message and the finally the <fin/> message. @return: A tuple with list of message data (id, element, data) and RSM element
@rtype: C{tuple}
""" """
response = toResponse(iq, 'result')
self.xmlstream.send(response)
mam_ = MAMQueryRequest.parse(iq.query) mam_ = MAMQueryRequest.parse(iq.query)
requestor = jid.JID(iq['from']) requestor = jid.JID(iq['from'])
@ -459,7 +475,7 @@ class MAMService(XMPPHandler, IQHandlerMixin):
unsupported_fields = [] unsupported_fields = []
if mam_.form: if mam_.form:
for key, field in mam_.form.fields.iteritems(): for key, field in mam_.form.fields.iteritems():
if key not in self._legacyFilters and key not in self.extra_filters: if key not in self._legacyFilters and key not in self.extra_fields:
log.msg('Ignored unsupported MAM filter: %s' % field) log.msg('Ignored unsupported MAM filter: %s' % field)
unsupported_fields.append(key) unsupported_fields.append(key)
for key in unsupported_fields: for key in unsupported_fields:
@ -468,11 +484,13 @@ class MAMService(XMPPHandler, IQHandlerMixin):
def forward_message(id_, elt, date): def forward_message(id_, elt, date):
msg = domish.Element((None, 'message')) msg = domish.Element((None, 'message'))
msg['to'] = iq['from'] msg['to'] = iq['from']
result = msg.addElement('result', NS_MAM) result = msg.addElement((NS_MAM, 'result'))
if iq.hasAttribute('queryid'): try:
result['queryid'] = iq.query['queryid'] result['queryid'] = iq.query['queryid']
except KeyError:
pass
result['id'] = id_ result['id'] = id_
forward = result.addElement('forwarded', NS_FORWARD) forward = result.addElement((NS_FORWARD, 'forwarded'))
forward.addChild(delay.Delay(date).toElement()) forward.addChild(delay.Delay(date).toElement())
forward.addChild(elt) forward.addChild(elt)
self.xmlstream.send(msg) self.xmlstream.send(msg)
@ -481,16 +499,16 @@ class MAMService(XMPPHandler, IQHandlerMixin):
msg_data, rsm_elt = result msg_data, rsm_elt = result
for data in msg_data: for data in msg_data:
forward_message(*data) forward_message(*data)
msg = domish.Element((None, 'message'))
msg['to'] = iq['from'] response = toResponse(iq, 'result')
fin = msg.addElement('fin', NS_MAM) fin = response.addElement((NS_MAM, 'fin'))
if iq.hasAttribute('queryid'):
fin['queryid'] = iq.query['queryid']
if rsm_elt is not None: if rsm_elt is not None:
fin.addChild(rsm_elt) fin.addChild(rsm_elt)
self.xmlstream.send(msg) self.xmlstream.send(response)
self.resource.onArchiveRequest(mam_, requestor).addCallback(cb) d = defer.maybeDeferred(self.resource.onArchiveRequest, mam_, requestor)
d.addCallback(cb)
iq.handled = True iq.handled = True
def _onPrefsGetRequest(self, iq): def _onPrefsGetRequest(self, iq):
@ -550,7 +568,7 @@ def datetime2utc(datetime):
return datetime.astimezone(tzutc()).strftime(stampFormat) return datetime.astimezone(tzutc()).strftime(stampFormat)
def buildForm(formType='submit', start=None, end=None, with_jid=None, extra=None): def buildForm(formType='submit', start=None, end=None, with_jid=None, extra_fields=None):
"""Prepare a Data Form for MAM. """Prepare a Data Form for MAM.
@param formType: The type of the Data Form ('submit' or 'form'). @param formType: The type of the Data Form ('submit' or 'form').
@ -565,32 +583,28 @@ def buildForm(formType='submit', start=None, end=None, with_jid=None, extra=None
@param with_jid: JID against which to match messages. @param with_jid: JID against which to match messages.
@type with_jid: L{JID<twisted.words.protocols.jabber.jid.JID>} @type with_jid: L{JID<twisted.words.protocols.jabber.jid.JID>}
@param extra: list of extra fields that are not defined by the @param extra_fields: list of extra data form fields that are not defined by the
specification. Each element of the list must be a dictionary specification.
specifying the attributes to build a wokkel.data_form.Field.
@type: C{list} @type: C{list}
@return: XEP-0004 Data Form object. @return: XEP-0004 Data Form object.
@rtype: L{Form<wokkel.data_form.Form>} @rtype: L{Form<wokkel.data_form.Form>}
""" """
form = data_form.Form(formType, formNamespace=NS_MAM) form = data_form.Form(formType, formNamespace=NS_MAM)
filters = []
if formType == 'form': if formType == 'form':
filters.extend(MAMService._legacyFilters.values()) for kwargs in MAMService._legacyFilters.values():
form.addField(data_form.Field(**kwargs))
elif formType == 'submit': elif formType == 'submit':
if start: if start:
filters.append({'var': 'start', 'value': datetime2utc(start)}) form.addField(data_form.Field(var='start', value=datetime2utc(start)))
if end: if end:
filters.append({'var': 'end', 'value': datetime2utc(end)}) form.addField(data_form.Field(var='end', value=datetime2utc(end)))
if with_jid: if with_jid:
# must specify the field type to overwrite default value in Field.__init__ form.addField(data_form.Field(fieldType='jid-single', var='with', value=with_jid.full()))
filters.append({'fieldType': 'jid-single', 'var': 'with', 'value': with_jid.full()})
if extra is not None: if extra_fields is not None:
filters.extend(extra) for field in extra_fields:
form.addField(field)
for field in filters:
form.addField(data_form.Field(**field))
return form return form