Mercurial > libervia-backend
comparison sat/core/xmpp.py @ 3028:ab2696e34d29
Python 3 port:
/!\ this is a huge commit
/!\ starting from this commit, SàT is needs Python 3.6+
/!\ SàT maybe be instable or some feature may not work anymore, this will improve with time
This patch port backend, bridge and frontends to Python 3.
Roughly this has been done this way:
- 2to3 tools has been applied (with python 3.7)
- all references to python2 have been replaced with python3 (notably shebangs)
- fixed files not handled by 2to3 (notably the shell script)
- several manual fixes
- fixed issues reported by Python 3 that where not handled in Python 2
- replaced "async" with "async_" when needed (it's a reserved word from Python 3.7)
- replaced zope's "implements" with @implementer decorator
- temporary hack to handle data pickled in database, as str or bytes may be returned,
to be checked later
- fixed hash comparison for password
- removed some code which is not needed anymore with Python 3
- deactivated some code which needs to be checked (notably certificate validation)
- tested with jp, fixed reported issues until some basic commands worked
- ported Primitivus (after porting dependencies like urwid satext)
- more manual fixes
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 13 Aug 2019 19:08:41 +0200 |
parents | 94708a7d3ecf |
children | fee60f17ebac |
comparison
equal
deleted
inserted
replaced
3027:ff5bcb12ae60 | 3028:ab2696e34d29 |
---|---|
1 #!/usr/bin/env python2 | 1 #!/usr/bin/env python3 |
2 # -*- coding: utf-8 -*- | 2 # -*- coding: utf-8 -*- |
3 | 3 |
4 # SAT: a jabber client | 4 # SAT: a jabber client |
5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org) | 5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org) |
6 | 6 |
38 from sat.core.log import getLogger | 38 from sat.core.log import getLogger |
39 from sat.core import exceptions | 39 from sat.core import exceptions |
40 from sat.memory import encryption | 40 from sat.memory import encryption |
41 from sat.memory import persistent | 41 from sat.memory import persistent |
42 from sat.tools import xml_tools | 42 from sat.tools import xml_tools |
43 from zope.interface import implements | 43 from zope.interface import implementer |
44 | 44 |
45 log = getLogger(__name__) | 45 log = getLogger(__name__) |
46 | 46 |
47 | 47 |
48 NS_X_DATA = u"jabber:x:data" | 48 NS_X_DATA = "jabber:x:data" |
49 NS_DISCO_INFO = u"http://jabber.org/protocol/disco#info" | 49 NS_DISCO_INFO = "http://jabber.org/protocol/disco#info" |
50 NS_XML_ELEMENT = u"urn:xmpp:xml-element" | 50 NS_XML_ELEMENT = "urn:xmpp:xml-element" |
51 NS_ROSTER_VER = u"urn:xmpp:features:rosterver" | 51 NS_ROSTER_VER = "urn:xmpp:features:rosterver" |
52 # we use 2 "@" which is illegal in a jid, to be sure we are not mixing keys | 52 # we use 2 "@" which is illegal in a jid, to be sure we are not mixing keys |
53 # with roster jids | 53 # with roster jids |
54 ROSTER_VER_KEY = u"@version@" | 54 ROSTER_VER_KEY = "@version@" |
55 | 55 |
56 | 56 |
57 class SatXMPPEntity(object): | 57 class SatXMPPEntity(object): |
58 """Common code for Client and Component""" | 58 """Common code for Client and Component""" |
59 | 59 |
63 # we monkey patch clientConnectionLost to handle networkEnabled/networkDisabled | 63 # we monkey patch clientConnectionLost to handle networkEnabled/networkDisabled |
64 # and to allow plugins to tune reconnection mechanism | 64 # and to allow plugins to tune reconnection mechanism |
65 clientConnectionFailed_ori = factory.clientConnectionFailed | 65 clientConnectionFailed_ori = factory.clientConnectionFailed |
66 clientConnectionLost_ori = factory.clientConnectionLost | 66 clientConnectionLost_ori = factory.clientConnectionLost |
67 factory.clientConnectionFailed = partial( | 67 factory.clientConnectionFailed = partial( |
68 self.connectionTerminated, term_type=u"failed", cb=clientConnectionFailed_ori) | 68 self.connectionTerminated, term_type="failed", cb=clientConnectionFailed_ori) |
69 factory.clientConnectionLost = partial( | 69 factory.clientConnectionLost = partial( |
70 self.connectionTerminated, term_type=u"lost", cb=clientConnectionLost_ori) | 70 self.connectionTerminated, term_type="lost", cb=clientConnectionLost_ori) |
71 | 71 |
72 factory.maxRetries = max_retries | 72 factory.maxRetries = max_retries |
73 factory.maxDelay = 30 | 73 factory.maxDelay = 30 |
74 # when self._connected_d is None, we are not connected | 74 # when self._connected_d is None, we are not connected |
75 # else, it's a deferred which fire on disconnection | 75 # else, it's a deferred which fire on disconnection |
85 # (key = progress id) | 85 # (key = progress id) |
86 self.actions = {} # used to keep track of actions for retrieval (key = action_id) | 86 self.actions = {} # used to keep track of actions for retrieval (key = action_id) |
87 self.encryption = encryption.EncryptionHandler(self) | 87 self.encryption = encryption.EncryptionHandler(self) |
88 | 88 |
89 def __unicode__(self): | 89 def __unicode__(self): |
90 return u"Client instance for profile {profile}".format(profile=self.profile) | 90 return "Client instance for profile {profile}".format(profile=self.profile) |
91 | 91 |
92 def __str__(self): | 92 def __str__(self): |
93 return self.__unicode__.encode('utf-8') | 93 return self.__unicode__.encode('utf-8') |
94 | 94 |
95 ## initialisation ## | 95 ## initialisation ## |
204 list_d = defer.DeferredList(conn_cb_list) | 204 list_d = defer.DeferredList(conn_cb_list) |
205 | 205 |
206 def logPluginResults(results): | 206 def logPluginResults(results): |
207 all_succeed = all([success for success, result in results]) | 207 all_succeed = all([success for success, result in results]) |
208 if not all_succeed: | 208 if not all_succeed: |
209 log.error(_(u"Plugins initialisation error")) | 209 log.error(_("Plugins initialisation error")) |
210 for idx, (success, result) in enumerate(results): | 210 for idx, (success, result) in enumerate(results): |
211 if not success: | 211 if not success: |
212 log.error( | 212 log.error( |
213 u"error (plugin %(name)s): %(failure)s" | 213 "error (plugin %(name)s): %(failure)s" |
214 % { | 214 % { |
215 "name": plugin_conn_cb[idx][0]._info["import_name"], | 215 "name": plugin_conn_cb[idx][0]._info["import_name"], |
216 "failure": result, | 216 "failure": result, |
217 } | 217 } |
218 ) | 218 ) |
224 | 224 |
225 def _disconnectionCb(self, __): | 225 def _disconnectionCb(self, __): |
226 self._connected_d = None | 226 self._connected_d = None |
227 | 227 |
228 def _disconnectionEb(self, failure_): | 228 def _disconnectionEb(self, failure_): |
229 log.error(_(u"Error while disconnecting: {}".format(failure_))) | 229 log.error(_("Error while disconnecting: {}".format(failure_))) |
230 | 230 |
231 def _authd(self, xmlstream): | 231 def _authd(self, xmlstream): |
232 super(SatXMPPEntity, self)._authd(xmlstream) | 232 super(SatXMPPEntity, self)._authd(xmlstream) |
233 log.debug(_(u"{profile} identified").format(profile=self.profile)) | 233 log.debug(_("{profile} identified").format(profile=self.profile)) |
234 self.streamInitialized() | 234 self.streamInitialized() |
235 | 235 |
236 def _finish_connection(self, __): | 236 def _finish_connection(self, __): |
237 self.conn_deferred.callback(None) | 237 self.conn_deferred.callback(None) |
238 | 238 |
239 def streamInitialized(self): | 239 def streamInitialized(self): |
240 """Called after _authd""" | 240 """Called after _authd""" |
241 log.debug(_(u"XML stream is initialized")) | 241 log.debug(_("XML stream is initialized")) |
242 if not self.host_app.trigger.point("xml_init", self): | 242 if not self.host_app.trigger.point("xml_init", self): |
243 return | 243 return |
244 self.postStreamInit() | 244 self.postStreamInit() |
245 | 245 |
246 def postStreamInit(self): | 246 def postStreamInit(self): |
247 """Workflow after stream initalisation.""" | 247 """Workflow after stream initalisation.""" |
248 log.info( | 248 log.info( |
249 _(u"********** [{profile}] CONNECTED **********").format(profile=self.profile) | 249 _("********** [{profile}] CONNECTED **********").format(profile=self.profile) |
250 ) | 250 ) |
251 | 251 |
252 # the following Deferred is used to know when we are connected | 252 # the following Deferred is used to know when we are connected |
253 # so we need to be set it to None when connection is lost | 253 # so we need to be set it to None when connection is lost |
254 self._connected_d = defer.Deferred() | 254 self._connected_d = defer.Deferred() |
271 disco_d.addCallback(self._finish_connection) | 271 disco_d.addCallback(self._finish_connection) |
272 | 272 |
273 def initializationFailed(self, reason): | 273 def initializationFailed(self, reason): |
274 log.error( | 274 log.error( |
275 _( | 275 _( |
276 u"ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" | 276 "ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" |
277 % {"profile": self.profile, "reason": reason} | 277 % {"profile": self.profile, "reason": reason} |
278 ) | 278 ) |
279 ) | 279 ) |
280 self.conn_deferred.errback(reason.value) | 280 self.conn_deferred.errback(reason.value) |
281 try: | 281 try: |
304 # if reconnection is disabled | 304 # if reconnection is disabled |
305 self._saved_connector = connector | 305 self._saved_connector = connector |
306 if reason is not None and not isinstance(reason.value, | 306 if reason is not None and not isinstance(reason.value, |
307 internet_error.ConnectionDone): | 307 internet_error.ConnectionDone): |
308 try: | 308 try: |
309 reason_str = unicode(reason.value) | 309 reason_str = str(reason.value) |
310 except Exception: | 310 except Exception: |
311 # FIXME: workaround for Android were p4a strips docstrings | 311 # FIXME: workaround for Android were p4a strips docstrings |
312 # while Twisted use docstring in __str__ | 312 # while Twisted use docstring in __str__ |
313 # TODO: create a ticket upstream, Twisted should work when optimization | 313 # TODO: create a ticket upstream, Twisted should work when optimization |
314 # is used | 314 # is used |
315 reason_str = unicode(reason.value.__class__) | 315 reason_str = str(reason.value.__class__) |
316 log.warning(u"Connection {term_type}: {reason}".format( | 316 log.warning("Connection {term_type}: {reason}".format( |
317 term_type = term_type, | 317 term_type = term_type, |
318 reason=reason_str)) | 318 reason=reason_str)) |
319 if not self.host_app.trigger.point(u"connection_" + term_type, connector, reason): | 319 if not self.host_app.trigger.point("connection_" + term_type, connector, reason): |
320 return | 320 return |
321 return cb(connector, reason) | 321 return cb(connector, reason) |
322 | 322 |
323 def networkDisabled(self): | 323 def networkDisabled(self): |
324 """Indicate that network has been completely disabled | 324 """Indicate that network has been completely disabled |
325 | 325 |
326 In other words, internet is not available anymore and transport must be stopped. | 326 In other words, internet is not available anymore and transport must be stopped. |
327 Retrying is disabled too, as it makes no sense to try without network, and it may | 327 Retrying is disabled too, as it makes no sense to try without network, and it may |
328 use resources (notably battery on mobiles). | 328 use resources (notably battery on mobiles). |
329 """ | 329 """ |
330 log.info(_(u"stopping connection because of network disabled")) | 330 log.info(_("stopping connection because of network disabled")) |
331 self.factory.continueTrying = 0 | 331 self.factory.continueTrying = 0 |
332 self._network_disabled = True | 332 self._network_disabled = True |
333 if self.xmlstream is not None: | 333 if self.xmlstream is not None: |
334 self.xmlstream.transport.abortConnection() | 334 self.xmlstream.transport.abortConnection() |
335 | 335 |
342 connector = self._saved_connector | 342 connector = self._saved_connector |
343 network_disabled = self._network_disabled | 343 network_disabled = self._network_disabled |
344 except AttributeError: | 344 except AttributeError: |
345 # connection has not been stopped by networkDisabled | 345 # connection has not been stopped by networkDisabled |
346 # we don't have to restart it | 346 # we don't have to restart it |
347 log.debug(u"no connection to restart") | 347 log.debug("no connection to restart") |
348 return | 348 return |
349 else: | 349 else: |
350 del self._network_disabled | 350 del self._network_disabled |
351 if not network_disabled: | 351 if not network_disabled: |
352 raise exceptions.InternalError(u"network_disabled should be True") | 352 raise exceptions.InternalError("network_disabled should be True") |
353 log.info(_(u"network is available, trying to connect")) | 353 log.info(_("network is available, trying to connect")) |
354 # we want to be sure to start fresh | 354 # we want to be sure to start fresh |
355 self.factory.resetDelay() | 355 self.factory.resetDelay() |
356 # we have a saved connector, meaning the connection has been stopped previously | 356 # we have a saved connector, meaning the connection has been stopped previously |
357 # we can now try to reconnect | 357 # we can now try to reconnect |
358 connector.connect() | 358 connector.connect() |
376 self._connected_d.callback(None) | 376 self._connected_d.callback(None) |
377 self.host_app.purgeEntity( | 377 self.host_app.purgeEntity( |
378 self.profile | 378 self.profile |
379 ) # and we remove references to this client | 379 ) # and we remove references to this client |
380 log.info( | 380 log.info( |
381 _(u"********** [{profile}] DISCONNECTED **********").format( | 381 _("********** [{profile}] DISCONNECTED **********").format( |
382 profile=self.profile | 382 profile=self.profile |
383 ) | 383 ) |
384 ) | 384 ) |
385 if not self.conn_deferred.called: | 385 if not self.conn_deferred.called: |
386 if reason is None: | 386 if reason is None: |
387 err = error.StreamError(u"Server unexpectedly closed the connection") | 387 err = error.StreamError("Server unexpectedly closed the connection") |
388 else: | 388 else: |
389 err = reason | 389 err = reason |
390 try: | 390 try: |
391 if err.value.args[0][0][2] == "certificate verify failed": | 391 if err.value.args[0][0][2] == "certificate verify failed": |
392 err = exceptions.InvalidCertificate( | 392 err = exceptions.InvalidCertificate( |
393 _(u"Your server certificate is not valid " | 393 _("Your server certificate is not valid " |
394 u"(its identity can't be checked).\n\n" | 394 "(its identity can't be checked).\n\n" |
395 u"This should never happen and may indicate that " | 395 "This should never happen and may indicate that " |
396 u"somebody is trying to spy on you.\n" | 396 "somebody is trying to spy on you.\n" |
397 u"Please contact your server administrator.")) | 397 "Please contact your server administrator.")) |
398 self.factory.stopTrying() | 398 self.factory.stopTrying() |
399 try: | 399 try: |
400 # with invalid certificate, we should not retry to connect | 400 # with invalid certificate, we should not retry to connect |
401 # so we delete saved connector to avoid reconnection if | 401 # so we delete saved connector to avoid reconnection if |
402 # networkEnabled is called. | 402 # networkEnabled is called. |
432 return False | 432 return False |
433 | 433 |
434 def entityDisconnect(self): | 434 def entityDisconnect(self): |
435 if not self.host_app.trigger.point("disconnecting", self): | 435 if not self.host_app.trigger.point("disconnecting", self): |
436 return | 436 return |
437 log.info(_(u"Disconnecting...")) | 437 log.info(_("Disconnecting...")) |
438 self.stopService() | 438 self.stopService() |
439 if self._connected_d is not None: | 439 if self._connected_d is not None: |
440 return self._connected_d | 440 return self._connected_d |
441 else: | 441 else: |
442 return defer.succeed(None) | 442 return defer.succeed(None) |
443 | 443 |
444 ## sending ## | 444 ## sending ## |
445 | 445 |
446 def IQ(self, type_=u"set", timeout=60): | 446 def IQ(self, type_="set", timeout=60): |
447 """shortcut to create an IQ element managing deferred | 447 """shortcut to create an IQ element managing deferred |
448 | 448 |
449 @param type_(unicode): IQ type ('set' or 'get') | 449 @param type_(unicode): IQ type ('set' or 'get') |
450 @param timeout(None, int): timeout in seconds | 450 @param timeout(None, int): timeout in seconds |
451 @return((D)domish.Element: result stanza | 451 @return((D)domish.Element: result stanza |
484 message_elt["from"] = data["from"].full() | 484 message_elt["from"] = data["from"].full() |
485 message_elt["type"] = data["type"] | 485 message_elt["type"] = data["type"] |
486 if data["uid"]: # key must be present but can be set to '' | 486 if data["uid"]: # key must be present but can be set to '' |
487 # by a plugin to avoid id on purpose | 487 # by a plugin to avoid id on purpose |
488 message_elt["id"] = data["uid"] | 488 message_elt["id"] = data["uid"] |
489 for lang, subject in data["subject"].iteritems(): | 489 for lang, subject in data["subject"].items(): |
490 subject_elt = message_elt.addElement("subject", content=subject) | 490 subject_elt = message_elt.addElement("subject", content=subject) |
491 if lang: | 491 if lang: |
492 subject_elt[(C.NS_XML, "lang")] = lang | 492 subject_elt[(C.NS_XML, "lang")] = lang |
493 for lang, message in data["message"].iteritems(): | 493 for lang, message in data["message"].items(): |
494 body_elt = message_elt.addElement("body", content=message) | 494 body_elt = message_elt.addElement("body", content=message) |
495 if lang: | 495 if lang: |
496 body_elt[(C.NS_XML, "lang")] = lang | 496 body_elt[(C.NS_XML, "lang")] = lang |
497 try: | 497 try: |
498 thread = data["extra"]["thread"] | 498 thread = data["extra"]["thread"] |
499 except KeyError: | 499 except KeyError: |
500 if "thread_parent" in data["extra"]: | 500 if "thread_parent" in data["extra"]: |
501 raise exceptions.InternalError( | 501 raise exceptions.InternalError( |
502 u"thread_parent found while there is not associated thread" | 502 "thread_parent found while there is not associated thread" |
503 ) | 503 ) |
504 else: | 504 else: |
505 thread_elt = message_elt.addElement("thread", content=thread) | 505 thread_elt = message_elt.addElement("thread", content=thread) |
506 try: | 506 try: |
507 thread_elt["parent"] = data["extra"]["thread_parent"] | 507 thread_elt["parent"] = data["extra"]["thread_parent"] |
544 assert mess_type in C.MESS_TYPE_ALL | 544 assert mess_type in C.MESS_TYPE_ALL |
545 | 545 |
546 data = { # dict is similar to the one used in client.onMessage | 546 data = { # dict is similar to the one used in client.onMessage |
547 "from": self.jid, | 547 "from": self.jid, |
548 "to": to_jid, | 548 "to": to_jid, |
549 "uid": uid or unicode(uuid.uuid4()), | 549 "uid": uid or str(uuid.uuid4()), |
550 "message": message, | 550 "message": message, |
551 "subject": subject, | 551 "subject": subject, |
552 "type": mess_type, | 552 "type": mess_type, |
553 "extra": extra, | 553 "extra": extra, |
554 "timestamp": time.time(), | 554 "timestamp": time.time(), |
597 pre_xml_treatments, | 597 pre_xml_treatments, |
598 post_xml_treatments, | 598 post_xml_treatments, |
599 ): | 599 ): |
600 return defer.succeed(None) | 600 return defer.succeed(None) |
601 | 601 |
602 log.debug(_(u"Sending message (type {type}, to {to})") | 602 log.debug(_("Sending message (type {type}, to {to})") |
603 .format(type=data["type"], to=to_jid.full())) | 603 .format(type=data["type"], to=to_jid.full())) |
604 | 604 |
605 pre_xml_treatments.addCallback(lambda __: self.generateMessageXML(data)) | 605 pre_xml_treatments.addCallback(lambda __: self.generateMessageXML(data)) |
606 pre_xml_treatments.chainDeferred(post_xml_treatments) | 606 pre_xml_treatments.chainDeferred(post_xml_treatments) |
607 post_xml_treatments.addCallback(self.sendMessageData) | 607 post_xml_treatments.addCallback(self.sendMessageData) |
608 if send_only: | 608 if send_only: |
609 log.debug(_(u"Triggers, storage and echo have been inhibited by the " | 609 log.debug(_("Triggers, storage and echo have been inhibited by the " |
610 u"'send_only' parameter")) | 610 "'send_only' parameter")) |
611 else: | 611 else: |
612 self.addPostXmlCallbacks(post_xml_treatments) | 612 self.addPostXmlCallbacks(post_xml_treatments) |
613 post_xml_treatments.addErrback(self._cancelErrorTrap) | 613 post_xml_treatments.addErrback(self._cancelErrorTrap) |
614 post_xml_treatments.addErrback(self.host_app.logErrback) | 614 post_xml_treatments.addErrback(self.host_app.logErrback) |
615 pre_xml_treatments.callback(data) | 615 pre_xml_treatments.callback(data) |
623 """Store message into database (for local history) | 623 """Store message into database (for local history) |
624 | 624 |
625 @param data: message data dictionnary | 625 @param data: message data dictionnary |
626 @param client: profile's client | 626 @param client: profile's client |
627 """ | 627 """ |
628 if data[u"type"] != C.MESS_TYPE_GROUPCHAT: | 628 if data["type"] != C.MESS_TYPE_GROUPCHAT: |
629 # we don't add groupchat message to history, as we get them back | 629 # we don't add groupchat message to history, as we get them back |
630 # and they will be added then | 630 # and they will be added then |
631 if data[u"message"] or data[u"subject"]: # we need a message to store | 631 if data["message"] or data["subject"]: # we need a message to store |
632 self.host_app.memory.addToHistory(self, data) | 632 self.host_app.memory.addToHistory(self, data) |
633 else: | 633 else: |
634 log.warning( | 634 log.warning( |
635 u"No message found" | 635 "No message found" |
636 ) # empty body should be managed by plugins before this point | 636 ) # empty body should be managed by plugins before this point |
637 return data | 637 return data |
638 | 638 |
639 def messageGetBridgeArgs(self, data): | 639 def messageGetBridgeArgs(self, data): |
640 """Generate args to use with bridge from data dict""" | 640 """Generate args to use with bridge from data dict""" |
641 return (data[u"uid"], data[u"timestamp"], data[u"from"].full(), | 641 return (data["uid"], data["timestamp"], data["from"].full(), |
642 data[u"to"].full(), data[u"message"], data[u"subject"], | 642 data["to"].full(), data["message"], data["subject"], |
643 data[u"type"], data[u"extra"]) | 643 data["type"], data["extra"]) |
644 | 644 |
645 | 645 |
646 def messageSendToBridge(self, data): | 646 def messageSendToBridge(self, data): |
647 """Send message to bridge, so frontends can display it | 647 """Send message to bridge, so frontends can display it |
648 | 648 |
649 @param data: message data dictionnary | 649 @param data: message data dictionnary |
650 @param client: profile's client | 650 @param client: profile's client |
651 """ | 651 """ |
652 if data[u"type"] != C.MESS_TYPE_GROUPCHAT: | 652 if data["type"] != C.MESS_TYPE_GROUPCHAT: |
653 # we don't send groupchat message to bridge, as we get them back | 653 # we don't send groupchat message to bridge, as we get them back |
654 # and they will be added the | 654 # and they will be added the |
655 if (data[u"message"] or data[u"subject"]): # we need a message to send | 655 if (data["message"] or data["subject"]): # we need a message to send |
656 # something | 656 # something |
657 | 657 |
658 # We send back the message, so all frontends are aware of it | 658 # We send back the message, so all frontends are aware of it |
659 self.host_app.bridge.messageNew( | 659 self.host_app.bridge.messageNew( |
660 *self.messageGetBridgeArgs(data), | 660 *self.messageGetBridgeArgs(data), |
661 profile=self.profile | 661 profile=self.profile |
662 ) | 662 ) |
663 else: | 663 else: |
664 log.warning(_(u"No message found")) | 664 log.warning(_("No message found")) |
665 return data | 665 return data |
666 | 666 |
667 | 667 |
668 @implementer(iwokkel.IDisco) | |
668 class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient): | 669 class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient): |
669 implements(iwokkel.IDisco) | |
670 trigger_suffix = "" | 670 trigger_suffix = "" |
671 is_component = False | 671 is_component = False |
672 | 672 |
673 def __init__(self, host_app, profile, user_jid, password, host=None, | 673 def __init__(self, host_app, profile, user_jid, password, host=None, |
674 port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES): | 674 port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES): |
679 # Currently, we use "client/pc/Salut à Toi", but as | 679 # Currently, we use "client/pc/Salut à Toi", but as |
680 # SàT is multi-frontends and can be used on mobile devices, as a bot, | 680 # SàT is multi-frontends and can be used on mobile devices, as a bot, |
681 # with a web frontend, | 681 # with a web frontend, |
682 # etc., we should implement a way to dynamically update identities through the | 682 # etc., we should implement a way to dynamically update identities through the |
683 # bridge | 683 # bridge |
684 self.identities = [disco.DiscoIdentity(u"client", u"pc", C.APP_NAME)] | 684 self.identities = [disco.DiscoIdentity("client", "pc", C.APP_NAME)] |
685 if sys.platform == "android": | 685 if sys.platform == "android": |
686 # FIXME: temporary hack as SRV is not working on android | 686 # FIXME: temporary hack as SRV is not working on android |
687 # TODO: remove this hack and fix SRV | 687 # TODO: remove this hack and fix SRV |
688 log.info(u"FIXME: Android hack, ignoring SRV") | 688 log.info("FIXME: Android hack, ignoring SRV") |
689 if host is None: | 689 if host is None: |
690 host = user_jid.host | 690 host = user_jid.host |
691 # for now we consider Android devices to be always phones | 691 # for now we consider Android devices to be always phones |
692 self.identities = [disco.DiscoIdentity(u"client", u"phone", C.APP_NAME)] | 692 self.identities = [disco.DiscoIdentity("client", "phone", C.APP_NAME)] |
693 | 693 |
694 hosts_map = host_app.memory.getConfig(None, "hosts_dict", {}) | 694 hosts_map = host_app.memory.getConfig(None, "hosts_dict", {}) |
695 if host is None and user_jid.host in hosts_map: | 695 if host is None and user_jid.host in hosts_map: |
696 host_data = hosts_map[user_jid.host] | 696 host_data = hosts_map[user_jid.host] |
697 if isinstance(host_data, basestring): | 697 if isinstance(host_data, str): |
698 host = host_data | 698 host = host_data |
699 elif isinstance(host_data, dict): | 699 elif isinstance(host_data, dict): |
700 if u"host" in host_data: | 700 if "host" in host_data: |
701 host = host_data[u"host"] | 701 host = host_data["host"] |
702 if u"port" in host_data: | 702 if "port" in host_data: |
703 port = host_data[u"port"] | 703 port = host_data["port"] |
704 else: | 704 else: |
705 log.warning( | 705 log.warning( |
706 _(u"invalid data used for host: {data}").format(data=host_data) | 706 _("invalid data used for host: {data}").format(data=host_data) |
707 ) | 707 ) |
708 host_data = None | 708 host_data = None |
709 if host_data is not None: | 709 if host_data is not None: |
710 log.info( | 710 log.info( |
711 u"using {host}:{port} for host {host_ori} as requested in config" | 711 "using {host}:{port} for host {host_ori} as requested in config" |
712 .format(host_ori=user_jid.host, host=host, port=port) | 712 .format(host_ori=user_jid.host, host=host, port=port) |
713 ) | 713 ) |
714 | 714 |
715 self.check_certificate = host_app.memory.getParamA( | 715 self.check_certificate = host_app.memory.getParamA( |
716 "check_certificate", "Connection", profile_key=profile) | 716 "check_certificate", "Connection", profile_key=profile) |
717 | 717 |
718 wokkel_client.XMPPClient.__init__( | 718 wokkel_client.XMPPClient.__init__( |
719 self, user_jid, password, host or None, port or C.XMPP_C2S_PORT, | 719 self, user_jid, password, host or None, port or C.XMPP_C2S_PORT, |
720 check_certificate = self.check_certificate | 720 # check_certificate = self.check_certificate # FIXME: currently disabled with Python 3 port |
721 ) | 721 ) |
722 SatXMPPEntity.__init__(self, host_app, profile, max_retries) | 722 SatXMPPEntity.__init__(self, host_app, profile, max_retries) |
723 | 723 |
724 if not self.check_certificate: | 724 if not self.check_certificate: |
725 msg = (_(u"Certificate validation is deactivated, this is unsecure and " | 725 msg = (_("Certificate validation is deactivated, this is unsecure and " |
726 u"somebody may be spying on you. If you have no good reason to disable " | 726 "somebody may be spying on you. If you have no good reason to disable " |
727 u"certificate validation, please activate \"Check certificate\" in your " | 727 "certificate validation, please activate \"Check certificate\" in your " |
728 u"settings in \"Connection\" tab.")) | 728 "settings in \"Connection\" tab.")) |
729 xml_tools.quickNote(host_app, self, msg, _(u"Security notice"), | 729 xml_tools.quickNote(host_app, self, msg, _("Security notice"), |
730 level = C.XMLUI_DATA_LVL_WARNING) | 730 level = C.XMLUI_DATA_LVL_WARNING) |
731 | 731 |
732 | 732 |
733 def _getPluginsList(self): | 733 def _getPluginsList(self): |
734 for p in self.host_app.plugins.itervalues(): | 734 for p in self.host_app.plugins.values(): |
735 if C.PLUG_MODE_CLIENT in p._info[u"modes"]: | 735 if C.PLUG_MODE_CLIENT in p._info["modes"]: |
736 yield p | 736 yield p |
737 | 737 |
738 def _createSubProtocols(self): | 738 def _createSubProtocols(self): |
739 self.messageProt = SatMessageProtocol(self.host_app) | 739 self.messageProt = SatMessageProtocol(self.host_app) |
740 self.messageProt.setHandlerParent(self) | 740 self.messageProt.setHandlerParent(self) |
793 # This is intented for e2e encryption which doesn't do full stanza | 793 # This is intented for e2e encryption which doesn't do full stanza |
794 # encryption (e.g. OTR) | 794 # encryption (e.g. OTR) |
795 # This trigger point can't cancel the method | 795 # This trigger point can't cancel the method |
796 yield self.host_app.trigger.asyncPoint("sendMessageData", self, mess_data, | 796 yield self.host_app.trigger.asyncPoint("sendMessageData", self, mess_data, |
797 triggers_no_cancel=True) | 797 triggers_no_cancel=True) |
798 self.send(mess_data[u"xml"]) | 798 self.send(mess_data["xml"]) |
799 defer.returnValue(mess_data) | 799 defer.returnValue(mess_data) |
800 | 800 |
801 def feedback(self, to_jid, message, extra=None): | 801 def feedback(self, to_jid, message, extra=None): |
802 """Send message to frontends | 802 """Send message to frontends |
803 | 803 |
809 in particular, info subtype can be specified with MESS_EXTRA_INFO | 809 in particular, info subtype can be specified with MESS_EXTRA_INFO |
810 """ | 810 """ |
811 if extra is None: | 811 if extra is None: |
812 extra = {} | 812 extra = {} |
813 self.host_app.bridge.messageNew( | 813 self.host_app.bridge.messageNew( |
814 uid=unicode(uuid.uuid4()), | 814 uid=str(uuid.uuid4()), |
815 timestamp=time.time(), | 815 timestamp=time.time(), |
816 from_jid=self.jid.full(), | 816 from_jid=self.jid.full(), |
817 to_jid=to_jid.full(), | 817 to_jid=to_jid.full(), |
818 message={u"": message}, | 818 message={"": message}, |
819 subject={}, | 819 subject={}, |
820 mess_type=C.MESS_TYPE_INFO, | 820 mess_type=C.MESS_TYPE_INFO, |
821 extra=extra, | 821 extra=extra, |
822 profile=self.profile, | 822 profile=self.profile, |
823 ) | 823 ) |
825 def _finish_connection(self, __): | 825 def _finish_connection(self, __): |
826 d = self.roster.requestRoster() | 826 d = self.roster.requestRoster() |
827 d.addCallback(lambda __: super(SatXMPPClient, self)._finish_connection(__)) | 827 d.addCallback(lambda __: super(SatXMPPClient, self)._finish_connection(__)) |
828 | 828 |
829 | 829 |
830 @implementer(iwokkel.IDisco) | |
830 class SatXMPPComponent(SatXMPPEntity, component.Component): | 831 class SatXMPPComponent(SatXMPPEntity, component.Component): |
831 """XMPP component | 832 """XMPP component |
832 | 833 |
833 This component are similar but not identical to clients. | 834 This component are similar but not identical to clients. |
834 An entry point plugin is launched after component is connected. | 835 An entry point plugin is launched after component is connected. |
835 Component need to instantiate MessageProtocol itself | 836 Component need to instantiate MessageProtocol itself |
836 """ | 837 """ |
837 | 838 |
838 implements(iwokkel.IDisco) | |
839 trigger_suffix = ( | 839 trigger_suffix = ( |
840 "Component" | 840 "Component" |
841 ) # used for to distinguish some trigger points set in SatXMPPEntity | 841 ) # used for to distinguish some trigger points set in SatXMPPEntity |
842 is_component = True | 842 is_component = True |
843 sendHistory = ( | 843 sendHistory = ( |
855 entry_point = host_app.memory.getEntryPoint(profile) | 855 entry_point = host_app.memory.getEntryPoint(profile) |
856 try: | 856 try: |
857 self.entry_plugin = host_app.plugins[entry_point] | 857 self.entry_plugin = host_app.plugins[entry_point] |
858 except KeyError: | 858 except KeyError: |
859 raise exceptions.NotFound( | 859 raise exceptions.NotFound( |
860 _(u"The requested entry point ({entry_point}) is not available").format( | 860 _("The requested entry point ({entry_point}) is not available").format( |
861 entry_point=entry_point | 861 entry_point=entry_point |
862 ) | 862 ) |
863 ) | 863 ) |
864 | 864 |
865 self.identities = [disco.DiscoIdentity(u"component", u"generic", C.APP_NAME)] | 865 self.identities = [disco.DiscoIdentity("component", "generic", C.APP_NAME)] |
866 # jid is set automatically on bind by Twisted for Client, but not for Component | 866 # jid is set automatically on bind by Twisted for Client, but not for Component |
867 self.jid = component_jid | 867 self.jid = component_jid |
868 if host is None: | 868 if host is None: |
869 try: | 869 try: |
870 host = component_jid.host.split(u".", 1)[1] | 870 host = component_jid.host.split(".", 1)[1] |
871 except IndexError: | 871 except IndexError: |
872 raise ValueError(u"Can't guess host from jid, please specify a host") | 872 raise ValueError("Can't guess host from jid, please specify a host") |
873 # XXX: component.Component expect unicode jid, while Client expect jid.JID. | 873 # XXX: component.Component expect unicode jid, while Client expect jid.JID. |
874 # this is not consistent, so we use jid.JID for SatXMPP* | 874 # this is not consistent, so we use jid.JID for SatXMPP* |
875 component.Component.__init__(self, host, port, component_jid.full(), password) | 875 component.Component.__init__(self, host, port, component_jid.full(), password) |
876 SatXMPPEntity.__init__(self, host_app, profile, max_retries) | 876 SatXMPPEntity.__init__(self, host_app, profile, max_retries) |
877 | 877 |
888 for recursive calls only, should not be modified by inital caller | 888 for recursive calls only, should not be modified by inital caller |
889 @raise InternalError: one of the plugin is not handling components | 889 @raise InternalError: one of the plugin is not handling components |
890 @raise KeyError: one plugin should be present in self.host_app.plugins but it | 890 @raise KeyError: one plugin should be present in self.host_app.plugins but it |
891 is not | 891 is not |
892 """ | 892 """ |
893 if C.PLUG_MODE_COMPONENT not in current._info[u"modes"]: | 893 if C.PLUG_MODE_COMPONENT not in current._info["modes"]: |
894 if not required: | 894 if not required: |
895 return | 895 return |
896 else: | 896 else: |
897 log.error( | 897 log.error( |
898 _( | 898 _( |
899 u"Plugin {current_name} is needed for {entry_name}, " | 899 "Plugin {current_name} is needed for {entry_name}, " |
900 u"but it doesn't handle component mode" | 900 "but it doesn't handle component mode" |
901 ).format( | 901 ).format( |
902 current_name=current._info[u"import_name"], | 902 current_name=current._info["import_name"], |
903 entry_name=self.entry_plugin._info[u"import_name"], | 903 entry_name=self.entry_plugin._info["import_name"], |
904 ) | 904 ) |
905 ) | 905 ) |
906 raise exceptions.InternalError(_(u"invalid plugin mode")) | 906 raise exceptions.InternalError(_("invalid plugin mode")) |
907 | 907 |
908 for import_name in current._info.get(C.PI_DEPENDENCIES, []): | 908 for import_name in current._info.get(C.PI_DEPENDENCIES, []): |
909 # plugins are already loaded as dependencies | 909 # plugins are already loaded as dependencies |
910 # so we know they are in self.host_app.plugins | 910 # so we know they are in self.host_app.plugins |
911 dep = self.host_app.plugins[import_name] | 911 dep = self.host_app.plugins[import_name] |
958 @param message_elt(domish.Element): raw <message> xml | 958 @param message_elt(domish.Element): raw <message> xml |
959 @param client(SatXMPPClient, None): client to map message id to uid | 959 @param client(SatXMPPClient, None): client to map message id to uid |
960 if None, mapping will not be done | 960 if None, mapping will not be done |
961 @return(dict): message data | 961 @return(dict): message data |
962 """ | 962 """ |
963 if message_elt.name != u"message": | 963 if message_elt.name != "message": |
964 log.warning(_( | 964 log.warning(_( |
965 u"parseMessage used with a non <message/> stanza, ignoring: {xml}" | 965 "parseMessage used with a non <message/> stanza, ignoring: {xml}" |
966 .format(xml=message_elt.toXml()))) | 966 .format(xml=message_elt.toXml()))) |
967 return {} | 967 return {} |
968 | 968 |
969 if message_elt.uri is None: | 969 if message_elt.uri is None: |
970 # wokkel element parsing strip out root namespace | 970 # wokkel element parsing strip out root namespace |
972 for c in message_elt.elements(): | 972 for c in message_elt.elements(): |
973 if c.uri is None: | 973 if c.uri is None: |
974 c.uri = C.NS_CLIENT | 974 c.uri = C.NS_CLIENT |
975 elif message_elt.uri != C.NS_CLIENT: | 975 elif message_elt.uri != C.NS_CLIENT: |
976 log.warning(_( | 976 log.warning(_( |
977 u"received <message> with a wrong namespace: {xml}" | 977 "received <message> with a wrong namespace: {xml}" |
978 .format(xml=message_elt.toXml()))) | 978 .format(xml=message_elt.toXml()))) |
979 | 979 |
980 client = self.parent | 980 client = self.parent |
981 | 981 |
982 if not message_elt.hasAttribute(u'to'): | 982 if not message_elt.hasAttribute('to'): |
983 message_elt['to'] = client.jid.full() | 983 message_elt['to'] = client.jid.full() |
984 | 984 |
985 message = {} | 985 message = {} |
986 subject = {} | 986 subject = {} |
987 extra = {} | 987 extra = {} |
988 data = { | 988 data = { |
989 u"from": jid.JID(message_elt["from"]), | 989 "from": jid.JID(message_elt["from"]), |
990 u"to": jid.JID(message_elt["to"]), | 990 "to": jid.JID(message_elt["to"]), |
991 u"uid": message_elt.getAttribute( | 991 "uid": message_elt.getAttribute( |
992 u"uid", unicode(uuid.uuid4()) | 992 "uid", str(uuid.uuid4()) |
993 ), # XXX: uid is not a standard attribute but may be added by plugins | 993 ), # XXX: uid is not a standard attribute but may be added by plugins |
994 u"message": message, | 994 "message": message, |
995 u"subject": subject, | 995 "subject": subject, |
996 u"type": message_elt.getAttribute(u"type", u"normal"), | 996 "type": message_elt.getAttribute("type", "normal"), |
997 u"extra": extra, | 997 "extra": extra, |
998 } | 998 } |
999 | 999 |
1000 try: | 1000 try: |
1001 message_id = data[u"extra"][u"message_id"] = message_elt[u"id"] | 1001 message_id = data["extra"]["message_id"] = message_elt["id"] |
1002 except KeyError: | 1002 except KeyError: |
1003 pass | 1003 pass |
1004 else: | 1004 else: |
1005 client.mess_id2uid[(data["from"], message_id)] = data["uid"] | 1005 client.mess_id2uid[(data["from"], message_id)] = data["uid"] |
1006 | 1006 |
1007 # message | 1007 # message |
1008 for e in message_elt.elements(C.NS_CLIENT, "body"): | 1008 for e in message_elt.elements(C.NS_CLIENT, "body"): |
1009 message[e.getAttribute((C.NS_XML, "lang"), "")] = unicode(e) | 1009 message[e.getAttribute((C.NS_XML, "lang"), "")] = str(e) |
1010 | 1010 |
1011 # subject | 1011 # subject |
1012 for e in message_elt.elements(C.NS_CLIENT, "subject"): | 1012 for e in message_elt.elements(C.NS_CLIENT, "subject"): |
1013 subject[e.getAttribute((C.NS_XML, "lang"), "")] = unicode(e) | 1013 subject[e.getAttribute((C.NS_XML, "lang"), "")] = str(e) |
1014 | 1014 |
1015 # delay and timestamp | 1015 # delay and timestamp |
1016 try: | 1016 try: |
1017 received_timestamp = message_elt._received_timestamp | 1017 received_timestamp = message_elt._received_timestamp |
1018 except AttributeError: | 1018 except AttributeError: |
1019 # message_elt._received_timestamp should have been set in onMessage | 1019 # message_elt._received_timestamp should have been set in onMessage |
1020 # but if parseMessage is called directly, it can be missing | 1020 # but if parseMessage is called directly, it can be missing |
1021 log.debug(u"missing received timestamp for {message_elt}".format( | 1021 log.debug("missing received timestamp for {message_elt}".format( |
1022 message_elt=message_elt)) | 1022 message_elt=message_elt)) |
1023 received_timestamp = time.time() | 1023 received_timestamp = time.time() |
1024 | 1024 |
1025 try: | 1025 try: |
1026 delay_elt = message_elt.elements(delay.NS_DELAY, "delay").next() | 1026 delay_elt = next(message_elt.elements(delay.NS_DELAY, "delay")) |
1027 except StopIteration: | 1027 except StopIteration: |
1028 data["timestamp"] = received_timestamp | 1028 data["timestamp"] = received_timestamp |
1029 else: | 1029 else: |
1030 parsed_delay = delay.Delay.fromElement(delay_elt) | 1030 parsed_delay = delay.Delay.fromElement(delay_elt) |
1031 data["timestamp"] = calendar.timegm(parsed_delay.stamp.utctimetuple()) | 1031 data["timestamp"] = calendar.timegm(parsed_delay.stamp.utctimetuple()) |
1058 # TODO: handle threads | 1058 # TODO: handle threads |
1059 message_elt._received_timestamp = time.time() | 1059 message_elt._received_timestamp = time.time() |
1060 client = self.parent | 1060 client = self.parent |
1061 if not "from" in message_elt.attributes: | 1061 if not "from" in message_elt.attributes: |
1062 message_elt["from"] = client.jid.host | 1062 message_elt["from"] = client.jid.host |
1063 log.debug(_(u"got message from: {from_}").format(from_=message_elt["from"])) | 1063 log.debug(_("got message from: {from_}").format(from_=message_elt["from"])) |
1064 | 1064 |
1065 # plugin can add their treatments to this deferred | 1065 # plugin can add their treatments to this deferred |
1066 post_treat = defer.Deferred() | 1066 post_treat = defer.Deferred() |
1067 | 1067 |
1068 d = self.host.trigger.asyncPoint( | 1068 d = self.host.trigger.asyncPoint( |
1075 if not data["message"] and not data["extra"] and not data["subject"]: | 1075 if not data["message"] and not data["extra"] and not data["subject"]: |
1076 raise failure.Failure(exceptions.CancelError("Cancelled empty message")) | 1076 raise failure.Failure(exceptions.CancelError("Cancelled empty message")) |
1077 return data | 1077 return data |
1078 | 1078 |
1079 def addToHistory(self, data): | 1079 def addToHistory(self, data): |
1080 if data.pop(u"history", None) == C.HISTORY_SKIP: | 1080 if data.pop("history", None) == C.HISTORY_SKIP: |
1081 log.info(u"history is skipped as requested") | 1081 log.info("history is skipped as requested") |
1082 data[u"extra"][u"history"] = C.HISTORY_SKIP | 1082 data["extra"]["history"] = C.HISTORY_SKIP |
1083 else: | 1083 else: |
1084 if data[u"message"] or data[u"subject"]: # we need a message to store | 1084 if data["message"] or data["subject"]: # we need a message to store |
1085 return self.host.memory.addToHistory(self.parent, data) | 1085 return self.host.memory.addToHistory(self.parent, data) |
1086 else: | 1086 else: |
1087 log.debug(u"not storing empty message to history: {data}" | 1087 log.debug("not storing empty message to history: {data}" |
1088 .format(data=data)) | 1088 .format(data=data)) |
1089 | 1089 |
1090 def bridgeSignal(self, __, data): | 1090 def bridgeSignal(self, __, data): |
1091 try: | 1091 try: |
1092 data["extra"]["received_timestamp"] = unicode(data["received_timestamp"]) | 1092 data["extra"]["received_timestamp"] = str(data["received_timestamp"]) |
1093 data["extra"]["delay_sender"] = data["delay_sender"] | 1093 data["extra"]["delay_sender"] = data["delay_sender"] |
1094 except KeyError: | 1094 except KeyError: |
1095 pass | 1095 pass |
1096 if C.MESS_KEY_ENCRYPTION in data: | 1096 if C.MESS_KEY_ENCRYPTION in data: |
1097 data[u"extra"][u"encrypted"] = C.BOOL_TRUE | 1097 data["extra"]["encrypted"] = C.BOOL_TRUE |
1098 if data is not None: | 1098 if data is not None: |
1099 if data["message"] or data["subject"] or data["type"] == C.MESS_TYPE_INFO: | 1099 if data["message"] or data["subject"] or data["type"] == C.MESS_TYPE_INFO: |
1100 self.host.bridge.messageNew( | 1100 self.host.bridge.messageNew( |
1101 data["uid"], | 1101 data["uid"], |
1102 data["timestamp"], | 1102 data["timestamp"], |
1107 data["type"], | 1107 data["type"], |
1108 data["extra"], | 1108 data["extra"], |
1109 profile=self.parent.profile, | 1109 profile=self.parent.profile, |
1110 ) | 1110 ) |
1111 else: | 1111 else: |
1112 log.debug(u"Discarding bridge signal for empty message: {data}".format( | 1112 log.debug("Discarding bridge signal for empty message: {data}".format( |
1113 data=data)) | 1113 data=data)) |
1114 return data | 1114 return data |
1115 | 1115 |
1116 def cancelErrorTrap(self, failure_): | 1116 def cancelErrorTrap(self, failure_): |
1117 """A message sending can be cancelled by a plugin treatment""" | 1117 """A message sending can be cancelled by a plugin treatment""" |
1129 self._groups = {} # map from groups to jids: key=group value=set of jids | 1129 self._groups = {} # map from groups to jids: key=group value=set of jids |
1130 | 1130 |
1131 @property | 1131 @property |
1132 def versioning(self): | 1132 def versioning(self): |
1133 """True if server support roster versioning""" | 1133 """True if server support roster versioning""" |
1134 return (NS_ROSTER_VER, u'ver') in self.parent.xmlstream.features | 1134 return (NS_ROSTER_VER, 'ver') in self.parent.xmlstream.features |
1135 | 1135 |
1136 @property | 1136 @property |
1137 def roster_cache(self): | 1137 def roster_cache(self): |
1138 """Cache of roster from storage | 1138 """Cache of roster from storage |
1139 | 1139 |
1146 """Register item in local cache | 1146 """Register item in local cache |
1147 | 1147 |
1148 item must be already registered in self._jids before this method is called | 1148 item must be already registered in self._jids before this method is called |
1149 @param item (RosterIem): item added | 1149 @param item (RosterIem): item added |
1150 """ | 1150 """ |
1151 log.debug(u"registering item: {}".format(item.entity.full())) | 1151 log.debug("registering item: {}".format(item.entity.full())) |
1152 if item.entity.resource: | 1152 if item.entity.resource: |
1153 log.warning( | 1153 log.warning( |
1154 u"Received a roster item with a resource, this is not common but not " | 1154 "Received a roster item with a resource, this is not common but not " |
1155 u"restricted by RFC 6121, this case may be not well tested." | 1155 "restricted by RFC 6121, this case may be not well tested." |
1156 ) | 1156 ) |
1157 if not item.subscriptionTo: | 1157 if not item.subscriptionTo: |
1158 if not item.subscriptionFrom: | 1158 if not item.subscriptionFrom: |
1159 log.info( | 1159 log.info( |
1160 _(u"There's no subscription between you and [{}]!").format( | 1160 _("There's no subscription between you and [{}]!").format( |
1161 item.entity.full() | 1161 item.entity.full() |
1162 ) | 1162 ) |
1163 ) | 1163 ) |
1164 else: | 1164 else: |
1165 log.info(_(u"You are not subscribed to [{}]!").format(item.entity.full())) | 1165 log.info(_("You are not subscribed to [{}]!").format(item.entity.full())) |
1166 if not item.subscriptionFrom: | 1166 if not item.subscriptionFrom: |
1167 log.info(_(u"[{}] is not subscribed to you!").format(item.entity.full())) | 1167 log.info(_("[{}] is not subscribed to you!").format(item.entity.full())) |
1168 | 1168 |
1169 for group in item.groups: | 1169 for group in item.groups: |
1170 self._groups.setdefault(group, set()).add(item.entity) | 1170 self._groups.setdefault(group, set()).add(item.entity) |
1171 | 1171 |
1172 @defer.inlineCallbacks | 1172 @defer.inlineCallbacks |
1176 @param version(unicode): version of roster in local cache | 1176 @param version(unicode): version of roster in local cache |
1177 """ | 1177 """ |
1178 roster_cache = self.roster_cache | 1178 roster_cache = self.roster_cache |
1179 yield roster_cache.clear() | 1179 yield roster_cache.clear() |
1180 roster_cache[ROSTER_VER_KEY] = version | 1180 roster_cache[ROSTER_VER_KEY] = version |
1181 for roster_jid, roster_item in self._jids.iteritems(): | 1181 for roster_jid, roster_item in self._jids.items(): |
1182 roster_jid_s = roster_jid.full() | 1182 roster_jid_s = roster_jid.full() |
1183 roster_item_elt = roster_item.toElement().toXml() | 1183 roster_item_elt = roster_item.toElement().toXml() |
1184 roster_cache[roster_jid_s] = roster_item_elt | 1184 roster_cache[roster_jid_s] = roster_item_elt |
1185 | 1185 |
1186 @defer.inlineCallbacks | 1186 @defer.inlineCallbacks |
1198 | 1198 |
1199 @defer.inlineCallbacks | 1199 @defer.inlineCallbacks |
1200 def requestRoster(self): | 1200 def requestRoster(self): |
1201 """Ask the server for Roster list """ | 1201 """Ask the server for Roster list """ |
1202 if self.versioning: | 1202 if self.versioning: |
1203 log.info(_(u"our server support roster versioning, we use it")) | 1203 log.info(_("our server support roster versioning, we use it")) |
1204 roster_cache = self.roster_cache | 1204 roster_cache = self.roster_cache |
1205 yield roster_cache.load() | 1205 yield roster_cache.load() |
1206 try: | 1206 try: |
1207 version = roster_cache[ROSTER_VER_KEY] | 1207 version = roster_cache[ROSTER_VER_KEY] |
1208 except KeyError: | 1208 except KeyError: |
1209 log.info(_(u"no roster in cache, we start fresh")) | 1209 log.info(_("no roster in cache, we start fresh")) |
1210 # u"" means we use versioning without valid roster in cache | 1210 # u"" means we use versioning without valid roster in cache |
1211 version = u"" | 1211 version = "" |
1212 else: | 1212 else: |
1213 log.info(_(u"We have roster v{version} in cache").format(version=version)) | 1213 log.info(_("We have roster v{version} in cache").format(version=version)) |
1214 # we deserialise cached roster to our local cache | 1214 # we deserialise cached roster to our local cache |
1215 for roster_jid_s, roster_item_elt_s in roster_cache.iteritems(): | 1215 for roster_jid_s, roster_item_elt_s in roster_cache.items(): |
1216 if roster_jid_s == ROSTER_VER_KEY: | 1216 if roster_jid_s == ROSTER_VER_KEY: |
1217 continue | 1217 continue |
1218 roster_jid = jid.JID(roster_jid_s) | 1218 roster_jid = jid.JID(roster_jid_s) |
1219 roster_item_elt = generic.parseXml(roster_item_elt_s.encode('utf-8')) | 1219 roster_item_elt = generic.parseXml(roster_item_elt_s.encode('utf-8')) |
1220 roster_item = xmppim.RosterItem.fromElement(roster_item_elt) | 1220 roster_item = xmppim.RosterItem.fromElement(roster_item_elt) |
1221 self._jids[roster_jid] = roster_item | 1221 self._jids[roster_jid] = roster_item |
1222 self._registerItem(roster_item) | 1222 self._registerItem(roster_item) |
1223 else: | 1223 else: |
1224 log.warning(_(u"our server doesn't support roster versioning")) | 1224 log.warning(_("our server doesn't support roster versioning")) |
1225 version = None | 1225 version = None |
1226 | 1226 |
1227 log.debug("requesting roster") | 1227 log.debug("requesting roster") |
1228 roster = yield self.getRoster(version=version) | 1228 roster = yield self.getRoster(version=version) |
1229 if roster is None: | 1229 if roster is None: |
1230 log.debug(u"empty roster result received, we'll get roster item with roster " | 1230 log.debug("empty roster result received, we'll get roster item with roster " |
1231 u"pushes") | 1231 "pushes") |
1232 else: | 1232 else: |
1233 # a full roster is received | 1233 # a full roster is received |
1234 self._groups.clear() | 1234 self._groups.clear() |
1235 self._jids = roster | 1235 self._jids = roster |
1236 for item in roster.itervalues(): | 1236 for item in roster.values(): |
1237 if not item.subscriptionTo and not item.subscriptionFrom and not item.ask: | 1237 if not item.subscriptionTo and not item.subscriptionFrom and not item.ask: |
1238 # XXX: current behaviour: we don't want contact in our roster list | 1238 # XXX: current behaviour: we don't want contact in our roster list |
1239 # if there is no presence subscription | 1239 # if there is no presence subscription |
1240 # may change in the future | 1240 # may change in the future |
1241 log.info( | 1241 log.info( |
1242 u"Removing contact {} from roster because there is no presence " | 1242 "Removing contact {} from roster because there is no presence " |
1243 u"subscription".format( | 1243 "subscription".format( |
1244 item.jid | 1244 item.jid |
1245 ) | 1245 ) |
1246 ) | 1246 ) |
1247 self.removeItem(item.entity) # FIXME: to be checked | 1247 self.removeItem(item.entity) # FIXME: to be checked |
1248 else: | 1248 else: |
1265 | 1265 |
1266 @param item: RosterItem | 1266 @param item: RosterItem |
1267 @return: dictionary of attributes | 1267 @return: dictionary of attributes |
1268 """ | 1268 """ |
1269 item_attr = { | 1269 item_attr = { |
1270 "to": unicode(item.subscriptionTo), | 1270 "to": str(item.subscriptionTo), |
1271 "from": unicode(item.subscriptionFrom), | 1271 "from": str(item.subscriptionFrom), |
1272 "ask": unicode(item.ask), | 1272 "ask": str(item.ask), |
1273 } | 1273 } |
1274 if item.name: | 1274 if item.name: |
1275 item_attr["name"] = item.name | 1275 item_attr["name"] = item.name |
1276 return item_attr | 1276 return item_attr |
1277 | 1277 |
1278 def setReceived(self, request): | 1278 def setReceived(self, request): |
1279 item = request.item | 1279 item = request.item |
1280 entity = item.entity | 1280 entity = item.entity |
1281 log.info(_(u"adding {entity} to roster").format(entity=entity.full())) | 1281 log.info(_("adding {entity} to roster").format(entity=entity.full())) |
1282 if request.version is not None: | 1282 if request.version is not None: |
1283 # we update the cache in storage | 1283 # we update the cache in storage |
1284 roster_cache = self.roster_cache | 1284 roster_cache = self.roster_cache |
1285 roster_cache[entity.full()] = item.toElement().toXml() | 1285 roster_cache[entity.full()] = item.toElement().toXml() |
1286 roster_cache[ROSTER_VER_KEY] = request.version | 1286 roster_cache[ROSTER_VER_KEY] = request.version |
1300 entity.full(), self.getAttributes(item), item.groups, self.parent.profile | 1300 entity.full(), self.getAttributes(item), item.groups, self.parent.profile |
1301 ) | 1301 ) |
1302 | 1302 |
1303 def removeReceived(self, request): | 1303 def removeReceived(self, request): |
1304 entity = request.item.entity | 1304 entity = request.item.entity |
1305 log.info(_(u"removing {entity} from roster").format(entity=entity.full())) | 1305 log.info(_("removing {entity} from roster").format(entity=entity.full())) |
1306 if request.version is not None: | 1306 if request.version is not None: |
1307 # we update the cache in storage | 1307 # we update the cache in storage |
1308 roster_cache = self.roster_cache | 1308 roster_cache = self.roster_cache |
1309 try: | 1309 try: |
1310 del roster_cache[request.item.entity.full()] | 1310 del roster_cache[request.item.entity.full()] |
1317 # we first remove item from local cache (self._groups and self._jids) | 1317 # we first remove item from local cache (self._groups and self._jids) |
1318 try: | 1318 try: |
1319 item = self._jids.pop(entity) | 1319 item = self._jids.pop(entity) |
1320 except KeyError: | 1320 except KeyError: |
1321 log.error( | 1321 log.error( |
1322 u"Received a roster remove event for an item not in cache ({})".format( | 1322 "Received a roster remove event for an item not in cache ({})".format( |
1323 entity | 1323 entity |
1324 ) | 1324 ) |
1325 ) | 1325 ) |
1326 return | 1326 return |
1327 for group in item.groups: | 1327 for group in item.groups: |
1330 jids_set.remove(entity) | 1330 jids_set.remove(entity) |
1331 if not jids_set: | 1331 if not jids_set: |
1332 del self._groups[group] | 1332 del self._groups[group] |
1333 except KeyError: | 1333 except KeyError: |
1334 log.warning( | 1334 log.warning( |
1335 u"there is no cache for the group [{group}] of the removed roster " | 1335 "there is no cache for the group [{group}] of the removed roster " |
1336 u"item [{jid_}]".format(group=group, jid=entity) | 1336 "item [{jid_}]".format(group=group, jid=entity) |
1337 ) | 1337 ) |
1338 | 1338 |
1339 # then we send the bridge signal | 1339 # then we send the bridge signal |
1340 self.host.bridge.contactDeleted(entity.full(), self.parent.profile) | 1340 self.host.bridge.contactDeleted(entity.full(), self.parent.profile) |
1341 | 1341 |
1342 def getGroups(self): | 1342 def getGroups(self): |
1343 """Return a list of groups""" | 1343 """Return a list of groups""" |
1344 return self._groups.keys() | 1344 return list(self._groups.keys()) |
1345 | 1345 |
1346 def getItem(self, entity_jid): | 1346 def getItem(self, entity_jid): |
1347 """Return RosterItem for a given jid | 1347 """Return RosterItem for a given jid |
1348 | 1348 |
1349 @param entity_jid(jid.JID): jid of the contact | 1349 @param entity_jid(jid.JID): jid of the contact |
1352 """ | 1352 """ |
1353 return self._jids.get(entity_jid, None) | 1353 return self._jids.get(entity_jid, None) |
1354 | 1354 |
1355 def getJids(self): | 1355 def getJids(self): |
1356 """Return all jids of the roster""" | 1356 """Return all jids of the roster""" |
1357 return self._jids.keys() | 1357 return list(self._jids.keys()) |
1358 | 1358 |
1359 def isJidInRoster(self, entity_jid): | 1359 def isJidInRoster(self, entity_jid): |
1360 """Return True if jid is in roster""" | 1360 """Return True if jid is in roster""" |
1361 return entity_jid in self._jids | 1361 return entity_jid in self._jids |
1362 | 1362 |
1368 return False | 1368 return False |
1369 return item.subscriptionFrom | 1369 return item.subscriptionFrom |
1370 | 1370 |
1371 def getItems(self): | 1371 def getItems(self): |
1372 """Return all items of the roster""" | 1372 """Return all items of the roster""" |
1373 return self._jids.values() | 1373 return list(self._jids.values()) |
1374 | 1374 |
1375 def getJidsFromGroup(self, group): | 1375 def getJidsFromGroup(self, group): |
1376 try: | 1376 try: |
1377 return self._groups[group] | 1377 return self._groups[group] |
1378 except KeyError: | 1378 except KeyError: |
1396 jids = set() | 1396 jids = set() |
1397 for group in groups: | 1397 for group in groups: |
1398 jids.update(self.getJidsFromGroup(group)) | 1398 jids.update(self.getJidsFromGroup(group)) |
1399 return jids | 1399 return jids |
1400 else: | 1400 else: |
1401 raise ValueError(u"Unexpected type_ {}".format(type_)) | 1401 raise ValueError("Unexpected type_ {}".format(type_)) |
1402 | 1402 |
1403 def getNick(self, entity_jid): | 1403 def getNick(self, entity_jid): |
1404 """Return a nick name for an entity | 1404 """Return a nick name for an entity |
1405 | 1405 |
1406 return nick choosed by user if available | 1406 return nick choosed by user if available |
1445 entity.full(), show or "", int(priority), statuses, self.parent.profile | 1445 entity.full(), show or "", int(priority), statuses, self.parent.profile |
1446 ) | 1446 ) |
1447 | 1447 |
1448 def unavailableReceived(self, entity, statuses=None): | 1448 def unavailableReceived(self, entity, statuses=None): |
1449 log.debug( | 1449 log.debug( |
1450 _(u"presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)") | 1450 _("presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)") |
1451 % {"entity": entity, C.PRESENCE_STATUSES: statuses} | 1451 % {"entity": entity, C.PRESENCE_STATUSES: statuses} |
1452 ) | 1452 ) |
1453 | 1453 |
1454 if not statuses: | 1454 if not statuses: |
1455 statuses = {} | 1455 statuses = {} |
1537 def unsubscribed(self, entity): | 1537 def unsubscribed(self, entity): |
1538 xmppim.PresenceClientProtocol.unsubscribed(self, entity) | 1538 xmppim.PresenceClientProtocol.unsubscribed(self, entity) |
1539 self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile) | 1539 self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile) |
1540 | 1540 |
1541 def subscribedReceived(self, entity): | 1541 def subscribedReceived(self, entity): |
1542 log.debug(_(u"subscription approved for [%s]") % entity.userhost()) | 1542 log.debug(_("subscription approved for [%s]") % entity.userhost()) |
1543 self.host.bridge.subscribe("subscribed", entity.userhost(), self.parent.profile) | 1543 self.host.bridge.subscribe("subscribed", entity.userhost(), self.parent.profile) |
1544 | 1544 |
1545 def unsubscribedReceived(self, entity): | 1545 def unsubscribedReceived(self, entity): |
1546 log.debug(_(u"unsubscription confirmed for [%s]") % entity.userhost()) | 1546 log.debug(_("unsubscription confirmed for [%s]") % entity.userhost()) |
1547 self.host.bridge.subscribe("unsubscribed", entity.userhost(), self.parent.profile) | 1547 self.host.bridge.subscribe("unsubscribed", entity.userhost(), self.parent.profile) |
1548 | 1548 |
1549 @defer.inlineCallbacks | 1549 @defer.inlineCallbacks |
1550 def subscribeReceived(self, entity): | 1550 def subscribeReceived(self, entity): |
1551 log.debug(_(u"subscription request from [%s]") % entity.userhost()) | 1551 log.debug(_("subscription request from [%s]") % entity.userhost()) |
1552 yield self.parent.roster.got_roster | 1552 yield self.parent.roster.got_roster |
1553 item = self.parent.roster.getItem(entity) | 1553 item = self.parent.roster.getItem(entity) |
1554 if item and item.subscriptionTo: | 1554 if item and item.subscriptionTo: |
1555 # We automatically accept subscription if we are already subscribed to | 1555 # We automatically accept subscription if we are already subscribed to |
1556 # contact presence | 1556 # contact presence |
1564 "subscribe", entity.userhost(), self.parent.profile | 1564 "subscribe", entity.userhost(), self.parent.profile |
1565 ) | 1565 ) |
1566 | 1566 |
1567 @defer.inlineCallbacks | 1567 @defer.inlineCallbacks |
1568 def unsubscribeReceived(self, entity): | 1568 def unsubscribeReceived(self, entity): |
1569 log.debug(_(u"unsubscription asked for [%s]") % entity.userhost()) | 1569 log.debug(_("unsubscription asked for [%s]") % entity.userhost()) |
1570 yield self.parent.roster.got_roster | 1570 yield self.parent.roster.got_roster |
1571 item = self.parent.roster.getItem(entity) | 1571 item = self.parent.roster.getItem(entity) |
1572 if item and item.subscriptionFrom: # we automatically remove contact | 1572 if item and item.subscriptionFrom: # we automatically remove contact |
1573 log.debug(_("automatic contact deletion")) | 1573 log.debug(_("automatic contact deletion")) |
1574 self.host.delContact(entity, self.parent.profile) | 1574 self.host.delContact(entity, self.parent.profile) |
1575 self.host.bridge.subscribe("unsubscribe", entity.userhost(), self.parent.profile) | 1575 self.host.bridge.subscribe("unsubscribe", entity.userhost(), self.parent.profile) |
1576 | 1576 |
1577 | 1577 |
1578 @implementer(iwokkel.IDisco) | |
1578 class SatDiscoProtocol(disco.DiscoClientProtocol): | 1579 class SatDiscoProtocol(disco.DiscoClientProtocol): |
1579 implements(iwokkel.IDisco) | |
1580 | 1580 |
1581 def __init__(self, host): | 1581 def __init__(self, host): |
1582 disco.DiscoClientProtocol.__init__(self) | 1582 disco.DiscoClientProtocol.__init__(self) |
1583 | 1583 |
1584 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | 1584 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): |
1597 generic.FallbackHandler.__init__(self) | 1597 generic.FallbackHandler.__init__(self) |
1598 | 1598 |
1599 def iqFallback(self, iq): | 1599 def iqFallback(self, iq): |
1600 if iq.handled is True: | 1600 if iq.handled is True: |
1601 return | 1601 return |
1602 log.debug(u"iqFallback: xml = [%s]" % (iq.toXml())) | 1602 log.debug("iqFallback: xml = [%s]" % (iq.toXml())) |
1603 generic.FallbackHandler.iqFallback(self, iq) | 1603 generic.FallbackHandler.iqFallback(self, iq) |
1604 | 1604 |
1605 | 1605 |
1606 class SatVersionHandler(generic.VersionHandler): | 1606 class SatVersionHandler(generic.VersionHandler): |
1607 | 1607 |
1613 # on ejabberd) generate its own hash for security check it reject our | 1613 # on ejabberd) generate its own hash for security check it reject our |
1614 # features (resulting in e.g. no notification on PEP) | 1614 # features (resulting in e.g. no notification on PEP) |
1615 return generic.VersionHandler.getDiscoInfo(self, requestor, target, None) | 1615 return generic.VersionHandler.getDiscoInfo(self, requestor, target, None) |
1616 | 1616 |
1617 | 1617 |
1618 @implementer(iwokkel.IDisco) | |
1618 class SatIdentityHandler(XMPPHandler): | 1619 class SatIdentityHandler(XMPPHandler): |
1619 """Manage disco Identity of SàT.""" | 1620 """Manage disco Identity of SàT.""" |
1620 implements(iwokkel.IDisco) | |
1621 # TODO: dynamic identity update (see docstring). Note that a XMPP entity can have | 1621 # TODO: dynamic identity update (see docstring). Note that a XMPP entity can have |
1622 # several identities | 1622 # several identities |
1623 | 1623 |
1624 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | 1624 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): |
1625 return self.parent.identities | 1625 return self.parent.identities |