comparison wokkel/mam.py @ 20:81f9b53ec7e4

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
author Goffi <goffi@goffi.org>
date Sun, 03 Jan 2016 18:36:41 +0100
parents a07bf3fb4b54
children 0640d72d6841
comparison
equal deleted inserted replaced
19:80f9a1a3d002 20:81f9b53ec7e4
1 # -*- coding: utf-8 -*- 1 # -*- coding: utf-8 -*-
2 # -*- test-case-name: wokkel.test.test_mam -*- 2 # -*- test-case-name: wokkel.test.test_mam -*-
3 # 3 #
4 # SàT Wokkel extension for Message Archive Management (XEP-0313) 4 # SàT Wokkel extension for Message Archive Management (XEP-0313)
5 # Copyright (C) 2015 Jérôme Poisson (goffi@goffi.org)
5 # Copyright (C) 2015 Adien Cossa (souliane@mailoo.org) 6 # Copyright (C) 2015 Adien Cossa (souliane@mailoo.org)
6 7
7 # This program is free software: you can redistribute it and/or modify 8 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by 9 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or 10 # the Free Software Foundation, either version 3 of the License, or
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details. 16 # GNU Affero General Public License for more details.
16 17
17 # You should have received a copy of the GNU Affero General Public License 18 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 #
20 # Copyright (c) Adrien Cossa.
21 # See LICENSE for details.
22 20
23 """ 21 """
24 XMPP Message Archive Management protocol. 22 XMPP Message Archive Management protocol.
25 23
26 This protocol is specified in 24 This protocol is specified in
32 from zope.interface import Interface, implements 30 from zope.interface import Interface, implements
33 31
34 from twisted.words.protocols.jabber.xmlstream import IQ, toResponse 32 from twisted.words.protocols.jabber.xmlstream import IQ, toResponse
35 from twisted.words.xish import domish 33 from twisted.words.xish import domish
36 from twisted.words.protocols.jabber import jid 34 from twisted.words.protocols.jabber import jid
35 from twisted.internet import defer
37 from twisted.python import log 36 from twisted.python import log
38 37
39 from wokkel.subprotocols import IQHandlerMixin, XMPPHandler 38 from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
40 from wokkel import disco, data_form, delay 39 from wokkel import disco, data_form, delay
41 40
42 import rsm 41 import rsm
43 42
44 NS_MAM = 'urn:xmpp:mam:0' 43 NS_MAM = 'urn:xmpp:mam:1'
45 NS_FORWARD = 'urn:xmpp:forward:0' 44 NS_FORWARD = 'urn:xmpp:forward:0'
46 45
47 FIELDS_REQUEST = "/iq[@type='get']/query[@xmlns='%s']" % NS_MAM 46 FIELDS_REQUEST = "/iq[@type='get']/query[@xmlns='%s']" % NS_MAM
48 ARCHIVE_REQUEST = "/iq[@type='set']/query[@xmlns='%s']" % NS_MAM 47 ARCHIVE_REQUEST = "/iq[@type='set']/query[@xmlns='%s']" % NS_MAM
49 PREFS_GET_REQUEST = "/iq[@type='get']/prefs[@xmlns='%s']" % NS_MAM 48 PREFS_GET_REQUEST = "/iq[@type='get']/prefs[@xmlns='%s']" % NS_MAM
82 @ivar rsm: RSM request instance. 81 @ivar rsm: RSM request instance.
83 @itype rsm: L{rsm.RSMRequest} 82 @itype rsm: L{rsm.RSMRequest}
84 83
85 @ivar node: pubsub node id if querying a pubsub node, else None. 84 @ivar node: pubsub node id if querying a pubsub node, else None.
86 @itype form: C{unicode} 85 @itype form: C{unicode}
87 """ 86
88 87 @ivar query_id: id to use to track the query
89 def __init__(self, form=None, rsm_=None, node=None): 88 @itype form: C{unicode}
89 """
90
91 def __init__(self, form=None, rsm_=None, node=None, query_id=None):
90 if form is not None: 92 if form is not None:
91 assert form.formType == 'submit' 93 assert form.formType == 'submit'
94 assert form.formNamespace == NS_MAM
92 self.form = form 95 self.form = form
93 self.rsm = rsm_ 96 self.rsm = rsm_
94 self.node = node 97 self.node = node
98 self.query_id = query_id
95 99
96 @classmethod 100 @classmethod
97 def parse(cls, element): 101 def parse(cls, element):
98 """Parse the DOM representation of a MAM <query/> request. 102 """Parse the DOM representation of a MAM <query/> request.
99 103
108 form = data_form.findForm(element, NS_MAM) 112 form = data_form.findForm(element, NS_MAM)
109 try: 113 try:
110 rsm_request = rsm.RSMRequest.parse(element) 114 rsm_request = rsm.RSMRequest.parse(element)
111 except rsm.RSMNotFoundError: 115 except rsm.RSMNotFoundError:
112 rsm_request = None 116 rsm_request = None
113 node = element['node'] if element.hasAttribute('node') else None 117 node = element.getAttribute('node')
114 return MAMQueryRequest(form, rsm_request, node) 118 query_id = element.getAttribute('queryid')
119 return MAMQueryRequest(form, rsm_request, node, query_id)
115 120
116 def toElement(self): 121 def toElement(self):
117 """ 122 """
118 Return the DOM representation of this RSM <query/> request. 123 Return the DOM representation of this RSM <query/> request.
119 124
120 @rtype: L{Element<twisted.words.xish.domish.Element>} 125 @rtype: L{Element<twisted.words.xish.domish.Element>}
121 """ 126 """
122 mam_elt = domish.Element((NS_MAM, 'query')) 127 mam_elt = domish.Element((NS_MAM, 'query'))
123 if self.node is not None: 128 if self.node is not None:
124 mam_elt['node'] = self.node 129 mam_elt['node'] = self.node
130 if self.query_id is not None:
131 mam_elt['queryid'] = self.query_id
125 if self.form is not None: 132 if self.form is not None:
126 mam_elt.addChild(self.form.toElement()) 133 mam_elt.addChild(self.form.toElement())
127 if self.rsm is not None: 134 if self.rsm is not None:
128 mam_elt.addChild(self.rsm.toElement()) 135 mam_elt.addChild(self.rsm.toElement())
129 136
156 163
157 @param never (list): A list of JID instances. 164 @param never (list): A list of JID instances.
158 @type never: C{list} 165 @type never: C{list}
159 """ 166 """
160 167
161 def __init__(self, default=None, always=None, never=None): 168 def __init__(self, default, always=None, never=None):
162 if default is not None: 169 assert default in ('always', 'never', 'roster')
163 assert default in ('always', 'never', 'roster')
164 self.default = default 170 self.default = default
165 if always is not None: 171 if always is not None:
166 assert isinstance(always, list) 172 assert isinstance(always, list)
167 else: 173 else:
168 always = [] 174 always = []
172 else: 178 else:
173 never = [] 179 never = []
174 self.never = never 180 self.never = never
175 181
176 @classmethod 182 @classmethod
177 def parse(cls, element): 183 def parse(cls, prefs_elt):
178 """Parse the DOM representation of a MAM <prefs/> request. 184 """Parse the DOM representation of a MAM <prefs/> request.
179 185
180 @param element: MAM <prefs/> request element. 186 @param prefs_elt: MAM <prefs/> request element.
181 @type element: L{Element<twisted.words.xish.domish.Element>} 187 @type prefs_elt: L{Element<twisted.words.xish.domish.Element>}
182 188
183 @return: MAMPrefs instance. 189 @return: MAMPrefs instance.
184 @rtype: L{MAMPrefs} 190 @rtype: L{MAMPrefs}
185 """ 191 """
186 if element.uri != NS_MAM or element.name != 'prefs': 192 if prefs_elt.uri != NS_MAM or prefs_elt.name != 'prefs':
187 raise MAMError('Element provided is not a MAM <prefs/> request') 193 raise MAMError('Element provided is not a MAM <prefs/> request')
188 default = element['default'] if element.hasAttribute('default') else None 194 try:
195 default = prefs_elt['default']
196 except KeyError:
197 # FIXME: return proper error here
198 raise MAMError('Element provided is not a valid MAM <prefs/> request')
199
189 prefs = {} 200 prefs = {}
190 for attr in ('always', 'never'): 201 for attr in ('always', 'never'):
191 prefs[attr] = [] 202 prefs[attr] = []
192 try: 203 try:
193 pref = domish.generateElementsNamed(element.elements(), attr).next() 204 pref = prefs_elt.elements(NS_MAM, attr).next()
194 for jid_s in domish.generateElementsNamed(pref.elements(), 'jid'): 205 except StopIteration:
206 # FIXME: return proper error here
207 raise MAMError('Element provided is not a valid MAM <prefs/> request')
208 else:
209 for jid_s in pref.elements(NS_MAM, 'jid'):
195 prefs[attr].append(jid.JID(jid_s)) 210 prefs[attr].append(jid.JID(jid_s))
196 except StopIteration:
197 pass
198 return MAMPrefs(default, **prefs) 211 return MAMPrefs(default, **prefs)
199 212
200 def toElement(self): 213 def toElement(self):
201 """ 214 """
202 Return the DOM representation of this RSM <prefs/>request. 215 Return the DOM representation of this RSM <prefs/>request.
233 MAM client. 246 MAM client.
234 247
235 This handler implements the protocol for sending out MAM requests. 248 This handler implements the protocol for sending out MAM requests.
236 """ 249 """
237 250
238 def queryArchive(self, service=None, form=None, rsm=None, node=None, sender=None): 251 def queryArchive(self, service=None, form=None, rsm=None, node=None, sender=None, query_id=None):
239 """Query a user, MUC or pubsub archive. 252 """Query a user, MUC or pubsub archive.
240 253
241 @param service: Entity offering the MAM service (None for user archives). 254 @param service: Entity offering the MAM service (None for user archives).
242 @type service: L{JID<twisted.words.protocols.jabber.jid.JID>} 255 @type service: L{JID<twisted.words.protocols.jabber.jid.JID>}
243 256
251 @type node: C{unicode} 264 @type node: C{unicode}
252 265
253 @param sender: Optional sender address. 266 @param sender: Optional sender address.
254 @type sender: L{JID<twisted.words.protocols.jabber.jid.JID>} 267 @type sender: L{JID<twisted.words.protocols.jabber.jid.JID>}
255 268
269 @param query_id: Optional query id
270 @type query_id: C{unicode}
271
256 @return: A deferred that fires upon receiving a response. 272 @return: A deferred that fires upon receiving a response.
257 @rtype: L{Deferred<twisted.internet.defer.Deferred>} 273 @rtype: L{Deferred<twisted.internet.defer.Deferred>}
258 """ 274 """
259 iq = IQ(self.xmlstream, 'set') 275 iq = IQ(self.xmlstream, 'set')
260 MAMQueryRequest(form, rsm, node).render(iq) 276 MAMQueryRequest(form, rsm, node, query_id).render(iq)
261 if sender is not None: 277 if sender is not None:
262 iq['from'] = unicode(sender) 278 iq['from'] = unicode(sender)
263 return iq.send(to=service.full() if service else None) 279 return iq.send(to=service.full() if service else None)
264 280
265 def queryFields(self, service=None, sender=None): 281 def queryFields(self, service=None, sender=None):
269 @type service: L{JID<twisted.words.protocols.jabber.jid.JID>} 285 @type service: L{JID<twisted.words.protocols.jabber.jid.JID>}
270 286
271 @param sender: Optional sender address. 287 @param sender: Optional sender address.
272 @type sender: L{JID<twisted.words.protocols.jabber.jid.JID>} 288 @type sender: L{JID<twisted.words.protocols.jabber.jid.JID>}
273 289
274 @return: A deferred that fires upon receiving a response. 290 @return: data Form with the fields, or None if not found
275 @rtype: L{Deferred<twisted.internet.defer.Deferred>} 291 @rtype: L{Deferred<twisted.internet.defer.Deferred>}
276 """ 292 """
277 # http://xmpp.org/extensions/xep-0313.html#query-form 293 # http://xmpp.org/extensions/xep-0313.html#query-form
278 iq = IQ(self.xmlstream, 'get') 294 iq = IQ(self.xmlstream, 'get')
279 MAMQueryRequest().render(iq) 295 MAMQueryRequest().render(iq)
280 if sender is not None: 296 if sender is not None:
281 iq['from'] = unicode(sender) 297 iq['from'] = unicode(sender)
282 return iq.send(to=service.full() if service else None) 298 d = iq.send(to=service.full() if service else None)
299 d.addCallback(lambda iq_result: iq_result.elements(NS_MAM, 'query').next())
300 d.addCallback(data_form.findForm, NS_MAM)
301 return d
283 302
284 def queryPrefs(self, service=None, sender=None): 303 def queryPrefs(self, service=None, sender=None):
285 """Retrieve the current user preferences. 304 """Retrieve the current user preferences.
286 305
287 @param service: Entity offering the MAM service (None for user archives). 306 @param service: Entity offering the MAM service (None for user archives).
405 """ 424 """
406 @param resource: instance implementing IMAMResource 425 @param resource: instance implementing IMAMResource
407 @type resource: L{object} 426 @type resource: L{object}
408 """ 427 """
409 self.resource = resource 428 self.resource = resource
410 self.extra_filters = {} 429 self.extra_fields = {}
411 430
412 def connectionInitialized(self): 431 def connectionInitialized(self):
413 """ 432 """
414 Called when the XML stream has been initialized. 433 Called when the XML stream has been initialized.
415 434
422 441
423 def addFilter(self, field): 442 def addFilter(self, field):
424 """ 443 """
425 Add a new filter for querying MAM archive. 444 Add a new filter for querying MAM archive.
426 445
427 @param field: dictionary specifying the attributes to build a 446 @param field: data form field of the filter
428 wokkel.data_form.Field. 447 @type field: L{Form<wokkel.data_form.Field>}
429 @type field: C{dict} 448 """
430 """ 449 self.extra_fields[field.var] = field
431 self.extra_filters[field.var] = field
432 450
433 def _onFieldsRequest(self, iq): 451 def _onFieldsRequest(self, iq):
434 """ 452 """
435 Called when a fields request has been received. 453 Called when a fields request has been received.
436 454
437 This immediately replies with a result response. 455 This immediately replies with a result response.
438 """ 456 """
439 response = toResponse(iq, 'result') 457 response = toResponse(iq, 'result')
440 query = response.addElement((NS_MAM, 'query')) 458 query = response.addElement((NS_MAM, 'query'))
441 query.addChild(buildForm('form', extra=self.extra_filters).toElement()) 459 query.addChild(buildForm('form', extra=self.extra_fields).toElement())
442 self.xmlstream.send(response) 460 self.xmlstream.send(response)
443 iq.handled = True 461 iq.handled = True
444 462
445 def _onArchiveRequest(self, iq): 463 def _onArchiveRequest(self, iq):
446 """ 464 """
447 Called when a message archive request has been received. 465 Called when a message archive request has been received.
448 466
449 This immediately replies with a result response, followed by the 467 This replies with the list of archived message and the <iq> result
450 list of archived message and the finally the <fin/> message. 468 @return: A tuple with list of message data (id, element, data) and RSM element
451 """ 469 @rtype: C{tuple}
452 response = toResponse(iq, 'result') 470 """
453 self.xmlstream.send(response)
454
455 mam_ = MAMQueryRequest.parse(iq.query) 471 mam_ = MAMQueryRequest.parse(iq.query)
456 requestor = jid.JID(iq['from']) 472 requestor = jid.JID(iq['from'])
457 473
458 # remove unsupported filters 474 # remove unsupported filters
459 unsupported_fields = [] 475 unsupported_fields = []
460 if mam_.form: 476 if mam_.form:
461 for key, field in mam_.form.fields.iteritems(): 477 for key, field in mam_.form.fields.iteritems():
462 if key not in self._legacyFilters and key not in self.extra_filters: 478 if key not in self._legacyFilters and key not in self.extra_fields:
463 log.msg('Ignored unsupported MAM filter: %s' % field) 479 log.msg('Ignored unsupported MAM filter: %s' % field)
464 unsupported_fields.append(key) 480 unsupported_fields.append(key)
465 for key in unsupported_fields: 481 for key in unsupported_fields:
466 del mam_.form.fields[key] 482 del mam_.form.fields[key]
467 483
468 def forward_message(id_, elt, date): 484 def forward_message(id_, elt, date):
469 msg = domish.Element((None, 'message')) 485 msg = domish.Element((None, 'message'))
470 msg['to'] = iq['from'] 486 msg['to'] = iq['from']
471 result = msg.addElement('result', NS_MAM) 487 result = msg.addElement((NS_MAM, 'result'))
472 if iq.hasAttribute('queryid'): 488 try:
473 result['queryid'] = iq.query['queryid'] 489 result['queryid'] = iq.query['queryid']
490 except KeyError:
491 pass
474 result['id'] = id_ 492 result['id'] = id_
475 forward = result.addElement('forwarded', NS_FORWARD) 493 forward = result.addElement((NS_FORWARD, 'forwarded'))
476 forward.addChild(delay.Delay(date).toElement()) 494 forward.addChild(delay.Delay(date).toElement())
477 forward.addChild(elt) 495 forward.addChild(elt)
478 self.xmlstream.send(msg) 496 self.xmlstream.send(msg)
479 497
480 def cb(result): 498 def cb(result):
481 msg_data, rsm_elt = result 499 msg_data, rsm_elt = result
482 for data in msg_data: 500 for data in msg_data:
483 forward_message(*data) 501 forward_message(*data)
484 msg = domish.Element((None, 'message')) 502
485 msg['to'] = iq['from'] 503 response = toResponse(iq, 'result')
486 fin = msg.addElement('fin', NS_MAM) 504 fin = response.addElement((NS_MAM, 'fin'))
487 if iq.hasAttribute('queryid'): 505
488 fin['queryid'] = iq.query['queryid']
489 if rsm_elt is not None: 506 if rsm_elt is not None:
490 fin.addChild(rsm_elt) 507 fin.addChild(rsm_elt)
491 self.xmlstream.send(msg) 508 self.xmlstream.send(response)
492 509
493 self.resource.onArchiveRequest(mam_, requestor).addCallback(cb) 510 d = defer.maybeDeferred(self.resource.onArchiveRequest, mam_, requestor)
511 d.addCallback(cb)
494 iq.handled = True 512 iq.handled = True
495 513
496 def _onPrefsGetRequest(self, iq): 514 def _onPrefsGetRequest(self, iq):
497 """ 515 """
498 Called when a prefs get request has been received. 516 Called when a prefs get request has been received.
548 """ 566 """
549 stampFormat = '%Y-%m-%dT%H:%M:%SZ' 567 stampFormat = '%Y-%m-%dT%H:%M:%SZ'
550 return datetime.astimezone(tzutc()).strftime(stampFormat) 568 return datetime.astimezone(tzutc()).strftime(stampFormat)
551 569
552 570
553 def buildForm(formType='submit', start=None, end=None, with_jid=None, extra=None): 571 def buildForm(formType='submit', start=None, end=None, with_jid=None, extra_fields=None):
554 """Prepare a Data Form for MAM. 572 """Prepare a Data Form for MAM.
555 573
556 @param formType: The type of the Data Form ('submit' or 'form'). 574 @param formType: The type of the Data Form ('submit' or 'form').
557 @type formType: C{unicode} 575 @type formType: C{unicode}
558 576
563 @type end: L{datetime<datetime.datetime>} 581 @type end: L{datetime<datetime.datetime>}
564 582
565 @param with_jid: JID against which to match messages. 583 @param with_jid: JID against which to match messages.
566 @type with_jid: L{JID<twisted.words.protocols.jabber.jid.JID>} 584 @type with_jid: L{JID<twisted.words.protocols.jabber.jid.JID>}
567 585
568 @param extra: list of extra fields that are not defined by the 586 @param extra_fields: list of extra data form fields that are not defined by the
569 specification. Each element of the list must be a dictionary 587 specification.
570 specifying the attributes to build a wokkel.data_form.Field.
571 @type: C{list} 588 @type: C{list}
572 589
573 @return: XEP-0004 Data Form object. 590 @return: XEP-0004 Data Form object.
574 @rtype: L{Form<wokkel.data_form.Form>} 591 @rtype: L{Form<wokkel.data_form.Form>}
575 """ 592 """
576 form = data_form.Form(formType, formNamespace=NS_MAM) 593 form = data_form.Form(formType, formNamespace=NS_MAM)
577 filters = []
578 594
579 if formType == 'form': 595 if formType == 'form':
580 filters.extend(MAMService._legacyFilters.values()) 596 for kwargs in MAMService._legacyFilters.values():
597 form.addField(data_form.Field(**kwargs))
581 elif formType == 'submit': 598 elif formType == 'submit':
582 if start: 599 if start:
583 filters.append({'var': 'start', 'value': datetime2utc(start)}) 600 form.addField(data_form.Field(var='start', value=datetime2utc(start)))
584 if end: 601 if end:
585 filters.append({'var': 'end', 'value': datetime2utc(end)}) 602 form.addField(data_form.Field(var='end', value=datetime2utc(end)))
586 if with_jid: 603 if with_jid:
587 # must specify the field type to overwrite default value in Field.__init__ 604 form.addField(data_form.Field(fieldType='jid-single', var='with', value=with_jid.full()))
588 filters.append({'fieldType': 'jid-single', 'var': 'with', 'value': with_jid.full()}) 605
589 606 if extra_fields is not None:
590 if extra is not None: 607 for field in extra_fields:
591 filters.extend(extra) 608 form.addField(field)
592
593 for field in filters:
594 form.addField(data_form.Field(**field))
595 609
596 return form 610 return form