comparison 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
comparison
equal deleted inserted replaced
2663:32b5f68a23b4 2664:e35a265ec174
140 self._plug_profile_getFeaturesCb({}) 140 self._plug_profile_getFeaturesCb({})
141 141
142 def _plug_profile_getFeaturesCb(self, features): 142 def _plug_profile_getFeaturesCb(self, features):
143 self.host.features = features 143 self.host.features = features
144 # FIXME: we don't use cached value at the moment, but keep the code for later use 144 # FIXME: we don't use cached value at the moment, but keep the code for later use
145 # it was previously used for avatars, but as we don't get full path here, it's better to request later 145 # it was previously used for avatars, but as we don't get full path here,
146 # self.host.bridge.getEntitiesData([], ProfileManager.cache_keys_to_get, profile=self.profile, callback=self._plug_profile_gotCachedValues, errback=self._plug_profile_failedCachedValues) 146 # it's better to request later
147 # self.host.bridge.getEntitiesData([], ProfileManager.cache_keys_to_get,
148 # profile=self.profile,
149 # callback=self._plug_profile_gotCachedValues,
150 # errback=self._plug_profile_failedCachedValues)
147 self._plug_profile_gotCachedValues({}) 151 self._plug_profile_gotCachedValues({})
148 152
149 def _plug_profile_failedCachedValues(self, failure): 153 def _plug_profile_failedCachedValues(self, failure):
150 log.error(u"Couldn't get cached values: {}".format(failure)) 154 log.error(u"Couldn't get cached values: {}".format(failure))
151 self._plug_profile_gotCachedValues({}) 155 self._plug_profile_gotCachedValues({})
271 class QuickApp(object): 275 class QuickApp(object):
272 """This class contain the main methods needed for the frontend""" 276 """This class contain the main methods needed for the frontend"""
273 277
274 MB_HANDLER = True # Set to False if the frontend doesn't manage microblog 278 MB_HANDLER = True # Set to False if the frontend doesn't manage microblog
275 AVATARS_HANDLER = True # set to False if avatars are not used 279 AVATARS_HANDLER = True # set to False if avatars are not used
280 ENCRYPTION_HANDLERS = True # set to False if encryption is handled separatly
276 281
277 def __init__(self, bridge_factory, xmlui, check_options=None, connect_bridge=True): 282 def __init__(self, bridge_factory, xmlui, check_options=None, connect_bridge=True):
278 """Create a frontend application 283 """Create a frontend application
279 284
280 @param bridge_factory: method to use to create the Bridge 285 @param bridge_factory: method to use to create the Bridge
281 @param xmlui: xmlui module 286 @param xmlui: xmlui module
282 @param check_options: method to call to check options (usually command line arguments) 287 @param check_options: method to call to check options (usually command line
288 arguments)
283 """ 289 """
284 self.xmlui = xmlui 290 self.xmlui = xmlui
285 self.menus = quick_menus.QuickMenusManager(self) 291 self.menus = quick_menus.QuickMenusManager(self)
286 ProfileManager.host = self 292 ProfileManager.host = self
287 self.profiles = ProfilesManager() 293 self.profiles = ProfilesManager()
288 self._plugs_in_progress = ( 294 self._plugs_in_progress = (
289 set() 295 set()
290 ) # profiles currently being plugged, used to (un)lock contact list updates 296 ) # profiles currently being plugged, used to (un)lock contact list updates
291 self.ready_profiles = set() # profiles which are connected and ready 297 self.ready_profiles = set() # profiles which are connected and ready
292 self.signals_cache = {} # used to keep signal received between start of plug_profile and when the profile is actualy ready 298 self.signals_cache = {} # used to keep signal received between start of
299 # plug_profile and when the profile is actualy ready
293 self.contact_lists = quick_contact_list.QuickContactListHandler(self) 300 self.contact_lists = quick_contact_list.QuickContactListHandler(self)
294 self.widgets = quick_widgets.QuickWidgetsManager(self) 301 self.widgets = quick_widgets.QuickWidgetsManager(self)
295 if check_options is not None: 302 if check_options is not None:
296 self.options = check_options() 303 self.options = check_options()
297 else: 304 else:
301 self.selected_widget = ( 308 self.selected_widget = (
302 None 309 None
303 ) # widget currently selected (must be filled by frontend) 310 ) # widget currently selected (must be filled by frontend)
304 311
305 # listeners 312 # listeners
306 self._listeners = {} # key: listener type ("avatar", "selected", etc), value: list of callbacks 313 self._listeners = {} # key: listener type ("avatar", "selected", etc),
314 # value: list of callbacks
307 315
308 # triggers 316 # triggers
309 self.trigger = ( 317 self.trigger = (
310 trigger.TriggerManager() 318 trigger.TriggerManager()
311 ) # trigger are used to change the default behaviour 319 ) # trigger are used to change the default behaviour
318 326
319 self._notif_id = 0 327 self._notif_id = 0
320 self._notifications = OrderedDict() 328 self._notifications = OrderedDict()
321 self.features = None 329 self.features = None
322 self.ns_map = {} # map of short name to namespaces 330 self.ns_map = {} # map of short name to namespaces
331 self.encryption_plugins = []
323 332
324 def connectBridge(self): 333 def connectBridge(self):
325 self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb) 334 self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb)
326 335
327 def _namespacesGetCb(self, ns_map): 336 def _namespacesGetCb(self, ns_map):
328 self.ns_map = ns_map 337 self.ns_map = ns_map
329 338
330 def _namespacesGetEb(self, failure_): 339 def _namespacesGetEb(self, failure_):
331 log.error(_(u"Can't get namespaces map: {msg}").format(msg=failure_)) 340 log.error(_(u"Can't get namespaces map: {msg}").format(msg=failure_))
332 341
342 def _encryptionPluginsGetCb(self, plugins):
343 self.encryption_plugins = plugins
344
345 def _encryptionPluginsGetEb(self, failure_):
346 log.warning(_(u"Can't retrieve encryption plugins: {msg}").format(msg=failure_))
347
333 def onBridgeConnected(self): 348 def onBridgeConnected(self):
334 self.bridge.namespacesGet( 349 self.bridge.namespacesGet(
335 callback=self._namespacesGetCb, errback=self._namespacesGetEb 350 callback=self._namespacesGetCb, errback=self._namespacesGetEb)
336 ) 351 # we cache available encryption plugins, as we'll use them on earch
352 # new chat widget
353 self.bridge.encryptionPluginsGet(
354 callback=self._encryptionPluginsGetCb,
355 errback=self._encryptionPluginsGetEb)
337 356
338 def _bridgeCb(self): 357 def _bridgeCb(self):
339 self.registerSignal("connected") 358 self.registerSignal("connected")
340 self.registerSignal("disconnected") 359 self.registerSignal("disconnected")
341 self.registerSignal("actionNew") 360 self.registerSignal("actionNew")
342 self.registerSignal("newContact") 361 self.registerSignal("newContact")
343 self.registerSignal("messageNew") 362 self.registerSignal("messageNew")
363 if self.ENCRYPTION_HANDLERS:
364 self.registerSignal("messageEncryptionStarted")
365 self.registerSignal("messageEncryptionStopped")
344 self.registerSignal("presenceUpdate") 366 self.registerSignal("presenceUpdate")
345 self.registerSignal("subscribe") 367 self.registerSignal("subscribe")
346 self.registerSignal("paramUpdate") 368 self.registerSignal("paramUpdate")
347 self.registerSignal("contactDeleted") 369 self.registerSignal("contactDeleted")
348 self.registerSignal("entityDataUpdated") 370 self.registerSignal("entityDataUpdated")
393 self, function_name, handler=None, iface="core", with_profile=True 415 self, function_name, handler=None, iface="core", with_profile=True
394 ): 416 ):
395 """Register a handler for a signal 417 """Register a handler for a signal
396 418
397 @param function_name (str): name of the signal to handle 419 @param function_name (str): name of the signal to handle
398 @param handler (instancemethod): method to call when the signal arrive, None for calling an automatically named handler (function_name + 'Handler') 420 @param handler (instancemethod): method to call when the signal arrive,
421 None for calling an automatically named handler (function_name + 'Handler')
399 @param iface (str): interface of the bridge to use ('core' or 'plugin') 422 @param iface (str): interface of the bridge to use ('core' or 'plugin')
400 @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 423 @param with_profile (boolean): True if the signal concerns a specific profile,
424 in that case the profile name has to be passed by the caller
401 """ 425 """
402 log.debug(u"registering signal {name}".format(name=function_name)) 426 log.debug(u"registering signal {name}".format(name=function_name))
403 if handler is None: 427 if handler is None:
404 handler = getattr(self, "{}{}".format(function_name, "Handler")) 428 handler = getattr(self, "{}{}".format(function_name, "Handler"))
405 if not with_profile: 429 if not with_profile:
413 raise exceptions.ProfileNotSetError 437 raise exceptions.ProfileNotSetError
414 profile = args[-1] 438 profile = args[-1]
415 if profile is not None: 439 if profile is not None:
416 if not self.check_profile(profile): 440 if not self.check_profile(profile):
417 if profile in self.profiles: 441 if profile in self.profiles:
418 # profile is not ready but is in self.profiles, that's mean that it's being connecting and we need to cache the signal 442 # profile is not ready but is in self.profiles, that's mean that
443 # it's being connecting and we need to cache the signal
419 self.signals_cache.setdefault(profile, []).append( 444 self.signals_cache.setdefault(profile, []).append(
420 (function_name, handler, args, kwargs) 445 (function_name, handler, args, kwargs)
421 ) 446 )
422 return # we ignore signal for profiles we don't manage 447 return # we ignore signal for profiles we don't manage
423 handler(*args, **kwargs) 448 handler(*args, **kwargs)
425 self.bridge.register_signal(function_name, signalReceived, iface) 450 self.bridge.register_signal(function_name, signalReceived, iface)
426 451
427 def addListener(self, type_, callback, profiles_filter=None): 452 def addListener(self, type_, callback, profiles_filter=None):
428 """Add a listener for an event 453 """Add a listener for an event
429 454
430 /!\ don't forget to remove listener when not used anymore (e.g. if you delete a widget) 455 /!\ don't forget to remove listener when not used anymore (e.g. if you delete a
456 widget)
431 @param type_: type of event, can be: 457 @param type_: type of event, can be:
432 - avatar: called when avatar data is updated 458 - avatar: called when avatar data is updated
433 args: (entity, avatar file, profile) 459 args: (entity, avatar file, profile)
434 - nick: called when nick data is updated 460 - nick: called when nick data is updated
435 args: (entity, new_nick, profile) 461 args: (entity, new_nick, profile)
442 - menu: called when a menu item is added or removed 468 - menu: called when a menu item is added or removed
443 args: (type_, path, path_i18n, item) were values are: 469 args: (type_, path, path_i18n, item) were values are:
444 type_: same as in [sat.core.sat_main.SAT.importMenu] 470 type_: same as in [sat.core.sat_main.SAT.importMenu]
445 path: same as in [sat.core.sat_main.SAT.importMenu] 471 path: same as in [sat.core.sat_main.SAT.importMenu]
446 path_i18n: translated path (or None if the item is removed) 472 path_i18n: translated path (or None if the item is removed)
447 item: instance of quick_menus.MenuItemBase or None if the item is removed 473 item: instance of quick_menus.MenuItemBase or None if the item is
474 removed
448 - gotMenus: called only once when menu are available (no arg) 475 - gotMenus: called only once when menu are available (no arg)
449 - progressFinished: called when a progressing action has just finished 476 - progressFinished: called when a progressing action has just finished
450 args: (progress_id, metadata, profile) 477 args: (progress_id, metadata, profile)
451 - progressError: called when a progressing action failed 478 - progressError: called when a progressing action failed
452 args: (progress_id, error_msg, profile): 479 args: (progress_id, error_msg, profile):
489 def check_profile(self, profile): 516 def check_profile(self, profile):
490 """Tell if the profile is currently followed by the application, and ready""" 517 """Tell if the profile is currently followed by the application, and ready"""
491 return profile in self.ready_profiles 518 return profile in self.ready_profiles
492 519
493 def postInit(self, profile_manager): 520 def postInit(self, profile_manager):
494 """Must be called after initialization is done, do all automatic task (auto plug profile) 521 """Must be called after initialization is done, do all automatic task
495 522
496 @param profile_manager: instance of a subclass of Quick_frontend.QuickProfileManager 523 (auto plug profile)
524 @param profile_manager: instance of a subclass of
525 Quick_frontend.QuickProfileManager
497 """ 526 """
498 if self.options and self.options.profile: 527 if self.options and self.options.profile:
499 profile_manager.autoconnect([self.options.profile]) 528 profile_manager.autoconnect([self.options.profile])
500 529
501 def profilePlugged(self, profile): 530 def profilePlugged(self, profile):
502 """Method called when the profile is fully plugged, to launch frontend specific workflow 531 """Method called when the profile is fully plugged
503 532
504 /!\ if you override the method and don't call the parent, be sure to add the profile to ready_profiles ! 533 This will launch frontend specific workflow
505 if you don't, all signals will stay in cache 534
535 /!\ if you override the method and don't call the parent, be sure to add the
536 profile to ready_profiles ! if you don't, all signals will stay in cache
506 537
507 @param profile(unicode): %(doc_profile)s 538 @param profile(unicode): %(doc_profile)s
508 """ 539 """
509 self._plugs_in_progress.remove(profile) 540 self._plugs_in_progress.remove(profile)
510 self.ready_profiles.add(profile) 541 self.ready_profiles.add(profile)
606 def newContactHandler(self, jid_s, attributes, groups, profile): 637 def newContactHandler(self, jid_s, attributes, groups, profile):
607 entity = jid.JID(jid_s) 638 entity = jid.JID(jid_s)
608 groups = list(groups) 639 groups = list(groups)
609 self.contact_lists[profile].setContact(entity, groups, attributes, in_roster=True) 640 self.contact_lists[profile].setContact(entity, groups, attributes, in_roster=True)
610 641
611 def messageNewHandler( 642 def messageNewHandler(self, uid, timestamp, from_jid_s, to_jid_s, msg, subject, type_,
612 self, uid, timestamp, from_jid_s, to_jid_s, msg, subject, type_, extra, profile 643 extra, profile):
613 ):
614 from_jid = jid.JID(from_jid_s) 644 from_jid = jid.JID(from_jid_s)
615 to_jid = jid.JID(to_jid_s) 645 to_jid = jid.JID(to_jid_s)
616 if not self.trigger.point( 646 if not self.trigger.point(
617 "messageNewTrigger", 647 "messageNewTrigger",
618 uid, 648 uid,
629 659
630 from_me = from_jid.bare == self.profiles[profile].whoami.bare 660 from_me = from_jid.bare == self.profiles[profile].whoami.bare
631 target = to_jid if from_me else from_jid 661 target = to_jid if from_me else from_jid
632 contact_list = self.contact_lists[profile] 662 contact_list = self.contact_lists[profile]
633 if target.resource and not contact_list.isRoom(target.bare): 663 if target.resource and not contact_list.isRoom(target.bare):
634 # we avoid resource locking, but we must keep resource for private MUC messages 664 # we avoid resource locking, but we must keep resource for private MUC
665 # messages
635 target = target.bare 666 target = target.bare
636 # we want to be sure to have at least one QuickChat instance 667 # we want to be sure to have at least one QuickChat instance
637 self.widgets.getOrCreateWidget( 668 self.widgets.getOrCreateWidget(
638 quick_chat.QuickChat, 669 quick_chat.QuickChat,
639 target, 670 target,
655 quick_chat.QuickChat, target=target, profiles=(profile,) 686 quick_chat.QuickChat, target=target, profiles=(profile,)
656 ): 687 ):
657 widget.messageNew( 688 widget.messageNew(
658 uid, timestamp, from_jid, target, msg, subject, type_, extra, profile 689 uid, timestamp, from_jid, target, msg, subject, type_, extra, profile
659 ) 690 )
691
692 def messageEncryptionStartedHandler(self, destinee_jid_s, plugin_data, profile):
693 destinee_jid = jid.JID(destinee_jid_s)
694 plugin_data = data_format.deserialise(plugin_data)
695 for widget in self.widgets.getWidgets(quick_chat.QuickChat,
696 target=destinee_jid.bare,
697 profiles=(profile,)):
698 widget.messageEncryptionStarted(plugin_data)
699
700 def messageEncryptionStoppedHandler(self, destinee_jid_s, plugin_data, profile):
701 destinee_jid = jid.JID(destinee_jid_s)
702 for widget in self.widgets.getWidgets(quick_chat.QuickChat,
703 target=destinee_jid.bare,
704 profiles=(profile,)):
705 widget.messageEncryptionStopped(plugin_data)
660 706
661 def messageStateHandler(self, uid, status, profile): 707 def messageStateHandler(self, uid, status, profile):
662 for widget in self.widgets.getWidgets(quick_chat.QuickChat, profiles=(profile,)): 708 for widget in self.widgets.getWidgets(quick_chat.QuickChat, profiles=(profile,)):
663 widget.onMessageState(uid, status, profile) 709 widget.onMessageState(uid, status, profile)
664 710
678 if extra is None: 724 if extra is None:
679 extra = {} 725 extra = {}
680 if callback is None: 726 if callback is None:
681 callback = ( 727 callback = (
682 lambda dummy=None: None 728 lambda dummy=None: None
683 ) # FIXME: optional argument is here because pyjamas doesn't support callback without arg with json proxy 729 ) # FIXME: optional argument is here because pyjamas doesn't support callback
730 # without arg with json proxy
684 if errback is None: 731 if errback is None:
685 errback = lambda failure: self.showDialog( 732 errback = lambda failure: self.showDialog(
686 failure.fullname, failure.message, "error" 733 failure.fullname, failure.message, "error"
687 ) 734 )
688 735
714 raise NotImplementedError 761 raise NotImplementedError
715 762
716 def presenceUpdateHandler(self, entity_s, show, priority, statuses, profile): 763 def presenceUpdateHandler(self, entity_s, show, priority, statuses, profile):
717 log.debug( 764 log.debug(
718 _( 765 _(
719 u"presence update for %(entity)s (show=%(show)s, priority=%(priority)s, statuses=%(statuses)s) [profile:%(profile)s]" 766 u"presence update for %(entity)s (show=%(show)s, priority=%(priority)s, "
767 u"statuses=%(statuses)s) [profile:%(profile)s]"
720 ) 768 )
721 % { 769 % {
722 "entity": entity_s, 770 "entity": entity_s,
723 C.PRESENCE_SHOW: show, 771 C.PRESENCE_SHOW: show,
724 C.PRESENCE_PRIORITY: priority, 772 C.PRESENCE_PRIORITY: priority,
1081 user_action=True, 1129 user_action=True,
1082 profile=C.PROF_KEY_NONE, 1130 profile=C.PROF_KEY_NONE,
1083 ): 1131 ):
1084 """Handle backend action 1132 """Handle backend action
1085 1133
1086 @param action_data(dict): action dict as sent by launchAction or returned by an UI action 1134 @param action_data(dict): action dict as sent by launchAction or returned by an
1135 UI action
1087 @param callback(None, callback): if not None, callback to use on XMLUI answer 1136 @param callback(None, callback): if not None, callback to use on XMLUI answer
1088 @param ui_show_cb(None, callback): if not None, method to call to show the XMLUI 1137 @param ui_show_cb(None, callback): if not None, method to call to show the XMLUI
1089 @param user_action(bool): if True, the action is a result of a user interaction 1138 @param user_action(bool): if True, the action is a result of a user interaction
1090 else the action come from backend direclty (i.e. actionNew) 1139 else the action come from backend direclty (i.e. actionNew)
1091 """ 1140 """
1214 @param entity(jid.JID): entity to get avatar from 1263 @param entity(jid.JID): entity to get avatar from
1215 @param cache_only(bool): if False avatar will be requested if not in cache 1264 @param cache_only(bool): if False avatar will be requested if not in cache
1216 with current vCard based implementation, it's better to keep True 1265 with current vCard based implementation, it's better to keep True
1217 except if we request avatars for roster items 1266 except if we request avatars for roster items
1218 @param hash_only(bool): if True avatar hash is returned, else full path 1267 @param hash_only(bool): if True avatar hash is returned, else full path
1219 @param ignore_cache(bool): if False, won't check local cache and will request backend in every case 1268 @param ignore_cache(bool): if False, won't check local cache and will request
1269 backend in every case
1220 @return (unicode, None): avatar full path (None if no avatar found) 1270 @return (unicode, None): avatar full path (None if no avatar found)
1221 """ 1271 """
1222 contact_list = self.contact_lists[profile] 1272 contact_list = self.contact_lists[profile]
1223 if ignore_cache: 1273 if ignore_cache:
1224 avatar = None 1274 avatar = None
1236 callback=lambda path: self._avatarGetCb( 1286 callback=lambda path: self._avatarGetCb(
1237 path, entity, contact_list, profile 1287 path, entity, contact_list, profile
1238 ), 1288 ),
1239 errback=lambda failure: self._avatarGetEb(failure, entity, contact_list), 1289 errback=lambda failure: self._avatarGetEb(failure, entity, contact_list),
1240 ) 1290 )
1241 # we set avatar to empty string to avoid requesting several time the same avatar 1291 # we set avatar to empty string to avoid requesting several time the same
1242 # while we are waiting for avatarGet result 1292 # avatar while we are waiting for avatarGet result
1243 contact_list.setCache(entity, "avatar", "") 1293 contact_list.setCache(entity, "avatar", "")
1244 return avatar 1294 return avatar
1245 1295
1246 def getDefaultAvatar(self, entity=None): 1296 def getDefaultAvatar(self, entity=None):
1247 """return default avatar to use with given entity 1297 """return default avatar to use with given entity