Mercurial > sat_tmp
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 |