diff sat_frontends/quick_frontend/quick_app.py @ 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 6ef2b4fa90a5
children bdb8276fd2da
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