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