comparison wokkel/mam.py @ 1:9d35f88168a1

tmp: update tmp.wokkel.rsm, add tmp.wokkel.mam
author souliane <souliane@mailoo.org>
date Fri, 09 Jan 2015 10:50:11 +0100
parents
children 4369549244e8
comparison
equal deleted inserted replaced
0:09e7c32a6a00 1:9d35f88168a1
1 # -*- test-case-name: wokkel.test.test_mam -*-
2 #
3 # Copyright (c) Adrien Cossa.
4 # See LICENSE for details.
5
6 """
7 XMPP Message Archive Management protocol.
8
9 This protocol is specified in
10 U{XEP-0313<http://xmpp.org/extensions/xep-0313.html>}.
11 """
12
13 from dateutil.tz import tzutc
14 from zope.interface import Interface, implements
15 from twisted.words.protocols.jabber.xmlstream import IQ, toResponse
16 from twisted.words.xish import domish
17 from twisted.words.protocols.jabber import jid
18 from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
19 from wokkel import disco, rsm, data_form
20
21
22 NS_MAM = 'urn:xmpp:mam:0'
23
24 FIELDS_REQUEST = "/iq[@type='get']/query[@xmlns='%s']" % NS_MAM
25 ARCHIVE_REQUEST = "/iq[@type='set']/query[@xmlns='%s']" % NS_MAM
26 PREFS_GET_REQUEST = "/iq[@type='get']/prefs[@xmlns='%s']" % NS_MAM
27 PREFS_SET_REQUEST = "/iq[@type='set']/prefs[@xmlns='%s']" % NS_MAM
28
29 # TODO: add the tests!
30
31
32 class MAMError(Exception):
33 """
34 MAM error.
35 """
36
37
38 class Unsupported(MAMError):
39 def __init__(self, feature, text=None):
40 self.feature = feature
41 MAMError.__init__(self, 'feature-not-implemented',
42 'unsupported',
43 feature,
44 text)
45
46 def __str__(self):
47 message = MAMError.__str__(self)
48 message += ', feature %r' % self.feature
49 return message
50
51
52 class MAMQueryRequest():
53 """
54 A Message Archive Management <query/> request.
55
56 @ivar form: Data Form specifing the filters.
57 @itype form: L{data_form.Form}
58
59 @ivar rsm: RSM request instance.
60 @itype rsm: L{rsm.RSMRequest}
61
62 @ivar node: pubsub node id if querying a pubsub node, else None.
63 @itype form: C{unicode}
64 """
65
66 form = None
67 rsm = None
68 node = None
69
70 def __init__(self, form=None, rsm=None, node=None):
71 self.form = form
72 self.rsm = rsm
73 self.node = node
74
75 @classmethod
76 def parse(cls, element):
77 """Parse the DOM representation of a MAM <query/> request.
78
79 @param element: MAM <query/> request element.
80 @type element: L{Element<twisted.words.xish.domish.Element>}
81
82 @return: MAMQueryRequest instance.
83 @rtype: L{MAMQueryRequest}
84 """
85 if element.uri != NS_MAM or element.name != 'query':
86 raise MAMError('Element provided is not a MAM <query/> request')
87 form = data_form.findForm(element, NS_MAM)
88 try:
89 rsm_request = rsm.RSMRequest.parse(element)
90 except rsm.RSMNotFoundError:
91 rsm_request = None
92 node = element['node'] if element.hasAttribute('node') else None
93 return MAMQueryRequest(form, rsm_request, node)
94
95 def toElement(self):
96 """
97 Return the DOM representation of this RSM <query/> request.
98
99 @rtype: L{Element<twisted.words.xish.domish.Element>}
100 """
101 mam_elt = domish.Element((NS_MAM, 'query'))
102 if self.node is not None:
103 mam_elt['node'] = self.node
104 if self.form is not None:
105 mam_elt.addChild(self.form.toElement())
106 if self.rsm is not None:
107 mam_elt.addChild(self.rsm.toElement())
108
109 return mam_elt
110
111 def render(self, parent):
112 """Embed the DOM representation of this MAM request in the given element.
113
114 @param parent: parent IQ element.
115 @type parent: L{Element<twisted.words.xish.domish.Element>}
116
117 @return: MAM request element.
118 @rtype: L{Element<twisted.words.xish.domish.Element>}
119 """
120 assert(parent.name == 'iq')
121 mam_elt = self.toElement()
122 parent.addChild(mam_elt)
123 return mam_elt
124
125
126 class MAMPrefs():
127 """
128 A Message Archive Management <prefs/> request.
129
130 @param default: A value in ('always', 'never', 'roster').
131 @type : C{unicode}
132
133 @param always (list): A list of JID instances.
134 @type always: C{list}
135
136 @param never (list): A list of JID instances.
137 @type never: C{list}
138 """
139
140 default = None
141 always = None
142 never = None
143
144 def __init__(self, default=None, always=None, never=None):
145 if default:
146 assert(default in ('always', 'never', 'roster'))
147 self.default = default
148 if always:
149 assert(isinstance(always, list))
150 self.always = always
151 else:
152 self.always = []
153 if never:
154 assert(isinstance(never, list))
155 self.never = never
156 else:
157 self.never = []
158
159 @classmethod
160 def parse(cls, element):
161 """Parse the DOM representation of a MAM <prefs/> request.
162
163 @param element: MAM <prefs/> request element.
164 @type element: L{Element<twisted.words.xish.domish.Element>}
165
166 @return: MAMPrefs instance.
167 @rtype: L{MAMPrefs}
168 """
169 if element.uri != NS_MAM or element.name != 'prefs':
170 raise MAMError('Element provided is not a MAM <prefs/> request')
171 default = element['default'] if element.hasAttribute('default') else None
172 prefs = {}
173 for attr in ('always', 'never'):
174 prefs[attr] = []
175 try:
176 pref = domish.generateElementsNamed(element.elements(), attr).next()
177 for jid_s in domish.generateElementsNamed(pref.elements(), 'jid'):
178 prefs[attr].append(jid.JID(jid_s))
179 except StopIteration:
180 pass
181 return MAMPrefs(default, **prefs)
182
183 def toElement(self):
184 """
185 Return the DOM representation of this RSM <prefs/>request.
186
187 @rtype: L{Element<twisted.words.xish.domish.Element>}
188 """
189 mam_elt = domish.Element((NS_MAM, 'prefs'))
190 if self.default:
191 mam_elt['default'] = self.default
192 for attr in ('always', 'never'):
193 attr_elt = mam_elt.addElement(attr)
194 jids = getattr(self, attr)
195 for jid in jids:
196 attr_elt.addElement('jid', content=jid.full())
197 return mam_elt
198
199 def render(self, parent):
200 """Embed the DOM representation of this MAM request in the given element.
201
202 @param parent: parent IQ element.
203 @type parent: L{Element<twisted.words.xish.domish.Element>}
204
205 @return: MAM request element.
206 @rtype: L{Element<twisted.words.xish.domish.Element>}
207 """
208 assert(parent.name == 'iq')
209 mam_elt = self.toElement()
210 parent.addChild(mam_elt)
211 return mam_elt
212
213
214 class MAMClient(XMPPHandler):
215 """
216 MAM client.
217
218 This handler implements the protocol for sending out MAM requests.
219 """
220
221 def queryArchive(self, service=None, form=None, rsm=None, node=None, sender=None):
222 """Query a user, MUC or pubsub archive.
223
224 @param service: Entity offering the MAM service (None for user archives).
225 @type service: L{JID<twisted.words.protocols.jabber.jid.JID>}
226
227 @param form: Data Form to filter the request.
228 @type form: L{Form<wokkel.data_form.Form>}
229
230 @param rsm: RSM request instance.
231 @type rsm: L{RSMRequest<wokkel.rsm.RSMRequest>}
232
233 @param node: Pubsub node to query, or None if inappropriate.
234 @type node: C{unicode}
235
236 @param sender: Optional sender address.
237 @type sender: L{JID<twisted.words.protocols.jabber.jid.JID>}
238
239 @return: A deferred that fires upon receiving a response.
240 @rtype: L{Deferred<twisted.internet.defer.Deferred>}
241 """
242 iq = IQ(self.xmlstream, 'set')
243 MAMQueryRequest(form, rsm, node).render(iq)
244 if sender is not None:
245 iq['from'] = unicode(sender)
246 return iq.send(to=service.full() if service else None)
247
248 def queryFields(self, service=None, sender=None):
249 """Ask the server about additional supported fields.
250
251 @param service: Entity offering the MAM service (None for user archives).
252 @type service: L{JID<twisted.words.protocols.jabber.jid.JID>}
253
254 @param sender: Optional sender address.
255 @type sender: L{JID<twisted.words.protocols.jabber.jid.JID>}
256
257 @return: A deferred that fires upon receiving a response.
258 @rtype: L{Deferred<twisted.internet.defer.Deferred>}
259 """
260 # http://xmpp.org/extensions/xep-0313.html#query-form
261 iq = IQ(self.xmlstream, 'get')
262 MAMQueryRequest().render(iq)
263 if sender is not None:
264 iq['from'] = unicode(sender)
265 return iq.send(to=service.full() if service else None)
266
267 def queryPrefs(self, service=None, sender=None):
268 """Retrieve the current user preferences.
269
270 @param service: Entity offering the MAM service (None for user archives).
271 @type service: L{JID<twisted.words.protocols.jabber.jid.JID>}
272
273 @param sender: Optional sender address.
274 @type sender: L{JID<twisted.words.protocols.jabber.jid.JID>}
275
276 @return: A deferred that fires upon receiving a response.
277 @rtype: L{Deferred<twisted.internet.defer.Deferred>}
278 """
279 # http://xmpp.org/extensions/xep-0313.html#prefs
280 iq = IQ(self.xmlstream, 'get')
281 MAMPrefs().render(iq)
282 if sender is not None:
283 iq['from'] = unicode(sender)
284 return iq.send(to=service.full() if service else None)
285
286 def setPrefs(self, service=None, default='roster', always=None, never=None, sender=None):
287 """Set new user preferences.
288
289 @param service: Entity offering the MAM service (None for user archives).
290 @type service: L{JID<twisted.words.protocols.jabber.jid.JID>}
291
292 @param default: A value in ('always', 'never', 'roster').
293 @type : C{unicode}
294
295 @param always (list): A list of JID instances.
296 @type always: C{list}
297
298 @param never (list): A list of JID instances.
299 @type never: C{list}
300
301 @param sender: Optional sender address.
302 @type sender: L{JID<twisted.words.protocols.jabber.jid.JID>}
303
304 @return: A deferred that fires upon receiving a response.
305 @rtype: L{Deferred<twisted.internet.defer.Deferred>}
306 """
307 # http://xmpp.org/extensions/xep-0313.html#prefs
308 assert(default is not None)
309 iq = IQ(self.xmlstream, 'set')
310 MAMPrefs(default, always, never).render(iq)
311 if sender is not None:
312 iq['from'] = unicode(sender)
313 return iq.send(to=service.full() if service else None)
314
315
316 class IMAMResource(Interface):
317
318 def onArchiveRequest(self, mam, rsm, requestor):
319 """
320
321 @param mam: The MAM <query/> request.
322 @type mam: L{MAMQueryReques<wokkel.mam.MAMQueryRequest>}
323
324 @param rsm: The RSM request.
325 @type rsm: L{RSMRequest<wokkel.rsm.RSMRequest>}
326
327 @param requestor: JID of the requestor.
328 @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
329
330 @return: The RSM answer.
331 @rtype: L{RSMResponse<wokkel.rsm.RSMResponse>}
332 """
333
334 def onPrefsGetRequest(self, requestor):
335 """
336
337 @param requestor: JID of the requestor.
338 @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
339
340 @return: The current settings.
341 @rtype: L{wokkel.mam.MAMPrefs}
342 """
343
344 def onPrefsSetRequest(self, prefs, requestor):
345 """
346
347 @param prefs: The new settings to set.
348 @type prefs: L{wokkel.mam.MAMPrefs}
349
350 @param requestor: JID of the requestor.
351 @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>}
352
353 @return: The new current settings.
354 @rtype: L{wokkel.mam.MAMPrefs}
355 """
356
357
358 class MAMService(XMPPHandler, IQHandlerMixin):
359 """
360 Protocol implementation for a MAM service.
361
362 This handler waits for XMPP Ping requests and sends a response.
363 """
364
365 implements(disco.IDisco)
366
367 iqHandlers = {FIELDS_REQUEST: '_onFieldsRequest',
368 ARCHIVE_REQUEST: '_onArchiveRequest',
369 PREFS_GET_REQUEST: '_onPrefsGetRequest',
370 PREFS_SET_REQUEST: '_onPrefsSetRequest'
371 }
372
373 _legacyFilters = {'start': {'fieldType': 'text-single',
374 'var': 'start',
375 'label': 'Starting time',
376 'desc': 'Starting time a the result period.',
377 },
378 'end': {'fieldType': 'text-single',
379 'var': 'end',
380 'label': 'Ending time',
381 'desc': 'Ending time of the result period.',
382 },
383 'with': {'fieldType': 'jid-single',
384 'var': 'with',
385 'label': 'Entity',
386 'desc': 'Entity against which to match message.',
387 },
388 }
389
390 extra_filters = []
391
392 def __init__(self, resource):
393 """
394 @param resource: instance implementing IMAMResource
395 @type resource: L{object}
396 """
397 self.resource = resource
398
399 def connectionInitialized(self):
400 """
401 Called when the XML stream has been initialized.
402
403 This sets up an observer for incoming ping requests.
404 """
405 self.xmlstream.addObserver(FIELDS_REQUEST, self.handleRequest)
406 self.xmlstream.addObserver(ARCHIVE_REQUEST, self.handleRequest)
407 self.xmlstream.addObserver(PREFS_GET_REQUEST, self.handleRequest)
408 self.xmlstream.addObserver(PREFS_SET_REQUEST, self.handleRequest)
409
410 def addFilter(self, field):
411 """
412 Add a new filter for querying MAM archive.
413
414 @param field: dictionary specifying the attributes to build a
415 wokkel.data_form.Field.
416 @type field: C{dict}
417 """
418 self.extra_filters.append(field)
419
420 def _onFieldsRequest(self, iq):
421 """
422 Called when a fields request has been received.
423
424 This immediately replies with a result response.
425 """
426 response = toResponse(iq, 'result')
427 query = response.addElement((NS_MAM, 'query'))
428 query.addChild(buildForm('form', extra=self.extra_filters).toElement())
429 self.xmlstream.send(response)
430 iq.handled = True
431
432 def _onArchiveRequest(self, iq):
433 """
434 Called when a message archive request has been received.
435
436 This immediately replies with a result response, followed by the
437 list of archived message and the finally the <fin/> message.
438 """
439 response = toResponse(iq, 'result')
440 self.xmlstream.send(response)
441
442 mam_ = MAMQueryRequest.parse(iq.query)
443 try:
444 rsm_ = rsm.RSMRequest.parse(iq.query)
445 except rsm.RSMNotFoundError:
446 rsm_ = None
447 requestor = jid.JID(iq['from'])
448 rsm_response = self.resource.onArchiveRequest(mam_, rsm_, requestor)
449
450 msg = domish.Element((None, 'message'))
451 fin = msg.addElement('fin', NS_MAM)
452 if iq.hasAttribute('queryid'):
453 fin['queryid'] = iq.query['queryid']
454 if rsm_response is not None:
455 fin.addChild(rsm_response.toElement())
456 self.xmlstream.send(msg)
457
458 iq.handled = True
459
460 def _onPrefsGetRequest(self, iq):
461 """
462 Called when a prefs get request has been received.
463
464 This immediately replies with a result response.
465 """
466 response = toResponse(iq, 'result')
467
468 requestor = jid.JID(iq['from'])
469 prefs = self.resource.onPrefsGetRequest(requestor)
470
471 response.addChild(prefs.toElement())
472 self.xmlstream.send(response)
473 iq.handled = True
474
475 def _onPrefsSetRequest(self, iq):
476 """
477 Called when a prefs get request has been received.
478
479 This immediately replies with a result response.
480 """
481 response = toResponse(iq, 'result')
482
483 prefs = MAMPrefs.parse(iq.prefs)
484 requestor = jid.JID(iq['from'])
485 prefs = self.resource.onPrefsSetRequest(prefs, requestor)
486
487 response.addChild(prefs.toElement())
488 self.xmlstream.send(response)
489 iq.handled = True
490
491 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
492 return [disco.DiscoFeature(NS_MAM)]
493
494 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
495 return []
496
497
498 def datetime2utc(datetime):
499 """Convert a datetime to a XEP-0082 compliant UTC datetime.
500
501 @param datetime: Offset-aware timestamp to convert.
502 @type datetime: L{datetime<datetime.datetime>}
503
504 @return: The datetime converted to UTC.
505 @rtype: C{unicode}
506 """
507 stampFormat = '%Y-%m-%dT%H:%M:%SZ'
508 return datetime.astimezone(tzutc()).strftime(stampFormat)
509
510
511 def buildForm(formType='submit', start=None, end=None, with_jid=None, extra=None):
512 """Prepare a Data Form for MAM.
513
514 @param formType: The type of the Data Form ('submit' or 'form').
515 @type formType: C{unicode}
516
517 @param start: Offset-aware timestamp to filter out older messages.
518 @type start: L{datetime<datetime.datetime>}
519
520 @param end: Offset-aware timestamp to filter out later messages.
521 @type end: L{datetime<datetime.datetime>}
522
523 @param with_jid: JID against which to match messages.
524 @type with_jid: L{JID<twisted.words.protocols.jabber.jid.JID>}
525
526 @param extra: list of extra fields that are not defined by the
527 specification. Each element of the list must be a dictionary
528 specifying the attributes to build a wokkel.data_form.Field.
529 @type: C{list}
530
531 @return: XEP-0004 Data Form object.
532 @rtype: L{Form<wokkel.data_form.Form>}
533 """
534 form = data_form.Form(formType, formNamespace=NS_MAM)
535 filters = []
536
537 if formType == 'form':
538 filters.extend(MAMService._legacyFilters.values())
539 elif formType == 'submit':
540 if start:
541 filters.append({'var': 'start', 'value': datetime2utc(start)})
542 if end:
543 filters.append({'var': 'end', 'value': datetime2utc(end)})
544 if with_jid:
545 # must specify the field type to overwrite default value in Field.__init__
546 filters.append({'fieldType': 'jid-single', 'var': 'with', 'value': with_jid.full()})
547
548 if extra is not None:
549 filters.extend(extra)
550
551 for field in filters:
552 form.addField(data_form.Field(**field))
553
554 return form