# HG changeset patch # User Goffi # Date 1534004695 -7200 # Node ID e35a265ec174ff730228aa7a8ed8458a32b8c256 # Parent 32b5f68a23b43f7f35bb2f0e1b9285fb9328e0c3 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. diff -r 32b5f68a23b4 -r e35a265ec174 sat_frontends/quick_frontend/quick_app.py --- 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 diff -r 32b5f68a23b4 -r e35a265ec174 sat_frontends/quick_frontend/quick_chat.py --- 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