changeset 2664:e35a265ec174

quick frontend (app, chat): encryption handling: - new QuickApp.ENCRYPTION_HANDLERS class attribute, if True (default), encryption handlers are set - encryption plugins are retrieved on startup and cached in QuickApp.encryption_plugins list - QuickChat's encrypted boolean attribute indicate if a session is currently encrypted. This is updated automatically if ENCRYPTION_HANDLERS is set - if ENCRYPTION_HANDLERS is set, messageEncryptionStarted and messageEncryptionStopped are called when suitable.
author Goffi <goffi@goffi.org>
date Sat, 11 Aug 2018 18:24:55 +0200
parents 32b5f68a23b4
children 20bf6887d1ed
files sat_frontends/quick_frontend/quick_app.py sat_frontends/quick_frontend/quick_chat.py
diffstat 2 files changed, 121 insertions(+), 57 deletions(-) [+]
line wrap: on
line diff
--- a/sat_frontends/quick_frontend/quick_app.py	Sat Aug 11 18:24:55 2018 +0200
+++ b/sat_frontends/quick_frontend/quick_app.py	Sat Aug 11 18:24:55 2018 +0200
@@ -142,8 +142,12 @@
     def _plug_profile_getFeaturesCb(self, features):
         self.host.features = features
         # FIXME: we don't use cached value at the moment, but keep the code for later use
-        #        it was previously used for avatars, but as we don't get full path here, it's better to request later
-        # self.host.bridge.getEntitiesData([], ProfileManager.cache_keys_to_get, profile=self.profile, callback=self._plug_profile_gotCachedValues, errback=self._plug_profile_failedCachedValues)
+        #        it was previously used for avatars, but as we don't get full path here,
+        #        it's better to request later
+        # self.host.bridge.getEntitiesData([], ProfileManager.cache_keys_to_get,
+        #                                  profile=self.profile,
+        #                                  callback=self._plug_profile_gotCachedValues,
+        #                                  errback=self._plug_profile_failedCachedValues)
         self._plug_profile_gotCachedValues({})
 
     def _plug_profile_failedCachedValues(self, failure):
@@ -273,13 +277,15 @@
 
     MB_HANDLER = True  # Set to False if the frontend doesn't manage microblog
     AVATARS_HANDLER = True  # set to False if avatars are not used
+    ENCRYPTION_HANDLERS = True  # set to False if encryption is handled separatly
 
     def __init__(self, bridge_factory, xmlui, check_options=None, connect_bridge=True):
         """Create a frontend application
 
         @param bridge_factory: method to use to create the Bridge
         @param xmlui: xmlui module
-        @param check_options: method to call to check options (usually command line arguments)
+        @param check_options: method to call to check options (usually command line
+            arguments)
         """
         self.xmlui = xmlui
         self.menus = quick_menus.QuickMenusManager(self)
@@ -289,7 +295,8 @@
             set()
         )  # profiles currently being plugged, used to (un)lock contact list updates
         self.ready_profiles = set()  # profiles which are connected and ready
-        self.signals_cache = {}  # used to keep signal received between start of plug_profile and when the profile is actualy ready
+        self.signals_cache = {}  # used to keep signal received between start of
+                                 # plug_profile and when the profile is actualy ready
         self.contact_lists = quick_contact_list.QuickContactListHandler(self)
         self.widgets = quick_widgets.QuickWidgetsManager(self)
         if check_options is not None:
@@ -303,7 +310,8 @@
         )  # widget currently selected (must be filled by frontend)
 
         # listeners
-        self._listeners = {}  # key: listener type ("avatar", "selected", etc), value: list of callbacks
+        self._listeners = {}  # key: listener type ("avatar", "selected", etc),
+                              # value: list of callbacks
 
         # triggers
         self.trigger = (
@@ -320,6 +328,7 @@
         self._notifications = OrderedDict()
         self.features = None
         self.ns_map = {}  # map of short name to namespaces
+        self.encryption_plugins = []
 
     def connectBridge(self):
         self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb)
@@ -330,10 +339,20 @@
     def _namespacesGetEb(self, failure_):
         log.error(_(u"Can't get namespaces map: {msg}").format(msg=failure_))
 
+    def _encryptionPluginsGetCb(self, plugins):
+        self.encryption_plugins = plugins
+
+    def _encryptionPluginsGetEb(self, failure_):
+        log.warning(_(u"Can't retrieve encryption plugins: {msg}").format(msg=failure_))
+
     def onBridgeConnected(self):
         self.bridge.namespacesGet(
-            callback=self._namespacesGetCb, errback=self._namespacesGetEb
-        )
+            callback=self._namespacesGetCb, errback=self._namespacesGetEb)
+        # we cache available encryption plugins, as we'll use them on earch
+        # new chat widget
+        self.bridge.encryptionPluginsGet(
+            callback=self._encryptionPluginsGetCb,
+            errback=self._encryptionPluginsGetEb)
 
     def _bridgeCb(self):
         self.registerSignal("connected")
@@ -341,6 +360,9 @@
         self.registerSignal("actionNew")
         self.registerSignal("newContact")
         self.registerSignal("messageNew")
+        if self.ENCRYPTION_HANDLERS:
+            self.registerSignal("messageEncryptionStarted")
+            self.registerSignal("messageEncryptionStopped")
         self.registerSignal("presenceUpdate")
         self.registerSignal("subscribe")
         self.registerSignal("paramUpdate")
@@ -395,9 +417,11 @@
         """Register a handler for a signal
 
         @param function_name (str): name of the signal to handle
-        @param handler (instancemethod): method to call when the signal arrive, None for calling an automatically named handler (function_name + 'Handler')
+        @param handler (instancemethod): method to call when the signal arrive,
+            None for calling an automatically named handler (function_name + 'Handler')
         @param iface (str): interface of the bridge to use ('core' or 'plugin')
-        @param with_profile (boolean): True if the signal concerns a specific profile, in that case the profile name has to be passed by the caller
+        @param with_profile (boolean): True if the signal concerns a specific profile,
+            in that case the profile name has to be passed by the caller
         """
         log.debug(u"registering signal {name}".format(name=function_name))
         if handler is None:
@@ -415,7 +439,8 @@
             if profile is not None:
                 if not self.check_profile(profile):
                     if profile in self.profiles:
-                        # profile is not ready but is in self.profiles, that's mean that it's being connecting and we need to cache the signal
+                        # profile is not ready but is in self.profiles, that's mean that
+                        # it's being connecting and we need to cache the signal
                         self.signals_cache.setdefault(profile, []).append(
                             (function_name, handler, args, kwargs)
                         )
@@ -427,7 +452,8 @@
     def addListener(self, type_, callback, profiles_filter=None):
         """Add a listener for an event
 
-        /!\ don't forget to remove listener when not used anymore (e.g. if you delete a widget)
+        /!\ don't forget to remove listener when not used anymore (e.g. if you delete a
+            widget)
         @param type_: type of event, can be:
             - avatar: called when avatar data is updated
                 args: (entity, avatar file, profile)
@@ -444,7 +470,8 @@
                     type_: same as in [sat.core.sat_main.SAT.importMenu]
                     path: same as in [sat.core.sat_main.SAT.importMenu]
                     path_i18n: translated path (or None if the item is removed)
-                    item: instance of quick_menus.MenuItemBase or None if the item is removed
+                    item: instance of quick_menus.MenuItemBase or None if the item is
+                          removed
             - gotMenus: called only once when menu are available (no arg)
             - progressFinished: called when a progressing action has just finished
                 args:  (progress_id, metadata, profile)
@@ -491,18 +518,22 @@
         return profile in self.ready_profiles
 
     def postInit(self, profile_manager):
-        """Must be called after initialization is done, do all automatic task (auto plug profile)
+        """Must be called after initialization is done, do all automatic task
 
-        @param profile_manager: instance of a subclass of Quick_frontend.QuickProfileManager
+        (auto plug profile)
+        @param profile_manager: instance of a subclass of
+            Quick_frontend.QuickProfileManager
         """
         if self.options and self.options.profile:
             profile_manager.autoconnect([self.options.profile])
 
     def profilePlugged(self, profile):
-        """Method called when the profile is fully plugged, to launch frontend specific workflow
+        """Method called when the profile is fully plugged
 
-        /!\ if you override the method and don't call the parent, be sure to add the profile to ready_profiles !
-            if you don't, all signals will stay in cache
+        This will launch frontend specific workflow
+
+        /!\ if you override the method and don't call the parent, be sure to add the
+            profile to ready_profiles ! if you don't, all signals will stay in cache
 
         @param profile(unicode): %(doc_profile)s
         """
@@ -608,9 +639,8 @@
         groups = list(groups)
         self.contact_lists[profile].setContact(entity, groups, attributes, in_roster=True)
 
-    def messageNewHandler(
-        self, uid, timestamp, from_jid_s, to_jid_s, msg, subject, type_, extra, profile
-    ):
+    def messageNewHandler(self, uid, timestamp, from_jid_s, to_jid_s, msg, subject, type_,
+                          extra, profile):
         from_jid = jid.JID(from_jid_s)
         to_jid = jid.JID(to_jid_s)
         if not self.trigger.point(
@@ -631,7 +661,8 @@
         target = to_jid if from_me else from_jid
         contact_list = self.contact_lists[profile]
         if target.resource and not contact_list.isRoom(target.bare):
-            # we avoid resource locking, but we must keep resource for private MUC messages
+            # we avoid resource locking, but we must keep resource for private MUC
+            # messages
             target = target.bare
         # we want to be sure to have at least one QuickChat instance
         self.widgets.getOrCreateWidget(
@@ -658,6 +689,21 @@
                 uid, timestamp, from_jid, target, msg, subject, type_, extra, profile
             )
 
+    def messageEncryptionStartedHandler(self, destinee_jid_s, plugin_data, profile):
+        destinee_jid = jid.JID(destinee_jid_s)
+        plugin_data = data_format.deserialise(plugin_data)
+        for widget in self.widgets.getWidgets(quick_chat.QuickChat,
+                                              target=destinee_jid.bare,
+                                              profiles=(profile,)):
+            widget.messageEncryptionStarted(plugin_data)
+
+    def messageEncryptionStoppedHandler(self, destinee_jid_s, plugin_data, profile):
+        destinee_jid = jid.JID(destinee_jid_s)
+        for widget in self.widgets.getWidgets(quick_chat.QuickChat,
+                                              target=destinee_jid.bare,
+                                              profiles=(profile,)):
+            widget.messageEncryptionStopped(plugin_data)
+
     def messageStateHandler(self, uid, status, profile):
         for widget in self.widgets.getWidgets(quick_chat.QuickChat, profiles=(profile,)):
             widget.onMessageState(uid, status, profile)
@@ -680,7 +726,8 @@
         if callback is None:
             callback = (
                 lambda dummy=None: None
-            )  # FIXME: optional argument is here because pyjamas doesn't support callback without arg with json proxy
+            )  # FIXME: optional argument is here because pyjamas doesn't support callback
+               #        without arg with json proxy
         if errback is None:
             errback = lambda failure: self.showDialog(
                 failure.fullname, failure.message, "error"
@@ -716,7 +763,8 @@
     def presenceUpdateHandler(self, entity_s, show, priority, statuses, profile):
         log.debug(
             _(
-                u"presence update for %(entity)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]"
+                u"presence update for %(entity)s (show=%(show)s, priority=%(priority)s, "
+                u"statuses=%(statuses)s) [profile:%(profile)s]"
             )
             % {
                 "entity": entity_s,
@@ -1083,7 +1131,8 @@
     ):
         """Handle backend action
 
-        @param action_data(dict): action dict as sent by launchAction or returned by an UI action
+        @param action_data(dict): action dict as sent by launchAction or returned by an
+            UI action
         @param callback(None, callback): if not None, callback to use on XMLUI answer
         @param ui_show_cb(None, callback): if not None, method to call to show the XMLUI
         @param user_action(bool): if True, the action is a result of a user interaction
@@ -1216,7 +1265,8 @@
             with current vCard based implementation, it's better to keep True
             except if we request avatars for roster items
         @param hash_only(bool): if True avatar hash is returned, else full path
-        @param ignore_cache(bool): if False, won't check local cache and will request backend in every case
+        @param ignore_cache(bool): if False, won't check local cache and will request
+            backend in every case
         @return (unicode, None): avatar full path (None if no avatar found)
         """
         contact_list = self.contact_lists[profile]
@@ -1238,8 +1288,8 @@
                 ),
                 errback=lambda failure: self._avatarGetEb(failure, entity, contact_list),
             )
-            # we set avatar to empty string to avoid requesting several time the same avatar
-            # while we are waiting for avatarGet result
+            # we set avatar to empty string to avoid requesting several time the same
+            # avatar while we are waiting for avatarGet result
             contact_list.setCache(entity, "avatar", "")
         return avatar
 
--- a/sat_frontends/quick_frontend/quick_chat.py	Sat Aug 11 18:24:55 2018 +0200
+++ b/sat_frontends/quick_frontend/quick_chat.py	Sat Aug 11 18:24:55 2018 +0200
@@ -19,14 +19,14 @@
 
 from sat.core.i18n import _
 from sat.core.log import getLogger
-
-log = getLogger(__name__)
+from sat.tools.common import data_format
 from sat.core import exceptions
 from sat_frontends.quick_frontend import quick_widgets
 from sat_frontends.quick_frontend.constants import Const as C
 from collections import OrderedDict
 from sat_frontends.tools import jid
 import time
+log = getLogger(__name__)
 
 try:
     from locale import getlocale
@@ -56,19 +56,8 @@
 class Message(object):
     """Message metadata"""
 
-    def __init__(
-        self,
-        parent,
-        uid,
-        timestamp,
-        from_jid,
-        to_jid,
-        msg,
-        subject,
-        type_,
-        extra,
-        profile,
-    ):
+    def __init__(self, parent, uid, timestamp, from_jid, to_jid, msg, subject, type_,
+                 extra, profile):
         self.parent = parent
         self.profile = profile
         self.uid = uid
@@ -269,18 +258,11 @@
 class QuickChat(quick_widgets.QuickWidget):
     visible_states = ["chat_state"]  # FIXME: to be removed, used only in quick_games
 
-    def __init__(
-        self,
-        host,
-        target,
-        type_=C.CHAT_ONE2ONE,
-        nick=None,
-        occupants=None,
-        subject=None,
-        profiles=None,
-    ):
+    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None,
+                 subject=None, profiles=None):
         """
-        @param type_: can be C.CHAT_ONE2ONE for single conversation or C.CHAT_GROUP for chat à la IRC
+        @param type_: can be C.CHAT_ONE2ONE for single conversation or C.CHAT_GROUP for
+                      chat à la IRC
         """
         self.lang = ""  # default language to use for messages
         quick_widgets.QuickWidget.__init__(self, host, target, profiles=profiles)
@@ -290,6 +272,7 @@
         assert type_ in (C.CHAT_ONE2ONE, C.CHAT_GROUP)
         self.current_target = target
         self.type = type_
+        self.encrypted = False  # True if this session is currently encrypted
         if type_ == C.CHAT_GROUP:
             if target.resource:
                 raise exceptions.InternalError(
@@ -332,6 +315,8 @@
         self.historyPrint(profile=self.profile)
         if self.subject is not None:
             self.setSubject(self.subject)
+        if self.host.ENCRYPTION_HANDLERS:
+            self.getEncryptionState()
 
     def onDelete(self):
         if self.host.AVATARS_HANDLER:
@@ -565,9 +550,27 @@
             errback=_historyGetEb,
         )
 
-    def messageNew(
-        self, uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile
-    ):
+    def messageEncryptionGetCb(self, session_data):
+        if session_data:
+            session_data = data_format.deserialise(session_data)
+            self.messageEncryptionStarted(session_data)
+
+    def messageEncryptionGetEb(self, failure_):
+        log.error(_(u"Can't get encryption state: {reason}").format(reason=failure_))
+
+    def getEncryptionState(self):
+        """Retrieve encryption state with current target.
+
+        Once state is retrieved, default messageEncryptionStarted will be called if
+        suitable
+        """
+        self.host.bridge.messageEncryptionGet(self.target, self.profile,
+                                              callback=self.messageEncryptionGetCb,
+                                              errback=self.messageEncryptionGetEb)
+
+
+    def messageNew(self, uid, timestamp, from_jid, to_jid, msg, subject, type_, extra,
+                   profile):
         if self._locked:
             self._cache[uid] = (
                 uid,
@@ -583,7 +586,8 @@
             return
         if self.type == C.CHAT_GROUP:
             if to_jid.resource and type_ != C.MESS_TYPE_GROUPCHAT:
-                # we have a private message, we forward it to a private conversation widget
+                # we have a private message, we forward it to a private conversation
+                # widget
                 chat_widget = self.getOrCreatePrivateWidget(to_jid)
                 chat_widget.messageNew(
                     uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile
@@ -612,6 +616,16 @@
             log.warning(u"Delayed message received after history, this should not happen")
         self.createMessage(message)
 
+    def messageEncryptionStarted(self, session_data):
+        self.encrypted = True
+        log.debug(_(u"message encryption started with {target} using {encryption}").format(
+            target=self.target, encryption=session_data[u'name']))
+
+    def messageEncryptionStopped(self, session_data):
+        self.encrypted = False
+        log.debug(_(u"message encryption stopped with {target} (was using {encryption})")
+                 .format(target=self.target, encryption=session_data[u'name']))
+
     def createMessage(self, message, append=False):
         """Must be implemented by frontend to create and show a new message widget