comparison src/tmp/wokkel/mam.py @ 1774:0c21dafedd22

tmp (wokkel.mam): cleaning and bug fix: - renamed MAMQueryRequest to MAMQuery - use of MAMQuery in queryArchive instead of separate arguments - fixed bad use of subprotocols.IQHandlerMixing.handleRequest - changed order of arguments in BuildForm to have more often used first - renamed parse to fromElement for coherency - added IMAMService interface - MAMService requestClass can be changed (it is specified in _request_class class attribute - use new-style classes where it make sense - MAMRequest.fromElement parse a <query/> parent instead of a <query/> directly
author Goffi <goffi@goffi.org>
date Tue, 05 Jan 2016 23:20:22 +0100
parents f525c272fd6d
children 21f0bcd3b637
comparison
equal deleted inserted replaced
1773:6e867caf4621 1774:0c21dafedd22
23 23
24 This protocol is specified in 24 This protocol is specified in
25 U{XEP-0313<http://xmpp.org/extensions/xep-0313.html>}. 25 U{XEP-0313<http://xmpp.org/extensions/xep-0313.html>}.
26 """ 26 """
27 27
28 from dateutil.tz import tzutc 28 from dateutil import tz
29 29
30 from zope.interface import Interface, implements 30 from zope.interface import implements
31 31 from zope.interface import Interface
32 from twisted.words.protocols.jabber.xmlstream import IQ, toResponse 32
33 from twisted.words.protocols.jabber import xmlstream
33 from twisted.words.xish import domish 34 from twisted.words.xish import domish
34 from twisted.words.protocols.jabber import jid 35 from twisted.words.protocols.jabber import jid
35 from twisted.internet import defer 36 from twisted.internet import defer
36 from twisted.python import log 37 from twisted.python import log
37 38
38 from wokkel.subprotocols import IQHandlerMixin, XMPPHandler 39 from wokkel import subprotocols
39 from wokkel import disco, data_form, delay 40 from wokkel import disco
41 from wokkel import data_form
42 from wokkel import delay
40 43
41 import rsm 44 import rsm
42 45
43 NS_MAM = 'urn:xmpp:mam:1' 46 NS_MAM = 'urn:xmpp:mam:1'
44 NS_FORWARD = 'urn:xmpp:forward:0' 47 NS_FORWARD = 'urn:xmpp:forward:0'
48 PREFS_GET_REQUEST = "/iq[@type='get']/prefs[@xmlns='%s']" % NS_MAM 51 PREFS_GET_REQUEST = "/iq[@type='get']/prefs[@xmlns='%s']" % NS_MAM
49 PREFS_SET_REQUEST = "/iq[@type='set']/prefs[@xmlns='%s']" % NS_MAM 52 PREFS_SET_REQUEST = "/iq[@type='set']/prefs[@xmlns='%s']" % NS_MAM
50 53
51 # TODO: add the tests! 54 # TODO: add the tests!
52 55
53
54 class MAMError(Exception): 56 class MAMError(Exception):
55 """ 57 """
56 MAM error. 58 MAM error.
57 """ 59 """
58
59 60
60 class Unsupported(MAMError): 61 class Unsupported(MAMError):
61 def __init__(self, feature, text=None): 62 def __init__(self, feature, text=None):
62 self.feature = feature 63 self.feature = feature
63 MAMError.__init__(self, 'feature-not-implemented', 64 MAMError.__init__(self, 'feature-not-implemented',
69 message = MAMError.__str__(self) 70 message = MAMError.__str__(self)
70 message += ', feature %r' % self.feature 71 message += ', feature %r' % self.feature
71 return message 72 return message
72 73
73 74
74 class MAMQueryRequest(): 75 class MAMRequest(object):
75 """ 76 """
76 A Message Archive Management <query/> request. 77 A Message Archive Management <query/> request.
77 78
78 @ivar form: Data Form specifing the filters. 79 @ivar form: Data Form specifing the filters.
79 @itype form: L{data_form.Form} 80 @itype form: L{data_form.Form}
96 self.rsm = rsm_ 97 self.rsm = rsm_
97 self.node = node 98 self.node = node
98 self.query_id = query_id 99 self.query_id = query_id
99 100
100 @classmethod 101 @classmethod
101 def parse(cls, element): 102 def fromElement(cls, element):
102 """Parse the DOM representation of a MAM <query/> request. 103 """Parse the DOM representation of a MAM <query/> request.
103 104
104 @param element: MAM <query/> request element. 105 @param element: element containing a MAM <query/>.
105 @type element: L{Element<twisted.words.xish.domish.Element>} 106 @type element: L{Element<twisted.words.xish.domish.Element>}
106 107
107 @return: MAMQueryRequest instance. 108 @return: MAMRequest instance.
108 @rtype: L{MAMQueryRequest} 109 @rtype: L{MAMRequest}
109 """ 110 """
110 if element.uri != NS_MAM or element.name != 'query':
111 raise MAMError('Element provided is not a MAM <query/> request')
112 form = data_form.findForm(element, NS_MAM)
113 try: 111 try:
114 rsm_request = rsm.RSMRequest.parse(element) 112 query = element.elements(NS_MAM, 'query').next()
113 except StopIteration:
114 raise MAMError("Can't find MAM <query/> in element")
115 form = data_form.findForm(query, NS_MAM)
116 try:
117 rsm_request = rsm.RSMRequest.fromElement(query)
115 except rsm.RSMNotFoundError: 118 except rsm.RSMNotFoundError:
116 rsm_request = None 119 rsm_request = None
117 node = element.getAttribute('node') 120 node = query.getAttribute('node')
118 query_id = element.getAttribute('queryid') 121 query_id = query.getAttribute('queryid')
119 return MAMQueryRequest(form, rsm_request, node, query_id) 122 return MAMRequest(form, rsm_request, node, query_id)
120 123
121 def toElement(self): 124 def toElement(self):
122 """ 125 """
123 Return the DOM representation of this RSM <query/> request. 126 Return the DOM representation of this RSM <query/> request.
124 127
149 mam_elt = self.toElement() 152 mam_elt = self.toElement()
150 parent.addChild(mam_elt) 153 parent.addChild(mam_elt)
151 return mam_elt 154 return mam_elt
152 155
153 156
154 class MAMPrefs(): 157 class MAMPrefs(object):
155 """ 158 """
156 A Message Archive Management <prefs/> request. 159 A Message Archive Management <prefs/> request.
157 160
158 @param default: A value in ('always', 'never', 'roster'). 161 @param default: A value in ('always', 'never', 'roster').
159 @type : C{unicode} or C{None} 162 @type : C{unicode} or C{None}
178 else: 181 else:
179 never = [] 182 never = []
180 self.never = never 183 self.never = never
181 184
182 @classmethod 185 @classmethod
183 def parse(cls, prefs_elt): 186 def fromElement(cls, prefs_elt):
184 """Parse the DOM representation of a MAM <prefs/> request. 187 """Parse the DOM representation of a MAM <prefs/> request.
185 188
186 @param prefs_elt: MAM <prefs/> request element. 189 @param prefs_elt: MAM <prefs/> request element.
187 @type prefs_elt: L{Element<twisted.words.xish.domish.Element>} 190 @type prefs_elt: L{Element<twisted.words.xish.domish.Element>}
188 191
239 mam_elt = self.toElement() 242 mam_elt = self.toElement()
240 parent.addChild(mam_elt) 243 parent.addChild(mam_elt)
241 return mam_elt 244 return mam_elt
242 245
243 246
244 class MAMClient(XMPPHandler): 247 class MAMClient(subprotocols.XMPPHandler):
245 """ 248 """
246 MAM client. 249 MAM client.
247 250
248 This handler implements the protocol for sending out MAM requests. 251 This handler implements the protocol for sending out MAM requests.
249 """ 252 """
250 253
251 def queryArchive(self, service=None, form=None, rsm=None, node=None, sender=None, query_id=None): 254 def queryArchive(self, mam_query, service=None, sender=None):
252 """Query a user, MUC or pubsub archive. 255 """Query a user, MUC or pubsub archive.
253 256
254 @param service: Entity offering the MAM service (None for user archives). 257 @param mam_query: query to use
258 @type form: L{MAMRequest}
259
260 @param service: Entity offering the MAM service (None for user server).
255 @type service: L{JID<twisted.words.protocols.jabber.jid.JID>} 261 @type service: L{JID<twisted.words.protocols.jabber.jid.JID>}
256
257 @param form: Data Form to filter the request.
258 @type form: L{Form<wokkel.data_form.Form>}
259
260 @param rsm: RSM request instance.
261 @type rsm: L{RSMRequest<wokkel.rsm.RSMRequest>}
262
263 @param node: Pubsub node to query, or None if inappropriate.
264 @type node: C{unicode}
265 262
266 @param sender: Optional sender address. 263 @param sender: Optional sender address.
267 @type sender: L{JID<twisted.words.protocols.jabber.jid.JID>} 264 @type sender: L{JID<twisted.words.protocols.jabber.jid.JID>}
268 265
269 @param query_id: Optional query id
270 @type query_id: C{unicode}
271
272 @return: A deferred that fires upon receiving a response. 266 @return: A deferred that fires upon receiving a response.
273 @rtype: L{Deferred<twisted.internet.defer.Deferred>} 267 @rtype: L{Deferred<twisted.internet.defer.Deferred>}
274 """ 268 """
275 iq = IQ(self.xmlstream, 'set') 269 iq = xmlstream.IQ(self.xmlstream, 'set')
276 MAMQueryRequest(form, rsm, node, query_id).render(iq) 270 mam_query.render(iq)
277 if sender is not None: 271 if sender is not None:
278 iq['from'] = unicode(sender) 272 iq['from'] = unicode(sender)
279 return iq.send(to=service.full() if service else None) 273 return iq.send(to=service.full() if service else None)
280 274
281 def queryFields(self, service=None, sender=None): 275 def queryFields(self, service=None, sender=None):
282 """Ask the server about additional supported fields. 276 """Ask the server about supported fields.
283 277
284 @param service: Entity offering the MAM service (None for user archives). 278 @param service: Entity offering the MAM service (None for user archives).
285 @type service: L{JID<twisted.words.protocols.jabber.jid.JID>} 279 @type service: L{JID<twisted.words.protocols.jabber.jid.JID>}
286 280
287 @param sender: Optional sender address. 281 @param sender: Optional sender address.
289 283
290 @return: data Form with the fields, or None if not found 284 @return: data Form with the fields, or None if not found
291 @rtype: L{Deferred<twisted.internet.defer.Deferred>} 285 @rtype: L{Deferred<twisted.internet.defer.Deferred>}
292 """ 286 """
293 # http://xmpp.org/extensions/xep-0313.html#query-form 287 # http://xmpp.org/extensions/xep-0313.html#query-form
294 iq = IQ(self.xmlstream, 'get') 288 iq = xmlstream.IQ(self.xmlstream, 'get')
295 MAMQueryRequest().render(iq) 289 MAMRequest().render(iq)
296 if sender is not None: 290 if sender is not None:
297 iq['from'] = unicode(sender) 291 iq['from'] = unicode(sender)
298 d = iq.send(to=service.full() if service else None) 292 d = iq.send(to=service.full() if service else None)
299 d.addCallback(lambda iq_result: iq_result.elements(NS_MAM, 'query').next()) 293 d.addCallback(lambda iq_result: iq_result.elements(NS_MAM, 'query').next())
300 d.addCallback(data_form.findForm, NS_MAM) 294 d.addCallback(data_form.findForm, NS_MAM)
311 305
312 @return: A deferred that fires upon receiving a response. 306 @return: A deferred that fires upon receiving a response.
313 @rtype: L{Deferred<twisted.internet.defer.Deferred>} 307 @rtype: L{Deferred<twisted.internet.defer.Deferred>}
314 """ 308 """
315 # http://xmpp.org/extensions/xep-0313.html#prefs 309 # http://xmpp.org/extensions/xep-0313.html#prefs
316 iq = IQ(self.xmlstream, 'get') 310 iq = xmlstream.IQ(self.xmlstream, 'get')
317 MAMPrefs().render(iq) 311 MAMPrefs().render(iq)
318 if sender is not None: 312 if sender is not None:
319 iq['from'] = unicode(sender) 313 iq['from'] = unicode(sender)
320 return iq.send(to=service.full() if service else None) 314 return iq.send(to=service.full() if service else None)
321 315
340 @return: A deferred that fires upon receiving a response. 334 @return: A deferred that fires upon receiving a response.
341 @rtype: L{Deferred<twisted.internet.defer.Deferred>} 335 @rtype: L{Deferred<twisted.internet.defer.Deferred>}
342 """ 336 """
343 # http://xmpp.org/extensions/xep-0313.html#prefs 337 # http://xmpp.org/extensions/xep-0313.html#prefs
344 assert default is not None 338 assert default is not None
345 iq = IQ(self.xmlstream, 'set') 339 iq = xmlstream.IQ(self.xmlstream, 'set')
346 MAMPrefs(default, always, never).render(iq) 340 MAMPrefs(default, always, never).render(iq)
347 if sender is not None: 341 if sender is not None:
348 iq['from'] = unicode(sender) 342 iq['from'] = unicode(sender)
349 return iq.send(to=service.full() if service else None) 343 return iq.send(to=service.full() if service else None)
350 344
353 347
354 def onArchiveRequest(self, mam, requestor): 348 def onArchiveRequest(self, mam, requestor):
355 """ 349 """
356 350
357 @param mam: The MAM <query/> request. 351 @param mam: The MAM <query/> request.
358 @type mam: L{MAMQueryReques<wokkel.mam.MAMQueryRequest>} 352 @type mam: L{MAMQueryReques<wokkel.mam.MAMRequest>}
359 353
360 @param requestor: JID of the requestor. 354 @param requestor: JID of the requestor.
361 @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>} 355 @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
362 356
363 @return: The RSM answer. 357 @return: The RSM answer.
385 379
386 @return: The new current settings. 380 @return: The new current settings.
387 @rtype: L{wokkel.mam.MAMPrefs} 381 @rtype: L{wokkel.mam.MAMPrefs}
388 """ 382 """
389 383
390 384 class IMAMService(Interface):
391 class MAMService(XMPPHandler, IQHandlerMixin): 385 """
386 Interface for XMPP MAM service.
387 """
388
389 def addFilter(self, field):
390 """
391 Add a new filter for querying MAM archive.
392
393 @param field: data form field of the filter
394 @type field: L{Form<wokkel.data_form.Field>}
395 """
396
397
398 class MAMService(subprotocols.XMPPHandler, subprotocols.IQHandlerMixin):
392 """ 399 """
393 Protocol implementation for a MAM service. 400 Protocol implementation for a MAM service.
394 401
395 This handler waits for XMPP Ping requests and sends a response. 402 This handler waits for XMPP Ping requests and sends a response.
396 """ 403 """
397 404 implements(IMAMService, disco.IDisco)
398 implements(disco.IDisco) 405
406 _request_class = MAMRequest
399 407
400 iqHandlers = {FIELDS_REQUEST: '_onFieldsRequest', 408 iqHandlers = {FIELDS_REQUEST: '_onFieldsRequest',
401 ARCHIVE_REQUEST: '_onArchiveRequest', 409 ARCHIVE_REQUEST: '_onArchiveRequest',
402 PREFS_GET_REQUEST: '_onPrefsGetRequest', 410 PREFS_GET_REQUEST: '_onPrefsGetRequest',
403 PREFS_SET_REQUEST: '_onPrefsSetRequest' 411 PREFS_SET_REQUEST: '_onPrefsSetRequest'
452 """ 460 """
453 Called when a fields request has been received. 461 Called when a fields request has been received.
454 462
455 This immediately replies with a result response. 463 This immediately replies with a result response.
456 """ 464 """
457 response = toResponse(iq, 'result')
458 query = response.addElement((NS_MAM, 'query'))
459 query.addChild(buildForm('form', extra=self.extra_fields).toElement())
460 self.xmlstream.send(response)
461 iq.handled = True 465 iq.handled = True
466 query = domish.Element((NS_MAM, 'query'))
467 query.addChild(buildForm(extra_fields=self.extra_fields).toElement(), formType='form')
468 return query
462 469
463 def _onArchiveRequest(self, iq): 470 def _onArchiveRequest(self, iq):
464 """ 471 """
465 Called when a message archive request has been received. 472 Called when a message archive request has been received.
466 473
467 This replies with the list of archived message and the <iq> result 474 This replies with the list of archived message and the <iq> result
468 @return: A tuple with list of message data (id, element, data) and RSM element 475 @return: A tuple with list of message data (id, element, data) and RSM element
469 @rtype: C{tuple} 476 @rtype: C{tuple}
470 """ 477 """
471 mam_ = MAMQueryRequest.parse(iq.query) 478 iq.handled = True
479 mam_ = self._request_class.fromElement(iq)
472 requestor = jid.JID(iq['from']) 480 requestor = jid.JID(iq['from'])
473 481
474 # remove unsupported filters 482 # remove unsupported filters
475 unsupported_fields = [] 483 unsupported_fields = []
476 if mam_.form: 484 if mam_.form:
479 log.msg('Ignored unsupported MAM filter: %s' % field) 487 log.msg('Ignored unsupported MAM filter: %s' % field)
480 unsupported_fields.append(key) 488 unsupported_fields.append(key)
481 for key in unsupported_fields: 489 for key in unsupported_fields:
482 del mam_.form.fields[key] 490 del mam_.form.fields[key]
483 491
484 def forward_message(id_, elt, date): 492 def forwardMessage(id_, elt, date):
485 msg = domish.Element((None, 'message')) 493 msg = domish.Element((None, 'message'))
486 msg['to'] = iq['from'] 494 msg['to'] = iq['from']
487 result = msg.addElement((NS_MAM, 'result')) 495 result = msg.addElement((NS_MAM, 'result'))
488 try: 496 if mam_.query_id is not None:
489 result['queryid'] = iq.query['queryid'] 497 result['queryid'] = mam_.query_id
490 except KeyError:
491 pass
492 result['id'] = id_ 498 result['id'] = id_
493 forward = result.addElement((NS_FORWARD, 'forwarded')) 499 forward = result.addElement((NS_FORWARD, 'forwarded'))
494 forward.addChild(delay.Delay(date).toElement()) 500 forward.addChild(delay.Delay(date).toElement())
495 forward.addChild(elt) 501 forward.addChild(elt)
496 self.xmlstream.send(msg) 502 self.xmlstream.send(msg)
497 503
498 def cb(result): 504 def cb(result):
499 msg_data, rsm_elt = result 505 msg_data, rsm_elt = result
500 for data in msg_data: 506 for data in msg_data:
501 forward_message(*data) 507 forwardMessage(*data)
502 508
503 response = toResponse(iq, 'result') 509 fin_elt = domish.Element((NS_MAM, 'fin'))
504 fin = response.addElement((NS_MAM, 'fin'))
505 510
506 if rsm_elt is not None: 511 if rsm_elt is not None:
507 fin.addChild(rsm_elt) 512 fin_elt.addChild(rsm_elt)
508 self.xmlstream.send(response) 513 return fin_elt
509 514
510 d = defer.maybeDeferred(self.resource.onArchiveRequest, mam_, requestor) 515 d = defer.maybeDeferred(self.resource.onArchiveRequest, mam_, requestor)
511 d.addCallback(cb) 516 d.addCallback(cb)
517 return d
518
519 def _onPrefsGetRequest(self, iq):
520 """
521 Called when a prefs get request has been received.
522
523 This immediately replies with a result response.
524 """
512 iq.handled = True 525 iq.handled = True
513 526 requestor = jid.JID(iq['from'])
514 def _onPrefsGetRequest(self, iq): 527
528 def cb(prefs):
529 return prefs.toElement()
530
531 d = self.resource.onPrefsGetRequest(requestor).addCallback(cb)
532 return d
533
534 def _onPrefsSetRequest(self, iq):
515 """ 535 """
516 Called when a prefs get request has been received. 536 Called when a prefs get request has been received.
517 537
518 This immediately replies with a result response. 538 This immediately replies with a result response.
519 """ 539 """
520 response = toResponse(iq, 'result') 540 iq.handled = True
521 541
542 prefs = MAMPrefs.fromElement(iq.prefs)
522 requestor = jid.JID(iq['from']) 543 requestor = jid.JID(iq['from'])
523 544
524 def cb(prefs): 545 def cb(prefs):
525 response.addChild(prefs.toElement()) 546 return prefs.toElement()
526 self.xmlstream.send(response) 547
527 548 d = self.resource.onPrefsSetRequest(prefs, requestor).addCallback(cb)
528 self.resource.onPrefsGetRequest(requestor).addCallback(cb) 549 return d
529 iq.handled = True
530
531 def _onPrefsSetRequest(self, iq):
532 """
533 Called when a prefs get request has been received.
534
535 This immediately replies with a result response.
536 """
537 response = toResponse(iq, 'result')
538
539 prefs = MAMPrefs.parse(iq.prefs)
540 requestor = jid.JID(iq['from'])
541
542 def cb(prefs):
543 response.addChild(prefs.toElement())
544 self.xmlstream.send(response)
545
546 self.resource.onPrefsSetRequest(prefs, requestor).addCallback(cb)
547 iq.handled = True
548 550
549 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): 551 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
550 if nodeIdentifier: 552 if nodeIdentifier:
551 return [] 553 return []
552 return [disco.DiscoFeature(NS_MAM)] 554 return [disco.DiscoFeature(NS_MAM)]
553 555
554 def getDiscoItems(self, requestor, target, nodeIdentifier=''): 556 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
555 return [] 557 return []
556 558
557 559
558 def datetime2utc(datetime): 560 def datetime2utc(datetime_obj):
559 """Convert a datetime to a XEP-0082 compliant UTC datetime. 561 """Convert a datetime to a XEP-0082 compliant UTC datetime.
560 562
561 @param datetime: Offset-aware timestamp to convert. 563 @param datetime_obj: Offset-aware timestamp to convert.
562 @type datetime: L{datetime<datetime.datetime>} 564 @type datetime_obj: L{datetime<datetime.datetime>}
563 565
564 @return: The datetime converted to UTC. 566 @return: The datetime converted to UTC.
565 @rtype: C{unicode} 567 @rtype: C{unicode}
566 """ 568 """
567 stampFormat = '%Y-%m-%dT%H:%M:%SZ' 569 stampFormat = '%Y-%m-%dT%H:%M:%SZ'
568 return datetime.astimezone(tzutc()).strftime(stampFormat) 570 return datetime_obj.astimezone(tz.tzutc()).strftime(stampFormat)
569 571
570 572
571 def buildForm(formType='submit', start=None, end=None, with_jid=None, extra_fields=None): 573 def buildForm(start=None, end=None, with_jid=None, extra_fields=None, formType='submit'):
572 """Prepare a Data Form for MAM. 574 """Prepare a Data Form for MAM.
573
574 @param formType: The type of the Data Form ('submit' or 'form').
575 @type formType: C{unicode}
576 575
577 @param start: Offset-aware timestamp to filter out older messages. 576 @param start: Offset-aware timestamp to filter out older messages.
578 @type start: L{datetime<datetime.datetime>} 577 @type start: L{datetime<datetime.datetime>}
579 578
580 @param end: Offset-aware timestamp to filter out later messages. 579 @param end: Offset-aware timestamp to filter out later messages.
584 @type with_jid: L{JID<twisted.words.protocols.jabber.jid.JID>} 583 @type with_jid: L{JID<twisted.words.protocols.jabber.jid.JID>}
585 584
586 @param extra_fields: list of extra data form fields that are not defined by the 585 @param extra_fields: list of extra data form fields that are not defined by the
587 specification. 586 specification.
588 @type: C{list} 587 @type: C{list}
588
589 @param formType: The type of the Data Form ('submit' or 'form').
590 @type formType: C{unicode}
589 591
590 @return: XEP-0004 Data Form object. 592 @return: XEP-0004 Data Form object.
591 @rtype: L{Form<wokkel.data_form.Form>} 593 @rtype: L{Form<wokkel.data_form.Form>}
592 """ 594 """
593 form = data_form.Form(formType, formNamespace=NS_MAM) 595 form = data_form.Form(formType, formNamespace=NS_MAM)